Skip to content

Commit 3dd6f43

Browse files
authored
fix: The final frame can not be larger than the Frame Length (#281)
The final frame content length is stored in the body header. This means that while it should be related to the frame length in the header, it is not required. This enforces this requirement. As well as adding a test and test vector for a zero length final frame.
1 parent bf54a60 commit 3dd6f43

File tree

6 files changed

+155
-12
lines changed

6 files changed

+155
-12
lines changed

modules/encrypt-browser/src/encrypt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export async function encrypt (
113113
/* The final frame has a variable length.
114114
* The value needs to be known, but should only be calculated once.
115115
* So I calculate how much of a frame I should have at the end.
116+
* This value will NEVER be larger than the frameLength.
116117
*/
117118
const finalFrameLength = frameLength - ((numberOfFrames * frameLength) - plaintextLength)
118119
const bodyContent = []

modules/encrypt-node/src/framed_encrypt_stream.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: Mes
164164
/* Push the authTag onto the end. Yes, I am abusing the name. */
165165
cipherContent.push(cipher.getAuthTag())
166166

167-
needs(frameSize === frameLength || isFinalFrame, 'Malformed frame')
167+
needs(frameSize === frameLength || (isFinalFrame && frameLength >= frameSize), 'Malformed frame')
168168

169169
for (const cipherText of cipherContent) {
170170
if (!this.push(cipherText)) {
@@ -190,11 +190,18 @@ type EncryptFrameInput = {
190190
export function getEncryptFrame (input: EncryptFrameInput): EncryptFrame {
191191
const { pendingFrame, messageHeader, getCipher, isFinalFrame } = input
192192
const { sequenceNumber, contentLength, content } = pendingFrame
193-
const frameIv = serialize.frameIv(messageHeader.headerIvLength, sequenceNumber)
193+
const { frameLength, contentType, messageId, headerIvLength } = messageHeader
194+
/* Precondition: The content length MUST correlate with the frameLength.
195+
* In the case of a regular frame,
196+
* the content length MUST strictly equal the frame length.
197+
* In the case of the final frame,
198+
* it MUST NOT be larger than the frame length.
199+
*/
200+
needs(frameLength === contentLength || (isFinalFrame && frameLength >= contentLength), `Malformed frame length and content length: ${JSON.stringify({ frameLength, contentLength, isFinalFrame })}`)
201+
const frameIv = serialize.frameIv(headerIvLength, sequenceNumber)
194202
const bodyHeader = Buffer.from(isFinalFrame
195203
? finalFrameHeader(sequenceNumber, frameIv, contentLength)
196204
: frameHeader(sequenceNumber, frameIv))
197-
const { contentType, messageId } = messageHeader
198205
const contentString = aadUtility.messageAADContentString({ contentType, isFinalFrame })
199206
const { buffer, byteOffset, byteLength } = aadUtility.messageAAD(messageId, contentString, sequenceNumber, contentLength)
200207
const cipher = getCipher(frameIv)

modules/encrypt-node/test/framed_encrypt_stream.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import * as chai from 'chai'
1919
import chaiAsPromised from 'chai-as-promised'
20-
import { getFramedEncryptStream } from '../src/framed_encrypt_stream'
20+
import { getFramedEncryptStream, getEncryptFrame } from '../src/framed_encrypt_stream'
2121

2222
chai.use(chaiAsPromised)
2323
const { expect } = chai
@@ -60,3 +60,90 @@ describe('getFramedEncryptStream', () => {
6060
expect(called).to.equal(true)
6161
})
6262
})
63+
64+
describe('getEncryptFrame', () => {
65+
it('can return an EncryptFrame', () => {
66+
const input = {
67+
pendingFrame: {
68+
content: [Buffer.from([1, 2, 3, 4, 5])],
69+
contentLength: 5,
70+
sequenceNumber: 1
71+
},
72+
isFinalFrame: false,
73+
getCipher: () => ({ setAAD: () => {} }) as any,
74+
messageHeader: {
75+
frameLength: 5,
76+
contentType: 2,
77+
messageId: Buffer.from([]),
78+
headerIvLength: 12 as 12,
79+
version: 1,
80+
type: 12,
81+
suiteId: 1,
82+
encryptionContext: {},
83+
encryptedDataKeys: []
84+
}
85+
}
86+
const test1 = getEncryptFrame(input)
87+
expect(test1.content).to.equal(input.pendingFrame.content)
88+
expect(test1.isFinalFrame).to.equal(input.isFinalFrame)
89+
90+
// Just a quick flip to make sure...
91+
input.isFinalFrame = true
92+
const test2 = getEncryptFrame(input)
93+
expect(test2.content).to.equal(input.pendingFrame.content)
94+
expect(test2.isFinalFrame).to.equal(input.isFinalFrame)
95+
})
96+
97+
it('Precondition: The content length MUST correlate with the frameLength.', () => {
98+
const inputFinalFrameToLarge = {
99+
pendingFrame: {
100+
content: [Buffer.from([1, 2, 3, 4, 5, 6])],
101+
// This exceeds the frameLength below
102+
contentLength: 6,
103+
sequenceNumber: 1
104+
},
105+
isFinalFrame: true,
106+
getCipher: () => ({ setAAD: () => {} }) as any,
107+
messageHeader: {
108+
frameLength: 5,
109+
contentType: 2,
110+
messageId: Buffer.from([]),
111+
headerIvLength: 12 as 12,
112+
version: 1,
113+
type: 12,
114+
suiteId: 1,
115+
encryptionContext: {},
116+
encryptedDataKeys: []
117+
}
118+
}
119+
120+
expect(() => getEncryptFrame(inputFinalFrameToLarge)).to.throw('Malformed frame length and content length:')
121+
122+
const inputFrame = {
123+
pendingFrame: {
124+
content: [Buffer.from([1, 2, 3, 4, 5])],
125+
contentLength: 5,
126+
sequenceNumber: 1
127+
},
128+
isFinalFrame: false,
129+
getCipher: () => ({ setAAD: () => {} }) as any,
130+
messageHeader: {
131+
frameLength: 5,
132+
contentType: 2,
133+
messageId: Buffer.from([]),
134+
headerIvLength: 12 as 12,
135+
version: 1,
136+
type: 12,
137+
suiteId: 1,
138+
encryptionContext: {},
139+
encryptedDataKeys: []
140+
}
141+
}
142+
143+
// Make sure that it must be equal as long as we are here...
144+
inputFrame.pendingFrame.contentLength = 4
145+
expect(() => getEncryptFrame(inputFrame)).to.throw('Malformed frame length and content length:')
146+
inputFrame.pendingFrame.contentLength = 6
147+
expect(() => getEncryptFrame(inputFrame)).to.throw('Malformed frame length and content length:')
148+
})
149+
})

modules/serialize/src/decode_body_header.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export function decodeFinalFrameBodyHeader (buffer: Uint8Array, headerInfo: Head
155155
needs(sequenceNumber > 0, 'Malformed sequenceNumber.')
156156
const iv = buffer.slice(readPos += 4, readPos += ivLength)
157157
const contentLength = dataView.getUint32(readPos)
158+
/* Postcondition: The final frame MUST NOT exceed the frameLength. */
159+
needs(headerInfo.messageHeader.frameLength >= contentLength, 'Final frame length exceeds frame length.')
158160
return {
159161
sequenceNumber,
160162
iv,

modules/serialize/test/decode_body_header.test.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('decodeFrameBodyHeader', () => {
101101
it('return final frame header', () => {
102102
const headerInfo = {
103103
messageHeader: {
104-
frameLength: 99,
104+
frameLength: 999,
105105
contentType: ContentType.FRAMED_DATA
106106
},
107107
algorithmSuite: {
@@ -205,7 +205,7 @@ describe('decodeFrameBodyHeader', () => {
205205
const buffer = concatBuffers(new Uint8Array(10), fixtures.finalFrameHeader())
206206
const headerInfo = {
207207
messageHeader: {
208-
frameLength: 99,
208+
frameLength: 999,
209209
contentType: ContentType.FRAMED_DATA
210210
},
211211
algorithmSuite: {
@@ -296,7 +296,7 @@ describe('decodeFinalFrameBodyHeader', () => {
296296
it('return final frame header from readPos', () => {
297297
const headerInfo = {
298298
messageHeader: {
299-
frameLength: 99,
299+
frameLength: 999,
300300
contentType: ContentType.FRAMED_DATA
301301
},
302302
algorithmSuite: {
@@ -318,12 +318,36 @@ describe('decodeFinalFrameBodyHeader', () => {
318318
expect(test.tagLength).to.eql(16)
319319
expect(test.isFinalFrame).to.eql(true)
320320
expect(test.contentType).to.eql(ContentType.FRAMED_DATA)
321+
expect(test.contentLength).to.eql(999)
322+
})
323+
324+
it('The final frame can be 0 length.', () => {
325+
const headerInfo = {
326+
messageHeader: {
327+
frameLength: 999,
328+
contentType: ContentType.FRAMED_DATA
329+
},
330+
algorithmSuite: {
331+
ivLength: 12,
332+
tagLength: 16
333+
}
334+
} as any
335+
const buffer = fixtures.finalFrameHeaderZeroBytes()
336+
337+
const test = decodeFinalFrameBodyHeader(buffer, headerInfo, 0)
338+
if (!test) throw new Error('failure')
339+
expect(test.sequenceNumber).to.eql(1)
340+
expect(test.iv).to.eql(fixtures.basicFrameIV())
341+
expect(test.tagLength).to.eql(16)
342+
expect(test.isFinalFrame).to.eql(true)
343+
expect(test.contentType).to.eql(ContentType.FRAMED_DATA)
344+
expect(test.contentLength).to.eql(0)
321345
})
322346

323347
it('Precondition: The contentType must be FRAMED_DATA to be a Final Frame.', () => {
324348
const headerInfo = {
325349
messageHeader: {
326-
frameLength: 99,
350+
frameLength: 999,
327351
contentType: 'not FRAMED_DATA'
328352
},
329353
algorithmSuite: {
@@ -338,7 +362,7 @@ describe('decodeFinalFrameBodyHeader', () => {
338362
it('Precondition: decodeFinalFrameBodyHeader readPos must be within the byte length of the buffer given.', () => {
339363
const headerInfo = {
340364
messageHeader: {
341-
frameLength: 99,
365+
frameLength: 999,
342366
contentType: ContentType.FRAMED_DATA
343367
},
344368
algorithmSuite: {
@@ -355,7 +379,7 @@ describe('decodeFinalFrameBodyHeader', () => {
355379
it('Postcondition: sequenceEnd must be SEQUENCE_NUMBER_END.', () => {
356380
const headerInfo = {
357381
messageHeader: {
358-
frameLength: 99,
382+
frameLength: 999,
359383
contentType: ContentType.FRAMED_DATA
360384
},
361385
algorithmSuite: {
@@ -371,7 +395,7 @@ describe('decodeFinalFrameBodyHeader', () => {
371395
it('Postcondition: decodeFinalFrameBodyHeader sequenceNumber must be greater than 0.', () => {
372396
const headerInfo = {
373397
messageHeader: {
374-
frameLength: 99,
398+
frameLength: 999,
375399
contentType: ContentType.FRAMED_DATA
376400
},
377401
algorithmSuite: {
@@ -388,7 +412,7 @@ describe('decodeFinalFrameBodyHeader', () => {
388412
const frameHeader = fixtures.finalFrameHeader()
389413
const headerInfo = {
390414
messageHeader: {
391-
frameLength: 99,
415+
frameLength: 999,
392416
contentType: ContentType.FRAMED_DATA
393417
},
394418
algorithmSuite: {
@@ -402,6 +426,24 @@ describe('decodeFinalFrameBodyHeader', () => {
402426
expect(test).to.eql(false)
403427
}
404428
})
429+
430+
it('Postcondition: The final frame MUST NOT exceed the frameLength.', () => {
431+
const headerInfo = {
432+
messageHeader: {
433+
// The content length in this final frame is 999
434+
// So I set the frame length to less than this
435+
frameLength: 99,
436+
contentType: ContentType.FRAMED_DATA
437+
},
438+
algorithmSuite: {
439+
ivLength: 12,
440+
tagLength: 16
441+
}
442+
} as any
443+
const buffer = fixtures.finalFrameHeader()
444+
445+
expect(() => decodeFinalFrameBodyHeader(buffer, headerInfo, 0)).to.throw('Final frame length exceeds frame length.')
446+
})
405447
})
406448

407449
describe('decodeNonFrameBodyHeader', () => {

modules/serialize/test/fixtures.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ export function finalFrameHeader () {
6161
return new Uint8Array([255, 255, 255, 255, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3, 231])
6262
}
6363

64+
export function finalFrameHeaderZeroBytes () {
65+
return new Uint8Array([255, 255, 255, 255, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0])
66+
}
67+
6468
export function invalidSequenceEndFinalFrameHeader () {
6569
return new Uint8Array([0, 255, 255, 255, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 3, 231])
6670
}

0 commit comments

Comments
 (0)