Skip to content

Commit 3426b2c

Browse files
author
Lucas McDonald
committed
Python ItemEncryptor
1 parent eb7679a commit 3426b2c

File tree

8 files changed

+694
-0
lines changed

8 files changed

+694
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Top-level class for encrypting and decrypting individual DynamoDB items."""
4+
from typing import Any
5+
6+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.client import (
7+
DynamoDbItemEncryptor,
8+
)
9+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.config import (
10+
DynamoDbItemEncryptorConfig,
11+
)
12+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.models import (
13+
DecryptItemInput,
14+
DecryptItemOutput,
15+
EncryptItemInput,
16+
EncryptItemOutput,
17+
)
18+
from aws_dbesdk_dynamodb.transform import (
19+
ddb_to_dict,
20+
dict_to_ddb,
21+
)
22+
23+
24+
class ItemEncryptor:
25+
"""Class providing item-level encryption for DynamoDB items / Python dictionaries."""
26+
27+
_internal_client: DynamoDbItemEncryptor
28+
29+
def __init__(
30+
self,
31+
item_encryptor_config: DynamoDbItemEncryptorConfig,
32+
):
33+
"""
34+
Create an ItemEncryptor.
35+
36+
Args:
37+
item_encryptor_config (DynamoDbItemEncryptorConfig): Encryption configuration object.
38+
39+
"""
40+
self._internal_client = DynamoDbItemEncryptor(config=item_encryptor_config)
41+
42+
def encrypt_python_item(
43+
self,
44+
plaintext_dict_item: dict[str, Any],
45+
) -> EncryptItemOutput:
46+
"""
47+
Encrypt a Python dictionary.
48+
49+
This method will transform the Python dictionary into DynamoDB JSON,
50+
encrypt the DynamoDB JSON,
51+
transform the encrypted DynamoDB JSON into an encrypted Python dictionary,
52+
then return the encrypted Python dictionary.
53+
54+
See the boto3 documentation for details on Python/DynamoDB type transfomations:
55+
https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/dynamodb/types.html
56+
57+
boto3 DynamoDB Tables and Resources expect items formatted as native Python dictionaries.
58+
Use this method to encrypt an item if you intend to pass the encrypted item
59+
to a boto3 DynamoDB Table or Resource interface to store it.
60+
(Alternatively, you can use this library's EncryptedTable or EncryptedResource interfaces
61+
to transparently encrypt items without an intermediary ItemEncryptor.)
62+
63+
Args:
64+
plaintext_dict_item (dict[str, Any]): A standard Python dictionary.
65+
66+
Returns:
67+
EncryptItemOutput: Structure containing the following fields:
68+
- `encrypted_item` (dict[str, Any]): The encrypted Python dictionary.
69+
**Note:** The item was encrypted as DynamoDB JSON, then transformed to a Python dictionary.
70+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header (parsed
71+
`aws_dbe_head` value).
72+
73+
Example:
74+
>>> plaintext_item = {
75+
... 'some': 'data',
76+
... 'more': 5
77+
... }
78+
>>> encrypt_output = item_encryptor.encrypt_python_item(plaintext_item)
79+
>>> encrypted_item = encrypt_output.encrypted_item
80+
>>> header = encrypt_output.parsed_header
81+
82+
"""
83+
plaintext_ddb_item = dict_to_ddb(plaintext_dict_item)
84+
encrypted_ddb_item: EncryptItemOutput = self.encrypt_dynamodb_item(plaintext_ddb_item)
85+
encrypted_dict_item = ddb_to_dict(encrypted_ddb_item.encrypted_item)
86+
return EncryptItemOutput(encrypted_item=encrypted_dict_item, parsed_header=encrypted_ddb_item.parsed_header)
87+
88+
def encrypt_dynamodb_item(
89+
self,
90+
plaintext_dynamodb_item: dict[str, dict[str, Any]],
91+
) -> EncryptItemOutput:
92+
"""
93+
Encrypt DynamoDB-formatted JSON.
94+
95+
boto3 DynamoDB clients expect items formatted as DynamoDB JSON:
96+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
97+
Use this method to encrypt an item if you intend to pass the encrypted item
98+
to a boto3 DynamoDB client to store it.
99+
(Alternatively, you can use this library's EncryptedClient interface
100+
to transparently encrypt items without an intermediary ItemEncryptor.)
101+
102+
Args:
103+
plaintext_dynamodb_item (dict[str, dict[str, Any]]): The item to encrypt formatted as DynamoDB JSON.
104+
105+
Returns:
106+
EncryptItemOutput: Structure containing the following fields:
107+
- `encrypted_item` (dict[str, Any]): A dictionary containing the encrypted DynamoDB item
108+
formatted as DynamoDB JSON.
109+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header (`aws_dbe_head` value).
110+
111+
Example:
112+
>>> plaintext_item = {
113+
... 'some': {'S': 'data'},
114+
... 'more': {'N': '5'}
115+
... }
116+
>>> encrypt_output = item_encryptor.encrypt_dynamodb_item(plaintext_item)
117+
>>> encrypted_item = encrypt_output.encrypted_item
118+
>>> header = encrypt_output.parsed_header
119+
120+
"""
121+
return self.encrypt_item(EncryptItemInput(plaintext_item=plaintext_dynamodb_item))
122+
123+
def encrypt_item(
124+
self,
125+
encrypt_item_input: EncryptItemInput,
126+
) -> EncryptItemOutput:
127+
"""
128+
Encrypt a DynamoDB item.
129+
130+
The input item should contain a dictionary formatted as DynamoDB JSON:
131+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
132+
133+
Args:
134+
encrypt_item_input (EncryptItemInput): Structure containing the following field:
135+
- `plaintext_item` (dict[str, Any]): The item to encrypt formatted as DynamoDB JSON.
136+
137+
Returns:
138+
EncryptItemOutput: Structure containing the following fields:
139+
- `encrypted_item` (dict[str, Any]): The encrypted DynamoDB item formatted as DynamoDB JSON.
140+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header
141+
(`aws_dbe_head` value).
142+
143+
Example:
144+
>>> plaintext_item = {
145+
... 'some': {'S': 'data'},
146+
... 'more': {'N': '5'}
147+
... }
148+
>>> encrypt_output = item_encryptor.encrypt_item(
149+
... EncryptItemInput(
150+
... plaintext_ddb_item = plaintext_item
151+
... )
152+
... )
153+
>>> encrypted_item = encrypt_output.encrypted_item
154+
>>> header = encrypt_output.parsed_header
155+
156+
"""
157+
return self._internal_client.encrypt_item(encrypt_item_input)
158+
159+
def decrypt_python_item(
160+
self,
161+
encrypted_dict_item: dict[str, Any],
162+
) -> DecryptItemOutput:
163+
"""
164+
Decrypt a Python dictionary.
165+
166+
This method will transform the Python dictionary into DynamoDB JSON,
167+
decrypt the DynamoDB JSON,
168+
transform the plaintext DynamoDB JSON into a plaintext Python dictionary,
169+
then return the plaintext Python dictionary.
170+
171+
See the boto3 documentation for details on Python/DynamoDB type transfomations:
172+
https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/dynamodb/types.html
173+
174+
boto3 DynamoDB Tables and Resources return items formatted as native Python dictionaries.
175+
Use this method to decrypt an item if you retrieve the encrypted item
176+
from a boto3 DynamoDB Table or Resource interface.
177+
(Alternatively, you can use this library's EncryptedTable or EncryptedResource interfaces
178+
to transparently decrypt items without an intermediary ItemEncryptor.)
179+
180+
Args:
181+
encrypted_dict_item (dict[str, Any]): A standard Python dictionary with encrypted values.
182+
183+
Returns:
184+
DecryptItemOutput: Structure containing the following fields:
185+
- `plaintext_item` (dict[str, Any]): The decrypted Python dictionary.
186+
**Note:** The item was decrypted as DynamoDB JSON, then transformed to a Python dictionary.
187+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header
188+
(parsed `aws_dbe_head` value).
189+
190+
Example:
191+
>>> encrypted_item = {
192+
... 'some': b'ENCRYPTED_DATA',
193+
... 'more': b'ENCRYPTED_DATA',
194+
... }
195+
>>> decrypt_output = item_encryptor.decrypt_python_item(encrypted_item)
196+
>>> plaintext_item = decrypt_output.plaintext_item
197+
>>> header = decrypt_output.parsed_header
198+
199+
"""
200+
encrypted_ddb_item = dict_to_ddb(encrypted_dict_item)
201+
plaintext_ddb_item: DecryptItemOutput = self.decrypt_dynamodb_item(encrypted_ddb_item)
202+
plaintext_dict_item = ddb_to_dict(plaintext_ddb_item.plaintext_item)
203+
return DecryptItemOutput(plaintext_item=plaintext_dict_item, parsed_header=plaintext_ddb_item.parsed_header)
204+
205+
def decrypt_dynamodb_item(
206+
self,
207+
encrypted_dynamodb_item: dict[str, dict[str, Any]],
208+
) -> DecryptItemOutput:
209+
"""
210+
Decrypt DynamoDB-formatted JSON.
211+
212+
boto3 DynamoDB clients return items formatted as DynamoDB JSON:
213+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
214+
Use this method to decrypt an item if you retrieved the encrypted item
215+
from a boto3 DynamoDB client.
216+
(Alternatively, you can use this library's EncryptedClient interface
217+
to transparently decrypt items without an intermediary ItemEncryptor.)
218+
219+
Args:
220+
encrypted_dynamodb_item (dict[str, dict[str, Any]]): The item to decrypt formatted as DynamoDB JSON.
221+
222+
Returns:
223+
DecryptItemOutput: Structure containing the following fields:
224+
- `plaintext_item` (dict[str, Any]): The plaintext DynamoDB item formatted as DynamoDB JSON.
225+
- `parsed_header` (Optional[ParsedHeader]): The decrypted DynamoDB item's header (`aws_dbe_head` value).
226+
227+
Example:
228+
>>> encrypted_item = {
229+
... 'some': {'B': b'ENCRYPTED_DATA'},
230+
... 'more': {'B': b'ENCRYPTED_DATA'}
231+
... }
232+
>>> decrypt_output = item_encryptor.decrypt_dynamodb_item(encrypted_item)
233+
>>> plaintext_item = decrypt_output.plaintext_item
234+
>>> header = decrypt_output.parsed_header
235+
236+
"""
237+
return self.decrypt_item(DecryptItemInput(encrypted_item=encrypted_dynamodb_item))
238+
239+
def decrypt_item(
240+
self,
241+
decrypt_item_input: DecryptItemInput,
242+
) -> DecryptItemOutput:
243+
"""
244+
Decrypt a DynamoDB item.
245+
246+
The input item should contain a dictionary formatted as DynamoDB JSON:
247+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
248+
249+
Args:
250+
decrypt_item_input (DecryptItemInput): Structure containing the following field:
251+
- `encrypted_item` (dict[str, Any]): The item to decrypt formatted as DynamoDB JSON.
252+
253+
Returns:
254+
DecryptItemOutput: Structure containing the following fields:
255+
- `plaintext_item` (dict[str, Any]): The decrypted DynamoDB item formatted as DynamoDB JSON.
256+
- `parsed_header` (Optional[ParsedHeader]): The decrypted DynamoDB item's header (`aws_dbe_head` value).
257+
258+
Example:
259+
>>> encrypted_item = {
260+
... 'some': {'B': b'ENCRYPTED_DATA'},
261+
... 'more': {'B': b'ENCRYPTED_DATA'}
262+
... }
263+
>>> decrypted_item = item_encryptor.decrypt_item(
264+
... DecryptItemInput(
265+
... encrypted_item = encrypted_item,
266+
... )
267+
... )
268+
>>> plaintext_item = decrypted_item.plaintext_item
269+
>>> header = decrypted_item.parsed_header
270+
271+
"""
272+
return self._internal_client.decrypt_item(decrypt_item_input)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Integration tests for the ItemEncryptor."""
4+
import pytest
5+
6+
from aws_dbesdk_dynamodb.encrypted.item import ItemEncryptor
7+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.models import (
8+
DecryptItemInput,
9+
EncryptItemInput,
10+
)
11+
12+
from ...constants import INTEG_TEST_DEFAULT_ITEM_ENCRYPTOR_CONFIG
13+
from ...items import complex_item_ddb, complex_item_dict, simple_item_ddb, simple_item_dict
14+
15+
16+
# Creates a matrix
17+
@pytest.fixture(params=[True, False], ids=["complex_item", "simple_item"])
18+
def use_complex_item(request):
19+
return request.param
20+
21+
22+
@pytest.fixture
23+
def test_dict_item(use_complex_item):
24+
if use_complex_item:
25+
return complex_item_dict
26+
return simple_item_dict
27+
28+
29+
@pytest.fixture
30+
def test_ddb_item(use_complex_item):
31+
if use_complex_item:
32+
return complex_item_ddb
33+
return simple_item_ddb
34+
35+
36+
item_encryptor = ItemEncryptor(INTEG_TEST_DEFAULT_ITEM_ENCRYPTOR_CONFIG)
37+
38+
39+
def test_GIVEN_valid_dict_item_WHEN_encrypt_python_item_AND_decrypt_python_item_THEN_round_trip_passes(test_dict_item):
40+
# Given: Valid dict item
41+
# When: encrypt_python_item
42+
encrypted_dict_item = item_encryptor.encrypt_python_item(test_dict_item).encrypted_item
43+
# Then: Encrypted dict item is returned
44+
assert encrypted_dict_item != test_dict_item
45+
# When: decrypt_python_item
46+
decrypted_dict_item = item_encryptor.decrypt_python_item(encrypted_dict_item).plaintext_item
47+
# Then: Decrypted dict item is returned and matches the original item
48+
assert decrypted_dict_item == test_dict_item
49+
50+
51+
def test_GIVEN_valid_ddb_item_WHEN_encrypt_dynamodb_item_AND_decrypt_dynamodb_item_THEN_round_trip_passes(
52+
test_ddb_item,
53+
):
54+
# Given: Valid ddb item
55+
# When: encrypt_dynamodb_item
56+
encrypted_ddb_item = item_encryptor.encrypt_dynamodb_item(test_ddb_item).encrypted_item
57+
# Then: Encrypted ddb item is returned
58+
assert encrypted_ddb_item != test_ddb_item
59+
# When: decrypt_dynamodb_item
60+
decrypted_ddb_item = item_encryptor.decrypt_dynamodb_item(encrypted_ddb_item).plaintext_item
61+
# Then: Decrypted ddb item is returned and matches the original item
62+
assert decrypted_ddb_item == test_ddb_item
63+
64+
65+
def test_GIVEN_valid_encrypt_item_input_WHEN_encrypt_item_AND_decrypt_item_THEN_round_trip_passes(test_ddb_item):
66+
# Given: Valid encrypt_item_input
67+
encrypt_item_input = EncryptItemInput(plaintext_item=test_ddb_item)
68+
# When: encrypt_item
69+
encrypted_item = item_encryptor.encrypt_item(encrypt_item_input).encrypted_item
70+
# Then: Encrypted item is returned
71+
assert encrypted_item != test_ddb_item
72+
# When: decrypt_item
73+
decrypt_item_input = DecryptItemInput(encrypted_item=encrypted_item)
74+
decrypted_item = item_encryptor.decrypt_item(decrypt_item_input).plaintext_item
75+
# Then: Decrypted item is returned and matches the original item
76+
assert decrypted_item == test_ddb_item
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."""

0 commit comments

Comments
 (0)