-
Notifications
You must be signed in to change notification settings - Fork 63
/
Copy pathkms_hkeyring_node.ts
498 lines (435 loc) · 21.6 KB
/
kms_hkeyring_node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
/**
* This class is the KMS H-keyring. This class is within the kms-keyring-node
* module because it is a KMS keyring variation. However, the KDF used in this
* keyring's operations will only work in Node.js runtimes and not browser JS.
* Thus, this H-keyring implementation is only Node compatible, and thus,
* resides in a node module, not a browser module
*/
import {
EncryptedDataKey,
immutableClass,
KeyringNode,
needs,
NodeAlgorithmSuite,
NodeDecryptionMaterial,
NodeEncryptionMaterial,
readOnlyProperty,
Catchable,
DecryptionMaterial,
isDecryptionMaterial,
} from '@aws-crypto/material-management'
import {
BranchKeyMaterialEntry,
CryptographicMaterialsCache,
getLocalCryptographicMaterialsCache,
} from '@aws-crypto/cache-material'
import {
destructureCiphertext,
getBranchKeyId,
getBranchKeyMaterials,
getCacheEntryId,
getPlaintextDataKey,
wrapPlaintextDataKey,
unwrapEncryptedDataKey,
filterEdk,
modifyEncryptionMaterial,
modifyDencryptionMaterial,
decompressBytesToUuidv4,
stringToUtf8Bytes,
} from './kms_hkeyring_node_helpers'
import {
BranchKeyStoreNode,
isIBranchKeyStoreNode,
} from '@aws-crypto/branch-keystore-node'
import {
BranchKeyIdSupplier,
isBranchKeyIdSupplier,
} from '@aws-crypto/kms-keyring'
import { randomBytes } from 'crypto'
export interface KmsHierarchicalKeyRingNodeInput {
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=implication
//# - MUST provide either a Branch Key Identifier or a [Branch Key Supplier](#branch-key-supplier)
branchKeyId?: string
branchKeyIdSupplier?: BranchKeyIdSupplier
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=implication
//# - MUST provide a [Keystore](../branch-key-store.md)
keyStore: BranchKeyStoreNode
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=implication
//# - MUST provide a [cache limit TTL](#cache-limit-ttl)
cacheLimitTtl: number
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=exception
//# - MAY provide a [Cache Type](#cache-type)
cache?: CryptographicMaterialsCache<NodeAlgorithmSuite>
maxCacheSize?: number
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=implication
//# - MAY provide a [Partition ID](#partition-id)
partitionId?: string
utf8Sorting?: boolean
}
export interface IKmsHierarchicalKeyRingNode extends KeyringNode {
branchKeyId?: string
branchKeyIdSupplier?: Readonly<BranchKeyIdSupplier>
keyStore: Readonly<BranchKeyStoreNode>
cacheLimitTtl: number
_onEncrypt(material: NodeEncryptionMaterial): Promise<NodeEncryptionMaterial>
_onDecrypt(
material: NodeDecryptionMaterial,
encryptedDataKeys: EncryptedDataKey[]
): Promise<NodeDecryptionMaterial>
cacheEntryHasExceededLimits(entry: BranchKeyMaterialEntry): boolean
}
export class KmsHierarchicalKeyRingNode
extends KeyringNode
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#interface
//= type=implication
//# MUST implement the [AWS Encryption SDK Keyring interface](../keyring-interface.md#interface)
implements IKmsHierarchicalKeyRingNode
{
public declare branchKeyId?: string
public declare branchKeyIdSupplier?: Readonly<BranchKeyIdSupplier>
public declare keyStore: Readonly<BranchKeyStoreNode>
public declare _logicalKeyStoreName: Buffer
public declare cacheLimitTtl: number
public declare maxCacheSize?: number
public declare _cmc: CryptographicMaterialsCache<NodeAlgorithmSuite>
declare readonly _partition: Buffer
public declare _utf8Sorting: boolean
constructor({
branchKeyId,
branchKeyIdSupplier,
keyStore,
cacheLimitTtl,
cache,
maxCacheSize,
partitionId,
utf8Sorting,
}: KmsHierarchicalKeyRingNodeInput) {
super()
needs(
!partitionId || typeof partitionId === 'string',
'Partition id must be a string.'
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id
//= type=implication
//# The Partition ID MUST NOT be changed after initialization.
readOnlyProperty(
this,
'_partition',
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id-1
//# It can either be a String provided by the user, which MUST be interpreted as the bytes of
//# UTF-8 Encoding of the String, or a v4 UUID, which SHOULD be interpreted as the 16 byte representation of the UUID.
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id-1
//# The constructor of the Hierarchical Keyring MUST record these bytes at construction time.
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id
//# If provided, it MUST be interpreted as UTF8 bytes.
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#partition-id
//= type=exception
//# If the PartitionId is NOT provided by the user, it MUST be set to the 16 byte representation of a v4 UUID.
partitionId ? stringToUtf8Bytes(partitionId) : randomBytes(64)
)
/* Precondition: The branch key id must be a string */
if (branchKeyId) {
needs(
typeof branchKeyId === 'string',
'The branch key id must be a string'
)
} else {
branchKeyId = undefined
}
/* Precondition: The branch key id supplier must be a BranchKeyIdSupplier */
if (branchKeyIdSupplier) {
needs(
isBranchKeyIdSupplier(branchKeyIdSupplier),
'The branch key id supplier must be a BranchKeyIdSupplier'
)
} else {
branchKeyIdSupplier = undefined
}
/* Precondition: The keystore must be a BranchKeyStore */
needs(
isIBranchKeyStoreNode(keyStore),
'The keystore must be a BranchKeyStore'
)
readOnlyProperty(
this,
'_logicalKeyStoreName',
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#logical-key-store-name
//# Logical Key Store Name MUST be converted to UTF8 Bytes to be used in
//# the cache identifiers.
stringToUtf8Bytes(keyStore.getKeyStoreInfo().logicalKeyStoreName)
)
/* Precondition: The cache limit TTL must be a number */
needs(
typeof cacheLimitTtl === 'number',
'The cache limit TTL must be a number'
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#cache-limit-ttl
//# The maximum amount of time in seconds that an entry within the cache may be used before it MUST be evicted.
//# The client MUST set a time-to-live (TTL) for [branch key materials](../structures.md#branch-key-materials) in the underlying cache.
//# This value MUST be greater than zero.
/* Precondition: Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds */
// In the MPL, TTL can be a non-negative signed 64-bit integer.
// However, JavaScript numbers cannot safely represent integers beyond
// Number.MAX_SAFE_INTEGER. Thus, we will cap TTL in seconds such that TTL
// in ms is <= Number.MAX_SAFE_INTEGER. TTL could be a BigInt type but this
// would require casting back to a number in order to configure the CMC,
// which leads to a lossy conversion
needs(
0 <= cacheLimitTtl && cacheLimitTtl * 1000 <= Number.MAX_SAFE_INTEGER,
'Cache limit TTL must be non-negative and less than or equal to (Number.MAX_SAFE_INTEGER / 1000) seconds'
)
/* Precondition: Must provide a branch key identifier or supplier */
needs(
branchKeyId || branchKeyIdSupplier,
'Must provide a branch key identifier or supplier'
)
readOnlyProperty(this, 'keyStore', Object.freeze(keyStore))
/* Postcondition: The keystore object is frozen */
// convert seconds to milliseconds
readOnlyProperty(this, 'cacheLimitTtl', cacheLimitTtl * 1000)
readOnlyProperty(this, 'branchKeyId', branchKeyId)
readOnlyProperty(
this,
'branchKeyIdSupplier',
branchKeyIdSupplier
? Object.freeze(branchKeyIdSupplier)
: branchKeyIdSupplier
)
/* Postcondition: Provided branch key supplier must be frozen */
if (cache) {
needs(!maxCacheSize, 'Max cache size not supported when passing a cache.')
} else {
/* Precondition: The max cache size must be a number */
needs(
// Order is important, 0 is a number but also false.
typeof maxCacheSize === 'number' || !maxCacheSize,
'The max cache size must be a number'
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//# If no max cache size is provided, the cryptographic materials cache MUST be configured to a
//# max cache size of 1000.
maxCacheSize = maxCacheSize === 0 || maxCacheSize ? maxCacheSize : 1000
/* Precondition: Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER */
needs(
0 <= maxCacheSize && maxCacheSize <= Number.MAX_SAFE_INTEGER,
'Max cache size must be non-negative and less than or equal Number.MAX_SAFE_INTEGER'
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//# On initialization the Hierarchical Keyring MUST initialize a [cryptographic-materials-cache](../local-cryptographic-materials-cache.md) with the configured cache limit TTL and the max cache size.
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//# If the Hierarchical Keyring does NOT get a `Shared` cache on initialization,
//# it MUST initialize a [cryptographic-materials-cache](../local-cryptographic-materials-cache.md)
//# with the user provided cache limit TTL and the entry capacity.
cache = getLocalCryptographicMaterialsCache(maxCacheSize)
}
readOnlyProperty(this, 'maxCacheSize', maxCacheSize)
readOnlyProperty(this, '_cmc', cache)
if (utf8Sorting === undefined) {
readOnlyProperty(this, '_utf8Sorting', false)
} else {
readOnlyProperty(this, '_utf8Sorting', utf8Sorting)
}
Object.freeze(this)
/* Postcondition: The HKR object must be frozen */
}
async _onEncrypt(
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt
//= type=implication
//# OnEncrypt MUST take [encryption materials](../structures.md#encryption-materials) as input.
encryptionMaterial: NodeEncryptionMaterial
): Promise<NodeEncryptionMaterial> {
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt
//# The `branchKeyId` used in this operation is either the configured branchKeyId, if supplied, or the result of the `branchKeySupplier`'s
//# `getBranchKeyId` operation, using the encryption material's encryption context as input.
const branchKeyId = getBranchKeyId(this, encryptionMaterial)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt
//# The hierarchical keyring MUST use the formulas specified in [Appendix A](#appendix-a-cache-entry-identifier-formulas)
//# to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier).
const cacheEntryId = getCacheEntryId(
this._logicalKeyStoreName,
this._partition,
branchKeyId
)
const branchKeyMaterials = await getBranchKeyMaterials(
this,
this._cmc,
branchKeyId,
cacheEntryId
)
// get a pdk (generate it if not already set)
const pdk = getPlaintextDataKey(encryptionMaterial)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt
//# If the keyring is unable to wrap a plaintext data key, OnEncrypt MUST fail
//# and MUST NOT modify the [decryption materials](structures.md#decryption-materials).
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt
//# - MUST wrap a data key with the branch key materials according to the [branch key wrapping](#branch-key-wrapping) section.
const edk = wrapPlaintextDataKey(
pdk,
branchKeyMaterials,
encryptionMaterial,
this._utf8Sorting
)
// return the modified encryption material with the new edk and newly
// generated pdk (if applicable)
return modifyEncryptionMaterial(encryptionMaterial, pdk, edk, branchKeyId)
}
async onDecrypt(
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//= type=implication
//# OnDecrypt MUST take [decryption materials](../structures.md#decryption-materials) and a list of [encrypted data keys](../structures.md#encrypted-data-keys) as input.
material: NodeDecryptionMaterial,
encryptedDataKeys: EncryptedDataKey[]
): Promise<DecryptionMaterial<NodeAlgorithmSuite>> {
needs(isDecryptionMaterial(material), 'Unsupported material type.')
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# If the decryption materials already contain a `PlainTextDataKey`, OnDecrypt MUST fail.
/* Precondition: If the decryption materials already contain a PlainTextDataKey, OnDecrypt MUST fail */
needs(
!material.hasUnencryptedDataKey,
'Decryption materials already contain a plaintext data key'
)
needs(
encryptedDataKeys.every((edk) => edk instanceof EncryptedDataKey),
'Unsupported EncryptedDataKey type'
)
const _material = await this._onDecrypt(material, encryptedDataKeys)
needs(
material === _material,
'New DecryptionMaterial instances can not be created.'
)
return material
}
cacheEntryHasExceededLimits({ now }: BranchKeyMaterialEntry): boolean {
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt
//# There MUST be a check (cacheEntryWithinLimits) to make sure that for the cache entry found, who's TTL has NOT expired,
//# `time.now() - cacheEntryCreationTime <= ttlSeconds` is true and
//# valid for TTL of the Hierarchical Keyring getting the cache entry.
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# There MUST be a check (cacheEntryWithinLimits) to make sure that for the cache entry found, who's TTL has NOT expired,
//# `time.now() - cacheEntryCreationTime <= ttlSeconds` is true and
//# valid for TTL of the Hierarchical Keyring getting the cache entry.
const age = Date.now() - now
return age > this.cacheLimitTtl
}
async _onDecrypt(
decryptionMaterial: NodeDecryptionMaterial,
encryptedDataKeyObjs: EncryptedDataKey[]
): Promise<NodeDecryptionMaterial> {
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# The `branchKeyId` used in this operation is either the configured branchKeyId, if supplied, or the result of the `branchKeySupplier`'s
//# `getBranchKeyId` operation, using the decryption material's encryption context as input.
const branchKeyId = getBranchKeyId(this, decryptionMaterial)
// filter out edk objects that don't match this keyring's configuration
const filteredEdkObjs = encryptedDataKeyObjs.filter((edkObj) =>
filterEdk(branchKeyId, edkObj)
)
/* Precondition: There must be an encrypted data key that matches this keyring configuration */
needs(
filteredEdkObjs.length > 0,
"There must be an encrypted data key that matches this keyring's configuration"
)
const errors: Catchable[] = []
for (const { encryptedDataKey: ciphertext } of filteredEdkObjs) {
let udk: Uint8Array | undefined = undefined
try {
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt
//# - Deserialize the UUID string representation of the `version` from the [encrypted data key](../structures.md#encrypted-data-key) [ciphertext](#ciphertext).
// get the branch key version (as compressed bytes) from the
// destructured ciphertext of the edk
const { branchKeyVersionAsBytesCompressed } = destructureCiphertext(
ciphertext,
decryptionMaterial.suite
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt
//# - The deserialized UUID string representation of the `version`
// uncompress the branch key version into regular utf8 bytes
const branchKeyVersionAsBytes = stringToUtf8Bytes(
decompressBytesToUuidv4(branchKeyVersionAsBytesCompressed)
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# The hierarchical keyring MUST use the OnDecrypt formula specified in [Appendix A](#decryption-materials)
//# in order to compute the [cache entry identifier](cryptographic-materials-cache.md#cache-identifier).
const cacheEntryId = getCacheEntryId(
this._logicalKeyStoreName,
this._partition,
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt
//# OnDecrypt MUST calculate the following values:
branchKeyId,
branchKeyVersionAsBytes
)
// get the string representation of the branch key version
const branchKeyVersionAsString = decompressBytesToUuidv4(
branchKeyVersionAsBytesCompressed
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# To decrypt each encrypted data key in the filtered set, the hierarchical keyring MUST attempt
//# to find the corresponding [branch key materials](../structures.md#branch-key-materials)
//# from the underlying [cryptographic materials cache](../local-cryptographic-materials-cache.md).
const branchKeyMaterials = await getBranchKeyMaterials(
this,
this._cmc,
branchKeyId,
cacheEntryId,
branchKeyVersionAsString
)
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# - MUST unwrap the encrypted data key with the branch key materials according to the [branch key unwrapping](#branch-key-unwrapping) section.
udk = unwrapEncryptedDataKey(
ciphertext,
branchKeyMaterials,
decryptionMaterial,
this._utf8Sorting
)
} catch (e) {
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# For each encrypted data key in the filtered set, one at a time, OnDecrypt MUST attempt to decrypt the encrypted data key.
//# If this attempt results in an error, then these errors MUST be collected.
errors.push({ errPlus: e })
}
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# If a decryption succeeds, this keyring MUST
//# add the resulting plaintext data key to the decryption materials and return the modified materials.
if (udk) {
return modifyDencryptionMaterial(decryptionMaterial, udk, branchKeyId)
}
}
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt
//# If OnDecrypt fails to successfully decrypt any [encrypted data key](../structures.md#encrypted-data-key),
//# then it MUST yield an error that includes all the collected errors
//# and MUST NOT modify the [decryption materials](structures.md#decryption-materials).
throw new Error(
errors.reduce(
(m, e, i) => `${m} Error #${i + 1} \n ${e.errPlus.stack} \n`,
'Unable to decrypt data key'
)
)
}
}
immutableClass(KmsHierarchicalKeyRingNode)
// The JS version has not been released with a Storm Tracking CMC
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=exception
//# If the cache to initialize is a [Storm Tracking Cryptographic Materials Cache](../storm-tracking-cryptographic-materials-cache.md#overview)
//# then the [Grace Period](../storm-tracking-cryptographic-materials-cache.md#grace-period) MUST be less than the [cache limit TTL](#cache-limit-ttl).
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#initialization
//= type=exception
//# If no `cache` is provided, a `DefaultCache` MUST be configured with entry capacity of 1000.
// These are not something we can enforce
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#logical-key-store-name
//= type=exception
//# > Note: Users MUST NEVER have two different physical Key Stores with the same Logical Key Store Name.
//= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#shared-cache-considerations
//= type=exception
//# Any keyring that has access to the `Shared` cache MAY be able to use materials
//# that it MAY or MAY NOT have direct access to.
//#
//# Users MUST make sure that all of Partition ID, Logical Key Store Name of the Key Store for the Hierarchical Keyring
//# and Branch Key ID are set to be the same for two Hierarchical Keyrings if and only they want the keyrings to share
//# cache entries.