Skip to content

Commit 7198100

Browse files
authored
fix: Better error messageing (#212)
resolves #152 When attempting to decrypt a set of encrypted data keys, if any attempt is successful, then the entire operation should be considered successful. However if no data key can be obtained, and there were errors, these errors should be visible to the caller. An excellent example is attempting to decrypt with a KMS CMK alias arn. The KMS Keyring will be unable to decrypt, but was returning no error. This resulted the Error 'Unencrypted data key is invalid.' This is because the default CMM sees that the material does not have any unencrypted data key. A better error message would be the one from KMS in this case. Updating with tests both the KMS Keyrings, as well as the MultiKeyring.
1 parent 7dfa1ae commit 7198100

File tree

4 files changed

+134
-13
lines changed

4 files changed

+134
-13
lines changed

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] {

0 commit comments

Comments
 (0)