Skip to content

Commit 643460e

Browse files
authored
Merge pull request #120 from mattsb42-aws/query
Fix EncryptedPaginator to successfully decrypt
2 parents ad0c123 + 12ea0e8 commit 643460e

File tree

11 files changed

+213
-90
lines changed

11 files changed

+213
-90
lines changed

CHANGELOG.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
Changelog
33
*********
44

5+
1.1.1 -- 2019-08-xx
6+
===================
7+
8+
Bugfixes
9+
--------
10+
* Fix :class:`EncryptedPaginator` to successfully decrypt when using :class:`AwsKmsCryptographicMaterialsProvider`
11+
`#118 <https://github.com/aws/aws-dynamodb-encryption-python/pull/118>`_
12+
513
1.1.0 -- 2019-03-13
614
===================
715

src/dynamodb_encryption_sdk/encrypted/client.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
crypto_config_from_kwargs,
2323
decrypt_batch_get_item,
2424
decrypt_get_item,
25+
decrypt_list_of_items,
2526
decrypt_multi_get,
2627
encrypt_batch_write_item,
2728
encrypt_put_item,
@@ -104,8 +105,11 @@ def paginate(self, **kwargs):
104105
crypto_config, ddb_kwargs = self._crypto_config_method(**kwargs)
105106

106107
for page in self._paginator.paginate(**ddb_kwargs):
107-
for pos, value in enumerate(page["Items"]):
108-
page["Items"][pos] = self._decrypt_method(item=value, crypto_config=crypto_config)
108+
page["Items"] = list(
109+
decrypt_list_of_items(
110+
crypto_config=crypto_config, decrypt_method=self._decrypt_method, items=page["Items"]
111+
)
112+
)
109113
yield page
110114

111115

src/dynamodb_encryption_sdk/internal/utils.py

+21-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from dynamodb_encryption_sdk.transform import dict_to_ddb
3030

3131
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
32-
from typing import Any, Bool, Callable, Dict, Text # noqa pylint: disable=unused-import
32+
from typing import Any, Bool, Callable, Dict, Iterable, Text # noqa pylint: disable=unused-import
3333
except ImportError: # pragma: no cover
3434
# We only actually need these imports when running the mypy checks
3535
pass
@@ -41,6 +41,7 @@
4141
"crypto_config_from_cache",
4242
"decrypt_get_item",
4343
"decrypt_multi_get",
44+
"decrypt_list_of_items",
4445
"decrypt_batch_get_item",
4546
"encrypt_put_item",
4647
"encrypt_batch_write_item",
@@ -171,6 +172,22 @@ def _item_transformer(crypto_transformer):
171172
return lambda x: x
172173

173174

175+
def decrypt_list_of_items(crypto_config, decrypt_method, items):
176+
# type: (CryptoConfig, Callable, Iterable[Any]) -> Iterable[Any]
177+
# TODO: narrow this down
178+
"""Iterate through a list of encrypted items, decrypting each item and yielding the plaintext item.
179+
180+
:param CryptoConfig crypto_config: :class:`CryptoConfig` to use
181+
:param callable decrypt_method: Method to use to decrypt items
182+
:param items: Iterable of encrypted items
183+
:return: Iterable of plaintext items
184+
"""
185+
for value in items:
186+
yield decrypt_method(
187+
item=value, crypto_config=crypto_config.with_item(_item_transformer(decrypt_method)(value))
188+
)
189+
190+
174191
def decrypt_multi_get(decrypt_method, crypto_config_method, read_method, **kwargs):
175192
# type: (Callable, Callable, Callable, **Any) -> Dict
176193
# TODO: narrow this down
@@ -186,11 +203,9 @@ def decrypt_multi_get(decrypt_method, crypto_config_method, read_method, **kwarg
186203
validate_get_arguments(kwargs)
187204
crypto_config, ddb_kwargs = crypto_config_method(**kwargs)
188205
response = read_method(**ddb_kwargs)
189-
for pos in range(len(response["Items"])):
190-
response["Items"][pos] = decrypt_method(
191-
item=response["Items"][pos],
192-
crypto_config=crypto_config.with_item(_item_transformer(decrypt_method)(response["Items"][pos])),
193-
)
206+
response["Items"] = list(
207+
decrypt_list_of_items(crypto_config=crypto_config, decrypt_method=decrypt_method, items=response["Items"])
208+
)
194209
return response
195210

196211

test/functional/encrypted/test_client.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
build_static_jce_cmp,
2121
client_batch_items_unprocessed_check,
2222
client_cycle_batch_items_check,
23-
client_cycle_batch_items_check_paginators,
23+
client_cycle_batch_items_check_scan_paginator,
2424
client_cycle_single_item_check,
2525
set_parametrized_actions,
2626
set_parametrized_cmp,
@@ -49,8 +49,8 @@ def _client_cycle_batch_items_check(materials_provider, initial_actions, initial
4949
)
5050

5151

52-
def _client_cycle_batch_items_check_paginators(materials_provider, initial_actions, initial_item):
53-
return client_cycle_batch_items_check_paginators(
52+
def _client_cycle_batch_items_check_scan_paginator(materials_provider, initial_actions, initial_item):
53+
return client_cycle_batch_items_check_scan_paginator(
5454
materials_provider, initial_actions, initial_item, TEST_TABLE_NAME, "us-west-2"
5555
)
5656

@@ -71,9 +71,9 @@ def test_ephemeral_batch_item_cycle(example_table, some_cmps, parametrized_actio
7171
_client_cycle_batch_items_check(some_cmps, parametrized_actions, parametrized_item)
7272

7373

74-
def test_ephemeral_batch_item_cycle_paginators(example_table, some_cmps, parametrized_actions, parametrized_item):
75-
"""Test a small number of curated CMPs against a small number of curated items using paginators."""
76-
_client_cycle_batch_items_check_paginators(some_cmps, parametrized_actions, parametrized_item)
74+
def test_ephemeral_batch_item_cycle_scan_paginator(example_table, some_cmps, parametrized_actions, parametrized_item):
75+
"""Test a small number of curated CMPs against a small number of curated items using the scan paginator."""
76+
_client_cycle_batch_items_check_scan_paginator(some_cmps, parametrized_actions, parametrized_item)
7777

7878

7979
def test_batch_item_unprocessed(example_table, parametrized_actions, parametrized_item):

test/functional/encrypted/test_item.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
def pytest_generate_tests(metafunc):
4040
set_parametrized_actions(metafunc)
41-
set_parametrized_cmp(metafunc)
41+
set_parametrized_cmp(metafunc, require_attributes=False)
4242
set_parametrized_item(metafunc)
4343

4444

test/functional/functional_test_utils.py

+114-49
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@
3434
from dynamodb_encryption_sdk.encrypted.table import EncryptedTable
3535
from dynamodb_encryption_sdk.identifiers import CryptoAction
3636
from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes
37+
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
3738
from dynamodb_encryption_sdk.material_providers.most_recent import MostRecentProvider
3839
from dynamodb_encryption_sdk.material_providers.static import StaticCryptographicMaterialsProvider
3940
from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore
4041
from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider
42+
from dynamodb_encryption_sdk.materials import CryptographicMaterials
4143
from dynamodb_encryption_sdk.materials.raw import RawDecryptionMaterials, RawEncryptionMaterials
42-
from dynamodb_encryption_sdk.structures import AttributeActions
44+
from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext
4345
from dynamodb_encryption_sdk.transform import ddb_to_dict, dict_to_ddb
4446

4547
RUNNING_IN_TRAVIS = "TRAVIS" in os.environ
@@ -164,6 +166,38 @@ def table_with_global_secondary_indexes():
164166
mock_dynamodb2().stop()
165167

166168

169+
class PassThroughCryptographicMaterialsProviderThatRequiresAttributes(CryptographicMaterialsProvider):
170+
"""Cryptographic materials provider that passes through to another, but requires that attributes are set.
171+
172+
If the EncryptionContext passed to decryption_materials or encryption_materials
173+
ever does not have attributes set,
174+
a ValueError is raised.
175+
Otherwise, it passes through to the passthrough CMP normally.
176+
"""
177+
178+
def __init__(self, passthrough_cmp):
179+
self._passthrough_cmp = passthrough_cmp
180+
181+
def _assert_attributes_set(self, encryption_context):
182+
# type: (EncryptionContext) -> None
183+
if not encryption_context.attributes:
184+
raise ValueError("Encryption context attributes MUST be set!")
185+
186+
def decryption_materials(self, encryption_context):
187+
# type: (EncryptionContext) -> CryptographicMaterials
188+
self._assert_attributes_set(encryption_context)
189+
return self._passthrough_cmp.decryption_materials(encryption_context)
190+
191+
def encryption_materials(self, encryption_context):
192+
# type: (EncryptionContext) -> CryptographicMaterials
193+
self._assert_attributes_set(encryption_context)
194+
return self._passthrough_cmp.encryption_materials(encryption_context)
195+
196+
def refresh(self):
197+
# type: () -> None
198+
self._passthrough_cmp.refresh()
199+
200+
167201
def _get_from_cache(dk_class, algorithm, key_length):
168202
"""Don't generate new keys every time. All we care about is that they are valid keys, not that they are unique."""
169203
try:
@@ -221,8 +255,15 @@ def _some_algorithm_pairs():
221255
_cmp_builders = {"static": build_static_jce_cmp, "wrapped": _build_wrapped_jce_cmp}
222256

223257

224-
def _all_possible_cmps(algorithm_generator):
225-
"""Generate all possible cryptographic materials providers based on the supplied generator."""
258+
def _all_possible_cmps(algorithm_generator, require_attributes):
259+
"""Generate all possible cryptographic materials providers based on the supplied generator.
260+
261+
require_attributes determines whether the CMP will be wrapped in
262+
PassThroughCryptographicMaterialsProviderThatRequiresAttributes
263+
to require that attributes are set on every request.
264+
This should ONLY be disabled on the item encryptor tests.
265+
All high-level helper clients MUST set the attributes before passing the encryption context down.
266+
"""
226267
# The AES combinations do the same thing, but this makes sure that the AESWrap name works as expected.
227268
yield _build_wrapped_jce_cmp("AESWrap", 256, "HmacSHA256", 256)
228269

@@ -242,17 +283,28 @@ def _all_possible_cmps(algorithm_generator):
242283
sig_key_length=signing_key_length,
243284
)
244285

245-
yield pytest.param(
246-
builder_func(encryption_algorithm, encryption_key_length, signing_algorithm, signing_key_length),
247-
id=id_string,
248-
)
286+
inner_cmp = builder_func(encryption_algorithm, encryption_key_length, signing_algorithm, signing_key_length)
287+
288+
if require_attributes:
289+
outer_cmp = PassThroughCryptographicMaterialsProviderThatRequiresAttributes(inner_cmp)
290+
else:
291+
outer_cmp = inner_cmp
292+
293+
yield pytest.param(outer_cmp, id=id_string)
249294

250295

251-
def set_parametrized_cmp(metafunc):
252-
"""Set paramatrized values for cryptographic materials providers."""
296+
def set_parametrized_cmp(metafunc, require_attributes=True):
297+
"""Set paramatrized values for cryptographic materials providers.
298+
299+
require_attributes determines whether the CMP will be wrapped in
300+
PassThroughCryptographicMaterialsProviderThatRequiresAttributes
301+
to require that attributes are set on every request.
302+
This should ONLY be disabled on the item encryptor tests.
303+
All high-level helper clients MUST set the attributes before passing the encryption context down.
304+
"""
253305
for name, algorithm_generator in (("all_the_cmps", _all_algorithm_pairs), ("some_cmps", _some_algorithm_pairs)):
254306
if name in metafunc.fixturenames:
255-
metafunc.parametrize(name, _all_possible_cmps(algorithm_generator))
307+
metafunc.parametrize(name, _all_possible_cmps(algorithm_generator, require_attributes))
256308

257309

258310
_ACTIONS = {
@@ -437,30 +489,34 @@ def cycle_batch_item_check(
437489
check_attribute_actions = initial_actions.copy()
438490
check_attribute_actions.set_index_keys(*list(TEST_KEY.keys()))
439491
items = _generate_items(initial_item, write_transformer)
492+
items_in_table = len(items)
440493

441494
_put_result = encrypted.batch_write_item( # noqa
442495
RequestItems={table_name: [{"PutRequest": {"Item": _item}} for _item in items]}
443496
)
444497

445-
ddb_keys = [write_transformer(key) for key in TEST_BATCH_KEYS]
446-
encrypted_result = raw.batch_get_item(RequestItems={table_name: {"Keys": ddb_keys}})
447-
check_many_encrypted_items(
448-
actual=encrypted_result["Responses"][table_name],
449-
expected=items,
450-
attribute_actions=check_attribute_actions,
451-
transformer=read_transformer,
452-
)
453-
454-
decrypted_result = encrypted.batch_get_item(RequestItems={table_name: {"Keys": ddb_keys}})
455-
assert_equal_lists_of_items(
456-
actual=decrypted_result["Responses"][table_name], expected=items, transformer=read_transformer
457-
)
498+
try:
499+
ddb_keys = [write_transformer(key) for key in TEST_BATCH_KEYS]
500+
encrypted_result = raw.batch_get_item(RequestItems={table_name: {"Keys": ddb_keys}})
501+
check_many_encrypted_items(
502+
actual=encrypted_result["Responses"][table_name],
503+
expected=items,
504+
attribute_actions=check_attribute_actions,
505+
transformer=read_transformer,
506+
)
458507

459-
if delete_items:
460-
_cleanup_items(encrypted, write_transformer, table_name)
508+
decrypted_result = encrypted.batch_get_item(RequestItems={table_name: {"Keys": ddb_keys}})
509+
assert_equal_lists_of_items(
510+
actual=decrypted_result["Responses"][table_name], expected=items, transformer=read_transformer
511+
)
512+
finally:
513+
if delete_items:
514+
_cleanup_items(encrypted, write_transformer, table_name)
515+
items_in_table = 0
461516

462517
del check_attribute_actions
463518
del items
519+
return items_in_table
464520

465521

466522
def cycle_batch_writer_check(raw_table, encrypted_table, initial_actions, initial_item):
@@ -692,16 +748,23 @@ def client_batch_items_unprocessed_check(
692748
)
693749

694750

695-
def client_cycle_batch_items_check_paginators(
751+
def client_cycle_batch_items_check_scan_paginator(
696752
materials_provider, initial_actions, initial_item, table_name, region_name=None
697753
):
754+
"""Helper function for testing the "scan" paginator.
755+
756+
Populate the specified table with encrypted items,
757+
scan the table with raw client paginator to get encrypted items,
758+
scan the table with encrypted client paginator to get decrypted items,
759+
then verify that all items appear to have been encrypted correctly.
760+
"""
698761
kwargs = {}
699762
if region_name is not None:
700763
kwargs["region_name"] = region_name
701764
client = boto3.client("dynamodb", **kwargs)
702765
e_client = EncryptedClient(client=client, materials_provider=materials_provider, attribute_actions=initial_actions)
703766

704-
cycle_batch_item_check(
767+
items_in_table = cycle_batch_item_check(
705768
raw=client,
706769
encrypted=e_client,
707770
initial_actions=initial_actions,
@@ -712,29 +775,31 @@ def client_cycle_batch_items_check_paginators(
712775
delete_items=False,
713776
)
714777

715-
encrypted_items = []
716-
raw_paginator = client.get_paginator("scan")
717-
for page in raw_paginator.paginate(TableName=table_name, ConsistentRead=True):
718-
encrypted_items.extend(page["Items"])
719-
720-
decrypted_items = []
721-
encrypted_paginator = e_client.get_paginator("scan")
722-
for page in encrypted_paginator.paginate(TableName=table_name, ConsistentRead=True):
723-
decrypted_items.extend(page["Items"])
724-
725-
print(encrypted_items)
726-
print(decrypted_items)
727-
728-
check_attribute_actions = initial_actions.copy()
729-
check_attribute_actions.set_index_keys(*list(TEST_KEY.keys()))
730-
check_many_encrypted_items(
731-
actual=encrypted_items,
732-
expected=decrypted_items,
733-
attribute_actions=check_attribute_actions,
734-
transformer=ddb_to_dict,
735-
)
778+
try:
779+
encrypted_items = []
780+
raw_paginator = client.get_paginator("scan")
781+
for page in raw_paginator.paginate(TableName=table_name, ConsistentRead=True):
782+
encrypted_items.extend(page["Items"])
783+
784+
decrypted_items = []
785+
encrypted_paginator = e_client.get_paginator("scan")
786+
for page in encrypted_paginator.paginate(TableName=table_name, ConsistentRead=True):
787+
decrypted_items.extend(page["Items"])
788+
789+
assert encrypted_items and decrypted_items
790+
assert len(encrypted_items) == len(decrypted_items) == items_in_table
791+
792+
check_attribute_actions = initial_actions.copy()
793+
check_attribute_actions.set_index_keys(*list(TEST_KEY.keys()))
794+
check_many_encrypted_items(
795+
actual=encrypted_items,
796+
expected=decrypted_items,
797+
attribute_actions=check_attribute_actions,
798+
transformer=ddb_to_dict,
799+
)
736800

737-
_cleanup_items(encrypted=e_client, write_transformer=dict_to_ddb, table_name=table_name)
801+
finally:
802+
_cleanup_items(encrypted=e_client, write_transformer=dict_to_ddb, table_name=table_name)
738803

739804
raw_scan_result = client.scan(TableName=table_name, ConsistentRead=True)
740805
e_scan_result = e_client.scan(TableName=table_name, ConsistentRead=True)

0 commit comments

Comments
 (0)