Skip to content

Commit bb60447

Browse files
authored
Merge branch 'master' into verify_version_and_type
2 parents 9b1ce1c + 1788d25 commit bb60447

File tree

11 files changed

+183
-22
lines changed

11 files changed

+183
-22
lines changed

modules/encrypt-node/src/encrypt.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ export async function encrypt (
2626
plaintext: Buffer|Uint8Array|Readable|string|NodeJS.ReadableStream,
2727
op: EncryptInput = {}
2828
): Promise<EncryptOutput> {
29-
const stream = encryptStream(cmm, op)
3029
const { encoding } = op
30+
if (plaintext instanceof Uint8Array) {
31+
op.plaintextLength = plaintext.byteLength
32+
} else if (typeof plaintext === 'string') {
33+
plaintext = Buffer.from(plaintext, encoding)
34+
op.plaintextLength = plaintext.byteLength
35+
}
3136

37+
const stream = encryptStream(cmm, op)
3238
const result: Buffer[] = []
3339
let messageHeader: MessageHeader|false = false
3440
stream
@@ -38,8 +44,6 @@ export async function encrypt (
3844
// This will check both Uint8Array|Buffer
3945
if (plaintext instanceof Uint8Array) {
4046
stream.end(plaintext)
41-
} else if (typeof plaintext === 'string') {
42-
stream.end(Buffer.from(plaintext, encoding))
4347
} else if (plaintext.readable) {
4448
plaintext.pipe(stream)
4549
} else {

modules/encrypt-node/src/encrypt_stream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function encryptStream (
7878

7979
wrappingStream.emit('MessageHeader', messageHeader)
8080

81-
const encryptStream = getFramedEncryptStream(getCipher, messageHeader, dispose)
81+
const encryptStream = getFramedEncryptStream(getCipher, messageHeader, dispose, plaintextLength)
8282
const signatureStream = new SignatureStream(getSigner)
8383

8484
pipeline(encryptStream, signatureStream)

modules/encrypt-node/src/framed_encrypt_stream.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
import {
1717
serializeFactory, aadFactory,
18-
MessageHeader // eslint-disable-line no-unused-vars
18+
MessageHeader, // eslint-disable-line no-unused-vars
19+
Maximum
1920
} from '@aws-crypto/serialize'
2021
// @ts-ignore
2122
import { Transform as PortableTransform } from 'readable-stream'
@@ -49,11 +50,17 @@ const ioTick = () => new Promise(resolve => setImmediate(resolve))
4950
const noop = () => {}
5051
type ErrBack = (err?: Error) => void
5152

52-
export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: MessageHeader, dispose: Function) {
53+
export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: MessageHeader, dispose: Function, plaintextLength?: number) {
5354
let accumulatingFrame: AccumulatingFrame = { contentLength: 0, content: [], sequenceNumber: 1 }
5455
let pathologicalDrain: Function = noop
5556
const { frameLength } = messageHeader
5657

58+
/* Precondition: plaintextLength must be within bounds.
59+
* The Maximum.BYTES_PER_MESSAGE is set to be within Number.MAX_SAFE_INTEGER
60+
* See serialize/identifiers.ts enum Maximum for more details.
61+
*/
62+
needs(!plaintextLength || (plaintextLength >= 0 && Maximum.BYTES_PER_MESSAGE >= plaintextLength), 'plaintextLength out of bounds.')
63+
5764
/* Keeping the messageHeader, accumulatingFrame and pathologicalDrain private is the intention here.
5865
* It is already unlikely that these values could be touched in the current composition of streams,
5966
* but a different composition may change this.
@@ -63,6 +70,11 @@ export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: Mes
6370
_transform (chunk: Buffer, encoding: string, callback: ErrBack) {
6471
const contentLeft = frameLength - accumulatingFrame.contentLength
6572

73+
/* Precondition: Must not process more than plaintextLength.
74+
* The plaintextLength is the MAXIMUM value that can be encrypted.
75+
*/
76+
needs(!plaintextLength || (plaintextLength -= chunk.length) >= 0, 'Encrypted data exceeded plaintextLength.')
77+
6678
/* Check for early return (Postcondition): Have not accumulated a frame. */
6779
if (contentLeft > chunk.length) {
6880
// eat more

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ describe('getFramedEncryptStream', () => {
3030
expect(test._transform).is.a('function')
3131
})
3232

33+
it('Precondition: plaintextLength must be within bounds.', () => {
34+
const getCipher: any = () => {}
35+
expect(() => getFramedEncryptStream(getCipher, {} as any, () => {}, -1)).to.throw(Error, 'plaintextLength out of bounds.')
36+
expect(() => getFramedEncryptStream(getCipher, {} as any, () => {}, Number.MAX_SAFE_INTEGER + 1)).to.throw(Error, 'plaintextLength out of bounds.')
37+
38+
/* Math is hard.
39+
* I want to make sure that I don't have an errant off by 1 error.
40+
*/
41+
expect(() => getFramedEncryptStream(getCipher, {} as any, () => {}, Number.MAX_SAFE_INTEGER)).to.not.throw(Error)
42+
})
43+
44+
it('Precondition: Must not process more than plaintextLength.', () => {
45+
const getCipher: any = () => {}
46+
const test = getFramedEncryptStream(getCipher, { } as any, () => {}, 8)
47+
48+
expect(() => test._transform(Buffer.from(Array(9)), 'binary', () => {})).to.throw(Error, 'Encrypted data exceeded plaintextLength.')
49+
})
50+
3351
it('Check for early return (Postcondition): Have not accumulated a frame.', () => {
3452
const getCipher: any = () => {}
3553
const frameLength = 10

modules/kms-keyring-browser/test/kms_keyring_browser.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const { expect } = chai
3838
/* Injected from @aws-sdk/karma-credential-loader. */
3939
declare const credentials: any
4040

41-
describe('KmsKeyringNode::constructor', () => {
41+
describe('KmsKeyringBrowser::constructor', () => {
4242
it('constructor decorates', async () => {
4343
const generatorKeyId = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt'
4444
const keyArn = 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f'
@@ -60,7 +60,7 @@ describe('KmsKeyringNode::constructor', () => {
6060
})
6161
})
6262

63-
describe('RawAesKeyringWebCrypto encrypt/decrypt', () => {
63+
describe('KmsKeyringBrowser encrypt/decrypt', () => {
6464
const generatorKeyId = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt'
6565
const keyArn = 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f'
6666
const keyIds = [keyArn]

modules/kms-keyring-node/test/kms_keyring_node.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('KmsKeyringNode::constructor', () => {
4949
})
5050
})
5151

52-
describe('RawAesKeyringWebCrypto encrypt/decrypt', () => {
52+
describe('KmsKeyringNode encrypt/decrypt', () => {
5353
const generatorKeyId = 'arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt'
5454
const keyArn = 'arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f'
5555
const keyIds = [keyArn]

modules/kms-keyring/src/kms_keyring.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ export function KmsKeyringClass<S extends SupportedAlgorithmSuites, Client exten
172172
return this.isDiscovery || keyIds.includes(providerInfo)
173173
})
174174

175+
let cmkErrors: Error[] = []
176+
175177
for (const edk of decryptableEDKs) {
176178
let dataKey: RequiredDecryptResponse|false = false
177179
try {
@@ -182,10 +184,11 @@ export function KmsKeyringClass<S extends SupportedAlgorithmSuites, Client exten
182184
grantTokens
183185
)
184186
} catch (e) {
185-
// there should be some debug here? or wrap?
186-
// Failures decrypt should not short-circuit the process
187-
// If the caller does not have access they may have access
188-
// through another Keyring.
187+
/* Failures onDecrypt should not short-circuit the process
188+
* If the caller does not have access they may have access
189+
* through another Keyring.
190+
*/
191+
cmkErrors.push(e)
189192
}
190193

191194
/* Check for early return (Postcondition): clientProvider may not return a client. */
@@ -204,6 +207,20 @@ export function KmsKeyringClass<S extends SupportedAlgorithmSuites, Client exten
204207
return material
205208
}
206209

210+
/* Postcondition: A CMK must provide a valid data key or KMS must not have raised any errors.
211+
* If I have a data key,
212+
* decrypt errors can be ignored.
213+
* However, if I was unable to decrypt a data key AND I have errors,
214+
* these errors should bubble up.
215+
* Otherwise, the only error customers will see is that
216+
* the material does not have an unencrypted data key.
217+
* So I return a concatenated Error message
218+
*/
219+
needs(material.hasValidKey() || (!material.hasValidKey() && !cmkErrors.length)
220+
, cmkErrors
221+
.reduce((m, e, i) => `${m} Error #${i + 1} \n ${e.stack} \n`,
222+
'Unable to decrypt data key and one or more KMS CMKs had an error. \n '))
223+
207224
return material
208225
}
209226
}

modules/kms-keyring/test/kms_keyring.ondecrypt.test.ts

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,20 @@ describe('KmsKeyring: _onDecrypt',
138138
const discovery = true
139139
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
140140

141+
let edkCount = 0
141142
const clientProvider: any = () => {
142143
return { decrypt }
143-
function decrypt () {
144-
throw new Error('failed to decrypt')
144+
function decrypt ({ CiphertextBlob, EncryptionContext, GrantTokens }: any) {
145+
if (edkCount === 0) {
146+
edkCount += 1
147+
throw new Error('failed to decrypt')
148+
}
149+
expect(EncryptionContext).to.deep.equal(context)
150+
expect(GrantTokens).to.equal(grantTokens)
151+
return {
152+
Plaintext: new Uint8Array(suite.keyLengthBytes),
153+
KeyId: Buffer.from(<Uint8Array>CiphertextBlob).toString('utf8')
154+
}
145155
}
146156
}
147157
class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible<NodeAlgorithmSuite>) {}
@@ -160,11 +170,11 @@ describe('KmsKeyring: _onDecrypt',
160170

161171
const material = await testKeyring.onDecrypt(
162172
new NodeDecryptionMaterial(suite, context),
163-
[edk]
173+
[edk, edk]
164174
)
165175

166-
expect(material.hasUnencryptedDataKey).to.equal(false)
167-
expect(material.keyringTrace).to.have.lengthOf(0)
176+
expect(material.hasUnencryptedDataKey).to.equal(true)
177+
expect(material.keyringTrace).to.have.lengthOf(1)
168178
})
169179

170180
it('Check for early return (Postcondition): clientProvider may not return a client.', async () => {
@@ -279,4 +289,49 @@ describe('KmsKeyring: _onDecrypt',
279289
[edk]
280290
)).to.rejectedWith(Error, 'Key length does not agree with the algorithm specification.')
281291
})
292+
293+
it('Postcondition: A CMK must provide a valid data key or KMS must not have raised any errors.', async () => {
294+
const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias'
295+
const context = { some: 'context' }
296+
const grantTokens = ['grant']
297+
const discovery = true
298+
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
299+
300+
const clientProviderError: any = () => {
301+
return { decrypt }
302+
function decrypt () {
303+
throw new Error('failed to decrypt')
304+
}
305+
}
306+
class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible<NodeAlgorithmSuite>) {}
307+
308+
const testKeyring = new TestKmsKeyring({
309+
clientProvider: clientProviderError,
310+
grantTokens,
311+
discovery
312+
})
313+
314+
const edk = new EncryptedDataKey({
315+
providerId: 'aws-kms',
316+
providerInfo: generatorKeyId,
317+
encryptedDataKey: Buffer.from(generatorKeyId)
318+
})
319+
320+
await expect(testKeyring.onDecrypt(
321+
new NodeDecryptionMaterial(suite, context),
322+
[edk, edk]
323+
)).to.rejectedWith(Error, 'Unable to decrypt data key and one or more KMS CMKs had an error.')
324+
325+
/* This will make the decrypt loop not have an error.
326+
* This will exercise the `(!material.hasValidKey() && !cmkErrors.length)` `needs` condition.
327+
*/
328+
const clientProviderNoError: any = () => false
329+
await expect(new TestKmsKeyring({
330+
clientProvider: clientProviderNoError,
331+
grantTokens,
332+
discovery
333+
}).onDecrypt(new NodeDecryptionMaterial(suite, context),
334+
[edk, edk]
335+
)).to.not.rejectedWith(Error)
336+
})
282337
})

modules/material-management/src/multi_keyring.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,37 @@ function buildPrivateOnDecrypt<S extends SupportedAlgorithmSuites> () {
113113
const children = this.children.slice()
114114
if (this.generator) children.unshift(this.generator)
115115

116+
let childKeyringErrors: Error[] = []
117+
116118
for (const keyring of children) {
117119
/* Check for early return (Postcondition): Do not attempt to decrypt once I have a valid key. */
118120
if (material.hasValidKey()) return material
119121

120122
try {
121123
await keyring.onDecrypt(material, encryptedDataKeys)
122124
} catch (e) {
123-
// there should be some debug here? or wrap?
124-
// Failures onDecrypt should not short-circuit the process
125-
// If the caller does not have access they may have access
126-
// through another Keyring.
125+
/* Failures onDecrypt should not short-circuit the process
126+
* If the caller does not have access they may have access
127+
* through another Keyring.
128+
*/
129+
childKeyringErrors.push(e)
127130
}
128131
}
132+
133+
/* Postcondition: A child keyring must provide a valid data key or no child keyring must have raised an error.
134+
* If I have a data key,
135+
* decrypt errors can be ignored.
136+
* However, if I was unable to decrypt a data key AND I have errors,
137+
* these errors should bubble up.
138+
* Otherwise, the only error customers will see is that
139+
* the material does not have an unencrypted data key.
140+
* So I return a concatenated Error message
141+
*/
142+
needs(material.hasValidKey() || (!material.hasValidKey() && !childKeyringErrors.length)
143+
, childKeyringErrors
144+
.reduce((m, e, i) => `${m} Error #${i + 1} \n ${e.stack} \n`,
145+
'Unable to decrypt data key and one or more child keyrings had an error. \n '))
146+
129147
return material
130148
}
131149
}

modules/material-management/test/multi_keyring.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,37 @@ describe('MultiKeyring: onDecrypt', () => {
358358
expect(unwrapDataKey(test.getUnencryptedDataKey())).to.deep.equal(unencryptedDataKey)
359359
expect(called).to.equal(true)
360360
})
361+
362+
it('Postcondition: A child keyring must provide a valid data key or no child keyring must have raised an error.', async () => {
363+
const suite = new NodeAlgorithmSuite(AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16)
364+
const [edk0] = makeEDKandTraceForDecrypt(0)
365+
const material = new NodeDecryptionMaterial(suite, {})
366+
const childNotSucceeded = keyRingFactory({
367+
async onDecrypt () {
368+
// Because this keyring does not return a value, it will result in an error
369+
},
370+
onEncrypt: never
371+
})
372+
const children = [childNotSucceeded]
373+
374+
const mkeyring = new MultiKeyringNode({ children })
375+
376+
await expect(mkeyring.onDecrypt(material, [edk0])).to.rejectedWith(Error, 'Unable to decrypt data key and one or more child keyrings had an error.')
377+
378+
/* This will make the decrypt loop not have an error.
379+
* This will exercise the `(!material.hasValidKey() && !childKeyringErrors.length)` `needs` condition.
380+
*/
381+
const childNoDataKey = keyRingFactory({
382+
async onDecrypt (material: NodeDecryptionMaterial /*, encryptedDataKeys: EncryptedDataKey[] */) {
383+
return material
384+
},
385+
onEncrypt: never
386+
})
387+
388+
const mkeyringNoErrors = new MultiKeyringNode({ children: [ childNoDataKey ] })
389+
390+
await expect(mkeyringNoErrors.onDecrypt(material, [edk0])).to.not.rejectedWith(Error)
391+
})
361392
})
362393

363394
function makeEDKandTraceForEncrypt (num: number): [EncryptedDataKey, KeyringTrace] {

modules/serialize/src/identifiers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export enum Maximum {
8080
* or some value larger 2 ** 63.
8181
*/
8282
BYTES_PER_CACHED_KEY_LIMIT = 2 ** 53 - 1, // eslint-disable-line no-unused-vars
83+
/* This value should be Maximum.FRAME_COUNT * Maximum.FRAME_SIZE.
84+
* However this would be ~ 2 ** 64, much larger than Number.MAX_SAFE_INTEGER.
85+
* For the same reasons outlined above in BYTES_PER_CACHED_KEY_LIMIT
86+
* this value is set to 2 ** 53 - 1.
87+
*/
88+
BYTES_PER_MESSAGE = 2 ** 53 - 1, // eslint-disable-line no-unused-vars
8389
// Maximum number of frames allowed in one message as defined in specification
8490
FRAME_COUNT = 2 ** 32 - 1, // eslint-disable-line no-unused-vars
8591
// Maximum bytes allowed in a single frame as defined in specification

0 commit comments

Comments
 (0)