Skip to content

Commit be914b7

Browse files
committed
feat(cryptographic-materials-cache): add support for branch key materials (#596)
* support branch key materials support branch key materials reinstall uuidv4 * reinstall uuidv4 within specific modules * install util package * uninstall uuidv4 package from code that may run in browser runtimes * generate uuid v4's using uuid package instead of uuidv4 * manually validate uuid v4's * install uuid package * remove uuidv4 regex validation * remove version lowercasing * add tests for v3 & v5
1 parent f6947a8 commit be914b7

10 files changed

+404
-6
lines changed

modules/cache-material/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"@aws-crypto/serialize": "file:../serialize",
2323
"@types/lru-cache": "^5.1.0",
2424
"lru-cache": "^6.0.0",
25-
"tslib": "^2.2.0"
25+
"tslib": "^2.2.0",
26+
"util": "^0.12.5"
2627
},
2728
"sideEffects": false,
2829
"main": "./build/main/src/index.js",

modules/cache-material/src/cryptographic_materials_cache.ts

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
EncryptionMaterial,
66
DecryptionMaterial,
77
SupportedAlgorithmSuites,
8+
BranchKeyMaterial,
89
} from '@aws-crypto/material-management'
910

1011
export interface CryptographicMaterialsCache<
@@ -16,16 +17,30 @@ export interface CryptographicMaterialsCache<
1617
plaintextLength: number,
1718
maxAge?: number
1819
): void
20+
1921
putDecryptionMaterial(
2022
key: string,
2123
response: DecryptionMaterial<S>,
2224
maxAge?: number
2325
): void
26+
27+
// a put operation to support branch key material
28+
putBranchKeyMaterial(
29+
key: string,
30+
response: BranchKeyMaterial,
31+
maxAge?: number
32+
): void
33+
2434
getEncryptionMaterial(
2535
key: string,
2636
plaintextLength: number
2737
): EncryptionMaterialEntry<S> | false
38+
2839
getDecryptionMaterial(key: string): DecryptionMaterialEntry<S> | false
40+
41+
// a get operation to support branch key material
42+
getBranchKeyMaterial(key: string): BranchKeyMaterialEntry | false
43+
2944
del(key: string): void
3045
}
3146

@@ -45,3 +60,7 @@ export interface DecryptionMaterialEntry<S extends SupportedAlgorithmSuites>
4560
extends Entry<S> {
4661
readonly response: DecryptionMaterial<S>
4762
}
63+
64+
export interface BranchKeyMaterialEntry {
65+
readonly response: BranchKeyMaterial
66+
}

modules/cache-material/src/get_local_cryptographic_materials_cache.ts

+43-3
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,31 @@ import {
99
needs,
1010
isEncryptionMaterial,
1111
isDecryptionMaterial,
12+
BranchKeyMaterial,
13+
isBranchKeyMaterial,
1214
} from '@aws-crypto/material-management'
1315

1416
import {
1517
CryptographicMaterialsCache,
1618
Entry,
1719
EncryptionMaterialEntry,
1820
DecryptionMaterialEntry,
21+
BranchKeyMaterialEntry,
1922
} from './cryptographic_materials_cache'
2023

24+
// define a broader type for local CMC entries that encompass BranchKeyMaterial
25+
// entries as well
26+
type LocalCmcEntry<S extends SupportedAlgorithmSuites> =
27+
| BranchKeyMaterialEntry
28+
| Entry<S>
29+
2130
export function getLocalCryptographicMaterialsCache<
2231
S extends SupportedAlgorithmSuites
2332
>(
2433
capacity: number,
2534
proactiveFrequency: number = 1000 * 60
2635
): CryptographicMaterialsCache<S> {
27-
const cache = new LRU<string, Entry<S>>({
36+
const cache = new LRU<string, LocalCmcEntry<S>>({
2837
max: capacity,
2938
dispose(_key, value) {
3039
/* Zero out the unencrypted dataKey, when the material is removed from the cache. */
@@ -82,6 +91,7 @@ export function getLocalCryptographicMaterialsCache<
8291

8392
cache.set(key, entry, maxAge)
8493
},
94+
8595
putDecryptionMaterial(
8696
key: string,
8797
material: DecryptionMaterial<S>,
@@ -100,6 +110,22 @@ export function getLocalCryptographicMaterialsCache<
100110

101111
cache.set(key, entry, maxAge)
102112
},
113+
114+
putBranchKeyMaterial(
115+
key: string,
116+
material: BranchKeyMaterial,
117+
maxAge?: number
118+
): void {
119+
/* Precondition: Only cache BranchKeyMaterial */
120+
needs(isBranchKeyMaterial(material), 'Malformed response.')
121+
122+
const entry = Object.seal({
123+
response: material,
124+
})
125+
126+
cache.set(key, entry, maxAge)
127+
},
128+
103129
getEncryptionMaterial(key: string, plaintextLength: number) {
104130
/* Precondition: plaintextLength can not be negative. */
105131
needs(plaintextLength >= 0, 'Malformed plaintextLength')
@@ -109,11 +135,13 @@ export function getLocalCryptographicMaterialsCache<
109135
/* Postcondition: Only return EncryptionMaterial. */
110136
needs(isEncryptionMaterial(entry.response), 'Malformed response.')
111137

112-
entry.bytesEncrypted += plaintextLength
113-
entry.messagesEncrypted += 1
138+
const encryptionMaterialEntry = entry as EncryptionMaterialEntry<S>
139+
encryptionMaterialEntry.bytesEncrypted += plaintextLength
140+
encryptionMaterialEntry.messagesEncrypted += 1
114141

115142
return entry as EncryptionMaterialEntry<S>
116143
},
144+
117145
getDecryptionMaterial(key: string) {
118146
const entry = cache.get(key)
119147
/* Check for early return (Postcondition): If this key does not have a DecryptionMaterial, return false. */
@@ -123,6 +151,18 @@ export function getLocalCryptographicMaterialsCache<
123151

124152
return entry as DecryptionMaterialEntry<S>
125153
},
154+
155+
getBranchKeyMaterial(key: string): BranchKeyMaterialEntry | false {
156+
const entry = cache.get(key)
157+
158+
/* Postcondition: If this key does not have a BranchKeyMaterial, return false */
159+
if (!entry) return false
160+
161+
/* Postcondition: Only return BranchKeyMaterial */
162+
needs(isBranchKeyMaterial(entry.response), 'Malformed response.')
163+
return entry as BranchKeyMaterialEntry
164+
},
165+
126166
del(key: string) {
127167
cache.del(key)
128168
},

modules/cache-material/test/get_local_cryptographic_materials_cache.test.ts

+90
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,67 @@ import {
1010
NodeEncryptionMaterial,
1111
NodeDecryptionMaterial,
1212
AlgorithmSuiteIdentifier,
13+
NodeBranchKeyMaterial,
1314
} from '@aws-crypto/material-management'
15+
import { v4 } from 'uuid'
1416

1517
const nodeSuite = new NodeAlgorithmSuite(
1618
AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256
1719
)
1820
const encryptionMaterial = new NodeEncryptionMaterial(nodeSuite, {})
1921
const decryptionMaterial = new NodeDecryptionMaterial(nodeSuite, {})
22+
const branchKeyMaterial = new NodeBranchKeyMaterial(
23+
Buffer.alloc(32),
24+
'id',
25+
v4(),
26+
{}
27+
)
2028

2129
describe('getLocalCryptographicMaterialsCache', () => {
2230
const {
2331
getEncryptionMaterial,
2432
getDecryptionMaterial,
33+
getBranchKeyMaterial,
2534
del,
2635
putEncryptionMaterial,
2736
putDecryptionMaterial,
37+
putBranchKeyMaterial,
2838
} = getLocalCryptographicMaterialsCache(100)
2939

40+
it('putBranchKeyMaterial', () => {
41+
const key = 'some encryption key'
42+
const response: any = branchKeyMaterial
43+
44+
putBranchKeyMaterial(key, response)
45+
const test = getBranchKeyMaterial(key)
46+
if (!test) throw new Error('never')
47+
expect(test.response === response).to.equal(true)
48+
expect(Object.isFrozen(test.response)).to.equal(true)
49+
})
50+
51+
it('Precondition: Only cache BranchKeyMaterial', () => {
52+
const key = 'some decryption key'
53+
const response: any = 'not material'
54+
55+
expect(() => putBranchKeyMaterial(key, response)).to.throw()
56+
})
57+
58+
it('Postcondition: If this key does not have a BranchKeyMaterial, return false', () => {
59+
const test = getBranchKeyMaterial('does-not-exist')
60+
expect(test).to.equal(false)
61+
})
62+
63+
it('Postcondition: Only return BranchKeyMaterial', () => {
64+
putDecryptionMaterial('key1', decryptionMaterial)
65+
putEncryptionMaterial('key2', encryptionMaterial, 1)
66+
67+
expect(() => getBranchKeyMaterial('key1')).to.throw()
68+
expect(() => getBranchKeyMaterial('key2')).to.throw()
69+
70+
putBranchKeyMaterial('key3', branchKeyMaterial)
71+
expect(() => getBranchKeyMaterial('key3'))
72+
})
73+
3074
it('putEncryptionMaterial', () => {
3175
const key = 'some encryption key'
3276
const response: any = encryptionMaterial
@@ -151,6 +195,52 @@ describe('getLocalCryptographicMaterialsCache', () => {
151195
})
152196

153197
describe('cache eviction', () => {
198+
it('putBranchKeyMaterial can exceed capacity', () => {
199+
const { getBranchKeyMaterial, putBranchKeyMaterial } =
200+
getLocalCryptographicMaterialsCache(1)
201+
202+
const key1 = 'key lost'
203+
const key2 = 'key replace'
204+
const response: any = branchKeyMaterial
205+
206+
putBranchKeyMaterial(key1, response)
207+
putBranchKeyMaterial(key2, response)
208+
const lost = getBranchKeyMaterial(key1)
209+
const found = getBranchKeyMaterial(key2)
210+
expect(lost).to.equal(false)
211+
expect(found).to.not.equal(false)
212+
})
213+
214+
it('putBranchKeyMaterial can be deleted', () => {
215+
const { getBranchKeyMaterial, putBranchKeyMaterial, del } =
216+
getLocalCryptographicMaterialsCache(1)
217+
218+
const key = 'key deleted'
219+
const response: any = branchKeyMaterial
220+
221+
putBranchKeyMaterial(key, response)
222+
del(key)
223+
const lost = getBranchKeyMaterial(key)
224+
expect(lost).to.equal(false)
225+
})
226+
227+
it('putBranchKeyMaterial can be garbage collected', async () => {
228+
const { getBranchKeyMaterial, putBranchKeyMaterial } =
229+
// set TTL to 10 ms so that our branch key material entry is evicted between the
230+
// put and get operation (which have a 20 ms gap). This will simulate a
231+
// case where we try to query our branch key material but it was already
232+
// garbage collected
233+
getLocalCryptographicMaterialsCache(1, 10)
234+
235+
const key = 'key lost'
236+
const response: any = branchKeyMaterial
237+
238+
putBranchKeyMaterial(key, response, 1)
239+
await new Promise((resolve) => setTimeout(resolve, 20))
240+
const lost = getBranchKeyMaterial(key)
241+
expect(lost).to.equal(false)
242+
})
243+
154244
it('putDecryptionMaterial can exceed capacity', () => {
155245
const { getDecryptionMaterial, putDecryptionMaterial } =
156246
getLocalCryptographicMaterialsCache(1)

modules/material-management/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"dependencies": {
2121
"asn1.js": "^5.3.0",
2222
"bn.js": "^5.1.1",
23-
"tslib": "^2.2.0"
23+
"tslib": "^2.2.0",
24+
"util": "^0.12.5"
2425
},
2526
"sideEffects": false,
2627
"main": "./build/main/src/index.js",

modules/material-management/src/cryptographic_material.ts

+90
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { KeyringTrace, KeyringTraceFlag } from './keyring_trace'
1717
import { NodeAlgorithmSuite } from './node_algorithms'
1818
import { WebCryptoAlgorithmSuite } from './web_crypto_algorithms'
1919
import { needs } from './needs'
20+
import { validate, version } from 'uuid'
2021

2122
/* KeyObject were introduced in v11.
2223
* They protect the data key better than a Buffer.
@@ -115,6 +116,91 @@ export interface CryptographicMaterial<T extends CryptographicMaterial<T>> {
115116
encryptionContext: Readonly<EncryptionContext>
116117
}
117118

119+
//= aws-encryption-sdk-specification/framework/structures.md#structure-3
120+
//# This structure MUST include all of the following fields:
121+
//#
122+
//# - [Branch Key](#branch-key)
123+
//# - [Branch Key Id](#branch-key-id)
124+
//# - [Branch Key Version](#branch-key-version)
125+
//# - [Encryption Context](#encryption-context-3)
126+
// structure is based on https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographicMaterialProviders/dafny/AwsCryptographyKeyStore/Model/KeyStore.smithy#L323
127+
export interface BranchKeyMaterial {
128+
branchKey(): Readonly<Buffer>
129+
branchKeyIdentifier: string
130+
branchKeyVersion: Readonly<Buffer>
131+
encryptionContext: Readonly<EncryptionContext>
132+
}
133+
134+
export class NodeBranchKeyMaterial implements BranchKeyMaterial {
135+
// all attributes are readonly so they are accessible outside the class but
136+
// they cannot be modified
137+
138+
// since all fields are objects, keep them immutable from external changes via
139+
// shared memory
140+
141+
// we want the branch key to be mutable within the class but immutable outside
142+
// the class
143+
private _branchKey: Buffer
144+
declare readonly branchKeyIdentifier: string
145+
declare readonly branchKeyVersion: Readonly<Buffer>
146+
declare readonly encryptionContext: Readonly<EncryptionContext>
147+
148+
constructor(
149+
branchKey: Buffer,
150+
branchKeyIdentifier: string,
151+
branchKeyVersion: string,
152+
encryptionContext: EncryptionContext
153+
) {
154+
/* Precondition: branchKey must be a 32 byte-long buffer */
155+
needs(branchKey.length === 32, 'Branch key must be 32 bytes long')
156+
157+
/* Precondition: encryptionContext must be an object, even if it is empty */
158+
needs(
159+
encryptionContext && typeof encryptionContext === 'object',
160+
'Encryption context must be set'
161+
)
162+
163+
/* Precondition: branch key ID is required */
164+
needs(branchKeyIdentifier, 'Empty branch key ID')
165+
166+
/* Precondition: branch key version must be valid version 4 uuid */
167+
needs(
168+
validate(branchKeyVersion) && version(branchKeyVersion) === 4,
169+
'Branch key version must be valid version 4 uuid'
170+
)
171+
172+
/* Postcondition: branch key is immutable */
173+
this._branchKey = Buffer.from(branchKey)
174+
175+
/* Postconditon: encryption context is immutable */
176+
this.encryptionContext = Object.freeze({ ...encryptionContext })
177+
178+
this.branchKeyIdentifier = branchKeyIdentifier
179+
180+
this.branchKeyVersion = Buffer.from(branchKeyVersion, 'utf-8')
181+
182+
Object.setPrototypeOf(this, NodeBranchKeyMaterial.prototype)
183+
/* Postcondition: instance is frozen */
184+
// preventing any modifications to its properties or methods.
185+
Object.freeze(this)
186+
}
187+
188+
// makes the branch key public to users wrapped in immutable access
189+
branchKey(): Readonly<Buffer> {
190+
return this._branchKey
191+
}
192+
193+
// this capability is not required of branch key materials according to the
194+
// specification. Using this is a good security practice so that the data
195+
// key's bytes are not preserved even in free memory
196+
zeroUnencryptedDataKey(): BranchKeyMaterial {
197+
this._branchKey.fill(0)
198+
return this
199+
}
200+
}
201+
// make the class immutable
202+
frozenClass(NodeBranchKeyMaterial)
203+
118204
export interface EncryptionMaterial<T extends CryptographicMaterial<T>>
119205
extends CryptographicMaterial<T> {
120206
encryptedDataKeys: EncryptedDataKey[]
@@ -372,6 +458,10 @@ export function isDecryptionMaterial(
372458
)
373459
}
374460

461+
export function isBranchKeyMaterial(obj: any): obj is NodeBranchKeyMaterial {
462+
return obj instanceof NodeBranchKeyMaterial
463+
}
464+
375465
export function decorateCryptographicMaterial<
376466
T extends CryptographicMaterial<T>
377467
>(material: T, setFlag: KeyringTraceFlag) {

0 commit comments

Comments
 (0)