Skip to content

Commit a55afd3

Browse files
committed
kms-keyring
1 parent 44d1e98 commit a55afd3

6 files changed

+354
-268
lines changed

modules/kms-keyring/src/kms_client_supplier.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function cacheClients<Client extends KMS> (
7676
}
7777

7878
type KMSOperations = keyof KMS
79-
/* It is possible that a malicious user can attempt a local resource
79+
/* It is possible that a malicious user can attempt a local resource
8080
* DOS by sending ciphertext with a large number of spurious regions.
8181
* This will fill the cache with regions and exhaust resources.
8282
* To avoid this, a call succeeds in contacting KMS.
@@ -88,6 +88,7 @@ function deferCache<Client extends KMS> (
8888
region: string,
8989
client: Client|false
9090
): Client|false {
91+
/* Check for early return (Postcondition): No client, then I cache false and move on. */
9192
if (!client) {
9293
clientsCache[region] = false
9394
return false
@@ -96,6 +97,7 @@ function deferCache<Client extends KMS> (
9697

9798
return (<KMSOperations[]>['encrypt', 'decrypt', 'generateDataKey']).reduce(wrapOperation, client)
9899

100+
/* Wrap each of the operations to cache the client on response */
99101
function wrapOperation (client: Client, name: KMSOperations): Client {
100102
type params = Parameters<KMS[typeof name]>
101103
type retValue = ReturnType<KMS[typeof name]>

modules/kms-keyring/src/kms_keyring.ts

Lines changed: 127 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { KmsClientSupplier } from './kms_client_supplier' // eslint-disable-line no-unused-vars
1717
import {
1818
needs,
19-
Keyring,
19+
Keyring, // eslint-disable-line no-unused-vars
2020
EncryptionMaterial, // eslint-disable-line no-unused-vars
2121
DecryptionMaterial, // eslint-disable-line no-unused-vars
2222
SupportedAlgorithmSuites, // eslint-disable-line no-unused-vars
@@ -34,123 +34,147 @@ import { regionFromKmsKeyArn } from './region_from_kms_key_arn'
3434

3535
export interface KmsKeyringInput<Client extends KMS> {
3636
clientProvider: KmsClientSupplier<Client>
37-
kmsKeys?: string[]
38-
generatorKmsKey?: string
37+
keyIds?: string[]
38+
generatorKeyId?: string
3939
grantTokens?: string
4040
}
4141

42-
export abstract class KmsKeyring<S extends SupportedAlgorithmSuites, Client extends KMS> extends Keyring<S> {
43-
public kmsKeys!: string[]
44-
public generatorKmsKey?: string
45-
public clientProvider!: KmsClientSupplier<Client>
46-
public grantTokens?: string
47-
48-
constructor ({ clientProvider, generatorKmsKey, kmsKeys = [], grantTokens }: KmsKeyringInput<Client>) {
49-
super()
50-
/* Precondition: All KMS key arns must be valid. */
51-
needs(!generatorKmsKey || !!regionFromKmsKeyArn(generatorKmsKey), 'Malformed arn.')
52-
needs(kmsKeys.every(keyarn => !!regionFromKmsKeyArn(keyarn)), 'Malformed arn.')
53-
/* Precondition: clientProvider needs to be a callable function. */
54-
needs(typeof clientProvider === 'function', '')
55-
56-
readOnlyProperty(this, 'clientProvider', clientProvider)
57-
readOnlyProperty(this, 'kmsKeys', kmsKeys)
58-
readOnlyProperty(this, 'generatorKmsKey', generatorKmsKey)
59-
readOnlyProperty(this, 'grantTokens', grantTokens)
60-
}
61-
62-
/* Keyrings *must* preserve the order of EDK's. The generatorKmsKey is the first on this list. */
63-
async _onEncrypt (material: EncryptionMaterial<S>, context?: EncryptionContext) {
64-
const kmsKeys = this.kmsKeys.slice()
65-
const { clientProvider, generatorKmsKey, grantTokens } = this
66-
if (generatorKmsKey && !material.hasUnencryptedDataKey) {
67-
const dataKey = await generateDataKey(clientProvider, material.suite.keyLengthBytes, generatorKmsKey, context, grantTokens)
68-
/* Precondition: A generatorKmsKey must generate if we do not have an unencrypted data key.
69-
* Client supplier is allowed to return undefined if, for example, user wants to exclude particular
70-
* regions. But if we are here it means that user configured keyring with a KMS key that was
71-
* incompatible with the client supplier in use.
72-
*/
73-
if (!dataKey) throw new Error('Generator KMS key did not generate a data key')
74-
75-
const flags = KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY |
76-
KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX |
77-
KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY
78-
const trace: KeyringTrace = { keyNamespace: KMS_PROVIDER_ID, keyName: dataKey.KeyId, flags }
79-
80-
material
81-
/* Postcondition: The unencryptedDataKey length must match the algorithm specification.
82-
* See cryptographic_materials as setUnencryptedDataKey will throw in this case.
83-
*/
84-
.setUnencryptedDataKey(dataKey.Plaintext, trace)
85-
.addEncryptedDataKey(kms2EncryptedDataKey(dataKey), KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY)
86-
} else if (generatorKmsKey) {
87-
kmsKeys.unshift(generatorKmsKey)
88-
}
42+
export interface KeyRing<S extends SupportedAlgorithmSuites, Client extends KMS> extends Keyring<S> {
43+
keyIds: string[]
44+
generatorKeyId?: string
45+
clientProvider: KmsClientSupplier<Client>
46+
grantTokens?: string
47+
_onEncrypt(material: EncryptionMaterial<S>, context?: EncryptionContext): Promise<EncryptionMaterial<S>>
48+
_onDecrypt(material: DecryptionMaterial<S>, encryptedDataKeys: EncryptedDataKey[], context?: EncryptionContext): Promise<DecryptionMaterial<S>>
49+
}
8950

90-
/* Precondition: If a generator does not exist, an unencryptedDataKey *must* already exist.
91-
* Furthermore *only* CMK's explicitly designated as generators can generate data keys.
92-
* See cryptographic_materials as getUnencryptedDataKey will throw in this case.
93-
*/
94-
const unencryptedDataKey = material.getUnencryptedDataKey()
51+
export interface KmsKeyRingConstructible<S extends SupportedAlgorithmSuites, Client extends KMS> {
52+
new(input: KmsKeyringInput<Client>): KeyRing<S, Client>
53+
}
9554

96-
const flags = KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY | KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX
97-
for (const kmsKey of kmsKeys) {
98-
const kmsEDK = await encrypt(clientProvider, unencryptedDataKey, kmsKey, context, grantTokens)
55+
export interface KeyRingConstructible<S extends SupportedAlgorithmSuites> {
56+
new(): Keyring<S>
57+
}
9958

100-
/* clientProvider may not return a client, in this case there is not an EDK to add */
101-
if (kmsEDK) material.addEncryptedDataKey(kms2EncryptedDataKey(kmsEDK), flags)
59+
export function KmsKeyringClass<S extends SupportedAlgorithmSuites, Client extends KMS> (
60+
BaseKeyring: KeyRingConstructible<S>
61+
): KmsKeyRingConstructible<S, Client> {
62+
class KmsKeyring extends BaseKeyring implements KeyRing<S, Client> {
63+
public keyIds!: string[]
64+
public generatorKeyId?: string
65+
public clientProvider!: KmsClientSupplier<Client>
66+
public grantTokens?: string
67+
68+
constructor ({ clientProvider, generatorKeyId, keyIds = [], grantTokens }: KmsKeyringInput<Client>) {
69+
super()
70+
/* Precondition: This is an abstract class. (But TypeScript does not have a clean way to model this) */
71+
needs(this.constructor !== KmsKeyring, 'new KmsKeyring is not allowed')
72+
/* Precondition: All KMS key arns must be valid. */
73+
needs(!generatorKeyId || !!regionFromKmsKeyArn(generatorKeyId), 'Malformed arn.')
74+
needs(keyIds.every(keyarn => !!regionFromKmsKeyArn(keyarn)), 'Malformed arn.')
75+
/* Precondition: clientProvider needs to be a callable function. */
76+
needs(typeof clientProvider === 'function', '')
77+
78+
readOnlyProperty(this, 'clientProvider', clientProvider)
79+
readOnlyProperty(this, 'keyIds', keyIds)
80+
readOnlyProperty(this, 'generatorKeyId', generatorKeyId)
81+
readOnlyProperty(this, 'grantTokens', grantTokens)
10282
}
10383

104-
return material
105-
}
106-
107-
async _onDecrypt (material: DecryptionMaterial<S>, encryptedDataKeys: EncryptedDataKey[], context?: EncryptionContext) {
108-
const kmsKeys = this.kmsKeys.slice()
109-
const { clientProvider, generatorKmsKey, grantTokens } = this
110-
if (generatorKmsKey) kmsKeys.unshift(generatorKmsKey)
111-
112-
/* If there are no key IDs in the list, keyring is in "discovery" mode and will attempt KMS calls with
113-
* every ARN it comes across in the message. If there are key IDs in the list, it will cross check the
114-
* ARN it reads with that list before attempting KMS calls. Note that if caller provided key IDs in
115-
* anything other than a CMK ARN format, the SDK will not attempt to decrypt those data keys, because
116-
* the EDK data format always specifies the CMK with the full (non-alias) ARN.
117-
*/
118-
const decryptableEDKs = encryptedDataKeys
119-
.filter(({ providerId, providerInfo }) => {
120-
if (providerId !== KMS_PROVIDER_ID) return false
121-
return kmsKeys.length
122-
? kmsKeys.includes(providerInfo)
123-
: true
124-
})
125-
126-
for (const edk of decryptableEDKs) {
127-
let dataKey: Required<DecryptOutput>|false = false
128-
try {
129-
dataKey = await decrypt(clientProvider, edk, context, grantTokens)
130-
} catch (e) {
131-
// there should be some debug here? or wrap?
132-
// Failures decrypt should not short-circuit the process
133-
// If the caller does not have access they may have access
134-
// through another Keyring.
84+
/* Keyrings *must* preserve the order of EDK's. The generatorKeyId is the first on this list. */
85+
async _onEncrypt (material: EncryptionMaterial<S>, context?: EncryptionContext) {
86+
const keyIds = this.keyIds.slice()
87+
const { clientProvider, generatorKeyId, grantTokens } = this
88+
if (generatorKeyId && !material.hasUnencryptedDataKey) {
89+
const dataKey = await generateDataKey(clientProvider, material.suite.keyLengthBytes, generatorKeyId, context, grantTokens)
90+
/* Precondition: A generatorKeyId must generate if we do not have an unencrypted data key.
91+
* Client supplier is allowed to return undefined if, for example, user wants to exclude particular
92+
* regions. But if we are here it means that user configured keyring with a KMS key that was
93+
* incompatible with the client supplier in use.
94+
*/
95+
if (!dataKey) throw new Error('Generator KMS key did not generate a data key')
96+
97+
const flags = KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY |
98+
KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX |
99+
KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY
100+
const trace: KeyringTrace = { keyNamespace: KMS_PROVIDER_ID, keyName: dataKey.KeyId, flags }
101+
102+
material
103+
/* Postcondition: The unencryptedDataKey length must match the algorithm specification.
104+
* See cryptographic_materials as setUnencryptedDataKey will throw in this case.
105+
*/
106+
.setUnencryptedDataKey(dataKey.Plaintext, trace)
107+
.addEncryptedDataKey(kms2EncryptedDataKey(dataKey), KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY)
108+
} else if (generatorKeyId) {
109+
keyIds.unshift(generatorKeyId)
135110
}
136111

137-
/* Check for early return (Postcondition): clientProvider may not return a client. */
138-
if (!dataKey) continue
112+
/* Precondition: If a generator does not exist, an unencryptedDataKey *must* already exist.
113+
* Furthermore *only* CMK's explicitly designated as generators can generate data keys.
114+
* See cryptographic_materials as getUnencryptedDataKey will throw in this case.
115+
*/
116+
const unencryptedDataKey = material.getUnencryptedDataKey()
139117

140-
/* Postcondition: The KeyId from KMS must match the encoded KeyID. */
141-
needs(dataKey.KeyId === edk.providerInfo, 'KMS Decryption key does not match serialized provider.')
118+
const flags = KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY | KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX
119+
for (const kmsKey of keyIds) {
120+
const kmsEDK = await encrypt(clientProvider, unencryptedDataKey, kmsKey, context, grantTokens)
142121

143-
const flags = KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY | KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX
144-
const trace: KeyringTrace = { keyNamespace: KMS_PROVIDER_ID, keyName: dataKey.KeyId, flags }
122+
/* clientProvider may not return a client, in this case there is not an EDK to add */
123+
if (kmsEDK) material.addEncryptedDataKey(kms2EncryptedDataKey(kmsEDK), flags)
124+
}
145125

146-
/* Postcondition: The unencryptedDataKey length must match the algorithm specification.
147-
* See cryptographic_materials as setUnencryptedDataKey will throw in this case.
148-
*/
149-
material.setUnencryptedDataKey(dataKey.Plaintext, trace)
150126
return material
151127
}
152128

153-
return material
129+
async _onDecrypt (material: DecryptionMaterial<S>, encryptedDataKeys: EncryptedDataKey[], context?: EncryptionContext) {
130+
const keyIds = this.keyIds.slice()
131+
const { clientProvider, generatorKeyId, grantTokens } = this
132+
if (generatorKeyId) keyIds.unshift(generatorKeyId)
133+
134+
/* If there are no key IDs in the list, keyring is in "discovery" mode and will attempt KMS calls with
135+
* every ARN it comes across in the message. If there are key IDs in the list, it will cross check the
136+
* ARN it reads with that list before attempting KMS calls. Note that if caller provided key IDs in
137+
* anything other than a CMK ARN format, the SDK will not attempt to decrypt those data keys, because
138+
* the EDK data format always specifies the CMK with the full (non-alias) ARN.
139+
*/
140+
const decryptableEDKs = encryptedDataKeys
141+
.filter(({ providerId, providerInfo }) => {
142+
if (providerId !== KMS_PROVIDER_ID) return false
143+
return keyIds.length
144+
? keyIds.includes(providerInfo)
145+
: true
146+
})
147+
148+
for (const edk of decryptableEDKs) {
149+
let dataKey: Required<DecryptOutput>|false = false
150+
try {
151+
dataKey = await decrypt(clientProvider, edk, context, grantTokens)
152+
} catch (e) {
153+
// there should be some debug here? or wrap?
154+
// Failures decrypt should not short-circuit the process
155+
// If the caller does not have access they may have access
156+
// through another Keyring.
157+
}
158+
159+
/* Check for early return (Postcondition): clientProvider may not return a client. */
160+
if (!dataKey) continue
161+
162+
/* Postcondition: The KeyId from KMS must match the encoded KeyID. */
163+
needs(dataKey.KeyId === edk.providerInfo, 'KMS Decryption key does not match serialized provider.')
164+
165+
const flags = KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY | KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX
166+
const trace: KeyringTrace = { keyNamespace: KMS_PROVIDER_ID, keyName: dataKey.KeyId, flags }
167+
168+
/* Postcondition: The unencryptedDataKey length must match the algorithm specification.
169+
* See cryptographic_materials as setUnencryptedDataKey will throw in this case.
170+
*/
171+
material.setUnencryptedDataKey(dataKey.Plaintext, trace)
172+
return material
173+
}
174+
175+
return material
176+
}
154177
}
178+
immutableClass(KmsKeyring)
179+
return KmsKeyring
155180
}
156-
immutableClass(KmsKeyring)

modules/kms-keyring/test/kms_client_supplier.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,46 @@ describe('cacheClients', () => {
142142
const test = getKmsClient(region)
143143
expect(test).instanceOf(TestKMS)
144144
expect(assertCount).to.equal(1)
145+
})
146+
147+
it('does not cache the client until KMS has been contacted', () => {
148+
const region = 'us-west-2'
149+
let assertCount = 0
150+
const TestKMS: any = class {
151+
constructor (config: KMSConfiguration) {
152+
expect(config.region).to.equal(region)
153+
assertCount++
154+
}
155+
}
156+
const getKmsClient = cacheClients(getClient(TestKMS))
157+
const test = getKmsClient(region)
158+
expect(test).instanceOf(TestKMS)
159+
expect(assertCount).to.equal(1)
160+
161+
const test2 = getKmsClient(region)
162+
expect(test === test2).to.equal(false)
163+
expect(assertCount).to.equal(2)
164+
})
165+
166+
it('cache the client after KMS has been contacted', async () => {
167+
const region = 'us-west-2'
168+
let assertCount = 0
169+
const TestKMS: any = class {
170+
constructor (config: KMSConfiguration) {
171+
expect(config.region).to.equal(region)
172+
assertCount++
173+
}
174+
async decrypt () {
175+
176+
}
177+
}
178+
const getKmsClient = cacheClients(getClient(TestKMS))
179+
const test = getKmsClient(region)
180+
if (!test) throw new Error('never')
181+
expect(test).instanceOf(TestKMS)
182+
expect(assertCount).to.equal(1)
183+
184+
await test.decrypt({} as any)
145185

146186
const test2 = getKmsClient(region)
147187
expect(test === test2).to.equal(true)

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,48 +17,50 @@
1717

1818
import { expect } from 'chai'
1919
import 'mocha'
20-
import { KmsKeyring } from '../src/kms_keyring'
21-
import { NodeAlgorithmSuite } from '@aws-crypto/material-management' // eslint-disable-line no-unused-vars
22-
import { KMS } from '../src/kms_types/KMS' // eslint-disable-line no-unused-vars
20+
import {
21+
KmsKeyringClass,
22+
KeyRingConstructible // eslint-disable-line no-unused-vars
23+
} from '../src/kms_keyring'
24+
import { NodeAlgorithmSuite, Keyring } from '@aws-crypto/material-management' // eslint-disable-line no-unused-vars
2325

2426
describe('KmsKeyring: constructor', () => {
2527
it('set properties', () => {
2628
const clientProvider: any = () => {}
27-
const generatorKmsKey = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias'
28-
const kmsKeys = ['arn:aws:kms:us-east-1:123456789012:alias/example-alias']
29+
const generatorKeyId = 'arn:aws:kms:us-east-1:123456789012:alias/example-alias'
30+
const keyIds = ['arn:aws:kms:us-east-1:123456789012:alias/example-alias']
2931
const grantTokens = 'grant'
3032

31-
class TestKmsKeyring extends KmsKeyring<NodeAlgorithmSuite, KMS> {}
33+
class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible<NodeAlgorithmSuite>) {}
3234

33-
const test = new TestKmsKeyring({ clientProvider, generatorKmsKey, kmsKeys, grantTokens })
35+
const test = new TestKmsKeyring({ clientProvider, generatorKeyId, keyIds, grantTokens })
3436
expect(test.clientProvider).to.equal(clientProvider)
35-
expect(test.generatorKmsKey).to.equal(generatorKmsKey)
36-
expect(test.kmsKeys).to.equal(kmsKeys)
37+
expect(test.generatorKeyId).to.equal(generatorKeyId)
38+
expect(test.keyIds).to.equal(keyIds)
3739
expect(test.grantTokens).to.equal(grantTokens)
3840
})
3941

4042
it('Precondition: All KMS key arns must be valid.', () => {
4143
const clientProvider: any = () => {}
42-
class TestKmsKeyring extends KmsKeyring<NodeAlgorithmSuite, KMS> {}
44+
class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible<NodeAlgorithmSuite>) {}
4345

4446
expect(() => new TestKmsKeyring({
4547
clientProvider,
46-
generatorKmsKey: 'Not arn'
48+
generatorKeyId: 'Not arn'
4749
})).to.throw()
4850

4951
expect(() => new TestKmsKeyring({
5052
clientProvider,
51-
kmsKeys: ['Not arn']
53+
keyIds: ['Not arn']
5254
})).to.throw()
5355

5456
expect(() => new TestKmsKeyring({
5557
clientProvider,
56-
kmsKeys: ['arn:aws:kms:us-east-1:123456789012:alias/example-alias', 'Not arn']
58+
keyIds: ['arn:aws:kms:us-east-1:123456789012:alias/example-alias', 'Not arn']
5759
})).to.throw()
5860
})
5961

6062
it('Precondition: clientProvider needs to be a callable function.', () => {
61-
class TestKmsKeyring extends KmsKeyring<NodeAlgorithmSuite, KMS> {}
63+
class TestKmsKeyring extends KmsKeyringClass(Keyring as KeyRingConstructible<NodeAlgorithmSuite>) {}
6264
const clientProvider: any = 'not function'
6365
expect(() => new TestKmsKeyring({ clientProvider })).to.throw()
6466
})

0 commit comments

Comments
 (0)