Skip to content

Commit 1e6a8de

Browse files
committed
chore(examples): Shared cache across Hierarchical Keyrings
1 parent 7cfe049 commit 1e6a8de

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.amazonaws.crypto.examples.keyrings.hierarchical;
5+
6+
import com.amazonaws.encryptionsdk.AwsCrypto;
7+
import com.amazonaws.encryptionsdk.CryptoResult;
8+
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
9+
import software.amazon.awssdk.services.kms.KmsClient;
10+
import software.amazon.cryptography.keystore.KeyStore;
11+
import software.amazon.cryptography.keystore.model.CreateKeyInput;
12+
import software.amazon.cryptography.keystore.model.KMSConfiguration;
13+
import software.amazon.cryptography.keystore.model.KeyStoreConfig;
14+
import software.amazon.cryptography.materialproviders.CryptographicMaterialsCache;
15+
import software.amazon.cryptography.materialproviders.ICryptographicMaterialsCache;
16+
import software.amazon.cryptography.materialproviders.IKeyring;
17+
import software.amazon.cryptography.materialproviders.MaterialProviders;
18+
import software.amazon.cryptography.materialproviders.model.CacheType;
19+
import software.amazon.cryptography.materialproviders.model.CreateAwsKmsHierarchicalKeyringInput;
20+
import software.amazon.cryptography.materialproviders.model.DefaultCache;
21+
import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
22+
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.Arrays;
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.concurrent.ExecutorService;
29+
import java.util.concurrent.Executors;
30+
import java.util.concurrent.atomic.AtomicInteger;
31+
32+
/**
33+
* This example demonstrates how to use a shared cache across multiple Hierarchical Keyrings.
34+
* With this functionality, users only need to maintain one common shared cache across multiple
35+
* Hierarchical Keyrings with different Key Stores instances/KMS Clients/KMS Keys.
36+
*
37+
* <p>There are two important parameters that users need to carefully set while providing the shared cache:
38+
*
39+
* <p>Partition ID - Partition ID is an optional parameter provided to the Hierarchical Keyring input,
40+
* which distinguishes Cryptographic Material Providers (i.e: Keyrings) writing to a cache.
41+
* - If the Partition ID is set and is the same for two Hierarchical Keyrings (or another Material Provider),
42+
* they CAN share the same cache entries in the cache.
43+
* - If the Partition ID is set and is different for two Hierarchical Keyrings (or another Material Provider),
44+
* they CANNOT share the same cache entries in the cache.
45+
* - If the Partition ID is not set by the user, it is initialized as a random 16-byte UUID which makes
46+
* it unique for every Hierarchical Keyring, and two Hierarchical Keyrings (or another Material Provider)
47+
* CANNOT share the same cache entries in the cache.
48+
*
49+
* <p>Logical Key Store Name - This parameter is set by the user when configuring the Key Store for
50+
* the Hierarchical Keyring. This is a logical name for the branch key store.
51+
* Suppose you have a physical Key Store (K). You create two instances of K (K1 and K2). Now, you create
52+
* two Hierarchical Keyrings (HK1 and HK2) with these Key Store instances (K1 and K2 respectively).
53+
* - If you want to share cache entries across these two keyrings, you should set the Logical Key Store Names
54+
* for both the Key Store instances (K1 and K2) to be the same.
55+
* - If you set the Logical Key Store Names for K1 and K2 to be different, HK1 (which uses Key Store instance K1)
56+
* and HK2 (which uses Key Store instance K2) will NOT be able to share cache entries.
57+
*
58+
* This is demonstrated in the example below.
59+
* Notice that both K1 and K2 are instances of the same physical Key Store (K).
60+
* You MUST NEVER have two different physical Key Stores with the same Logical Key Store Name.
61+
*
62+
* Important Note: If you have two or more Hierarchy Keyrings with:
63+
* - Same Partition ID
64+
* - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
65+
* - Same Branch Key ID
66+
* then they WILL share the cache entries in the Shared Cache.
67+
* Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID
68+
* to be the same for two Hierarchical Keyrings only if you want them to share cache entries.
69+
*
70+
* <p>This example first creates a shared cache that you can use across multiple Hierarchical Keyrings.
71+
* The example then configures a Hierarchical Keyring (HK1 and HK2) with the shared cache,
72+
* a Branch Key ID and two instances (K1 and K2) of the same physical Key Store (K) respectively,
73+
* i.e. HK1 with K1 and HK2 with K2. The example demonstrates that if you set the same Partition ID
74+
* for HK1 and HK2, the two keyrings can share cache entries.
75+
* If you set different Partition ID of the Hierarchical Keyrings, or different
76+
* Logical Key Store Names of the Key Store instances, then the keyrings will NOT
77+
* be able to share cache entries.
78+
*
79+
* <p>This example requires access to the DDB Table (K) where you are storing the Branch Keys. This
80+
* table must be configured with the following primary key configuration: - Partition key is named
81+
* "partition_key" with type (S) - Sort key is named "sort_key" with type (S)
82+
*
83+
* <p>This example also requires using a KMS Key. You need the following access on this key: -
84+
* GenerateDataKeyWithoutPlaintext - Decrypt
85+
*/
86+
public class SharedCacheAcrossHierarchicalKeyringsExample {
87+
private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8);
88+
89+
public static void encryptAndDecryptWithKeyring(
90+
String keyStoreTableName, String logicalKeyStoreName, String partitionId, String kmsKeyId) {
91+
// Create the CryptographicMaterialsCache (CMC) to share across multiple Hierarchical Keyrings
92+
// using the Material Providers Library
93+
// This CMC takes in:
94+
// - CacheType
95+
final MaterialProviders matProv =
96+
MaterialProviders.builder()
97+
.MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
98+
.build();
99+
100+
final CacheType cache =
101+
CacheType.builder()
102+
.Default(DefaultCache.builder().entryCapacity(100).build())
103+
.build();
104+
105+
final CreateCryptographicMaterialsCacheInput cryptographicMaterialsCacheInput =
106+
CreateCryptographicMaterialsCacheInput.builder()
107+
.cache(cache)
108+
.build();
109+
110+
final ICryptographicMaterialsCache sharedCryptographicMaterialsCache =
111+
matProv.CreateCryptographicMaterialsCache(cryptographicMaterialsCacheInput);
112+
113+
// Create a CacheType object for the sharedCryptographicMaterialsCache
114+
// Note that the `cache` parameter in the Hierarchical Keyring Input takes a `CacheType` as input
115+
final CacheType sharedCache =
116+
CacheType.builder()
117+
// This is the `Shared` CacheType that passes an already initialized shared cache
118+
.Shared(sharedCryptographicMaterialsCache)
119+
.build();
120+
121+
// Instantiate the SDK
122+
// This builds the AwsCrypto client with the RequireEncryptRequireDecrypt commitment policy,
123+
// which enforces that this client only encrypts using committing algorithm suites and enforces
124+
// that this client will only decrypt encrypted messages that were created with a committing
125+
// algorithm suite.
126+
// This is the default commitment policy if you build the client with
127+
// `AwsCrypto.builder().build()`
128+
// or `AwsCrypto.standard()`.
129+
final AwsCrypto crypto = AwsCrypto.builder().build();
130+
131+
// Configure your KeyStore resource keystore1.
132+
// This SHOULD be the same configuration that you used
133+
// to initially create and populate your physical KeyStore.
134+
// Note that ddbTableName keyStoreTableName is the physical Key Store,
135+
// and keystore1 is instances of this physical Key Store.
136+
final KeyStore keystore1 =
137+
KeyStore.builder()
138+
.KeyStoreConfig(
139+
KeyStoreConfig.builder()
140+
.ddbClient(DynamoDbClient.create())
141+
.ddbTableName(keyStoreTableName)
142+
.logicalKeyStoreName(logicalKeyStoreName)
143+
.kmsClient(KmsClient.create())
144+
.kmsConfiguration(KMSConfiguration.builder().kmsKeyArn(kmsKeyId).build())
145+
.build())
146+
.build();
147+
148+
// Call CreateKey to create a new active branch key
149+
final String branchKeyId =
150+
keystore1.CreateKey(CreateKeyInput.builder().build()).branchKeyIdentifier();
151+
152+
// Create the Hierarchical Keyring HK1 with Key Store instance K1, partitionId,
153+
// the shared Cache and the BranchKeyId.
154+
// Note that we are now providing an already initialized shared cache instead of just mentioning
155+
// the cache type and the Hierarchical Keyring initializing a cache at initialization.
156+
final CreateAwsKmsHierarchicalKeyringInput keyringInput1 =
157+
CreateAwsKmsHierarchicalKeyringInput.builder()
158+
.keyStore(keystore1)
159+
.branchKeyId(branchKeyId)
160+
.ttlSeconds(600)
161+
.cache(sharedCache)
162+
.partitionId(partitionId)
163+
.build();
164+
final IKeyring hierarchicalKeyring1 = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput1);
165+
166+
// Create example encryption context
167+
Map<String, String> encryptionContext = new HashMap<>();
168+
encryptionContext.put("encryption", "context");
169+
encryptionContext.put("is not", "secret");
170+
encryptionContext.put("but adds", "useful metadata");
171+
encryptionContext.put("that can help you", "be confident that");
172+
encryptionContext.put("the data you are handling", "is what you think it is");
173+
174+
// Encrypt the data for encryptionContext using hierarchicalKeyring1
175+
final CryptoResult<byte[], ?> encryptResult1 =
176+
crypto.encryptData(hierarchicalKeyring1, EXAMPLE_DATA, encryptionContext);
177+
178+
// Decrypt your encrypted data using the same keyring HK1 you used on encrypt.
179+
final CryptoResult<byte[], ?> decryptResult1 =
180+
crypto.decryptData(hierarchicalKeyring1, encryptResult1.getResult());
181+
assert Arrays.equals(decryptResult1.getResult(), EXAMPLE_DATA);
182+
183+
// Through the above encrypt and decrypt roundtrip, the cache will be populated and
184+
// the cache entries can be used by another Hierarchical Keyring with the
185+
// - Same Partition ID
186+
// - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
187+
// - Same Branch Key ID
188+
189+
// Configure your KeyStore resource keystore2.
190+
// This SHOULD be the same configuration that you used
191+
// to initially create and populate your physical KeyStore.
192+
// Note that ddbTableName keyStoreTableName is the physical Key Store,
193+
// and keystore2 is instances of this physical Key Store.
194+
195+
// Note that for this example, keystore2 is identical to keystore1.
196+
// You can optionally change configurations like KMS Client or KMS Key ID based
197+
// on your use-case.
198+
// Make sure you have the required permissions to use different configurations.
199+
200+
// - If you want to share cache entries across two keyrings HK1 and HK2,
201+
// you should set the Logical Key Store Names for both
202+
// Key Store instances (K1 and K2) to be the same.
203+
// - If you set the Logical Key Store Names for K1 and K2 to be different,
204+
// HK1 (which uses Key Store instance K1) and HK2 (which uses Key Store
205+
// instance K2) will NOT be able to share cache entries.
206+
final KeyStore keystore2 =
207+
KeyStore.builder()
208+
.KeyStoreConfig(
209+
KeyStoreConfig.builder()
210+
.ddbClient(DynamoDbClient.create())
211+
.ddbTableName(keyStoreTableName)
212+
.logicalKeyStoreName(logicalKeyStoreName)
213+
.kmsClient(KmsClient.create())
214+
.kmsConfiguration(KMSConfiguration.builder().kmsKeyArn(kmsKeyId).build())
215+
.build())
216+
.build();
217+
218+
// Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
219+
// and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries
220+
// (and experience cache HITS).
221+
final CreateAwsKmsHierarchicalKeyringInput keyringInput2 =
222+
CreateAwsKmsHierarchicalKeyringInput.builder()
223+
.keyStore(keystore2)
224+
.branchKeyId(branchKeyId)
225+
.ttlSeconds(600)
226+
.cache(sharedCache)
227+
.partitionId(partitionId)
228+
.build();
229+
final IKeyring hierarchicalKeyring2 = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput2);
230+
231+
// Encrypt the data for encryptionContext using hierarchicalKeyring2
232+
final CryptoResult<byte[], ?> encryptResult2 =
233+
crypto.encryptData(hierarchicalKeyring2, EXAMPLE_DATA, encryptionContext);
234+
235+
// Decrypt your encrypted data using the same keyring HK2 you used on encrypt.
236+
final CryptoResult<byte[], ?> decryptResult2 =
237+
crypto.decryptData(hierarchicalKeyring2, encryptResult2.getResult());
238+
assert Arrays.equals(decryptResult2.getResult(), EXAMPLE_DATA);
239+
}
240+
241+
public static void main(final String[] args) {
242+
if (args.length <= 0) {
243+
throw new IllegalArgumentException(
244+
"To run this example, include the keyStoreTableName, logicalKeyStoreName, partitionId, and kmsKeyId in args");
245+
}
246+
final String keyStoreTableName = args[0];
247+
final String logicalKeyStoreName = args[1];
248+
final String partitionId = args[2];
249+
final String kmsKeyId = args[3];
250+
encryptAndDecryptWithKeyring(keyStoreTableName, logicalKeyStoreName, partitionId, kmsKeyId);
251+
}
252+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.amazonaws.crypto.examples.keyrings;
5+
6+
import com.amazonaws.crypto.examples.keyrings.hierarchical.SharedCacheAcrossHierarchicalKeyringsExample;
7+
import com.amazonaws.encryptionsdk.kms.KMSTestFixtures;
8+
import org.junit.Test;
9+
10+
public class SharedCacheAcrossHierarchicalKeyringsExampleTest {
11+
@Test
12+
public void testEncryptAndDecrypt() {
13+
SharedCacheAcrossHierarchicalKeyringsExample.encryptAndDecryptWithKeyring(
14+
KMSTestFixtures.TEST_KEYSTORE_NAME,
15+
KMSTestFixtures.TEST_LOGICAL_KEYSTORE_NAME,
16+
KMSTestFixtures.HIERARCHY_KEYRING_PARTITION_ID,
17+
KMSTestFixtures.TEST_KEYSTORE_KMS_KEY_ID);
18+
}
19+
}

src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package com.amazonaws.encryptionsdk;
55

66
import com.amazonaws.crypto.examples.keyrings.AwsKmsHierarchicalKeyringExampleTest;
7+
import com.amazonaws.crypto.examples.keyrings.SharedCacheAcrossHierarchicalKeyringsExampleTest;
78
import com.amazonaws.crypto.examples.keyrings.AwsKmsRsaKeyringExampleTest;
89
import com.amazonaws.crypto.examples.keyrings.BasicEncryptionKeyringExampleTest;
910
import com.amazonaws.crypto.examples.keyrings.DiscoveryDecryptionKeyringExampleTest;
@@ -120,6 +121,7 @@
120121
AwsKmsRsaKeyringExampleTest.class,
121122
DiscoveryDecryptionKeyringExampleTest.class,
122123
AwsKmsHierarchicalKeyringExampleTest.class,
124+
SharedCacheAcrossHierarchicalKeyringsExampleTest.class,
123125
SetCommitmentPolicyExampleTest.class,
124126
SetEncryptionAlgorithmKeyringExampleTest.class,
125127
ParsedCiphertextTest.class,

src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ private KMSTestFixtures() {
3737
public static final String TEST_LOGICAL_KEYSTORE_NAME = "KeyStoreDdbTable";
3838
public static final String TEST_KEYSTORE_KMS_KEY_ID =
3939
"arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126";
40+
public static final String HIERARCHY_KEYRING_PARTITION_ID = "partition_id";
4041
}

0 commit comments

Comments
 (0)