diff --git a/modules/encrypt-browser/src/encrypt.ts b/modules/encrypt-browser/src/encrypt.ts index 85676b7c5..ccaeae1f3 100644 --- a/modules/encrypt-browser/src/encrypt.ts +++ b/modules/encrypt-browser/src/encrypt.ts @@ -138,14 +138,14 @@ export async function _encrypt( await getSubtleEncrypt(headerIv, header)(new Uint8Array(0)) ) - const numberOfFrames = Math.ceil(plaintextLength / frameLength) + // In the case of plaintextLength == 0 there still needs to be 1 frame. + const numberOfFrames = Math.max(Math.ceil(plaintextLength / frameLength), 1) /* The final frame has a variable length. * The value needs to be known, but should only be calculated once. * So I calculate how much of a frame I should have at the end. * This value will NEVER be larger than the frameLength. */ - const finalFrameLength = - frameLength - (numberOfFrames * frameLength - plaintextLength) + const finalFrameLength = plaintextLength - (numberOfFrames - 1) * frameLength const bodyContent = [] for ( diff --git a/modules/encrypt-browser/test/encrypt.test.ts b/modules/encrypt-browser/test/encrypt.test.ts index dd11cebb2..506dfca73 100644 --- a/modules/encrypt-browser/test/encrypt.test.ts +++ b/modules/encrypt-browser/test/encrypt.test.ts @@ -252,4 +252,45 @@ describe('encrypt structural testing', () => { // This will throw if it does not deserialize correctly deserializeSignature(footerSection) }) + + it('can encrypt empty message', async () => { + const encryptionContext = { simple: 'context' } + + const plaintext = new Uint8Array() + const { result, messageHeader } = await encrypt(keyRing, plaintext, { + encryptionContext, + }) + + /* The default algorithm suite will add a signature key to the context. + * So I only check that the passed context elements exist. + */ + expect(messageHeader.encryptionContext) + .to.haveOwnProperty('simple') + .and.to.equal('context') + expect(messageHeader.encryptedDataKeys).lengthOf(1) + expect(messageHeader.encryptedDataKeys[0]).to.deep.equal(edk) + + const headerInfo = deserializeMessageHeader(result) + if (!headerInfo) throw new Error('I should never see this error') + + expect(messageHeader).to.deep.equal(headerInfo.messageHeader) + + const readPos = + headerInfo.headerLength + headerInfo.headerAuth.headerAuthLength + const bodyHeader = decodeBodyHeader(result, headerInfo, readPos) + + if (!bodyHeader) throw new Error('I should never see this error') + + expect(bodyHeader.isFinalFrame).to.equal(true) + expect(bodyHeader.contentLength).to.equal(0) + + const sigPos = + bodyHeader.readPos + bodyHeader.contentLength + bodyHeader.tagLength / 8 + + // This implicitly tests that I have consumed all the data, + // because otherwise the footer section will be too large + const footerSection = result.slice(sigPos) + // This will throw if it does not deserialize correctly + deserializeSignature(footerSection) + }) }) diff --git a/modules/encrypt-node/test/encrypt.test.ts b/modules/encrypt-node/test/encrypt.test.ts index c04e8a910..6f38ef27e 100644 --- a/modules/encrypt-node/test/encrypt.test.ts +++ b/modules/encrypt-node/test/encrypt.test.ts @@ -370,6 +370,46 @@ describe('encrypt structural testing', () => { // This will throw if it does not deserialize correctly deserializeSignature(footerSection) }) + + it('can encrypt empty message', async () => { + const plaintext = new Uint8Array() + const encryptionContext = { simple: 'context' } + const { result, messageHeader } = await encrypt(keyRing, plaintext, { + encryptionContext, + }) + + /* The default algorithm suite will add a signature key to the context. + * So I only check that the passed context elements exist. + */ + expect(messageHeader.encryptionContext) + .to.haveOwnProperty('simple') + .and.to.equal('context') + expect(messageHeader.encryptedDataKeys).lengthOf(1) + expect(messageHeader.encryptedDataKeys[0]).to.deep.equal(edk) + + const headerInfo = deserializeMessageHeader(result) + if (!headerInfo) throw new Error('this should never happen') + + expect(messageHeader).to.deep.equal(headerInfo.messageHeader) + + const readPos = + headerInfo.headerLength + headerInfo.headerAuth.headerAuthLength + const bodyHeader = decodeBodyHeader(result, headerInfo, readPos) + + if (!bodyHeader) throw new Error('I should never see this error') + + expect(bodyHeader.isFinalFrame).to.equal(true) + expect(bodyHeader.contentLength).to.equal(0) + + const sigPos = + bodyHeader.readPos + bodyHeader.contentLength + bodyHeader.tagLength / 8 + + // This implicitly tests that I have consumed all the data, + // because otherwise the footer section will be too large + const footerSection = result.slice(sigPos) + // This will throw if it does not deserialize correctly + deserializeSignature(footerSection) + }) }) async function finishedAsync(stream: any) {