Skip to content

Commit c617880

Browse files
feat(Examples): Create discovery keyring example (#179)
1 parent a5e943d commit c617880

File tree

3 files changed

+274
-0
lines changed

3 files changed

+274
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package software.aws.cryptography.examples.keyring;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
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.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig;
16+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
17+
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
18+
import software.amazon.cryptography.materialproviders.IKeyring;
19+
import software.amazon.cryptography.materialproviders.MaterialProviders;
20+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkDiscoveryMultiKeyringInput;
21+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkMultiKeyringInput;
22+
import software.amazon.cryptography.materialproviders.model.DiscoveryFilter;
23+
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
24+
import software.aws.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
25+
26+
/*
27+
This example sets up a MRK discovery multi-keyring to decrypt data using
28+
the DynamoDB encryption client. A discovery keyring is not provided with any wrapping
29+
keys; instead, it recognizes the KMS key that was used to encrypt a data key,
30+
and asks KMS to decrypt with that KMS key. Discovery keyrings cannot be used
31+
to encrypt data.
32+
33+
For more information on discovery keyrings, see
34+
https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery
35+
36+
This example encrypts an item using an MRK multi-keyring and puts the
37+
encrypted item to the configured DynamoDb table. Then, it gets the item
38+
from the table and decrypts it using the discovery keyring.
39+
40+
Running this example requires access to the DDB Table whose name
41+
is provided in CLI arguments.
42+
This table must be configured with the following
43+
primary key configuration:
44+
- Partition key is named "partition_key" with type (S)
45+
- Sort key is named "sort_key" with type (S)
46+
*/
47+
public class MrkDiscoveryMultiKeyringExample {
48+
49+
public static void MultiMrkDiscoveryKeyringGetItemPutItem(String ddbTableName, String keyArn,
50+
List<String> accountIds, List<String> regions) {
51+
// 1. Create a single MRK multi-keyring using the key arn.
52+
// Although this example demonstrates use of the MRK discovery multi-keyring,
53+
// a discovery keyring cannot be used to encrypt. So we will need to construct
54+
// a non-discovery keyring for this example to encrypt. For more information on MRK
55+
// multi-keyrings, see the MultiMrkKeyringExample in this directory.
56+
// Though this is an "MRK multi-keyring", we do not need to provide multiple keys,
57+
// and can use single-region KMS keys. We will provide a single key here; this
58+
// can be either an MRK or a single-region key.
59+
final MaterialProviders matProv = MaterialProviders.builder()
60+
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
61+
.build();
62+
final CreateAwsKmsMrkMultiKeyringInput createAwsKmsMrkMultiKeyringInput =
63+
CreateAwsKmsMrkMultiKeyringInput.builder()
64+
.generator(keyArn)
65+
.build();
66+
IKeyring encryptKeyring = matProv.CreateAwsKmsMrkMultiKeyring(createAwsKmsMrkMultiKeyringInput);
67+
68+
// 2. Configure which attributes are encrypted and/or signed when writing new items.
69+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
70+
// we must explicitly configure how they should be treated during item encryption:
71+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and icncluded in the signature
72+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
73+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
74+
final Map<String, CryptoAction> attributeActions = new HashMap<>();
75+
attributeActions.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY
76+
attributeActions.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY
77+
attributeActions.put("sensitive_data", CryptoAction.ENCRYPT_AND_SIGN);
78+
79+
// 3. Configure which attributes we expect to be included in the signature
80+
// when reading items. There are two options for configuring this:
81+
//
82+
// - (Recommended) Configure `allowedUnauthenticatedAttributesPrefix`:
83+
// When defining your DynamoDb schema and deciding on attribute names,
84+
// choose a distinguishing prefix (such as ":") for all attributes that
85+
// you do not want to include in the signature.
86+
// This has two main benefits:
87+
// - It is easier to reason about the security and authenticity of data within your item
88+
// when all unauthenticated data is easily distinguishable by their attribute name.
89+
// - If you need to add new unauthenticated attributes in the future,
90+
// you can easily make the corresponding update to your `attributeActions`
91+
// and immediately start writing to that new attribute, without
92+
// any other configuration update needed.
93+
// Once you configure this field, it is not safe to update it.
94+
//
95+
// - Configure `allowedUnauthenticatedAttributes`: You may also explicitly list
96+
// a set of attributes that should be considered unauthenticated when encountered
97+
// on read. Be careful if you use this configuration. Do not remove an attribute
98+
// name from this configuration, even if you are no longer writing with that attribute,
99+
// as old items may still include this attribute, and our configuration needs to know
100+
// to continue to exclude this attribute from the signature scope.
101+
// If you add new attribute names to this field, you must first deploy the update to this
102+
// field to all readers in your host fleet before deploying the update to start writing
103+
// with that new attribute.
104+
//
105+
// For this example, we currently authenticate all attributes. To make it easier to
106+
// add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
107+
final String unauthAttrPrefix = ":";
108+
109+
// 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
110+
final Map<String, DynamoDbTableEncryptionConfig> tableConfigs = new HashMap<>();
111+
final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig.builder()
112+
.logicalTableName(ddbTableName)
113+
.partitionKeyName("partition_key")
114+
.sortKeyName("sort_key")
115+
.attributeActions(attributeActions)
116+
.keyring(encryptKeyring)
117+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
118+
.build();
119+
tableConfigs.put(ddbTableName, config);
120+
121+
// 5. Create the DynamoDb Encryption Interceptor
122+
DynamoDbEncryptionInterceptor encryptionInterceptor = DynamoDbEncryptionInterceptor.builder()
123+
.config(DynamoDbTablesEncryptionConfig.builder()
124+
.tableEncryptionConfigs(tableConfigs)
125+
.build())
126+
.build();
127+
128+
// 6. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
129+
final DynamoDbClient ddbClient = DynamoDbClient.builder()
130+
.overrideConfiguration(
131+
ClientOverrideConfiguration.builder()
132+
.addExecutionInterceptor(encryptionInterceptor)
133+
.build())
134+
.build();
135+
136+
// 7. Put an item into our table using the above client.
137+
// Before the item gets sent to DynamoDb, it will be encrypted
138+
// client-side using the MRK multi-keyring.
139+
final HashMap<String, AttributeValue> item = new HashMap<>();
140+
item.put("partition_key", AttributeValue.builder().s("awsKmsMrkDiscoveryMultiKeyringItem").build());
141+
item.put("sort_key", AttributeValue.builder().n("0").build());
142+
item.put("sensitive_data", AttributeValue.builder().s("encrypt and sign me!").build());
143+
144+
final PutItemRequest putRequest = PutItemRequest.builder()
145+
.tableName(ddbTableName)
146+
.item(item)
147+
.build();
148+
149+
final PutItemResponse putResponse = ddbClient.putItem(putRequest);
150+
151+
// Demonstrate that PutItem succeeded
152+
assert 200 == putResponse.sdkHttpResponse().statusCode();
153+
154+
// 8. Construct a discovery filter.
155+
// A discovery filter limits the set of encrypted data keys
156+
// the keyring can use to decrypt data.
157+
// We will only let the keyring use keys in the selected AWS accounts
158+
// and in the `aws` partition.
159+
// This is the suggested config for most users; for more detailed config, see
160+
// https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery
161+
DiscoveryFilter discoveryFilter = DiscoveryFilter.builder()
162+
.partition("aws")
163+
.accountIds(accountIds)
164+
.build();
165+
166+
// 9. Construct a discovery keyring.
167+
// Note that we choose to use the MRK discovery multi-keyring, even though
168+
// our original keyring used a single KMS key.
169+
CreateAwsKmsMrkDiscoveryMultiKeyringInput createAwsKmsMrkDiscoveryMultiKeyringInput =
170+
CreateAwsKmsMrkDiscoveryMultiKeyringInput.builder()
171+
.discoveryFilter(discoveryFilter)
172+
.regions(regions)
173+
.build();
174+
IKeyring decryptKeyring = matProv.CreateAwsKmsMrkDiscoveryMultiKeyring(
175+
createAwsKmsMrkDiscoveryMultiKeyringInput);
176+
177+
// 10. Create new DDB config and client using the decrypt discovery keyring.
178+
// This is the same as the above config, except we pass in the decrypt keyring.
179+
final Map<String, DynamoDbTableEncryptionConfig> tableConfigsForDecrypt = new HashMap<>();
180+
final DynamoDbTableEncryptionConfig configForDecrypt = DynamoDbTableEncryptionConfig.builder()
181+
.logicalTableName(ddbTableName)
182+
.partitionKeyName("partition_key")
183+
.sortKeyName("sort_key")
184+
.attributeActions(attributeActions)
185+
// Add decrypt keyring here
186+
.keyring(decryptKeyring)
187+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
188+
.build();
189+
tableConfigsForDecrypt.put(ddbTableName, configForDecrypt);
190+
191+
DynamoDbEncryptionInterceptor encryptionInterceptorForDecrypt = DynamoDbEncryptionInterceptor.builder()
192+
.config(DynamoDbTablesEncryptionConfig.builder()
193+
.tableEncryptionConfigs(tableConfigsForDecrypt)
194+
.build())
195+
.build();
196+
197+
final DynamoDbClient ddbClientForDecrypt = DynamoDbClient.builder()
198+
.overrideConfiguration(
199+
ClientOverrideConfiguration.builder()
200+
.addExecutionInterceptor(encryptionInterceptorForDecrypt)
201+
.build())
202+
.build();
203+
204+
// 11. Get the item back from our table using the client.
205+
// The client will retrieve encrypted items from the DDB table, then
206+
// detect the KMS key that was used to encrypt their data keys.
207+
// The client will make a request to KMS to decrypt with the encrypting KMS key.
208+
// If the client has permission to decrypt with the KMS key,
209+
// the client will decrypt the item client-side using the keyring
210+
// and return the original item.
211+
final HashMap<String, AttributeValue> keyToGet = new HashMap<>();
212+
keyToGet.put("partition_key", AttributeValue.builder().s("awsKmsMrkDiscoveryMultiKeyringItem").build());
213+
keyToGet.put("sort_key", AttributeValue.builder().n("0").build());
214+
215+
final GetItemRequest getRequest = GetItemRequest.builder()
216+
.key(keyToGet)
217+
.tableName(ddbTableName)
218+
.build();
219+
220+
final GetItemResponse getResponse = ddbClientForDecrypt.getItem(getRequest);
221+
222+
// Demonstrate that GetItem succeeded and returned the decrypted item
223+
assert 200 == getResponse.sdkHttpResponse().statusCode();
224+
final Map<String, AttributeValue> returnedItem = getResponse.item();
225+
assert returnedItem.get("sensitive_data").s().equals("encrypt and sign me!");
226+
}
227+
228+
public static void main(final String[] args) {
229+
if (args.length <= 1) {
230+
throw new IllegalArgumentException("To run this example, include the ddbTable, mrkKeyArn, accounts, and region in args");
231+
}
232+
final String ddbTableName = args[0];
233+
final String mrkArn = args[1];
234+
235+
// We will assume only 1 AWS account and 1 region will be passed into args.
236+
// To add more of either, change this number, then pass them into args.
237+
int numberOfAwsAccounts = 1;
238+
int numberOfRegions = 1;
239+
240+
List<String> accounts = new ArrayList<>();
241+
int firstAccountIndex = 2; // 2 because args[0] is ddbTableName and args[1] is mrkArn
242+
for (int i = firstAccountIndex; i < firstAccountIndex + numberOfAwsAccounts; i++) {
243+
accounts.add(args[i]);
244+
}
245+
List<String> regions = new ArrayList<>();
246+
int firstRegionIndex = firstAccountIndex + numberOfAwsAccounts;
247+
for (int i = firstRegionIndex; i < firstRegionIndex + numberOfRegions; i++) {
248+
regions.add(args[i]);
249+
}
250+
MultiMrkDiscoveryKeyringGetItemPutItem(ddbTableName, mrkArn, accounts, regions);
251+
}
252+
}

Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/aws/cryptography/examples/TestUtils.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ public class TestUtils {
55
public static final String TEST_LOGICAL_KEYSTORE_NAME = "KeyStoreTestTable";
66
public static final String TEST_KEYSTORE_KMS_KEY_ID = "arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126";
77

8+
public static final String TEST_AWS_ACCOUNT_ID = "658956600833";
9+
public static final String TEST_AWS_REGION = "us-west-2";
810
// These are public KMS Keys that MUST only be used for testing, and MUST NOT be used for any production data
911
public static final String TEST_KMS_KEY_ID = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f";
1012
public static final String TEST_MRK_KEY_ID = "arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package software.aws.cryptography.examples.keyring;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import org.testng.annotations.Test;
6+
import software.aws.cryptography.examples.TestUtils;
7+
8+
public class TestMrkDiscoveryMultiKeyringExample {
9+
@Test
10+
public void TestMrkDiscoveryMultiKeyringExample() {
11+
List<String> accounts = Collections.singletonList(TestUtils.TEST_AWS_ACCOUNT_ID);
12+
List<String> regions = Collections.singletonList(TestUtils.TEST_AWS_REGION);
13+
14+
MrkDiscoveryMultiKeyringExample.MultiMrkDiscoveryKeyringGetItemPutItem(
15+
TestUtils.TEST_DDB_TABLE_NAME,
16+
TestUtils.TEST_MRK_KEY_ID,
17+
accounts,
18+
regions);
19+
}
20+
}

0 commit comments

Comments
 (0)