1
+ using System ;
2
+ using System . Collections . Generic ;
3
+ using System . Diagnostics ;
4
+ using System . Net ;
5
+ using System . Threading . Tasks ;
6
+ using Amazon . DynamoDBv2 ;
7
+ using Amazon . DynamoDBv2 . Model ;
8
+ using Amazon . KeyManagementService ;
9
+ using AWS . Cryptography . DbEncryptionSDK . DynamoDb ;
10
+ using AWS . Cryptography . DbEncryptionSDK . StructuredEncryption ;
11
+ using AWS . Cryptography . KeyStore ;
12
+ using AWS . Cryptography . MaterialProviders ;
13
+
14
+ /*
15
+ This example demonstrates how to use a shared cache across multiple Hierarchical Keyrings.
16
+ With this functionality, users only need to maintain one common shared cache across multiple
17
+ Hierarchical Keyrings with different Key Stores instances/KMS Clients/KMS Keys.
18
+
19
+ There are three important parameters that users need to carefully set while providing the shared cache:
20
+
21
+ 1. Partition ID - Partition ID is an optional parameter provided to the Hierarchical Keyring input,
22
+ which distinguishes Cryptographic Material Providers (i.e: Keyrings) writing to a cache.
23
+ - If the Partition ID is set and is the same for two Hierarchical Keyrings (or another Material Provider),
24
+ they CAN share the same cache entries in the cache.
25
+ - If the Partition ID is set and is different for two Hierarchical Keyrings (or another Material Provider),
26
+ they CANNOT share the same cache entries in the cache.
27
+ - If the Partition ID is not set by the user, it is initialized as a random 16-byte UUID which makes
28
+ it unique for every Hierarchical Keyring, and two Hierarchical Keyrings (or another Material Provider)
29
+ CANNOT share the same cache entries in the cache.
30
+
31
+ 2. Logical Key Store Name - This parameter is set by the user when configuring the Key Store for
32
+ the Hierarchical Keyring. This is a logical name for the branch key store.
33
+ Suppose you have a physical Key Store (K). You create two instances of K (K1 and K2). Now, you create
34
+ two Hierarchical Keyrings (HK1 and HK2) with these Key Store instances (K1 and K2 respectively).
35
+ - If you want to share cache entries across these two keyrings, you should set the Logical Key Store Names
36
+ for both the Key Store instances (K1 and K2) to be the same.
37
+ - If you set the Logical Key Store Names for K1 and K2 to be different, HK1 (which uses Key Store instance K1)
38
+ and HK2 (which uses Key Store instance K2) will NOT be able to share cache entries.
39
+
40
+ 3. Branch Key ID - Choose an effective Branch Key ID Schema
41
+
42
+ This is demonstrated in the example below.
43
+ Notice that both K1 and K2 are instances of the same physical Key Store (K).
44
+ You MUST NEVER have two different physical Key Stores with the same Logical Key Store Name.
45
+
46
+ Important Note: If you have two or more Hierarchy Keyrings with:
47
+ - Same Partition ID
48
+ - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
49
+ - Same Branch Key ID
50
+ then they WILL share the cache entries in the Shared Cache.
51
+ Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID
52
+ to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries.
53
+
54
+ This example sets up DynamoDb Encryption for the AWS SDK client using the Hierarchical
55
+ Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb.
56
+ These branch keys are used to protect your data keys, and these branch keys are themselves
57
+ protected by a root KMS Key.
58
+
59
+ This example first creates a shared cache that you can use across multiple Hierarchical Keyrings.
60
+ The example then configures a Hierarchical Keyring (HK1 and HK2) with the shared cache,
61
+ a Branch Key ID and two instances (K1 and K2) of the same physical Key Store (K) respectively,
62
+ i.e. HK1 with K1 and HK2 with K2. The example demonstrates that if you set the same Partition ID
63
+ for HK1 and HK2, the two keyrings can share cache entries.
64
+ If you set different Partition ID of the Hierarchical Keyrings, or different
65
+ Logical Key Store Names of the Key Store instances, then the keyrings will NOT
66
+ be able to share cache entries.
67
+
68
+ Running this example requires access to the DDB Table whose name
69
+ is provided in CLI arguments.
70
+ This table must be configured with the following
71
+ primary key configuration:
72
+ - Partition key is named "partition_key" with type (S)
73
+ - Sort key is named "sort_key" with type (S)
74
+
75
+ This example also requires using a KMS Key whose ARN
76
+ is provided in CLI arguments. You need the following access
77
+ on this key:
78
+ - GenerateDataKeyWithoutPlaintext
79
+ - Decrypt
80
+ */
81
+ public class SharedCacheAcrossHierarchicalKeyringsExample
82
+ {
83
+ public static async Task SharedCacheAcrossHierarchicalKeyringsGetItemPutItem ( String branchKeyId )
84
+ {
85
+ var ddbTableName = TestUtils . TEST_DDB_TABLE_NAME ;
86
+ var keyStoreTableName = TestUtils . TEST_KEYSTORE_NAME ;
87
+ var logicalKeyStoreName = TestUtils . TEST_LOGICAL_KEYSTORE_NAME ;
88
+ var partitionId = TestUtils . TEST_PARTITION_ID ;
89
+ var kmsKeyId = TestUtils . TEST_KEYSTORE_KMS_KEY_ID ;
90
+
91
+ // 1. Create the CryptographicMaterialsCache (CMC) to share across multiple Hierarchical Keyrings
92
+ // using the Material Providers Library
93
+ // This CMC takes in:
94
+ // - CacheType
95
+ var materialProviders = new MaterialProviders ( new MaterialProvidersConfig ( ) ) ;
96
+
97
+ var cache = new CacheType { Default = new DefaultCache { EntryCapacity = 100 } } ;
98
+
99
+ var cryptographicMaterialsCacheInput = new CreateCryptographicMaterialsCacheInput { Cache = cache } ;
100
+
101
+ var sharedCryptographicMaterialsCache = materialProviders . CreateCryptographicMaterialsCache ( cryptographicMaterialsCacheInput ) ;
102
+
103
+ // 2. Create a CacheType object for the sharedCryptographicMaterialsCache
104
+ // Note that the `cache` parameter in the Hierarchical Keyring Input takes a `CacheType` as input
105
+ // Here, we pass a `Shared` CacheType that passes an already initialized shared cache
106
+ var sharedCache = new CacheType { Shared = sharedCryptographicMaterialsCache } ;
107
+
108
+ // Initial KeyStore Setup: This example requires that you have already
109
+ // created your KeyStore, and have populated it with a new branch key.
110
+
111
+ // 3. Configure your KeyStore resource keystore1.
112
+ // This SHOULD be the same configuration that you used
113
+ // to initially create and populate your KeyStore.
114
+ // Note that keyStoreTableName is the physical Key Store,
115
+ // and keystore1 is instances of this physical Key Store.
116
+ var keystore1 = new KeyStore ( new KeyStoreConfig
117
+ {
118
+ DdbClient = new AmazonDynamoDBClient ( ) ,
119
+ DdbTableName = keyStoreTableName ,
120
+ LogicalKeyStoreName = logicalKeyStoreName ,
121
+ KmsClient = new AmazonKeyManagementServiceClient ( ) ,
122
+ KmsConfiguration = new KMSConfiguration { KmsKeyArn = kmsKeyId }
123
+ } ) ;
124
+
125
+ // 4. Create the Hierarchical Keyring HK1 with Key Store instance K1, partitionId,
126
+ // the shared Cache and the BranchKeyId.
127
+ // Note that we are now providing an already initialized shared cache instead of just mentioning
128
+ // the cache type and the Hierarchical Keyring initializing a cache at initialization.
129
+
130
+ // This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a
131
+ // BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more
132
+ // information.
133
+
134
+ // Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and
135
+ // Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache.
136
+ // partitionId for this example is a random UUID
137
+ var keyringInput1 = new CreateAwsKmsHierarchicalKeyringInput
138
+ {
139
+ KeyStore = keystore1 ,
140
+ branchKeyId = branchKeyId ,
141
+ TtlSeconds = 600 , // This dictates how often we call back to KMS to authorize use of the branch keys
142
+ Cache = sharedCache ,
143
+ PartitionId = partitionId
144
+ } ;
145
+ var hierarchicalKeyring1 = matProv . CreateAwsKmsHierarchicalKeyring ( keyringInput1 ) ;
146
+
147
+ // 4. Configure which attributes are encrypted and/or signed when writing new items.
148
+ // For each attribute that may exist on the items we plan to write to our DynamoDbTable,
149
+ // we must explicitly configure how they should be treated during item encryption:
150
+ // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
151
+ // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
152
+ // - DO_NOTHING: The attribute is not encrypted and not included in the signature
153
+ var attributeActionsOnEncrypt = new Dictionary < String , CryptoAction >
154
+ {
155
+ [ "partition_key" ] = CryptoAction . SIGN_ONLY , // Our partition attribute must be SIGN_ONLY
156
+ [ "sort_key" ] = CryptoAction . SIGN_ONLY , // Our sort attribute must be SIGN_ONLY
157
+ [ "sensitive_data" ] = CryptoAction . ENCRYPT_AND_SIGN
158
+ } ;
159
+
160
+ // 5. Get the DDB Client for Hierarchical Keyring 1.
161
+ var ddbClient1 = GetDdbClient (
162
+ ddbTableName ,
163
+ hierarchicalKeyring1 ,
164
+ attributeActionsOnEncrypt
165
+ ) ;
166
+
167
+ // 6. Encrypt Decrypt roundtrip with ddbClient1
168
+ PutGetItems ( ddbTableName , ddbClient1 ) ;
169
+
170
+ // Through the above encrypt and decrypt roundtrip, the cache will be populated and
171
+ // the cache entries can be used by another Hierarchical Keyring with the
172
+ // - Same Partition ID
173
+ // - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
174
+ // - Same Branch Key ID
175
+
176
+ // 7. Configure your KeyStore resource keystore2.
177
+ // This SHOULD be the same configuration that you used
178
+ // to initially create and populate your physical KeyStore.
179
+ // Note that keyStoreTableName is the physical Key Store,
180
+ // and keystore2 is instances of this physical Key Store.
181
+
182
+ // Note that for this example, keystore2 is identical to keystore1.
183
+ // You can optionally change configurations like KMS Client or KMS Key ID based
184
+ // on your use-case.
185
+ // Make sure you have the required permissions to use different configurations.
186
+
187
+ // - If you want to share cache entries across two keyrings HK1 and HK2,
188
+ // you should set the Logical Key Store Names for both
189
+ // Key Store instances (K1 and K2) to be the same.
190
+ // - If you set the Logical Key Store Names for K1 and K2 to be different,
191
+ // HK1 (which uses Key Store instance K1) and HK2 (which uses Key Store
192
+ // instance K2) will NOT be able to share cache entries.
193
+ var keystore2 = new KeyStore ( new KeyStoreConfig
194
+ {
195
+ DdbClient = new AmazonDynamoDBClient ( ) ,
196
+ DdbTableName = keyStoreTableName ,
197
+ LogicalKeyStoreName = logicalKeyStoreName ,
198
+ KmsClient = new AmazonKeyManagementServiceClient ( ) ,
199
+ KmsConfiguration = new KMSConfiguration { KmsKeyArn = kmsKeyId }
200
+ } ) ;
201
+
202
+ // 8. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
203
+ // and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries
204
+ // (and experience cache HITS).
205
+
206
+ // Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and
207
+ // Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache.
208
+ // partitionId for this example is a random UUID
209
+ var keyringInput2 = new CreateAwsKmsHierarchicalKeyringInput
210
+ {
211
+ KeyStore = keystore2 ,
212
+ branchKeyId = branchKeyId ,
213
+ TtlSeconds = 600 , // This dictates how often we call back to KMS to authorize use of the branch keys
214
+ Cache = sharedCache ,
215
+ PartitionId = partitionId
216
+ } ;
217
+ var hierarchicalKeyring2 = matProv . CreateAwsKmsHierarchicalKeyring ( keyringInput2 ) ;
218
+
219
+ // 9. Get the DDB Client for Hierarchical Keyring 2.
220
+ var ddbClient2 = GetDdbClient (
221
+ ddbTableName ,
222
+ hierarchicalKeyring2 ,
223
+ attributeActionsOnEncrypt
224
+ ) ;
225
+
226
+ // 10. Encrypt Decrypt roundtrip with ddbClient2
227
+ PutGetItems ( ddbTableName , ddbClient2 ) ;
228
+ }
229
+
230
+ public static Client . DynamoDbClient GetDdbClient (
231
+ String ddbTableName ,
232
+ MaterialProviders . IKeyring hierarchicalKeyring ,
233
+ Dictionary < String , CryptoAction > attributeActionsOnEncrypt
234
+ )
235
+ {
236
+ // Configure which attributes we expect to be included in the signature
237
+ // when reading items. There are two options for configuring this:
238
+ //
239
+ // - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
240
+ // When defining your DynamoDb schema and deciding on attribute names,
241
+ // choose a distinguishing prefix (such as ":") for all attributes that
242
+ // you do not want to include in the signature.
243
+ // This has two main benefits:
244
+ // - It is easier to reason about the security and authenticity of data within your item
245
+ // when all unauthenticated data is easily distinguishable by their attribute name.
246
+ // - If you need to add new unauthenticated attributes in the future,
247
+ // you can easily make the corresponding update to your `attributeActionsOnEncrypt`
248
+ // and immediately start writing to that new attribute, without
249
+ // any other configuration update needed.
250
+ // Once you configure this field, it is not safe to update it.
251
+ //
252
+ // - Configure `allowedUnsignedAttributes`: You may also explicitly list
253
+ // a set of attributes that should be considered unauthenticated when encountered
254
+ // on read. Be careful if you use this configuration. Do not remove an attribute
255
+ // name from this configuration, even if you are no longer writing with that attribute,
256
+ // as old items may still include this attribute, and our configuration needs to know
257
+ // to continue to exclude this attribute from the signature scope.
258
+ // If you add new attribute names to this field, you must first deploy the update to this
259
+ // field to all readers in your host fleet before deploying the update to start writing
260
+ // with that new attribute.
261
+ //
262
+ // For this example, we currently authenticate all attributes. To make it easier to
263
+ // add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
264
+ const String unsignAttrPrefix = ":" ;
265
+
266
+ // Create the DynamoDb Encryption configuration for the table we will be writing to.
267
+ var tableConfigs = new Dictionary < String , DynamoDbTableEncryptionConfig >
268
+ {
269
+ [ ddbTableName ] = new DynamoDbTableEncryptionConfig
270
+ {
271
+ LogicalTableName = ddbTableName ,
272
+ PartitionKeyName = "partition_key" ,
273
+ SortKeyName = "sort_key" ,
274
+ AttributeActionsOnEncrypt = attributeActionsOnEncrypt ,
275
+ Keyring = hierarchicalKeyring ,
276
+ AllowedUnsignedAttributePrefix = unsignAttrPrefix
277
+ }
278
+ } ;
279
+
280
+ // Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
281
+ var ddbClient = new Client . DynamoDbClient (
282
+ new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs } ) ;
283
+
284
+ return ddbClient ;
285
+ }
286
+
287
+ public static void PutGetItems (
288
+ String ddbTableName ,
289
+ Client . DynamoDbClient ddbClient
290
+ )
291
+ {
292
+ // Put an item into our table using the given ddb client.
293
+ // Before the item gets sent to DynamoDb, it will be encrypted
294
+ // client-side, according to our configuration.
295
+ // This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a
296
+ // BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more
297
+ // information.
298
+ var item = new Dictionary < String , AttributeValue >
299
+ {
300
+ [ "partition_key" ] = new AttributeValue ( "id" ) ,
301
+ [ "sort_key" ] = new AttributeValue { N = "0" } ,
302
+ [ "sensitive_data" ] = new AttributeValue ( "encrypt and sign me!" )
303
+ } ;
304
+ var putRequest = new PutItemRequest
305
+ {
306
+ TableName = ddbTableName ,
307
+ Item = item
308
+ } ;
309
+
310
+ var putResponse = await ddbClient . PutItemAsync ( putRequest ) ;
311
+
312
+ // Demonstrate that PutItem succeeded
313
+ Debug . Assert ( putResponse . HttpStatusCode == HttpStatusCode . OK ) ;
314
+
315
+ // Get the item back from our table using the same client.
316
+ // The client will decrypt the item client-side, and return
317
+ // back the original item.
318
+ // This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a
319
+ // BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more
320
+ // information.
321
+ var keyToGet = new Dictionary < String , AttributeValue >
322
+ {
323
+ [ "partition_key" ] = new AttributeValue ( "id" ) ,
324
+ [ "sort_key" ] = new AttributeValue { N = "0" }
325
+ } ;
326
+ var getRequest = new GetItemRequest
327
+ {
328
+ Key = keyToGet ,
329
+ TableName = ddbTableName
330
+ } ;
331
+ var getResponse = await ddbClient . GetItemAsync ( getRequest ) ;
332
+
333
+ // Demonstrate that GetItem succeeded and returned the decrypted item
334
+ Debug . Assert ( getResponse . HttpStatusCode == HttpStatusCode . OK ) ;
335
+ var returnedItem = getResponse . Item ;
336
+ Debug . Assert ( returnedItem [ "sensitive_data" ] . S . Equals ( "encrypt and sign me!" ) ) ;
337
+ }
338
+ }
0 commit comments