Skip to content

Commit 7e40f20

Browse files
committed
WIP chore(python): simple example for encrypted table
1 parent 44c45da commit 7e40f20

File tree

13 files changed

+952
-0
lines changed

13 files changed

+952
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Stub to allow relative imports of examples from tests."""
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example demonstrating DynamoDb Encryption using a Hierarchical Keyring with EncryptedClient.
5+
6+
This example sets up DynamoDb Encryption for the AWS SDK client
7+
using the Hierarchical Keyring, which establishes a key hierarchy
8+
where "branch" keys are persisted in DynamoDb.
9+
These branch keys are used to protect your data keys,
10+
and these branch keys are themselves protected by a root KMS Key.
11+
12+
Running this example requires access to the DDB Table whose name
13+
is provided in CLI arguments.
14+
This table must be configured with the following
15+
primary key configuration:
16+
- Partition key is named "partition_key" with type (S)
17+
- Sort key is named "sort_key" with type (S)
18+
19+
This example also requires using a KMS Key whose ARN
20+
is provided in CLI arguments. You need the following access
21+
on this key:
22+
- GenerateDataKeyWithoutPlaintext
23+
- Decrypt
24+
"""
25+
26+
import boto3
27+
from aws_cryptographic_material_providers.keystore.client import KeyStore
28+
from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
29+
from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
30+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
31+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
32+
from aws_cryptographic_material_providers.mpl.models import (
33+
CacheTypeDefault,
34+
CreateAwsKmsHierarchicalKeyringInput,
35+
DefaultCache,
36+
)
37+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
38+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption
39+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import (
40+
DynamoDbEncryptionConfig,
41+
)
42+
from aws_dbesdk_dynamodb.structures.dynamodb import (
43+
CreateDynamoDbEncryptionBranchKeyIdSupplierInput,
44+
DynamoDbTableEncryptionConfig,
45+
DynamoDbTablesEncryptionConfig,
46+
)
47+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
48+
CryptoAction,
49+
)
50+
51+
from ..example_branch_key_id_supplier import ExampleBranchKeyIdSupplier
52+
53+
54+
def hierarchical_keyring_client_example(
55+
ddb_table_name: str,
56+
tenant1_branch_key_id: str,
57+
tenant2_branch_key_id: str,
58+
keystore_table_name: str,
59+
logical_keystore_name: str,
60+
kms_key_id: str,
61+
):
62+
"""
63+
Demonstrate using a hierarchical keyring with multiple tenants using EncryptedClient.
64+
65+
:param ddb_table_name: The name of the DynamoDB table
66+
:param tenant1_branch_key_id: Branch key ID for tenant 1
67+
:param tenant2_branch_key_id: Branch key ID for tenant 2
68+
:param keystore_table_name: The name of the KeyStore DynamoDB table
69+
:param logical_keystore_name: The logical name for this keystore
70+
:param kms_key_id: The ARN of the KMS key to use
71+
"""
72+
# Initial KeyStore Setup: This example requires that you have already
73+
# created your KeyStore, and have populated it with two new branch keys.
74+
# See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
75+
# for an example of how to do this.
76+
77+
# 1. Configure your KeyStore resource.
78+
# This SHOULD be the same configuration that you used
79+
# to initially create and populate your KeyStore.
80+
keystore = KeyStore(
81+
config=KeyStoreConfig(
82+
ddb_client=boto3.client("dynamodb"),
83+
ddb_table_name=keystore_table_name,
84+
logical_key_store_name=logical_keystore_name,
85+
kms_client=boto3.client("kms"),
86+
kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id),
87+
)
88+
)
89+
90+
# 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
91+
ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig())
92+
branch_key_id_supplier = ddb_enc.create_dynamo_db_encryption_branch_key_id_supplier(
93+
input=CreateDynamoDbEncryptionBranchKeyIdSupplierInput(
94+
ddb_key_branch_key_id_supplier=ExampleBranchKeyIdSupplier(tenant1_branch_key_id, tenant2_branch_key_id)
95+
)
96+
).branch_key_id_supplier
97+
98+
# 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
99+
# With this configuration, the AWS SDK Client ultimately configured will be capable
100+
# of encrypting or decrypting items for either tenant (assuming correct KMS access).
101+
# If you want to restrict the client to only encrypt or decrypt for a single tenant,
102+
# configure this Hierarchical Keyring using `.branch_key_id=tenant1_branch_key_id` instead
103+
# of `.branch_key_id_supplier=branch_key_id_supplier`.
104+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
105+
106+
keyring_input = CreateAwsKmsHierarchicalKeyringInput(
107+
key_store=keystore,
108+
branch_key_id_supplier=branch_key_id_supplier,
109+
ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys
110+
cache=CacheTypeDefault( # This dictates how many branch keys will be held locally
111+
value=DefaultCache(entry_capacity=100)
112+
),
113+
)
114+
115+
hierarchical_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
116+
117+
# 4. Configure which attributes are encrypted and/or signed when writing new items.
118+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
119+
# we must explicitly configure how they should be treated during item encryption:
120+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
121+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
122+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
123+
attribute_actions = {
124+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
125+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
126+
"tenant_sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
127+
}
128+
129+
# 5. Configure which attributes we expect to be included in the signature
130+
# when reading items. There are two options for configuring this:
131+
#
132+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
133+
# When defining your DynamoDb schema and deciding on attribute names,
134+
# choose a distinguishing prefix (such as ":") for all attributes that
135+
# you do not want to include in the signature.
136+
# This has two main benefits:
137+
# - It is easier to reason about the security and authenticity of data within your item
138+
# when all unauthenticated data is easily distinguishable by their attribute name.
139+
# - If you need to add new unauthenticated attributes in the future,
140+
# you can easily make the corresponding update to your `attribute_actions`
141+
# and immediately start writing to that new attribute, without
142+
# any other configuration update needed.
143+
# Once you configure this field, it is not safe to update it.
144+
#
145+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
146+
# a set of attributes that should be considered unauthenticated when encountered
147+
# on read. Be careful if you use this configuration. Do not remove an attribute
148+
# name from this configuration, even if you are no longer writing with that attribute,
149+
# as old items may still include this attribute, and our configuration needs to know
150+
# to continue to exclude this attribute from the signature scope.
151+
# If you add new attribute names to this field, you must first deploy the update to this
152+
# field to all readers in your host fleet before deploying the update to start writing
153+
# with that new attribute.
154+
#
155+
# For this example, we currently authenticate all attributes. To make it easier to
156+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
157+
unsign_attr_prefix = ":"
158+
159+
# 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
160+
table_config = DynamoDbTableEncryptionConfig(
161+
logical_table_name=ddb_table_name,
162+
partition_key_name="partition_key",
163+
sort_key_name="sort_key",
164+
attribute_actions_on_encrypt=attribute_actions,
165+
keyring=hierarchical_keyring,
166+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
167+
)
168+
169+
table_configs = {ddb_table_name: table_config}
170+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
171+
172+
# 7. Create the EncryptedClient
173+
ddb_client = boto3.client("dynamodb")
174+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
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+
# `tenant1_branch_key_id` will be used to encrypt this item.
182+
item = {
183+
"partition_key": {"S": "tenant1Id"},
184+
"sort_key": {"N": "0"},
185+
"tenant_sensitive_data": {"S": "encrypt and sign me!"},
186+
}
187+
188+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
189+
190+
# Demonstrate that PutItem succeeded
191+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
192+
193+
# 9. Get the item back from our table using the same client.
194+
# The client will decrypt the item client-side, and return
195+
# back the original item.
196+
# Because the returned item's partition value is "tenantId1",
197+
# based on the code we wrote in the ExampleBranchKeySupplier,
198+
# `tenant1_branch_key_id` will be used to decrypt this item.
199+
key_to_get = {"partition_key": {"S": "tenant1Id"}, "sort_key": {"N": "0"}}
200+
201+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
202+
203+
# Demonstrate that GetItem succeeded and returned the decrypted item
204+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
205+
returned_item = get_response["Item"]
206+
assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!"

0 commit comments

Comments
 (0)