Skip to content

Commit 2865f56

Browse files
committed
feat: Adds VersionKey API to version the branch key
1 parent fcaf49f commit 2865f56

6 files changed

Lines changed: 436 additions & 9 deletions

File tree

modules/branch-keystore-node/src/branch_keystore.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ interface IBranchKeyStoreNode {
4545
//= type=implication
4646
//# - [GetKeyStoreInfo](#getkeystoreinfo)
4747
getKeyStoreInfo(): KeyStoreInfoOutput
48+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#operations
49+
//= type=implication
50+
//# - [VersionKey](#versionkey)
51+
versionKey(input: VersionKeyInput): Promise<void>
4852
}
49-
5053
//= aws-encryption-sdk-specification/framework/branch-key-store.md#getkeystoreinfo
5154
//= type=implication
5255
//# This MUST include:
@@ -64,6 +67,10 @@ export interface KeyStoreInfoOutput {
6467
kmsConfiguration: KmsConfig
6568
}
6669

70+
export interface VersionKeyInput {
71+
branchKeyIdentifier: string
72+
}
73+
6774
export class BranchKeyStoreNode implements IBranchKeyStoreNode {
6875
public declare readonly logicalKeyStoreName: string
6976
public declare readonly kmsConfiguration: Readonly<KmsKeyConfig>
@@ -383,6 +390,35 @@ export class BranchKeyStoreNode implements IBranchKeyStoreNode {
383390
return branchKeyMaterials
384391
}
385392

393+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
394+
//# On invocation, the caller:
395+
//# - MUST supply a `branch-key-id`
396+
async versionKey(input: VersionKeyInput): Promise<void> {
397+
needs(input.branchKeyIdentifier, 'MUST supply a branch-key-id')
398+
399+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
400+
//# If the Keystore's KMS Configuration is `Discovery` or `MRDiscovery`,
401+
//# this operation MUST immediately fail.
402+
needs(
403+
typeof this.kmsConfiguration._config === 'object' &&
404+
('identifier' in this.kmsConfiguration._config ||
405+
'mrkIdentifier' in this.kmsConfiguration._config),
406+
'VersionKey is not supported with Discovery or MRDiscovery KMS Configuration'
407+
)
408+
409+
const { versionActiveBranchKey } = await import('./key_helpers')
410+
await versionActiveBranchKey({
411+
branchKeyIdentifier: input.branchKeyIdentifier,
412+
logicalKeyStoreName: this.logicalKeyStoreName,
413+
kmsConfiguration: this.kmsConfiguration,
414+
grantTokens: this.grantTokens,
415+
kmsClient: this.kmsClient,
416+
ddbClient: (this.storage as any).ddbClient,
417+
ddbTableName: (this.storage as any).ddbTableName,
418+
storage: this.storage,
419+
})
420+
}
421+
386422
//= aws-encryption-sdk-specification/framework/branch-key-store.md#getkeystoreinfo
387423
//= type=implication
388424
//# This operation MUST return the keystore information in this keystore configuration.
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import {
5+
KMSClient,
6+
GenerateDataKeyWithoutPlaintextCommand,
7+
ReEncryptCommand,
8+
} from '@aws-sdk/client-kms'
9+
import {
10+
DynamoDBClient,
11+
TransactWriteItemsCommand,
12+
} from '@aws-sdk/client-dynamodb'
13+
import { v4 } from 'uuid'
14+
import { needs } from '@aws-crypto/material-management'
15+
import { KmsKeyConfig } from './kms_config'
16+
import {
17+
BRANCH_KEY_IDENTIFIER_FIELD,
18+
TYPE_FIELD,
19+
BRANCH_KEY_FIELD,
20+
KEY_CREATE_TIME_FIELD,
21+
HIERARCHY_VERSION_FIELD,
22+
TABLE_FIELD,
23+
BRANCH_KEY_TYPE_PREFIX,
24+
BRANCH_KEY_ACTIVE_TYPE,
25+
BRANCH_KEY_ACTIVE_VERSION_FIELD,
26+
} from './constants'
27+
import { IBranchKeyStorage } from './types'
28+
29+
interface VersionKeyParams {
30+
branchKeyIdentifier: string
31+
logicalKeyStoreName: string
32+
kmsConfiguration: Readonly<KmsKeyConfig>
33+
grantTokens?: ReadonlyArray<string>
34+
kmsClient: KMSClient
35+
ddbClient: DynamoDBClient
36+
ddbTableName: string
37+
storage: IBranchKeyStorage
38+
}
39+
40+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-and-beacon-key-creation
41+
//# - `timestamp`: a timestamp for the current time.
42+
//# This timestamp MUST be in ISO 8601 format in UTC, to microsecond precision
43+
//# (e.g. "YYYY-MM-DDTHH:mm:ss.ssssssZ")
44+
function getCurrentTimestamp(): string {
45+
const now = new Date()
46+
return now.toISOString().replace('Z', '000Z')
47+
}
48+
49+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#active-encryption-context
50+
//# The ACTIVE encryption context value of the `type` attribute MUST equal to `"branch:ACTIVE"`.
51+
//# The ACTIVE encryption context MUST have a `version` attribute.
52+
//# The `version` attribute MUST store the branch key version formatted like `"branch:version:"` + `version`.
53+
function buildActiveEncryptionContext(decryptOnlyContext: {
54+
[key: string]: string
55+
}): { [key: string]: string } {
56+
const activeContext = { ...decryptOnlyContext }
57+
activeContext[BRANCH_KEY_ACTIVE_VERSION_FIELD] = activeContext[TYPE_FIELD]
58+
activeContext[TYPE_FIELD] = BRANCH_KEY_ACTIVE_TYPE
59+
return activeContext
60+
}
61+
62+
function toAttributeMap(
63+
encryptionContext: { [key: string]: string },
64+
ciphertextBlob: Uint8Array
65+
): { [key: string]: any } {
66+
const item: { [key: string]: any } = {}
67+
68+
for (const [key, value] of Object.entries(encryptionContext)) {
69+
if (key === TABLE_FIELD) continue
70+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#writing-branch-key-and-beacon-key-to-keystore
71+
//# - "hierarchy-version" (N): 1
72+
if (key === HIERARCHY_VERSION_FIELD) {
73+
item[key] = { N: value }
74+
} else {
75+
item[key] = { S: value }
76+
}
77+
}
78+
79+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#writing-branch-key-and-beacon-key-to-keystore
80+
//# - "enc" (B): the wrapped DECRYPT_ONLY Branch Key `CiphertextBlob` from the KMS operation
81+
item[BRANCH_KEY_FIELD] = { B: ciphertextBlob }
82+
83+
return item
84+
}
85+
86+
function getKmsKeyArn(
87+
kmsConfiguration: Readonly<KmsKeyConfig>
88+
): string | undefined {
89+
return typeof kmsConfiguration._config === 'object' &&
90+
'identifier' in kmsConfiguration._config
91+
? kmsConfiguration._config.identifier
92+
: typeof kmsConfiguration._config === 'object' &&
93+
'mrkIdentifier' in kmsConfiguration._config
94+
? kmsConfiguration._config.mrkIdentifier
95+
: undefined
96+
}
97+
98+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
99+
//# On invocation, the caller:
100+
//# - MUST supply a `branch-key-id`
101+
export async function versionActiveBranchKey(
102+
params: VersionKeyParams
103+
): Promise<void> {
104+
const {
105+
branchKeyIdentifier,
106+
logicalKeyStoreName,
107+
kmsConfiguration,
108+
grantTokens,
109+
kmsClient,
110+
ddbClient,
111+
ddbTableName,
112+
storage,
113+
} = params
114+
115+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
116+
//# VersionKey MUST first get the active version for the branch key from the keystore
117+
//# by calling AWS DDB `GetItem` using the `branch-key-id` as the Partition Key
118+
//# and `"branch:ACTIVE"` value as the Sort Key.
119+
const activeKey = await storage.getEncryptedActiveBranchKey(
120+
branchKeyIdentifier
121+
)
122+
123+
needs(
124+
activeKey.branchKeyId === branchKeyIdentifier,
125+
'Unexpected branch key id'
126+
)
127+
128+
needs(
129+
activeKey.encryptionContext[TABLE_FIELD] === logicalKeyStoreName,
130+
'Unexpected logical table name'
131+
)
132+
133+
const kmsKeyArn = getKmsKeyArn(kmsConfiguration)
134+
needs(kmsKeyArn, 'KMS Key ARN is required')
135+
136+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
137+
//# The `kms-arn` field of DDB response item MUST be compatible with
138+
//# the configured `KMS ARN` in the AWS KMS Configuration for this keystore.
139+
const oldActiveContext = activeKey.encryptionContext
140+
141+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#authenticating-a-keystore-item
142+
//# The operation MUST call AWS KMS API ReEncrypt with a request constructed as follows:
143+
//# - `SourceEncryptionContext` MUST be the encryption context constructed above
144+
//# - `SourceKeyId` MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore.
145+
//# - `CiphertextBlob` MUST be the `enc` attribute value on the AWS DDB response item
146+
//# - `GrantTokens` MUST be the configured grant tokens.
147+
//# - `DestinationKeyId` MUST be compatible with the configured KMS Key in the AWS KMS Configuration for this keystore.
148+
//# - `DestinationEncryptionContext` MUST be the encryption context constructed above
149+
await kmsClient.send(
150+
new ReEncryptCommand({
151+
SourceKeyId: kmsKeyArn,
152+
SourceEncryptionContext: oldActiveContext,
153+
CiphertextBlob: activeKey.ciphertextBlob,
154+
DestinationKeyId: kmsKeyArn,
155+
DestinationEncryptionContext: oldActiveContext,
156+
GrantTokens: grantTokens ? [...grantTokens] : undefined,
157+
})
158+
)
159+
160+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#branch-key-and-beacon-key-creation
161+
//# - `version`: a new guid. This guid MUST be version 4 UUID
162+
const branchKeyVersion = v4()
163+
const timestamp = getCurrentTimestamp()
164+
165+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
166+
//# The wrapped Branch Keys, DECRYPT_ONLY and ACTIVE,
167+
//# MUST be created according to Wrapped Branch Key Creation.
168+
const decryptOnlyContext: { [key: string]: string } = {
169+
...oldActiveContext,
170+
[TYPE_FIELD]: `${BRANCH_KEY_TYPE_PREFIX}${branchKeyVersion}`,
171+
[KEY_CREATE_TIME_FIELD]: timestamp,
172+
}
173+
delete decryptOnlyContext[BRANCH_KEY_ACTIVE_VERSION_FIELD]
174+
175+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#wrapped-branch-key-creation
176+
//# The operation MUST call AWS KMS API GenerateDataKeyWithoutPlaintext
177+
//# with a request constructed as follows:
178+
//# - `KeyId` MUST be the configured `AWS KMS Key ARN` in the AWS KMS Configuration for this keystore.
179+
//# - `NumberOfBytes` MUST be 32.
180+
//# - `EncryptionContext` MUST be the DECRYPT_ONLY encryption context for branch keys.
181+
//# - GenerateDataKeyWithoutPlaintext `GrantTokens` MUST be this keystore's grant tokens.
182+
const decryptOnlyResponse = await kmsClient.send(
183+
new GenerateDataKeyWithoutPlaintextCommand({
184+
KeyId: kmsKeyArn,
185+
NumberOfBytes: 32,
186+
EncryptionContext: decryptOnlyContext,
187+
GrantTokens: grantTokens ? [...grantTokens] : undefined,
188+
})
189+
)
190+
191+
needs(
192+
decryptOnlyResponse.CiphertextBlob,
193+
'Failed to generate new DECRYPT_ONLY branch key'
194+
)
195+
196+
const newActiveContext = buildActiveEncryptionContext(decryptOnlyContext)
197+
198+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#wrapped-branch-key-creation
199+
//# The operation MUST call AWS KMS API ReEncrypt with a request constructed as follows:
200+
//# - `SourceEncryptionContext` MUST be the DECRYPT_ONLY encryption context for branch keys.
201+
//# - `SourceKeyId` MUST be the configured `AWS KMS Key ARN` in the AWS KMS Configuration for this keystore.
202+
//# - `CiphertextBlob` MUST be the wrapped DECRYPT_ONLY Branch Key.
203+
//# - `DestinationKeyId` MUST be the configured `AWS KMS Key ARN` in the AWS KMS Configuration for this keystore.
204+
//# - `DestinationEncryptionContext` MUST be the ACTIVE encryption context for branch keys.
205+
const activeResponse = await kmsClient.send(
206+
new ReEncryptCommand({
207+
SourceKeyId: kmsKeyArn,
208+
SourceEncryptionContext: decryptOnlyContext,
209+
CiphertextBlob: decryptOnlyResponse.CiphertextBlob,
210+
DestinationKeyId: kmsKeyArn,
211+
DestinationEncryptionContext: newActiveContext,
212+
GrantTokens: grantTokens ? [...grantTokens] : undefined,
213+
})
214+
)
215+
216+
needs(
217+
activeResponse.CiphertextBlob,
218+
'Failed to generate new ACTIVE branch key'
219+
)
220+
221+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
222+
//# To add the new branch key to the keystore,
223+
//# the operation MUST call Amazon DynamoDB API TransactWriteItems.
224+
//# The call to Amazon DynamoDB TransactWriteItems MUST use the configured Amazon DynamoDB Client to make the call.
225+
await ddbClient.send(
226+
new TransactWriteItemsCommand({
227+
TransactItems: [
228+
{
229+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
230+
//# - PUT:
231+
//# - ConditionExpression: `attribute_not_exists(branch-key-id)`
232+
Put: {
233+
TableName: ddbTableName,
234+
Item: toAttributeMap(
235+
decryptOnlyContext,
236+
decryptOnlyResponse.CiphertextBlob
237+
),
238+
ConditionExpression: 'attribute_not_exists(#bkid)',
239+
ExpressionAttributeNames: {
240+
'#bkid': BRANCH_KEY_IDENTIFIER_FIELD,
241+
},
242+
},
243+
},
244+
{
245+
//= aws-encryption-sdk-specification/framework/branch-key-store.md#versionkey
246+
//# - PUT:
247+
//# - ConditionExpression: `attribute_exists(branch-key-id) AND enc = :encOld`
248+
//# - ExpressionAttributeValues: `{":encOld" := DDB.AttributeValue.B(oldCiphertextBlob)}`
249+
Put: {
250+
TableName: ddbTableName,
251+
Item: toAttributeMap(
252+
newActiveContext,
253+
activeResponse.CiphertextBlob
254+
),
255+
ConditionExpression: 'attribute_exists(#bkid) AND #enc = :encOld',
256+
ExpressionAttributeNames: {
257+
'#bkid': BRANCH_KEY_IDENTIFIER_FIELD,
258+
'#enc': BRANCH_KEY_FIELD,
259+
},
260+
ExpressionAttributeValues: {
261+
':encOld': { B: activeKey.ciphertextBlob },
262+
},
263+
},
264+
},
265+
],
266+
})
267+
)
268+
}

0 commit comments

Comments
 (0)