Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4a6e251

Browse files
committedJun 3, 2025·
chore(python): examples for keyrings with EncryptedClient
1 parent 33c173f commit 4a6e251

32 files changed

+3649
-8
lines changed
 

‎Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
180180
final IKeyring hierarchicalKeyring1 =
181181
matProv.CreateAwsKmsHierarchicalKeyring(keyringInput1);
182182

183-
// 4. Configure which attributes are encrypted and/or signed when writing new items.
183+
// 5. Configure which attributes are encrypted and/or signed when writing new items.
184184
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
185185
// we must explicitly configure how they should be treated during item encryption:
186186
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
@@ -194,14 +194,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
194194
CryptoAction.ENCRYPT_AND_SIGN
195195
);
196196

197-
// 5. Get the DDB Client for Hierarchical Keyring 1.
197+
// 6. Get the DDB Client for Hierarchical Keyring 1.
198198
final DynamoDbClient ddbClient1 = GetDdbClient(
199199
ddbTableName,
200200
hierarchicalKeyring1,
201201
attributeActionsOnEncrypt
202202
);
203203

204-
// 6. Encrypt Decrypt roundtrip with ddbClient1
204+
// 7. Encrypt Decrypt roundtrip with ddbClient1
205205
PutGetItems(ddbTableName, ddbClient1);
206206

207207
// Through the above encrypt and decrypt roundtrip, the cache will be populated and
@@ -210,7 +210,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
210210
// - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
211211
// - Same Branch Key ID
212212

213-
// 7. Configure your KeyStore resource keystore2.
213+
// 8. Configure your KeyStore resource keystore2.
214214
// This SHOULD be the same configuration that you used
215215
// to initially create and populate your physical KeyStore.
216216
// Note that keyStoreTableName is the physical Key Store,
@@ -243,7 +243,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
243243
)
244244
.build();
245245

246-
// 8. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
246+
// 9. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
247247
// and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries
248248
// (and experience cache HITS).
249249

@@ -262,14 +262,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
262262
final IKeyring hierarchicalKeyring2 =
263263
matProv.CreateAwsKmsHierarchicalKeyring(keyringInput2);
264264

265-
// 9. Get the DDB Client for Hierarchical Keyring 2.
265+
// 10. Get the DDB Client for Hierarchical Keyring 2.
266266
final DynamoDbClient ddbClient2 = GetDdbClient(
267267
ddbTableName,
268268
hierarchicalKeyring2,
269269
attributeActionsOnEncrypt
270270
);
271271

272-
// 10. Encrypt Decrypt roundtrip with ddbClient2
272+
// 11. Encrypt Decrypt roundtrip with ddbClient2
273273
PutGetItems(ddbTableName, ddbClient2);
274274
}
275275

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example for creating a new key in a KeyStore.
5+
6+
The Hierarchical Keyring Example and Searchable Encryption Examples rely on the
7+
existence of a DDB-backed key store with pre-existing branch key material or
8+
beacon key material.
9+
10+
See the "Create KeyStore Table Example" for how to first set up the DDB Table
11+
that will back this KeyStore.
12+
13+
Demonstrates configuring a KeyStore and using a helper method to create a branch
14+
key and beacon key that share the same Id. A new beacon key is always created
15+
alongside a new branch key, even if searchable encryption is not being used.
16+
17+
Note: This key creation should occur within your control plane.
18+
"""
19+
20+
import boto3
21+
from aws_cryptographic_material_providers.keystore.client import KeyStore
22+
from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
23+
from aws_cryptographic_material_providers.keystore.models import (
24+
CreateKeyInput,
25+
KMSConfigurationKmsKeyArn,
26+
)
27+
28+
29+
def keystore_create_key(key_store_table_name: str, logical_key_store_name: str, kms_key_arn: str) -> str:
30+
"""Create a new branch key and beacon key in our KeyStore."""
31+
# 1. Configure your KeyStore resource.
32+
# This SHOULD be the same configuration that was used to create the DDB table
33+
# in the "Create KeyStore Table Example".
34+
keystore: KeyStore = KeyStore(
35+
KeyStoreConfig(
36+
ddb_table_name=key_store_table_name,
37+
kms_configuration=KMSConfigurationKmsKeyArn(kms_key_arn),
38+
logical_key_store_name=logical_key_store_name,
39+
kms_client=boto3.client("kms"),
40+
ddb_client=boto3.client("dynamodb"),
41+
)
42+
)
43+
44+
# 2. Create a new branch key and beacon key in our KeyStore.
45+
# Both the branch key and the beacon key will share an Id.
46+
# This creation is eventually consistent.
47+
branch_key_id = keystore.create_key(CreateKeyInput()).branch_key_identifier
48+
49+
return branch_key_id
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example for creating a DynamoDB table for use as a KeyStore.
5+
6+
The Hierarchical Keyring Example and Searchable Encryption Examples rely on the
7+
existence of a DDB-backed key store with pre-existing branch key material or
8+
beacon key material.
9+
10+
Shows how to configure a KeyStore and use a helper method to create the DDB table
11+
that will be used to persist branch keys and beacons keys for this KeyStore.
12+
13+
This table creation should occur within your control plane and only needs to occur
14+
once. While not demonstrated in this example, you should additionally use the
15+
`VersionKey` API on the KeyStore to periodically rotate your branch key material.
16+
"""
17+
18+
import boto3
19+
from aws_cryptographic_material_providers.keystore.client import KeyStore
20+
from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
21+
from aws_cryptographic_material_providers.keystore.models import (
22+
CreateKeyStoreInput,
23+
KMSConfigurationKmsKeyArn,
24+
)
25+
26+
27+
def keystore_create_table(keystore_table_name: str, logical_keystore_name: str, kms_key_arn: str):
28+
"""
29+
Create KeyStore Table Example.
30+
31+
:param keystore_table_name: The name of the DynamoDB table to create
32+
:param logical_keystore_name: The logical name for this keystore
33+
:param kms_key_arn: The ARN of the KMS key to use for protecting branch keys
34+
"""
35+
# 1. Configure your KeyStore resource.
36+
# `ddb_table_name` is the name you want for the DDB table that
37+
# will back your keystore.
38+
# `kms_key_arn` is the KMS Key that will protect your branch keys and beacon keys
39+
# when they are stored in your DDB table.
40+
keystore = KeyStore(
41+
config=KeyStoreConfig(
42+
ddb_client=boto3.client("dynamodb"),
43+
ddb_table_name=keystore_table_name,
44+
logical_key_store_name=logical_keystore_name,
45+
kms_client=boto3.client("kms"),
46+
kms_configuration=KMSConfigurationKmsKeyArn(kms_key_arn),
47+
)
48+
)
49+
50+
# 2. Create the DynamoDb table that will store the branch keys and beacon keys.
51+
# This checks if the correct table already exists at `ddb_table_name`
52+
# by using the DescribeTable API. If no table exists,
53+
# it will create one. If a table exists, it will verify
54+
# the table's configuration and will error if the configuration is incorrect.
55+
keystore.create_key_store(input=CreateKeyStoreInput())
56+
# It may take a couple of minutes for the table to become ACTIVE,
57+
# at which point it is ready to store branch and beacon keys.
58+
# See the Create KeyStore Key Example for how to populate
59+
# this table.

‎Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
)
4646

4747

48-
def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str) -> None:
48+
def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str):
4949
"""Encrypt and decrypt an item with an ItemEncryptor."""
5050
# 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
5151
# For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example implementation of a branch key ID supplier.
5+
6+
Used in the 'HierarchicalKeyringExample'.
7+
In that example, we have a table where we distinguish multiple tenants
8+
by a tenant ID that is stored in our partition attribute.
9+
The expectation is that this does not produce a confused deputy
10+
because the tenants are separated by partition.
11+
In order to create a Hierarchical Keyring that is capable of encrypting or
12+
decrypting data for either tenant, we implement this interface
13+
to map the correct branch key ID to the correct tenant ID.
14+
"""
15+
from typing import Dict
16+
17+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.references import (
18+
IDynamoDbKeyBranchKeyIdSupplier,
19+
)
20+
from aws_dbesdk_dynamodb.structures.dynamodb import GetBranchKeyIdFromDdbKeyInput, GetBranchKeyIdFromDdbKeyOutput
21+
22+
23+
class ExampleBranchKeyIdSupplier(IDynamoDbKeyBranchKeyIdSupplier):
24+
"""Example implementation of a branch key ID supplier."""
25+
26+
branch_key_id_for_tenant1: str
27+
branch_key_id_for_tenant2: str
28+
29+
def __init__(self, tenant1_id: str, tenant2_id: str):
30+
"""
31+
Initialize a branch key ID supplier.
32+
33+
:param tenant1_id: Branch key ID for tenant 1
34+
:param tenant2_id: Branch key ID for tenant 2
35+
"""
36+
self.branch_key_id_for_tenant1 = tenant1_id
37+
self.branch_key_id_for_tenant2 = tenant2_id
38+
39+
def get_branch_key_id_from_ddb_key(self, param: GetBranchKeyIdFromDdbKeyInput) -> GetBranchKeyIdFromDdbKeyOutput:
40+
"""
41+
Get branch key ID from the tenant ID in input's DDB key.
42+
43+
:param param: Input containing DDB key
44+
:return: Output containing branch key ID
45+
:raises ValueError: If DDB key is invalid or contains invalid tenant ID
46+
"""
47+
key: Dict[str, Dict] = param.ddb_key
48+
49+
if "partition_key" not in key:
50+
raise ValueError("Item invalid, does not contain expected partition key attribute.")
51+
52+
tenant_key_id = key["partition_key"]["S"]
53+
54+
if tenant_key_id == "tenant1Id":
55+
branch_key_id = self.branch_key_id_for_tenant1
56+
elif tenant_key_id == "tenant2Id":
57+
branch_key_id = self.branch_key_id_for_tenant2
58+
else:
59+
raise ValueError("Item does not contain valid tenant ID")
60+
61+
return GetBranchKeyIdFromDdbKeyOutput(branch_key_id=branch_key_id)
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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.
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+
Establishing a key hierarchy like this has two benefits:
13+
14+
First, by caching the branch key material, and only calling back
15+
to KMS to re-establish authentication regularly according to your configured TTL,
16+
you limit how often you need to call back to KMS to protect your data.
17+
This is a performance/security tradeoff, where your authentication, audit, and
18+
logging from KMS is no longer one-to-one with every encrypt or decrypt call.
19+
However, the benefit is that you no longer have to make a
20+
network call to KMS for every encrypt or decrypt.
21+
22+
Second, this key hierarchy makes it easy to hold multi-tenant data
23+
that is isolated per branch key in a single DynamoDb table.
24+
You can create a branch key for each tenant in your table,
25+
and encrypt all that tenant's data under that distinct branch key.
26+
On decrypt, you can either statically configure a single branch key
27+
to ensure you are restricting decryption to a single tenant,
28+
or you can implement an interface that lets you map the primary key on your items
29+
to the branch key that should be responsible for decrypting that data.
30+
31+
This example then demonstrates configuring a Hierarchical Keyring
32+
with a Branch Key ID Supplier to encrypt and decrypt data for
33+
two separate tenants.
34+
35+
Running this example requires access to the DDB Table whose name
36+
is provided in CLI arguments.
37+
This table must be configured with the following
38+
primary key configuration:
39+
- Partition key is named "partition_key" with type (S)
40+
- Sort key is named "sort_key" with type (S)
41+
42+
This example also requires using a KMS Key whose ARN
43+
is provided in CLI arguments. You need the following access
44+
on this key:
45+
- GenerateDataKeyWithoutPlaintext
46+
- Decrypt
47+
"""
48+
49+
import boto3
50+
from aws_cryptographic_material_providers.keystore.client import KeyStore
51+
from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
52+
from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
53+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
54+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
55+
from aws_cryptographic_material_providers.mpl.models import (
56+
CacheTypeDefault,
57+
CreateAwsKmsHierarchicalKeyringInput,
58+
DefaultCache,
59+
)
60+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
61+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption
62+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import (
63+
DynamoDbEncryptionConfig,
64+
)
65+
from aws_dbesdk_dynamodb.structures.dynamodb import (
66+
CreateDynamoDbEncryptionBranchKeyIdSupplierInput,
67+
DynamoDbTableEncryptionConfig,
68+
DynamoDbTablesEncryptionConfig,
69+
)
70+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
71+
CryptoAction,
72+
)
73+
74+
from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier
75+
76+
77+
def hierarchical_keyring_get_item_put_item(
78+
ddb_table_name: str,
79+
tenant1_branch_key_id: str,
80+
tenant2_branch_key_id: str,
81+
keystore_table_name: str,
82+
logical_keystore_name: str,
83+
kms_key_id: str,
84+
):
85+
"""
86+
Demonstrate using a hierarchical keyring with multiple tenants.
87+
88+
:param ddb_table_name: The name of the DynamoDB table
89+
:param tenant1_branch_key_id: Branch key ID for tenant 1
90+
:param tenant2_branch_key_id: Branch key ID for tenant 2
91+
:param keystore_table_name: The name of the KeyStore DynamoDB table
92+
:param logical_keystore_name: The logical name for this keystore
93+
:param kms_key_id: The ARN of the KMS key to use
94+
"""
95+
# Initial KeyStore Setup: This example requires that you have already
96+
# created your KeyStore, and have populated it with two new branch keys.
97+
# See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
98+
# for an example of how to do this.
99+
100+
# 1. Configure your KeyStore resource.
101+
# This SHOULD be the same configuration that you used
102+
# to initially create and populate your KeyStore.
103+
keystore = KeyStore(
104+
config=KeyStoreConfig(
105+
ddb_client=boto3.client("dynamodb"),
106+
ddb_table_name=keystore_table_name,
107+
logical_key_store_name=logical_keystore_name,
108+
kms_client=boto3.client("kms"),
109+
kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id),
110+
)
111+
)
112+
113+
# 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
114+
ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig())
115+
branch_key_id_supplier = ddb_enc.create_dynamo_db_encryption_branch_key_id_supplier(
116+
input=CreateDynamoDbEncryptionBranchKeyIdSupplierInput(
117+
ddb_key_branch_key_id_supplier=ExampleBranchKeyIdSupplier(tenant1_branch_key_id, tenant2_branch_key_id)
118+
)
119+
).branch_key_id_supplier
120+
121+
# 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
122+
# With this configuration, the AWS SDK Client ultimately configured will be capable
123+
# of encrypting or decrypting items for either tenant (assuming correct KMS access).
124+
# If you want to restrict the client to only encrypt or decrypt for a single tenant,
125+
# configure this Hierarchical Keyring using `.branch_key_id=tenant1_branch_key_id` instead
126+
# of `.branch_key_id_supplier=branch_key_id_supplier`.
127+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
128+
129+
keyring_input = CreateAwsKmsHierarchicalKeyringInput(
130+
key_store=keystore,
131+
branch_key_id_supplier=branch_key_id_supplier,
132+
ttl_seconds=600, # This dictates how often we call back to KMS to authorize use of the branch keys
133+
cache=CacheTypeDefault( # This dictates how many branch keys will be held locally
134+
value=DefaultCache(entry_capacity=100)
135+
),
136+
)
137+
138+
hierarchical_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
139+
140+
# 4. Configure which attributes are encrypted and/or signed when writing new items.
141+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
142+
# we must explicitly configure how they should be treated during item encryption:
143+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
144+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
145+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
146+
attribute_actions = {
147+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
148+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
149+
"tenant_sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
150+
}
151+
152+
# 5. Configure which attributes we expect to be included in the signature
153+
# when reading items. There are two options for configuring this:
154+
#
155+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
156+
# When defining your DynamoDb schema and deciding on attribute names,
157+
# choose a distinguishing prefix (such as ":") for all attributes that
158+
# you do not want to include in the signature.
159+
# This has two main benefits:
160+
# - It is easier to reason about the security and authenticity of data within your item
161+
# when all unauthenticated data is easily distinguishable by their attribute name.
162+
# - If you need to add new unauthenticated attributes in the future,
163+
# you can easily make the corresponding update to your `attribute_actions`
164+
# and immediately start writing to that new attribute, without
165+
# any other configuration update needed.
166+
# Once you configure this field, it is not safe to update it.
167+
#
168+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
169+
# a set of attributes that should be considered unauthenticated when encountered
170+
# on read. Be careful if you use this configuration. Do not remove an attribute
171+
# name from this configuration, even if you are no longer writing with that attribute,
172+
# as old items may still include this attribute, and our configuration needs to know
173+
# to continue to exclude this attribute from the signature scope.
174+
# If you add new attribute names to this field, you must first deploy the update to this
175+
# field to all readers in your host fleet before deploying the update to start writing
176+
# with that new attribute.
177+
#
178+
# For this example, we currently authenticate all attributes. To make it easier to
179+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
180+
unsign_attr_prefix = ":"
181+
182+
# 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
183+
table_config = DynamoDbTableEncryptionConfig(
184+
logical_table_name=ddb_table_name,
185+
partition_key_name="partition_key",
186+
sort_key_name="sort_key",
187+
attribute_actions_on_encrypt=attribute_actions,
188+
keyring=hierarchical_keyring,
189+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
190+
)
191+
192+
table_configs = {ddb_table_name: table_config}
193+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
194+
195+
# 7. Create the EncryptedClient
196+
ddb_client = boto3.client("dynamodb")
197+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
198+
199+
# 8. Put an item into our table using the above client.
200+
# Before the item gets sent to DynamoDb, it will be encrypted
201+
# client-side, according to our configuration.
202+
# Because the item we are writing uses "tenantId1" as our partition value,
203+
# based on the code we wrote in the ExampleBranchKeySupplier,
204+
# `tenant1_branch_key_id` will be used to encrypt this item.
205+
item = {
206+
"partition_key": {"S": "tenant1Id"},
207+
"sort_key": {"N": "0"},
208+
"tenant_sensitive_data": {"S": "encrypt and sign me!"},
209+
}
210+
211+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
212+
213+
# Demonstrate that PutItem succeeded
214+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
215+
216+
# 9. Get the item back from our table using the same client.
217+
# The client will decrypt the item client-side, and return
218+
# back the original item.
219+
# Because the returned item's partition value is "tenantId1",
220+
# based on the code we wrote in the ExampleBranchKeySupplier,
221+
# `tenant1_branch_key_id` will be used to decrypt this item.
222+
key_to_get = {"partition_key": {"S": "tenant1Id"}, "sort_key": {"N": "0"}}
223+
224+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
225+
226+
# Demonstrate that GetItem succeeded and returned the decrypted item
227+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
228+
returned_item = get_response["Item"]
229+
assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!"

‎Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py

Lines changed: 451 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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 KMS RSA Keyring.
5+
6+
The KMS RSA Keyring uses a KMS RSA key pair to encrypt and decrypt records. The client
7+
uses the downloaded public key to encrypt items it adds to the table. The keyring
8+
uses the private key to decrypt existing table items it retrieves by calling
9+
KMS' decrypt API.
10+
11+
Running this example requires access to the DDB Table whose name is provided
12+
in CLI arguments. This table must be configured with the following primary key
13+
configuration:
14+
- Partition key is named "partition_key" with type (S)
15+
- Sort key is named "sort_key" with type (S)
16+
17+
The example also requires access to a KMS RSA key. Our tests provide a KMS RSA
18+
ARN that anyone can use, but you can also provide your own KMS RSA key.
19+
To use your own KMS RSA key, you must have either:
20+
- Its public key downloaded in a UTF-8 encoded PEM file
21+
- kms:GetPublicKey permissions on that key
22+
23+
If you do not have the public key downloaded, running this example through its
24+
main method will download the public key for you by calling kms:GetPublicKey.
25+
You must also have kms:Decrypt permissions on the KMS RSA key.
26+
"""
27+
28+
import os
29+
30+
import boto3
31+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
32+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
33+
from aws_cryptographic_material_providers.mpl.models import (
34+
CreateAwsKmsRsaKeyringInput,
35+
DBEAlgorithmSuiteId,
36+
)
37+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
38+
from aws_dbesdk_dynamodb.structures.dynamodb import (
39+
DynamoDbTableEncryptionConfig,
40+
DynamoDbTablesEncryptionConfig,
41+
)
42+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
43+
CryptoAction,
44+
)
45+
from cryptography.hazmat.primitives import serialization
46+
47+
DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "KmsRsaKeyringExamplePublicKey.pem"
48+
49+
50+
def kms_rsa_keyring_example(
51+
ddb_table_name: str, rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME
52+
):
53+
"""
54+
Create a KMS RSA keyring and use it to encrypt/decrypt DynamoDB items.
55+
56+
:param ddb_table_name: The name of the DynamoDB table
57+
:param rsa_key_arn: ARN of the KMS RSA key
58+
:param rsa_public_key_filename: Path to the public key PEM file
59+
"""
60+
# 1. Load UTF-8 encoded public key PEM file.
61+
# You may have an RSA public key file already defined.
62+
# If not, the main method in this class will call
63+
# the KMS RSA key, retrieve its public key, and store it
64+
# in a PEM file for example use.
65+
try:
66+
with open(rsa_public_key_filename, "rb") as f:
67+
public_key_utf8_encoded = f.read()
68+
except IOError as e:
69+
raise RuntimeError("IOError while reading public key from file") from e
70+
71+
# 2. Create a KMS RSA keyring.
72+
# This keyring takes in:
73+
# - kms_client
74+
# - kms_key_id: Must be an ARN representing a KMS RSA key
75+
# - public_key: A ByteBuffer of a UTF-8 encoded PEM file representing the public
76+
# key for the key passed into kms_key_id
77+
# - encryption_algorithm: Must be either RSAES_OAEP_SHA_256 or RSAES_OAEP_SHA_1
78+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
79+
80+
keyring_input = CreateAwsKmsRsaKeyringInput(
81+
kms_key_id=rsa_key_arn,
82+
kms_client=boto3.client("kms"),
83+
public_key=public_key_utf8_encoded,
84+
encryption_algorithm="RSAES_OAEP_SHA_256",
85+
)
86+
87+
kms_rsa_keyring = mat_prov.create_aws_kms_rsa_keyring(input=keyring_input)
88+
89+
# 3. Configure which attributes are encrypted and/or signed when writing new items.
90+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
91+
# we must explicitly configure how they should be treated during item encryption:
92+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
93+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
94+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
95+
attribute_actions = {
96+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
97+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
98+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
99+
}
100+
101+
# 4. Configure which attributes we expect to be included in the signature
102+
# when reading items. There are two options for configuring this:
103+
#
104+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
105+
# When defining your DynamoDb schema and deciding on attribute names,
106+
# choose a distinguishing prefix (such as ":") for all attributes that
107+
# you do not want to include in the signature.
108+
# This has two main benefits:
109+
# - It is easier to reason about the security and authenticity of data within your item
110+
# when all unauthenticated data is easily distinguishable by their attribute name.
111+
# - If you need to add new unauthenticated attributes in the future,
112+
# you can easily make the corresponding update to your `attribute_actions`
113+
# and immediately start writing to that new attribute, without
114+
# any other configuration update needed.
115+
# Once you configure this field, it is not safe to update it.
116+
#
117+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
118+
# a set of attributes that should be considered unauthenticated when encountered
119+
# on read. Be careful if you use this configuration. Do not remove an attribute
120+
# name from this configuration, even if you are no longer writing with that attribute,
121+
# as old items may still include this attribute, and our configuration needs to know
122+
# to continue to exclude this attribute from the signature scope.
123+
# If you add new attribute names to this field, you must first deploy the update to this
124+
# field to all readers in your host fleet before deploying the update to start writing
125+
# with that new attribute.
126+
#
127+
# For this example, we currently authenticate all attributes. To make it easier to
128+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
129+
unsign_attr_prefix = ":"
130+
131+
# 5. Create the DynamoDb Encryption configuration for the table we will be writing to.
132+
# Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite
133+
# that does not use asymmetric signing.
134+
table_config = DynamoDbTableEncryptionConfig(
135+
logical_table_name=ddb_table_name,
136+
partition_key_name="partition_key",
137+
sort_key_name="sort_key",
138+
attribute_actions_on_encrypt=attribute_actions,
139+
keyring=kms_rsa_keyring,
140+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
141+
# Specify algorithmSuite without asymmetric signing here
142+
# As of v3.0.0, the only supported algorithmSuite without asymmetric signing is
143+
# ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384.
144+
algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384,
145+
)
146+
147+
table_configs = {ddb_table_name: table_config}
148+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
149+
150+
# 6. Create the EncryptedClient
151+
ddb_client = boto3.client("dynamodb")
152+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
153+
154+
# 7. Put an item into our table using the above client.
155+
# Before the item gets sent to DynamoDb, it will be encrypted
156+
# client-side using the KMS RSA keyring.
157+
item = {
158+
"partition_key": {"S": "awsKmsRsaKeyringItem"},
159+
"sort_key": {"N": "0"},
160+
"sensitive_data": {"S": "encrypt and sign me!"},
161+
}
162+
163+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
164+
165+
# Demonstrate that PutItem succeeded
166+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
167+
168+
# 8. Get the item back from our table using the client.
169+
# The client will decrypt the item client-side using the RSA keyring
170+
# and return the original item.
171+
key_to_get = {"partition_key": {"S": "awsKmsRsaKeyringItem"}, "sort_key": {"N": "0"}}
172+
173+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
174+
175+
# Demonstrate that GetItem succeeded and returned the decrypted item
176+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
177+
returned_item = get_response["Item"]
178+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
179+
180+
181+
def should_get_new_public_key(rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME) -> bool:
182+
"""
183+
Check if we need to get a new public key.
184+
185+
:param rsa_public_key_filename: Path to the public key PEM file
186+
:return: True if we need to get a new public key, False otherwise
187+
"""
188+
# Check if a public key file already exists
189+
public_key_file = os.path.exists(rsa_public_key_filename)
190+
191+
# If a public key file already exists: do not overwrite existing file
192+
if public_key_file:
193+
return False
194+
195+
# If file is not present, generate a new key pair
196+
return True
197+
198+
199+
def write_public_key_pem_for_rsa_key(
200+
rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME
201+
):
202+
"""
203+
Get the public key from KMS and write it to a PEM file.
204+
205+
:param rsa_key_arn: The ARN of the KMS RSA key
206+
:param rsa_public_key_filename: Path to write the public key PEM file
207+
"""
208+
# Safety check: Validate file is not present
209+
if os.path.exists(rsa_public_key_filename):
210+
raise RuntimeError("getRsaPublicKey will not overwrite existing PEM files")
211+
212+
# This code will call KMS to get the public key for the KMS RSA key.
213+
# You must have kms:GetPublicKey permissions on the key for this to succeed.
214+
# The public key will be written to the file EXAMPLE_RSA_PUBLIC_KEY_FILENAME.
215+
kms_client = boto3.client("kms")
216+
response = kms_client.get_public_key(KeyId=rsa_key_arn)
217+
public_key_bytes = response["PublicKey"]
218+
219+
# Convert the public key to PEM format
220+
public_key = serialization.load_der_public_key(public_key_bytes)
221+
pem_data = public_key.public_bytes(
222+
encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
223+
)
224+
225+
# Write the PEM file
226+
try:
227+
with open(rsa_public_key_filename, "wb") as f:
228+
f.write(pem_data)
229+
except IOError as e:
230+
raise RuntimeError("IOError while writing public key PEM") from e
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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 MRK discovery multi-keyring.
5+
6+
A discovery keyring is not provided with any wrapping keys; instead, it recognizes
7+
the KMS key that was used to encrypt a data key, and asks KMS to decrypt with that
8+
KMS key. Discovery keyrings cannot be used to encrypt data.
9+
10+
For more information on discovery keyrings, see:
11+
https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery
12+
13+
The example encrypts an item using an MRK multi-keyring and puts the encrypted
14+
item to the configured DynamoDb table. Then, it gets the item from the table and
15+
decrypts it using the discovery keyring.
16+
17+
Running this example requires access to the DDB Table whose name is provided in
18+
CLI arguments. This table must be configured with the following primary key
19+
configuration:
20+
- Partition key is named "partition_key" with type (S)
21+
- Sort key is named "sort_key" with type (S)
22+
"""
23+
from typing import List
24+
25+
import boto3
26+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
27+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
28+
from aws_cryptographic_material_providers.mpl.models import (
29+
CreateAwsKmsMrkDiscoveryMultiKeyringInput,
30+
CreateAwsKmsMrkMultiKeyringInput,
31+
DiscoveryFilter,
32+
)
33+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
34+
from aws_dbesdk_dynamodb.structures.dynamodb import (
35+
DynamoDbTableEncryptionConfig,
36+
DynamoDbTablesEncryptionConfig,
37+
)
38+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
39+
CryptoAction,
40+
)
41+
42+
43+
def multi_mrk_discovery_keyring_get_item_put_item(
44+
ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str]
45+
):
46+
"""
47+
Demonstrate using a MRK discovery multi-keyring.
48+
49+
:param ddb_table_name: The name of the DynamoDB table
50+
:param key_arn: The ARN of the KMS key to use for encryption
51+
:param account_ids: List of AWS account IDs for discovery filter
52+
:param regions: List of AWS regions for discovery keyring
53+
"""
54+
# 1. Create a single MRK multi-keyring using the key arn.
55+
# Although this example demonstrates use of the MRK discovery multi-keyring,
56+
# a discovery keyring cannot be used to encrypt. So we will need to construct
57+
# a non-discovery keyring for this example to encrypt. For more information on MRK
58+
# multi-keyrings, see the MultiMrkKeyringExample in this directory.
59+
# Though this is an "MRK multi-keyring", we do not need to provide multiple keys,
60+
# and can use single-region KMS keys. We will provide a single key here; this
61+
# can be either an MRK or a single-region key.
62+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
63+
64+
encrypt_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
65+
input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn)
66+
)
67+
68+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
69+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
70+
# we must explicitly configure how they should be treated during item encryption:
71+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
72+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
73+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
74+
attribute_actions_on_encrypt = {
75+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
76+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
77+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
78+
}
79+
80+
# 3. Configure which attributes we expect to be included in the signature
81+
# when reading items. There are two options for configuring this:
82+
#
83+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
84+
# When defining your DynamoDb schema and deciding on attribute names,
85+
# choose a distinguishing prefix (such as ":") for all attributes that
86+
# you do not want to include in the signature.
87+
# This has two main benefits:
88+
# - It is easier to reason about the security and authenticity of data within your item
89+
# when all unauthenticated data is easily distinguishable by their attribute name.
90+
# - If you need to add new unauthenticated attributes in the future,
91+
# you can easily make the corresponding update to your `attribute_actions_on_encrypt`
92+
# and immediately start writing to that new attribute, without
93+
# any other configuration update needed.
94+
# Once you configure this field, it is not safe to update it.
95+
#
96+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
97+
# a set of attributes that should be considered unauthenticated when encountered
98+
# on read. Be careful if you use this configuration. Do not remove an attribute
99+
# name from this configuration, even if you are no longer writing with that attribute,
100+
# as old items may still include this attribute, and our configuration needs to know
101+
# to continue to exclude this attribute from the signature scope.
102+
# If you add new attribute names to this field, you must first deploy the update to this
103+
# field to all readers in your host fleet before deploying the update to start writing
104+
# with that new attribute.
105+
#
106+
# For this example, we currently authenticate all attributes. To make it easier to
107+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
108+
unsign_attr_prefix = ":"
109+
110+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
111+
table_config = DynamoDbTableEncryptionConfig(
112+
logical_table_name=ddb_table_name,
113+
partition_key_name="partition_key",
114+
sort_key_name="sort_key",
115+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
116+
keyring=encrypt_keyring,
117+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
118+
)
119+
120+
table_configs = {ddb_table_name: table_config}
121+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
122+
123+
# 5. Create the EncryptedClient
124+
ddb_client = boto3.client("dynamodb")
125+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
126+
127+
# 6. Put an item into our table using the above client.
128+
# Before the item gets sent to DynamoDb, it will be encrypted
129+
# client-side using the MRK multi-keyring.
130+
item = {
131+
"partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"},
132+
"sort_key": {"N": "0"},
133+
"sensitive_data": {"S": "encrypt and sign me!"},
134+
}
135+
136+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
137+
138+
# Demonstrate that PutItem succeeded
139+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
140+
141+
# 7. Construct a discovery filter.
142+
# A discovery filter limits the set of encrypted data keys
143+
# the keyring can use to decrypt data.
144+
# We will only let the keyring use keys in the selected AWS accounts
145+
# and in the `aws` partition.
146+
# This is the suggested config for most users; for more detailed config, see
147+
# https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery
148+
discovery_filter = DiscoveryFilter(partition="aws", account_ids=account_ids)
149+
150+
# 8. Construct a discovery keyring.
151+
# Note that we choose to use the MRK discovery multi-keyring, even though
152+
# our original keyring used a single KMS key.
153+
decrypt_keyring = mat_prov.create_aws_kms_mrk_discovery_multi_keyring(
154+
input=CreateAwsKmsMrkDiscoveryMultiKeyringInput(discovery_filter=discovery_filter, regions=regions)
155+
)
156+
157+
# 9. Create new DDB config and client using the decrypt discovery keyring.
158+
# This is the same as the above config, except we pass in the decrypt keyring.
159+
table_config_for_decrypt = DynamoDbTableEncryptionConfig(
160+
logical_table_name=ddb_table_name,
161+
partition_key_name="partition_key",
162+
sort_key_name="sort_key",
163+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
164+
# Add decrypt keyring here
165+
keyring=decrypt_keyring,
166+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
167+
)
168+
169+
table_configs_for_decrypt = {ddb_table_name: table_config_for_decrypt}
170+
tables_config_for_decrypt = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs_for_decrypt)
171+
172+
encrypted_ddb_client_for_decrypt = EncryptedClient(client=ddb_client, encryption_config=tables_config_for_decrypt)
173+
174+
# 10. Get the item back from our table using the client.
175+
# The client will retrieve encrypted items from the DDB table, then
176+
# detect the KMS key that was used to encrypt their data keys.
177+
# The client will make a request to KMS to decrypt with the encrypting KMS key.
178+
# If the client has permission to decrypt with the KMS key,
179+
# the client will decrypt the item client-side using the keyring
180+
# and return the original item.
181+
key_to_get = {"partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"}, "sort_key": {"N": "0"}}
182+
183+
get_response = encrypted_ddb_client_for_decrypt.get_item(TableName=ddb_table_name, Key=key_to_get)
184+
185+
# Demonstrate that GetItem succeeded and returned the decrypted item
186+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
187+
returned_item = get_response["Item"]
188+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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 an MRK multi-keyring configuration.
5+
6+
The MRK multi-keyring accepts multiple AWS KMS MRKs (multi-region keys) or regular
7+
AWS KMS keys (single-region keys) and uses them to encrypt and decrypt data. Data
8+
encrypted using an MRK multi-keyring can be decrypted using any of its component
9+
keys. If a component key is an MRK with a replica in a second region, the replica
10+
key can also be used to decrypt data.
11+
12+
For more information on MRKs and multi-keyrings, see:
13+
- MRKs: https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html
14+
- Multi-keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html
15+
16+
The example creates a new MRK multi-keyring consisting of one MRK (labeled as the
17+
"generator keyring") and one single-region key (labeled as the only "child keyring").
18+
The MRK also has a replica in a second region.
19+
20+
The example encrypts a test item using the MRK multi-keyring and puts the encrypted
21+
item to the provided DynamoDb table. Then, it gets the item from the table and
22+
decrypts it using three different configs:
23+
1. The MRK multi-keyring, where the MRK key is used to decrypt
24+
2. Another MRK multi-keyring, where the replica MRK key is used to decrypt
25+
3. Another MRK multi-keyring, where the single-region key that was present
26+
in the original MRK multi-keyring is used to decrypt
27+
28+
Running this example requires access to the DDB Table whose name is provided in
29+
CLI arguments. This table must be configured with the following primary key
30+
configuration:
31+
- Partition key is named "partition_key" with type (S)
32+
- Sort key is named "sort_key" with type (S)
33+
34+
Since this example demonstrates multi-region use cases, it requires a default
35+
region set in your AWS client. You can set a default region through the AWS CLI:
36+
aws configure set region [region-name]
37+
For example:
38+
aws configure set region us-west-2
39+
40+
For more information on using AWS CLI to set config, see:
41+
https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configure/set.html
42+
"""
43+
import boto3
44+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
45+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
46+
from aws_cryptographic_material_providers.mpl.models import (
47+
CreateAwsKmsMrkMultiKeyringInput,
48+
)
49+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
50+
from aws_dbesdk_dynamodb.structures.dynamodb import (
51+
DynamoDbTableEncryptionConfig,
52+
DynamoDbTablesEncryptionConfig,
53+
)
54+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
55+
CryptoAction,
56+
)
57+
58+
59+
def multi_mrk_keyring_get_item_put_item(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str):
60+
"""
61+
Demonstrate using a MRK multi-keyring.
62+
63+
:param ddb_table_name: The name of the DynamoDB table
64+
:param mrk_key_arn: The ARN of the MRK key to use as generator
65+
:param key_arn: The ARN of the single-region key to use as child
66+
:param mrk_replica_key_arn: The ARN of the MRK replica key
67+
"""
68+
# 1. Create a single MRK multi-keyring using the MRK arn and the single-region key arn.
69+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
70+
71+
# Create the multi-keyring, using the MRK as the generator key,
72+
# and the single-region key as a child key.
73+
# Note that the generator key will generate and encrypt a plaintext data key
74+
# and all child keys will only encrypt that same plaintext data key.
75+
# As such, you must have permission to call KMS:GenerateDataKey on your generator key
76+
# and permission to call KMS:Encrypt on all child keys.
77+
# For more information, see the AWS docs on multi-keyrings above.
78+
aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
79+
input=CreateAwsKmsMrkMultiKeyringInput(generator=mrk_key_arn, kms_key_ids=[key_arn])
80+
)
81+
82+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
83+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
84+
# we must explicitly configure how they should be treated during item encryption:
85+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
86+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
87+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
88+
attribute_actions_on_encrypt = {
89+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
90+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
91+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
92+
}
93+
94+
# 3. Configure which attributes we expect to be included in the signature
95+
# when reading items. There are two options for configuring this:
96+
#
97+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
98+
# When defining your DynamoDb schema and deciding on attribute names,
99+
# choose a distinguishing prefix (such as ":") for all attributes that
100+
# you do not want to include in the signature.
101+
# This has two main benefits:
102+
# - It is easier to reason about the security and authenticity of data within your item
103+
# when all unauthenticated data is easily distinguishable by their attribute name.
104+
# - If you need to add new unauthenticated attributes in the future,
105+
# you can easily make the corresponding update to your `attribute_actions_on_encrypt`
106+
# and immediately start writing to that new attribute, without
107+
# any other configuration update needed.
108+
# Once you configure this field, it is not safe to update it.
109+
#
110+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
111+
# a set of attributes that should be considered unauthenticated when encountered
112+
# on read. Be careful if you use this configuration. Do not remove an attribute
113+
# name from this configuration, even if you are no longer writing with that attribute,
114+
# as old items may still include this attribute, and our configuration needs to know
115+
# to continue to exclude this attribute from the signature scope.
116+
# If you add new attribute names to this field, you must first deploy the update to this
117+
# field to all readers in your host fleet before deploying the update to start writing
118+
# with that new attribute.
119+
#
120+
# For this example, we currently authenticate all attributes. To make it easier to
121+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
122+
unsign_attr_prefix = ":"
123+
124+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
125+
table_config = DynamoDbTableEncryptionConfig(
126+
logical_table_name=ddb_table_name,
127+
partition_key_name="partition_key",
128+
sort_key_name="sort_key",
129+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
130+
keyring=aws_kms_mrk_multi_keyring,
131+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
132+
)
133+
134+
table_configs = {ddb_table_name: table_config}
135+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
136+
137+
# 5. Create the EncryptedClient
138+
ddb_client = boto3.client("dynamodb")
139+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
140+
141+
# 6. Put an item into our table using the above client.
142+
# Before the item gets sent to DynamoDb, it will be encrypted
143+
# client-side using the MRK multi-keyring.
144+
# The data key protecting this item will be encrypted
145+
# with all the KMS Keys in this keyring, so that it can be
146+
# decrypted with any one of those KMS Keys.
147+
item = {
148+
"partition_key": {"S": "awsKmsMrkMultiKeyringItem"},
149+
"sort_key": {"N": "0"},
150+
"sensitive_data": {"S": "encrypt and sign me!"},
151+
}
152+
153+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
154+
155+
# Demonstrate that PutItem succeeded
156+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
157+
158+
# 7. Get the item back from our table using the client.
159+
# The client will decrypt the item client-side using the MRK
160+
# and return back the original item.
161+
# Since the generator key is the first available key in the keyring,
162+
# that is the KMS Key that will be used to decrypt this item.
163+
key_to_get = {"partition_key": {"S": "awsKmsMrkMultiKeyringItem"}, "sort_key": {"N": "0"}}
164+
165+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
166+
167+
# Demonstrate that GetItem succeeded and returned the decrypted item
168+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
169+
returned_item = get_response["Item"]
170+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
171+
172+
# 8. Create a MRK keyring using the replica MRK arn.
173+
# We will use this to demonstrate that the replica MRK
174+
# can decrypt data created with the original MRK,
175+
# even when the replica MRK was not present in the
176+
# encrypting multi-keyring.
177+
only_replica_key_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
178+
input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[mrk_replica_key_arn])
179+
)
180+
181+
# 9. Create a new config and client using the MRK keyring.
182+
# This is the same setup as above, except we provide the MRK keyring to the config.
183+
only_replica_key_table_config = DynamoDbTableEncryptionConfig(
184+
logical_table_name=ddb_table_name,
185+
partition_key_name="partition_key",
186+
sort_key_name="sort_key",
187+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
188+
keyring=only_replica_key_mrk_multi_keyring, # Only replica keyring added here
189+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
190+
)
191+
192+
only_replica_key_table_configs = {ddb_table_name: only_replica_key_table_config}
193+
only_replica_key_tables_config = DynamoDbTablesEncryptionConfig(
194+
table_encryption_configs=only_replica_key_table_configs
195+
)
196+
197+
only_replica_key_encrypted_ddb_client = EncryptedClient(
198+
client=ddb_client, encryption_config=only_replica_key_tables_config
199+
)
200+
201+
# 10. Get the item back from our table using the client configured with the replica.
202+
# The client will decrypt the item client-side using the replica MRK
203+
# and return back the original item.
204+
only_replica_key_get_response = only_replica_key_encrypted_ddb_client.get_item(
205+
TableName=ddb_table_name, Key=key_to_get
206+
)
207+
208+
# Demonstrate that GetItem succeeded and returned the decrypted item
209+
assert only_replica_key_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
210+
only_replica_key_returned_item = only_replica_key_get_response["Item"]
211+
assert only_replica_key_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
212+
213+
# 11. Create an AWS KMS keyring using the single-region key ARN.
214+
# We will use this to demonstrate that the single-region key
215+
# can decrypt data created with the MRK multi-keyring,
216+
# since it is present in the keyring used to encrypt.
217+
only_srk_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
218+
input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[key_arn])
219+
)
220+
221+
# 12. Create a new config and client using the AWS KMS keyring.
222+
# This is the same setup as above, except we provide the AWS KMS keyring to the config.
223+
only_srk_table_config = DynamoDbTableEncryptionConfig(
224+
logical_table_name=ddb_table_name,
225+
partition_key_name="partition_key",
226+
sort_key_name="sort_key",
227+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
228+
keyring=only_srk_keyring, # Only single-region key keyring added here
229+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
230+
)
231+
232+
only_srk_table_configs = {ddb_table_name: only_srk_table_config}
233+
only_srk_tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=only_srk_table_configs)
234+
235+
only_srk_encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=only_srk_tables_config)
236+
237+
# 13. Get the item back from our table using the client configured with the AWS KMS keyring.
238+
# The client will decrypt the item client-side using the single-region key
239+
# and return back the original item.
240+
only_srk_get_response = only_srk_encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
241+
242+
# Demonstrate that GetItem succeeded and returned the decrypted item
243+
assert only_srk_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
244+
only_srk_returned_item = only_srk_get_response["Item"]
245+
assert only_srk_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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 multi-keyring configuration.
5+
6+
A multi-keyring accepts multiple keyrings and uses them to encrypt and decrypt data.
7+
Data encrypted with a multi-keyring can be decrypted with any of its component keyrings.
8+
9+
For more information on multi-keyrings, see:
10+
https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html
11+
12+
The example creates a multi-keyring consisting of an AWS KMS keyring (labeled the
13+
"generator keyring") and a raw AES keyring (labeled as the only "child keyring").
14+
It encrypts a test item using the multi-keyring and puts the encrypted item to the
15+
provided DynamoDb table. Then, it gets the item from the table and decrypts it
16+
using only the raw AES keyring.
17+
18+
The example takes an `aes_key_bytes` parameter representing a 256-bit AES key.
19+
If run through the class's main method, it will create a new key. In practice,
20+
users should not randomly generate a key, but instead retrieve an existing key
21+
from a secure key management system (e.g. an HSM).
22+
23+
Running this example requires access to the DDB Table whose name is provided in
24+
CLI arguments. This table must be configured with the following primary key
25+
configuration:
26+
- Partition key is named "partition_key" with type (S)
27+
- Sort key is named "sort_key" with type (S)
28+
"""
29+
import boto3
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+
AesWrappingAlg,
34+
CreateAwsKmsMrkMultiKeyringInput,
35+
CreateMultiKeyringInput,
36+
CreateRawAesKeyringInput,
37+
)
38+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
39+
from aws_dbesdk_dynamodb.structures.dynamodb import (
40+
DynamoDbTableEncryptionConfig,
41+
DynamoDbTablesEncryptionConfig,
42+
)
43+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
44+
CryptoAction,
45+
)
46+
47+
48+
def multi_keyring_get_item_put_item(ddb_table_name: str, key_arn: str, aes_key_bytes: bytes):
49+
"""
50+
Demonstrate using a multi-keyring.
51+
52+
:param ddb_table_name: The name of the DynamoDB table
53+
:param key_arn: The ARN of the KMS key to use
54+
:param aes_key_bytes: The AES key bytes to use
55+
"""
56+
# 1. Create the raw AES keyring.
57+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
58+
59+
raw_aes_keyring_input = CreateRawAesKeyringInput(
60+
key_name="my-aes-key-name",
61+
key_namespace="my-key-namespace",
62+
wrapping_key=aes_key_bytes,
63+
wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16,
64+
)
65+
66+
raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=raw_aes_keyring_input)
67+
68+
# 2. Create the AWS KMS keyring.
69+
# We create a MRK multi keyring, as this interface also supports
70+
# single-region KMS keys (standard KMS keys),
71+
# and creates the KMS client for us automatically.
72+
aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
73+
input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn)
74+
)
75+
76+
# 3. Create the multi-keyring.
77+
# We will label the AWS KMS keyring as the generator and the raw AES keyring as the
78+
# only child keyring.
79+
# You must provide a generator keyring to encrypt data.
80+
# You may provide additional child keyrings. Each child keyring will be able to
81+
# decrypt data encrypted with the multi-keyring on its own. It does not need
82+
# knowledge of any other child keyrings or the generator keyring to decrypt.
83+
multi_keyring = mat_prov.create_multi_keyring(
84+
input=CreateMultiKeyringInput(generator=aws_kms_mrk_multi_keyring, child_keyrings=[raw_aes_keyring])
85+
)
86+
87+
# 4. Configure which attributes are encrypted and/or signed when writing new items.
88+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
89+
# we must explicitly configure how they should be treated during item encryption:
90+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
91+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
92+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
93+
attribute_actions = {
94+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
95+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
96+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
97+
}
98+
99+
# 5. Configure which attributes we expect to be included in the signature
100+
# when reading items. There are two options for configuring this:
101+
#
102+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
103+
# When defining your DynamoDb schema and deciding on attribute names,
104+
# choose a distinguishing prefix (such as ":") for all attributes that
105+
# you do not want to include in the signature.
106+
# This has two main benefits:
107+
# - It is easier to reason about the security and authenticity of data within your item
108+
# when all unauthenticated data is easily distinguishable by their attribute name.
109+
# - If you need to add new unauthenticated attributes in the future,
110+
# you can easily make the corresponding update to your `attribute_actions`
111+
# and immediately start writing to that new attribute, without
112+
# any other configuration update needed.
113+
# Once you configure this field, it is not safe to update it.
114+
#
115+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
116+
# a set of attributes that should be considered unauthenticated when encountered
117+
# on read. Be careful if you use this configuration. Do not remove an attribute
118+
# name from this configuration, even if you are no longer writing with that attribute,
119+
# as old items may still include this attribute, and our configuration needs to know
120+
# to continue to exclude this attribute from the signature scope.
121+
# If you add new attribute names to this field, you must first deploy the update to this
122+
# field to all readers in your host fleet before deploying the update to start writing
123+
# with that new attribute.
124+
#
125+
# For this example, we currently authenticate all attributes. To make it easier to
126+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
127+
unsign_attr_prefix = ":"
128+
129+
# 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
130+
# Note that this example creates one config/client combination for PUT, and another
131+
# for GET. The PUT config uses the multi-keyring, while the GET config uses the
132+
# raw AES keyring. This is solely done to demonstrate that a keyring included as
133+
# a child of a multi-keyring can be used to decrypt data on its own.
134+
table_config = DynamoDbTableEncryptionConfig(
135+
logical_table_name=ddb_table_name,
136+
partition_key_name="partition_key",
137+
sort_key_name="sort_key",
138+
attribute_actions_on_encrypt=attribute_actions,
139+
keyring=multi_keyring, # Multi-keyring is added here
140+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
141+
)
142+
143+
table_configs = {ddb_table_name: table_config}
144+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
145+
146+
# 7. Create the EncryptedClient
147+
ddb_client = boto3.client("dynamodb")
148+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
149+
150+
# 8. Put an item into our table using the above client.
151+
# Before the item gets sent to DynamoDb, it will be encrypted
152+
# client-side using the multi-keyring.
153+
# The item will be encrypted with all wrapping keys in the keyring,
154+
# so that it can be decrypted with any one of the keys.
155+
item = {
156+
"partition_key": {"S": "multiKeyringItem"},
157+
"sort_key": {"N": "0"},
158+
"sensitive_data": {"S": "encrypt and sign me!"},
159+
}
160+
161+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
162+
163+
# Demonstrate that PutItem succeeded
164+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
165+
166+
# 9. Get the item back from our table using the above client.
167+
# The client will decrypt the item client-side using the AWS KMS
168+
# keyring, and return back the original item.
169+
# Since the generator key is the first available key in the keyring,
170+
# that is the key that will be used to decrypt this item.
171+
key_to_get = {"partition_key": {"S": "multiKeyringItem"}, "sort_key": {"N": "0"}}
172+
173+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
174+
175+
# Demonstrate that GetItem succeeded and returned the decrypted item
176+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
177+
returned_item = get_response["Item"]
178+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
179+
180+
# 10. Create a new config and client with only the raw AES keyring to GET the item
181+
# This is the same setup as above, except the config uses the `raw_aes_keyring`.
182+
only_aes_keyring_table_config = DynamoDbTableEncryptionConfig(
183+
logical_table_name=ddb_table_name,
184+
partition_key_name="partition_key",
185+
sort_key_name="sort_key",
186+
attribute_actions_on_encrypt=attribute_actions,
187+
keyring=raw_aes_keyring, # Raw AES keyring is added here
188+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
189+
)
190+
191+
only_aes_keyring_table_configs = {ddb_table_name: only_aes_keyring_table_config}
192+
only_aes_keyring_tables_config = DynamoDbTablesEncryptionConfig(
193+
table_encryption_configs=only_aes_keyring_table_configs
194+
)
195+
196+
only_aes_keyring_encrypted_ddb_client = EncryptedClient(
197+
client=ddb_client, encryption_config=only_aes_keyring_tables_config
198+
)
199+
200+
# 11. Get the item back from our table using the client
201+
# configured with only the raw AES keyring.
202+
# The client will decrypt the item client-side using the raw
203+
# AES keyring, and return back the original item.
204+
only_aes_keyring_get_response = only_aes_keyring_encrypted_ddb_client.get_item(
205+
TableName=ddb_table_name, Key=key_to_get
206+
)
207+
208+
# Demonstrate that GetItem succeeded and returned the decrypted item
209+
assert only_aes_keyring_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
210+
only_aes_keyring_returned_item = only_aes_keyring_get_response["Item"]
211+
assert only_aes_keyring_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 raw AES Keyring.
5+
6+
The raw AES Keyring takes in an AES key and uses that key to protect the data
7+
keys that encrypt and decrypt DynamoDb table items.
8+
9+
This example takes an `aes_key_bytes` parameter representing a 256-bit AES key.
10+
If run through the script's main method, it will create a new key. In practice,
11+
users should not randomly generate a key, but instead retrieve an existing key
12+
from a secure key management system (e.g. an HSM).
13+
14+
This example encrypts a test item using the provided AES key and puts the encrypted
15+
item to the provided DynamoDb table. Then, it gets the item from the table and
16+
decrypts it.
17+
18+
Running this example requires access to the DDB Table whose name is provided in
19+
CLI arguments. This table must be configured with the following primary key
20+
configuration:
21+
- Partition key is named "partition_key" with type (S)
22+
- Sort key is named "sort_key" with type (S)
23+
"""
24+
25+
import boto3
26+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
27+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
28+
from aws_cryptographic_material_providers.mpl.models import (
29+
AesWrappingAlg,
30+
CreateRawAesKeyringInput,
31+
)
32+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
33+
from aws_dbesdk_dynamodb.structures.dynamodb import (
34+
DynamoDbTableEncryptionConfig,
35+
DynamoDbTablesEncryptionConfig,
36+
)
37+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
38+
CryptoAction,
39+
)
40+
41+
42+
def raw_aes_keyring_get_item_put_item(ddb_table_name: str, aes_key_bytes: bytes):
43+
"""
44+
Demonstrate using a raw AES keyring.
45+
46+
:param ddb_table_name: The name of the DynamoDB table
47+
:param aes_key_bytes: The AES key bytes to use
48+
"""
49+
# 1. Create the keyring.
50+
# The DynamoDb encryption client uses this to encrypt and decrypt items.
51+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
52+
53+
keyring_input = CreateRawAesKeyringInput(
54+
key_name="my-aes-key-name",
55+
key_namespace="my-key-namespace",
56+
wrapping_key=aes_key_bytes,
57+
wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16,
58+
)
59+
60+
raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=keyring_input)
61+
62+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
63+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
64+
# we must explicitly configure how they should be treated during item encryption:
65+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
66+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
67+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
68+
attribute_actions = {
69+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
70+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
71+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
72+
}
73+
74+
# 3. Configure which attributes we expect to be included in the signature
75+
# when reading items. There are two options for configuring this:
76+
#
77+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
78+
# When defining your DynamoDb schema and deciding on attribute names,
79+
# choose a distinguishing prefix (such as ":") for all attributes that
80+
# you do not want to include in the signature.
81+
# This has two main benefits:
82+
# - It is easier to reason about the security and authenticity of data within your item
83+
# when all unauthenticated data is easily distinguishable by their attribute name.
84+
# - If you need to add new unauthenticated attributes in the future,
85+
# you can easily make the corresponding update to your `attribute_actions`
86+
# and immediately start writing to that new attribute, without
87+
# any other configuration update needed.
88+
# Once you configure this field, it is not safe to update it.
89+
#
90+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
91+
# a set of attributes that should be considered unauthenticated when encountered
92+
# on read. Be careful if you use this configuration. Do not remove an attribute
93+
# name from this configuration, even if you are no longer writing with that attribute,
94+
# as old items may still include this attribute, and our configuration needs to know
95+
# to continue to exclude this attribute from the signature scope.
96+
# If you add new attribute names to this field, you must first deploy the update to this
97+
# field to all readers in your host fleet before deploying the update to start writing
98+
# with that new attribute.
99+
#
100+
# For this example, we currently authenticate all attributes. To make it easier to
101+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
102+
unsign_attr_prefix = ":"
103+
104+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
105+
table_config = DynamoDbTableEncryptionConfig(
106+
logical_table_name=ddb_table_name,
107+
partition_key_name="partition_key",
108+
sort_key_name="sort_key",
109+
attribute_actions_on_encrypt=attribute_actions,
110+
keyring=raw_aes_keyring,
111+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
112+
)
113+
114+
table_configs = {ddb_table_name: table_config}
115+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
116+
117+
# 5. Create the EncryptedClient
118+
ddb_client = boto3.client("dynamodb")
119+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
120+
121+
# 6. Put an item into our table using the above client.
122+
# Before the item gets sent to DynamoDb, it will be encrypted
123+
# client-side, according to our configuration.
124+
item = {
125+
"partition_key": {"S": "rawAesKeyringItem"},
126+
"sort_key": {"N": "0"},
127+
"sensitive_data": {"S": "encrypt and sign me!"},
128+
}
129+
130+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
131+
132+
# Demonstrate that PutItem succeeded
133+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
134+
135+
# 7. Get the item back from our table using the same client.
136+
# The client will decrypt the item client-side, and return
137+
# back the original item.
138+
key_to_get = {"partition_key": {"S": "rawAesKeyringItem"}, "sort_key": {"N": "0"}}
139+
140+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
141+
142+
# Demonstrate that GetItem succeeded and returned the decrypted item
143+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
144+
returned_item = get_response["Item"]
145+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"

‎Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py

Lines changed: 564 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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 raw RSA Keyring.
5+
6+
The raw RSA Keyring uses an RSA key pair to encrypt and decrypt records.
7+
The keyring accepts PEM encodings of the key pair as UTF-8 interpreted bytes.
8+
The client uses the public key to encrypt items it adds to the table and
9+
uses the private key to decrypt existing table items it retrieves.
10+
11+
The example loads a key pair from PEM files with paths defined in:
12+
- EXAMPLE_RSA_PRIVATE_KEY_FILENAME
13+
- EXAMPLE_RSA_PUBLIC_KEY_FILENAME
14+
15+
If you do not provide these files, running this example through the main method
16+
will generate these files for you in the directory where the example is run.
17+
In practice, users of this library should not generate new key pairs like this,
18+
and should instead retrieve an existing key from a secure key management system
19+
(e.g. an HSM).
20+
21+
You may also provide your own key pair by placing PEM files in the directory
22+
where the example is run or modifying the paths in the code below. These files
23+
must be valid PEM encodings of the key pair as UTF-8 encoded bytes. If you do
24+
provide your own key pair, or if a key pair already exists, this class' main
25+
method will not generate a new key pair.
26+
27+
The example loads a key pair from disk, encrypts a test item, and puts the
28+
encrypted item to the provided DynamoDb table. Then, it gets the item from
29+
the table and decrypts it.
30+
31+
Running this example requires access to the DDB Table whose name is provided
32+
in CLI arguments. This table must be configured with the following primary
33+
key configuration:
34+
- Partition key is named "partition_key" with type (S)
35+
- Sort key is named "sort_key" with type (S)
36+
"""
37+
import os
38+
39+
import boto3
40+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
41+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
42+
from aws_cryptographic_material_providers.mpl.models import (
43+
CreateRawRsaKeyringInput,
44+
PaddingScheme,
45+
)
46+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
47+
from aws_dbesdk_dynamodb.structures.dynamodb import (
48+
DynamoDbTableEncryptionConfig,
49+
DynamoDbTablesEncryptionConfig,
50+
)
51+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
52+
CryptoAction,
53+
)
54+
from cryptography.hazmat.primitives import serialization
55+
from cryptography.hazmat.primitives.asymmetric import rsa
56+
57+
EXAMPLE_RSA_PRIVATE_KEY_FILENAME = "RawRsaKeyringExamplePrivateKey.pem"
58+
EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "RawRsaKeyringExamplePublicKey.pem"
59+
60+
61+
def raw_rsa_keyring_example(ddb_table_name: str):
62+
"""
63+
Create a Raw RSA keyring and use it to encrypt/decrypt DynamoDB items.
64+
65+
:param ddb_table_name: The name of the DynamoDB table
66+
"""
67+
# 1. Load key pair from UTF-8 encoded PEM files.
68+
# You may provide your own PEM files to use here.
69+
# If you do not, the main method in this class will generate PEM
70+
# files for example use. Do not use these files for any other purpose.
71+
try:
72+
with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "rb") as f:
73+
public_key_utf8_encoded = f.read()
74+
except IOError as e:
75+
raise RuntimeError("IOError while reading public key from file") from e
76+
77+
try:
78+
with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "rb") as f:
79+
private_key_utf8_encoded = f.read()
80+
except IOError as e:
81+
raise RuntimeError("IOError while reading private key from file") from e
82+
83+
# 2. Create the keyring.
84+
# The DynamoDb encryption client uses this to encrypt and decrypt items.
85+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
86+
87+
keyring_input = CreateRawRsaKeyringInput(
88+
key_name="my-rsa-key-name",
89+
key_namespace="my-key-namespace",
90+
padding_scheme=PaddingScheme.OAEP_SHA256_MGF1,
91+
public_key=public_key_utf8_encoded,
92+
private_key=private_key_utf8_encoded,
93+
)
94+
95+
raw_rsa_keyring = mat_prov.create_raw_rsa_keyring(input=keyring_input)
96+
97+
# 3. Configure which attributes are encrypted and/or signed when writing new items.
98+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
99+
# we must explicitly configure how they should be treated during item encryption:
100+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
101+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
102+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
103+
attribute_actions = {
104+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
105+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
106+
"sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
107+
}
108+
109+
# 4. Configure which attributes we expect to be included in the signature
110+
# when reading items. There are two options for configuring this:
111+
#
112+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
113+
# When defining your DynamoDb schema and deciding on attribute names,
114+
# choose a distinguishing prefix (such as ":") for all attributes that
115+
# you do not want to include in the signature.
116+
# This has two main benefits:
117+
# - It is easier to reason about the security and authenticity of data within your item
118+
# when all unauthenticated data is easily distinguishable by their attribute name.
119+
# - If you need to add new unauthenticated attributes in the future,
120+
# you can easily make the corresponding update to your `attribute_actions`
121+
# and immediately start writing to that new attribute, without
122+
# any other configuration update needed.
123+
# Once you configure this field, it is not safe to update it.
124+
#
125+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
126+
# a set of attributes that should be considered unauthenticated when encountered
127+
# on read. Be careful if you use this configuration. Do not remove an attribute
128+
# name from this configuration, even if you are no longer writing with that attribute,
129+
# as old items may still include this attribute, and our configuration needs to know
130+
# to continue to exclude this attribute from the signature scope.
131+
# If you add new attribute names to this field, you must first deploy the update to this
132+
# field to all readers in your host fleet before deploying the update to start writing
133+
# with that new attribute.
134+
#
135+
# For this example, we currently authenticate all attributes. To make it easier to
136+
# add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
137+
unsign_attr_prefix = ":"
138+
139+
# 5. Create the DynamoDb Encryption configuration for the table we will be writing to.
140+
table_config = DynamoDbTableEncryptionConfig(
141+
logical_table_name=ddb_table_name,
142+
partition_key_name="partition_key",
143+
sort_key_name="sort_key",
144+
attribute_actions_on_encrypt=attribute_actions,
145+
keyring=raw_rsa_keyring,
146+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
147+
)
148+
149+
table_configs = {ddb_table_name: table_config}
150+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
151+
152+
# 6. Create the EncryptedClient
153+
ddb_client = boto3.client("dynamodb")
154+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
155+
156+
# 7. Put an item into our table using the above client.
157+
# Before the item gets sent to DynamoDb, it will be encrypted
158+
# client-side using the Raw RSA keyring.
159+
item = {
160+
"partition_key": {"S": "rawRsaKeyringItem"},
161+
"sort_key": {"N": "0"},
162+
"sensitive_data": {"S": "encrypt and sign me!"},
163+
}
164+
165+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
166+
167+
# Demonstrate that PutItem succeeded
168+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
169+
170+
# 8. Get the item back from our table using the same client.
171+
# The client will decrypt the item client-side using the Raw RSA keyring
172+
# and return the original item.
173+
key_to_get = {"partition_key": {"S": "rawRsaKeyringItem"}, "sort_key": {"N": "0"}}
174+
175+
get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
176+
177+
# Demonstrate that GetItem succeeded and returned the decrypted item
178+
assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
179+
returned_item = get_response["Item"]
180+
assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
181+
182+
183+
def should_generate_new_rsa_key_pair() -> bool:
184+
"""
185+
Check if we need to generate a new RSA key pair.
186+
187+
:return: True if we need to generate a new key pair, False otherwise
188+
"""
189+
# Check if a key pair already exists
190+
private_key_file = os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME)
191+
public_key_file = os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME)
192+
193+
# If a key pair already exists: do not overwrite existing key pair
194+
if private_key_file and public_key_file:
195+
return False
196+
197+
# If only one file is present: throw exception
198+
if private_key_file and not public_key_file:
199+
raise ValueError(f"Missing public key file at {EXAMPLE_RSA_PUBLIC_KEY_FILENAME}")
200+
if not private_key_file and public_key_file:
201+
raise ValueError(f"Missing private key file at {EXAMPLE_RSA_PRIVATE_KEY_FILENAME}")
202+
203+
# If neither file is present, generate a new key pair
204+
return True
205+
206+
207+
def generate_rsa_key_pair():
208+
"""Generate a new RSA key pair and save to PEM files."""
209+
# Safety check: Validate neither file is present
210+
if os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) or os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME):
211+
raise FileExistsError("generateRsaKeyPair will not overwrite existing PEM files")
212+
213+
# This code will generate a new RSA key pair for example use.
214+
# The public and private key will be written to the files:
215+
# - public: EXAMPLE_RSA_PUBLIC_KEY_FILENAME
216+
# - private: EXAMPLE_RSA_PRIVATE_KEY_FILENAME
217+
# In practice, you should not generate this in your code, and should instead
218+
# retrieve this key from a secure key management system (e.g. HSM)
219+
# This key is created here for example purposes only.
220+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
221+
222+
# Write private key PEM file
223+
private_key_pem = private_key.private_bytes(
224+
encoding=serialization.Encoding.PEM,
225+
format=serialization.PrivateFormat.PKCS8,
226+
encryption_algorithm=serialization.NoEncryption(),
227+
)
228+
229+
try:
230+
with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "wb") as f:
231+
f.write(private_key_pem)
232+
except IOError as e:
233+
raise OSError("IOError while writing private key PEM") from e
234+
235+
# Write public key PEM file
236+
public_key = private_key.public_key()
237+
public_key_pem = public_key.public_bytes(
238+
encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
239+
)
240+
241+
try:
242+
with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "wb") as f:
243+
f.write(public_key_pem)
244+
except IOError as e:
245+
raise RuntimeError("IOError while writing public key PEM") from e

‎Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py

Lines changed: 352 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
Example demonstrating error handling for failed decryption during DynamoDB Scan operations.
6+
7+
Uses the Scan operation to show how to retrieve error messages from the
8+
returned CollectionOfErrors when some of the Scan results do not decrypt successfully.
9+
10+
Running this example requires access to the DDB Table whose name is provided in
11+
CLI arguments. This table must be configured with the following primary key
12+
configuration:
13+
- Partition key is named "partition_key" with type (S)
14+
- Sort key is named "sort_key" with type (N)
15+
"""
16+
17+
import sys
18+
19+
import boto3
20+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
21+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
22+
from aws_cryptographic_material_providers.mpl.errors import CollectionOfErrors
23+
from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsMrkMultiKeyringInput, DBEAlgorithmSuiteId
24+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
25+
from aws_dbesdk_dynamodb.structures.dynamodb import (
26+
DynamoDbTableEncryptionConfig,
27+
DynamoDbTablesEncryptionConfig,
28+
)
29+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
30+
CryptoAction,
31+
)
32+
33+
34+
def print_exception(e: Exception, indent: str = ""):
35+
"""
36+
Print exception and any nested CollectionOfErrors.
37+
38+
:param e: Exception to print
39+
:param indent: Indentation string for nested errors
40+
"""
41+
print(indent + str(e), file=sys.stderr)
42+
if isinstance(e.__cause__, CollectionOfErrors):
43+
print(indent + str(e.__cause__), file=sys.stderr)
44+
for err in e.__cause__.list():
45+
print_exception(err, indent + " ")
46+
elif isinstance(e, CollectionOfErrors):
47+
for err in e.list():
48+
print_exception(err, indent + " ")
49+
50+
51+
def scan_error(kms_key_id: str, ddb_table_name: str):
52+
"""
53+
Demonstrate handling scan errors.
54+
55+
:param kms_key_id: The ARN of the KMS key to use
56+
:param ddb_table_name: The name of the DynamoDB table
57+
"""
58+
# 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
59+
# For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
60+
# We will use the `create_aws_kms_mrk_multi_keyring` method to create this keyring,
61+
# as it will correctly handle both single region and Multi-Region KMS Keys.
62+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
63+
64+
kms_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
65+
input=CreateAwsKmsMrkMultiKeyringInput(generator=kms_key_id)
66+
)
67+
68+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
69+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
70+
# we must explicitly configure how they should be treated during item encryption:
71+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
72+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
73+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
74+
attribute_actions = {
75+
"partition_key": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
76+
"sort_key": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
77+
"attribute1": CryptoAction.ENCRYPT_AND_SIGN,
78+
"attribute2": CryptoAction.SIGN_ONLY,
79+
":attribute3": CryptoAction.DO_NOTHING,
80+
}
81+
82+
# 3. Configure which attributes we expect to be included in the signature
83+
# when reading items. There are two options for configuring this:
84+
#
85+
# - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
86+
# When defining your DynamoDb schema and deciding on attribute names,
87+
# choose a distinguishing prefix (such as ":") for all attributes that
88+
# you do not want to include in the signature.
89+
# This has two main benefits:
90+
# - It is easier to reason about the security and authenticity of data within your item
91+
# when all unauthenticated data is easily distinguishable by their attribute name.
92+
# - If you need to add new unauthenticated attributes in the future,
93+
# you can easily make the corresponding update to your `attribute_actions`
94+
# and immediately start writing to that new attribute, without
95+
# any other configuration update needed.
96+
# Once you configure this field, it is not safe to update it.
97+
#
98+
# - Configure `allowed_unsigned_attributes`: You may also explicitly list
99+
# a set of attributes that should be considered unauthenticated when encountered
100+
# on read. Be careful if you use this configuration. Do not remove an attribute
101+
# name from this configuration, even if you are no longer writing with that attribute,
102+
# as old items may still include this attribute, and our configuration needs to know
103+
# to continue to exclude this attribute from the signature scope.
104+
# If you add new attribute names to this field, you must first deploy the update to this
105+
# field to all readers in your host fleet before deploying the update to start writing
106+
# with that new attribute.
107+
#
108+
# For this example, we have designed our DynamoDb table such that any attribute name with
109+
# the ":" prefix should be considered unauthenticated.
110+
unsign_attr_prefix = ":"
111+
112+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
113+
table_config = DynamoDbTableEncryptionConfig(
114+
logical_table_name=ddb_table_name,
115+
partition_key_name="partition_key",
116+
sort_key_name="sort_key",
117+
attribute_actions_on_encrypt=attribute_actions,
118+
keyring=kms_keyring,
119+
allowed_unsigned_attribute_prefix=unsign_attr_prefix,
120+
# Specifying an algorithm suite is not required,
121+
# but is done here to demonstrate how to do so.
122+
# We suggest using the
123+
# `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite,
124+
# which includes AES-GCM with key derivation, signing, and key commitment.
125+
# This is also the default algorithm suite if one is not specified in this config.
126+
# For more information on supported algorithm suites, see:
127+
# https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html
128+
algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384,
129+
)
130+
131+
table_configs = {ddb_table_name: table_config}
132+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
133+
134+
# 5. Create the EncryptedClient
135+
ddb_client = boto3.client("dynamodb")
136+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
137+
138+
# 6. Perform a Scan for which some records will not decrypt
139+
expression_attribute_values = {":prefix": {"S": "Broken"}}
140+
141+
try:
142+
encrypted_ddb_client.scan(
143+
TableName=ddb_table_name,
144+
FilterExpression="begins_with(partition_key, :prefix)",
145+
ExpressionAttributeValues=expression_attribute_values,
146+
)
147+
assert False, "scan should have failed"
148+
except Exception as e:
149+
print_exception(e)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Test cleanup utilities for DynamoDB Encryption SDK.
5+
6+
This module provides utilities for cleaning up resources after running tests.
7+
8+
WARNING: Please be careful. This is only a test utility and should NOT be used in production code.
9+
It is specifically designed for cleaning up test resources after test execution.
10+
- Running this code on production resources or any data you want to keep could result
11+
in cryptographic shredding (permanent loss of access to encrypted data).
12+
- Only use this on test resources that you are willing to permanently delete.
13+
- Never run this against any production DynamoDB tables. Ensure you have backups
14+
of any important data before running cleanup operations.
15+
"""
16+
import boto3
17+
18+
BRANCH_KEY_IDENTIFIER_FIELD = "branch-key-id"
19+
TYPE_FIELD = "type"
20+
21+
22+
def delete_branch_key(
23+
identifier: str,
24+
table_name: str,
25+
ddb_client: boto3.client,
26+
) -> bool:
27+
"""
28+
Delete all branch key items with the given identifier.
29+
30+
Args:
31+
identifier: Branch key identifier to delete
32+
table_name: DynamoDB table name
33+
ddb_client: DynamoDB client to use
34+
35+
Returns:
36+
True if all items were deleted, False if more than 100 items exist
37+
38+
Raises:
39+
ValueError: If an item is not a branch key
40+
41+
"""
42+
if ddb_client is None:
43+
ddb_client = boto3.client("dynamodb")
44+
45+
# Query for items with matching identifier
46+
query_response = ddb_client.query(
47+
TableName=table_name,
48+
KeyConditionExpression="#pk = :pk",
49+
ExpressionAttributeNames={"#pk": BRANCH_KEY_IDENTIFIER_FIELD},
50+
ExpressionAttributeValues={":pk": {"S": identifier}},
51+
)
52+
53+
items = query_response.get("Items", [])
54+
if not items:
55+
return True
56+
57+
# Create delete requests for each item
58+
delete_items = []
59+
for item in items:
60+
if TYPE_FIELD not in item:
61+
raise ValueError("Item is not a branch key")
62+
63+
delete_item = {
64+
"Delete": {
65+
"Key": {BRANCH_KEY_IDENTIFIER_FIELD: {"S": identifier}, TYPE_FIELD: item[TYPE_FIELD]},
66+
"TableName": table_name,
67+
}
68+
}
69+
delete_items.append(delete_item)
70+
71+
if not delete_items:
72+
return True
73+
74+
# DynamoDB transactions are limited to 100 items
75+
if len(delete_items) > 100:
76+
delete_items = delete_items[:100]
77+
78+
# Execute the delete transaction
79+
ddb_client.transact_write_items(TransactItems=delete_items)
80+
81+
# Return False if we had to truncate the deletion
82+
return len(items) <= 100
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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test hierarchical keyring example."""
4+
import time
5+
6+
import pytest
7+
8+
from ...src.create_keystore_key_example import keystore_create_key
9+
from ...src.keyring.hierarchical_keyring_example import hierarchical_keyring_get_item_put_item
10+
from ..cleanup import delete_branch_key
11+
from ..test_utils import (
12+
TEST_DDB_TABLE_NAME,
13+
TEST_KEYSTORE_KMS_KEY_ID,
14+
TEST_KEYSTORE_NAME,
15+
TEST_LOGICAL_KEYSTORE_NAME,
16+
)
17+
18+
pytestmark = [pytest.mark.examples]
19+
20+
21+
def test_hierarchical_keyring_example():
22+
"""Test hierarchical_keyring_example."""
23+
# Create new branch keys for test
24+
key_id1 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
25+
key_id2 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
26+
27+
# Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
28+
# our test fails due to eventual consistency issues.
29+
time.sleep(5)
30+
31+
hierarchical_keyring_get_item_put_item(
32+
TEST_DDB_TABLE_NAME, key_id1, key_id2, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID
33+
)
34+
35+
# Cleanup Branch Key
36+
delete_branch_key(key_id1, TEST_KEYSTORE_NAME, None)
37+
delete_branch_key(key_id2, TEST_KEYSTORE_NAME, None)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test KMS ECDH keyring examples."""
4+
import pytest
5+
6+
from ...src.keyring.kms_ecdh_keyring_example import (
7+
EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME,
8+
EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME,
9+
kms_ecdh_discovery_get_item,
10+
kms_ecdh_keyring_get_item_put_item,
11+
should_get_new_public_keys,
12+
write_public_key_pem_for_ecc_key,
13+
)
14+
from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, TEST_KMS_ECDH_KEY_ID_P256_SENDER
15+
16+
pytestmark = [pytest.mark.examples]
17+
18+
19+
def test_kms_ecdh_keyring_example_static():
20+
"""Test kms_ecdh_keyring_example with static configuration."""
21+
# You may provide your own ECC public keys at
22+
# - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME
23+
# - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME.
24+
# If you provide these, the keys MUST be on curve P256
25+
# This must be the public key for the ECC key represented at eccKeyArn
26+
# If this file is not present, this will write a UTF-8 encoded PEM file for you.
27+
if should_get_new_public_keys():
28+
write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME)
29+
write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME)
30+
31+
kms_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER)
32+
33+
34+
def test_kms_ecdh_keyring_example_discovery():
35+
"""Test kms_ecdh_keyring_example with discovery configuration."""
36+
# In this example you do not need to provide the recipient ECC Public Key.
37+
# On initialization, the keyring will call KMS:getPublicKey on the configured
38+
# recipientKmsIdentifier set on the keyring. This example uses the previous example
39+
# to write an item meant for the recipient.
40+
kms_ecdh_discovery_get_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test for the KMS RSA keyring example."""
4+
import pytest
5+
6+
from ...src.keyring.kms_rsa_keyring_example import (
7+
kms_rsa_keyring_example,
8+
should_get_new_public_key,
9+
write_public_key_pem_for_rsa_key,
10+
)
11+
from ..test_utils import (
12+
TEST_DDB_TABLE_NAME,
13+
TEST_KMS_RSA_KEY_ID,
14+
)
15+
16+
pytestmark = [pytest.mark.examples]
17+
18+
19+
def test_kms_rsa_keyring_example():
20+
"""Test the KMS RSA keyring example."""
21+
# You may provide your own RSA public key at EXAMPLE_RSA_PUBLIC_KEY_FILENAME.
22+
# This must be the public key for the RSA key represented at rsa_key_arn.
23+
# If this file is not present, this will write a UTF-8 encoded PEM file for you.
24+
if should_get_new_public_key():
25+
write_public_key_pem_for_rsa_key(TEST_KMS_RSA_KEY_ID)
26+
27+
kms_rsa_keyring_example(TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test MRK discovery multi-keyring example."""
4+
import pytest
5+
6+
from ...src.keyring.mrk_discovery_multi_keyring_example import multi_mrk_discovery_keyring_get_item_put_item
7+
from ..test_utils import (
8+
TEST_AWS_ACCOUNT_ID,
9+
TEST_AWS_REGION,
10+
TEST_DDB_TABLE_NAME,
11+
TEST_MRK_KEY_ID,
12+
)
13+
14+
pytestmark = [pytest.mark.examples]
15+
16+
17+
def test_mrk_discovery_multi_keyring_example():
18+
"""Test mrk_discovery_multi_keyring_example."""
19+
accounts = [TEST_AWS_ACCOUNT_ID]
20+
regions = [TEST_AWS_REGION]
21+
22+
multi_mrk_discovery_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, accounts, regions)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test MRK multi-keyring example."""
4+
import pytest
5+
6+
from ...src.keyring.mrk_multi_keyring_example import multi_mrk_keyring_get_item_put_item
7+
from ..test_utils import (
8+
TEST_DDB_TABLE_NAME,
9+
TEST_KMS_KEY_ID,
10+
TEST_MRK_KEY_ID,
11+
TEST_MRK_REPLICA_KEY_ID_US_EAST_1,
12+
)
13+
14+
pytestmark = [pytest.mark.examples]
15+
16+
17+
def test_mrk_multi_keyring_example():
18+
"""Test mrk_multi_keyring_example."""
19+
multi_mrk_keyring_get_item_put_item(
20+
TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, TEST_KMS_KEY_ID, TEST_MRK_REPLICA_KEY_ID_US_EAST_1
21+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test multi-keyring example."""
4+
import secrets
5+
6+
import pytest
7+
8+
from ...src.keyring.multi_keyring_example import multi_keyring_get_item_put_item
9+
from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID
10+
11+
pytestmark = [pytest.mark.examples]
12+
13+
14+
def test_multi_keyring_example():
15+
"""Test multi_keyring_example."""
16+
# Generate a new AES key
17+
aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits
18+
19+
multi_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, aes_key_bytes)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test raw AES keyring example."""
4+
import secrets
5+
6+
import pytest
7+
8+
from ...src.keyring.raw_aes_keyring_example import raw_aes_keyring_get_item_put_item
9+
from ..test_utils import TEST_DDB_TABLE_NAME
10+
11+
pytestmark = [pytest.mark.examples]
12+
13+
14+
def test_raw_aes_keyring_example():
15+
"""Test raw_aes_keyring_example."""
16+
# Generate a new AES key
17+
aes_key_bytes = secrets.token_bytes(32) # 32 bytes = 256 bits
18+
19+
raw_aes_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, aes_key_bytes)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test raw ECDH keyring examples."""
4+
import pytest
5+
from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec
6+
7+
from ...src.keyring.raw_ecdh_keyring_example import (
8+
discovery_raw_ecdh_keyring_get_item,
9+
ephemeral_raw_ecdh_keyring_put_item,
10+
generate_ecc_key_pairs,
11+
raw_ecdh_keyring_get_item_put_item,
12+
should_generate_new_ecc_key_pairs,
13+
)
14+
from ..test_utils import TEST_DDB_TABLE_NAME
15+
16+
pytestmark = [pytest.mark.examples]
17+
18+
19+
def test_static_raw_ecdh_keyring_example():
20+
"""Test raw_ecdh_keyring_example with static configuration."""
21+
# You may provide your own ECC Key pairs in the files located at
22+
# - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER
23+
# - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
24+
# If you provide this, the keys MUST be on curve P256
25+
# If these files are not present, this will generate a pair for you.
26+
# For this example we will use the curve P256.
27+
if should_generate_new_ecc_key_pairs():
28+
generate_ecc_key_pairs()
29+
30+
# Part of using these keyrings is knowing which curve the keys used in the key agreement
31+
# lie on. The keyring will fail if the keys do not lie on the configured curve.
32+
raw_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
33+
34+
35+
def test_ephemeral_raw_ecdh_keyring_example():
36+
"""Test raw_ecdh_keyring_example with ephemeral configuration."""
37+
# You may provide your own ECC Public Key in the files located at
38+
# - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
39+
# If you provide this, the keys MUST be on curve P256
40+
# If these files are not present, this will generate a pair for you.
41+
# For this example we will use the curve P256.
42+
if should_generate_new_ecc_key_pairs():
43+
generate_ecc_key_pairs()
44+
45+
# Part of using these keyrings is knowing which curve the keys used in the key agreement
46+
# lie on. The keyring will fail if the keys do not lie on the configured curve.
47+
ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
48+
49+
50+
def test_discovery_raw_ecdh_keyring_example():
51+
"""Test raw_ecdh_keyring_example with discovery configuration."""
52+
# You may provide your own ECC Public Key in the files located at
53+
# - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
54+
# - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT
55+
# If you provide this, the keys MUST be on curve P256
56+
# If these files are not present, this will generate a pair for you.
57+
# For this example we will use the curve P256.
58+
if should_generate_new_ecc_key_pairs():
59+
generate_ecc_key_pairs()
60+
61+
# The discovery configuration is not allowed to encrypt
62+
# To understand this example best, we will write a record with the ephemeral configuration
63+
# in the previous example. This means that the recipient public key configured on
64+
# both keyrings is the same. This means that the other party has the recipient public key
65+
# and is writing messages meant only for the owner of the recipient public key to decrypt.
66+
67+
# In this call we are writing a record that is written with an ephemeral sender key pair.
68+
# The recipient will be able to decrypt the message
69+
ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
70+
71+
# In this call we are reading a record that was written with the recipient's public key.
72+
# It will use the recipient's private key and the sender's public key stored in the message to
73+
# calculate the appropriate shared secret to successfully decrypt the message.
74+
discovery_raw_ecdh_keyring_get_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test for the Raw RSA keyring example."""
4+
import pytest
5+
6+
from ...src.keyring.raw_rsa_keyring_example import (
7+
generate_rsa_key_pair,
8+
raw_rsa_keyring_example,
9+
should_generate_new_rsa_key_pair,
10+
)
11+
from ..test_utils import TEST_DDB_TABLE_NAME
12+
13+
pytestmark = [pytest.mark.examples]
14+
15+
16+
def test_raw_rsa_keyring_example():
17+
"""Test the Raw RSA keyring example."""
18+
# You may provide your own RSA key pair in the files located at
19+
# - EXAMPLE_RSA_PRIVATE_KEY_FILENAME
20+
# - EXAMPLE_RSA_PUBLIC_KEY_FILENAME
21+
# If these files are not present, this will generate a pair for you
22+
if should_generate_new_rsa_key_pair():
23+
generate_rsa_key_pair()
24+
25+
raw_rsa_keyring_example(TEST_DDB_TABLE_NAME)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test for the shared cache across hierarchical keyrings example."""
4+
import time
5+
6+
import pytest
7+
8+
from ...src.create_keystore_key_example import keystore_create_key
9+
from ...src.keyring.shared_cache_across_hierarchical_keyrings_example import (
10+
shared_cache_across_hierarchical_keyrings_example,
11+
)
12+
from ..cleanup import delete_branch_key
13+
from ..test_utils import (
14+
TEST_DDB_TABLE_NAME,
15+
TEST_KEYSTORE_KMS_KEY_ID,
16+
TEST_KEYSTORE_NAME,
17+
TEST_LOGICAL_KEYSTORE_NAME,
18+
TEST_PARTITION_ID,
19+
)
20+
21+
pytestmark = [pytest.mark.examples]
22+
23+
24+
def test_shared_cache_across_hierarchical_keyrings_example():
25+
"""Test the shared cache across hierarchical keyrings example."""
26+
# Create new branch key for test
27+
key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
28+
29+
# Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
30+
# our test fails due to eventual consistency issues.
31+
time.sleep(5)
32+
33+
shared_cache_across_hierarchical_keyrings_example(
34+
TEST_DDB_TABLE_NAME,
35+
key_id,
36+
TEST_KEYSTORE_NAME,
37+
TEST_LOGICAL_KEYSTORE_NAME,
38+
TEST_PARTITION_ID,
39+
TEST_KEYSTORE_KMS_KEY_ID,
40+
)
41+
42+
# Cleanup Branch Key
43+
delete_branch_key(key_id, TEST_KEYSTORE_NAME, None)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test create key store key example."""
4+
import pytest
5+
6+
from ..src.create_keystore_key_example import keystore_create_key
7+
from .cleanup import delete_branch_key
8+
from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME
9+
10+
pytestmark = [pytest.mark.examples]
11+
12+
13+
def test_create_keystore_key_example():
14+
"""Test create_key_store_key_example."""
15+
key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
16+
17+
assert key_id is not None
18+
19+
# Cleanup Branch Key
20+
delete_branch_key(key_id, TEST_KEYSTORE_NAME, None)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test create key store table example."""
4+
import pytest
5+
6+
from ..src.create_keystore_table_example import keystore_create_table
7+
from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME
8+
9+
pytestmark = [pytest.mark.examples]
10+
11+
12+
def test_create_keystore_table_example():
13+
"""Test create_key_store_table_example."""
14+
keystore_create_table(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test scan error example."""
4+
import pytest
5+
6+
from ..src.scan_error_example import scan_error
7+
from .test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID
8+
9+
pytestmark = [pytest.mark.examples]
10+
11+
12+
def test_scan_error():
13+
"""Test scan_error."""
14+
scan_error(TEST_KMS_KEY_ID, TEST_DDB_TABLE_NAME)

0 commit comments

Comments
 (0)
Please sign in to comment.