diff --git a/modules/encrypt-browser/src/encrypt.ts b/modules/encrypt-browser/src/encrypt.ts index 8baf90f20..4d7d54f2c 100644 --- a/modules/encrypt-browser/src/encrypt.ts +++ b/modules/encrypt-browser/src/encrypt.ts @@ -124,14 +124,24 @@ export async function encrypt ( const frameHeader = isFinalFrame ? serialize.finalFrameHeader(sequenceNumber, frameIv, finalFrameLength) : serialize.frameHeader(sequenceNumber, frameIv) + const contentString = messageAADContentString({ contentType: messageHeader.contentType, isFinalFrame }) const messageAdditionalData = messageAAD( messageId, - messageAADContentString({ contentType: messageHeader.contentType, isFinalFrame }), + contentString, sequenceNumber, - plaintextLength + isFinalFrame ? finalFrameLength : frameLength ) - const cipherBufferAndAuthTag = await getSubtleEncrypt(frameIv, messageAdditionalData)(plaintext) + /* Slicing an ArrayBuffer in a browser is suboptimal. + * It makes a copy.s + * So I just make a new view for the length of the frame. + */ + const framePlaintext = new Uint8Array( + plaintext.buffer, + (sequenceNumber - 1) * frameLength, + isFinalFrame ? finalFrameLength : frameLength + ) + const cipherBufferAndAuthTag = await getSubtleEncrypt(frameIv, messageAdditionalData)(framePlaintext) bodyContent.push(frameHeader, cipherBufferAndAuthTag) } diff --git a/modules/encrypt-browser/test/encrypt.test.ts b/modules/encrypt-browser/test/encrypt.test.ts index 3b2b4f625..25f63f17f 100644 --- a/modules/encrypt-browser/test/encrypt.test.ts +++ b/modules/encrypt-browser/test/encrypt.test.ts @@ -26,7 +26,9 @@ import { importForWebCryptoEncryptionMaterial } from '@aws-crypto/material-management-browser' import { - deserializeFactory + deserializeFactory, + decodeBodyHeader, + deserializeSignature } from '@aws-crypto/serialize' import { encrypt } from '../src/index' import { toUtf8, fromUtf8 } from '@aws-sdk/util-utf8-browser' @@ -88,6 +90,35 @@ describe('encrypt structural testing', () => { it('Precondition: The frameLength must be less than the maximum frame size for browser encryption.', async () => { const frameLength = 0 - expect(encrypt(keyRing, 'asdf', { frameLength })).to.rejectedWith(Error) + expect(encrypt(keyRing, fromUtf8('asdf'), { frameLength })).to.rejectedWith(Error) + }) + + it('can fully parse a framed message', async () => { + const plaintext = fromUtf8('asdf') + const frameLength = 1 + const { cipherMessage } = await encrypt(keyRing, plaintext, { frameLength }) + + const headerInfo = deserializeMessageHeader(cipherMessage) + if (!headerInfo) throw new Error('this should never happen') + + const tagLength = headerInfo.algorithmSuite.tagLength / 8 + let readPos = headerInfo.headerLength + headerInfo.algorithmSuite.ivLength + tagLength + let i = 0 + let bodyHeader: any + // for every frame... + for (; i < 4; i++) { + bodyHeader = decodeBodyHeader(cipherMessage, headerInfo, readPos) + if (!bodyHeader) throw new Error('this should never happen') + readPos = bodyHeader.readPos + bodyHeader.contentLength + tagLength + } + + expect(i).to.equal(4) // 4 frames + expect(bodyHeader.isFinalFrame).to.equal(true) // we got to the end + + // This implicitly tests that I have consumed all the data, + // because otherwise the footer section will be too large + const footerSection = cipherMessage.slice(readPos) + // 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 aa42a4523..82029b01e 100644 --- a/modules/encrypt-node/test/encrypt.test.ts +++ b/modules/encrypt-node/test/encrypt.test.ts @@ -26,6 +26,8 @@ import { } from '@aws-crypto/material-management-node' import { deserializeFactory, + decodeBodyHeader, + deserializeSignature, MessageHeader // eslint-disable-line no-unused-vars } from '@aws-crypto/serialize' import { encrypt, encryptStream } from '../src/index' @@ -189,6 +191,35 @@ describe('encrypt structural testing', () => { const frameLength = 0 expect(encrypt(keyRing, 'asdf', { frameLength })).to.rejectedWith(Error) }) + + it('can fully parse a framed message', async () => { + const plaintext = 'asdf' + const frameLength = 1 + const { ciphertext } = await encrypt(keyRing, plaintext, { frameLength }) + + const headerInfo = deserializeMessageHeader(ciphertext) + if (!headerInfo) throw new Error('this should never happen') + + const tagLength = headerInfo.algorithmSuite.tagLength / 8 + let readPos = headerInfo.headerLength + headerInfo.algorithmSuite.ivLength + tagLength + let i = 0 + let bodyHeader: any + // for every frame... + for (; i < 5; i++) { + bodyHeader = decodeBodyHeader(ciphertext, headerInfo, readPos) + if (!bodyHeader) throw new Error('this should never happen') + readPos = bodyHeader.readPos + bodyHeader.contentLength + tagLength + } + + expect(i).to.equal(5) // 4 frames + expect(bodyHeader.isFinalFrame).to.equal(true) // we got to the end + + // This implicitly tests that I have consumed all the data, + // because otherwise the footer section will be too large + const footerSection = ciphertext.slice(readPos) + // This will throw if it does not deserialize correctly + deserializeSignature(footerSection) + }) }) function finishedAsync (stream: any) {