-
Notifications
You must be signed in to change notification settings - Fork 63
/
Copy pathcryptographic_material.ts
943 lines (867 loc) · 34.7 KB
/
cryptographic_material.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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import {
MixedBackendCryptoKey,
SupportedAlgorithmSuites,
AwsEsdkJsCryptoKey,
AwsEsdkJsKeyUsage,
EncryptionContext,
AwsEsdkKeyObject,
AwsEsdkCreateSecretKey,
} from './types'
import { EncryptedDataKey } from './encrypted_data_key'
import { SignatureKey, VerificationKey } from './signature_key'
import { frozenClass, readOnlyProperty } from './immutable_class'
import { KeyringTrace, KeyringTraceFlag } from './keyring_trace'
import { NodeAlgorithmSuite } from './node_algorithms'
import { WebCryptoAlgorithmSuite } from './web_crypto_algorithms'
import { needs } from './needs'
import { validate, version } from 'uuid'
/* KeyObject were introduced in v11.
* They protect the data key better than a Buffer.
* Their use is preferred.
* When they are available, the AWS Encryption SDK will proscribe their use.
* See: https://nodejs.org/api/crypto.html#crypto_class_keyobject
*/
interface AwsEsdkKeyObjectInstanceOf {
new (): AwsEsdkKeyObject
}
type AwsEsdkCrypto = {
KeyObject: AwsEsdkKeyObjectInstanceOf
createSecretKey: AwsEsdkCreateSecretKey
}
export const supportsKeyObject = (function () {
try {
const { KeyObject, createSecretKey } = require('crypto') as AwsEsdkCrypto // eslint-disable-line @typescript-eslint/no-var-requires
if (!KeyObject || !createSecretKey) return false
return { KeyObject, createSecretKey }
} catch (ex) {
return false
}
})()
/*
* This public interface to the CryptographicMaterial object is provided for
* developers of CMMs and keyrings only. If you are a user of the AWS Encryption
* SDK and you are not developing your own CMMs and/or keyrings, you do not
* need to use it and you should not do so.
*
* The CryptographicMaterial's purpose is to bind together all the required elements for
* encrypting or decrypting a payload.
* The functional data key (unencrypted or CryptoKey) is the most sensitive data and needs to
* be protected. The longer this data persists in memory the
* greater the opportunity to be invalidated. Because
* a Caching CMM exists it is important to ensure that the
* unencrypted data key and its meta data can not be manipulated,
* and that the unencrypted data key can be zeroed when
* it is no longer needed.
*/
const timingSafeEqual: (a: Uint8Array, b: Uint8Array) => boolean =
(function () {
try {
/* It is possible for `require` to return an empty object, or an object
* that does not implement `timingSafeEqual`.
* in this case I need a fallback
*/
const { timingSafeEqual: nodeTimingSafeEqual } = require('crypto') // eslint-disable-line @typescript-eslint/no-var-requires
return nodeTimingSafeEqual || portableTimingSafeEqual
} catch (e) {
return portableTimingSafeEqual
}
/* https://codahale.com/a-lesson-in-timing-attacks/ */
function portableTimingSafeEqual(a: Uint8Array, b: Uint8Array) {
/* It is *possible* that a runtime could optimize this constant time function.
* Adding `eval` could prevent the optimization, but this is no guarantee.
* The eval below is commented out
* because if a browser is using a Content Security Policy with `'unsafe-eval'`
* it would fail on this eval.
* The value in attempting to ensure that this function is not optimized
* is not worth the cost of making customers allow `'unsafe-eval'`.
* If you want to copy this function for your own use,
* please review the timing-attack link above.
* Side channel attacks are pernicious and subtle.
*/
// eval('') // eslint-disable-line no-eval
/* Check for early return (Postcondition) UNTESTED: Size is well-know information
* and does not leak information about contents.
*/
if (a.byteLength !== b.byteLength) return false
let diff = 0
for (let i = 0; i < b.length; i++) {
diff |= a[i] ^ b[i]
}
return diff === 0
}
})()
export interface FunctionalCryptographicMaterial {
hasValidKey: () => boolean
}
export interface CryptographicMaterial<T extends CryptographicMaterial<T>> {
suite: SupportedAlgorithmSuites
setUnencryptedDataKey: (
dataKey: Uint8Array | AwsEsdkKeyObject,
trace: KeyringTrace
) => T
getUnencryptedDataKey: () => Uint8Array | AwsEsdkKeyObject
zeroUnencryptedDataKey: () => T
hasUnencryptedDataKey: boolean
keyringTrace: KeyringTrace[]
encryptionContext: Readonly<EncryptionContext>
}
//= aws-encryption-sdk-specification/framework/structures.md#structure-3
//# This structure MUST include all of the following fields:
//#
//# - [Branch Key](#branch-key)
//# - [Branch Key Id](#branch-key-id)
//# - [Branch Key Version](#branch-key-version)
//# - [Encryption Context](#encryption-context-3)
// structure is based on https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographicMaterialProviders/dafny/AwsCryptographyKeyStore/Model/KeyStore.smithy#L323
export interface BranchKeyMaterial {
branchKey(): Readonly<Buffer>
branchKeyIdentifier: string
branchKeyVersion: Readonly<Buffer>
encryptionContext: Readonly<EncryptionContext>
}
export class NodeBranchKeyMaterial implements BranchKeyMaterial {
// all attributes are readonly so they are accessible outside the class but
// they cannot be modified
// since all fields are objects, keep them immutable from external changes via
// shared memory
// we want the branch key to be mutable within the class but immutable outside
// the class
private _branchKey: Buffer
declare readonly branchKeyIdentifier: string
declare readonly branchKeyVersion: Readonly<Buffer>
declare readonly encryptionContext: Readonly<EncryptionContext>
constructor(
branchKey: Buffer,
branchKeyIdentifier: string,
branchKeyVersion: string,
encryptionContext: EncryptionContext
) {
/* Precondition: Branch key must be a Buffer */
needs(branchKey instanceof Buffer, 'Branch key must be a Buffer')
/* Precondition: Branch key id must be a string */
needs(
typeof branchKeyIdentifier === 'string',
'Branch key id must be a string'
)
/* Precondition: Branch key version must be a string */
needs(
typeof branchKeyVersion === 'string',
'Branch key version must be a string'
)
/* Precondition: encryptionContext must be an object, even if it is empty */
needs(
encryptionContext && typeof encryptionContext === 'object',
'Encryption context must be an object'
)
/* Precondition: branchKey must be a 32 byte-long buffer */
needs(branchKey.length === 32, 'Branch key must be 32 bytes long')
/* Precondition: branch key ID is required */
needs(branchKeyIdentifier, 'Empty branch key ID')
/* Precondition: branch key version must be valid version 4 uuid */
needs(
validate(branchKeyVersion) && version(branchKeyVersion) === 4,
'Branch key version must be valid version 4 uuid'
)
/* Postcondition: branch key is immutable */
this._branchKey = Buffer.from(branchKey)
/* Postconditon: encryption context is immutable */
this.encryptionContext = Object.freeze({ ...encryptionContext })
this.branchKeyIdentifier = branchKeyIdentifier
this.branchKeyVersion = Buffer.from(branchKeyVersion, 'utf-8')
Object.setPrototypeOf(this, NodeBranchKeyMaterial.prototype)
/* Postcondition: instance is frozen */
// preventing any modifications to its properties or methods.
Object.freeze(this)
}
// makes the branch key public to users wrapped in immutable access
branchKey(): Readonly<Buffer> {
return this._branchKey
}
// this capability is not required of branch key materials according to the
// specification. Using this is a good security practice so that the data
// key's bytes are not preserved even in free memory
zeroUnencryptedDataKey(): BranchKeyMaterial {
this._branchKey.fill(0)
return this
}
}
// make the class immutable
frozenClass(NodeBranchKeyMaterial)
export interface EncryptionMaterial<T extends CryptographicMaterial<T>>
extends CryptographicMaterial<T> {
encryptedDataKeys: EncryptedDataKey[]
addEncryptedDataKey: (edk: EncryptedDataKey, flags: KeyringTraceFlag) => T
setSignatureKey: (key: SignatureKey) => T
signatureKey?: SignatureKey
}
export interface DecryptionMaterial<T extends CryptographicMaterial<T>>
extends CryptographicMaterial<T> {
setVerificationKey: (key: VerificationKey) => T
verificationKey?: VerificationKey
}
export interface WebCryptoMaterial<T extends CryptographicMaterial<T>>
extends CryptographicMaterial<T> {
setCryptoKey: (
dataKey: AwsEsdkJsCryptoKey | MixedBackendCryptoKey,
trace: KeyringTrace
) => T
getCryptoKey: () => AwsEsdkJsCryptoKey | MixedBackendCryptoKey
hasCryptoKey: boolean
validUsages: ReadonlyArray<AwsEsdkJsKeyUsage>
}
export class NodeEncryptionMaterial
implements
Readonly<EncryptionMaterial<NodeEncryptionMaterial>>,
FunctionalCryptographicMaterial
{
suite: NodeAlgorithmSuite
setUnencryptedDataKey!: (
dataKey: Uint8Array | AwsEsdkKeyObject,
trace: KeyringTrace
) => NodeEncryptionMaterial
getUnencryptedDataKey!: () => Uint8Array | AwsEsdkKeyObject
zeroUnencryptedDataKey!: () => NodeEncryptionMaterial
hasUnencryptedDataKey!: boolean
keyringTrace: KeyringTrace[] = []
encryptedDataKeys!: EncryptedDataKey[]
addEncryptedDataKey!: (
edk: EncryptedDataKey,
flags: KeyringTraceFlag
) => NodeEncryptionMaterial
setSignatureKey!: (key: SignatureKey) => NodeEncryptionMaterial
signatureKey?: SignatureKey
encryptionContext: Readonly<EncryptionContext>
constructor(suite: NodeAlgorithmSuite, encryptionContext: EncryptionContext) {
/* Precondition: NodeEncryptionMaterial suite must be NodeAlgorithmSuite. */
needs(
suite instanceof NodeAlgorithmSuite,
'Suite must be a NodeAlgorithmSuite'
)
this.suite = suite
/* Precondition: NodeEncryptionMaterial encryptionContext must be an object, even if it is empty. */
needs(
encryptionContext && typeof encryptionContext === 'object',
'Encryption context must be set'
)
this.encryptionContext = Object.freeze({ ...encryptionContext })
// EncryptionMaterial have generated a data key on setUnencryptedDataKey
const setFlags = KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY
decorateCryptographicMaterial<NodeEncryptionMaterial>(this, setFlags)
decorateEncryptionMaterial<NodeEncryptionMaterial>(this)
Object.setPrototypeOf(this, NodeEncryptionMaterial.prototype)
Object.freeze(this)
}
hasValidKey() {
return this.hasUnencryptedDataKey
}
}
frozenClass(NodeEncryptionMaterial)
export class NodeDecryptionMaterial
implements
Readonly<DecryptionMaterial<NodeDecryptionMaterial>>,
FunctionalCryptographicMaterial
{
suite: NodeAlgorithmSuite
setUnencryptedDataKey!: (
dataKey: Uint8Array | AwsEsdkKeyObject,
trace: KeyringTrace
) => NodeDecryptionMaterial
getUnencryptedDataKey!: () => Uint8Array | AwsEsdkKeyObject
zeroUnencryptedDataKey!: () => NodeDecryptionMaterial
hasUnencryptedDataKey!: boolean
keyringTrace: KeyringTrace[] = []
setVerificationKey!: (key: VerificationKey) => NodeDecryptionMaterial
verificationKey?: VerificationKey
encryptionContext: Readonly<EncryptionContext>
constructor(suite: NodeAlgorithmSuite, encryptionContext: EncryptionContext) {
/* Precondition: NodeDecryptionMaterial suite must be NodeAlgorithmSuite. */
needs(
suite instanceof NodeAlgorithmSuite,
'Suite must be a NodeAlgorithmSuite'
)
this.suite = suite
/* Precondition: NodeDecryptionMaterial encryptionContext must be an object, even if it is empty. */
needs(
encryptionContext && typeof encryptionContext === 'object',
'Encryption context must be set'
)
this.encryptionContext = Object.freeze({ ...encryptionContext })
// DecryptionMaterial have decrypted a data key on setUnencryptedDataKey
const setFlags = KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY
decorateCryptographicMaterial<NodeDecryptionMaterial>(this, setFlags)
decorateDecryptionMaterial<NodeDecryptionMaterial>(this)
Object.setPrototypeOf(this, NodeDecryptionMaterial.prototype)
Object.freeze(this)
}
hasValidKey() {
return this.hasUnencryptedDataKey
}
}
frozenClass(NodeDecryptionMaterial)
export class WebCryptoEncryptionMaterial
implements
Readonly<EncryptionMaterial<WebCryptoEncryptionMaterial>>,
Readonly<WebCryptoMaterial<WebCryptoEncryptionMaterial>>,
FunctionalCryptographicMaterial
{
suite: WebCryptoAlgorithmSuite
setUnencryptedDataKey!: (
dataKey: Uint8Array | AwsEsdkKeyObject,
trace: KeyringTrace
) => WebCryptoEncryptionMaterial
getUnencryptedDataKey!: () => Uint8Array | AwsEsdkKeyObject
zeroUnencryptedDataKey!: () => WebCryptoEncryptionMaterial
hasUnencryptedDataKey!: boolean
keyringTrace: KeyringTrace[] = []
encryptedDataKeys!: EncryptedDataKey[]
addEncryptedDataKey!: (
edk: EncryptedDataKey,
flags: KeyringTraceFlag
) => WebCryptoEncryptionMaterial
setSignatureKey!: (key: SignatureKey) => WebCryptoEncryptionMaterial
signatureKey?: SignatureKey
setCryptoKey!: (
dataKey: AwsEsdkJsCryptoKey | MixedBackendCryptoKey,
trace: KeyringTrace
) => WebCryptoEncryptionMaterial
getCryptoKey!: () => AwsEsdkJsCryptoKey | MixedBackendCryptoKey
hasCryptoKey!: boolean
validUsages: ReadonlyArray<AwsEsdkJsKeyUsage>
encryptionContext: Readonly<EncryptionContext>
constructor(
suite: WebCryptoAlgorithmSuite,
encryptionContext: EncryptionContext
) {
/* Precondition: WebCryptoEncryptionMaterial suite must be WebCryptoAlgorithmSuite. */
needs(
suite instanceof WebCryptoAlgorithmSuite,
'Suite must be a WebCryptoAlgorithmSuite'
)
this.suite = suite
this.validUsages = Object.freeze([
'deriveKey',
'encrypt',
] as AwsEsdkJsKeyUsage[])
/* Precondition: WebCryptoEncryptionMaterial encryptionContext must be an object, even if it is empty. */
needs(
encryptionContext && typeof encryptionContext === 'object',
'Encryption context must be set'
)
this.encryptionContext = Object.freeze({ ...encryptionContext })
// EncryptionMaterial have generated a data key on setUnencryptedDataKey
const setFlag = KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY
decorateCryptographicMaterial<WebCryptoEncryptionMaterial>(this, setFlag)
decorateEncryptionMaterial<WebCryptoEncryptionMaterial>(this)
decorateWebCryptoMaterial<WebCryptoEncryptionMaterial>(this, setFlag)
Object.setPrototypeOf(this, WebCryptoEncryptionMaterial.prototype)
Object.freeze(this)
}
hasValidKey() {
return this.hasUnencryptedDataKey && this.hasCryptoKey
}
}
frozenClass(WebCryptoEncryptionMaterial)
export class WebCryptoDecryptionMaterial
implements
Readonly<DecryptionMaterial<WebCryptoDecryptionMaterial>>,
Readonly<WebCryptoMaterial<WebCryptoDecryptionMaterial>>,
FunctionalCryptographicMaterial
{
suite: WebCryptoAlgorithmSuite
setUnencryptedDataKey!: (
dataKey: Uint8Array | AwsEsdkKeyObject,
trace: KeyringTrace
) => WebCryptoDecryptionMaterial
getUnencryptedDataKey!: () => Uint8Array | AwsEsdkKeyObject
zeroUnencryptedDataKey!: () => WebCryptoDecryptionMaterial
hasUnencryptedDataKey!: boolean
keyringTrace: KeyringTrace[] = []
setVerificationKey!: (key: VerificationKey) => WebCryptoDecryptionMaterial
verificationKey?: VerificationKey
setCryptoKey!: (
dataKey: AwsEsdkJsCryptoKey | MixedBackendCryptoKey,
trace: KeyringTrace
) => WebCryptoDecryptionMaterial
getCryptoKey!: () => AwsEsdkJsCryptoKey | MixedBackendCryptoKey
hasCryptoKey!: boolean
validUsages: ReadonlyArray<AwsEsdkJsKeyUsage>
encryptionContext: Readonly<EncryptionContext>
constructor(
suite: WebCryptoAlgorithmSuite,
encryptionContext: EncryptionContext
) {
/* Precondition: WebCryptoDecryptionMaterial suite must be WebCryptoAlgorithmSuite. */
needs(
suite instanceof WebCryptoAlgorithmSuite,
'Suite must be a WebCryptoAlgorithmSuite'
)
this.suite = suite
this.validUsages = Object.freeze([
'deriveKey',
'decrypt',
] as AwsEsdkJsKeyUsage[])
/* Precondition: WebCryptoDecryptionMaterial encryptionContext must be an object, even if it is empty. */
needs(
encryptionContext && typeof encryptionContext === 'object',
'Encryption context must be set'
)
this.encryptionContext = Object.freeze({ ...encryptionContext })
// DecryptionMaterial have decrypted a data key on setUnencryptedDataKey
const setFlag = KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY
decorateCryptographicMaterial<WebCryptoDecryptionMaterial>(this, setFlag)
decorateDecryptionMaterial<WebCryptoDecryptionMaterial>(this)
decorateWebCryptoMaterial<WebCryptoDecryptionMaterial>(this, setFlag)
Object.setPrototypeOf(this, WebCryptoDecryptionMaterial.prototype)
Object.freeze(this)
}
hasValidKey() {
return this.hasCryptoKey
}
}
frozenClass(WebCryptoDecryptionMaterial)
export function isEncryptionMaterial(
obj: any
): obj is WebCryptoEncryptionMaterial | NodeEncryptionMaterial {
return (
obj instanceof WebCryptoEncryptionMaterial ||
obj instanceof NodeEncryptionMaterial
)
}
export function isDecryptionMaterial(
obj: any
): obj is WebCryptoDecryptionMaterial | NodeDecryptionMaterial {
return (
obj instanceof WebCryptoDecryptionMaterial ||
obj instanceof NodeDecryptionMaterial
)
}
export function isBranchKeyMaterial(obj: any): obj is NodeBranchKeyMaterial {
return obj instanceof NodeBranchKeyMaterial
}
export function decorateCryptographicMaterial<
T extends CryptographicMaterial<T>
>(material: T, setFlag: KeyringTraceFlag) {
/* Precondition: setFlag must be in the set of KeyringTraceFlag.SET_FLAGS. */
needs(setFlag & KeyringTraceFlag.SET_FLAGS, 'Invalid setFlag')
/* When a KeyringTraceFlag is passed to setUnencryptedDataKey,
* it must be valid for the type of material.
* It is invalid to claim that EncryptionMaterial were decrypted.
*/
const deniedSetFlags =
(KeyringTraceFlag.SET_FLAGS ^ setFlag) |
(setFlag === KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY
? KeyringTraceFlag.DECRYPT_FLAGS
: setFlag === KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY
? KeyringTraceFlag.ENCRYPT_FLAGS
: 0)
let unencryptedDataKeyZeroed = false
let unencryptedDataKey: AwsEsdkKeyObject | Uint8Array
// This copy of the unencryptedDataKey is stored to insure that the
// unencrypted data key is *never* modified. Since the
// unencryptedDataKey is returned by reference, any change
// to it would be propagated to any cached versions.
let udkForVerification: Uint8Array
const setUnencryptedDataKey = (
dataKey: Uint8Array | AwsEsdkKeyObject,
trace: KeyringTrace
) => {
/* Avoid making unnecessary copies of the dataKey. */
const tempUdk =
dataKey instanceof Uint8Array ? dataKey : unwrapDataKey(dataKey)
/* All security conditions are tested here and failures will throw. */
verifyUnencryptedDataKeyForSet(tempUdk, trace)
unencryptedDataKey = wrapWithKeyObjectIfSupported(dataKey)
udkForVerification = new Uint8Array(tempUdk)
material.keyringTrace.push(trace)
return material
}
const getUnencryptedDataKey = (): Uint8Array | AwsEsdkKeyObject => {
/* Precondition: unencryptedDataKey must be set before we can return it. */
needs(unencryptedDataKey, 'unencryptedDataKey has not been set')
/* Precondition: unencryptedDataKey must not be Zeroed out.
* Returning a null key would be incredibly bad.
*/
needs(!unencryptedDataKeyZeroed, 'unencryptedDataKey has been zeroed.')
/* Precondition: The unencryptedDataKey must not have been modified.
* If the unencryptedDataKey is a KeyObject,
* then the security around modification is handled in C.
* Do not duplicate the secret just to check...
*/
needs(
!(unencryptedDataKey instanceof Uint8Array) ||
timingSafeEqual(udkForVerification, unwrapDataKey(unencryptedDataKey)),
'unencryptedDataKey has been corrupted.'
)
return unencryptedDataKey
}
Object.defineProperty(material, 'hasUnencryptedDataKey', {
// Check that we have both not zeroed AND that we have not set
get: () => !!unencryptedDataKey && !unencryptedDataKeyZeroed,
enumerable: true,
})
const zeroUnencryptedDataKey = () => {
/* These checks are separated on purpose. It should be impossible to have only one unset.
* *But* if it was the case, I *must* make sure I zero out the set one, and not leave it up to GC.
* If I only checked on say unencryptedDataKey, and udkForVerification was somehow set,
* doing the simplest thing would be to set both to new Uint8Array.
* Leaving udkForVerification to be garbage collected.
* This level of insanity is due to the fact that we are dealing with the unencrypted data key.
*/
let unsetCount = 0
/* Precondition: If the unencryptedDataKey has not been set, it should not be settable later. */
if (!unencryptedDataKey) {
unencryptedDataKey = new Uint8Array()
unsetCount += 1
}
/* Precondition: If the udkForVerification has not been set, it should not be settable later. */
if (!udkForVerification) {
udkForVerification = new Uint8Array()
unsetCount += 1
}
/* The KeyObject manages its own ref counter.
* Once there are no more users, it will clean the memory.
*/
if (!(unencryptedDataKey instanceof Uint8Array)) {
unencryptedDataKey = new Uint8Array()
}
unencryptedDataKey.fill(0)
udkForVerification.fill(0)
unencryptedDataKeyZeroed = true
/* Postcondition UNTESTED: Both unencryptedDataKey and udkForVerification must be either set or unset.
* If it is ever the case that only one was unset, then something is wrong in a profound way.
* It is not clear how this could ever happen, unless someone is manipulating the OS...
*/
needs(
unsetCount === 0 || unsetCount === 2,
'Either unencryptedDataKey or udkForVerification was not set.'
)
return material
}
readOnlyProperty(material, 'setUnencryptedDataKey', setUnencryptedDataKey)
readOnlyProperty(material, 'getUnencryptedDataKey', getUnencryptedDataKey)
readOnlyProperty(material, 'zeroUnencryptedDataKey', zeroUnencryptedDataKey)
return material
function verifyUnencryptedDataKeyForSet(
dataKey: Uint8Array,
trace: KeyringTrace
) {
/* Precondition: unencryptedDataKey must not be set. Modifying the unencryptedDataKey is denied */
needs(!unencryptedDataKey, 'unencryptedDataKey has already been set')
/* Precondition: dataKey must be Binary Data */
needs(dataKey instanceof Uint8Array, 'dataKey must be a Uint8Array')
/* Precondition: dataKey should have an ArrayBuffer that *only* stores the key.
* This is a simple check to make sure that the key is not stored on
* a large potentially shared ArrayBuffer.
* If this was the case, it may be possible to find or manipulate.
*/
needs(
dataKey.byteOffset === 0,
'Unencrypted Master Key must be an isolated buffer.'
)
/* Precondition: The data key length must agree with algorithm specification.
* If this is not the case, it either means ciphertext was tampered
* with or the keyring implementation is not setting the length properly.
*/
needs(
dataKey.byteLength === material.suite.keyLengthBytes,
'Key length does not agree with the algorithm specification.'
)
/* Precondition: Trace must be set, and the flag must indicate that the data key was generated. */
needs(
trace && trace.keyName && trace.keyNamespace,
'Malformed KeyringTrace'
)
/* Precondition: On set the required KeyringTraceFlag must be set. */
needs(trace.flags & setFlag, 'Required KeyringTraceFlag not set')
/* Precondition: Only valid flags are allowed.
* An unencrypted data key can not be both generated and decrypted.
*/
needs(!(trace.flags & deniedSetFlags), 'Invalid KeyringTraceFlags set.')
}
}
export function decorateEncryptionMaterial<T extends EncryptionMaterial<T>>(
material: T
) {
const deniedEncryptFlags =
KeyringTraceFlag.SET_FLAGS | KeyringTraceFlag.DECRYPT_FLAGS
const encryptedDataKeys: EncryptedDataKey[] = []
let signatureKey: Readonly<SignatureKey> | undefined
const addEncryptedDataKey = (
edk: EncryptedDataKey,
flags: KeyringTraceFlag
) => {
/* Precondition: If a data key has not already been generated, there must be no EDKs.
* Pushing EDKs on the list before the data key has been generated may cause the list of
* EDKs to be inconsistent. (i.e., they would decrypt to different data keys.)
*/
needs(material.hasUnencryptedDataKey, 'Unencrypted data key not set.')
/* Precondition: Edk must be EncryptedDataKey
* Putting things onto the list that are not EncryptedDataKey
* may cause the list of EDKs to be inconsistent. (i.e. they may not serialize, or be mutable)
*/
needs(
edk instanceof EncryptedDataKey,
'Unsupported instance of encryptedDataKey'
)
/* Precondition: flags must indicate that the key was encrypted. */
needs(
flags & KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY,
'Encrypted data key flag must be set.'
)
/* Precondition: flags must not include a setFlag or a decrypt flag.
* The setFlag is reserved for setting the unencrypted data key
* and must only occur once in the set of KeyringTrace flags.
* The two setFlags in use are:
* KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY
* KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY
*
* KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX is reserved for the decrypt path
*/
needs(!(flags & deniedEncryptFlags), 'Invalid flag for EncryptedDataKey.')
material.keyringTrace.push({
keyName: edk.providerInfo,
keyNamespace: edk.providerId,
flags,
})
encryptedDataKeys.push(edk)
return material
}
readOnlyProperty(material, 'addEncryptedDataKey', addEncryptedDataKey)
Object.defineProperty(material, 'encryptedDataKeys', {
// I only want EDKs added through addEncryptedDataKey
// so I return a new array
get: () => [...encryptedDataKeys],
enumerable: true,
})
const setSignatureKey = (key: SignatureKey) => {
/* Precondition: The SignatureKey stored must agree with the algorithm specification.
* If this is not the case it means the MaterialManager or Keyring is not setting
* the SignatureKey correctly
*/
needs(
material.suite.signatureCurve,
'Algorithm specification does not support signatures.'
)
/* Precondition: signatureKey must not be set. Modifying the signatureKey is denied. */
needs(!signatureKey, 'Signature key has already been set.')
/* Precondition: key must be a SignatureKey. */
needs(key instanceof SignatureKey, 'Unsupported instance of key')
signatureKey = key
return material
}
readOnlyProperty(material, 'setSignatureKey', setSignatureKey)
Object.defineProperty(material, 'signatureKey', {
get: () => {
/* Precondition: The SignatureKey requested must agree with the algorithm specification.
* If this is not the case it means the MaterialManager or Keyring is not setting
* the SignatureKey correctly
*/
needs(
!!material.suite.signatureCurve === !!signatureKey,
'Algorithm specification not satisfied.'
)
return signatureKey
},
enumerable: true,
})
return material
}
export function decorateDecryptionMaterial<T extends DecryptionMaterial<T>>(
material: T
) {
// Verification Key
let verificationKey: Readonly<VerificationKey> | undefined
const setVerificationKey = (key: VerificationKey) => {
/* Precondition: The VerificationKey stored must agree with the algorithm specification.
* If this is not the case it means the MaterialManager or Keyring is not setting
* the VerificationKey correctly
*/
needs(
material.suite.signatureCurve,
'Algorithm specification does not support signatures.'
)
/* Precondition: verificationKey must not be set. Modifying the verificationKey is denied. */
needs(!verificationKey, 'Verification key has already been set.')
/* Precondition: key must be a VerificationKey. */
needs(key instanceof VerificationKey, 'Unsupported instance of key')
verificationKey = key
return material
}
readOnlyProperty(material, 'setVerificationKey', setVerificationKey)
Object.defineProperty(material, 'verificationKey', {
get: () => {
/* Precondition: The VerificationKey requested must agree with the algorithm specification.
* If this is not the case it means the MaterialManager or Keyring is not setting
* the VerificationKey correctly
*/
needs(
!!material.suite.signatureCurve === !!verificationKey,
'Algorithm specification not satisfied.'
)
return verificationKey
},
enumerable: true,
})
return material
}
export function decorateWebCryptoMaterial<T extends WebCryptoMaterial<T>>(
material: T,
setFlags: KeyringTraceFlag
) {
let cryptoKey:
| Readonly<AwsEsdkJsCryptoKey | MixedBackendCryptoKey>
| undefined
const setCryptoKey = (
dataKey: AwsEsdkJsCryptoKey | MixedBackendCryptoKey,
trace: KeyringTrace
) => {
/* Precondition: cryptoKey must not be set. Modifying the cryptoKey is denied */
needs(!cryptoKey, 'cryptoKey is already set.')
/* Precondition: dataKey must be a supported type. */
needs(
isCryptoKey(dataKey) || isMixedBackendCryptoKey(dataKey),
'Unsupported dataKey type.'
)
/* Precondition: The CryptoKey must match the algorithm suite specification. */
needs(
isValidCryptoKey(dataKey, material),
'CryptoKey settings not acceptable.'
)
/* If the material does not have an unencrypted data key,
* then we are setting the crypto key here and need a keyring trace .
*/
if (!material.hasUnencryptedDataKey) {
/* Precondition: If the CryptoKey is the only version, the trace information must be set here. */
needs(
trace && trace.keyName && trace.keyNamespace,
'Malformed KeyringTrace'
)
/* Precondition: On setting the CryptoKey the required KeyringTraceFlag must be set. */
needs(trace.flags & setFlags, 'Required KeyringTraceFlag not set')
/* If I a setting a cryptoKey without an unencrypted data key,
* an unencrypted data should never be set.
* The expectation is if you are setting the cryptoKey *first* then
* the unencrypted data key has already been "handled".
* This ensures that a cryptoKey and an unencrypted data key always match.
*/
material.zeroUnencryptedDataKey()
material.keyringTrace.push(trace)
}
if (isCryptoKey(dataKey)) {
cryptoKey = dataKey
} else {
const { zeroByteCryptoKey, nonZeroByteCryptoKey } = dataKey
cryptoKey = Object.freeze({ zeroByteCryptoKey, nonZeroByteCryptoKey })
}
return material
}
readOnlyProperty(material, 'setCryptoKey', setCryptoKey)
const getCryptoKey = () => {
/* Precondition: The cryptoKey must be set before we can return it. */
needs(cryptoKey, 'Crypto key is not set.')
// In the case of MixedBackendCryptoKey the object
// has already been frozen above so it is safe to return
return cryptoKey as Readonly<AwsEsdkJsCryptoKey | MixedBackendCryptoKey>
}
readOnlyProperty(material, 'getCryptoKey', getCryptoKey)
Object.defineProperty(material, 'hasCryptoKey', {
get: () => !!cryptoKey,
enumerable: true,
})
return material
}
export function isCryptoKey(dataKey: any): dataKey is AwsEsdkJsCryptoKey {
return (
dataKey &&
'algorithm' in dataKey &&
'type' in dataKey &&
'usages' in dataKey &&
'extractable' in dataKey
)
}
export function isValidCryptoKey<T extends WebCryptoMaterial<T>>(
dataKey: AwsEsdkJsCryptoKey | MixedBackendCryptoKey,
material: T
): boolean {
if (!isCryptoKey(dataKey)) {
const { zeroByteCryptoKey, nonZeroByteCryptoKey } = dataKey
return (
isValidCryptoKey(zeroByteCryptoKey, material) &&
isValidCryptoKey(nonZeroByteCryptoKey, material)
)
}
const { suite, validUsages } = material
const { encryption, keyLength, kdf } = suite
/* See:
* https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey
* https://developer.mozilla.org/en-US/docs/Web/API/AesKeyGenParams
*/
const { type, algorithm, usages, extractable } = dataKey
// @ts-ignore length is an optional value...
const { name, length } = algorithm
/* MSRCrypto, for legacy reasons,
* normalizes the algorithm name
* to lower case.
* https://github.com/microsoft/MSR-JavaScript-Crypto/issues/1
* For now, I'm going to upper case the name.
*/
// Only symmetric algorithms
return (
type === 'secret' &&
// Must match the suite
((kdf && name.toUpperCase() === kdf) ||
(name.toUpperCase() === encryption && length === keyLength)) &&
/* Only valid usage are: encrypt|decrypt|deriveKey
* The complexity between deriveKey and suite.kdf should be handled in the Material class.
*/
usages.some((u) => validUsages.includes(u)) &&
// Since CryptoKey can not be zeroized, not extractable is the next best thing
!extractable
)
}
function isMixedBackendCryptoKey(
dataKey: any
): dataKey is MixedBackendCryptoKey {
const { zeroByteCryptoKey, nonZeroByteCryptoKey } = dataKey
return isCryptoKey(zeroByteCryptoKey) && isCryptoKey(nonZeroByteCryptoKey)
}
export function keyUsageForMaterial<T extends WebCryptoMaterial<T>>(
material: T
): AwsEsdkJsKeyUsage {
const { suite } = material
if (suite.kdf) return 'deriveKey'
return subtleFunctionForMaterial(material)
}
export function subtleFunctionForMaterial<T extends WebCryptoMaterial<T>>(
material: T
) {
if (material instanceof WebCryptoEncryptionMaterial) return 'encrypt'
if (material instanceof WebCryptoDecryptionMaterial) return 'decrypt'
throw new Error('Unsupported material')
}
export function unwrapDataKey(
dataKey: Uint8Array | AwsEsdkKeyObject
): Uint8Array {
if (dataKey instanceof Uint8Array) return dataKey
if (supportsKeyObject && dataKey instanceof supportsKeyObject.KeyObject)
return dataKey.export()
throw new Error('Unsupported dataKey type')
}
export function wrapWithKeyObjectIfSupported(
dataKey: Uint8Array | AwsEsdkKeyObject
): Uint8Array | AwsEsdkKeyObject {
if (supportsKeyObject) {
if (dataKey instanceof Uint8Array) {
const ko = supportsKeyObject.createSecretKey(dataKey)
/* Postcondition: Zero the secret. It is now inside the KeyObject. */
dataKey.fill(0)
return ko
}
if (dataKey instanceof supportsKeyObject.KeyObject) return dataKey
} else if (dataKey instanceof Uint8Array) {
return dataKey
}
throw new Error('Unsupported dataKey type')
}