Skip to content

Commit 12ded89

Browse files
author
Lucas McDonald
committed
m
1 parent 9ab80c5 commit 12ded89

File tree

77 files changed

+28112
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+28112
-0
lines changed
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+
4+
# Initialize generated Dafny
5+
from .internaldafny.generated import module_
6+
7+
# Initialize externs
8+
from .internaldafny import extern
9+
10+
"""
11+
boto3 uses Python's decimal library to deserialize numbers retrieved by resources
12+
(Tables, etc.) from strings to `decimal.Decimal`s.
13+
boto3 deserializes strings to Decimals according to its DYNAMODB_CONTEXT:
14+
https://github.com/boto/boto3/blob/develop/boto3/dynamodb/types.py#L37-L42
15+
16+
boto3 is configured to raise an exception if the deserialization is "Rounded": (`traps: [.. Rounded]`)
17+
https://docs.python.org/3/library/decimal.html#decimal.Rounded
18+
"Rounded" means some digits were discarded.
19+
However, those digits may have been 0, and no information is lost.
20+
21+
boto3 is also configured to raise an exception if the deserialization is "Inexact":
22+
https://docs.python.org/3/library/decimal.html#decimal.Inexact
23+
"Inexact" means non-zero digits are discarded, and the result is inexact.
24+
25+
Other DBESDK DynamoDB runtimes treat "Rounded" as acceptable, but "Inexact" as unacceptable.
26+
By default, boto3 will treat both "Rounded" and "Inexact" as unacceptable.
27+
28+
For DBESDK DynamoDB, change the DynamoDB context to treat "Rounded" as acceptable.
29+
"""
30+
import boto3.dynamodb.types
31+
from decimal import Rounded
32+
33+
old_context = boto3.dynamodb.types.DYNAMODB_CONTEXT
34+
old_traps = old_context.__getattribute__("traps")
35+
# traps structure: {k (trap class) : v (True if trap should raise Exception; False otherwise)}
36+
old_traps[Rounded] = False
37+
boto3.dynamodb.types.DYNAMODB_CONTEXT.__setattr__("traps", old_traps)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import abc
4+
from typing import Any, Dict
5+
from abc import abstractmethod
6+
7+
class EncryptedBotoInterface(abc.ABC):
8+
"""Interface for encrypted boto3 interfaces."""
9+
10+
def _copy_sdk_response_to_dbesdk_response(self, sdk_response: Dict[str, Any], dbesdk_response: Dict[str, Any]) -> Dict[str, Any]:
11+
"""Copy any missing fields from the SDK response to the DBESDK response.
12+
13+
Args:
14+
sdk_response: The raw SDK response
15+
dbesdk_response: The current DBESDK response
16+
17+
Returns:
18+
dict: The DBESDK response with any missing fields copied from SDK response
19+
"""
20+
for sdk_response_key, sdk_response_value in sdk_response.items():
21+
if sdk_response_key not in dbesdk_response:
22+
dbesdk_response[sdk_response_key] = sdk_response_value
23+
return dbesdk_response
24+
25+
@property
26+
@abstractmethod
27+
def _boto_client_attr_name(self) -> str:
28+
"""Name of the attribute containing the underlying boto3 client."""
29+
30+
def __getattr__(self, name: str) -> Any:
31+
"""Delegate unknown attributes to the underlying client.
32+
33+
Args:
34+
name: The name of the attribute to get
35+
36+
Returns:
37+
Any: The attribute value from the underlying client
38+
39+
Raises:
40+
AttributeError: If the attribute doesn't exist on the underlying client
41+
"""
42+
client_attr = getattr(self, self._boto_client_attr_name)
43+
if hasattr(client_attr, name):
44+
return getattr(client_attr, name)
45+
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

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

Lines changed: 447 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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.config import (
7+
DynamoDbItemEncryptorConfig,
8+
)
9+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_itemencryptor.client import (
10+
DynamoDbItemEncryptor,
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+
dict_to_ddb,
20+
ddb_to_dict,
21+
)
22+
23+
class ItemEncryptor:
24+
"""Class providing item-level encryption for DynamoDB items / Python dictionaries."""
25+
26+
_internal_client: DynamoDbItemEncryptor
27+
28+
def __init__(
29+
self,
30+
item_encryptor_config: DynamoDbItemEncryptorConfig,
31+
):
32+
"""
33+
Create an ItemEncryptor.
34+
35+
Parameters:
36+
item_encryptor_config (DynamoDbItemEncryptorConfig): Encryption configuration object.
37+
"""
38+
self._internal_client = DynamoDbItemEncryptor(config = item_encryptor_config)
39+
40+
def encrypt_python_item(
41+
self,
42+
plaintext_dict_item: dict[str, Any],
43+
) -> EncryptItemOutput:
44+
"""
45+
Encrypt a Python dictionary.
46+
47+
This method will transform the Python dictionary into DynamoDB JSON,
48+
encrypt the DynamoDB JSON,
49+
transform the encrypted DynamoDB JSON into an encrypted Python dictionary,
50+
then return the encrypted Python dictionary.
51+
52+
See the boto3 documentation for details on Python/DynamoDB type transfomations:
53+
https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/dynamodb/types.html
54+
55+
boto3 DynamoDB Tables and Resources expect items formatted as native Python dictionaries.
56+
Use this method to encrypt an item if you intend to pass the encrypted item
57+
to a boto3 DynamoDB Table or Resource interface to store it.
58+
(Alternatively, you can use this library's EncryptedTable or EncryptedResource interfaces
59+
to transparently encrypt items without an intermediary ItemEncryptor.)
60+
61+
Parameters:
62+
plaintext_dict_item (dict[str, Any]): A standard Python dictionary.
63+
64+
Returns:
65+
EncryptItemOutput: Structure containing the following fields:
66+
- `encrypted_item` (dict[str, Any]): The encrypted Python dictionary.
67+
**Note:** The item was encrypted as DynamoDB JSON, then transformed to a Python dictionary.
68+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header (parsed `aws_dbe_head` value).
69+
70+
Example:
71+
72+
>>> plaintext_item = {
73+
... 'some': 'data',
74+
... 'more': 5
75+
... }
76+
>>> encrypt_output = item_encryptor.encrypt_python_item(plaintext_item)
77+
>>> encrypted_item = encrypt_output.encrypted_item
78+
>>> header = encrypt_output.parsed_header
79+
"""
80+
plaintext_ddb_item = dict_to_ddb(plaintext_dict_item)
81+
encrypted_ddb_item: EncryptItemOutput = self.encrypt_dynamodb_item(plaintext_ddb_item)
82+
encrypted_dict_item = ddb_to_dict(encrypted_ddb_item.encrypted_item)
83+
return EncryptItemOutput(
84+
encrypted_item = encrypted_dict_item,
85+
parsed_header = encrypted_ddb_item.parsed_header
86+
)
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+
Parameters:
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+
113+
>>> plaintext_item = {
114+
... 'some': {'S': 'data'},
115+
... 'more': {'N': '5'}
116+
... }
117+
>>> encrypt_output = item_encryptor.encrypt_dynamodb_item(plaintext_item)
118+
>>> encrypted_item = encrypt_output.encrypted_item
119+
>>> header = encrypt_output.parsed_header
120+
"""
121+
return self.encrypt_item(
122+
EncryptItemInput(
123+
plaintext_item = plaintext_dynamodb_item
124+
)
125+
)
126+
127+
def encrypt_item(
128+
self,
129+
encrypt_item_input: EncryptItemInput,
130+
) -> EncryptItemOutput:
131+
"""
132+
Encrypt a DynamoDB item.
133+
134+
The input item should contain a dictionary formatted as DynamoDB JSON:
135+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
136+
137+
Parameters:
138+
encrypt_item_input (EncryptItemInput): Structure containing the following field:
139+
- `plaintext_item` (dict[str, Any]): The item to encrypt formatted as DynamoDB JSON.
140+
141+
Returns:
142+
EncryptItemOutput: Structure containing the following fields:
143+
- `encrypted_item` (dict[str, Any]): The encrypted DynamoDB item formatted as DynamoDB JSON.
144+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header (`aws_dbe_head` value).
145+
146+
Example:
147+
148+
>>> plaintext_item = {
149+
... 'some': {'S': 'data'},
150+
... 'more': {'N': '5'}
151+
... }
152+
>>> encrypt_output = item_encryptor.encrypt_item(
153+
... EncryptItemInput(
154+
... plaintext_ddb_item = plaintext_item
155+
... )
156+
... )
157+
>>> encrypted_item = encrypt_output.encrypted_item
158+
>>> header = encrypt_output.parsed_header
159+
"""
160+
return self._internal_client.encrypt_item(encrypt_item_input)
161+
162+
def decrypt_python_item(
163+
self,
164+
encrypted_dict_item: dict[str, Any],
165+
) -> DecryptItemOutput:
166+
"""
167+
Decrypt a Python dictionary.
168+
169+
This method will transform the Python dictionary into DynamoDB JSON,
170+
decrypt the DynamoDB JSON,
171+
transform the plaintext DynamoDB JSON into a plaintext Python dictionary,
172+
then return the plaintext Python dictionary.
173+
174+
See the boto3 documentation for details on Python/DynamoDB type transfomations:
175+
https://boto3.amazonaws.com/v1/documentation/api/latest/_modules/boto3/dynamodb/types.html
176+
177+
boto3 DynamoDB Tables and Resources return items formatted as native Python dictionaries.
178+
Use this method to decrypt an item if you retrieve the encrypted item
179+
from a boto3 DynamoDB Table or Resource interface.
180+
(Alternatively, you can use this library's EncryptedTable or EncryptedResource interfaces
181+
to transparently decrypt items without an intermediary ItemEncryptor.)
182+
183+
Parameters:
184+
encrypted_dict_item (dict[str, Any]): A standard Python dictionary with encrypted values.
185+
186+
Returns:
187+
DecryptItemOutput: Structure containing the following fields:
188+
- `plaintext_item` (dict[str, Any]): The decrypted Python dictionary.
189+
**Note:** The item was decrypted as DynamoDB JSON, then transformed to a Python dictionary.
190+
- `parsed_header` (Optional[ParsedHeader]): The encrypted DynamoDB item's header (parsed `aws_dbe_head` value).
191+
192+
Example:
193+
194+
>>> encrypted_item = {
195+
... 'some': b'ENCRYPTED_DATA',
196+
... 'more': b'ENCRYPTED_DATA',
197+
... }
198+
>>> decrypt_output = item_encryptor.decrypt_python_item(encrypted_item)
199+
>>> plaintext_item = decrypt_output.plaintext_item
200+
>>> header = decrypt_output.parsed_header
201+
"""
202+
encrypted_ddb_item = dict_to_ddb(encrypted_dict_item)
203+
plaintext_ddb_item: DecryptItemOutput = self.decrypt_dynamodb_item(encrypted_ddb_item)
204+
plaintext_dict_item = ddb_to_dict(plaintext_ddb_item.plaintext_item)
205+
return DecryptItemOutput(
206+
plaintext_item = plaintext_dict_item,
207+
parsed_header = plaintext_ddb_item.parsed_header
208+
)
209+
210+
def decrypt_dynamodb_item(
211+
self,
212+
encrypted_dynamodb_item: dict[str, dict[str, Any]],
213+
) -> DecryptItemOutput:
214+
"""
215+
Decrypt DynamoDB-formatted JSON.
216+
217+
boto3 DynamoDB clients return items formatted as DynamoDB JSON:
218+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
219+
Use this method to decrypt an item if you retrieved the encrypted item
220+
from a boto3 DynamoDB client.
221+
(Alternatively, you can use this library's EncryptedClient interface
222+
to transparently decrypt items without an intermediary ItemEncryptor.)
223+
224+
Parameters:
225+
encrypted_ddb_item (dict[str, dict[str, Any]]): The item to decrypt formatted as DynamoDB JSON.
226+
227+
Returns:
228+
DecryptItemOutput: Structure containing the following fields:
229+
- `plaintext_item` (dict[str, Any]): The plaintext DynamoDB item formatted as DynamoDB JSON.
230+
- `parsed_header` (Optional[ParsedHeader]): The decrypted DynamoDB item's header (`aws_dbe_head` value).
231+
232+
Example:
233+
234+
>>> encrypted_item = {
235+
... 'some': {'B': b'ENCRYPTED_DATA'},
236+
... 'more': {'B': b'ENCRYPTED_DATA'}
237+
... }
238+
>>> decrypt_output = item_encryptor.decrypt_dynamodb_item(encrypted_item)
239+
>>> plaintext_item = decrypt_output.plaintext_item
240+
>>> header = decrypt_output.parsed_header
241+
"""
242+
return self.decrypt_item(
243+
DecryptItemInput(
244+
encrypted_item = encrypted_dynamodb_item
245+
)
246+
)
247+
248+
def decrypt_item(
249+
self,
250+
decrypt_item_input: DecryptItemInput,
251+
) -> DecryptItemOutput:
252+
"""
253+
Decrypt a DynamoDB item.
254+
255+
The input item should contain a dictionary formatted as DynamoDB JSON:
256+
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.LowLevelAPI.html
257+
258+
Parameters:
259+
decrypt_item_input (DecryptItemInput): Structure containing the following field:
260+
- `encrypted_item` (dict[str, Any]): The item to decrypt formatted as DynamoDB JSON.
261+
262+
Returns:
263+
DecryptItemOutput: Structure containing the following fields:
264+
- `plaintext_item` (dict[str, Any]): The decrypted DynamoDB item formatted as DynamoDB JSON.
265+
- `parsed_header` (Optional[ParsedHeader]): The decrypted DynamoDB item's header (`aws_dbe_head` value).
266+
267+
Example:
268+
269+
>>> encrypted_item = {
270+
... 'some': {'B': b'ENCRYPTED_DATA'},
271+
... 'more': {'B': b'ENCRYPTED_DATA'}
272+
... }
273+
>>> decrypted_item = item_encryptor.decrypt_item(
274+
... DecryptItemInput(
275+
... encrypted_item = encrypted_item,
276+
... )
277+
... )
278+
>>> plaintext_item = decrypted_item.plaintext_item
279+
>>> header = decrypted_item.parsed_header
280+
"""
281+
return self._internal_client.decrypt_item(decrypt_item_input)

0 commit comments

Comments
 (0)