Skip to content

Commit 62fab00

Browse files
feat(Examples): Add multi-keyring example (#172)
1 parent 511be10 commit 62fab00

File tree

2 files changed

+309
-0
lines changed

2 files changed

+309
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
package software.aws.cryptography.examples.keyring;
2+
3+
import com.amazonaws.services.dynamodbv2.datamodeling.internal.Utils;
4+
import java.nio.ByteBuffer;
5+
import java.security.NoSuchAlgorithmException;
6+
import java.security.SecureRandom;
7+
import java.util.Collections;
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
import javax.crypto.KeyGenerator;
11+
import javax.crypto.SecretKey;
12+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
13+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
14+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
15+
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
16+
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
17+
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
18+
import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;
19+
import software.amazon.awssdk.services.kms.KmsClient;
20+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig;
21+
import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig;
22+
import software.amazon.cryptography.materialproviders.IKeyring;
23+
import software.amazon.cryptography.materialproviders.MaterialProviders;
24+
import software.amazon.cryptography.materialproviders.model.AesWrappingAlg;
25+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsKeyringInput;
26+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkMultiKeyringInput;
27+
import software.amazon.cryptography.materialproviders.model.CreateMultiKeyringInput;
28+
import software.amazon.cryptography.materialproviders.model.CreateRawAesKeyringInput;
29+
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
30+
import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction;
31+
import software.aws.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor;
32+
33+
/*
34+
This example sets up DynamoDb Encryption for the AWS SDK client
35+
using the multi-keyring. This keyring takes in multiple keyrings
36+
and uses them to encrypt and decrypt data. Data encrypted with
37+
a multi-keyring can be decrypted with any of its component keyrings.
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 multi-keyring consisting of an AWS KMS
43+
keyring (labeled the "generator keyring") and a raw AES keyring
44+
(labeled as the only "child keyring"). It encrypts a test item
45+
using the multi-keyring and puts the encrypted item to the provided
46+
DynamoDb table. Then, it gets the item from the table and decrypts it
47+
using only the raw AES keyring.
48+
49+
This example takes in an `aesKeyBytes` parameter. This parameter
50+
should be a ByteBuffer representing a 256-bit AES key. If this example
51+
is run through the class' main method, it will create a new key.
52+
In practice, users of this library should not randomly generate a key,
53+
and should instead retrieve an existing key from a secure key
54+
management system (e.g. an HSM).
55+
56+
Running this example requires access to the DDB Table whose name
57+
is provided in CLI arguments.
58+
This table must be configured with the following
59+
primary key configuration:
60+
- Partition key is named "partition_key" with type (S)
61+
- Sort key is named "sort_key" with type (S)
62+
*/
63+
public class MultiKeyringExample {
64+
65+
public static void MultiKeyringGetItemPutItem(String ddbTableName, String keyArn, ByteBuffer aesKeyBytes) {
66+
// 1. Create the raw AES keyring.
67+
final MaterialProviders matProv = MaterialProviders.builder()
68+
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
69+
.build();
70+
final CreateRawAesKeyringInput createRawAesKeyringInput = CreateRawAesKeyringInput.builder()
71+
.keyName("my-aes-key-name")
72+
.keyNamespace("my-key-namespace")
73+
.wrappingKey(aesKeyBytes)
74+
.wrappingAlg(AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16)
75+
.build();
76+
IKeyring rawAesKeyring = matProv.CreateRawAesKeyring(createRawAesKeyringInput);
77+
78+
// 2. Create the AWS KMS keyring.
79+
// We create a MRK multi keyring, as this interface also supports
80+
// single-region KMS keys (standard KMS keys),
81+
// and creates the KMS client for us automatically.
82+
final CreateAwsKmsMrkMultiKeyringInput createAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput.builder()
83+
.generator(keyArn)
84+
.build();
85+
IKeyring awsKmsMrkMultiKeyring = matProv.CreateAwsKmsMrkMultiKeyring(createAwsKmsMrkMultiKeyringInput);
86+
87+
// 3. Create the multi-keyring.
88+
// We will label the AWS KMS keyring as the generator and the raw AES keyring as the
89+
// only child keyring.
90+
// You must provide a generator keyring to encrypt data.
91+
// You may provide additional child keyrings. Each child keyring will be able to
92+
// decrypt data encrypted with the multi-keyring on its own. It does not need
93+
// knowledge of any other child keyrings or the generator keyring to decrypt.
94+
final CreateMultiKeyringInput createMultiKeyringInput = CreateMultiKeyringInput.builder()
95+
.generator(awsKmsMrkMultiKeyring)
96+
.childKeyrings(Collections.singletonList(rawAesKeyring))
97+
.build();
98+
IKeyring multiKeyring = matProv.CreateMultiKeyring(createMultiKeyringInput);
99+
100+
// 4. Configure which attributes are encrypted and/or signed when writing new items.
101+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
102+
// we must explicitly configure how they should be treated during item encryption:
103+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
104+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
105+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
106+
final Map<String, CryptoAction> attributeActions = new HashMap<>();
107+
attributeActions.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY
108+
attributeActions.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY
109+
attributeActions.put("sensitive_data", CryptoAction.ENCRYPT_AND_SIGN);
110+
111+
// 5. Configure which attributes we expect to be included in the signature
112+
// when reading items. There are two options for configuring this:
113+
//
114+
// - (Recommended) Configure `allowedUnauthenticatedAttributesPrefix`:
115+
// When defining your DynamoDb schema and deciding on attribute names,
116+
// choose a distinguishing prefix (such as ":") for all attributes that
117+
// you do not want to include in the signature.
118+
// This has two main benefits:
119+
// - It is easier to reason about the security and authenticity of data within your item
120+
// when all unauthenticated data is easily distinguishable by their attribute name.
121+
// - If you need to add new unauthenticated attributes in the future,
122+
// you can easily make the corresponding update to your `attributeActions`
123+
// and immediately start writing to that new attribute, without
124+
// any other configuration update needed.
125+
// Once you configure this field, it is not safe to update it.
126+
//
127+
// - Configure `allowedUnauthenticatedAttributes`: You may also explicitly list
128+
// a set of attributes that should be considered unauthenticated when encountered
129+
// on read. Be careful if you use this configuration. Do not remove an attribute
130+
// name from this configuration, even if you are no longer writing with that attribute,
131+
// as old items may still include this attribute, and our configuration needs to know
132+
// to continue to exclude this attribute from the signature scope.
133+
// If you add new attribute names to this field, you must first deploy the update to this
134+
// field to all readers in your host fleet before deploying the update to start writing
135+
// with that new attribute.
136+
//
137+
// For this example, we currently authenticate all attributes. To make it easier to
138+
// add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
139+
final String unauthAttrPrefix = ":";
140+
141+
// 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
142+
// Note that this example creates one config/client combination for PUT, and another
143+
// for GET. The PUT config uses the multi-keyring, while the GET config uses the
144+
// raw AES keyring. This is solely done to demonstrate that a keyring included as
145+
// a child of a multi-keyring can be used to decrypt data on its own.
146+
final Map<String, DynamoDbTableEncryptionConfig> tableConfigs = new HashMap<>();
147+
final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig.builder()
148+
.logicalTableName(ddbTableName)
149+
.partitionKeyName("partition_key")
150+
.sortKeyName("sort_key")
151+
.attributeActions(attributeActions)
152+
// Multi-keyring is added here
153+
.keyring(multiKeyring)
154+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
155+
.build();
156+
tableConfigs.put(ddbTableName, config);
157+
158+
// 7. Create the DynamoDb Encryption Interceptor
159+
DynamoDbEncryptionInterceptor encryptionInterceptor = DynamoDbEncryptionInterceptor.builder()
160+
.config(DynamoDbTablesEncryptionConfig.builder()
161+
.tableEncryptionConfigs(tableConfigs)
162+
.build())
163+
.build();
164+
165+
// 8. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
166+
final DynamoDbClient ddbClient = DynamoDbClient.builder()
167+
.overrideConfiguration(
168+
ClientOverrideConfiguration.builder()
169+
.addExecutionInterceptor(encryptionInterceptor)
170+
.build())
171+
.build();
172+
173+
// 9. Put an item into our table using the above client.
174+
// Before the item gets sent to DynamoDb, it will be encrypted
175+
// client-side using the multi-keyring.
176+
// The item will be encrypted with all wrapping keys in the keyring,
177+
// so that it can be decrypted with any one of the keys.
178+
final HashMap<String, AttributeValue> item = new HashMap<>();
179+
item.put("partition_key", AttributeValue.builder().s("multiKeyringItem").build());
180+
item.put("sort_key", AttributeValue.builder().n("0").build());
181+
item.put("sensitive_data", AttributeValue.builder().s("encrypt and sign me!").build());
182+
183+
final PutItemRequest putRequest = PutItemRequest.builder()
184+
.tableName(ddbTableName)
185+
.item(item)
186+
.build();
187+
188+
final PutItemResponse putResponse = ddbClient.putItem(putRequest);
189+
190+
// Demonstrate that PutItem succeeded
191+
assert 200 == putResponse.sdkHttpResponse().statusCode();
192+
193+
// 10. Get the item back from our table using the above client.
194+
// The client will decrypt the item client-side using the AWS KMS
195+
// keyring, and return back the original item.
196+
// Since the generator key is the first available key in the keyring,
197+
// that is the key that will be used to decrypt this item.
198+
final HashMap<String, AttributeValue> keyToGet = new HashMap<>();
199+
keyToGet.put("partition_key", AttributeValue.builder().s("multiKeyringItem").build());
200+
keyToGet.put("sort_key", AttributeValue.builder().n("0").build());
201+
202+
final GetItemRequest getRequest = GetItemRequest.builder()
203+
.key(keyToGet)
204+
.tableName(ddbTableName)
205+
.build();
206+
207+
final GetItemResponse getResponse = ddbClient.getItem(getRequest);
208+
209+
// Demonstrate that GetItem succeeded and returned the decrypted item
210+
assert 200 == getResponse.sdkHttpResponse().statusCode();
211+
final Map<String, AttributeValue> returnedItem = getResponse.item();
212+
assert returnedItem.get("sensitive_data").s().equals("encrypt and sign me!");
213+
214+
// 11. Create a new config and client with only the raw AES keyring to GET the item
215+
// This is the same setup as above, except the config uses the `rawAesKeyring`.
216+
final Map<String, DynamoDbTableEncryptionConfig> onlyAesKeyringTableConfigs = new HashMap<>();
217+
final DynamoDbTableEncryptionConfig onlyAesKeyringConfig = DynamoDbTableEncryptionConfig.builder()
218+
.logicalTableName(ddbTableName)
219+
.partitionKeyName("partition_key")
220+
.sortKeyName("sort_key")
221+
.attributeActions(attributeActions)
222+
// Raw AES keyring is added here
223+
.keyring(rawAesKeyring)
224+
.allowedUnauthenticatedAttributePrefix(unauthAttrPrefix)
225+
.build();
226+
onlyAesKeyringTableConfigs.put(ddbTableName, onlyAesKeyringConfig);
227+
228+
DynamoDbEncryptionInterceptor onlyAesKeyringEncryptionInterceptor = DynamoDbEncryptionInterceptor.builder()
229+
.config(DynamoDbTablesEncryptionConfig.builder()
230+
.tableEncryptionConfigs(onlyAesKeyringTableConfigs)
231+
.build())
232+
.build();
233+
234+
final DynamoDbClient onlyAesKeyringDdbClient = DynamoDbClient.builder()
235+
.overrideConfiguration(
236+
ClientOverrideConfiguration.builder()
237+
.addExecutionInterceptor(onlyAesKeyringEncryptionInterceptor)
238+
.build())
239+
.build();
240+
241+
// 12. Get the item back from our table using the client
242+
// configured with only the raw AES keyring.
243+
// The client will decrypt the item client-side using the raw
244+
// AES keyring, and return back the original item.
245+
final HashMap<String, AttributeValue> onlyAesKeyringKeyToGet = new HashMap<>();
246+
onlyAesKeyringKeyToGet.put("partition_key", AttributeValue.builder().s("multiKeyringItem").build());
247+
onlyAesKeyringKeyToGet.put("sort_key", AttributeValue.builder().n("0").build());
248+
249+
final GetItemRequest onlyAesKeyringGetRequest = GetItemRequest.builder()
250+
.key(onlyAesKeyringKeyToGet)
251+
.tableName(ddbTableName)
252+
.build();
253+
254+
final GetItemResponse onlyAesKeyringGetResponse = onlyAesKeyringDdbClient.getItem(onlyAesKeyringGetRequest);
255+
256+
// Demonstrate that GetItem succeeded and returned the decrypted item
257+
assert 200 == getResponse.sdkHttpResponse().statusCode();
258+
final Map<String, AttributeValue> onlyAesKeyringReturnedItem = onlyAesKeyringGetResponse.item();
259+
assert onlyAesKeyringReturnedItem.get("sensitive_data").s().equals("encrypt and sign me!");
260+
}
261+
262+
public static void main(final String[] args) {
263+
if (args.length <= 1) {
264+
throw new IllegalArgumentException("To run this example, include the ddbTable and keyArn in args");
265+
}
266+
final String ddbTableName = args[0];
267+
final String keyArn = args[1];
268+
269+
// Generate a new AES key
270+
ByteBuffer aesKeyBytes = generateAesKeyBytes();
271+
272+
MultiKeyringGetItemPutItem(ddbTableName, keyArn, aesKeyBytes);
273+
}
274+
275+
static ByteBuffer generateAesKeyBytes() {
276+
// This example uses BouncyCastle's KeyGenerator to generate the key bytes.
277+
// In practice, you should not generate this key in your code, and should instead
278+
// retrieve this key from a secure key management system (e.g. HSM).
279+
// This key is created here for example purposes only and should not be used for any other purpose.
280+
KeyGenerator aesGen;
281+
try {
282+
aesGen = KeyGenerator.getInstance("AES");
283+
} catch (NoSuchAlgorithmException e) {
284+
throw new RuntimeException("No such algorithm", e);
285+
}
286+
aesGen.init(256, new SecureRandom());
287+
SecretKey encryptionKey = aesGen.generateKey();
288+
ByteBuffer encryptionKeyByteBuffer = ByteBuffer.wrap(encryptionKey.getEncoded());
289+
return encryptionKeyByteBuffer;
290+
}
291+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package software.aws.cryptography.examples.keyring;
2+
3+
import java.nio.ByteBuffer;
4+
import org.testng.annotations.Test;
5+
import software.aws.cryptography.examples.TestUtils;
6+
7+
public class TestMultiKeyringExample {
8+
@Test
9+
public void TestMultiKeyringExample() {
10+
// Generate a new AES key
11+
ByteBuffer aesKeyBytes = RawAesKeyringExample.generateAesKeyBytes();
12+
13+
MultiKeyringExample.MultiKeyringGetItemPutItem(
14+
TestUtils.TEST_DDB_TABLE_NAME,
15+
TestUtils.TEST_KMS_KEY_ID,
16+
aesKeyBytes);
17+
}
18+
}

0 commit comments

Comments
 (0)