1
+ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ use crate :: test_utils;
5
+ use aws_db_esdk:: aws_cryptography_dbEncryptionSdk_dynamoDb:: types:: DynamoDbTableEncryptionConfig ;
6
+ use aws_db_esdk:: aws_cryptography_dbEncryptionSdk_structuredEncryption:: types:: CryptoAction ;
7
+ use aws_db_esdk:: aws_cryptography_materialProviders:: client as mpl_client;
8
+ use aws_db_esdk:: aws_cryptography_materialProviders:: types:: material_providers_config:: MaterialProvidersConfig ;
9
+ use aws_db_esdk:: aws_cryptography_materialProviders:: types:: PaddingScheme ;
10
+ use aws_db_esdk:: intercept:: DbEsdkInterceptor ;
11
+ use aws_db_esdk:: DynamoDbTablesEncryptionConfig ;
12
+ use aws_sdk_dynamodb:: types:: AttributeValue ;
13
+ use std:: collections:: HashMap ;
14
+ use std:: fs:: File ;
15
+ use std:: io:: Read ;
16
+ use std:: io:: Write ;
17
+ use std:: path:: Path ;
18
+
19
+ /*
20
+ This example sets up DynamoDb Encryption for the AWS SDK client
21
+ using the Hierarchical Keyring, which establishes a key hierarchy
22
+ where "branch" keys are persisted in DynamoDb.
23
+ These branch keys are used to protect your data keys,
24
+ and these branch keys are themselves protected by a root KMS Key.
25
+
26
+ Establishing a key hierarchy like this has two benefits:
27
+
28
+ First, by caching the branch key material, and only calling back
29
+ to KMS to re-establish authentication regularly according to your configured TTL,
30
+ you limit how often you need to call back to KMS to protect your data.
31
+ This is a performance/security tradeoff, where your authentication, audit, and
32
+ logging from KMS is no longer one-to-one with every encrypt or decrypt call.
33
+ However, the benefit is that you no longer have to make a
34
+ network call to KMS for every encrypt or decrypt.
35
+
36
+ Second, this key hierarchy makes it easy to hold multi-tenant data
37
+ that is isolated per branch key in a single DynamoDb table.
38
+ You can create a branch key for each tenant in your table,
39
+ and encrypt all that tenant's data under that distinct branch key.
40
+ On decrypt, you can either statically configure a single branch key
41
+ to ensure you are restricting decryption to a single tenant,
42
+ or you can implement an interface that lets you map the primary key on your items
43
+ to the branch key that should be responsible for decrypting that data.
44
+
45
+ This example then demonstrates configuring a Hierarchical Keyring
46
+ with a Branch Key ID Supplier to encrypt and decrypt data for
47
+ two separate tenants.
48
+
49
+ Running this example requires access to the DDB Table whose name
50
+ is provided in CLI arguments.
51
+ This table must be configured with the following
52
+ primary key configuration:
53
+ - Partition key is named "partition_key" with type (S)
54
+ - Sort key is named "sort_key" with type (S)
55
+
56
+ This example also requires using a KMS Key whose ARN
57
+ is provided in CLI arguments. You need the following access
58
+ on this key:
59
+ - GenerateDataKeyWithoutPlaintext
60
+ - Decrypt
61
+ */
62
+ pub async fn HierarchicalKeyringGetItemPutItem ( String tenant1BranchKeyId , String tenant2BranchKeyId )
63
+ {
64
+ /*
65
+ var ddbTableName = TestUtils.TEST_DDB_TABLE_NAME;
66
+ var keyStoreTableName = TestUtils.TEST_KEYSTORE_NAME;
67
+ var logicalKeyStoreName = TestUtils.TEST_LOGICAL_KEYSTORE_NAME;
68
+ var kmsKeyId = TestUtils.TEST_KEYSTORE_KMS_KEY_ID;
69
+
70
+ // Initial KeyStore Setup: This example requires that you have already
71
+ // created your KeyStore, and have populated it with two new branch keys.
72
+ // See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
73
+ // for an example of how to do this.
74
+
75
+ // 1. Configure your KeyStore resource.
76
+ // This SHOULD be the same configuration that you used
77
+ // to initially create and populate your KeyStore.
78
+ var keystore = new KeyStore(new KeyStoreConfig
79
+ {
80
+ DdbClient = new AmazonDynamoDBClient(),
81
+ DdbTableName = keyStoreTableName,
82
+ LogicalKeyStoreName = logicalKeyStoreName,
83
+ KmsClient = new AmazonKeyManagementServiceClient(),
84
+ KmsConfiguration = new KMSConfiguration { KmsKeyArn = kmsKeyId }
85
+ });
86
+
87
+
88
+ // 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
89
+ var ddbEnc = new DynamoDbEncryption(new DynamoDbEncryptionConfig());
90
+ var branchKeyIdSupplier = ddbEnc.CreateDynamoDbEncryptionBranchKeyIdSupplier(
91
+ new CreateDynamoDbEncryptionBranchKeyIdSupplierInput
92
+ {
93
+ DdbKeyBranchKeyIdSupplier = new ExampleBranchKeyIdSupplier(tenant1BranchKeyId, tenant2BranchKeyId)
94
+ }).BranchKeyIdSupplier;
95
+
96
+ // 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
97
+ // With this configuration, the AWS SDK Client ultimately configured will be capable
98
+ // of encrypting or decrypting items for either tenant (assuming correct KMS access).
99
+ // If you want to restrict the client to only encrypt or decrypt for a single tenant,
100
+ // configure this Hierarchical Keyring using `.branchKeyId(tenant1BranchKeyId)` instead
101
+ // of `.branchKeyIdSupplier(branchKeyIdSupplier)`.
102
+ var matProv = new MaterialProviders(new MaterialProvidersConfig());
103
+ var keyringInput = new CreateAwsKmsHierarchicalKeyringInput
104
+ {
105
+ KeyStore = keystore,
106
+ BranchKeyIdSupplier = branchKeyIdSupplier,
107
+ TtlSeconds = 600, // This dictates how often we call back to KMS to authorize use of the branch keys
108
+ Cache = new CacheType
109
+ {
110
+ Default = new DefaultCache { EntryCapacity = 100 }
111
+ }
112
+ };
113
+ var hierarchicalKeyring = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput);
114
+
115
+ // 4. Configure which attributes are encrypted and/or signed when writing new items.
116
+ // For each attribute that may exist on the items we plan to write to our DynamoDbTable,
117
+ // we must explicitly configure how they should be treated during item encryption:
118
+ // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
119
+ // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
120
+ // - DO_NOTHING: The attribute is not encrypted and not included in the signature
121
+ var attributeActionsOnEncrypt = new Dictionary<String, CryptoAction>
122
+ {
123
+ ["partition_key"] = CryptoAction.SIGN_ONLY, // Our partition attribute must be SIGN_ONLY
124
+ ["sort_key"] = CryptoAction.SIGN_ONLY, // Our sort attribute must be SIGN_ONLY
125
+ ["tenant_sensitive_data"] = CryptoAction.ENCRYPT_AND_SIGN
126
+ };
127
+
128
+ // 5. Configure which attributes we expect to be included in the signature
129
+ // when reading items. There are two options for configuring this:
130
+ //
131
+ // - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
132
+ // When defining your DynamoDb schema and deciding on attribute names,
133
+ // choose a distinguishing prefix (such as ":") for all attributes that
134
+ // you do not want to include in the signature.
135
+ // This has two main benefits:
136
+ // - It is easier to reason about the security and authenticity of data within your item
137
+ // when all unauthenticated data is easily distinguishable by their attribute name.
138
+ // - If you need to add new unauthenticated attributes in the future,
139
+ // you can easily make the corresponding update to your `attributeActionsOnEncrypt`
140
+ // and immediately start writing to that new attribute, without
141
+ // any other configuration update needed.
142
+ // Once you configure this field, it is not safe to update it.
143
+ //
144
+ // - Configure `allowedUnsignedAttributes`: You may also explicitly list
145
+ // a set of attributes that should be considered unauthenticated when encountered
146
+ // on read. Be careful if you use this configuration. Do not remove an attribute
147
+ // name from this configuration, even if you are no longer writing with that attribute,
148
+ // as old items may still include this attribute, and our configuration needs to know
149
+ // to continue to exclude this attribute from the signature scope.
150
+ // If you add new attribute names to this field, you must first deploy the update to this
151
+ // field to all readers in your host fleet before deploying the update to start writing
152
+ // with that new attribute.
153
+ //
154
+ // For this example, we currently authenticate all attributes. To make it easier to
155
+ // add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
156
+ const String unsignAttrPrefix = ":";
157
+
158
+ // 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
159
+ var tableConfigs = new Dictionary<String, DynamoDbTableEncryptionConfig>
160
+ {
161
+ [ddbTableName] = new DynamoDbTableEncryptionConfig
162
+ {
163
+ LogicalTableName = ddbTableName,
164
+ PartitionKeyName = "partition_key",
165
+ SortKeyName = "sort_key",
166
+ AttributeActionsOnEncrypt = attributeActionsOnEncrypt,
167
+ Keyring = hierarchicalKeyring,
168
+ AllowedUnsignedAttributePrefix = unsignAttrPrefix
169
+ }
170
+ };
171
+
172
+ // 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
173
+ var ddb = new Client.DynamoDbClient(
174
+ new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs });
175
+
176
+ // 8. Put an item into our table using the above client.
177
+ // Before the item gets sent to DynamoDb, it will be encrypted
178
+ // client-side, according to our configuration.
179
+ // Because the item we are writing uses "tenantId1" as our partition value,
180
+ // based on the code we wrote in the ExampleBranchKeySupplier,
181
+ // `tenant1BranchKeyId` will be used to encrypt this item.
182
+ var item = new Dictionary<String, AttributeValue>
183
+ {
184
+ ["partition_key"] = new AttributeValue("tenant1Id"),
185
+ ["sort_key"] = new AttributeValue { N = "0" },
186
+ ["tenant_sensitive_data"] = new AttributeValue("encrypt and sign me!")
187
+ };
188
+ var putRequest = new PutItemRequest
189
+ {
190
+ TableName = ddbTableName,
191
+ Item = item
192
+ };
193
+
194
+ var putResponse = await ddb.PutItemAsync(putRequest);
195
+
196
+ // Demonstrate that PutItem succeeded
197
+ Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK);
198
+
199
+ // 10. Get the item back from our table using the same client.
200
+ // The client will decrypt the item client-side, and return
201
+ // back the original item.
202
+ // Because the returned item's partition value is "tenantId1",
203
+ // based on the code we wrote in the ExampleBranchKeySupplier,
204
+ // `tenant1BranchKeyId` will be used to decrypt this item.
205
+ var keyToGet = new Dictionary<String, AttributeValue>
206
+ {
207
+ ["partition_key"] = new AttributeValue("tenant1Id"),
208
+ ["sort_key"] = new AttributeValue { N = "0" }
209
+ };
210
+ var getRequest = new GetItemRequest
211
+ {
212
+ Key = keyToGet,
213
+ TableName = ddbTableName
214
+ };
215
+ var getResponse = await ddb.GetItemAsync(getRequest);
216
+
217
+ // Demonstrate that GetItem succeeded and returned the decrypted item
218
+ Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK);
219
+ var returnedItem = getResponse.Item;
220
+ Debug.Assert(returnedItem["tenant_sensitive_data"].S.Equals("encrypt and sign me!"));
221
+ */
222
+ println ! ( "hierarchical_keyring successful." ) ;
223
+
224
+ }
0 commit comments