Skip to content

Commit 1b0bdff

Browse files
feat: Python EncryptedClient impl and tests (#1894)
1 parent c93bf08 commit 1b0bdff

File tree

12 files changed

+1527
-0
lines changed

12 files changed

+1527
-0
lines changed

DynamoDbEncryption/runtimes/python/src/aws_dbesdk_dynamodb/encrypted/client.py

Lines changed: 647 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Integration tests for encrypted interfaces.
2+
3+
These integration tests verify that encrypted boto3 interfaces behave as drop-in replacements for plaintext boto3 interfaces.
4+
5+
Each test runs with both a plaintext client and an encrypted client, using the same request parameters and expecting the same response.
6+
7+
This validates that encrypted clients expect the same input shapes as plaintext clients
8+
and encrypted clients return the same output shapes as plaintext clients.
9+
10+
This guarantees that users can substitute encrypted interfaces without modifying their application logic.

DynamoDbEncryption/runtimes/python/test/integ/encrypted/test_client.py

Lines changed: 592 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""High-level helper class to provide an encrypting wrapper for boto3 DynamoDB clients."""
4+
import pytest
5+
from botocore.client import BaseClient
6+
from mock import MagicMock
7+
8+
from aws_dbesdk_dynamodb.encrypted.client import (
9+
EncryptedClient,
10+
)
11+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.models import (
12+
DynamoDbTablesEncryptionConfig,
13+
)
14+
15+
mock_boto3_dynamodb_client = MagicMock(__class__=BaseClient)
16+
mock_tables_encryption_config = MagicMock(__class__=DynamoDbTablesEncryptionConfig)
17+
18+
19+
def test_GIVEN_valid_inputs_WHEN_create_EncryptedClient_THEN_success():
20+
# Given: Valid EncryptedClient inputs
21+
# When: Create EncryptedClient
22+
EncryptedClient(
23+
client=mock_boto3_dynamodb_client,
24+
encryption_config=mock_tables_encryption_config,
25+
)
26+
# Then: Success
27+
28+
29+
def test_GIVEN_invalid_class_attribute_WHEN_getattr_THEN_raise_error():
30+
# Create a mock with a specific spec that excludes our unknown attribute
31+
mock_boto3_dynamodb_client = MagicMock(spec=["put_item", "get_item", "query", "scan"])
32+
encrypted_client = EncryptedClient(
33+
client=mock_boto3_dynamodb_client,
34+
encryption_config=mock_tables_encryption_config,
35+
)
36+
37+
# Then: AttributeError is raised
38+
with pytest.raises(AttributeError):
39+
# Given: Invalid class attribute: not_a_valid_attribute_on_EncryptedClient_nor_boto3_client
40+
# When: getattr is called
41+
encrypted_client.not_a_valid_attribute_on_EncryptedClient_nor_boto3_client()
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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example for using an EncryptedClient to put and get an encrypted item.
5+
6+
Running this example requires access to the DDB Table whose name
7+
is provided in the function arguments.
8+
This table must be configured with the following
9+
primary key configuration:
10+
- Partition key is named "partition_key" with type (S)
11+
- Sort key is named "sort_key" with type (N)
12+
"""
13+
14+
import boto3
15+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
16+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
17+
from aws_cryptographic_material_providers.mpl.models import (
18+
CreateAwsKmsMrkMultiKeyringInput,
19+
DBEAlgorithmSuiteId,
20+
)
21+
from aws_cryptographic_material_providers.mpl.references import IKeyring
22+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
23+
from aws_dbesdk_dynamodb.structures.dynamodb import (
24+
DynamoDbTableEncryptionConfig,
25+
DynamoDbTablesEncryptionConfig,
26+
)
27+
from aws_dbesdk_dynamodb.structures.structured_encryption import (
28+
CryptoAction,
29+
)
30+
31+
32+
def encrypted_client_put_get_example(
33+
kms_key_id: str,
34+
dynamodb_table_name: str,
35+
):
36+
"""Use an EncryptedClient to put and get an encrypted item."""
37+
# 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
38+
# For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
39+
# We will use the `CreateMrkMultiKeyring` method to create this keyring,
40+
# as it will correctly handle both single region and Multi-Region KMS Keys.
41+
mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
42+
kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput = CreateAwsKmsMrkMultiKeyringInput(
43+
generator=kms_key_id,
44+
)
45+
kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(input=kms_mrk_multi_keyring_input)
46+
47+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
48+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
49+
# we must explicitly configure how they should be treated during item encryption:
50+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
51+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
52+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
53+
attribute_actions_on_encrypt = {
54+
"partition_key": CryptoAction.SIGN_ONLY,
55+
"sort_key": CryptoAction.SIGN_ONLY,
56+
"attribute1": CryptoAction.ENCRYPT_AND_SIGN,
57+
"attribute2": CryptoAction.SIGN_ONLY,
58+
":attribute3": CryptoAction.DO_NOTHING,
59+
}
60+
61+
# 3. Configure which attributes we expect to be included in the signature
62+
# when reading items. There are two options for configuring this:
63+
#
64+
# - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
65+
# When defining your DynamoDb schema and deciding on attribute names,
66+
# choose a distinguishing prefix (such as ":") for all attributes that
67+
# you do not want to include in the signature.
68+
# This has two main benefits:
69+
# - It is easier to reason about the security and authenticity of data within your item
70+
# when all unauthenticated data is easily distinguishable by their attribute name.
71+
# - If you need to add new unauthenticated attributes in the future,
72+
# you can easily make the corresponding update to your `attributeActionsOnEncrypt`
73+
# and immediately start writing to that new attribute, without
74+
# any other configuration update needed.
75+
# Once you configure this field, it is not safe to update it.
76+
#
77+
# - Configure `allowedUnsignedAttributes`: You may also explicitly list
78+
# a set of attributes that should be considered unauthenticated when encountered
79+
# on read. Be careful if you use this configuration. Do not remove an attribute
80+
# name from this configuration, even if you are no longer writing with that attribute,
81+
# as old items may still include this attribute, and our configuration needs to know
82+
# to continue to exclude this attribute from the signature scope.
83+
# If you add new attribute names to this field, you must first deploy the update to this
84+
# field to all readers in your host fleet before deploying the update to start writing
85+
# with that new attribute.
86+
#
87+
# For this example, we have designed our DynamoDb table such that any attribute name with
88+
# the ":" prefix should be considered unauthenticated.
89+
unsignAttrPrefix: str = ":"
90+
91+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
92+
table_configs = {}
93+
table_config = DynamoDbTableEncryptionConfig(
94+
logical_table_name=dynamodb_table_name,
95+
partition_key_name="partition_key",
96+
sort_key_name="sort_key",
97+
attribute_actions_on_encrypt=attribute_actions_on_encrypt,
98+
keyring=kms_mrk_multi_keyring,
99+
allowed_unsigned_attribute_prefix=unsignAttrPrefix,
100+
# Specifying an algorithm suite is not required,
101+
# but is done here to demonstrate how to do so.
102+
# We suggest using the
103+
# `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite,
104+
# which includes AES-GCM with key derivation, signing, and key commitment.
105+
# This is also the default algorithm suite if one is not specified in this config.
106+
# For more information on supported algorithm suites, see:
107+
# https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html
108+
algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384,
109+
)
110+
table_configs[dynamodb_table_name] = table_config
111+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
112+
113+
# 5. Create the EncryptedClient
114+
encrypted_client = EncryptedClient(
115+
client=boto3.client("dynamodb"),
116+
encryption_config=tables_config,
117+
)
118+
119+
# 6. Put an item into our table using the above client.
120+
# Before the item gets sent to DynamoDb, it will be encrypted
121+
# client-side, according to our configuration.
122+
item_to_encrypt = {
123+
"partition_key": {"S": "BasicPutGetExample"},
124+
"sort_key": {"N": "0"},
125+
"attribute1": {"S": "encrypt and sign me!"},
126+
"attribute2": {"S": "sign me!"},
127+
":attribute3": {"S": "ignore me!"},
128+
}
129+
130+
put_item_request = {
131+
"TableName": dynamodb_table_name,
132+
"Item": item_to_encrypt,
133+
}
134+
135+
put_item_response = encrypted_client.put_item(**put_item_request)
136+
137+
# Demonstrate that PutItem succeeded
138+
assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200
139+
140+
# 7. Get the item back from our table using the same client.
141+
# The client will decrypt the item client-side, and return
142+
# back the original item.
143+
key_to_get = {"partition_key": {"S": "BasicPutGetExample"}, "sort_key": {"N": "0"}}
144+
145+
get_item_request = {"TableName": dynamodb_table_name, "Key": key_to_get}
146+
147+
get_item_response = encrypted_client.get_item(**get_item_request)
148+
149+
# Demonstrate that GetItem succeeded and returned the decrypted item
150+
assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200
151+
assert get_item_response["Item"] == item_to_encrypt
152+
153+
# 8. Clean up the item we put into the table by deleting it.
154+
delete_item_request = {"TableName": dynamodb_table_name, "Key": key_to_get}
155+
delete_item_response = encrypted_client.delete_item(**delete_item_request)
156+
157+
# Demonstrate that DeleteItem succeeded
158+
assert delete_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200
159+
get_item_response = encrypted_client.get_item(**get_item_request)
160+
assert "Item" not in get_item_response
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Test suite for the EncryptedClient example."""
4+
import pytest
5+
6+
from ...src.basic_put_get_example.with_encrypted_client import encrypted_client_put_get_example
7+
8+
pytestmark = [pytest.mark.examples]
9+
10+
11+
def test_encrypted_client_put_get_example():
12+
"""Test function for encrypt and decrypt using the EncryptedClient example."""
13+
test_kms_key_id = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"
14+
test_dynamodb_table_name = "DynamoDbEncryptionInterceptorTestTable"
15+
encrypted_client_put_get_example(test_kms_key_id, test_dynamodb_table_name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import boto3
2+
import aws_dbesdk_dynamodb_test_vectors.internaldafny.generated.CreateInterceptedDDBClient
3+
import aws_cryptography_internal_dynamodb.internaldafny.extern
4+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.dafny_to_smithy import aws_cryptography_dbencryptionsdk_dynamodb_DynamoDbTablesEncryptionConfig
5+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
6+
from smithy_dafny_standard_library.internaldafny.generated import Wrappers
7+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.errors import _smithy_error_to_dafny_error
8+
from aws_dbesdk_dynamodb_test_vectors.waiting_boto3_ddb_client import WaitingLocalDynamoClient
9+
10+
class default__:
11+
@staticmethod
12+
def CreateVanillaDDBClient():
13+
try:
14+
return aws_cryptography_internal_dynamodb.internaldafny.extern.Com_Amazonaws_Dynamodb.default__.DynamoDBClient(WaitingLocalDynamoClient())
15+
except Exception as e:
16+
return Wrappers.Result_Failure(_smithy_error_to_dafny_error(e))
17+
18+
@staticmethod
19+
def CreateInterceptedDDBClient(dafny_encryption_config):
20+
try:
21+
native_encryption_config = aws_cryptography_dbencryptionsdk_dynamodb_DynamoDbTablesEncryptionConfig(dafny_encryption_config)
22+
boto3_client = WaitingLocalDynamoClient()
23+
encrypted_client = EncryptedClient(client = boto3_client, encryption_config = native_encryption_config)
24+
return aws_cryptography_internal_dynamodb.internaldafny.extern.Com_Amazonaws_Dynamodb.default__.DynamoDBClient(encrypted_client)
25+
except Exception as e:
26+
return Wrappers.Result_Failure(_smithy_error_to_dafny_error(e))
27+
28+
aws_dbesdk_dynamodb_test_vectors.internaldafny.generated.CreateInterceptedDDBClient.default__ = default__
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0

TestVectors/runtimes/python/test/client/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Wrapper file for executing Dafny tests from pytest.
5+
This allows us to import modules required by Dafny-generated tests
6+
before executing Dafny-generated tests.
7+
pytest will find and execute the `test_dafny` method below,
8+
which will execute the `internaldafny_test_executor.py` file in the `dafny` directory.
9+
"""
10+
11+
import sys
12+
13+
# Different from standard test_dafny_wrapper due to weird test structure.
14+
test_dir = '/'.join(__file__.split("/")[:-2])
15+
16+
sys.path.append(test_dir + "/internaldafny/extern")
17+
sys.path.append(test_dir + "/internaldafny/generated")
18+
19+
# Import extern to use an EncryptedClient as the wrapped DBESDK client.
20+
import aws_dbesdk_dynamodb_test_vectors.internaldafny.extern.CreateInterceptedDDBClient
21+
# Import extern to use the ItemEncryptor with DDB JSON-formatted items.
22+
# (EncryptedClients use DDB JSON-formatted items by default.)
23+
import aws_dbesdk_dynamodb_test_vectors.internaldafny.extern.CreateWrappedDynamoDbItemEncryptor
24+
25+
def test_dafny():
26+
from ..internaldafny.generated import __main__

0 commit comments

Comments
 (0)