Skip to content

Commit f9aa29d

Browse files
committed
align cryptographic materials and add write-only interface
1 parent f254e73 commit f9aa29d

File tree

7 files changed

+638
-115
lines changed

7 files changed

+638
-115
lines changed

src/aws_encryption_sdk/exceptions.py

+14
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class InvalidDataKeyError(AWSEncryptionSDKClientError):
5353
"""Exception class for Invalid Data Keys."""
5454

5555

56+
class InvalidKeyringTraceError(AWSEncryptionSDKClientError):
57+
"""Exception class for invalid Keyring Traces.
58+
59+
.. versionadded:: 1.5.0
60+
"""
61+
62+
5663
class InvalidProviderIdError(AWSEncryptionSDKClientError):
5764
"""Exception class for Invalid Provider IDs."""
5865

@@ -73,6 +80,13 @@ class DecryptKeyError(AWSEncryptionSDKClientError):
7380
"""Exception class for errors encountered when MasterKeys try to decrypt data keys."""
7481

7582

83+
class SignatureKeyError(AWSEncryptionSDKClientError):
84+
"""Exception class for errors encountered with signing or verification keys.
85+
86+
.. versionadded:: 1.5.0
87+
"""
88+
89+
7690
class ActionNotAllowedError(AWSEncryptionSDKClientError):
7791
"""Exception class for errors encountered when attempting to perform unallowed actions."""
7892

src/aws_encryption_sdk/materials_managers/__init__.py

+236-43
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818
import six
1919
from attr.validators import deep_iterable, deep_mapping, instance_of, optional
2020

21-
from ..identifiers import Algorithm, KeyringTraceFlag
22-
from ..internal.utils.streams import ROStream
23-
from ..structures import DataKey, EncryptedDataKey, KeyringTrace
21+
from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError
22+
from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag
23+
from aws_encryption_sdk.internal.utils.streams import ROStream
24+
from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, RawDataKey
25+
26+
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
27+
from typing import Iterable, Union # noqa pylint: disable=unused-import
28+
except ImportError: # pragma: no cover
29+
# We only actually need these imports when running the mypy checks
30+
pass
2431

2532

2633
@attr.s(hash=False)
@@ -41,15 +48,24 @@ class EncryptionMaterialsRequest(object):
4148
:param int plaintext_length: Length of source plaintext (optional)
4249
"""
4350

44-
encryption_context = attr.ib(validator=attr.validators.instance_of(dict))
45-
frame_length = attr.ib(validator=attr.validators.instance_of(six.integer_types))
46-
plaintext_rostream = attr.ib(
47-
default=None, validator=attr.validators.optional(attr.validators.instance_of(ROStream))
48-
)
49-
algorithm = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(Algorithm)))
50-
plaintext_length = attr.ib(
51-
default=None, validator=attr.validators.optional(attr.validators.instance_of(six.integer_types))
51+
encryption_context = attr.ib(
52+
validator=deep_mapping(
53+
key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types)
54+
)
5255
)
56+
frame_length = attr.ib(validator=instance_of(six.integer_types))
57+
plaintext_rostream = attr.ib(default=None, validator=optional(instance_of(ROStream)))
58+
algorithm = attr.ib(default=None, validator=optional(instance_of(Algorithm)))
59+
plaintext_length = attr.ib(default=None, validator=optional(instance_of(six.integer_types)))
60+
61+
62+
def _data_key_to_raw_data_key(data_key):
63+
# type: (Union[DataKey, RawDataKey, None]) -> Union[RawDataKey, None]
64+
"""Convert a :class:`DataKey` into a :class:`RawDataKey`."""
65+
if isinstance(data_key, RawDataKey) or data_key is None:
66+
return data_key
67+
68+
return RawDataKey.from_data_key(data_key=data_key)
5369

5470

5571
@attr.s
@@ -73,13 +89,87 @@ class CryptographicMaterials(object):
7389
deep_mapping(key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types))
7490
)
7591
)
76-
data_encryption_key = attr.ib(default=None, validator=optional(instance_of(DataKey)))
77-
encrypted_data_keys = attr.ib(
78-
default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey)))
92+
data_encryption_key = attr.ib(
93+
default=None, validator=optional(instance_of(RawDataKey)), converter=_data_key_to_raw_data_key
7994
)
80-
keyring_trace = attr.ib(
95+
_keyring_trace = attr.ib(
8196
default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(KeyringTrace)))
8297
)
98+
_initialized = False
99+
100+
def __attrs_post_init__(self):
101+
"""Freeze attributes after initialization."""
102+
self._initialized = True
103+
104+
def __setattr__(self, key, value):
105+
"""Do not allow attributes to be changed once an instance is initialized."""
106+
if self._initialized:
107+
raise AttributeError("can't set attribute")
108+
109+
self._setattr(key, value)
110+
111+
def _setattr(self, key, value):
112+
"""Special __setattr__ to avoid having to perform multi-level super calls."""
113+
super(CryptographicMaterials, self).__setattr__(key, value)
114+
115+
def _validate_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags):
116+
# type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> None
117+
"""Validate that the provided data encryption key and keyring trace match for each other and the materials.
118+
119+
:param RawDataKey data_encryption_key: Data encryption key
120+
:param KeyringTrace keyring_trace: Keyring trace corresponding to data_encryption_key
121+
:param required_flags: Iterable of required flags
122+
:type required_flags: iterable of :class:`KeyringTraceFlag`
123+
:raises AttributeError: if data encryption key is already set
124+
:raises InvalidKeyringTraceError: if keyring trace does not match decrypt action
125+
:raises InvalidKeyringTraceError: if keyring trace does not match data key provider
126+
:raises InvalidDataKeyError: if data key length does not match algorithm suite
127+
"""
128+
if self.data_encryption_key is not None:
129+
raise AttributeError("Data encryption key is already set.")
130+
131+
for flag in required_flags:
132+
if flag not in keyring_trace.flags:
133+
raise InvalidKeyringTraceError("Keyring flags do not match action.")
134+
135+
if keyring_trace.wrapping_key != data_encryption_key.key_provider:
136+
raise InvalidKeyringTraceError("Keyring trace does not match data key provider.")
137+
138+
if len(data_encryption_key.data_key) != self.algorithm.kdf_input_len:
139+
raise InvalidDataKeyError(
140+
"Invalid data key length {actual} must be {expected}.".format(
141+
actual=len(data_encryption_key.data_key), expected=self.algorithm.kdf_input_len
142+
)
143+
)
144+
145+
def _add_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags):
146+
# type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> None
147+
"""Add a plaintext data encryption key.
148+
149+
:param RawDataKey data_encryption_key: Data encryption key
150+
:param KeyringTrace keyring_trace: Trace of actions that a keyring performed
151+
while getting this data encryption key
152+
:raises AttributeError: if data encryption key is already set
153+
:raises InvalidKeyringTraceError: if keyring trace does not match required actions
154+
:raises InvalidKeyringTraceError: if keyring trace does not match data key provider
155+
:raises InvalidDataKeyError: if data key length does not match algorithm suite
156+
"""
157+
self._validate_data_encryption_key(
158+
data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, required_flags=required_flags
159+
)
160+
161+
data_key = _data_key_to_raw_data_key(data_key=data_encryption_key)
162+
163+
super(CryptographicMaterials, self).__setattr__("data_encryption_key", data_key)
164+
self._keyring_trace.append(keyring_trace)
165+
166+
@property
167+
def keyring_trace(self):
168+
"""Return a read-only version of the keyring trace.
169+
170+
:rtype: tuple
171+
"""
172+
return tuple(self._keyring_trace)
83173

84174

85175
@attr.s(hash=False, init=False)
@@ -106,7 +196,10 @@ class EncryptionMaterials(CryptographicMaterials):
106196
:type keyring_trace: list of :class:`KeyringTrace`
107197
"""
108198

109-
signing_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes)))
199+
_encrypted_data_keys = attr.ib(
200+
default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey)))
201+
)
202+
signing_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes)))
110203

111204
def __init__(
112205
self,
@@ -116,7 +209,7 @@ def __init__(
116209
encryption_context=None,
117210
signing_key=None,
118211
**kwargs
119-
):
212+
): # noqa we define this in the class docstring
120213
if algorithm is None:
121214
raise TypeError("algorithm must not be None")
122215

@@ -127,12 +220,79 @@ def __init__(
127220
algorithm=algorithm,
128221
encryption_context=encryption_context,
129222
data_encryption_key=data_encryption_key,
130-
encrypted_data_keys=encrypted_data_keys,
131223
**kwargs
132224
)
133-
self.signing_key = signing_key
225+
self._setattr("signing_key", signing_key)
226+
self._setattr("_encrypted_data_keys", encrypted_data_keys)
134227
attr.validate(self)
135228

229+
@property
230+
def encrypted_data_keys(self):
231+
"""Return a read-only version of the encrypted data keys.
232+
233+
:rtype: frozenset
234+
"""
235+
return frozenset(self._encrypted_data_keys)
236+
237+
def add_data_encryption_key(self, data_encryption_key, keyring_trace):
238+
# type: (Union[DataKey, RawDataKey], KeyringTrace) -> None
239+
"""Add a plaintext data encryption key.
240+
241+
.. versionadded:: 1.5.0
242+
243+
:param RawDataKey data_encryption_key: Data encryption key
244+
:param KeyringTrace keyring_trace: Trace of actions that a keyring performed
245+
while getting this data encryption key
246+
:raises AttributeError: if data encryption key is already set
247+
:raises InvalidKeyringTraceError: if keyring trace does not match generate action
248+
:raises InvalidKeyringTraceError: if keyring trace does not match data key provider
249+
:raises InvalidDataKeyError: if data key length does not match algorithm suite
250+
"""
251+
self._add_data_encryption_key(
252+
data_encryption_key=data_encryption_key,
253+
keyring_trace=keyring_trace,
254+
required_flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY},
255+
)
256+
257+
def add_encrypted_data_key(self, encrypted_data_key, keyring_trace):
258+
# type: (EncryptedDataKey, KeyringTrace) -> None
259+
"""Add an encrypted data key with corresponding keyring trace.
260+
261+
.. versionadded:: 1.5.0
262+
263+
:param EncryptedDataKey encrypted_data_key: Encrypted data key to add
264+
:param KeyringTrace keyring_trace: Trace of actions that a keyring performed
265+
while getting this encrypted data key
266+
:raises InvalidKeyringTraceError: if keyring trace does not match generate action
267+
:raises InvalidKeyringTraceError: if keyring trace does not match data key encryptor
268+
"""
269+
if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY not in keyring_trace.flags:
270+
raise InvalidKeyringTraceError("Keyring flags do not match action.")
271+
272+
if keyring_trace.wrapping_key != encrypted_data_key.key_provider:
273+
raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.")
274+
275+
self._encrypted_data_keys.add(encrypted_data_key)
276+
self._keyring_trace.append(keyring_trace)
277+
278+
def add_signing_key(self, signing_key):
279+
# type: (bytes) -> None
280+
"""Add a signing key.
281+
282+
.. versionadded:: 1.5.0
283+
284+
:param bytes signing_key: Signing key
285+
:raises AttributeError: if signing key is already set
286+
:raises SignatureKeyError: if algorithm suite does not support signing keys
287+
"""
288+
if self.signing_key is not None:
289+
raise AttributeError("Signing key is already set.")
290+
291+
if self.algorithm.signing_algorithm_info is None:
292+
raise SignatureKeyError("Algorithm suite does not support signing keys.")
293+
294+
self._setattr("signing_key", signing_key)
295+
136296

137297
@attr.s(hash=False)
138298
class DecryptionMaterialsRequest(object):
@@ -147,9 +307,14 @@ class DecryptionMaterialsRequest(object):
147307
:param dict encryption_context: Encryption context to provide to master keys for underlying decrypt requests
148308
"""
149309

150-
algorithm = attr.ib(validator=attr.validators.instance_of(Algorithm))
151-
encrypted_data_keys = attr.ib(validator=attr.validators.instance_of(set))
152-
encryption_context = attr.ib(validator=attr.validators.instance_of(dict))
310+
algorithm = attr.ib(validator=instance_of(Algorithm))
311+
# TODO: Restrict this to only EncryptedDataKeys
312+
encrypted_data_keys = attr.ib(validator=deep_iterable(member_validator=instance_of((EncryptedDataKey, DataKey))))
313+
encryption_context = attr.ib(
314+
validator=deep_mapping(
315+
key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types)
316+
)
317+
)
153318

154319

155320
_DEFAULT_SENTINEL = object()
@@ -163,35 +328,33 @@ class DecryptionMaterials(CryptographicMaterials):
163328
164329
.. versionadded:: 1.5.0
165330
166-
The **algorithm**, **data_encryption_key**, **encrypted_data_keys**,
167-
**encryption_context**, and **keyring_trace** parameters.
331+
The **algorithm**, **data_encryption_key**, **encryption_context**, and **keyring_trace** parameters.
168332
169333
.. versionadded:: 1.5.0
170334
171335
All parameters are now optional.
172336
173337
:param Algorithm algorithm: Algorithm to use for encrypting message (optional)
174338
:param DataKey data_encryption_key: Plaintext data key to use for encrypting message (optional)
175-
:param encrypted_data_keys: List of encrypted data keys (optional)
176-
:type encrypted_data_keys: list of :class:`EncryptedDataKey`
177339
:param dict encryption_context: Encryption context tied to `encrypted_data_keys` (optional)
178340
:param bytes verification_key: Raw signature verification key (optional)
179341
:param keyring_trace: Any KeyRing trace entries (optional)
180342
:type keyring_trace: list of :class:`KeyringTrace`
181343
"""
182344

183-
verification_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes)))
345+
verification_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes)))
184346

185-
def __init__(self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs):
186-
if any(
187-
(
188-
data_key is _DEFAULT_SENTINEL and "data_encryption_key" not in kwargs,
189-
data_key is not _DEFAULT_SENTINEL and "data_encryption_key" in kwargs,
190-
)
191-
):
192-
raise TypeError("Exactly one of data_key or data_encryption_key must be set")
347+
def __init__(
348+
self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs
349+
): # noqa we define this in the class docstring
193350

194-
if data_key is not _DEFAULT_SENTINEL and "data_encryption_key" not in kwargs:
351+
legacy_data_key_set = data_key is not _DEFAULT_SENTINEL
352+
data_encryption_key_set = "data_encryption_key" in kwargs
353+
354+
if legacy_data_key_set and data_encryption_key_set:
355+
raise TypeError("Either data_key or data_encryption_key can be used but not both")
356+
357+
if legacy_data_key_set and not data_encryption_key_set:
195358
kwargs["data_encryption_key"] = data_key
196359

197360
for legacy_missing in ("algorithm", "encryption_context"):
@@ -200,16 +363,46 @@ def __init__(self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs):
200363

201364
super(DecryptionMaterials, self).__init__(**kwargs)
202365

203-
self.verification_key = verification_key
366+
self._setattr("verification_key", verification_key)
204367
attr.validate(self)
205368

206369
@property
207370
def data_key(self):
208-
"""Backwards-compatible shim."""
371+
"""Backwards-compatible shim for access to data key."""
209372
return self.data_encryption_key
210373

211-
@data_key.setter
212-
def data_key(self, value):
213-
# type: (DataKey) -> None
214-
"""Backwards-compatible shim."""
215-
self.data_encryption_key = value
374+
def add_data_encryption_key(self, data_encryption_key, keyring_trace):
375+
# type: (Union[DataKey, RawDataKey], KeyringTrace) -> None
376+
"""Add a plaintext data encryption key.
377+
378+
.. versionadded:: 1.5.0
379+
380+
:param RawDataKey data_encryption_key: Data encryption key
381+
:param KeyringTrace keyring_trace: Trace of actions that a keyring performed
382+
while getting this data encryption key
383+
:raises AttributeError: if data encryption key is already set
384+
:raises InvalidKeyringTraceError: if keyring trace does not match decrypt action
385+
:raises InvalidKeyringTraceError: if keyring trace does not match data key provider
386+
:raises InvalidDataKeyError: if data key length does not match algorithm suite
387+
"""
388+
self._add_data_encryption_key(
389+
data_encryption_key=data_encryption_key,
390+
keyring_trace=keyring_trace,
391+
required_flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY},
392+
)
393+
394+
def add_verification_key(self, verification_key):
395+
# type: (bytes) -> None
396+
"""Add a verification key.
397+
398+
.. versionadded:: 1.5.0
399+
400+
:param bytes verification_key: Verification key
401+
"""
402+
if self.verification_key is not None:
403+
raise AttributeError("Verification key is already set.")
404+
405+
if self.algorithm.signing_algorithm_info is None:
406+
raise SignatureKeyError("Algorithm suite does not support signing keys.")
407+
408+
self._setattr("verification_key", verification_key)

0 commit comments

Comments
 (0)