main/keyring/aws_kms_hierarchical/
aws_kms_hierarchical_keyring_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 sets up the Hierarchical Keyring, which establishes a key hierarchy where "branch"
6 keys are persisted in DynamoDb. These branch keys are used to protect your data keys, and these
7 branch keys are themselves protected by a KMS Key.
8
9 Establishing a key hierarchy like this has two benefits:
10 First, by caching the branch key material, and only calling KMS to re-establish authentication
11 regularly according to your configured TTL, you limit how often you need to call KMS to protect
12 your data. This is a performance security tradeoff, where your authentication, audit, and logging
13 from KMS is no longer one-to-one with every encrypt or decrypt call. Additionally, KMS Cloudtrail
14 cannot be used to distinguish Encrypt and Decrypt calls, and you cannot restrict who has
15 Encryption rights from who has Decryption rights since they both ONLY need KMS:Decrypt. However,
16 the benefit is that you no longer have to make a network call to KMS for every encrypt or
17 decrypt.
18
19 Second, this key hierarchy facilitates cryptographic isolation of a tenant's data in a
20 multi-tenant data store. Each tenant can have a unique Branch Key, that is only used to protect
21 the tenant's data. You can either statically configure a single branch key to ensure you are
22 restricting access to a single tenant, or you can implement an interface that selects the Branch
23 Key based on the Encryption Context.
24
25 This example demonstrates configuring a Hierarchical Keyring with a Branch Key ID Supplier to
26 encrypt and decrypt data for two separate tenants.
27
28 This example requires access to the DDB Table where you are storing the Branch Keys. This
29 table must be configured with the following primary key configuration: - Partition key is named
30 "partition_key" with type (S) - Sort key is named "sort_key" with type (S).
31
32 This example also requires using a KMS Key. You need the following access on this key:
33 - GenerateDataKeyWithoutPlaintext
34 - Decrypt
35
36 For more information on how to use Hierarchical Keyrings, see
37 https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-hierarchical-keyring.html
38*/
39
40use super::create_branch_key_id::create_branch_key_id;
41use super::example_branch_key_id_supplier::ExampleBranchKeyIdSupplier;
42use aws_esdk::client as esdk_client;
43use aws_esdk::key_store::client as keystore_client;
44use aws_esdk::key_store::types::key_store_config::KeyStoreConfig;
45use aws_esdk::key_store::types::KmsConfiguration;
46use aws_esdk::material_providers::client as mpl_client;
47use aws_esdk::material_providers::types::material_providers_config::MaterialProvidersConfig;
48use aws_esdk::types::aws_encryption_sdk_config::AwsEncryptionSdkConfig;
49use aws_esdk::types::error::Error::AwsCryptographicMaterialProvidersError;
50use std::collections::HashMap;
51
52pub async fn encrypt_and_decrypt_with_keyring(
53    example_data: &str,
54    key_store_table_name: &str,
55    logical_key_store_name: &str,
56    key_store_kms_key_id: &str,
57) -> Result<(), crate::BoxError> {
58    // 1. Instantiate the encryption SDK client.
59    // This builds the default client with the RequireEncryptRequireDecrypt commitment policy,
60    // which enforces that this client only encrypts using committing algorithm suites and enforces
61    // that this client will only decrypt encrypted messages that were created with a committing
62    // algorithm suite.
63    let esdk_config = AwsEncryptionSdkConfig::builder().build()?;
64    let esdk_client = esdk_client::Client::from_conf(esdk_config)?;
65
66    // 2. Create a KMS client and DynamoDB client.
67    let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
68    let kms_client = aws_sdk_kms::Client::new(&sdk_config);
69    let ddb_client = aws_sdk_dynamodb::Client::new(&sdk_config);
70
71    // 3. Configure your KeyStore resource.
72    //    This SHOULD be the same configuration that you used
73    //    to initially create and populate your KeyStore.
74    let key_store_config = KeyStoreConfig::builder()
75        .kms_client(kms_client)
76        .ddb_client(ddb_client)
77        .ddb_table_name(key_store_table_name)
78        .logical_key_store_name(logical_key_store_name)
79        .kms_configuration(KmsConfiguration::KmsKeyArn(
80            key_store_kms_key_id.to_string(),
81        ))
82        .build()?;
83
84    let key_store = keystore_client::Client::from_conf(key_store_config)?;
85
86    // 4. Call CreateKey to create two new active branch keys
87    let branch_key_id_a: String = create_branch_key_id(
88        key_store_table_name,
89        logical_key_store_name,
90        key_store_kms_key_id,
91    )
92    .await?;
93    let branch_key_id_b: String = create_branch_key_id(
94        key_store_table_name,
95        logical_key_store_name,
96        key_store_kms_key_id,
97    )
98    .await?;
99
100    // 5. Create a branch key supplier that maps the branch key id to a more readable format
101    let branch_key_id_supplier =
102        ExampleBranchKeyIdSupplier::new(&branch_key_id_a, &branch_key_id_b);
103
104    // 6. Create the Hierarchical Keyring.
105    let mpl_config = MaterialProvidersConfig::builder().build()?;
106    let mpl = mpl_client::Client::from_conf(mpl_config)?;
107
108    let hierarchical_keyring = mpl
109        .create_aws_kms_hierarchical_keyring()
110        .key_store(key_store.clone())
111        .branch_key_id_supplier(branch_key_id_supplier)
112        .ttl_seconds(600)
113        .send()
114        .await?;
115
116    // 7. Create encryption context for both tenants.
117    //    The Branch Key Id supplier uses the encryption context to determine which branch key id will
118    //    be used to encrypt data.
119    // Remember that your encryption context is NOT SECRET.
120    // For more information, see
121    // https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context
122
123    // Create encryption context for TenantA
124    let encryption_context_a = HashMap::from([
125        ("tenant".to_string(), "TenantA".to_string()),
126        ("encryption".to_string(), "context".to_string()),
127        ("is not".to_string(), "secret".to_string()),
128        ("but adds".to_string(), "useful metadata".to_string()),
129        (
130            "that can help you".to_string(),
131            "be confident that".to_string(),
132        ),
133        (
134            "the data you are handling".to_string(),
135            "is what you think it is".to_string(),
136        ),
137    ]);
138
139    // Create encryption context for TenantB
140    let encryption_context_b = HashMap::from([
141        ("tenant".to_string(), "TenantB".to_string()),
142        ("encryption".to_string(), "context".to_string()),
143        ("is not".to_string(), "secret".to_string()),
144        ("but adds".to_string(), "useful metadata".to_string()),
145        (
146            "that can help you".to_string(),
147            "be confident that".to_string(),
148        ),
149        (
150            "the data you are handling".to_string(),
151            "is what you think it is".to_string(),
152        ),
153    ]);
154
155    // 8. Encrypt the data with encryption_contextA & encryption_contextB
156    let plaintext = example_data.as_bytes();
157
158    let encryption_response_a = esdk_client
159        .encrypt()
160        .plaintext(plaintext)
161        .keyring(hierarchical_keyring.clone())
162        .encryption_context(encryption_context_a.clone())
163        .send()
164        .await?;
165
166    let ciphertext_a = encryption_response_a
167        .ciphertext
168        .expect("Unable to unwrap ciphertext from encryption response");
169
170    let encryption_response_b = esdk_client
171        .encrypt()
172        .plaintext(plaintext)
173        .keyring(hierarchical_keyring.clone())
174        .encryption_context(encryption_context_b.clone())
175        .send()
176        .await?;
177
178    let ciphertext_b = encryption_response_b
179        .ciphertext
180        .expect("Unable to unwrap ciphertext from encryption response");
181
182    // 9. Demonstrate that the ciphertexts and plaintext are different.
183    // (This is an example for demonstration; you do not need to do this in your own code.)
184    assert_ne!(
185        ciphertext_a,
186        aws_smithy_types::Blob::new(plaintext),
187        "Ciphertext and plaintext data are the same. Invalid encryption"
188    );
189
190    assert_ne!(
191        ciphertext_b,
192        aws_smithy_types::Blob::new(plaintext),
193        "Ciphertext and plaintext data are the same. Invalid encryption"
194    );
195
196    // 10. To attest that TenantKeyB cannot decrypt a message written by TenantKeyA,
197    //    and vice versa, let's construct more restrictive hierarchical keyrings.
198    let hierarchical_keyring_a = mpl
199        .create_aws_kms_hierarchical_keyring()
200        .key_store(key_store.clone())
201        .branch_key_id(branch_key_id_a)
202        .ttl_seconds(600)
203        .send()
204        .await?;
205
206    let hierarchical_keyring_b = mpl
207        .create_aws_kms_hierarchical_keyring()
208        .key_store(key_store)
209        .branch_key_id(branch_key_id_b)
210        .ttl_seconds(600)
211        .send()
212        .await?;
213
214    // 11. Demonstrate that data encrypted by one tenant's key
215    // cannot be decrypted with by a keyring specific to another tenant.
216
217    // Keyring with tenant B's branch key cannot decrypt data encrypted with tenant A's branch key
218    // This will fail and raise a AwsCryptographicMaterialProvidersError,
219    // which we swallow ONLY for demonstration purposes.
220    let decryption_response_mismatch_1 = esdk_client
221        .decrypt()
222        .ciphertext(ciphertext_a.clone())
223        .keyring(hierarchical_keyring_b.clone())
224        // Provide the encryption context that was supplied to the encrypt method
225        .encryption_context(encryption_context_a.clone())
226        .send()
227        .await;
228
229    match decryption_response_mismatch_1 {
230        Ok(_) => panic!(
231            "Decrypt with wrong tenant's hierarchical keyring MUST \
232                            raise AwsCryptographicMaterialProvidersError"
233        ),
234        Err(AwsCryptographicMaterialProvidersError { error: _e }) => (),
235        _ => panic!("Unexpected error type"),
236    }
237
238    // Keyring with tenant A's branch key cannot decrypt data encrypted with tenant B's branch key.
239    // This will fail and raise a AwsCryptographicMaterialProvidersError,
240    // which we swallow ONLY for demonstration purposes.
241    let decryption_response_mismatch_2 = esdk_client
242        .decrypt()
243        .ciphertext(ciphertext_b.clone())
244        .keyring(hierarchical_keyring_a.clone())
245        // Provide the encryption context that was supplied to the encrypt method
246        .encryption_context(encryption_context_b.clone())
247        .send()
248        .await;
249
250    match decryption_response_mismatch_2 {
251        Ok(_) => panic!(
252            "Decrypt with wrong tenant's hierarchical keyring MUST \
253                            raise AwsCryptographicMaterialProvidersError"
254        ),
255        Err(AwsCryptographicMaterialProvidersError { error: _e }) => (),
256        _ => panic!("Unexpected error type"),
257    }
258
259    // 12. Demonstrate that data encrypted by one tenant's branch key can be decrypted by that tenant,
260    //     and that the decrypted data matches the input data.
261    let decryption_response_a = esdk_client
262        .decrypt()
263        .ciphertext(ciphertext_a)
264        .keyring(hierarchical_keyring_a)
265        // Provide the encryption context that was supplied to the encrypt method
266        .encryption_context(encryption_context_a)
267        .send()
268        .await?;
269
270    let decrypted_plaintext_a = decryption_response_a
271        .plaintext
272        .expect("Unable to unwrap plaintext from decryption response");
273
274    // Demonstrate that the decrypted plaintext is identical to the original plaintext.
275    // (This is an example for demonstration; you do not need to do this in your own code.)
276    assert_eq!(
277        decrypted_plaintext_a,
278        aws_smithy_types::Blob::new(plaintext),
279        "Decrypted plaintext should be identical to the original plaintext. Invalid decryption"
280    );
281
282    // Similarly for TenantB
283    let decryption_response_b = esdk_client
284        .decrypt()
285        .ciphertext(ciphertext_b)
286        .keyring(hierarchical_keyring_b)
287        // Provide the encryption context that was supplied to the encrypt method
288        .encryption_context(encryption_context_b)
289        .send()
290        .await?;
291
292    let decrypted_plaintext_b = decryption_response_b
293        .plaintext
294        .expect("Unable to unwrap plaintext from decryption response");
295
296    // Demonstrate that the decrypted plaintext is identical to the original plaintext.
297    // (This is an example for demonstration; you do not need to do this in your own code.)
298    assert_eq!(
299        decrypted_plaintext_b,
300        aws_smithy_types::Blob::new(plaintext),
301        "Decrypted plaintext should be identical to the original plaintext. Invalid decryption"
302    );
303
304    println!("Hierarchical Keyring Example Completed Successfully");
305
306    Ok(())
307}
308
309#[tokio::test(flavor = "multi_thread")]
310pub async fn test_encrypt_and_decrypt_with_keyring() -> Result<(), crate::BoxError2> {
311    // Test function for encrypt and decrypt using the Hierarchical Keyring example
312    use crate::example_utils::utils;
313
314    encrypt_and_decrypt_with_keyring(
315        utils::TEST_EXAMPLE_DATA,
316        utils::TEST_KEY_STORE_NAME,
317        utils::TEST_LOGICAL_KEY_STORE_NAME,
318        utils::TEST_KEY_STORE_KMS_KEY_ID,
319    )
320    .await?;
321
322    Ok(())
323}