Skip to content

Commit 7e20331

Browse files
feat: Python ItemEncryptor impl and tests (#1889)
1 parent 0e4fd02 commit 7e20331

File tree

8 files changed

+723
-0
lines changed

8 files changed

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