Skip to content

Commit dd1a953

Browse files
committed
m
1 parent 07de35f commit dd1a953

File tree

7 files changed

+1399
-0
lines changed

7 files changed

+1399
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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_dbEncryptionSdk_dynamoDb::types::DynamoDbTableEncryptionConfig;
6+
use aws_db_esdk::aws_cryptography_dbEncryptionSdk_structuredEncryption::types::CryptoAction;
7+
use aws_db_esdk::aws_cryptography_materialProviders::client as mpl_client;
8+
use aws_db_esdk::aws_cryptography_materialProviders::types::material_providers_config::MaterialProvidersConfig;
9+
use aws_db_esdk::aws_cryptography_materialProviders::types::PaddingScheme;
10+
use aws_db_esdk::intercept::DbEsdkInterceptor;
11+
use aws_db_esdk::DynamoDbTablesEncryptionConfig;
12+
use aws_sdk_dynamodb::types::AttributeValue;
13+
use std::collections::HashMap;
14+
use std::fs::File;
15+
use std::io::Read;
16+
use std::io::Write;
17+
use std::path::Path;
18+
19+
/*
20+
This example sets up DynamoDb Encryption for the AWS SDK client
21+
using the Hierarchical Keyring, which establishes a key hierarchy
22+
where "branch" keys are persisted in DynamoDb.
23+
These branch keys are used to protect your data keys,
24+
and these branch keys are themselves protected by a root KMS Key.
25+
26+
Establishing a key hierarchy like this has two benefits:
27+
28+
First, by caching the branch key material, and only calling back
29+
to KMS to re-establish authentication regularly according to your configured TTL,
30+
you limit how often you need to call back to KMS to protect your data.
31+
This is a performance/security tradeoff, where your authentication, audit, and
32+
logging from KMS is no longer one-to-one with every encrypt or decrypt call.
33+
However, the benefit is that you no longer have to make a
34+
network call to KMS for every encrypt or decrypt.
35+
36+
Second, this key hierarchy makes it easy to hold multi-tenant data
37+
that is isolated per branch key in a single DynamoDb table.
38+
You can create a branch key for each tenant in your table,
39+
and encrypt all that tenant's data under that distinct branch key.
40+
On decrypt, you can either statically configure a single branch key
41+
to ensure you are restricting decryption to a single tenant,
42+
or you can implement an interface that lets you map the primary key on your items
43+
to the branch key that should be responsible for decrypting that data.
44+
45+
This example then demonstrates configuring a Hierarchical Keyring
46+
with a Branch Key ID Supplier to encrypt and decrypt data for
47+
two separate tenants.
48+
49+
Running this example requires access to the DDB Table whose name
50+
is provided in CLI arguments.
51+
This table must be configured with the following
52+
primary key configuration:
53+
- Partition key is named "partition_key" with type (S)
54+
- Sort key is named "sort_key" with type (S)
55+
56+
This example also requires using a KMS Key whose ARN
57+
is provided in CLI arguments. You need the following access
58+
on this key:
59+
- GenerateDataKeyWithoutPlaintext
60+
- Decrypt
61+
*/
62+
pub async fn HierarchicalKeyringGetItemPutItem(String tenant1BranchKeyId, String tenant2BranchKeyId)
63+
{
64+
/*
65+
var ddbTableName = TestUtils.TEST_DDB_TABLE_NAME;
66+
var keyStoreTableName = TestUtils.TEST_KEYSTORE_NAME;
67+
var logicalKeyStoreName = TestUtils.TEST_LOGICAL_KEYSTORE_NAME;
68+
var kmsKeyId = TestUtils.TEST_KEYSTORE_KMS_KEY_ID;
69+
70+
// Initial KeyStore Setup: This example requires that you have already
71+
// created your KeyStore, and have populated it with two new branch keys.
72+
// See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
73+
// for an example of how to do this.
74+
75+
// 1. Configure your KeyStore resource.
76+
// This SHOULD be the same configuration that you used
77+
// to initially create and populate your KeyStore.
78+
var keystore = new KeyStore(new KeyStoreConfig
79+
{
80+
DdbClient = new AmazonDynamoDBClient(),
81+
DdbTableName = keyStoreTableName,
82+
LogicalKeyStoreName = logicalKeyStoreName,
83+
KmsClient = new AmazonKeyManagementServiceClient(),
84+
KmsConfiguration = new KMSConfiguration { KmsKeyArn = kmsKeyId }
85+
});
86+
87+
88+
// 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
89+
var ddbEnc = new DynamoDbEncryption(new DynamoDbEncryptionConfig());
90+
var branchKeyIdSupplier = ddbEnc.CreateDynamoDbEncryptionBranchKeyIdSupplier(
91+
new CreateDynamoDbEncryptionBranchKeyIdSupplierInput
92+
{
93+
DdbKeyBranchKeyIdSupplier = new ExampleBranchKeyIdSupplier(tenant1BranchKeyId, tenant2BranchKeyId)
94+
}).BranchKeyIdSupplier;
95+
96+
// 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
97+
// With this configuration, the AWS SDK Client ultimately configured will be capable
98+
// of encrypting or decrypting items for either tenant (assuming correct KMS access).
99+
// If you want to restrict the client to only encrypt or decrypt for a single tenant,
100+
// configure this Hierarchical Keyring using `.branchKeyId(tenant1BranchKeyId)` instead
101+
// of `.branchKeyIdSupplier(branchKeyIdSupplier)`.
102+
var matProv = new MaterialProviders(new MaterialProvidersConfig());
103+
var keyringInput = new CreateAwsKmsHierarchicalKeyringInput
104+
{
105+
KeyStore = keystore,
106+
BranchKeyIdSupplier = branchKeyIdSupplier,
107+
TtlSeconds = 600, // This dictates how often we call back to KMS to authorize use of the branch keys
108+
Cache = new CacheType
109+
{
110+
Default = new DefaultCache { EntryCapacity = 100 }
111+
}
112+
};
113+
var hierarchicalKeyring = matProv.CreateAwsKmsHierarchicalKeyring(keyringInput);
114+
115+
// 4. Configure which attributes are encrypted and/or signed when writing new items.
116+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
117+
// we must explicitly configure how they should be treated during item encryption:
118+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
119+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
120+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
121+
var attributeActionsOnEncrypt = new Dictionary<String, CryptoAction>
122+
{
123+
["partition_key"] = CryptoAction.SIGN_ONLY, // Our partition attribute must be SIGN_ONLY
124+
["sort_key"] = CryptoAction.SIGN_ONLY, // Our sort attribute must be SIGN_ONLY
125+
["tenant_sensitive_data"] = CryptoAction.ENCRYPT_AND_SIGN
126+
};
127+
128+
// 5. Configure which attributes we expect to be included in the signature
129+
// when reading items. There are two options for configuring this:
130+
//
131+
// - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
132+
// When defining your DynamoDb schema and deciding on attribute names,
133+
// choose a distinguishing prefix (such as ":") for all attributes that
134+
// you do not want to include in the signature.
135+
// This has two main benefits:
136+
// - It is easier to reason about the security and authenticity of data within your item
137+
// when all unauthenticated data is easily distinguishable by their attribute name.
138+
// - If you need to add new unauthenticated attributes in the future,
139+
// you can easily make the corresponding update to your `attributeActionsOnEncrypt`
140+
// and immediately start writing to that new attribute, without
141+
// any other configuration update needed.
142+
// Once you configure this field, it is not safe to update it.
143+
//
144+
// - Configure `allowedUnsignedAttributes`: You may also explicitly list
145+
// a set of attributes that should be considered unauthenticated when encountered
146+
// on read. Be careful if you use this configuration. Do not remove an attribute
147+
// name from this configuration, even if you are no longer writing with that attribute,
148+
// as old items may still include this attribute, and our configuration needs to know
149+
// to continue to exclude this attribute from the signature scope.
150+
// If you add new attribute names to this field, you must first deploy the update to this
151+
// field to all readers in your host fleet before deploying the update to start writing
152+
// with that new attribute.
153+
//
154+
// For this example, we currently authenticate all attributes. To make it easier to
155+
// add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
156+
const String unsignAttrPrefix = ":";
157+
158+
// 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
159+
var tableConfigs = new Dictionary<String, DynamoDbTableEncryptionConfig>
160+
{
161+
[ddbTableName] = new DynamoDbTableEncryptionConfig
162+
{
163+
LogicalTableName = ddbTableName,
164+
PartitionKeyName = "partition_key",
165+
SortKeyName = "sort_key",
166+
AttributeActionsOnEncrypt = attributeActionsOnEncrypt,
167+
Keyring = hierarchicalKeyring,
168+
AllowedUnsignedAttributePrefix = unsignAttrPrefix
169+
}
170+
};
171+
172+
// 7. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
173+
var ddb = new Client.DynamoDbClient(
174+
new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs });
175+
176+
// 8. Put an item into our table using the above client.
177+
// Before the item gets sent to DynamoDb, it will be encrypted
178+
// client-side, according to our configuration.
179+
// Because the item we are writing uses "tenantId1" as our partition value,
180+
// based on the code we wrote in the ExampleBranchKeySupplier,
181+
// `tenant1BranchKeyId` will be used to encrypt this item.
182+
var item = new Dictionary<String, AttributeValue>
183+
{
184+
["partition_key"] = new AttributeValue("tenant1Id"),
185+
["sort_key"] = new AttributeValue { N = "0" },
186+
["tenant_sensitive_data"] = new AttributeValue("encrypt and sign me!")
187+
};
188+
var putRequest = new PutItemRequest
189+
{
190+
TableName = ddbTableName,
191+
Item = item
192+
};
193+
194+
var putResponse = await ddb.PutItemAsync(putRequest);
195+
196+
// Demonstrate that PutItem succeeded
197+
Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK);
198+
199+
// 10. Get the item back from our table using the same client.
200+
// The client will decrypt the item client-side, and return
201+
// back the original item.
202+
// Because the returned item's partition value is "tenantId1",
203+
// based on the code we wrote in the ExampleBranchKeySupplier,
204+
// `tenant1BranchKeyId` will be used to decrypt this item.
205+
var keyToGet = new Dictionary<String, AttributeValue>
206+
{
207+
["partition_key"] = new AttributeValue("tenant1Id"),
208+
["sort_key"] = new AttributeValue { N = "0" }
209+
};
210+
var getRequest = new GetItemRequest
211+
{
212+
Key = keyToGet,
213+
TableName = ddbTableName
214+
};
215+
var getResponse = await ddb.GetItemAsync(getRequest);
216+
217+
// Demonstrate that GetItem succeeded and returned the decrypted item
218+
Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK);
219+
var returnedItem = getResponse.Item;
220+
Debug.Assert(returnedItem["tenant_sensitive_data"].S.Equals("encrypt and sign me!"));
221+
*/
222+
println!("hierarchical_keyring successful.");
223+
224+
}

0 commit comments

Comments
 (0)