Skip to content

Commit 36aea0e

Browse files
committed
m
1 parent 08e30b8 commit 36aea0e

File tree

29 files changed

+4765
-70
lines changed

29 files changed

+4765
-70
lines changed

releases/rust/db_esdk/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target
2+
Cargo.lock
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::operation::get_branch_key_id_from_ddb_key::GetBranchKeyIdFromDdbKeyInput;
5+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::operation::get_branch_key_id_from_ddb_key::GetBranchKeyIdFromDdbKeyOutput;
6+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::types::error::Error;
7+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::types::DynamoDbKeyBranchKeyIdSupplier;
8+
9+
// Used in the 'HierarchicalKeyringExample'.
10+
// In that example, we have a table where we distinguish multiple tenants
11+
// by a tenant ID that is stored in our partition attribute.
12+
// The expectation is that this does not produce a confused deputy
13+
// because the tenants are separated by partition.
14+
// In order to create a Hierarchical Keyring that is capable of encrypting or
15+
// decrypting data for either tenant, we implement this interface
16+
// to map the correct branch key ID to the correct tenant ID.
17+
pub struct ExampleBranchKeyIdSupplier {
18+
branch_key_id_for_tenant1: String,
19+
branch_key_id_for_tenant2: String,
20+
}
21+
22+
impl ExampleBranchKeyIdSupplier {
23+
pub fn new(tenant1_id: &str, tenant2_id: &str) -> Self {
24+
Self {
25+
branch_key_id_for_tenant1: tenant1_id.to_string(),
26+
branch_key_id_for_tenant2: tenant2_id.to_string(),
27+
}
28+
}
29+
}
30+
31+
impl DynamoDbKeyBranchKeyIdSupplier for ExampleBranchKeyIdSupplier {
32+
fn get_branch_key_id_from_ddb_key(
33+
&mut self,
34+
input: GetBranchKeyIdFromDdbKeyInput,
35+
) -> Result<GetBranchKeyIdFromDdbKeyOutput, Error> {
36+
let key = input.ddb_key.unwrap();
37+
38+
if !key.contains_key("partition_key") {
39+
return Err(Error::DynamoDbEncryptionException {
40+
message: "Item invalid, does not contain expected partition key attribute."
41+
.to_string(),
42+
});
43+
}
44+
let tenant_key_id = key["partition_key"].as_s().unwrap();
45+
46+
if tenant_key_id == "tenant1Id" {
47+
Ok(GetBranchKeyIdFromDdbKeyOutput::builder()
48+
.branch_key_id(self.branch_key_id_for_tenant1.clone())
49+
.build()
50+
.unwrap())
51+
} else if tenant_key_id == "tenant2Id" {
52+
Ok(GetBranchKeyIdFromDdbKeyOutput::builder()
53+
.branch_key_id(self.branch_key_id_for_tenant2.clone())
54+
.build()
55+
.unwrap())
56+
} else {
57+
Err(Error::DynamoDbEncryptionException {
58+
message: "Item does not contain valid tenant ID.".to_string(),
59+
})
60+
}
61+
}
62+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::test_utils;
5+
use aws_db_esdk::aws_cryptography_keyStore::types::KmsConfiguration;
6+
use aws_db_esdk::aws_cryptography_keyStore::types::key_store_config::KeyStoreConfig;
7+
use aws_db_esdk::aws_cryptography_materialProviders::client as mpl_client;
8+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::client as dbesdk_client;
9+
use aws_db_esdk::aws_cryptography_keyStore::client as keystore_client;
10+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::types::dynamo_db_encryption_config::DynamoDbEncryptionConfig;
11+
use super::branch_key_id_supplier::ExampleBranchKeyIdSupplier;
12+
use aws_db_esdk::aws_cryptography_materialProviders::types::material_providers_config::MaterialProvidersConfig;
13+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::types::dynamo_db_key_branch_key_id_supplier::DynamoDbKeyBranchKeyIdSupplierRef;
14+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_structuredEncryption::types::CryptoAction;
15+
use std::collections::HashMap;
16+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_dynamoDb::types::DynamoDbTableEncryptionConfig;
17+
use aws_db_esdk::DynamoDbTablesEncryptionConfig;
18+
use aws_db_esdk::intercept::DbEsdkInterceptor;
19+
use aws_sdk_dynamodb::types::AttributeValue;
20+
21+
/*
22+
This example sets up DynamoDb Encryption for the AWS SDK client
23+
using the Hierarchical Keyring, which establishes a key hierarchy
24+
where "branch" keys are persisted in DynamoDb.
25+
These branch keys are used to protect your data keys,
26+
and these branch keys are themselves protected by a root KMS Key.
27+
28+
Establishing a key hierarchy like this has two benefits:
29+
30+
First, by caching the branch key material, and only calling back
31+
to KMS to re-establish authentication regularly according to your configured TTL,
32+
you limit how often you need to call back to KMS to protect your data.
33+
This is a performance/security tradeoff, where your authentication, audit, and
34+
logging from KMS is no longer one-to-one with every encrypt or decrypt call.
35+
However, the benefit is that you no longer have to make a
36+
network call to KMS for every encrypt or decrypt.
37+
38+
Second, this key hierarchy makes it easy to hold multi-tenant data
39+
that is isolated per branch key in a single DynamoDb table.
40+
You can create a branch key for each tenant in your table,
41+
and encrypt all that tenant's data under that distinct branch key.
42+
On decrypt, you can either statically configure a single branch key
43+
to ensure you are restricting decryption to a single tenant,
44+
or you can implement an interface that lets you map the primary key on your items
45+
to the branch key that should be responsible for decrypting that data.
46+
47+
This example then demonstrates configuring a Hierarchical Keyring
48+
with a Branch Key ID Supplier to encrypt and decrypt data for
49+
two separate tenants.
50+
51+
Running this example requires access to the DDB Table whose name
52+
is provided in CLI arguments.
53+
This table must be configured with the following
54+
primary key configuration:
55+
- Partition key is named "partition_key" with type (S)
56+
- Sort key is named "sort_key" with type (S)
57+
58+
This example also requires using a KMS Key whose ARN
59+
is provided in CLI arguments. You need the following access
60+
on this key:
61+
- GenerateDataKeyWithoutPlaintext
62+
- Decrypt
63+
*/
64+
pub async fn put_item_get_item(tenant1_branch_key_id: &str, tenant2_branch_key_id: &str) {
65+
let ddb_table_name = test_utils::TEST_DDB_TABLE_NAME;
66+
67+
let keystore_table_name = test_utils::TEST_KEYSTORE_NAME;
68+
let logical_keystore_name = test_utils::TEST_LOGICAL_KEYSTORE_NAME;
69+
let kms_key_id = test_utils::TEST_KEYSTORE_KMS_KEY_ID;
70+
71+
// Initial KeyStore Setup: This example requires that you have already
72+
// created your KeyStore, and have populated it with two new branch keys.
73+
// See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
74+
// for an example of how to do this.
75+
76+
// 1. Configure your KeyStore resource.
77+
// This SHOULD be the same configuration that you used
78+
// to initially create and populate your KeyStore.
79+
let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
80+
let key_store_config = KeyStoreConfig::builder()
81+
.kms_client(aws_sdk_kms::Client::new(&sdk_config))
82+
.ddb_client(aws_sdk_dynamodb::Client::new(&sdk_config))
83+
.ddb_table_name(keystore_table_name)
84+
.logical_key_store_name(logical_keystore_name)
85+
.kms_configuration(KmsConfiguration::KmsKeyArn(kms_key_id.to_string()))
86+
.build()
87+
.unwrap();
88+
89+
let key_store = keystore_client::Client::from_conf(key_store_config).unwrap();
90+
91+
// 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
92+
let dbesdk_config = DynamoDbEncryptionConfig::builder().build().unwrap();
93+
let dbesdk = dbesdk_client::Client::from_conf(dbesdk_config).unwrap();
94+
let supplier = ExampleBranchKeyIdSupplier::new(tenant1_branch_key_id, tenant2_branch_key_id);
95+
let supplier_ref = DynamoDbKeyBranchKeyIdSupplierRef {
96+
inner: ::std::rc::Rc::new(std::cell::RefCell::new(supplier)),
97+
};
98+
let branch_key_id_supplier = dbesdk
99+
.create_dynamo_db_encryption_branch_key_id_supplier()
100+
.ddb_key_branch_key_id_supplier(supplier_ref)
101+
.send()
102+
.await
103+
.unwrap()
104+
.branch_key_id_supplier
105+
.unwrap();
106+
107+
// 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
108+
// With this configuration, the AWS SDK Client ultimately configured will be capable
109+
// of encrypting or decrypting items for either tenant (assuming correct KMS access).
110+
// If you want to restrict the client to only encrypt or decrypt for a single tenant,
111+
// configure this Hierarchical Keyring using `.branchKeyId(tenant1BranchKeyId)` instead
112+
// of `.branchKeyIdSupplier(branchKeyIdSupplier)`.
113+
let mpl_config = MaterialProvidersConfig::builder().build().unwrap();
114+
let mpl = mpl_client::Client::from_conf(mpl_config).unwrap();
115+
116+
let hierarchical_keyring = mpl
117+
.create_aws_kms_hierarchical_keyring()
118+
.branch_key_id_supplier(branch_key_id_supplier)
119+
.key_store(key_store)
120+
.ttl_seconds(600)
121+
.send()
122+
.await
123+
.unwrap();
124+
125+
// 4. Configure which attributes are encrypted and/or signed when writing new items.
126+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
127+
// we must explicitly configure how they should be treated during item encryption:
128+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
129+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
130+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
131+
let attribute_actions_on_encrypt = HashMap::from([
132+
("partition_key".to_string(), CryptoAction::SignOnly), // Our partition attribute must be SIGN_ONLY
133+
("sort_key".to_string(), CryptoAction::SignOnly), // Our sort attribute must be SIGN_ONLY
134+
(
135+
"tenant_sensitive_data".to_string(),
136+
CryptoAction::EncryptAndSign,
137+
),
138+
]);
139+
140+
// 5. Configure which attributes we expect to be included in the signature
141+
// when reading items. There are two options for configuring this:
142+
//
143+
// - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
144+
// When defining your DynamoDb schema and deciding on attribute names,
145+
// choose a distinguishing prefix (such as ":") for all attributes that
146+
// you do not want to include in the signature.
147+
// This has two main benefits:
148+
// - It is easier to reason about the security and authenticity of data within your item
149+
// when all unauthenticated data is easily distinguishable by their attribute name.
150+
// - If you need to add new unauthenticated attributes in the future,
151+
// you can easily make the corresponding update to your `attributeActionsOnEncrypt`
152+
// and immediately start writing to that new attribute, without
153+
// any other configuration update needed.
154+
// Once you configure this field, it is not safe to update it.
155+
//
156+
// - Configure `allowedUnsignedAttributes`: You may also explicitly list
157+
// a set of attributes that should be considered unauthenticated when encountered
158+
// on read. Be careful if you use this configuration. Do not remove an attribute
159+
// name from this configuration, even if you are no longer writing with that attribute,
160+
// as old items may still include this attribute, and our configuration needs to know
161+
// to continue to exclude this attribute from the signature scope.
162+
// If you add new attribute names to this field, you must first deploy the update to this
163+
// field to all readers in your host fleet before deploying the update to start writing
164+
// with that new attribute.
165+
//
166+
// For this example, we currently authenticate all attributes. To make it easier to
167+
// add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
168+
const UNSIGNED_ATTR_PREFIX: &str = ":";
169+
170+
// 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
171+
let table_config = DynamoDbTableEncryptionConfig::builder()
172+
.logical_table_name(ddb_table_name)
173+
.partition_key_name("partition_key")
174+
.sort_key_name("sort_key")
175+
.attribute_actions_on_encrypt(attribute_actions_on_encrypt)
176+
.keyring(hierarchical_keyring)
177+
.allowed_unsigned_attribute_prefix(UNSIGNED_ATTR_PREFIX)
178+
.build()
179+
.unwrap();
180+
181+
let table_configs = DynamoDbTablesEncryptionConfig::builder()
182+
.table_encryption_configs(HashMap::from([(ddb_table_name.to_string(), table_config)]))
183+
.build()
184+
.unwrap();
185+
186+
// 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
187+
let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
188+
let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
189+
.interceptor(DbEsdkInterceptor::new(table_configs))
190+
.build();
191+
let ddb = aws_sdk_dynamodb::Client::from_conf(dynamo_config);
192+
193+
// 8. Put an item into our table using the above client.
194+
// Before the item gets sent to DynamoDb, it will be encrypted
195+
// client-side, according to our configuration.
196+
// Because the item we are writing uses "tenantId1" as our partition value,
197+
// based on the code we wrote in the ExampleBranchKeySupplier,
198+
// `tenant1BranchKeyId` will be used to encrypt this item.
199+
let item = HashMap::from([
200+
(
201+
"partition_key".to_string(),
202+
AttributeValue::S("tenant1Id".to_string()),
203+
),
204+
("sort_key".to_string(), AttributeValue::N("0".to_string())),
205+
(
206+
"tenant_sensitive_data".to_string(),
207+
AttributeValue::S("encrypt and sign me!".to_string()),
208+
),
209+
]);
210+
211+
let _resp = ddb
212+
.put_item()
213+
.table_name(ddb_table_name)
214+
.set_item(Some(item.clone()))
215+
.send()
216+
.await
217+
.unwrap();
218+
219+
// 10. Get the item back from our table using the same client.
220+
// The client will decrypt the item client-side, and return
221+
// back the original item.
222+
// Because the returned item's partition value is "tenantId1",
223+
// based on the code we wrote in the ExampleBranchKeySupplier,
224+
// `tenant1BranchKeyId` will be used to decrypt this item.
225+
let key_to_get = HashMap::from([
226+
(
227+
"partition_key".to_string(),
228+
AttributeValue::S("tenant1Id".to_string()),
229+
),
230+
("sort_key".to_string(), AttributeValue::N("0".to_string())),
231+
]);
232+
233+
let resp = ddb
234+
.get_item()
235+
.table_name(ddb_table_name)
236+
.set_key(Some(key_to_get))
237+
.consistent_read(true)
238+
.send()
239+
.await
240+
.unwrap();
241+
242+
assert_eq!(resp.item, Some(item));
243+
println!("hierarchical_keyring successful.");
244+
}

0 commit comments

Comments
 (0)