Skip to content

Commit 32f7fa2

Browse files
authored
fix: browser-encrypt can encrypt 0 bytes (#866)
Browser encryption now correctly encrypts 0 byte messages. Added structural tests for both Node.js and browser `encrypt`.
1 parent d38fe3d commit 32f7fa2

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-3
lines changed

modules/encrypt-browser/src/encrypt.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,14 @@ export async function _encrypt(
138138
await getSubtleEncrypt(headerIv, header)(new Uint8Array(0))
139139
)
140140

141-
const numberOfFrames = Math.ceil(plaintextLength / frameLength)
141+
// In the case of plaintextLength == 0 there still needs to be 1 frame.
142+
const numberOfFrames = Math.max(Math.ceil(plaintextLength / frameLength), 1)
142143
/* The final frame has a variable length.
143144
* The value needs to be known, but should only be calculated once.
144145
* So I calculate how much of a frame I should have at the end.
145146
* This value will NEVER be larger than the frameLength.
146147
*/
147-
const finalFrameLength =
148-
frameLength - (numberOfFrames * frameLength - plaintextLength)
148+
const finalFrameLength = plaintextLength - (numberOfFrames - 1) * frameLength
149149
const bodyContent = []
150150

151151
for (

modules/encrypt-browser/test/encrypt.test.ts

+41
Original file line numberDiff line numberDiff line change
@@ -252,4 +252,45 @@ describe('encrypt structural testing', () => {
252252
// This will throw if it does not deserialize correctly
253253
deserializeSignature(footerSection)
254254
})
255+
256+
it('can encrypt empty message', async () => {
257+
const encryptionContext = { simple: 'context' }
258+
259+
const plaintext = new Uint8Array()
260+
const { result, messageHeader } = await encrypt(keyRing, plaintext, {
261+
encryptionContext,
262+
})
263+
264+
/* The default algorithm suite will add a signature key to the context.
265+
* So I only check that the passed context elements exist.
266+
*/
267+
expect(messageHeader.encryptionContext)
268+
.to.haveOwnProperty('simple')
269+
.and.to.equal('context')
270+
expect(messageHeader.encryptedDataKeys).lengthOf(1)
271+
expect(messageHeader.encryptedDataKeys[0]).to.deep.equal(edk)
272+
273+
const headerInfo = deserializeMessageHeader(result)
274+
if (!headerInfo) throw new Error('I should never see this error')
275+
276+
expect(messageHeader).to.deep.equal(headerInfo.messageHeader)
277+
278+
const readPos =
279+
headerInfo.headerLength + headerInfo.headerAuth.headerAuthLength
280+
const bodyHeader = decodeBodyHeader(result, headerInfo, readPos)
281+
282+
if (!bodyHeader) throw new Error('I should never see this error')
283+
284+
expect(bodyHeader.isFinalFrame).to.equal(true)
285+
expect(bodyHeader.contentLength).to.equal(0)
286+
287+
const sigPos =
288+
bodyHeader.readPos + bodyHeader.contentLength + bodyHeader.tagLength / 8
289+
290+
// This implicitly tests that I have consumed all the data,
291+
// because otherwise the footer section will be too large
292+
const footerSection = result.slice(sigPos)
293+
// This will throw if it does not deserialize correctly
294+
deserializeSignature(footerSection)
295+
})
255296
})

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

+40
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,46 @@ describe('encrypt structural testing', () => {
370370
// This will throw if it does not deserialize correctly
371371
deserializeSignature(footerSection)
372372
})
373+
374+
it('can encrypt empty message', async () => {
375+
const plaintext = new Uint8Array()
376+
const encryptionContext = { simple: 'context' }
377+
const { result, messageHeader } = await encrypt(keyRing, plaintext, {
378+
encryptionContext,
379+
})
380+
381+
/* The default algorithm suite will add a signature key to the context.
382+
* So I only check that the passed context elements exist.
383+
*/
384+
expect(messageHeader.encryptionContext)
385+
.to.haveOwnProperty('simple')
386+
.and.to.equal('context')
387+
expect(messageHeader.encryptedDataKeys).lengthOf(1)
388+
expect(messageHeader.encryptedDataKeys[0]).to.deep.equal(edk)
389+
390+
const headerInfo = deserializeMessageHeader(result)
391+
if (!headerInfo) throw new Error('this should never happen')
392+
393+
expect(messageHeader).to.deep.equal(headerInfo.messageHeader)
394+
395+
const readPos =
396+
headerInfo.headerLength + headerInfo.headerAuth.headerAuthLength
397+
const bodyHeader = decodeBodyHeader(result, headerInfo, readPos)
398+
399+
if (!bodyHeader) throw new Error('I should never see this error')
400+
401+
expect(bodyHeader.isFinalFrame).to.equal(true)
402+
expect(bodyHeader.contentLength).to.equal(0)
403+
404+
const sigPos =
405+
bodyHeader.readPos + bodyHeader.contentLength + bodyHeader.tagLength / 8
406+
407+
// This implicitly tests that I have consumed all the data,
408+
// because otherwise the footer section will be too large
409+
const footerSection = result.slice(sigPos)
410+
// This will throw if it does not deserialize correctly
411+
deserializeSignature(footerSection)
412+
})
373413
})
374414

375415
async function finishedAsync(stream: any) {

0 commit comments

Comments
 (0)