Skip to content

Commit a2f2ed9

Browse files
authored
fix: browser framed encryption (#156)
resolves #155 Browser encrypt did not break up the plaintext buffer for each frame. It needs to break up the plaintext and identify the correct length in the messageAAD
1 parent 8e33d7d commit a2f2ed9

File tree

3 files changed

+77
-5
lines changed

3 files changed

+77
-5
lines changed

modules/encrypt-browser/src/encrypt.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,24 @@ export async function encrypt (
124124
const frameHeader = isFinalFrame
125125
? serialize.finalFrameHeader(sequenceNumber, frameIv, finalFrameLength)
126126
: serialize.frameHeader(sequenceNumber, frameIv)
127+
const contentString = messageAADContentString({ contentType: messageHeader.contentType, isFinalFrame })
127128
const messageAdditionalData = messageAAD(
128129
messageId,
129-
messageAADContentString({ contentType: messageHeader.contentType, isFinalFrame }),
130+
contentString,
130131
sequenceNumber,
131-
plaintextLength
132+
isFinalFrame ? finalFrameLength : frameLength
132133
)
133134

134-
const cipherBufferAndAuthTag = await getSubtleEncrypt(frameIv, messageAdditionalData)(plaintext)
135+
/* Slicing an ArrayBuffer in a browser is suboptimal.
136+
* It makes a copy.s
137+
* So I just make a new view for the length of the frame.
138+
*/
139+
const framePlaintext = new Uint8Array(
140+
plaintext.buffer,
141+
(sequenceNumber - 1) * frameLength,
142+
isFinalFrame ? finalFrameLength : frameLength
143+
)
144+
const cipherBufferAndAuthTag = await getSubtleEncrypt(frameIv, messageAdditionalData)(framePlaintext)
135145

136146
bodyContent.push(frameHeader, cipherBufferAndAuthTag)
137147
}

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import {
2626
importForWebCryptoEncryptionMaterial
2727
} from '@aws-crypto/material-management-browser'
2828
import {
29-
deserializeFactory
29+
deserializeFactory,
30+
decodeBodyHeader,
31+
deserializeSignature
3032
} from '@aws-crypto/serialize'
3133
import { encrypt } from '../src/index'
3234
import { toUtf8, fromUtf8 } from '@aws-sdk/util-utf8-browser'
@@ -88,6 +90,35 @@ describe('encrypt structural testing', () => {
8890

8991
it('Precondition: The frameLength must be less than the maximum frame size for browser encryption.', async () => {
9092
const frameLength = 0
91-
expect(encrypt(keyRing, 'asdf', { frameLength })).to.rejectedWith(Error)
93+
expect(encrypt(keyRing, fromUtf8('asdf'), { frameLength })).to.rejectedWith(Error)
94+
})
95+
96+
it('can fully parse a framed message', async () => {
97+
const plaintext = fromUtf8('asdf')
98+
const frameLength = 1
99+
const { cipherMessage } = await encrypt(keyRing, plaintext, { frameLength })
100+
101+
const headerInfo = deserializeMessageHeader(cipherMessage)
102+
if (!headerInfo) throw new Error('this should never happen')
103+
104+
const tagLength = headerInfo.algorithmSuite.tagLength / 8
105+
let readPos = headerInfo.headerLength + headerInfo.algorithmSuite.ivLength + tagLength
106+
let i = 0
107+
let bodyHeader: any
108+
// for every frame...
109+
for (; i < 4; i++) {
110+
bodyHeader = decodeBodyHeader(cipherMessage, headerInfo, readPos)
111+
if (!bodyHeader) throw new Error('this should never happen')
112+
readPos = bodyHeader.readPos + bodyHeader.contentLength + tagLength
113+
}
114+
115+
expect(i).to.equal(4) // 4 frames
116+
expect(bodyHeader.isFinalFrame).to.equal(true) // we got to the end
117+
118+
// This implicitly tests that I have consumed all the data,
119+
// because otherwise the footer section will be too large
120+
const footerSection = cipherMessage.slice(readPos)
121+
// This will throw if it does not deserialize correctly
122+
deserializeSignature(footerSection)
92123
})
93124
})

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
} from '@aws-crypto/material-management-node'
2727
import {
2828
deserializeFactory,
29+
decodeBodyHeader,
30+
deserializeSignature,
2931
MessageHeader // eslint-disable-line no-unused-vars
3032
} from '@aws-crypto/serialize'
3133
import { encrypt, encryptStream } from '../src/index'
@@ -189,6 +191,35 @@ describe('encrypt structural testing', () => {
189191
const frameLength = 0
190192
expect(encrypt(keyRing, 'asdf', { frameLength })).to.rejectedWith(Error)
191193
})
194+
195+
it('can fully parse a framed message', async () => {
196+
const plaintext = 'asdf'
197+
const frameLength = 1
198+
const { ciphertext } = await encrypt(keyRing, plaintext, { frameLength })
199+
200+
const headerInfo = deserializeMessageHeader(ciphertext)
201+
if (!headerInfo) throw new Error('this should never happen')
202+
203+
const tagLength = headerInfo.algorithmSuite.tagLength / 8
204+
let readPos = headerInfo.headerLength + headerInfo.algorithmSuite.ivLength + tagLength
205+
let i = 0
206+
let bodyHeader: any
207+
// for every frame...
208+
for (; i < 5; i++) {
209+
bodyHeader = decodeBodyHeader(ciphertext, headerInfo, readPos)
210+
if (!bodyHeader) throw new Error('this should never happen')
211+
readPos = bodyHeader.readPos + bodyHeader.contentLength + tagLength
212+
}
213+
214+
expect(i).to.equal(5) // 4 frames
215+
expect(bodyHeader.isFinalFrame).to.equal(true) // we got to the end
216+
217+
// This implicitly tests that I have consumed all the data,
218+
// because otherwise the footer section will be too large
219+
const footerSection = ciphertext.slice(readPos)
220+
// This will throw if it does not deserialize correctly
221+
deserializeSignature(footerSection)
222+
})
192223
})
193224

194225
function finishedAsync (stream: any) {

0 commit comments

Comments
 (0)