Skip to content

Commit 1bd5fa6

Browse files
authored
feat: expose keyring trace in results (#224)
* feat: add CryptoResult and plumb in use * chore: simplify test parametrization * chore: adjust RawKeyring functional tests to use CryptoResult and check keyring trace * chore: add unit tests for CryptoResult * docs: fix docstrings and typehints in structures * chore: verify that CryptoResult casts to a tuple as expected * docs: add keyrings link and clarify how CryptoResult could break you
1 parent 9402826 commit 1bd5fa6

File tree

8 files changed

+247
-78
lines changed

8 files changed

+247
-78
lines changed

CHANGELOG.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
Changelog
33
*********
44

5+
1.5.0 -- 2020-xx-xx
6+
===================
7+
8+
Major Features
9+
--------------
10+
11+
* Add `keyrings`_.
12+
* Change one-step APIs to return a :class:`CryptoResult` rather than a tuple.
13+
* Modified APIs: ``aws_encryption_sdk.encrypt`` and ``aws_encryption_sdk.decrypt``.
14+
15+
.. note::
16+
17+
For backwards compatibility,
18+
:class:`CryptoResult` also unpacks like a 2-member tuple.
19+
This allows for backwards compatibility with the previous outputs
20+
so this change should not break any existing consumers
21+
unless you are specifically relying on the output being an instance of :class:`tuple`.
22+
23+
524
1.4.1 -- 2019-09-20
625
===================
726

@@ -193,3 +212,4 @@ Minor
193212
.. _pylint: https://www.pylint.org/
194213
.. _flake8: http://flake8.pycqa.org/en/latest/
195214
.. _doc8: https://launchpad.net/doc8
215+
.. _keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html

src/aws_encryption_sdk/__init__.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
StreamDecryptor,
1515
StreamEncryptor,
1616
)
17+
from aws_encryption_sdk.structures import CryptoResult
18+
19+
__all__ = ("encrypt", "decrypt", "stream")
1720

1821

1922
def encrypt(**kwargs):
@@ -26,6 +29,13 @@ def encrypt(**kwargs):
2629
.. versionadded:: 1.5.0
2730
The *keyring* parameter.
2831
32+
.. versionadded:: 1.5.0
33+
34+
For backwards compatibility,
35+
the new :class:`CryptoResult` return value also unpacks like a 2-member tuple.
36+
This allows for backwards compatibility with the previous outputs
37+
so this change should not break any existing consumers.
38+
2939
.. code:: python
3040
3141
>>> import aws_encryption_sdk
@@ -67,12 +77,13 @@ def encrypt(**kwargs):
6777
:param algorithm: Algorithm to use for encryption
6878
:type algorithm: aws_encryption_sdk.identifiers.Algorithm
6979
:param int frame_length: Frame length in bytes
70-
:returns: Tuple containing the encrypted ciphertext and the message header object
71-
:rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader`
80+
:returns: Encrypted message, message metadata (header), and keyring trace
81+
:rtype: CryptoResult
7282
"""
7383
with StreamEncryptor(**kwargs) as encryptor:
7484
ciphertext = encryptor.read()
75-
return ciphertext, encryptor.header
85+
86+
return CryptoResult(result=ciphertext, header=encryptor.header, keyring_trace=encryptor.keyring_trace)
7687

7788

7889
def decrypt(**kwargs):
@@ -85,6 +96,13 @@ def decrypt(**kwargs):
8596
.. versionadded:: 1.5.0
8697
The *keyring* parameter.
8798
99+
.. versionadded:: 1.5.0
100+
101+
For backwards compatibility,
102+
the new :class:`CryptoResult` return value also unpacks like a 2-member tuple.
103+
This allows for backwards compatibility with the previous outputs
104+
so this change should not break any existing consumers.
105+
88106
.. code:: python
89107
90108
>>> import aws_encryption_sdk
@@ -117,12 +135,13 @@ def decrypt(**kwargs):
117135
118136
:param int max_body_length: Maximum frame size (or content length for non-framed messages)
119137
in bytes to read from ciphertext message.
120-
:returns: Tuple containing the decrypted plaintext and the message header object
121-
:rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader`
138+
:returns: Decrypted plaintext, message metadata (header), and keyring trace
139+
:rtype: CryptoResult
122140
"""
123141
with StreamDecryptor(**kwargs) as decryptor:
124142
plaintext = decryptor.read()
125-
return plaintext, decryptor.header
143+
144+
return CryptoResult(result=plaintext, header=decryptor.header, keyring_trace=decryptor.keyring_trace)
126145

127146

128147
def stream(**kwargs):
@@ -182,6 +201,3 @@ def stream(**kwargs):
182201
return _stream_map[mode.lower()](**kwargs)
183202
except KeyError:
184203
raise ValueError("Unsupported mode: {}".format(mode))
185-
186-
187-
__all__ = ("encrypt", "decrypt", "stream")

src/aws_encryption_sdk/materials_managers/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, RawDataKey
2626

2727
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
28-
from typing import Any, FrozenSet, Iterable, Tuple, Union # noqa pylint: disable=unused-import
28+
from typing import Any, Iterable, Tuple, Union # noqa pylint: disable=unused-import
2929
except ImportError: # pragma: no cover
3030
# We only actually need these imports when running the mypy checks
3131
pass
@@ -238,10 +238,10 @@ def __init__(
238238

239239
@property
240240
def encrypted_data_keys(self):
241-
# type: () -> FrozenSet[EncryptedDataKey]
241+
# type: () -> Tuple[EncryptedDataKey]
242242
"""Return a read-only version of the encrypted data keys.
243243
244-
:rtype: frozenset
244+
:rtype: Tuple[EncryptedDataKey]
245245
"""
246246
return tuple(self._encrypted_data_keys)
247247

src/aws_encryption_sdk/streaming_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class _EncryptionStream(io.IOBase):
137137
_message_prepped = None # type: bool
138138
source_stream = None
139139
_stream_length = None # type: int
140+
keyring_trace = ()
140141

141142
def __new__(cls, **kwargs):
142143
"""Perform necessary handling for _EncryptionStream instances that should be
@@ -443,6 +444,7 @@ def _prep_message(self):
443444
self._encryption_materials = self.config.materials_manager.get_encryption_materials(
444445
request=encryption_materials_request
445446
)
447+
self.keyring_trace = self._encryption_materials.keyring_trace
446448

447449
if self.config.algorithm is not None and self._encryption_materials.algorithm != self.config.algorithm:
448450
raise ActionNotAllowedError(
@@ -779,6 +781,8 @@ def _read_header(self):
779781
encryption_context=header.encryption_context,
780782
)
781783
decryption_materials = self.config.materials_manager.decrypt_materials(request=decrypt_materials_request)
784+
self.keyring_trace = decryption_materials.keyring_trace
785+
782786
if decryption_materials.verification_key is None:
783787
self.verifier = None
784788
else:

src/aws_encryption_sdk/structures.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion
2121
from aws_encryption_sdk.internal.str_ops import to_bytes, to_str
2222

23+
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
24+
from typing import Tuple # noqa pylint: disable=unused-import
25+
except ImportError: # pragma: no cover
26+
# We only actually need these imports when running the mypy checks
27+
pass
28+
2329

2430
@attr.s(hash=True)
2531
class MasterKeyInfo(object):
@@ -107,8 +113,7 @@ class KeyringTrace(object):
107113
.. versionadded:: 1.5.0
108114
109115
:param MasterKeyInfo wrapping_key: Wrapping key used
110-
:param flags: Actions performed
111-
:type flags: set of :class:`KeyringTraceFlag`
116+
:param Set[KeyringTraceFlag] flags: Actions performed
112117
"""
113118

114119
wrapping_key = attr.ib(validator=instance_of(MasterKeyInfo))
@@ -120,19 +125,14 @@ class MessageHeader(object):
120125
# pylint: disable=too-many-instance-attributes
121126
"""Deserialized message header object.
122127
123-
:param version: Message format version, per spec
124-
:type version: SerializationVersion
125-
:param type: Message content type, per spec
126-
:type type: ObjectType
127-
:param algorithm: Algorithm to use for encryption
128-
:type algorithm: Algorithm
128+
:param SerializationVersion version: Message format version, per spec
129+
:param ObjectType type: Message content type, per spec
130+
:param AlgorithmSuite algorithm: Algorithm to use for encryption
129131
:param bytes message_id: Message ID
130-
:param dict encryption_context: Dictionary defining encryption context
131-
:param encrypted_data_keys: Encrypted data keys
132-
:type encrypted_data_keys: set of :class:`aws_encryption_sdk.structures.EncryptedDataKey`
133-
:param content_type: Message content framing type (framed/non-framed)
134-
:type content_type: ContentType
135-
:param bytes content_aad_length: empty
132+
:param Dict[str,str] encryption_context: Dictionary defining encryption context
133+
:param Sequence[EncryptedDataKey] encrypted_data_keys: Encrypted data keys
134+
:param ContentType content_type: Message content framing type (framed/non-framed)
135+
:param int content_aad_length: empty
136136
:param int header_iv_length: Bytes in Initialization Vector value found in header
137137
:param int frame_length: Length of message frame in bytes
138138
"""
@@ -152,3 +152,41 @@ class MessageHeader(object):
152152
content_aad_length = attr.ib(hash=True, validator=instance_of(six.integer_types))
153153
header_iv_length = attr.ib(hash=True, validator=instance_of(six.integer_types))
154154
frame_length = attr.ib(hash=True, validator=instance_of(six.integer_types))
155+
156+
157+
@attr.s
158+
class CryptoResult(object):
159+
"""Result container for one-shot cryptographic API results.
160+
161+
.. versionadded:: 1.5.0
162+
163+
.. note::
164+
165+
For backwards compatibility,
166+
this container also unpacks like a 2-member tuple.
167+
This allows for backwards compatibility with the previous outputs.
168+
169+
:param bytes result: Binary results of the cryptographic operation
170+
:param MessageHeader header: Encrypted message metadata
171+
:param Tuple[KeyringTrace] keyring_trace: Keyring trace entries
172+
"""
173+
174+
result = attr.ib(validator=instance_of(bytes))
175+
header = attr.ib(validator=instance_of(MessageHeader))
176+
keyring_trace = attr.ib(validator=deep_iterable(member_validator=instance_of(KeyringTrace)))
177+
178+
def __attrs_post_init__(self):
179+
"""Construct the inner tuple for backwards compatibility."""
180+
self._legacy_container = (self.result, self.header)
181+
182+
def __len__(self):
183+
"""Emulate the inner tuple."""
184+
return self._legacy_container.__len__()
185+
186+
def __iter__(self):
187+
"""Emulate the inner tuple."""
188+
return self._legacy_container.__iter__()
189+
190+
def __getitem__(self, key):
191+
"""Emulate the inner tuple."""
192+
return self._legacy_container.__getitem__(key)

0 commit comments

Comments
 (0)