Skip to content

Commit 2136eb4

Browse files
wip
1 parent 9bbb645 commit 2136eb4

File tree

10 files changed

+385
-151
lines changed

10 files changed

+385
-151
lines changed

DynamoDbEncryption/runtimes/python/src/aws_database_encryption_sdk/encryptor/client.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -54,56 +54,70 @@ def __init__(
5454
config = encryption_config
5555
)
5656

57-
def put_item(self, **kwargs):
58-
# TODO: refactor shared logic (DDB/Python conversions, client/table)
57+
def _maybe_transform_request_to_dynamodb_item(self, item_key, **kwargs):
5958
if self._expect_standard_dictionaries:
60-
dynamodb_item = dict_to_ddb(kwargs["Item"])
59+
dynamodb_item = dict_to_ddb(kwargs[item_key])
6160
dynamodb_input = kwargs
62-
dynamodb_input["Item"] = dynamodb_item
61+
dynamodb_input[item_key] = dynamodb_item
6362
else:
6463
dynamodb_input = kwargs
64+
return dynamodb_input
65+
66+
def _maybe_transform_response_item_to_python_dict(self, response):
67+
if self._expect_standard_dictionaries:
68+
if hasattr(response, "Item"):
69+
response["Item"] = ddb_to_dict(response["Item"])
70+
71+
def _copy_sdk_response_to_dbesdk_response(self, sdk_response, dbesdk_response):
72+
for sdk_response_key, sdk_response_value in sdk_response.items():
73+
if sdk_response_key not in dbesdk_response:
74+
dbesdk_response[sdk_response_key] = sdk_response_value
75+
76+
def put_item(self, **kwargs):
77+
# TODO: refactor shared logic (DDB/Python conversions, client/table)
78+
dynamodb_input = self._maybe_transform_request_to_dynamodb_item(item_key = "Item", **kwargs)
79+
# if self._expect_standard_dictionaries:
80+
# dynamodb_item = dict_to_ddb(kwargs["Item"])
81+
# dynamodb_input = kwargs
82+
# dynamodb_input["Item"] = dynamodb_item
83+
# else:
84+
# dynamodb_input = kwargs
6585
transformed_request = self._transformer.put_item_input_transform(
6686
PutItemInputTransformInput(
6787
sdk_input = dynamodb_input
6888
)
6989
).transformed_input
70-
sdk_output = self._client.put_item(**transformed_request)
71-
transformed_response = self._transformer.put_item_output_transform(
90+
sdk_response = self._client.put_item(**transformed_request)
91+
dbesdk_response = self._transformer.put_item_output_transform(
7292
PutItemOutputTransformInput(
7393
original_input = dynamodb_input,
74-
sdk_output = sdk_output,
94+
sdk_output = sdk_response,
7595
)
7696
).transformed_output
77-
response = transformed_response
78-
for sdk_output_key, sdk_output_value in sdk_output.items():
79-
if sdk_output_key not in transformed_response:
80-
response[sdk_output_key] = sdk_output_value
81-
# TODO: standard dicts transform output
97+
self._copy_sdk_response_to_dbesdk_response(sdk_response, dbesdk_response)
98+
self._maybe_transform_response_to_python_dict(dbesdk_response)
8299
return response
83100

84101
def get_item(self, **kwargs):
85-
if self._expect_standard_dictionaries:
86-
dynamodb_item = dict_to_ddb(kwargs["Key"])
87-
dynamodb_input = kwargs
88-
dynamodb_input["Key"] = dynamodb_item
89-
else:
90-
dynamodb_input = kwargs
102+
dynamodb_input = self._maybe_transform_request_to_dynamodb_item(item_key = "Key", **kwargs)
91103
transformed_request = self._transformer.get_item_input_transform(
92104
GetItemInputTransformInput(
93105
sdk_input = dynamodb_input
94106
)
95107
).transformed_input
96-
sdk_output = self._client.get_item(**transformed_request)
97-
transformed_response = self._transformer.get_item_output_transform(
108+
sdk_response = self._client.get_item(**transformed_request)
109+
dbesdk_response = self._transformer.get_item_output_transform(
98110
GetItemOutputTransformInput(
99111
original_input = dynamodb_input,
100-
sdk_output = sdk_output,
112+
sdk_output = sdk_response,
101113
)
102114
).transformed_output
103-
response = transformed_response
104-
for sdk_output_key, sdk_output_value in sdk_output.items():
105-
if sdk_output_key not in transformed_response:
106-
response[sdk_output_key] = sdk_output_value
107-
if self._expect_standard_dictionaries:
108-
response["Item"] = ddb_to_dict(response["Item"])
115+
self._copy_sdk_response_to_dbesdk_response(sdk_response, dbesdk_response)
116+
self._maybe_transform_response_to_python_dict(dbesdk_response)
109117
return response
118+
119+
def __getattr__(self, name):
120+
if hasattr(self._client, name):
121+
print(f'calling underlyign client {name=}')
122+
return getattr(self._client, name)
123+
raise KeyError("idk")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Python build artifacts
2+
__pycache__
3+
**/__pycache__
4+
*.pyc
5+
src/**.egg-info/
6+
build
7+
poetry.lock
8+
**/poetry.lock
9+
dist
10+
11+
# Dafny-generated Python
12+
**/internaldafny/generated/*.py
13+
14+
# Python test artifacts
15+
.tox
16+
.pytest_cache
17+
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: 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: 140 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
This example sets up an EncryptedClient wrapper for the AWS SDK client
5+
and uses the PutItem and GetItem DDB APIs to demonstrate
6+
putting a client-side encrypted item into DynamoDb
7+
and then retrieving and decrypting that item from DynamoDb.
8+
9+
Running this example requires access to the DDB Table whose name
10+
is provided in the function arguments.
11+
This table must be configured with the following
12+
primary key configuration:
13+
- Partition key is named "partition_key" with type (S)
14+
- Sort key is named "sort_key" with type (N)
15+
16+
This example also requires access to the KMS key ARN with permissions: (TODO)
17+
"""
118
import boto3
219
from boto3.dynamodb.types import Binary
320
from decimal import Decimal
@@ -13,111 +30,141 @@
1330
from aws_database_encryption_sdk.smithygenerated.aws_cryptography_dbencryptionsdk_structuredencryption.models import (
1431
CryptoAction,
1532
)
16-
17-
mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(
18-
config=MaterialProvidersConfig()
19-
)
20-
21-
kms_key_id = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"
22-
mrk_key_id = "arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7"
23-
24-
kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput =\
25-
CreateAwsKmsMrkMultiKeyringInput(
26-
generator=mrk_key_id,
27-
kms_key_ids=[kms_key_id]
28-
)
29-
30-
kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(
31-
input=kms_mrk_multi_keyring_input
32-
)
33-
34-
attribute_actions_on_encrypt = {
35-
"partition_key": CryptoAction.SIGN_ONLY,
36-
"sort_key": CryptoAction.SIGN_ONLY,
37-
"attribute1": CryptoAction.ENCRYPT_AND_SIGN,
38-
"attribute2": CryptoAction.SIGN_ONLY,
39-
":attribute3": CryptoAction.DO_NOTHING,
40-
}
41-
42-
# 2. Create encryption context.
43-
# Remember that your encryption context is NOT SECRET.
44-
# For more information, see
45-
# https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context
46-
encryption_context = {
47-
"encryption": "context",
48-
"is not": "secret",
49-
"but adds": "useful metadata",
50-
"that can help you": "be confident that",
51-
"the data you are handling": "is what you think it is",
52-
}
53-
54-
unsignAttrPrefix: str = ":"
55-
5633
from aws_database_encryption_sdk.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.models import (
5734
DynamoDbTableEncryptionConfig,
5835
DynamoDbTablesEncryptionConfig,
5936
)
60-
61-
ddb_table_name = "DynamoDbEncryptionInterceptorTestTable"
62-
63-
table_configs = {}
64-
65-
table_config = DynamoDbTableEncryptionConfig(
66-
logical_table_name = ddb_table_name,
67-
partition_key_name = "partition_key",
68-
sort_key_name = "sort_key",
69-
attribute_actions_on_encrypt = attribute_actions_on_encrypt,
70-
keyring = kms_mrk_multi_keyring,
71-
allowed_unsigned_attribute_prefix = unsignAttrPrefix,
72-
algorithm_suite_id = DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384,
73-
)
74-
75-
table_configs[ddb_table_name] = table_config
76-
77-
tables_config = DynamoDbTablesEncryptionConfig(
78-
table_encryption_configs = table_configs
79-
)
80-
8137
from aws_database_encryption_sdk.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
8238
GetItemOutputTransformInput,
8339
PutItemInputTransformInput
8440
)
85-
86-
# "Main"
87-
8841
from aws_database_encryption_sdk.encryptor.client import (
8942
EncryptedClient
9043
)
9144

92-
item_to_encrypt = {
93-
"partition_key": "LucasPythonTesting",
94-
"sort_key": 1234,
95-
"attribute1": "abc"
96-
}
97-
98-
encrypted_client = EncryptedClient(
99-
client = boto3.client("dynamodb"),
100-
encryption_config = tables_config,
101-
expect_standard_dictionaries = True,
102-
)
103-
104-
put_item = {
105-
"TableName": ddb_table_name,
106-
"Item": item_to_encrypt,
107-
}
108-
109-
put_item_output = encrypted_client.put_item(**put_item)
110-
111-
item_to_get = {
112-
"partition_key": "LucasPythonTesting",
113-
"sort_key": 1234,
114-
}
45+
def encrypted_client_put_get_example(
46+
kms_key_id: str,
47+
dynamodb_table_name: str,
48+
):
49+
# 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
50+
# For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
51+
# We will use the `CreateMrkMultiKeyring` method to create this keyring,
52+
# as it will correctly handle both single region and Multi-Region KMS Keys.
53+
mat_prov: AwsCryptographicMaterialProviders = AwsCryptographicMaterialProviders(
54+
config=MaterialProvidersConfig()
55+
)
56+
kms_mrk_multi_keyring_input: CreateAwsKmsMrkMultiKeyringInput =\
57+
CreateAwsKmsMrkMultiKeyringInput(
58+
generator=kms_key_id,
59+
)
60+
kms_mrk_multi_keyring: IKeyring = mat_prov.create_aws_kms_mrk_multi_keyring(
61+
input=kms_mrk_multi_keyring_input
62+
)
11563

116-
get_item = {
117-
"TableName": ddb_table_name,
118-
"Key": item_to_get
119-
}
64+
# 2. Configure which attributes are encrypted and/or signed when writing new items.
65+
# For each attribute that may exist on the items we plan to write to our DynamoDbTable,
66+
# we must explicitly configure how they should be treated during item encryption:
67+
# - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
68+
# - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
69+
# - DO_NOTHING: The attribute is not encrypted and not included in the signature
70+
attribute_actions_on_encrypt = {
71+
"partition_key": CryptoAction.SIGN_ONLY,
72+
"sort_key": CryptoAction.SIGN_ONLY,
73+
"attribute1": CryptoAction.ENCRYPT_AND_SIGN,
74+
"attribute2": CryptoAction.SIGN_ONLY,
75+
":attribute3": CryptoAction.DO_NOTHING,
76+
}
77+
78+
# 3. Configure which attributes we expect to be included in the signature
79+
# when reading items. There are two options for configuring this:
80+
#
81+
# - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
82+
# When defining your DynamoDb schema and deciding on attribute names,
83+
# choose a distinguishing prefix (such as ":") for all attributes that
84+
# you do not want to include in the signature.
85+
# This has two main benefits:
86+
# - It is easier to reason about the security and authenticity of data within your item
87+
# when all unauthenticated data is easily distinguishable by their attribute name.
88+
# - If you need to add new unauthenticated attributes in the future,
89+
# you can easily make the corresponding update to your `attributeActionsOnEncrypt`
90+
# and immediately start writing to that new attribute, without
91+
# any other configuration update needed.
92+
# Once you configure this field, it is not safe to update it.
93+
#
94+
# - Configure `allowedUnsignedAttributes`: You may also explicitly list
95+
# a set of attributes that should be considered unauthenticated when encountered
96+
# on read. Be careful if you use this configuration. Do not remove an attribute
97+
# name from this configuration, even if you are no longer writing with that attribute,
98+
# as old items may still include this attribute, and our configuration needs to know
99+
# to continue to exclude this attribute from the signature scope.
100+
# If you add new attribute names to this field, you must first deploy the update to this
101+
# field to all readers in your host fleet before deploying the update to start writing
102+
# with that new attribute.
103+
#
104+
# For this example, we have designed our DynamoDb table such that any attribute name with
105+
# the ":" prefix should be considered unauthenticated.
106+
unsignAttrPrefix: str = ":"
107+
108+
# 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
109+
table_configs = {}
110+
table_config = DynamoDbTableEncryptionConfig(
111+
logical_table_name = dynamodb_table_name,
112+
partition_key_name = "partition_key",
113+
sort_key_name = "sort_key",
114+
attribute_actions_on_encrypt = attribute_actions_on_encrypt,
115+
keyring = kms_mrk_multi_keyring,
116+
allowed_unsigned_attribute_prefix = unsignAttrPrefix,
117+
algorithm_suite_id = DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384,
118+
)
119+
table_configs[dynamodb_table_name] = table_config
120+
tables_config = DynamoDbTablesEncryptionConfig(
121+
table_encryption_configs = table_configs
122+
)
120123

121-
get_item_output = encrypted_client.get_item(**get_item)
124+
# 5. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs
125+
encrypted_client = EncryptedClient(
126+
client = boto3.client("dynamodb"),
127+
encryption_config = tables_config,
128+
expect_standard_dictionaries = True,
129+
)
122130

123-
assert get_item_output["Item"] == item_to_encrypt
131+
# 6. Put an item into our table using the above client.
132+
# Before the item gets sent to DynamoDb, it will be encrypted
133+
# client-side, according to our configuration.
134+
item_to_encrypt = {
135+
"partition_key": "BasicPutGetExample",
136+
"sort_key": 0,
137+
"attribute1": "encrypt and sign me!",
138+
"attribute2": "sign me!",
139+
":attribute3": "ignore me!",
140+
}
141+
142+
put_item_request = {
143+
"TableName": dynamodb_table_name,
144+
"Item": item_to_encrypt,
145+
}
146+
147+
put_item_response = encrypted_client.put_item(**put_item_request)
148+
149+
# Demonstrate that PutItem succeeded
150+
assert put_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200
151+
152+
# 7. Get the item back from our table using the same client.
153+
# The client will decrypt the item client-side, and return
154+
# back the original item.
155+
key_to_get = {
156+
"partition_key": "BasicPutGetExample",
157+
"sort_key": 0,
158+
}
159+
160+
get_item_request = {
161+
"TableName": dynamodb_table_name,
162+
"Key": key_to_get
163+
}
164+
165+
get_item_response = encrypted_client.get_item(**get_item_request)
166+
167+
# Demonstrate that GetItem succeeded
168+
assert get_item_response["ResponseMetadata"]["HTTPStatusCode"] == 200
169+
assert get_item_response["Item"] == item_to_encrypt
170+
assert get_item_response["Item"]["attribute1"] == "encrypt and sign me!"

0 commit comments

Comments
 (0)