Skip to content

fix: browser framed encryption #156

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions modules/encrypt-browser/src/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
35 changes: 33 additions & 2 deletions modules/encrypt-browser/test/encrypt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
})
})
31 changes: 31 additions & 0 deletions modules/encrypt-node/test/encrypt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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) {
Expand Down