Skip to content

Commit cdcc2f0

Browse files
committed
fix: sequence number order
The sequence number **must** be honored. This is especially important for non-signed algorithm suites. The test vector added to this PR illustrates this. It is the encrypted string `’asdf’` with a frame length of 1. The frames have been rearranged to `’fdsa’`. Without this change the ESDK would decrypt this blob.
1 parent eb4813e commit cdcc2f0

File tree

5 files changed

+62
-1
lines changed

5 files changed

+62
-1
lines changed

modules/decrypt-browser/src/decrypt.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,21 @@ interface FramedDecryptOptions extends BodyDecryptOptions {
100100
async function bodyDecrypt ({ buffer, getSubtleDecrypt, headerInfo }: BodyDecryptOptions) {
101101
let readPos = headerInfo.headerIv.byteLength + headerInfo.rawHeader.byteLength + headerInfo.headerAuthTag.byteLength
102102
const clearBuffers: ArrayBuffer[] = []
103+
let sequenceNumber = 0
103104
while (true) {
105+
/* Keeping track of the sequence number myself. */
106+
sequenceNumber += 1
107+
104108
const { clearBlob, frameInfo } = await framedDecrypt({ buffer, getSubtleDecrypt, headerInfo, readPos })
109+
110+
/* Precondition: The sequenceNumber is required to monotonically increase, starting from 1.
111+
* This is to avoid a bad actor from abusing the sequence number on un-signed algorithm suites.
112+
* If the frame size matched the data format (say NDJSON),
113+
* then the data could be significantly altered just by rearranging the frames.
114+
* Non-framed data returns a sequenceNumber of 1.
115+
*/
116+
needs(frameInfo.sequenceNumber === sequenceNumber, 'Encrypted body sequence out of order.')
117+
105118
clearBuffers.push(clearBlob)
106119
readPos = frameInfo.readPos
107120
if (frameInfo.isFinalFrame) {

modules/decrypt-browser/test/fixtures.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@ export function encryptionContext () {
3838
}
3939
}
4040

41+
export function frameSequenceOutOfOrder () {
42+
/* This is the string 'asdf' encrypted with ALG_AES128_GCM_IV12_TAG16.
43+
* The data key is all zeros.
44+
* The frames have been reordered in order to test decryption failure.
45+
*/
46+
return [
47+
1, 128, 0, 20, 111, 198, 208, 129, 64, 41, 115, 91, 247, 27, 148, 148, 102, 70, 149, 210, 0, 0, 0, 1, 0, 1, 107, 0, 1, 107, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 12, 0, 0, 0, 1, // header
48+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 178, 85, 20, 93, 96, 34, 206, 116, 216, 115, 183, 217, 192, 84, 155, 15, // header auth
49+
0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 217, 75, 144, 128, 252, 7, 157, 174, 2, 89, 248, 206, 24, 122, 69, 226, 95, // f
50+
0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 114, 251, 4, 157, 182, 94, 8, 230, 234, 185, 222, 120, 209, 235, 186, 207, 112, // d
51+
0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 44, 171, 202, 172, 191, 0, 232, 145, 31, 77, 90, 8, 233, 45, 106, 60, 176, // s
52+
0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 248, 112, 28, 63, 229, 61, 238, 146, 255, 228, 38, 170, 100, 42, 251, 21, 208] // a
53+
}
54+
4155
class TestKeyring extends KeyringWebCrypto {
4256
async _onEncrypt () : Promise<WebCryptoEncryptionMaterial> {
4357
throw new Error('I should never see this error')

modules/decrypt-node/src/verify_stream.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ interface VerifyState {
4949
authTagBuffer: Buffer
5050
currentFrame?: BodyHeader
5151
signatureInfo: Buffer
52+
sequenceNumber: number
5253
}
5354

5455
export class VerifyStream extends PortableTransformWithType {
5556
private _headerInfo!: HeaderInfo
5657
private _verifyState: VerifyState = {
5758
buffer: Buffer.alloc(0),
5859
authTagBuffer: Buffer.alloc(0),
59-
signatureInfo: Buffer.alloc(0)
60+
signatureInfo: Buffer.alloc(0),
61+
sequenceNumber: 0
6062
}
6163
private _verify?: AWSVerify
6264
private _maxBodySize?: number
@@ -118,6 +120,17 @@ export class VerifyStream extends PortableTransformWithType {
118120
*/
119121
needs(!this._maxBodySize || this._maxBodySize >= frameHeader.contentLength, 'maxBodySize exceeded.')
120122

123+
/* Keeping track of the sequence number myself. */
124+
state.sequenceNumber += 1
125+
126+
/* Precondition: The sequence number is required to monotonically increase, starting from 1.
127+
* This is to avoid a bad actor from abusing the sequence number on un-signed algorithm suites.
128+
* If the frame size matched the data format (say NDJSON),
129+
* then the data could be significantly altered just by rearranging the frames.
130+
* Non-framed data returns a sequenceNumber of 1.
131+
*/
132+
needs(frameHeader.sequenceNumber === state.sequenceNumber, 'Encrypted body sequence out of order.')
133+
121134
if (this._verify) {
122135
this._verify.update(frameBuffer.slice(0, frameHeader.readPos))
123136
}

modules/decrypt-node/test/decrypt.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,12 @@ describe('decrypt', () => {
6767
expect(messageHeader.encryptionContext).to.deep.equal(fixtures.encryptionContext())
6868
expect(test.toString('base64')).to.equal(fixtures.base64Plaintext())
6969
})
70+
71+
it('Precondition: The sequence number is required to monotonically increase, starting from 1.', async () => {
72+
expect(decrypt(
73+
fixtures.decryptKeyring(),
74+
fixtures.frameSequenceOutOfOrder(),
75+
{ encoding: 'base64' }
76+
)).to.rejectedWith(Error)
77+
})
7078
})

modules/decrypt-node/test/fixtures.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ export function encryptionContext () {
3737
}
3838
}
3939

40+
export function frameSequenceOutOfOrder () {
41+
/* This is the string 'asdf' encrypted with ALG_AES128_GCM_IV12_TAG16.
42+
* The data key is all zeros.
43+
* The frames have been reordered in order to test decryption failure.
44+
*/
45+
return 'AYAAFG/G0IFAKXNb9xuUlGZGldIAAAABAAFrAAFrAAMAAAACAAAAAAwAAAAB' + // header
46+
'AAAAAAAAAAAAAAAAslUUXWAiznTYc7fZwFSbDw' + // header auth
47+
'AAAAQAAAAAAAAAAAAAAAR9lLkID8B52uAln4zhh6ReJf' + // f
48+
'AAAAMAAAAAAAAAAAAAAAPnL7BJ22Xgjm6rneeNHrus9w' + // d
49+
'AAAAIAAAAAAAAAAAAAAAKiyryqy/AOiRH01aCOktajyw' + // s
50+
'AAAAEAAAAAAAAAAAAAAAEPhwHD/lPe6S/+QmqmQq+xXQ' // a
51+
}
52+
4053
export function decryptKeyring () {
4154
class TestKeyring extends KeyringNode {
4255
async _onEncrypt () : Promise<NodeEncryptionMaterial> {

0 commit comments

Comments
 (0)