main/keyring/aws_kms_hierarchical/shared_cache_across_hierarchical_keyrings_example.rs
1// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4/*
5 This example demonstrates how to use a shared cache across multiple Hierarchical Keyrings.
6 With this functionality, users only need to maintain one common shared cache across multiple
7 Hierarchical Keyrings with different Key Stores instances/KMS Clients/KMS Keys.
8
9 If you want to use a Shared Cache, you need to initialize it only once, and
10 pass the same cache `shared_cache` to different hierarchical keyrings.
11
12 There are three important parameters that users need to carefully set while providing the shared cache:
13
14 1. Partition ID - Partition ID is an optional parameter provided to the Hierarchical Keyring input,
15 which distinguishes Cryptographic Material Providers (i.e: Keyrings) writing to a cache.
16 - If the Partition ID is set and is the same for two Hierarchical Keyrings (or another Material Provider),
17 they CAN share the same cache entries in the cache.
18 - If the Partition ID is set and is different for two Hierarchical Keyrings (or another Material Provider),
19 they CANNOT share the same cache entries in the cache.
20 - If the Partition ID is not set by the user, it is initialized as a random 16-byte UUID which makes
21 it unique for every Hierarchical Keyring, and two Hierarchical Keyrings (or another Material Provider)
22 CANNOT share the same cache entries in the cache.
23
24 2. Logical Key Store Name - This parameter is set by the user when configuring the Key Store for
25 the Hierarchical Keyring. This is a logical name for the branch key store.
26 Suppose you have a physical Key Store (K). You create two instances of K (K1 and K2). Now, you create
27 two Hierarchical Keyrings (HK1 and HK2) with these Key Store instances (K1 and K2 respectively).
28 - If you want to share cache entries across these two keyrings, you should set the Logical Key Store Names
29 for both the Key Store instances (K1 and K2) to be the same.
30 - If you set the Logical Key Store Names for K1 and K2 to be different, HK1 (which uses Key Store instance K1)
31 and HK2 (which uses Key Store instance K2) will NOT be able to share cache entries.
32
33 3. Branch Key ID - Choose an effective Branch Key ID Schema
34
35 This is demonstrated in the example below.
36 Notice that both K1 and K2 are instances of the same physical Key Store (K).
37 You MUST NEVER have two different physical Key Stores with the same Logical Key Store Name.
38
39 Important Note: If you have two or more Hierarchy Keyrings with:
40 - Same Partition ID
41 - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
42 - Same Branch Key ID
43 then they WILL share the cache entries in the Shared Cache.
44 Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID
45 to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries.
46
47 This example first creates a shared cache that you can use across multiple Hierarchical Keyrings.
48 The example then configures a Hierarchical Keyring (HK1 and HK2) with the shared cache,
49 a Branch Key ID and two instances (K1 and K2) of the same physical Key Store (K) respectively,
50 i.e. HK1 with K1 and HK2 with K2. The example demonstrates that if you set the same Partition ID
51 for HK1 and HK2, the two keyrings can share cache entries.
52 If you set different Partition ID of the Hierarchical Keyrings, or different
53 Logical Key Store Names of the Key Store instances, then the keyrings will NOT
54 be able to share cache entries.
55
56 This example requires access to the DDB Table (K) where you are storing the Branch Keys. This
57 table must be configured with the following primary key configuration: - Partition key is named
58 "partition_key" with type (S) - Sort key is named "sort_key" with type (S)
59
60 This example also requires using a KMS Key. You need the following access on this key:
61 - GenerateDataKeyWithoutPlaintext
62 - Decrypt
63*/
64
65use super::create_branch_key_id::create_branch_key_id;
66use aws_esdk::client as esdk_client;
67use aws_esdk::key_store::client as keystore_client;
68use aws_esdk::key_store::types::key_store_config::KeyStoreConfig;
69use aws_esdk::key_store::types::KmsConfiguration;
70use aws_esdk::material_providers::client as mpl_client;
71use aws_esdk::material_providers::types::cryptographic_materials_cache::CryptographicMaterialsCacheRef;
72use aws_esdk::material_providers::types::material_providers_config::MaterialProvidersConfig;
73use aws_esdk::material_providers::types::CacheType;
74use aws_esdk::material_providers::types::DefaultCache;
75use aws_esdk::types::aws_encryption_sdk_config::AwsEncryptionSdkConfig;
76use std::collections::HashMap;
77
78pub async fn encrypt_and_decrypt_with_keyring(
79 example_data: &str,
80 key_store_table_name: &str,
81 logical_key_store_name: &str,
82 key_store_kms_key_id: &str,
83) -> Result<(), crate::BoxError> {
84 // 1a. Create the CryptographicMaterialsCache (CMC) to share across multiple Hierarchical Keyrings
85 // using the Material Providers Library
86 // This CMC takes in:
87 // - CacheType
88 let mpl_config = MaterialProvidersConfig::builder().build()?;
89 let mpl = mpl_client::Client::from_conf(mpl_config)?;
90
91 let cache: CacheType = CacheType::Default(DefaultCache::builder().entry_capacity(100).build()?);
92
93 let shared_cryptographic_materials_cache: CryptographicMaterialsCacheRef = mpl
94 .create_cryptographic_materials_cache()
95 .cache(cache)
96 .send()
97 .await?;
98
99 // 1b. Create a CacheType object for the shared_cryptographic_materials_cache
100 // Note that the `cache` parameter in the Hierarchical Keyring Input takes a `CacheType` as input
101 // Here, we pass a `Shared` CacheType that passes an already initialized shared cache.
102
103 // If you want to use a Shared Cache, you need to initialize it only once, and
104 // pass the same cache `shared_cache` to different hierarchical keyrings.
105
106 // CryptographicMaterialsCacheRef is an Rc (Reference Counted), so if you clone it to
107 // pass it to different Hierarchical Keyrings, it will still point to the same
108 // underlying cache, and increment the reference count accordingly.
109 let shared_cache: CacheType = CacheType::Shared(shared_cryptographic_materials_cache);
110
111 // 2. Instantiate the encryption SDK client.
112 // This builds the default client with the RequireEncryptRequireDecrypt commitment policy,
113 // which enforces that this client only encrypts using committing algorithm suites and enforces
114 // that this client will only decrypt encrypted messages that were created with a committing
115 // algorithm suite.
116 let esdk_config = AwsEncryptionSdkConfig::builder().build()?;
117 let esdk_client = esdk_client::Client::from_conf(esdk_config)?;
118
119 // 3. Configure your Key Store resource key_store1.
120 // This SHOULD be the same configuration that you used
121 // to initially create and populate your physical Key Store.
122 // Note that key_store_table_name is the physical Key Store,
123 // and key_store1 is instances of this physical Key Store.
124 let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
125 let key_store_config = KeyStoreConfig::builder()
126 .kms_client(aws_sdk_kms::Client::new(&sdk_config))
127 .ddb_client(aws_sdk_dynamodb::Client::new(&sdk_config))
128 .ddb_table_name(key_store_table_name)
129 .logical_key_store_name(logical_key_store_name)
130 .kms_configuration(KmsConfiguration::KmsKeyArn(
131 key_store_kms_key_id.to_string(),
132 ))
133 .build()?;
134
135 let key_store1 = keystore_client::Client::from_conf(key_store_config.clone())?;
136
137 // 4. Call create_branch_key_id to create one new active branch key
138 let branch_key_id: String = create_branch_key_id(
139 key_store_table_name,
140 logical_key_store_name,
141 key_store_kms_key_id,
142 )
143 .await?;
144
145 // 5. Create the Hierarchical Keyring HK1 with Key Store instance K1, partition_id,
146 // the shared_cache and the branch_key_id.
147 // Note that we are now providing an already initialized shared cache instead of just mentioning
148 // the cache type and the Hierarchical Keyring initializing a cache at initialization.
149
150 // partition_id for this example is a random UUID
151 let partition_id = "91c1b6a2-6fc3-4539-ad5e-938d597ed730".to_string();
152
153 // Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and
154 // Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache
155 let keyring1 = mpl
156 .create_aws_kms_hierarchical_keyring()
157 .key_store(key_store1)
158 .branch_key_id(branch_key_id.clone())
159 // CryptographicMaterialsCacheRef is an Rc (Reference Counted), so if you clone it to
160 // pass it to different Hierarchical Keyrings, it will still point to the same
161 // underlying cache, and increment the reference count accordingly.
162 .cache(shared_cache.clone())
163 .ttl_seconds(600)
164 .partition_id(partition_id.clone())
165 .send()
166 .await?;
167
168 // 6. Create encryption context.
169 // Remember that your encryption context is NOT SECRET.
170 // For more information, see
171 // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context
172 let encryption_context = HashMap::from([
173 ("encryption".to_string(), "context".to_string()),
174 ("is not".to_string(), "secret".to_string()),
175 ("but adds".to_string(), "useful metadata".to_string()),
176 (
177 "that can help you".to_string(),
178 "be confident that".to_string(),
179 ),
180 (
181 "the data you are handling".to_string(),
182 "is what you think it is".to_string(),
183 ),
184 ]);
185
186 // 7. Encrypt the data for encryption_context using keyring1
187 let plaintext = example_data.as_bytes();
188
189 let encryption_response1 = esdk_client
190 .encrypt()
191 .plaintext(plaintext)
192 .keyring(keyring1.clone())
193 .encryption_context(encryption_context.clone())
194 .send()
195 .await?;
196
197 let ciphertext1 = encryption_response1
198 .ciphertext
199 .expect("Unable to unwrap ciphertext from encryption response");
200
201 // 8. Demonstrate that the ciphertexts and plaintext are different.
202 // (This is an example for demonstration; you do not need to do this in your own code.)
203 assert_ne!(
204 ciphertext1,
205 aws_smithy_types::Blob::new(plaintext),
206 "Ciphertext and plaintext data are the same. Invalid encryption"
207 );
208
209 // 9. Decrypt your encrypted data using the same keyring HK1 you used on encrypt.
210 let decryption_response1 = esdk_client
211 .decrypt()
212 .ciphertext(ciphertext1)
213 .keyring(keyring1)
214 // Provide the encryption context that was supplied to the encrypt method
215 .encryption_context(encryption_context.clone())
216 .send()
217 .await?;
218
219 let decrypted_plaintext1 = decryption_response1
220 .plaintext
221 .expect("Unable to unwrap plaintext from decryption response");
222
223 // 10. Demonstrate that the decrypted plaintext is identical to the original plaintext.
224 // (This is an example for demonstration; you do not need to do this in your own code.)
225 assert_eq!(
226 decrypted_plaintext1,
227 aws_smithy_types::Blob::new(plaintext),
228 "Decrypted plaintext should be identical to the original plaintext. Invalid decryption"
229 );
230
231 // 11. Through the above encrypt and decrypt roundtrip, the cache will be populated and
232 // the cache entries can be used by another Hierarchical Keyring with the
233 // - Same Partition ID
234 // - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
235 // - Same Branch Key ID
236
237 // Configure your Key Store resource key_store2.
238 // This SHOULD be the same configuration that you used
239 // to initially create and populate your physical Key Store.
240 // Note that key_store_table_name is the physical Key Store,
241 // and key_store2 is instances of this physical Key Store.
242
243 // Note that for this example, key_store2 is identical to key_store1.
244 // You can optionally change configurations like KMS Client or KMS Key ID based
245 // on your use-case.
246 // Make sure you have the required permissions to use different configurations.
247
248 // - If you want to share cache entries across two keyrings HK1 and HK2,
249 // you should set the Logical Key Store Names for both
250 // Key Store instances (K1 and K2) to be the same.
251 // - If you set the Logical Key Store Names for K1 and K2 to be different,
252 // HK1 (which uses Key Store instance K1) and HK2 (which uses Key Store
253 // instance K2) will NOT be able to share cache entries.
254 let key_store2 = keystore_client::Client::from_conf(key_store_config.clone())?;
255
256 // 12. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared_cache
257 // and the same partition_id and branch_key_id used in HK1 because we want to share cache entries
258 // (and experience cache HITS).
259
260 // Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and
261 // Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache
262 let keyring2 = mpl
263 .create_aws_kms_hierarchical_keyring()
264 .key_store(key_store2)
265 .branch_key_id(branch_key_id)
266 .cache(shared_cache)
267 .ttl_seconds(600)
268 .partition_id(partition_id)
269 .send()
270 .await?;
271
272 // 13. This encrypt-decrypt roundtrip with HK2 will experience Cache HITS from previous HK1 roundtrip
273 // Encrypt the data for encryption_context using keyring2
274 let encryption_response2 = esdk_client
275 .encrypt()
276 .plaintext(plaintext)
277 .keyring(keyring2.clone())
278 .encryption_context(encryption_context.clone())
279 .send()
280 .await?;
281
282 let ciphertext2 = encryption_response2
283 .ciphertext
284 .expect("Unable to unwrap ciphertext from encryption response");
285
286 // 14. Demonstrate that the ciphertexts and plaintext are different.
287 // (This is an example for demonstration; you do not need to do this in your own code.)
288 assert_ne!(
289 ciphertext2,
290 aws_smithy_types::Blob::new(plaintext),
291 "Ciphertext and plaintext data are the same. Invalid encryption"
292 );
293
294 // 15. Decrypt your encrypted data using the same keyring HK2 you used on encrypt.
295 let decryption_response2 = esdk_client
296 .decrypt()
297 .ciphertext(ciphertext2)
298 .keyring(keyring2)
299 // Provide the encryption context that was supplied to the encrypt method
300 .encryption_context(encryption_context)
301 .send()
302 .await?;
303
304 let decrypted_plaintext2 = decryption_response2
305 .plaintext
306 .expect("Unable to unwrap plaintext from decryption response");
307
308 // 10. Demonstrate that the decrypted plaintext is identical to the original plaintext.
309 // (This is an example for demonstration; you do not need to do this in your own code.)
310 assert_eq!(
311 decrypted_plaintext2,
312 aws_smithy_types::Blob::new(plaintext),
313 "Decrypted plaintext should be identical to the original plaintext. Invalid decryption"
314 );
315
316 println!("Shared Cache Across Hierarchical Keyrings Example Completed Successfully");
317
318 Ok(())
319}
320
321#[tokio::test(flavor = "multi_thread")]
322pub async fn test_encrypt_and_decrypt_with_keyring() -> Result<(), crate::BoxError2> {
323 // Test function for encrypt and decrypt using the Shared Cache Across Hierarchical Keyrings example
324 use crate::example_utils::utils;
325
326 encrypt_and_decrypt_with_keyring(
327 utils::TEST_EXAMPLE_DATA,
328 utils::TEST_KEY_STORE_NAME,
329 utils::TEST_LOGICAL_KEY_STORE_NAME,
330 utils::TEST_KEY_STORE_KMS_KEY_ID,
331 )
332 .await?;
333
334 Ok(())
335}