Skip to content

Commit 1acc137

Browse files
feat(Examples): Add MRK multi-keyring example (#173)
1 parent 62fab00 commit 1acc137

File tree

4 files changed

+346
-1
lines changed

4 files changed

+346
-1
lines changed

Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ dependencies {
6060
implementation("software.amazon.cryptography:AwsCryptographicMaterialProviders:1.0-SNAPSHOT")
6161

6262
implementation(platform("software.amazon.awssdk:bom:2.19.1"))
63+
implementation("software.amazon.awssdk:arns")
6364
implementation("software.amazon.awssdk:dynamodb")
6465
implementation("software.amazon.awssdk:dynamodb-enhanced")
6566
implementation("software.amazon.awssdk:kms")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
package software.aws.cryptography.examples.keyring;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import software.amazon.awssdk.arns.Arn;
7+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
8+
import software.amazon.awssdk.regions.Region;
9+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
10+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
11+
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
12+
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
13+
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
14+
import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;
15+
import software.amazon.awssdk.services.kms.KmsClient;
16+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig;
17+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
18+
import software.amazon.cryptography.materialproviders.IKeyring;
19+
import software.amazon.cryptography.materialproviders.MaterialProviders;
20+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsKeyringInput;
21+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkKeyringInput;
22+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkMultiKeyringInput;
23+
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
24+
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
25+
import software.aws.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
26+
27+
/*
28+
This example sets up DynamoDb Encryption for the AWS SDK client
29+
using the MRK multi-keyring. This keyring takes in multiple AWS KMS
30+
MRKs (multi-region keys) or regular AWS KMS keys (single-region keys)
31+
and uses them to encrypt and decrypt data. Data encrypted using an MRK
32+
multi-keyring can be decrypted using any of its component keys. If a component
33+
key is an MRK with a replica in a second region, the replica key can also be
34+
used to decrypt data.
35+
36+
For more information on MRKs, see
37+
https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html
38+
39+
For more information on multi-keyrings, see
40+
https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html
41+
42+
This example creates a new MRK multi-keyring consisting of one MRK
43+
(labeled as the "generator keyring") and one single-region key (labeled
44+
as the only "child keyring"). The MRK also has a replica in a second region.
45+
46+
This example encrypts a test item using the MRK multi-keyring and puts the
47+
encrypted item to the provided DynamoDb table. Then, it gets the item
48+
from the table and decrypts it using three different configs:
49+
1. The MRK multi-keyring, where the MRK key is used to decrypt
50+
2. Another MRK multi-keyring, where the replica MRK key is used to decrypt
51+
3. Another MRK multi-keyring, where the single-region key that was present
52+
in the original MRK multi-keyring is used to decrypt
53+
54+
Running this example requires access to the DDB Table whose name
55+
is provided in CLI arguments.
56+
This table must be configured with the following
57+
primary key configuration:
58+
- Partition key is named "partition_key" with type (S)
59+
- Sort key is named "sort_key" with type (S)
60+
61+
This example demonstrates multi-region use cases. As a result,
62+
it requires that you have a default region set in your AWS client.
63+
You can set a default region through the AWS CLI with
64+
`aws configure set region [region-name]`
65+
e.g.
66+
`aws configure set region us-west-2`
67+
For more information on using AWS CLI to set config, see
68+
https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configure/set.html
69+
*/
70+
public class MultiMrkKeyringExample {
71+
72+
public static void MultiMrkKeyringGetItemPutItem(String ddbTableName, String mrkKeyArn, String keyArn,
73+
String mrkReplicaKeyArn) {
74+
// 1. Create a single MRK multi-keyring using the MRK arn and the single-region key arn.
75+
final MaterialProviders matProv = MaterialProviders.builder()
76+
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
77+
.build();
78+
// Create the multi-keyring, using the MRK as the generator key,
79+
// and the single-region key as a child key.
80+
// Note that the generator key will generate and encrypt a plaintext data key
81+
// and all child keys will only encrypt that same plaintext data key.
82+
// As such, you must have permission to call KMS:GenerateDataKey on your generator key
83+
// and permission to call KMS:Encrypt on all child keys.
84+
// For more information, see the AWS docs on multi-keyrings above.
85+
final CreateAwsKmsMrkMultiKeyringInput createAwsKmsMrkMultiKeyringInput =
86+
CreateAwsKmsMrkMultiKeyringInput.builder()
87+
.generator(mrkKeyArn)
88+
.kmsKeyIds(Collections.singletonList(keyArn))
89+
.build();
90+
IKeyring awsKmsMrkMultiKeyring = matProv.CreateAwsKmsMrkMultiKeyring(createAwsKmsMrkMultiKeyringInput);
91+
92+
// 2. Configure which attributes are encrypted and/or signed when writing new items.
93+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
94+
// we must explicitly configure how they should be treated during item encryption:
95+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
96+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
97+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
98+
final Map<String, CryptoAction> attributeActions = new HashMap<>();
99+
attributeActions.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY
100+
attributeActions.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY
101+
attributeActions.put("sensitive_data", CryptoAction.ENCRYPT_AND_SIGN);
102+
103+
// 3. Configure which attributes we expect to be included in the signature
104+
// when reading items. There are two options for configuring this:
105+
//
106+
// - (Recommended) Configure `allowedUnauthenticatedAttributesPrefix`:
107+
// When defining your DynamoDb schema and deciding on attribute names,
108+
// choose a distinguishing prefix (such as ":") for all attributes that
109+
// you do not want to include in the signature.
110+
// This has two main benefits:
111+
// - It is easier to reason about the security and authenticity of data within your item
112+
// when all unauthenticated data is easily distinguishable by their attribute name.
113+
// - If you need to add new unauthenticated attributes in the future,
114+
// you can easily make the corresponding update to your `attributeActions`
115+
// and immediately start writing to that new attribute, without
116+
// any other configuration update needed.
117+
// Once you configure this field, it is not safe to update it.
118+
//
119+
// - Configure `allowedUnauthenticatedAttributes`: You may also explicitly list
120+
// a set of attributes that should be considered unauthenticated when encountered
121+
// on read. Be careful if you use this configuration. Do not remove an attribute
122+
// name from this configuration, even if you are no longer writing with that attribute,
123+
// as old items may still include this attribute, and our configuration needs to know
124+
// to continue to exclude this attribute from the signature scope.
125+
// If you add new attribute names to this field, you must first deploy the update to this
126+
// field to all readers in your host fleet before deploying the update to start writing
127+
// with that new attribute.
128+
//
129+
// For this example, we currently authenticate all attributes. To make it easier to
130+
// add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
131+
final String unauthAttrPrefix = ":";
132+
133+
// 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
134+
final Map<String, DynamoDbTableEncryptionConfig> tableConfigs = new HashMap<>();
135+
final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig.builder()
136+
.logicalTableName(ddbTableName)
137+
.partitionKeyName("partition_key")
138+
.sortKeyName("sort_key")
139+
.attributeActions(attributeActions)
140+
.keyring(awsKmsMrkMultiKeyring)
141+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
142+
.build();
143+
tableConfigs.put(ddbTableName, config);
144+
145+
// 5. Create the DynamoDb Encryption Interceptor
146+
DynamoDbEncryptionInterceptor encryptionInterceptor = DynamoDbEncryptionInterceptor.builder()
147+
.config(DynamoDbTablesEncryptionConfig.builder()
148+
.tableEncryptionConfigs(tableConfigs)
149+
.build())
150+
.build();
151+
152+
// 6. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
153+
final DynamoDbClient ddbClient = DynamoDbClient.builder()
154+
.overrideConfiguration(
155+
ClientOverrideConfiguration.builder()
156+
.addExecutionInterceptor(encryptionInterceptor)
157+
.build())
158+
.build();
159+
160+
// 7. Put an item into our table using the above client.
161+
// Before the item gets sent to DynamoDb, it will be encrypted
162+
// client-side using the MRK multi-keyring.
163+
// The data key protecting this item will be encrypted
164+
// with all the KMS Keys in this keyring, so that it can be
165+
// decrypted with any one of those KMS Keys.
166+
final HashMap<String, AttributeValue> item = new HashMap<>();
167+
item.put("partition_key", AttributeValue.builder().s("awsKmsMrkMultiKeyringItem").build());
168+
item.put("sort_key", AttributeValue.builder().n("0").build());
169+
item.put("sensitive_data", AttributeValue.builder().s("encrypt and sign me!").build());
170+
171+
final PutItemRequest putRequest = PutItemRequest.builder()
172+
.tableName(ddbTableName)
173+
.item(item)
174+
.build();
175+
176+
final PutItemResponse putResponse = ddbClient.putItem(putRequest);
177+
178+
// Demonstrate that PutItem succeeded
179+
assert 200 == putResponse.sdkHttpResponse().statusCode();
180+
181+
// 8. Get the item back from our table using the client.
182+
// The client will decrypt the item client-side using the MRK
183+
// and return back the original item.
184+
// Since the generator key is the first available key in the keyring,
185+
// that is the KMS Key that will be used to decrypt this item.
186+
final HashMap<String, AttributeValue> keyToGet = new HashMap<>();
187+
keyToGet.put("partition_key", AttributeValue.builder().s("awsKmsMrkMultiKeyringItem").build());
188+
keyToGet.put("sort_key", AttributeValue.builder().n("0").build());
189+
190+
final GetItemRequest getRequest = GetItemRequest.builder()
191+
.key(keyToGet)
192+
.tableName(ddbTableName)
193+
.build();
194+
195+
final GetItemResponse getResponse = ddbClient.getItem(getRequest);
196+
197+
// Demonstrate that GetItem succeeded and returned the decrypted item
198+
assert 200 == getResponse.sdkHttpResponse().statusCode();
199+
final Map<String, AttributeValue> returnedItem = getResponse.item();
200+
assert returnedItem.get("sensitive_data").s().equals("encrypt and sign me!");
201+
202+
// 9. Create a MRK keyring using the replica MRK arn.
203+
// We will use this to demonstrate that the replica MRK
204+
// can decrypt data created with the original MRK,
205+
// even when the replica MRK was not present in the
206+
// encrypting multi-keyring.
207+
final CreateAwsKmsMrkMultiKeyringInput onlyReplicaKeyCreateAwsKmsMrkMultiKeyringInput =
208+
CreateAwsKmsMrkMultiKeyringInput.builder()
209+
.kmsKeyIds(Collections.singletonList(mrkReplicaKeyArn))
210+
.build();
211+
IKeyring onlyReplicaKeyMrkMultiKeyring = matProv.CreateAwsKmsMrkMultiKeyring(
212+
onlyReplicaKeyCreateAwsKmsMrkMultiKeyringInput);
213+
214+
// 10. Create a new config and client using the MRK keyring.
215+
// This is the same setup as above, except we provide the MRK keyring to the config.
216+
final Map<String, DynamoDbTableEncryptionConfig> onlyReplicaKeyTableConfigs = new HashMap<>();
217+
final DynamoDbTableEncryptionConfig onlyReplicaKeyConfig = DynamoDbTableEncryptionConfig.builder()
218+
.logicalTableName(ddbTableName)
219+
.partitionKeyName("partition_key")
220+
.sortKeyName("sort_key")
221+
.attributeActions(attributeActions)
222+
// Only replica keyring added here
223+
.keyring(onlyReplicaKeyMrkMultiKeyring)
224+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
225+
.build();
226+
onlyReplicaKeyTableConfigs.put(ddbTableName, onlyReplicaKeyConfig);
227+
228+
DynamoDbEncryptionInterceptor onlyReplicaKeyEncryptionInterceptor = DynamoDbEncryptionInterceptor.builder()
229+
.config(DynamoDbTablesEncryptionConfig.builder()
230+
.tableEncryptionConfigs(onlyReplicaKeyTableConfigs)
231+
.build())
232+
.build();
233+
234+
final DynamoDbClient onlyReplicaKeyDdbClient = DynamoDbClient.builder()
235+
.overrideConfiguration(
236+
ClientOverrideConfiguration.builder()
237+
.addExecutionInterceptor(onlyReplicaKeyEncryptionInterceptor)
238+
.build())
239+
.build();
240+
241+
// 11. Get the item back from our table using the client configured with the replica.
242+
// The client will decrypt the item client-side using the replica MRK
243+
// and return back the original item.
244+
final HashMap<String, AttributeValue> onlyReplicaKeyKeyToGet = new HashMap<>();
245+
onlyReplicaKeyKeyToGet.put("partition_key", AttributeValue.builder().s("awsKmsMrkMultiKeyringItem").build());
246+
onlyReplicaKeyKeyToGet.put("sort_key", AttributeValue.builder().n("0").build());
247+
248+
final GetItemRequest onlyReplicaKeyGetRequest = GetItemRequest.builder()
249+
.key(onlyReplicaKeyKeyToGet)
250+
.tableName(ddbTableName)
251+
.build();
252+
253+
final GetItemResponse onlyReplicaKeyGetResponse = onlyReplicaKeyDdbClient.getItem(onlyReplicaKeyGetRequest);
254+
255+
// Demonstrate that GetItem succeeded and returned the decrypted item
256+
assert 200 == onlyReplicaKeyGetResponse.sdkHttpResponse().statusCode();
257+
final Map<String, AttributeValue> onlyReplicaKeyReturnedItem = onlyReplicaKeyGetResponse.item();
258+
assert onlyReplicaKeyReturnedItem.get("sensitive_data").s().equals("encrypt and sign me!");
259+
260+
// 12. Create an AWS KMS keyring using the single-region key ARN.
261+
// We will use this to demonstrate that the single-region key
262+
// can decrypt data created with the MRK multi-keyring,
263+
// since it is present in the keyring used to encrypt.
264+
final CreateAwsKmsMrkMultiKeyringInput onlySrkCreateAwsKmsMrkMultiKeyringInput =
265+
CreateAwsKmsMrkMultiKeyringInput.builder()
266+
.kmsKeyIds(Collections.singletonList(keyArn))
267+
.build();
268+
IKeyring onlySrkKeyring = matProv.CreateAwsKmsMrkMultiKeyring(onlySrkCreateAwsKmsMrkMultiKeyringInput);
269+
270+
// 13. Create a new config and client using the AWS KMS keyring.
271+
// This is the same setup as above, except we provide the AWS KMS keyring to the config.
272+
final Map<String, DynamoDbTableEncryptionConfig> onlySrkTableConfigs = new HashMap<>();
273+
final DynamoDbTableEncryptionConfig onlySrkConfig = DynamoDbTableEncryptionConfig.builder()
274+
.logicalTableName(ddbTableName)
275+
.partitionKeyName("partition_key")
276+
.sortKeyName("sort_key")
277+
.attributeActions(attributeActions)
278+
// Only single-region key keyring added here
279+
.keyring(onlySrkKeyring)
280+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
281+
.build();
282+
onlySrkTableConfigs.put(ddbTableName, onlySrkConfig);
283+
284+
DynamoDbEncryptionInterceptor onlySrkEncryptionInterceptor = DynamoDbEncryptionInterceptor.builder()
285+
.config(DynamoDbTablesEncryptionConfig.builder()
286+
.tableEncryptionConfigs(onlySrkTableConfigs)
287+
.build())
288+
.build();
289+
290+
final DynamoDbClient onlySrkDdbClient = DynamoDbClient.builder()
291+
.overrideConfiguration(
292+
ClientOverrideConfiguration.builder()
293+
.addExecutionInterceptor(onlySrkEncryptionInterceptor)
294+
.build())
295+
.build();
296+
297+
// 14. Get the item back from our table using the client configured with the AWS KMS keyring.
298+
// The client will decrypt the item client-side using the single-region key
299+
// and return back the original item.
300+
final HashMap<String, AttributeValue> onlySrkKeyToGet = new HashMap<>();
301+
onlySrkKeyToGet.put("partition_key", AttributeValue.builder().s("awsKmsMrkMultiKeyringItem").build());
302+
onlySrkKeyToGet.put("sort_key", AttributeValue.builder().n("0").build());
303+
304+
final GetItemRequest onlySrkGetRequest = GetItemRequest.builder()
305+
.key(onlySrkKeyToGet)
306+
.tableName(ddbTableName)
307+
.build();
308+
309+
final GetItemResponse onlySrkGetResponse = onlySrkDdbClient.getItem(onlySrkGetRequest);
310+
311+
// Demonstrate that GetItem succeeded and returned the decrypted item
312+
assert 200 == onlySrkGetResponse.sdkHttpResponse().statusCode();
313+
final Map<String, AttributeValue> onlySrkReturnedItem = onlySrkGetResponse.item();
314+
assert onlySrkReturnedItem.get("sensitive_data").s().equals("encrypt and sign me!");
315+
}
316+
317+
public static void main(final String[] args) {
318+
if (args.length <= 1) {
319+
throw new IllegalArgumentException("To run this example, include the ddbTable, mrkKeyArn, keyArn, and mrkReplicaKeyArn in args");
320+
}
321+
final String ddbTableName = args[0];
322+
final String mrkKeyArn = args[1];
323+
final String srkArn = args[2];
324+
final String mrkReplicaKeyArn = args[3];
325+
MultiMrkKeyringGetItemPutItem(ddbTableName, mrkKeyArn, srkArn, mrkReplicaKeyArn);
326+
}
327+
}

0 commit comments

Comments
 (0)