-
Notifications
You must be signed in to change notification settings - Fork 63
/
Copy pathraw_aes_keyring_browser.ts
288 lines (266 loc) · 10.3 KB
/
raw_aes_keyring_browser.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
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import {
KeyringWebCrypto,
needs,
WebCryptoEncryptionMaterial,
WebCryptoDecryptionMaterial,
EncryptedDataKey,
KeyringTraceFlag,
immutableClass,
readOnlyProperty,
WebCryptoAlgorithmSuite,
currySubtleFunction,
_importCryptoKey,
unwrapDataKey,
importForWebCryptoEncryptionMaterial,
importForWebCryptoDecryptionMaterial,
AwsEsdkJsCryptoKey,
} from '@aws-crypto/material-management-browser'
import { serializeFactory, concatBuffers } from '@aws-crypto/serialize'
import {
_onEncrypt,
_onDecrypt,
WebCryptoRawAesMaterial,
rawAesEncryptedDataKeyFactory,
rawAesEncryptedPartsFactory,
WrappingSuiteIdentifier,
WrapKey,
UnwrapKey,
} from '@aws-crypto/raw-keyring'
import { fromUtf8, toUtf8 } from '@aws-sdk/util-utf8-browser'
import { randomValuesOnly } from '@aws-crypto/random-source-browser'
import {
getWebCryptoBackend,
getNonZeroByteBackend,
} from '@aws-crypto/web-crypto-backend'
const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory(
toUtf8,
fromUtf8
)
const { rawAesEncryptedParts } = rawAesEncryptedPartsFactory(fromUtf8)
const encryptFlags =
KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY |
KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX
const decryptFlags =
KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY |
KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX
export type RawAesKeyringWebCryptoInput = {
keyNamespace: string
keyName: string
masterKey: AwsEsdkJsCryptoKey
wrappingSuite: WrappingSuiteIdentifier
utf8Sorting?: boolean
}
export class RawAesKeyringWebCrypto extends KeyringWebCrypto {
public declare keyNamespace: string
public declare keyName: string
declare _wrapKey: WrapKey<WebCryptoAlgorithmSuite>
declare _unwrapKey: UnwrapKey<WebCryptoAlgorithmSuite>
public declare _utf8Sorting: boolean
constructor(input: RawAesKeyringWebCryptoInput) {
super()
const { keyName, keyNamespace, masterKey, wrappingSuite, utf8Sorting } =
input
/* Precondition: AesKeyringWebCrypto needs identifying information for encrypt and decrypt. */
needs(keyName && keyNamespace, 'Identifying information must be defined.')
/* Precondition: RawAesKeyringWebCrypto requires a wrappingSuite to be a valid RawAesWrappingSuite. */
const wrappingMaterial = new WebCryptoRawAesMaterial(wrappingSuite)
/* Precondition: unencryptedMasterKey must correspond to the WebCryptoAlgorithmSuite specification.
* Note: the KeyringTrace and flag are _only_ set because I am reusing an existing implementation.
* See: raw_aes_material.ts in @aws-crypto/raw-keyring for details
*/
.setCryptoKey(masterKey, {
keyNamespace,
keyName,
flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY,
})
if (utf8Sorting === undefined) {
readOnlyProperty(this, '_utf8Sorting', false)
} else {
readOnlyProperty(this, '_utf8Sorting', utf8Sorting)
}
// default will be false
const { serializeEncryptionContext } = serializeFactory(fromUtf8, {
utf8Sorting: this._utf8Sorting,
})
const _wrapKey = async (material: WebCryptoEncryptionMaterial) => {
/* The AAD section is uInt16BE(length) + AAD
* see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad
* However, the RAW Keyring wants _only_ the ADD.
* So, I just slice off the length.
*/
const aad = serializeEncryptionContext(material.encryptionContext).slice(
2
)
const { keyNamespace, keyName } = this
return aesGcmWrapKey(
keyNamespace,
keyName,
material,
aad,
wrappingMaterial
)
}
const _unwrapKey = async (
material: WebCryptoDecryptionMaterial,
edk: EncryptedDataKey
) => {
/* The AAD section is uInt16BE(length) + AAD
* see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad
* However, the RAW Keyring wants _only_ the ADD.
* So, I just slice off the length.
*/
const aad = serializeEncryptionContext(material.encryptionContext).slice(
2
)
const { keyNamespace, keyName } = this
return aesGcmUnwrapKey(
keyNamespace,
keyName,
material,
wrappingMaterial,
edk,
aad
)
}
readOnlyProperty(this, 'keyName', keyName)
readOnlyProperty(this, 'keyNamespace', keyNamespace)
readOnlyProperty(this, '_wrapKey', _wrapKey)
readOnlyProperty(this, '_unwrapKey', _unwrapKey)
}
_filter({ providerId, providerInfo }: EncryptedDataKey) {
const { keyNamespace, keyName } = this
return providerId === keyNamespace && providerInfo.startsWith(keyName)
}
_rawOnEncrypt = _onEncrypt<WebCryptoAlgorithmSuite, RawAesKeyringWebCrypto>(
randomValuesOnly
)
_onEncrypt = async (material: WebCryptoEncryptionMaterial) => {
const _material = await this._rawOnEncrypt(material)
return importForWebCryptoEncryptionMaterial(_material)
}
/* onDecrypt does not need to import the crypto key, because this is handled in the unwrap operation
* Encrypt needs to have access to the unencrypted data key to encrypt with other keyrings
* but once I have functional material no other decrypt operations need to be performed.
*/
_onDecrypt = _onDecrypt<WebCryptoAlgorithmSuite, RawAesKeyringWebCrypto>()
static async importCryptoKey(
masterKey: Uint8Array,
wrappingSuite: WrappingSuiteIdentifier
): Promise<AwsEsdkJsCryptoKey> {
needs(masterKey instanceof Uint8Array, 'Unsupported master key type.')
const material = new WebCryptoRawAesMaterial(wrappingSuite)
/* Precondition: masterKey must correspond to the algorithm suite specification.
* Note: the KeyringTrace and flag are _only_ set because I am reusing an existing implementation.
* See: raw_aes_material.ts in @aws-crypto/raw-keyring for details
*/
.setUnencryptedDataKey(masterKey, {
keyNamespace: 'importOnly',
keyName: 'importOnly',
flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY,
})
return backendForRawAesMasterKey().then(async (backend) =>
_importCryptoKey(backend.subtle, material, ['encrypt', 'decrypt'])
)
}
}
immutableClass(RawAesKeyringWebCrypto)
/**
* Uses aes-gcm to encrypt the data key and return the passed WebCryptoEncryptionMaterial with
* an EncryptedDataKey added.
* @param keyNamespace [String] The keyring namespace (for KeyringTrace)
* @param keyName [String] The keyring name (for KeyringTrace and to extract the extra info stored in providerInfo)
* @param material [WebCryptoEncryptionMaterial] The target material to which the EncryptedDataKey will be added
* @param aad [Uint8Array] The serialized aad (EncryptionContext)
* @param wrappingMaterial [WebCryptoRawAesMaterial] The material used to decrypt the EncryptedDataKey
* @returns [WebCryptoEncryptionMaterial] Mutates and returns the same WebCryptoEncryptionMaterial that was passed but with an EncryptedDataKey added
*/
async function aesGcmWrapKey(
keyNamespace: string,
keyName: string,
material: WebCryptoEncryptionMaterial,
aad: Uint8Array,
wrappingMaterial: WebCryptoRawAesMaterial
): Promise<WebCryptoEncryptionMaterial> {
const backend = await backendForRawAesMasterKey()
const iv = await backend.randomValues(material.suite.ivLength)
const getSubtleInfo = currySubtleFunction(
wrappingMaterial,
backend,
'encrypt'
)
const info = new Uint8Array()
const { getSubtleEncrypt } = await getSubtleInfo(info)
const dataKey = unwrapDataKey(material.getUnencryptedDataKey())
const buffer = await getSubtleEncrypt(iv, aad)(dataKey)
const ciphertext = new Uint8Array(
buffer,
0,
buffer.byteLength - material.suite.tagLength / 8
)
const authTag = new Uint8Array(
buffer,
buffer.byteLength - material.suite.tagLength / 8
)
const edk = rawAesEncryptedDataKey(
keyNamespace,
keyName,
iv,
ciphertext,
authTag
)
return material.addEncryptedDataKey(edk, encryptFlags)
}
/**
* Uses aes-gcm to decrypt the encrypted data key and return the passed WebCryptoDecryptionMaterial with
* the unencrypted data key set.
* @param keyNamespace [String] The keyring namespace (for KeyringTrace)
* @param keyName [String] The keyring name (for KeyringTrace and to extract the extra info stored in providerInfo)
* @param material [WebCryptoDecryptionMaterial] The target material to which the decrypted data key will be added
* @param wrappingMaterial [WebCryptoRawAesMaterial] The material used to decrypt the EncryptedDataKey
* @param edk [EncryptedDataKey] The EncryptedDataKey on which to operate
* @param aad [Uint8Array] The serialized aad (EncryptionContext)
* @returns [WebCryptoDecryptionMaterial] Mutates and returns the same WebCryptoDecryptionMaterial that was passed but with the unencrypted data key set
*/
async function aesGcmUnwrapKey(
keyNamespace: string,
keyName: string,
material: WebCryptoDecryptionMaterial,
wrappingMaterial: WebCryptoRawAesMaterial,
edk: EncryptedDataKey,
aad: Uint8Array
): Promise<WebCryptoDecryptionMaterial> {
const { suite } = material
const { iv, ciphertext, authTag } = rawAesEncryptedParts(suite, keyName, edk)
const backend = await backendForRawAesMasterKey()
const getSubtleInfo = currySubtleFunction(
wrappingMaterial,
backend,
'decrypt'
)
const info = new Uint8Array()
const getSubtleDecrypt = await getSubtleInfo(info)
const buffer = await getSubtleDecrypt(
iv,
aad
)(concatBuffers(ciphertext, authTag))
const trace = { keyNamespace, keyName, flags: decryptFlags }
material.setUnencryptedDataKey(new Uint8Array(buffer), trace)
return importForWebCryptoDecryptionMaterial(material)
}
/**
* The master key can not be zero length.
* If the back end is mixed,
* to support both zero and non-zero byte AES-GCM operations,
* then the `NonZeroByteBackend` should be the native implementation.
* I assert that it should be slightly harder to exfiltrate
* from the native implementation than a JS implementation.
* So I *force* the master key to be stored in the native implementation **only**.
*/
async function backendForRawAesMasterKey() {
const backend = await getWebCryptoBackend()
const { randomValues } = backend
const subtle = getNonZeroByteBackend(backend)
return { randomValues, subtle }
}