Skip to content

Commit 40492ac

Browse files
authored
Merge branch 'master' into prtemplate
2 parents fc609af + b7dc81e commit 40492ac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1048
-243
lines changed

modules/decrypt-browser/src/decrypt.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,21 @@ interface FramedDecryptOptions extends BodyDecryptOptions {
100100
async function bodyDecrypt ({ buffer, getSubtleDecrypt, headerInfo }: BodyDecryptOptions) {
101101
let readPos = headerInfo.headerIv.byteLength + headerInfo.rawHeader.byteLength + headerInfo.headerAuthTag.byteLength
102102
const clearBuffers: ArrayBuffer[] = []
103+
let sequenceNumber = 0
103104
while (true) {
105+
/* Keeping track of the sequence number myself. */
106+
sequenceNumber += 1
107+
104108
const { clearBlob, frameInfo } = await framedDecrypt({ buffer, getSubtleDecrypt, headerInfo, readPos })
109+
110+
/* Precondition: The sequenceNumber is required to monotonically increase, starting from 1.
111+
* This is to avoid a bad actor from abusing the sequence number on un-signed algorithm suites.
112+
* If the frame size matched the data format (say NDJSON),
113+
* then the data could be significantly altered just by rearranging the frames.
114+
* Non-framed data returns a sequenceNumber of 1.
115+
*/
116+
needs(frameInfo.sequenceNumber === sequenceNumber, 'Encrypted body sequence out of order.')
117+
105118
clearBuffers.push(clearBlob)
106119
readPos = frameInfo.readPos
107120
if (frameInfo.isFinalFrame) {

modules/decrypt-browser/test/decrypt.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,15 @@ describe('decrypt', () => {
3232
expect(messageHeader.encryptionContext).to.deep.equal(fixtures.encryptionContext())
3333
expect(test).to.deep.equal(fixtures.plaintext())
3434
})
35+
36+
it('Precondition: The sequence number is required to monotonically increase, starting from 1.', async () => {
37+
return decrypt(
38+
fixtures.decryptKeyring(),
39+
fixtures.frameSequenceOutOfOrder()
40+
).then(() => {
41+
throw new Error('should not succeed')
42+
}, err => {
43+
expect(err).to.be.instanceOf(Error)
44+
})
45+
})
3546
})

modules/decrypt-browser/test/fixtures.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ export function encryptionContext () {
3838
}
3939
}
4040

41+
export function frameSequenceOutOfOrder () {
42+
/* This is the string 'asdf' encrypted with ALG_AES128_GCM_IV12_TAG16.
43+
* The data key is all zeros.
44+
* The frames have been reordered in order to test decryption failure.
45+
*/
46+
return new Uint8Array([
47+
1, 128, 0, 20, 111, 198, 208, 129, 64, 41, 115, 91, 247, 27, 148, 148, 102, 70, 149, 210, 0, 0, 0, 1, 0, 1, 107, 0, 1, 107, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 12, 0, 0, 0, 1, // header
48+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 178, 85, 20, 93, 96, 34, 206, 116, 216, 115, 183, 217, 192, 84, 155, 15, // header auth
49+
0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 217, 75, 144, 128, 252, 7, 157, 174, 2, 89, 248, 206, 24, 122, 69, 226, 95, // f
50+
0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 114, 251, 4, 157, 182, 94, 8, 230, 234, 185, 222, 120, 209, 235, 186, 207, 112, // d
51+
0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 44, 171, 202, 172, 191, 0, 232, 145, 31, 77, 90, 8, 233, 45, 106, 60, 176, // s
52+
0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 248, 112, 28, 63, 229, 61, 238, 146, 255, 228, 38, 170, 100, 42, 251, 21, 208 // a
53+
])
54+
}
55+
4156
class TestKeyring extends KeyringWebCrypto {
4257
async _onEncrypt () : Promise<WebCryptoEncryptionMaterial> {
4358
throw new Error('I should never see this error')

modules/decrypt-node/src/decipher_stream.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
// @ts-ignore
1717
import { Transform as PortableTransform } from 'readable-stream'
1818
import { Transform } from 'stream' // eslint-disable-line no-unused-vars
19-
import { DecipherGCM } from 'crypto' // eslint-disable-line no-unused-vars
20-
import { needs } from '@aws-crypto/material-management-node'
19+
import {
20+
needs,
21+
GetDecipher, // eslint-disable-line no-unused-vars
22+
AwsEsdkJsDecipherGCM // eslint-disable-line no-unused-vars
23+
} from '@aws-crypto/material-management-node'
2124
import {
2225
aadFactory,
2326
ContentType // eslint-disable-line no-unused-vars
@@ -31,12 +34,12 @@ const PortableTransformWithType = (<new (...args: any[]) => Transform>PortableTr
3134
export interface DecipherInfo {
3235
messageId: Buffer
3336
contentType: ContentType
34-
getDecipher: (iv: Uint8Array) => DecipherGCM
37+
getDecipher: GetDecipher
3538
dispose: () => void
3639
}
3740

3841
interface DecipherState {
39-
decipher: DecipherGCM
42+
decipher: AwsEsdkJsDecipherGCM
4043
content: Buffer[]
4144
contentLength: number
4245
}

modules/decrypt-node/src/decrypt_stream.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Duplexify from 'duplexify'
2525
import { Duplex } from 'stream' // eslint-disable-line no-unused-vars
2626

2727
// @ts-ignore
28-
import { pipeline } from 'readable-stream'
28+
import { pipeline, PassThrough } from 'readable-stream'
2929

3030
export interface DecryptStreamOptions {
3131
maxBodySize?: number
@@ -44,7 +44,13 @@ export function decryptStream (
4444
const verifyStream = new VerifyStream({ maxBodySize })
4545
const decipherStream = getDecipherStream()
4646

47-
pipeline(parseHeaderStream, verifyStream, decipherStream)
47+
/* pipeline will _either_ stream.destroy or the callback.
48+
* decipherStream uses destroy to dispose the material.
49+
* So I tack a pass though stream onto the end.
50+
*/
51+
pipeline(parseHeaderStream, verifyStream, decipherStream, new PassThrough(), (err: Error) => {
52+
if (err) stream.emit('error', err)
53+
})
4854

4955
const stream = new Duplexify(parseHeaderStream, decipherStream)
5056

modules/decrypt-node/src/parse_header_stream.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,17 @@ export class ParseHeaderStream extends PortableTransformWithType {
108108
// The header is parsed, pass control
109109
const readPos = rawHeader.byteLength + headerIv.byteLength + headerAuthTag.byteLength
110110
const tail = headerBuffer.slice(readPos)
111-
this._transform = (chunk: any, _enc: string, cb: Function) => cb(null, chunk)
111+
/* needs calls in downstream _transform streams will throw.
112+
* But streams are async.
113+
* So this error should be turned into an `.emit('error', ex)`.
114+
*/
115+
this._transform = (chunk: any, _enc: string, cb: Function) => {
116+
try {
117+
cb(null, chunk)
118+
} catch (ex) {
119+
this.emit('error', ex)
120+
}
121+
}
112122
// flush the tail. Stream control is now in the verify and decrypt streams
113123
return setImmediate(() => this._transform(tail, encoding, callback))
114124
})

modules/decrypt-node/src/verify_stream.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
// @ts-ignore
1717
import { Transform as PortableTransform } from 'readable-stream'
1818
import { Transform } from 'stream' // eslint-disable-line no-unused-vars
19-
import { DecipherGCM } from 'crypto' // eslint-disable-line no-unused-vars
2019
import {
2120
needs,
22-
GetVerify // eslint-disable-line no-unused-vars
21+
GetVerify, // eslint-disable-line no-unused-vars
22+
GetDecipher // eslint-disable-line no-unused-vars
2323
} from '@aws-crypto/material-management-node'
2424
import {
2525
deserializeSignature,
@@ -35,7 +35,7 @@ const PortableTransformWithType = (<new (...args: any[]) => Transform>PortableTr
3535

3636
export interface VerifyInfo {
3737
headerInfo: HeaderInfo
38-
getDecipher: (iv: Uint8Array) => DecipherGCM
38+
getDecipher: GetDecipher
3939
dispose: () => void
4040
verify?: AWSVerify
4141
}
@@ -49,14 +49,16 @@ interface VerifyState {
4949
authTagBuffer: Buffer
5050
currentFrame?: BodyHeader
5151
signatureInfo: Buffer
52+
sequenceNumber: number
5253
}
5354

5455
export class VerifyStream extends PortableTransformWithType {
5556
private _headerInfo!: HeaderInfo
5657
private _verifyState: VerifyState = {
5758
buffer: Buffer.alloc(0),
5859
authTagBuffer: Buffer.alloc(0),
59-
signatureInfo: Buffer.alloc(0)
60+
signatureInfo: Buffer.alloc(0),
61+
sequenceNumber: 0
6062
}
6163
private _verify?: AWSVerify
6264
private _maxBodySize?: number
@@ -118,6 +120,17 @@ export class VerifyStream extends PortableTransformWithType {
118120
*/
119121
needs(!this._maxBodySize || this._maxBodySize >= frameHeader.contentLength, 'maxBodySize exceeded.')
120122

123+
/* Keeping track of the sequence number myself. */
124+
state.sequenceNumber += 1
125+
126+
/* Precondition: The sequence number is required to monotonically increase, starting from 1.
127+
* This is to avoid a bad actor from abusing the sequence number on un-signed algorithm suites.
128+
* If the frame size matched the data format (say NDJSON),
129+
* then the data could be significantly altered just by rearranging the frames.
130+
* Non-framed data returns a sequenceNumber of 1.
131+
*/
132+
needs(frameHeader.sequenceNumber === state.sequenceNumber, 'Encrypted body sequence out of order.')
133+
121134
if (this._verify) {
122135
this._verify.update(frameBuffer.slice(0, frameHeader.readPos))
123136
}

modules/decrypt-node/test/decrypt.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,12 @@ describe('decrypt', () => {
6767
expect(messageHeader.encryptionContext).to.deep.equal(fixtures.encryptionContext())
6868
expect(test.toString('base64')).to.equal(fixtures.base64Plaintext())
6969
})
70+
71+
it('Precondition: The sequence number is required to monotonically increase, starting from 1.', async () => {
72+
return expect(decrypt(
73+
fixtures.decryptKeyring(),
74+
fixtures.frameSequenceOutOfOrder(),
75+
{ encoding: 'base64' }
76+
)).to.rejectedWith(Error, 'Encrypted body sequence out of order.')
77+
})
7078
})

modules/decrypt-node/test/fixtures.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ export function encryptionContext () {
3737
}
3838
}
3939

40+
export function frameSequenceOutOfOrder () {
41+
/* This is the string 'asdf' encrypted with ALG_AES128_GCM_IV12_TAG16.
42+
* The data key is all zeros.
43+
* The frames have been reordered in order to test decryption failure.
44+
*/
45+
return 'AYAAFG/G0IFAKXNb9xuUlGZGldIAAAABAAFrAAFrAAMAAAACAAAAAAwAAAAB' + // header
46+
'AAAAAAAAAAAAAAAAslUUXWAiznTYc7fZwFSbDw' + // header auth
47+
'AAAAQAAAAAAAAAAAAAAAR9lLkID8B52uAln4zhh6ReJf' + // f
48+
'AAAAMAAAAAAAAAAAAAAAPnL7BJ22Xgjm6rneeNHrus9w' + // d
49+
'AAAAIAAAAAAAAAAAAAAAKiyryqy/AOiRH01aCOktajyw' + // s
50+
'AAAAEAAAAAAAAAAAAAAAEPhwHD/lPe6S/+QmqmQq+xXQ' // a
51+
}
52+
4053
export function decryptKeyring () {
4154
class TestKeyring extends KeyringNode {
4255
async _onEncrypt () : Promise<NodeEncryptionMaterial> {

modules/encrypt-browser/src/encrypt.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
AlgorithmSuiteIdentifier,
2222
getEncryptHelper,
2323
KeyringWebCrypto,
24+
needs,
2425
WebCryptoMaterialsManager // eslint-disable-line no-unused-vars
2526
} from '@aws-crypto/material-management-browser'
2627
import {
@@ -35,7 +36,8 @@ import {
3536
serializeSignatureInfo,
3637
FRAME_LENGTH,
3738
MESSAGE_ID_LENGTH,
38-
raw2der
39+
raw2der,
40+
Maximum
3941
} from '@aws-crypto/serialize'
4042
import { fromUtf8 } from '@aws-sdk/util-utf8-browser'
4143
import { getWebCryptoBackend } from '@aws-crypto/web-crypto-backend'
@@ -60,6 +62,9 @@ export async function encrypt (
6062
plaintext: Uint8Array,
6163
{ suiteId, encryptionContext, frameLength = FRAME_LENGTH }: EncryptInput = {}
6264
): Promise<EncryptResult> {
65+
/* Precondition: The frameLength must be less than the maximum frame size for browser encryption. */
66+
needs(frameLength > 0 && Maximum.FRAME_SIZE >= frameLength, `frameLength out of bounds: 0 > frameLength >= ${Maximum.FRAME_SIZE}`)
67+
6368
const backend = await getWebCryptoBackend()
6469
if (!backend) throw new Error('No supported crypto backend')
6570

@@ -119,14 +124,24 @@ export async function encrypt (
119124
const frameHeader = isFinalFrame
120125
? serialize.finalFrameHeader(sequenceNumber, frameIv, finalFrameLength)
121126
: serialize.frameHeader(sequenceNumber, frameIv)
127+
const contentString = messageAADContentString({ contentType: messageHeader.contentType, isFinalFrame })
122128
const messageAdditionalData = messageAAD(
123129
messageId,
124-
messageAADContentString({ contentType: messageHeader.contentType, isFinalFrame }),
130+
contentString,
125131
sequenceNumber,
126-
plaintextLength
132+
isFinalFrame ? finalFrameLength : frameLength
127133
)
128134

129-
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)
130145

131146
bodyContent.push(frameHeader, cipherBufferAndAuthTag)
132147
}

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

Lines changed: 37 additions & 1 deletion
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'
@@ -85,4 +87,38 @@ describe('encrypt structural testing', () => {
8587

8688
expect(messageHeader).to.deep.equal(messageInfo.messageHeader)
8789
})
90+
91+
it('Precondition: The frameLength must be less than the maximum frame size for browser encryption.', async () => {
92+
const frameLength = 0
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)
123+
})
88124
})

modules/encrypt-node/src/encrypt_stream.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
import {
1717
NodeDefaultCryptographicMaterialsManager, NodeAlgorithmSuite, AlgorithmSuiteIdentifier, // eslint-disable-line no-unused-vars
1818
KeyringNode, NodeEncryptionMaterial, getEncryptHelper, EncryptionContext, // eslint-disable-line no-unused-vars
19-
NodeMaterialsManager // eslint-disable-line no-unused-vars
19+
NodeMaterialsManager, // eslint-disable-line no-unused-vars
20+
needs
2021
} from '@aws-crypto/material-management-node'
2122
import { getFramedEncryptStream } from './framed_encrypt_stream'
2223
import { SignatureStream } from './signature_stream'
@@ -26,7 +27,8 @@ import {
2627
MessageHeader, // eslint-disable-line no-unused-vars
2728
serializeFactory, kdfInfo, ContentType, SerializationVersion, ObjectType,
2829
FRAME_LENGTH,
29-
MESSAGE_ID_LENGTH
30+
MESSAGE_ID_LENGTH,
31+
Maximum
3032
} from '@aws-crypto/serialize'
3133

3234
// @ts-ignore
@@ -56,6 +58,9 @@ export function encryptStream (
5658
): Duplex {
5759
const { suiteId, context, frameLength = FRAME_LENGTH } = op
5860

61+
/* Precondition: The frameLength must be less than the maximum frame size Node.js stream. */
62+
needs(frameLength > 0 && Maximum.FRAME_SIZE >= frameLength, `frameLength out of bounds: 0 > frameLength >= ${Maximum.FRAME_SIZE}`)
63+
5964
/* If the cmm is a Keyring, wrap it with NodeDefaultCryptographicMaterialsManager. */
6065
cmm = cmm instanceof KeyringNode
6166
? new NodeDefaultCryptographicMaterialsManager(cmm)

modules/encrypt-node/src/framed_encrypt_stream.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ import {
1919
} from '@aws-crypto/serialize'
2020
// @ts-ignore
2121
import { Transform as PortableTransform } from 'readable-stream'
22-
import { CipherGCM } from 'crypto' // eslint-disable-line no-unused-vars
2322
import { Transform } from 'stream' // eslint-disable-line no-unused-vars
24-
import { needs } from '@aws-crypto/material-management-node'
23+
import {
24+
GetCipher, // eslint-disable-line no-unused-vars
25+
AwsEsdkJsCipherGCM, // eslint-disable-line no-unused-vars
26+
needs
27+
} from '@aws-crypto/material-management-node'
2528

2629
const fromUtf8 = (input: string) => Buffer.from(input, 'utf8')
2730
const serialize = serializeFactory(fromUtf8)
@@ -38,7 +41,7 @@ interface EncryptFrame {
3841
content: Buffer[]
3942
bodyHeader: Buffer
4043
headerSent?: boolean
41-
cipher: CipherGCM,
44+
cipher: AwsEsdkJsCipherGCM,
4245
isFinalFrame: boolean
4346
}
4447

@@ -165,8 +168,6 @@ export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: Mes
165168
})()
166169
}
167170

168-
type GetCipher = (iv: Uint8Array) => CipherGCM
169-
170171
type EncryptFrameInput = {
171172
pendingFrame: AccumulatingFrame,
172173
messageHeader: MessageHeader,

0 commit comments

Comments
 (0)