From a3d947d88c674825a64375008af054a0c0b56ad0 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 20 Jun 2019 17:36:09 -0700 Subject: [PATCH 01/22] bump attrs to 19.1.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f04114485..67be39a90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ boto3>=1.4.4 cryptography>=1.8.1 -attrs>=17.4.0 +attrs>=19.1.0 wrapt>=1.10.11 \ No newline at end of file From daaacba44fc9a1b7a60b099a45c3349097942b70 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 20 Jun 2019 17:36:54 -0700 Subject: [PATCH 02/22] add keyring trace and integrate into updated encrytion/decryption materials --- src/aws_encryption_sdk/identifiers.py | 10 ++ .../materials_managers/__init__.py | 150 +++++++++++++++--- src/aws_encryption_sdk/structures.py | 16 ++ test/unit/test_material_managers.py | 76 +++++++-- test/unit/test_material_managers_default.py | 6 +- 5 files changed, 217 insertions(+), 41 deletions(-) diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 1bd9bb1f1..0add9724d 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -328,3 +328,13 @@ class ContentAADString(Enum): FRAME_STRING_ID = b"AWSKMSEncryptionClient Frame" FINAL_FRAME_STRING_ID = b"AWSKMSEncryptionClient Final Frame" NON_FRAMED_STRING_ID = b"AWSKMSEncryptionClient Single Block" + + +class KeyRingTraceFlag(Enum): + """KeyRing Trace actions.""" + + WRAPPING_KEY_GENERATED_DATA_KEY = 1 + WRAPPING_KEY_ENCRYPTED_DATA_KEY = 2 + WRAPPING_KEY_DECRYPTED_DATA_KEY = 3 + WRAPPING_KEY_SIGNED_ENC_CTX = 4 + WRAPPING_KEY_VERIFIED_ENC_CTX = 5 diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index bc5230c51..18ebd7898 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -16,10 +16,11 @@ """ import attr import six +from attr.validators import deep_iterable, deep_mapping, instance_of, optional -from ..identifiers import Algorithm +from ..identifiers import Algorithm, KeyRingTraceFlag from ..internal.utils.streams import ROStream -from ..structures import DataKey +from ..structures import DataKey, EncryptedDataKey, KeyRingTrace @attr.s(hash=False) @@ -51,28 +52,87 @@ class EncryptionMaterialsRequest(object): ) -@attr.s(hash=False) -class EncryptionMaterials(object): +@attr.s +class CryptographicMaterials(object): + """Cryptographic materials core. + + .. versionadded:: 1.5.0 + + :param Algorithm algorithm: Algorithm to use for encrypting message + :param dict encryption_context: Encryption context tied to `encrypted_data_keys` + :param DataKey data_encryption_key: Plaintext data key to use for encrypting message + :param encrypted_data_keys: List of encrypted data keys + :type encrypted_data_keys: list of :class:`EncryptedDataKey` + :param keyring_trace: Any KeyRing trace entries + :type keyring_trace: list of :class:`KeyRingTrace` + """ + + algorithm = attr.ib(validator=optional(instance_of(Algorithm))) + encryption_context = attr.ib( + validator=optional( + deep_mapping(key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types)) + ) + ) + data_encryption_key = attr.ib(default=None, validator=optional(instance_of(DataKey))) + encrypted_data_keys = attr.ib( + default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey))) + ) + keyring_trace = attr.ib( + default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(KeyRingTrace))) + ) + + +@attr.s(hash=False, init=False) +class EncryptionMaterials(CryptographicMaterials): """Encryption materials returned by a crypto material manager's `get_encryption_materials` method. .. versionadded:: 1.3.0 - :param algorithm: Algorithm to use for encrypting message - :type algorithm: aws_encryption_sdk.identifiers.Algorithm - :param data_encryption_key: Plaintext data key to use for encrypting message - :type data_encryption_key: aws_encryption_sdk.structures.DataKey - :param encrypted_data_keys: List of encrypted data keys - :type encrypted_data_keys: list of `aws_encryption_sdk.structures.EncryptedDataKey` + .. versionadded:: 1.5.0 + + The **keyring_trace** parameter. + + .. versionadded:: 1.5.0 + + Most parameters are now optional. + + :param Algorithm algorithm: Algorithm to use for encrypting message + :param DataKey data_encryption_key: Plaintext data key to use for encrypting message (optional) + :param encrypted_data_keys: List of encrypted data keys (optional) + :type encrypted_data_keys: list of :class:`EncryptedDataKey` :param dict encryption_context: Encryption context tied to `encrypted_data_keys` - :param bytes signing_key: Encoded signing key + :param bytes signing_key: Encoded signing key (optional) + :param keyring_trace: Any KeyRing trace entries (optional) + :type keyring_trace: list of :class:`KeyRingTrace` """ - algorithm = attr.ib(validator=attr.validators.instance_of(Algorithm)) - data_encryption_key = attr.ib(validator=attr.validators.instance_of(DataKey)) - encrypted_data_keys = attr.ib(validator=attr.validators.instance_of(set)) - encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) signing_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) + def __init__( + self, + algorithm=None, + data_encryption_key=None, + encrypted_data_keys=None, + encryption_context=None, + signing_key=None, + **kwargs + ): + if algorithm is None: + raise TypeError("algorithm must not be None") + + if encryption_context is None: + raise TypeError("encryption_context must not be None") + + super(EncryptionMaterials, self).__init__( + algorithm=algorithm, + encryption_context=encryption_context, + data_encryption_key=data_encryption_key, + encrypted_data_keys=encrypted_data_keys, + **kwargs + ) + self.signing_key = signing_key + attr.validate(self) + @attr.s(hash=False) class DecryptionMaterialsRequest(object): @@ -92,16 +152,64 @@ class DecryptionMaterialsRequest(object): encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) -@attr.s(hash=False) -class DecryptionMaterials(object): +_DEFAULT_SENTINEL = object() + + +@attr.s(hash=False, init=False) +class DecryptionMaterials(CryptographicMaterials): """Decryption materials returned by a crypto material manager's `decrypt_materials` method. .. versionadded:: 1.3.0 - :param data_key: Plaintext data key to use with message decryption - :type data_key: aws_encryption_sdk.structures.DataKey - :param bytes verification_key: Raw signature verification key + .. versionadded:: 1.5.0 + + The **algorithm**, **data_encryption_key**, **encrypted_data_keys**, + **encryption_context**, and **keyring_trace** parameters. + + .. versionadded:: 1.5.0 + + All parameters are now optional. + + :param Algorithm algorithm: Algorithm to use for encrypting message (optional) + :param DataKey data_encryption_key: Plaintext data key to use for encrypting message (optional) + :param encrypted_data_keys: List of encrypted data keys (optional) + :type encrypted_data_keys: list of :class:`EncryptedDataKey` + :param dict encryption_context: Encryption context tied to `encrypted_data_keys` (optional) + :param bytes verification_key: Raw signature verification key (optional) + :param keyring_trace: Any KeyRing trace entries (optional) + :type keyring_trace: list of :class:`KeyRingTrace` """ - data_key = attr.ib(validator=attr.validators.instance_of(DataKey)) verification_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) + + def __init__(self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs): + if any( + ( + data_key is _DEFAULT_SENTINEL and "data_encryption_key" not in kwargs, + data_key is not _DEFAULT_SENTINEL and "data_encryption_key" in kwargs, + ) + ): + raise TypeError("Exactly one of data_key or data_encryption_key must be set") + + if data_key is not _DEFAULT_SENTINEL and "data_encryption_key" not in kwargs: + kwargs["data_encryption_key"] = data_key + + for legacy_missing in ("algorithm", "encryption_context"): + if legacy_missing not in kwargs: + kwargs[legacy_missing] = None + + super(DecryptionMaterials, self).__init__(**kwargs) + + self.verification_key = verification_key + attr.validate(self) + + @property + def data_key(self): + """Backwards-compatible shim.""" + return self.data_encryption_key + + @data_key.setter + def data_key(self, value): + # type: (DataKey) -> None + """Backwards-compatible shim.""" + self.data_encryption_key = value diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index 8229d65fb..86b87b932 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -13,6 +13,7 @@ """Public data structures for aws_encryption_sdk.""" import attr import six +from attr.validators import deep_iterable, instance_of import aws_encryption_sdk.identifiers from aws_encryption_sdk.internal.str_ops import to_bytes, to_str @@ -104,3 +105,18 @@ class EncryptedDataKey(object): key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) encrypted_data_key = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) + + +@attr.s +class KeyRingTrace(object): + """Record of all actions that a KeyRing performed with a wrapping key. + + :param MasterKeyInfo wrapping_key: Wrapping key used + :param flags: Actions performed + :type flags: set of :class:`KeyRingTraceFlag` + """ + + wrapping_key = attr.ib(validator=instance_of(MasterKeyInfo)) + flags = attr.ib( + validator=deep_iterable(member_validator=instance_of(aws_encryption_sdk.identifiers.KeyRingTraceFlag)) + ) diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index fcd4977f5..9eb32a125 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -12,66 +12,93 @@ # language governing permissions and limitations under the License. """Test suite for aws_encryption_sdk.materials_managers""" import pytest -from mock import MagicMock +from mock import MagicMock, sentinel from pytest_mock import mocker # noqa pylint: disable=unused-import -from aws_encryption_sdk.identifiers import Algorithm +from aws_encryption_sdk.identifiers import KeyRingTraceFlag +from aws_encryption_sdk.internal.defaults import ALGORITHM from aws_encryption_sdk.internal.utils.streams import ROStream from aws_encryption_sdk.materials_managers import ( + CryptographicMaterials, DecryptionMaterials, DecryptionMaterialsRequest, EncryptionMaterials, EncryptionMaterialsRequest, ) -from aws_encryption_sdk.structures import DataKey +from aws_encryption_sdk.structures import DataKey, KeyRingTrace, MasterKeyInfo pytestmark = [pytest.mark.unit, pytest.mark.local] +_DATA_KEY = DataKey( + key_provider=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), + data_key=b"1234567890123456789012", + encrypted_data_key=b"asdf", +) _VALID_KWARGS = { + "CryptographicMaterials": dict( + algorithm=ALGORITHM, + encryption_context={"additional": "data"}, + data_encryption_key=_DATA_KEY, + encrypted_data_keys=[], + keyring_trace=[ + KeyRingTrace( + wrapping_key=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), + flags={KeyRingTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + ], + ), "EncryptionMaterialsRequest": dict( encryption_context={}, plaintext_rostream=MagicMock(__class__=ROStream), frame_length=5, - algorithm=MagicMock(__class__=Algorithm), + algorithm=ALGORITHM, plaintext_length=5, ), "EncryptionMaterials": dict( - algorithm=MagicMock(__class__=Algorithm), - data_encryption_key=MagicMock(__class__=DataKey), + algorithm=ALGORITHM, + data_encryption_key=_DATA_KEY, encrypted_data_keys=set([]), encryption_context={}, signing_key=b"", ), - "DecryptionMaterialsRequest": dict( - algorithm=MagicMock(__class__=Algorithm), encrypted_data_keys=set([]), encryption_context={} + "DecryptionMaterialsRequest": dict(algorithm=ALGORITHM, encrypted_data_keys=set([]), encryption_context={}), + "DecryptionMaterials": dict( + data_key=_DATA_KEY, verification_key=b"ex_verification_key", algorithm=ALGORITHM, encryption_context={} ), - "DecryptionMaterials": dict(data_key=MagicMock(__class__=DataKey), verification_key=b"ex_verification_key"), } +_REMOVE = object() @pytest.mark.parametrize( "attr_class, invalid_kwargs", ( + (CryptographicMaterials, dict(algorithm=1234)), + (CryptographicMaterials, dict(encryption_context=1234)), + (CryptographicMaterials, dict(data_encryption_key=1234)), + (CryptographicMaterials, dict(encrypted_data_keys=1234)), + (CryptographicMaterials, dict(keyring_trace=1234)), (EncryptionMaterialsRequest, dict(encryption_context=None)), (EncryptionMaterialsRequest, dict(frame_length="not an int")), (EncryptionMaterialsRequest, dict(algorithm="not an Algorithm or None")), (EncryptionMaterialsRequest, dict(plaintext_length="not an int or None")), (EncryptionMaterials, dict(algorithm=None)), - (EncryptionMaterials, dict(data_encryption_key=None)), - (EncryptionMaterials, dict(encrypted_data_keys=None)), (EncryptionMaterials, dict(encryption_context=None)), (EncryptionMaterials, dict(signing_key=u"not bytes or None")), (DecryptionMaterialsRequest, dict(algorithm=None)), (DecryptionMaterialsRequest, dict(encrypted_data_keys=None)), (DecryptionMaterialsRequest, dict(encryption_context=None)), - (DecryptionMaterials, dict(data_key=None)), (DecryptionMaterials, dict(verification_key=5555)), + (DecryptionMaterials, dict(data_key=_DATA_KEY, data_encryption_key=_DATA_KEY)), + (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_REMOVE)), ), ) def test_attributes_fails(attr_class, invalid_kwargs): kwargs = _VALID_KWARGS[attr_class.__name__].copy() kwargs.update(invalid_kwargs) + purge_keys = [key for key, val in kwargs.items() if val is _REMOVE] + for key in purge_keys: + del kwargs[key] with pytest.raises(TypeError): attr_class(**kwargs) @@ -85,14 +112,29 @@ def test_encryption_materials_request_attributes_defaults(): def test_encryption_materials_defaults(): test = EncryptionMaterials( - algorithm=MagicMock(__class__=Algorithm), - data_encryption_key=MagicMock(__class__=DataKey), - encrypted_data_keys=set([]), - encryption_context={}, + algorithm=ALGORITHM, data_encryption_key=_DATA_KEY, encrypted_data_keys=set([]), encryption_context={} ) assert test.signing_key is None def test_decryption_materials_defaults(): - test = DecryptionMaterials(data_key=MagicMock(__class__=DataKey)) + test = DecryptionMaterials(data_key=_DATA_KEY) assert test.verification_key is None + assert test.algorithm is None + assert test.encryption_context is None + + +def test_decryption_materials_legacy_data_key_get(): + test = DecryptionMaterials(data_encryption_key=_DATA_KEY) + + assert test.data_encryption_key is _DATA_KEY + assert test.data_key is _DATA_KEY + + +def test_decryption_materials_legacy_data_key_set(): + test = DecryptionMaterials(data_encryption_key=_DATA_KEY) + + test.data_key = sentinel.data_key + + assert test.data_encryption_key is sentinel.data_key + assert test.data_key is sentinel.data_key diff --git a/test/unit/test_material_managers_default.py b/test/unit/test_material_managers_default.py index 9d6bd949f..6eeef525b 100644 --- a/test/unit/test_material_managers_default.py +++ b/test/unit/test_material_managers_default.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.materials_managers import EncryptionMaterials from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager -from aws_encryption_sdk.structures import DataKey +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -34,7 +34,7 @@ def patch_for_dcmm_encrypt(mocker): DefaultCryptoMaterialsManager._generate_signing_key_and_update_encryption_context.return_value = mock_signing_key mocker.patch.object(aws_encryption_sdk.materials_managers.default, "prepare_data_keys") mock_data_encryption_key = MagicMock(__class__=DataKey) - mock_encrypted_data_keys = set([mock_data_encryption_key]) + mock_encrypted_data_keys = set([MagicMock(__class__=EncryptedDataKey)]) result_pair = mock_data_encryption_key, mock_encrypted_data_keys aws_encryption_sdk.materials_managers.default.prepare_data_keys.return_value = result_pair yield result_pair, mock_signing_key @@ -128,7 +128,7 @@ def test_get_encryption_materials(patch_for_dcmm_encrypt): assert isinstance(test, EncryptionMaterials) assert test.algorithm is cmm.algorithm assert test.data_encryption_key is patch_for_dcmm_encrypt[0][0] - assert test.encrypted_data_keys is patch_for_dcmm_encrypt[0][1] + assert test.encrypted_data_keys == patch_for_dcmm_encrypt[0][1] assert test.encryption_context == encryption_context assert test.signing_key == patch_for_dcmm_encrypt[1] From f254e73fe92bbbd37a655233e30eb407c2b291cd Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 20 Jun 2019 17:40:06 -0700 Subject: [PATCH 03/22] s/KeyRing/Keyring/g --- src/aws_encryption_sdk/identifiers.py | 2 +- .../materials_managers/__init__.py | 12 ++++++------ src/aws_encryption_sdk/structures.py | 6 +++--- test/unit/test_material_managers.py | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 0add9724d..0bec8aacd 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -330,7 +330,7 @@ class ContentAADString(Enum): NON_FRAMED_STRING_ID = b"AWSKMSEncryptionClient Single Block" -class KeyRingTraceFlag(Enum): +class KeyringTraceFlag(Enum): """KeyRing Trace actions.""" WRAPPING_KEY_GENERATED_DATA_KEY = 1 diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 18ebd7898..94fd30ece 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -18,9 +18,9 @@ import six from attr.validators import deep_iterable, deep_mapping, instance_of, optional -from ..identifiers import Algorithm, KeyRingTraceFlag +from ..identifiers import Algorithm, KeyringTraceFlag from ..internal.utils.streams import ROStream -from ..structures import DataKey, EncryptedDataKey, KeyRingTrace +from ..structures import DataKey, EncryptedDataKey, KeyringTrace @attr.s(hash=False) @@ -64,7 +64,7 @@ class CryptographicMaterials(object): :param encrypted_data_keys: List of encrypted data keys :type encrypted_data_keys: list of :class:`EncryptedDataKey` :param keyring_trace: Any KeyRing trace entries - :type keyring_trace: list of :class:`KeyRingTrace` + :type keyring_trace: list of :class:`KeyringTrace` """ algorithm = attr.ib(validator=optional(instance_of(Algorithm))) @@ -78,7 +78,7 @@ class CryptographicMaterials(object): default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey))) ) keyring_trace = attr.ib( - default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(KeyRingTrace))) + default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(KeyringTrace))) ) @@ -103,7 +103,7 @@ class EncryptionMaterials(CryptographicMaterials): :param dict encryption_context: Encryption context tied to `encrypted_data_keys` :param bytes signing_key: Encoded signing key (optional) :param keyring_trace: Any KeyRing trace entries (optional) - :type keyring_trace: list of :class:`KeyRingTrace` + :type keyring_trace: list of :class:`KeyringTrace` """ signing_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) @@ -177,7 +177,7 @@ class DecryptionMaterials(CryptographicMaterials): :param dict encryption_context: Encryption context tied to `encrypted_data_keys` (optional) :param bytes verification_key: Raw signature verification key (optional) :param keyring_trace: Any KeyRing trace entries (optional) - :type keyring_trace: list of :class:`KeyRingTrace` + :type keyring_trace: list of :class:`KeyringTrace` """ verification_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index 86b87b932..aed2f9163 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -108,15 +108,15 @@ class EncryptedDataKey(object): @attr.s -class KeyRingTrace(object): +class KeyringTrace(object): """Record of all actions that a KeyRing performed with a wrapping key. :param MasterKeyInfo wrapping_key: Wrapping key used :param flags: Actions performed - :type flags: set of :class:`KeyRingTraceFlag` + :type flags: set of :class:`KeyringTraceFlag` """ wrapping_key = attr.ib(validator=instance_of(MasterKeyInfo)) flags = attr.ib( - validator=deep_iterable(member_validator=instance_of(aws_encryption_sdk.identifiers.KeyRingTraceFlag)) + validator=deep_iterable(member_validator=instance_of(aws_encryption_sdk.identifiers.KeyringTraceFlag)) ) diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index 9eb32a125..fa216c8d8 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -15,7 +15,7 @@ from mock import MagicMock, sentinel from pytest_mock import mocker # noqa pylint: disable=unused-import -from aws_encryption_sdk.identifiers import KeyRingTraceFlag +from aws_encryption_sdk.identifiers import KeyringTraceFlag from aws_encryption_sdk.internal.defaults import ALGORITHM from aws_encryption_sdk.internal.utils.streams import ROStream from aws_encryption_sdk.materials_managers import ( @@ -25,7 +25,7 @@ EncryptionMaterials, EncryptionMaterialsRequest, ) -from aws_encryption_sdk.structures import DataKey, KeyRingTrace, MasterKeyInfo +from aws_encryption_sdk.structures import DataKey, KeyringTrace, MasterKeyInfo pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -42,9 +42,9 @@ data_encryption_key=_DATA_KEY, encrypted_data_keys=[], keyring_trace=[ - KeyRingTrace( + KeyringTrace( wrapping_key=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), - flags={KeyRingTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, ) ], ), From f9aa29dcdbd19902edcc693ceae44420686368d9 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 26 Jun 2019 20:10:05 -0700 Subject: [PATCH 04/22] align cryptographic materials and add write-only interface --- src/aws_encryption_sdk/exceptions.py | 14 + .../materials_managers/__init__.py | 279 ++++++++++++++--- src/aws_encryption_sdk/structures.py | 130 +++++--- test/unit/test_material_managers.py | 282 ++++++++++++++++-- test/unit/test_material_managers_default.py | 19 +- .../test_streaming_client_stream_encryptor.py | 2 +- test/unit/test_structures.py | 27 ++ 7 files changed, 638 insertions(+), 115 deletions(-) diff --git a/src/aws_encryption_sdk/exceptions.py b/src/aws_encryption_sdk/exceptions.py index a71d414c0..cd60ab6bd 100644 --- a/src/aws_encryption_sdk/exceptions.py +++ b/src/aws_encryption_sdk/exceptions.py @@ -53,6 +53,13 @@ class InvalidDataKeyError(AWSEncryptionSDKClientError): """Exception class for Invalid Data Keys.""" +class InvalidKeyringTraceError(AWSEncryptionSDKClientError): + """Exception class for invalid Keyring Traces. + + .. versionadded:: 1.5.0 + """ + + class InvalidProviderIdError(AWSEncryptionSDKClientError): """Exception class for Invalid Provider IDs.""" @@ -73,6 +80,13 @@ class DecryptKeyError(AWSEncryptionSDKClientError): """Exception class for errors encountered when MasterKeys try to decrypt data keys.""" +class SignatureKeyError(AWSEncryptionSDKClientError): + """Exception class for errors encountered with signing or verification keys. + + .. versionadded:: 1.5.0 + """ + + class ActionNotAllowedError(AWSEncryptionSDKClientError): """Exception class for errors encountered when attempting to perform unallowed actions.""" diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 94fd30ece..75589cc83 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -18,9 +18,16 @@ import six from attr.validators import deep_iterable, deep_mapping, instance_of, optional -from ..identifiers import Algorithm, KeyringTraceFlag -from ..internal.utils.streams import ROStream -from ..structures import DataKey, EncryptedDataKey, KeyringTrace +from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError +from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag +from aws_encryption_sdk.internal.utils.streams import ROStream +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, RawDataKey + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable, Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass @attr.s(hash=False) @@ -41,15 +48,24 @@ class EncryptionMaterialsRequest(object): :param int plaintext_length: Length of source plaintext (optional) """ - encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) - frame_length = attr.ib(validator=attr.validators.instance_of(six.integer_types)) - plaintext_rostream = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(ROStream)) - ) - algorithm = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(Algorithm))) - plaintext_length = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(six.integer_types)) + encryption_context = attr.ib( + validator=deep_mapping( + key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) + ) ) + frame_length = attr.ib(validator=instance_of(six.integer_types)) + plaintext_rostream = attr.ib(default=None, validator=optional(instance_of(ROStream))) + algorithm = attr.ib(default=None, validator=optional(instance_of(Algorithm))) + plaintext_length = attr.ib(default=None, validator=optional(instance_of(six.integer_types))) + + +def _data_key_to_raw_data_key(data_key): + # type: (Union[DataKey, RawDataKey, None]) -> Union[RawDataKey, None] + """Convert a :class:`DataKey` into a :class:`RawDataKey`.""" + if isinstance(data_key, RawDataKey) or data_key is None: + return data_key + + return RawDataKey.from_data_key(data_key=data_key) @attr.s @@ -73,13 +89,87 @@ class CryptographicMaterials(object): deep_mapping(key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types)) ) ) - data_encryption_key = attr.ib(default=None, validator=optional(instance_of(DataKey))) - encrypted_data_keys = attr.ib( - default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey))) + data_encryption_key = attr.ib( + default=None, validator=optional(instance_of(RawDataKey)), converter=_data_key_to_raw_data_key ) - keyring_trace = attr.ib( + _keyring_trace = attr.ib( default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(KeyringTrace))) ) + _initialized = False + + def __attrs_post_init__(self): + """Freeze attributes after initialization.""" + self._initialized = True + + def __setattr__(self, key, value): + """Do not allow attributes to be changed once an instance is initialized.""" + if self._initialized: + raise AttributeError("can't set attribute") + + self._setattr(key, value) + + def _setattr(self, key, value): + """Special __setattr__ to avoid having to perform multi-level super calls.""" + super(CryptographicMaterials, self).__setattr__(key, value) + + def _validate_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags): + # type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> None + """Validate that the provided data encryption key and keyring trace match for each other and the materials. + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Keyring trace corresponding to data_encryption_key + :param required_flags: Iterable of required flags + :type required_flags: iterable of :class:`KeyringTraceFlag` + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match decrypt action + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + if self.data_encryption_key is not None: + raise AttributeError("Data encryption key is already set.") + + for flag in required_flags: + if flag not in keyring_trace.flags: + raise InvalidKeyringTraceError("Keyring flags do not match action.") + + if keyring_trace.wrapping_key != data_encryption_key.key_provider: + raise InvalidKeyringTraceError("Keyring trace does not match data key provider.") + + if len(data_encryption_key.data_key) != self.algorithm.kdf_input_len: + raise InvalidDataKeyError( + "Invalid data key length {actual} must be {expected}.".format( + actual=len(data_encryption_key.data_key), expected=self.algorithm.kdf_input_len + ) + ) + + def _add_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags): + # type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> None + """Add a plaintext data encryption key. + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this data encryption key + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match required actions + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + self._validate_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, required_flags=required_flags + ) + + data_key = _data_key_to_raw_data_key(data_key=data_encryption_key) + + super(CryptographicMaterials, self).__setattr__("data_encryption_key", data_key) + self._keyring_trace.append(keyring_trace) + + @property + def keyring_trace(self): + """Return a read-only version of the keyring trace. + + :rtype: tuple + """ + return tuple(self._keyring_trace) @attr.s(hash=False, init=False) @@ -106,7 +196,10 @@ class EncryptionMaterials(CryptographicMaterials): :type keyring_trace: list of :class:`KeyringTrace` """ - signing_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) + _encrypted_data_keys = attr.ib( + default=attr.Factory(list), validator=optional(deep_iterable(member_validator=instance_of(EncryptedDataKey))) + ) + signing_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes))) def __init__( self, @@ -116,7 +209,7 @@ def __init__( encryption_context=None, signing_key=None, **kwargs - ): + ): # noqa we define this in the class docstring if algorithm is None: raise TypeError("algorithm must not be None") @@ -127,12 +220,79 @@ def __init__( algorithm=algorithm, encryption_context=encryption_context, data_encryption_key=data_encryption_key, - encrypted_data_keys=encrypted_data_keys, **kwargs ) - self.signing_key = signing_key + self._setattr("signing_key", signing_key) + self._setattr("_encrypted_data_keys", encrypted_data_keys) attr.validate(self) + @property + def encrypted_data_keys(self): + """Return a read-only version of the encrypted data keys. + + :rtype: frozenset + """ + return frozenset(self._encrypted_data_keys) + + def add_data_encryption_key(self, data_encryption_key, keyring_trace): + # type: (Union[DataKey, RawDataKey], KeyringTrace) -> None + """Add a plaintext data encryption key. + + .. versionadded:: 1.5.0 + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this data encryption key + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match generate action + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + self._add_data_encryption_key( + data_encryption_key=data_encryption_key, + keyring_trace=keyring_trace, + required_flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + + def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): + # type: (EncryptedDataKey, KeyringTrace) -> None + """Add an encrypted data key with corresponding keyring trace. + + .. versionadded:: 1.5.0 + + :param EncryptedDataKey encrypted_data_key: Encrypted data key to add + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this encrypted data key + :raises InvalidKeyringTraceError: if keyring trace does not match generate action + :raises InvalidKeyringTraceError: if keyring trace does not match data key encryptor + """ + if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY not in keyring_trace.flags: + raise InvalidKeyringTraceError("Keyring flags do not match action.") + + if keyring_trace.wrapping_key != encrypted_data_key.key_provider: + raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.") + + self._encrypted_data_keys.add(encrypted_data_key) + self._keyring_trace.append(keyring_trace) + + def add_signing_key(self, signing_key): + # type: (bytes) -> None + """Add a signing key. + + .. versionadded:: 1.5.0 + + :param bytes signing_key: Signing key + :raises AttributeError: if signing key is already set + :raises SignatureKeyError: if algorithm suite does not support signing keys + """ + if self.signing_key is not None: + raise AttributeError("Signing key is already set.") + + if self.algorithm.signing_algorithm_info is None: + raise SignatureKeyError("Algorithm suite does not support signing keys.") + + self._setattr("signing_key", signing_key) + @attr.s(hash=False) class DecryptionMaterialsRequest(object): @@ -147,9 +307,14 @@ class DecryptionMaterialsRequest(object): :param dict encryption_context: Encryption context to provide to master keys for underlying decrypt requests """ - algorithm = attr.ib(validator=attr.validators.instance_of(Algorithm)) - encrypted_data_keys = attr.ib(validator=attr.validators.instance_of(set)) - encryption_context = attr.ib(validator=attr.validators.instance_of(dict)) + algorithm = attr.ib(validator=instance_of(Algorithm)) + # TODO: Restrict this to only EncryptedDataKeys + encrypted_data_keys = attr.ib(validator=deep_iterable(member_validator=instance_of((EncryptedDataKey, DataKey)))) + encryption_context = attr.ib( + validator=deep_mapping( + key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) + ) + ) _DEFAULT_SENTINEL = object() @@ -163,8 +328,7 @@ class DecryptionMaterials(CryptographicMaterials): .. versionadded:: 1.5.0 - The **algorithm**, **data_encryption_key**, **encrypted_data_keys**, - **encryption_context**, and **keyring_trace** parameters. + The **algorithm**, **data_encryption_key**, **encryption_context**, and **keyring_trace** parameters. .. versionadded:: 1.5.0 @@ -172,26 +336,25 @@ class DecryptionMaterials(CryptographicMaterials): :param Algorithm algorithm: Algorithm to use for encrypting message (optional) :param DataKey data_encryption_key: Plaintext data key to use for encrypting message (optional) - :param encrypted_data_keys: List of encrypted data keys (optional) - :type encrypted_data_keys: list of :class:`EncryptedDataKey` :param dict encryption_context: Encryption context tied to `encrypted_data_keys` (optional) :param bytes verification_key: Raw signature verification key (optional) :param keyring_trace: Any KeyRing trace entries (optional) :type keyring_trace: list of :class:`KeyringTrace` """ - verification_key = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(bytes))) + verification_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes))) - def __init__(self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs): - if any( - ( - data_key is _DEFAULT_SENTINEL and "data_encryption_key" not in kwargs, - data_key is not _DEFAULT_SENTINEL and "data_encryption_key" in kwargs, - ) - ): - raise TypeError("Exactly one of data_key or data_encryption_key must be set") + def __init__( + self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs + ): # noqa we define this in the class docstring - if data_key is not _DEFAULT_SENTINEL and "data_encryption_key" not in kwargs: + legacy_data_key_set = data_key is not _DEFAULT_SENTINEL + data_encryption_key_set = "data_encryption_key" in kwargs + + if legacy_data_key_set and data_encryption_key_set: + raise TypeError("Either data_key or data_encryption_key can be used but not both") + + if legacy_data_key_set and not data_encryption_key_set: kwargs["data_encryption_key"] = data_key for legacy_missing in ("algorithm", "encryption_context"): @@ -200,16 +363,46 @@ def __init__(self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs): super(DecryptionMaterials, self).__init__(**kwargs) - self.verification_key = verification_key + self._setattr("verification_key", verification_key) attr.validate(self) @property def data_key(self): - """Backwards-compatible shim.""" + """Backwards-compatible shim for access to data key.""" return self.data_encryption_key - @data_key.setter - def data_key(self, value): - # type: (DataKey) -> None - """Backwards-compatible shim.""" - self.data_encryption_key = value + def add_data_encryption_key(self, data_encryption_key, keyring_trace): + # type: (Union[DataKey, RawDataKey], KeyringTrace) -> None + """Add a plaintext data encryption key. + + .. versionadded:: 1.5.0 + + :param RawDataKey data_encryption_key: Data encryption key + :param KeyringTrace keyring_trace: Trace of actions that a keyring performed + while getting this data encryption key + :raises AttributeError: if data encryption key is already set + :raises InvalidKeyringTraceError: if keyring trace does not match decrypt action + :raises InvalidKeyringTraceError: if keyring trace does not match data key provider + :raises InvalidDataKeyError: if data key length does not match algorithm suite + """ + self._add_data_encryption_key( + data_encryption_key=data_encryption_key, + keyring_trace=keyring_trace, + required_flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY}, + ) + + def add_verification_key(self, verification_key): + # type: (bytes) -> None + """Add a verification key. + + .. versionadded:: 1.5.0 + + :param bytes verification_key: Verification key + """ + if self.verification_key is not None: + raise AttributeError("Verification key is already set.") + + if self.algorithm.signing_algorithm_info is None: + raise SignatureKeyError("Algorithm suite does not support signing keys.") + + self._setattr("verification_key", verification_key) diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index aed2f9163..f433f8d95 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -11,49 +11,16 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Public data structures for aws_encryption_sdk.""" +import copy + import attr import six -from attr.validators import deep_iterable, instance_of +from attr.validators import deep_iterable, deep_mapping, instance_of -import aws_encryption_sdk.identifiers +from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion from aws_encryption_sdk.internal.str_ops import to_bytes, to_str -@attr.s(hash=True) -class MessageHeader(object): - """Deserialized message header object. - - :param version: Message format version, per spec - :type version: aws_encryption_sdk.identifiers.SerializationVersion - :param type: Message content type, per spec - :type type: aws_encryption_sdk.identifiers.ObjectType - :param algorithm: Algorithm to use for encryption - :type algorithm: aws_encryption_sdk.identifiers.Algorithm - :param bytes message_id: Message ID - :param dict encryption_context: Dictionary defining encryption context - :param encrypted_data_keys: Encrypted data keys - :type encrypted_data_keys: set of :class:`aws_encryption_sdk.structures.EncryptedDataKey` - :param content_type: Message content framing type (framed/non-framed) - :type content_type: aws_encryption_sdk.identifiers.ContentType - :param bytes content_aad_length: empty - :param int header_iv_length: Bytes in Initialization Vector value found in header - :param int frame_length: Length of message frame in bytes - """ - - version = attr.ib( - hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.SerializationVersion) - ) - type = attr.ib(hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.ObjectType)) - algorithm = attr.ib(hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.Algorithm)) - message_id = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) - encryption_context = attr.ib(hash=True, validator=attr.validators.instance_of(dict)) - encrypted_data_keys = attr.ib(hash=True, validator=attr.validators.instance_of(set)) - content_type = attr.ib(hash=True, validator=attr.validators.instance_of(aws_encryption_sdk.identifiers.ContentType)) - content_aad_length = attr.ib(hash=True, validator=attr.validators.instance_of(six.integer_types)) - header_iv_length = attr.ib(hash=True, validator=attr.validators.instance_of(six.integer_types)) - frame_length = attr.ib(hash=True, validator=attr.validators.instance_of(six.integer_types)) - - @attr.s(hash=True) class MasterKeyInfo(object): """Contains information necessary to identify a Master Key. @@ -62,8 +29,8 @@ class MasterKeyInfo(object): :param bytes key_info: MasterKey key_info value """ - provider_id = attr.ib(hash=True, validator=attr.validators.instance_of((six.string_types, bytes)), converter=to_str) - key_info = attr.ib(hash=True, validator=attr.validators.instance_of((six.string_types, bytes)), converter=to_bytes) + provider_id = attr.ib(hash=True, validator=instance_of((six.string_types, bytes)), converter=to_str) + key_info = attr.ib(hash=True, validator=instance_of((six.string_types, bytes)), converter=to_bytes) @attr.s(hash=True) @@ -75,8 +42,20 @@ class RawDataKey(object): :param bytes data_key: Plaintext data key """ - key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) - data_key = attr.ib(hash=True, repr=False, validator=attr.validators.instance_of(bytes)) + key_provider = attr.ib(hash=True, validator=instance_of(MasterKeyInfo)) + data_key = attr.ib(hash=True, repr=False, validator=instance_of(bytes)) + + @classmethod + def from_data_key(cls, data_key): + # type: (DataKey) -> RawDataKey + """Build an :class:`RawDataKey` from a :class:`DataKey`. + + .. versionadded:: 1.5.0 + """ + if not isinstance(data_key, DataKey): + raise TypeError("data_key must be type DataKey not {}".format(type(data_key).__name__)) + + return RawDataKey(key_provider=copy.copy(data_key.key_provider), data_key=copy.copy(data_key.data_key)) @attr.s(hash=True) @@ -89,9 +68,9 @@ class DataKey(object): :param bytes encrypted_data_key: Encrypted data key """ - key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) - data_key = attr.ib(hash=True, repr=False, validator=attr.validators.instance_of(bytes)) - encrypted_data_key = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) + key_provider = attr.ib(hash=True, validator=instance_of(MasterKeyInfo)) + data_key = attr.ib(hash=True, repr=False, validator=instance_of(bytes)) + encrypted_data_key = attr.ib(hash=True, validator=instance_of(bytes)) @attr.s(hash=True) @@ -103,20 +82,75 @@ class EncryptedDataKey(object): :param bytes encrypted_data_key: Encrypted data key """ - key_provider = attr.ib(hash=True, validator=attr.validators.instance_of(MasterKeyInfo)) - encrypted_data_key = attr.ib(hash=True, validator=attr.validators.instance_of(bytes)) + key_provider = attr.ib(hash=True, validator=instance_of(MasterKeyInfo)) + encrypted_data_key = attr.ib(hash=True, validator=instance_of(bytes)) + + @classmethod + def from_data_key(cls, data_key): + # type: (DataKey) -> EncryptedDataKey + """Build an :class:`EncryptedDataKey` from a :class:`DataKey`. + + .. versionadded:: 1.5.0 + """ + if not isinstance(data_key, DataKey): + raise TypeError("data_key must be type DataKey not {}".format(type(data_key).__name__)) + + return EncryptedDataKey( + key_provider=copy.copy(data_key.key_provider), encrypted_data_key=copy.copy(data_key.encrypted_data_key) + ) @attr.s class KeyringTrace(object): """Record of all actions that a KeyRing performed with a wrapping key. + .. versionadded:: 1.5.0 + :param MasterKeyInfo wrapping_key: Wrapping key used :param flags: Actions performed :type flags: set of :class:`KeyringTraceFlag` """ wrapping_key = attr.ib(validator=instance_of(MasterKeyInfo)) - flags = attr.ib( - validator=deep_iterable(member_validator=instance_of(aws_encryption_sdk.identifiers.KeyringTraceFlag)) + flags = attr.ib(validator=deep_iterable(member_validator=instance_of(KeyringTraceFlag))) + + +@attr.s(hash=True) +class MessageHeader(object): + """Deserialized message header object. + + :param version: Message format version, per spec + :type version: SerializationVersion + :param type: Message content type, per spec + :type type: ObjectType + :param algorithm: Algorithm to use for encryption + :type algorithm: Algorithm + :param bytes message_id: Message ID + :param dict encryption_context: Dictionary defining encryption context + :param encrypted_data_keys: Encrypted data keys + :type encrypted_data_keys: set of :class:`aws_encryption_sdk.structures.EncryptedDataKey` + :param content_type: Message content framing type (framed/non-framed) + :type content_type: ContentType + :param bytes content_aad_length: empty + :param int header_iv_length: Bytes in Initialization Vector value found in header + :param int frame_length: Length of message frame in bytes + """ + + version = attr.ib(hash=True, validator=instance_of(SerializationVersion)) + type = attr.ib(hash=True, validator=instance_of(ObjectType)) + algorithm = attr.ib(hash=True, validator=instance_of(Algorithm)) + message_id = attr.ib(hash=True, validator=instance_of(bytes)) + encryption_context = attr.ib( + hash=True, + validator=deep_mapping( + key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) + ), + ) + # TODO: Restrict this to only EncryptedDataKeys + encrypted_data_keys = attr.ib( + hash=True, validator=deep_iterable(member_validator=instance_of((EncryptedDataKey, DataKey))) ) + content_type = attr.ib(hash=True, validator=instance_of(ContentType)) + content_aad_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) + header_iv_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) + frame_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index fa216c8d8..a1816fe10 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -12,10 +12,11 @@ # language governing permissions and limitations under the License. """Test suite for aws_encryption_sdk.materials_managers""" import pytest -from mock import MagicMock, sentinel +from mock import MagicMock from pytest_mock import mocker # noqa pylint: disable=unused-import -from aws_encryption_sdk.identifiers import KeyringTraceFlag +from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError +from aws_encryption_sdk.identifiers import AlgorithmSuite, KeyringTraceFlag from aws_encryption_sdk.internal.defaults import ALGORITHM from aws_encryption_sdk.internal.utils.streams import ROStream from aws_encryption_sdk.materials_managers import ( @@ -24,8 +25,9 @@ DecryptionMaterialsRequest, EncryptionMaterials, EncryptionMaterialsRequest, + _data_key_to_raw_data_key, ) -from aws_encryption_sdk.structures import DataKey, KeyringTrace, MasterKeyInfo +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -34,13 +36,14 @@ data_key=b"1234567890123456789012", encrypted_data_key=b"asdf", ) +_RAW_DATA_KEY = RawDataKey.from_data_key(_DATA_KEY) +_ENCRYPTED_DATA_KEY = EncryptedDataKey.from_data_key(_DATA_KEY) _VALID_KWARGS = { "CryptographicMaterials": dict( algorithm=ALGORITHM, encryption_context={"additional": "data"}, data_encryption_key=_DATA_KEY, - encrypted_data_keys=[], keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), @@ -70,6 +73,15 @@ _REMOVE = object() +def _copy_and_update_kwargs(class_name, mod_kwargs): + kwargs = _VALID_KWARGS[class_name].copy() + kwargs.update(mod_kwargs) + purge_keys = [key for key, val in kwargs.items() if val is _REMOVE] + for key in purge_keys: + del kwargs[key] + return kwargs + + @pytest.mark.parametrize( "attr_class, invalid_kwargs", ( @@ -90,19 +102,30 @@ (DecryptionMaterialsRequest, dict(encryption_context=None)), (DecryptionMaterials, dict(verification_key=5555)), (DecryptionMaterials, dict(data_key=_DATA_KEY, data_encryption_key=_DATA_KEY)), - (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_REMOVE)), ), ) def test_attributes_fails(attr_class, invalid_kwargs): - kwargs = _VALID_KWARGS[attr_class.__name__].copy() - kwargs.update(invalid_kwargs) - purge_keys = [key for key, val in kwargs.items() if val is _REMOVE] - for key in purge_keys: - del kwargs[key] + kwargs = _copy_and_update_kwargs(attr_class.__name__, invalid_kwargs) with pytest.raises(TypeError): attr_class(**kwargs) +@pytest.mark.parametrize( + "attr_class, kwargs_modification", + ( + (CryptographicMaterials, {}), + (EncryptionMaterials, {}), + (DecryptionMaterials, {}), + (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_REMOVE)), + (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_RAW_DATA_KEY)), + (DecryptionMaterials, dict(data_key=_RAW_DATA_KEY, data_encryption_key=_REMOVE)), + ), +) +def test_attributes_good(attr_class, kwargs_modification): + kwargs = _copy_and_update_kwargs(attr_class.__name__, kwargs_modification) + attr_class(**kwargs) + + def test_encryption_materials_request_attributes_defaults(): test = EncryptionMaterialsRequest(encryption_context={}, frame_length=5) assert test.plaintext_rostream is None @@ -127,14 +150,239 @@ def test_decryption_materials_defaults(): def test_decryption_materials_legacy_data_key_get(): test = DecryptionMaterials(data_encryption_key=_DATA_KEY) - assert test.data_encryption_key is _DATA_KEY - assert test.data_key is _DATA_KEY + assert test.data_encryption_key == _RAW_DATA_KEY + assert test.data_key == _RAW_DATA_KEY -def test_decryption_materials_legacy_data_key_set(): - test = DecryptionMaterials(data_encryption_key=_DATA_KEY) +@pytest.mark.parametrize( + "data_key, expected", ((_DATA_KEY, _RAW_DATA_KEY), (_RAW_DATA_KEY, _RAW_DATA_KEY), (None, None)) +) +def test_data_key_to_raw_data_key_success(data_key, expected): + test = _data_key_to_raw_data_key(data_key=data_key) + + assert test == expected + + +def test_data_key_to_raw_data_key_fail(): + with pytest.raises(TypeError) as excinfo: + _data_key_to_raw_data_key(data_key="not a data key") + + excinfo.match("data_key must be type DataKey not str") + + +def _cryptographic_materials_attributes(): + for material in (CryptographicMaterials, EncryptionMaterials, DecryptionMaterials): + for attribute in ( + "algorithm", + "encryption_context", + "data_encryption_key", + "_keyring_trace", + "keyring_trace", + "_initialized", + ): + yield material, attribute + + for attribute in ("_encrypted_data_keys", "encrypted_data_keys", "signing_key"): + yield EncryptionMaterials, attribute + + for attribute in ("data_key", "verification_key"): + yield DecryptionMaterials, attribute + + +@pytest.mark.parametrize("material_class, attribute_name", _cryptographic_materials_attributes()) +def test_cryptographic_materials_cannot_change_attribute(material_class, attribute_name): + test = material_class(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(AttributeError) as excinfo: + setattr(test, attribute_name, 42) + + excinfo.match("can't set attribute") + + +@pytest.mark.parametrize("material_class", (CryptographicMaterials, EncryptionMaterials, DecryptionMaterials)) +def test_immutable_keyring_trace(material_class): + materials = material_class(**_VALID_KWARGS[material_class.__name__]) + + with pytest.raises(AttributeError): + materials.keyring_trace.append(42) + + +def test_immutable_encrypted_data_keys(): + materials = EncryptionMaterials(**_VALID_KWARGS["EncryptionMaterials"]) + + with pytest.raises(AttributeError): + materials.encrypted_data_keys.add(42) + + +@pytest.mark.parametrize( + "material_class, flag", + ( + (EncryptionMaterials, KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY), + (DecryptionMaterials, KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY), + ), +) +def test_add_data_encryption_key_success(material_class, flag): + kwargs = _copy_and_update_kwargs(material_class.__name__, dict(data_encryption_key=_REMOVE, data_key=_REMOVE)) + materials = material_class(**kwargs) + + materials.add_data_encryption_key( + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), data_key=b"1" * ALGORITHM.kdf_input_len + ), + keyring_trace=KeyringTrace(wrapping_key=MasterKeyInfo(provider_id="a", key_info=b"b"), flags={flag}), + ) + + +def _add_data_encryption_key_test_cases(): + for material_class, required_flags in ( + (EncryptionMaterials, KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY), + (DecryptionMaterials, KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY), + ): + yield ( + material_class, + dict(data_encryption_key=_RAW_DATA_KEY, data_key=_REMOVE), + _RAW_DATA_KEY, + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), + AttributeError, + "Data encryption key is already set.", + ) + yield ( + material_class, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + _RAW_DATA_KEY, + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags=set()), + InvalidKeyringTraceError, + "Keyring flags do not match action.", + ) + yield ( + material_class, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + RawDataKey(key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), data_key=b"asdf"), + KeyringTrace(wrapping_key=MasterKeyInfo(provider_id="c", key_info=b"d"), flags={required_flags}), + InvalidKeyringTraceError, + "Keyring trace does not match data key provider.", + ) + yield ( + material_class, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + RawDataKey(key_provider=_RAW_DATA_KEY.key_provider, data_key=b"1234"), + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), + InvalidDataKeyError, + r"Invalid data key length *", + ) + + +@pytest.mark.parametrize( + "material_class, mod_kwargs, data_encryption_key, keyring_trace, exception_type, exception_message", + _add_data_encryption_key_test_cases(), +) +def test_add_data_encryption_key_fail( + material_class, mod_kwargs, data_encryption_key, keyring_trace, exception_type, exception_message +): + kwargs = _copy_and_update_kwargs(material_class.__name__, mod_kwargs) + materials = material_class(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.add_data_encryption_key(data_encryption_key=data_encryption_key, keyring_trace=keyring_trace) + + excinfo.match(exception_message) + + +def test_add_encrypted_data_key_success(): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", {}) + materials = EncryptionMaterials(**kwargs) + + materials.add_encrypted_data_key( + _ENCRYPTED_DATA_KEY, + keyring_trace=KeyringTrace( + wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + ), + ) + + +@pytest.mark.parametrize( + "encrypted_data_key, keyring_trace, exception_type, exception_message", + ( + ( + _ENCRYPTED_DATA_KEY, + KeyringTrace(wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags=set()), + InvalidKeyringTraceError, + "Keyring flags do not match action.", + ), + ( + EncryptedDataKey(key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), encrypted_data_key=b"asdf"), + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id="not a match", key_info=b"really not a match"), + flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY}, + ), + InvalidKeyringTraceError, + "Keyring trace does not match data key encryptor.", + ), + ), +) +def test_add_encrypted_data_key_fail(encrypted_data_key, keyring_trace, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", {}) + materials = EncryptionMaterials(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.add_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) + + excinfo.match(exception_message) + + +def test_add_signing_key_success(): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", dict(signing_key=_REMOVE)) + materials = EncryptionMaterials(**kwargs) + + materials.add_signing_key(signing_key=b"") + + +@pytest.mark.parametrize( + "mod_kwargs, signing_key, exception_type, exception_message", + ( + ({}, b"", AttributeError, "Signing key is already set."), + ( + dict(signing_key=_REMOVE, algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16), + b"", + SignatureKeyError, + "Algorithm suite does not support signing keys.", + ), + ), +) +def test_add_signing_key_fail(mod_kwargs, signing_key, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", mod_kwargs) + materials = EncryptionMaterials(**kwargs) + + with pytest.raises(exception_type) as excinfo: + materials.add_signing_key(signing_key=signing_key) + + excinfo.match(exception_message) + + +def test_add_verification_key_success(): + kwargs = _copy_and_update_kwargs("DecryptionMaterials", dict(verification_key=_REMOVE)) + materials = DecryptionMaterials(**kwargs) + + materials.add_verification_key(verification_key=b"") + + +@pytest.mark.parametrize( + "mod_kwargs, verification_key, exception_type, exception_message", + ( + ({}, b"", AttributeError, "Verification key is already set."), + ( + dict(verification_key=_REMOVE, algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16), + b"", + SignatureKeyError, + "Algorithm suite does not support signing keys.", + ), + ), +) +def test_add_verification_key_fail(mod_kwargs, verification_key, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("DecryptionMaterials", mod_kwargs) + materials = DecryptionMaterials(**kwargs) - test.data_key = sentinel.data_key + with pytest.raises(exception_type) as excinfo: + materials.add_verification_key(verification_key=verification_key) - assert test.data_encryption_key is sentinel.data_key - assert test.data_key is sentinel.data_key + excinfo.match(exception_message) diff --git a/test/unit/test_material_managers_default.py b/test/unit/test_material_managers_default.py index 6eeef525b..20aaa8dd6 100644 --- a/test/unit/test_material_managers_default.py +++ b/test/unit/test_material_managers_default.py @@ -22,10 +22,17 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.materials_managers import EncryptionMaterials from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager -from aws_encryption_sdk.structures import DataKey, EncryptedDataKey +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey pytestmark = [pytest.mark.unit, pytest.mark.local] +_DATA_KEY = DataKey( + key_provider=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), + data_key=b"1234567890123456789012", + encrypted_data_key=b"asdf", +) +_ENCRYPTED_DATA_KEY = EncryptedDataKey.from_data_key(_DATA_KEY) + @pytest.fixture def patch_for_dcmm_encrypt(mocker): @@ -33,8 +40,8 @@ def patch_for_dcmm_encrypt(mocker): mock_signing_key = b"ex_signing_key" DefaultCryptoMaterialsManager._generate_signing_key_and_update_encryption_context.return_value = mock_signing_key mocker.patch.object(aws_encryption_sdk.materials_managers.default, "prepare_data_keys") - mock_data_encryption_key = MagicMock(__class__=DataKey) - mock_encrypted_data_keys = set([MagicMock(__class__=EncryptedDataKey)]) + mock_data_encryption_key = _DATA_KEY + mock_encrypted_data_keys = set([_ENCRYPTED_DATA_KEY]) result_pair = mock_data_encryption_key, mock_encrypted_data_keys aws_encryption_sdk.materials_managers.default.prepare_data_keys.return_value = result_pair yield result_pair, mock_signing_key @@ -50,7 +57,7 @@ def patch_for_dcmm_decrypt(mocker): def build_cmm(): mock_mkp = MagicMock(__class__=MasterKeyProvider) - mock_mkp.decrypt_data_key_from_list.return_value = MagicMock(__class__=DataKey) + mock_mkp.decrypt_data_key_from_list.return_value = _DATA_KEY mock_mkp.master_keys_for_encryption.return_value = ( sentinel.primary_mk, set([sentinel.primary_mk, sentinel.mk_a, sentinel.mk_b]), @@ -127,7 +134,7 @@ def test_get_encryption_materials(patch_for_dcmm_encrypt): ) assert isinstance(test, EncryptionMaterials) assert test.algorithm is cmm.algorithm - assert test.data_encryption_key is patch_for_dcmm_encrypt[0][0] + assert test.data_encryption_key == RawDataKey.from_data_key(patch_for_dcmm_encrypt[0][0]) assert test.encrypted_data_keys == patch_for_dcmm_encrypt[0][1] assert test.encryption_context == encryption_context assert test.signing_key == patch_for_dcmm_encrypt[1] @@ -232,5 +239,5 @@ def test_decrypt_materials(mocker, patch_for_dcmm_decrypt): cmm._load_verification_key_from_encryption_context.assert_called_once_with( algorithm=mock_request.algorithm, encryption_context=mock_request.encryption_context ) - assert test.data_key is cmm.master_key_provider.decrypt_data_key_from_list.return_value + assert test.data_key == RawDataKey.from_data_key(cmm.master_key_provider.decrypt_data_key_from_list.return_value) assert test.verification_key == patch_for_dcmm_decrypt diff --git a/test/unit/test_streaming_client_stream_encryptor.py b/test/unit/test_streaming_client_stream_encryptor.py index 501214e9f..5cb2b8e37 100644 --- a/test/unit/test_streaming_client_stream_encryptor.py +++ b/test/unit/test_streaming_client_stream_encryptor.py @@ -247,7 +247,7 @@ def test_prep_message_framed_message( encryption_context=VALUES["encryption_context"], ) test_encryptor.content_type = ContentType.FRAMED_DATA - test_encryption_context = {aws_encryption_sdk.internal.defaults.ENCODED_SIGNER_KEY: sentinel.decoded_bytes} + test_encryption_context = {aws_encryption_sdk.internal.defaults.ENCODED_SIGNER_KEY: "DECODED_BYTES"} self.mock_encryption_materials.encryption_context = test_encryption_context self.mock_encryption_materials.encrypted_data_keys = self.mock_encrypted_data_keys diff --git a/test/unit/test_structures.py b/test/unit/test_structures.py index 1a9caa01d..efce6fadc 100644 --- a/test/unit/test_structures.py +++ b/test/unit/test_structures.py @@ -107,3 +107,30 @@ def test_data_key_repr_str(cls, params): assert data_key_check not in str(test) assert data_key_check not in repr(test) + + +@pytest.fixture +def ex_data_key(): + return DataKey(**VALID_KWARGS[DataKey][0]) + + +def test_encrypted_data_key_from_data_key_success(ex_data_key): + test = EncryptedDataKey.from_data_key(ex_data_key) + + assert test.key_provider == ex_data_key.key_provider + assert test.encrypted_data_key == ex_data_key.encrypted_data_key + + +def test_raw_data_key_from_data_key_success(ex_data_key): + test = RawDataKey.from_data_key(ex_data_key) + + assert test.key_provider == ex_data_key.key_provider + assert test.data_key == ex_data_key.data_key + + +@pytest.mark.parametrize("data_key_class", (EncryptedDataKey, RawDataKey)) +def test_raw_and_encrypted_data_key_from_data_key_fail(data_key_class): + with pytest.raises(TypeError) as excinfo: + data_key_class.from_data_key(b"ahjseofij") + + excinfo.match(r"data_key must be type DataKey not bytes") From 01759b9170b32361e465dd2d28f10c2a98138c76 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 26 Jun 2019 20:15:11 -0700 Subject: [PATCH 05/22] encrypted_data_keys must only contain EncryptedDataKey --- src/aws_encryption_sdk/materials_managers/__init__.py | 3 +-- src/aws_encryption_sdk/structures.py | 5 +---- test/unit/test_caches.py | 8 +++----- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 75589cc83..4ed77e315 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -308,8 +308,7 @@ class DecryptionMaterialsRequest(object): """ algorithm = attr.ib(validator=instance_of(Algorithm)) - # TODO: Restrict this to only EncryptedDataKeys - encrypted_data_keys = attr.ib(validator=deep_iterable(member_validator=instance_of((EncryptedDataKey, DataKey)))) + encrypted_data_keys = attr.ib(validator=deep_iterable(member_validator=instance_of(EncryptedDataKey))) encryption_context = attr.ib( validator=deep_mapping( key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index f433f8d95..635f661ce 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -146,10 +146,7 @@ class MessageHeader(object): key_validator=instance_of(six.string_types), value_validator=instance_of(six.string_types) ), ) - # TODO: Restrict this to only EncryptedDataKeys - encrypted_data_keys = attr.ib( - hash=True, validator=deep_iterable(member_validator=instance_of((EncryptedDataKey, DataKey))) - ) + encrypted_data_keys = attr.ib(hash=True, validator=deep_iterable(member_validator=instance_of(EncryptedDataKey))) content_type = attr.ib(hash=True, validator=instance_of(ContentType)) content_aad_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) header_iv_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) diff --git a/test/unit/test_caches.py b/test/unit/test_caches.py index 250ad6d5b..58c1b4944 100644 --- a/test/unit/test_caches.py +++ b/test/unit/test_caches.py @@ -27,7 +27,7 @@ ) from aws_encryption_sdk.identifiers import Algorithm from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest -from aws_encryption_sdk.structures import DataKey, MasterKeyInfo +from aws_encryption_sdk.structures import EncryptedDataKey, MasterKeyInfo pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -47,19 +47,17 @@ }, "encrypted_data_keys": [ { - "key": DataKey( + "key": EncryptedDataKey( key_provider=MasterKeyInfo(provider_id="this is a provider ID", key_info=b"this is some key info"), - data_key=b"super secret key!", encrypted_data_key=b"super secret key, now with encryption!", ), "hash": b"TYoFeYuxns/FBlaw4dsRDOv25OCEKuZG9iXt5iEdJ8LU7n5glgkDAVxWUEYC4JKKykJdHkaVpxcDvNqS6UswiQ==", }, { - "key": DataKey( + "key": EncryptedDataKey( key_provider=MasterKeyInfo( provider_id="another provider ID!", key_info=b"this is some different key info" ), - data_key=b"better super secret key!", encrypted_data_key=b"better super secret key, now with encryption!", ), "hash": b"wSrDlPM2ocIj9MAtD94ULSR0Qrt1muBovBDRL+DsSTNphJEM3CZ/h3OyvYL8BR2EIXx0m7GYwv8dGtyZL2D87w==", From e8e5b82744def6b7de11f9f3fd6c3225ddf3ff6d Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 26 Jun 2019 20:28:02 -0700 Subject: [PATCH 06/22] fix test to be Python 2 compatible --- test/unit/test_structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/test_structures.py b/test/unit/test_structures.py index efce6fadc..e1070c574 100644 --- a/test/unit/test_structures.py +++ b/test/unit/test_structures.py @@ -133,4 +133,4 @@ def test_raw_and_encrypted_data_key_from_data_key_fail(data_key_class): with pytest.raises(TypeError) as excinfo: data_key_class.from_data_key(b"ahjseofij") - excinfo.match(r"data_key must be type DataKey not bytes") + excinfo.match(r"data_key must be type DataKey not *") From 469600cda0319842a550aaf9fc627fdbe5a8b30f Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 28 Jun 2019 13:34:01 -0700 Subject: [PATCH 07/22] data encryption key must be set before encrypted data keys can be added to EncryptionMaterials --- .../materials_managers/__init__.py | 4 ++++ test/unit/test_material_managers.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 4ed77e315..916516dfc 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -263,9 +263,13 @@ def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): :param EncryptedDataKey encrypted_data_key: Encrypted data key to add :param KeyringTrace keyring_trace: Trace of actions that a keyring performed while getting this encrypted data key + :raises AttributeError: if data encryption key is not set :raises InvalidKeyringTraceError: if keyring trace does not match generate action :raises InvalidKeyringTraceError: if keyring trace does not match data key encryptor """ + if self.data_encryption_key is None: + raise AttributeError("Data encryption key is not set.") + if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY not in keyring_trace.flags: raise InvalidKeyringTraceError("Keyring flags do not match action.") diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index a1816fe10..6e2f9330e 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -301,15 +301,17 @@ def test_add_encrypted_data_key_success(): @pytest.mark.parametrize( - "encrypted_data_key, keyring_trace, exception_type, exception_message", + "mod_kwargs, encrypted_data_key, keyring_trace, exception_type, exception_message", ( ( + {}, _ENCRYPTED_DATA_KEY, KeyringTrace(wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags=set()), InvalidKeyringTraceError, "Keyring flags do not match action.", ), ( + {}, EncryptedDataKey(key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), encrypted_data_key=b"asdf"), KeyringTrace( wrapping_key=MasterKeyInfo(provider_id="not a match", key_info=b"really not a match"), @@ -318,10 +320,19 @@ def test_add_encrypted_data_key_success(): InvalidKeyringTraceError, "Keyring trace does not match data key encryptor.", ), + ( + dict(data_encryption_key=_REMOVE), + _ENCRYPTED_DATA_KEY, + KeyringTrace( + wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + ), + AttributeError, + "Data encryption key is not set.", + ), ), ) -def test_add_encrypted_data_key_fail(encrypted_data_key, keyring_trace, exception_type, exception_message): - kwargs = _copy_and_update_kwargs("EncryptionMaterials", {}) +def test_add_encrypted_data_key_fail(mod_kwargs, encrypted_data_key, keyring_trace, exception_type, exception_message): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", mod_kwargs) materials = EncryptionMaterials(**kwargs) with pytest.raises(exception_type) as excinfo: From a27ff748b3ce01463eeefb33d6820e463199fa40 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 1 Jul 2019 17:08:19 -0700 Subject: [PATCH 08/22] add signing/verification key checks to Encryption/DecryptionMaterials --- .../materials_managers/__init__.py | 5 +++++ test/unit/test_material_managers.py | 15 +++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 916516dfc..3dff7563a 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -20,6 +20,7 @@ from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag +from aws_encryption_sdk.internal.crypto.authentication import Signer, Verifier from aws_encryption_sdk.internal.utils.streams import ROStream from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, RawDataKey @@ -295,6 +296,8 @@ def add_signing_key(self, signing_key): if self.algorithm.signing_algorithm_info is None: raise SignatureKeyError("Algorithm suite does not support signing keys.") + Signer.from_key_bytes(algorithm=self.algorithm, key_bytes=signing_key) + self._setattr("signing_key", signing_key) @@ -408,4 +411,6 @@ def add_verification_key(self, verification_key): if self.algorithm.signing_algorithm_info is None: raise SignatureKeyError("Algorithm suite does not support signing keys.") + Verifier.from_key_bytes(algorithm=self.algorithm, key_bytes=verification_key) + self._setattr("verification_key", verification_key) diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index 6e2f9330e..e0cc55972 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -11,12 +11,16 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Test suite for aws_encryption_sdk.materials_managers""" + import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec from mock import MagicMock from pytest_mock import mocker # noqa pylint: disable=unused-import from aws_encryption_sdk.exceptions import InvalidDataKeyError, InvalidKeyringTraceError, SignatureKeyError from aws_encryption_sdk.identifiers import AlgorithmSuite, KeyringTraceFlag +from aws_encryption_sdk.internal.crypto.authentication import Signer, Verifier from aws_encryption_sdk.internal.defaults import ALGORITHM from aws_encryption_sdk.internal.utils.streams import ROStream from aws_encryption_sdk.materials_managers import ( @@ -38,6 +42,9 @@ ) _RAW_DATA_KEY = RawDataKey.from_data_key(_DATA_KEY) _ENCRYPTED_DATA_KEY = EncryptedDataKey.from_data_key(_DATA_KEY) +_SIGNATURE_PRIVATE_KEY = ec.generate_private_key(ALGORITHM.signing_algorithm_info(), default_backend()) +_SIGNING_KEY = Signer(algorithm=ALGORITHM, key=_SIGNATURE_PRIVATE_KEY) +_VERIFICATION_KEY = Verifier(algorithm=ALGORITHM, key=_SIGNATURE_PRIVATE_KEY.public_key()) _VALID_KWARGS = { "CryptographicMaterials": dict( @@ -63,11 +70,11 @@ data_encryption_key=_DATA_KEY, encrypted_data_keys=set([]), encryption_context={}, - signing_key=b"", + signing_key=_SIGNING_KEY.key_bytes(), ), "DecryptionMaterialsRequest": dict(algorithm=ALGORITHM, encrypted_data_keys=set([]), encryption_context={}), "DecryptionMaterials": dict( - data_key=_DATA_KEY, verification_key=b"ex_verification_key", algorithm=ALGORITHM, encryption_context={} + data_key=_DATA_KEY, verification_key=_VERIFICATION_KEY.key_bytes(), algorithm=ALGORITHM, encryption_context={} ), } _REMOVE = object() @@ -345,7 +352,7 @@ def test_add_signing_key_success(): kwargs = _copy_and_update_kwargs("EncryptionMaterials", dict(signing_key=_REMOVE)) materials = EncryptionMaterials(**kwargs) - materials.add_signing_key(signing_key=b"") + materials.add_signing_key(signing_key=_SIGNING_KEY.key_bytes()) @pytest.mark.parametrize( @@ -374,7 +381,7 @@ def test_add_verification_key_success(): kwargs = _copy_and_update_kwargs("DecryptionMaterials", dict(verification_key=_REMOVE)) materials = DecryptionMaterials(**kwargs) - materials.add_verification_key(verification_key=b"") + materials.add_verification_key(verification_key=_VERIFICATION_KEY.key_bytes()) @pytest.mark.parametrize( From b311cda30958283860f198f2f61a24d928af6d9e Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Mon, 1 Jul 2019 19:03:50 -0700 Subject: [PATCH 09/22] DecryptionMaterials.algorithm must be set before DecryptionMaterials.add_data_encryption_key can be called --- src/aws_encryption_sdk/materials_managers/__init__.py | 3 +++ test/unit/test_material_managers.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 3dff7563a..6d8669b22 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -391,6 +391,9 @@ def add_data_encryption_key(self, data_encryption_key, keyring_trace): :raises InvalidKeyringTraceError: if keyring trace does not match data key provider :raises InvalidDataKeyError: if data key length does not match algorithm suite """ + if self.algorithm is None: + raise AttributeError("Algorithm is not set") + self._add_data_encryption_key( data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index e0cc55972..dcfa984bf 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -277,6 +277,14 @@ def _add_data_encryption_key_test_cases(): InvalidDataKeyError, r"Invalid data key length *", ) + yield ( + DecryptionMaterials, + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, algorithm=_REMOVE), + RawDataKey(key_provider=_RAW_DATA_KEY.key_provider, data_key=b"1234"), + KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), + AttributeError, + "Algorithm is not set" + ) @pytest.mark.parametrize( From 10ded57e01e1e4d130860ae94c10f5b7db397471 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 2 Jul 2019 11:52:29 -0700 Subject: [PATCH 10/22] update materials docs and typehints --- .../materials_managers/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 6d8669b22..ac63c4978 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -25,7 +25,7 @@ from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, RawDataKey try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Iterable, Union # noqa pylint: disable=unused-import + from typing import Any, FrozenSet, Iterable, Tuple, Union # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -77,9 +77,7 @@ class CryptographicMaterials(object): :param Algorithm algorithm: Algorithm to use for encrypting message :param dict encryption_context: Encryption context tied to `encrypted_data_keys` - :param DataKey data_encryption_key: Plaintext data key to use for encrypting message - :param encrypted_data_keys: List of encrypted data keys - :type encrypted_data_keys: list of :class:`EncryptedDataKey` + :param RawDataKey data_encryption_key: Plaintext data key to use for encrypting message :param keyring_trace: Any KeyRing trace entries :type keyring_trace: list of :class:`KeyringTrace` """ @@ -103,6 +101,7 @@ def __attrs_post_init__(self): self._initialized = True def __setattr__(self, key, value): + # type: (str, Any) -> None """Do not allow attributes to be changed once an instance is initialized.""" if self._initialized: raise AttributeError("can't set attribute") @@ -110,6 +109,7 @@ def __setattr__(self, key, value): self._setattr(key, value) def _setattr(self, key, value): + # type: (str, Any) -> None """Special __setattr__ to avoid having to perform multi-level super calls.""" super(CryptographicMaterials, self).__setattr__(key, value) @@ -150,6 +150,8 @@ def _add_data_encryption_key(self, data_encryption_key, keyring_trace, required_ :param RawDataKey data_encryption_key: Data encryption key :param KeyringTrace keyring_trace: Trace of actions that a keyring performed while getting this data encryption key + :param required_flags: Iterable of required flags + :type required_flags: iterable of :class:`KeyringTraceFlag` :raises AttributeError: if data encryption key is already set :raises InvalidKeyringTraceError: if keyring trace does not match required actions :raises InvalidKeyringTraceError: if keyring trace does not match data key provider @@ -166,6 +168,7 @@ def _add_data_encryption_key(self, data_encryption_key, keyring_trace, required_ @property def keyring_trace(self): + # type: () -> Tuple[KeyringTrace] """Return a read-only version of the keyring trace. :rtype: tuple @@ -229,6 +232,7 @@ def __init__( @property def encrypted_data_keys(self): + # type: () -> FrozenSet[EncryptedDataKey] """Return a read-only version of the encrypted data keys. :rtype: frozenset @@ -296,6 +300,7 @@ def add_signing_key(self, signing_key): if self.algorithm.signing_algorithm_info is None: raise SignatureKeyError("Algorithm suite does not support signing keys.") + # Verify that the signing key matches the algorithm Signer.from_key_bytes(algorithm=self.algorithm, key_bytes=signing_key) self._setattr("signing_key", signing_key) @@ -374,6 +379,7 @@ def __init__( @property def data_key(self): + # type: () -> RawDataKey """Backwards-compatible shim for access to data key.""" return self.data_encryption_key @@ -414,6 +420,7 @@ def add_verification_key(self, verification_key): if self.algorithm.signing_algorithm_info is None: raise SignatureKeyError("Algorithm suite does not support signing keys.") + # Verify that the verification key matches the algorithm Verifier.from_key_bytes(algorithm=self.algorithm, key_bytes=verification_key) self._setattr("verification_key", verification_key) From 4f95e53d6177285792060571b1029b08c6438150 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 2 Jul 2019 18:11:45 -0700 Subject: [PATCH 11/22] EncryptionMaterials must not be initialized with encrypted_data_keys but no data_encryption_key --- .../materials_managers/__init__.py | 3 +++ test/unit/test_material_managers.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index ac63c4978..3045d6a50 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -220,6 +220,9 @@ def __init__( if encryption_context is None: raise TypeError("encryption_context must not be None") + if data_encryption_key is None and encrypted_data_keys is not None: + raise TypeError("encrypted_data_keys cannot be provided without data_encryption_key") + super(EncryptionMaterials, self).__init__( algorithm=algorithm, encryption_context=encryption_context, diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index dcfa984bf..e75cd7c44 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -104,6 +104,7 @@ def _copy_and_update_kwargs(class_name, mod_kwargs): (EncryptionMaterials, dict(algorithm=None)), (EncryptionMaterials, dict(encryption_context=None)), (EncryptionMaterials, dict(signing_key=u"not bytes or None")), + (EncryptionMaterials, dict(data_encryption_key=_REMOVE)), (DecryptionMaterialsRequest, dict(algorithm=None)), (DecryptionMaterialsRequest, dict(encrypted_data_keys=None)), (DecryptionMaterialsRequest, dict(encryption_context=None)), @@ -229,7 +230,9 @@ def test_immutable_encrypted_data_keys(): ), ) def test_add_data_encryption_key_success(material_class, flag): - kwargs = _copy_and_update_kwargs(material_class.__name__, dict(data_encryption_key=_REMOVE, data_key=_REMOVE)) + kwargs = _copy_and_update_kwargs( + material_class.__name__, dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE) + ) materials = material_class(**kwargs) materials.add_data_encryption_key( @@ -247,7 +250,7 @@ def _add_data_encryption_key_test_cases(): ): yield ( material_class, - dict(data_encryption_key=_RAW_DATA_KEY, data_key=_REMOVE), + dict(data_encryption_key=_RAW_DATA_KEY, data_key=_REMOVE, encrypted_data_keys=_REMOVE), _RAW_DATA_KEY, KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), AttributeError, @@ -255,7 +258,7 @@ def _add_data_encryption_key_test_cases(): ) yield ( material_class, - dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE), _RAW_DATA_KEY, KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags=set()), InvalidKeyringTraceError, @@ -263,7 +266,7 @@ def _add_data_encryption_key_test_cases(): ) yield ( material_class, - dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE), RawDataKey(key_provider=MasterKeyInfo(provider_id="a", key_info=b"b"), data_key=b"asdf"), KeyringTrace(wrapping_key=MasterKeyInfo(provider_id="c", key_info=b"d"), flags={required_flags}), InvalidKeyringTraceError, @@ -271,7 +274,7 @@ def _add_data_encryption_key_test_cases(): ) yield ( material_class, - dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE), RawDataKey(key_provider=_RAW_DATA_KEY.key_provider, data_key=b"1234"), KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), InvalidDataKeyError, @@ -279,11 +282,11 @@ def _add_data_encryption_key_test_cases(): ) yield ( DecryptionMaterials, - dict(data_encryption_key=_REMOVE, data_key=_REMOVE, algorithm=_REMOVE), + dict(data_encryption_key=_REMOVE, data_key=_REMOVE, encrypted_data_keys=_REMOVE, algorithm=_REMOVE), RawDataKey(key_provider=_RAW_DATA_KEY.key_provider, data_key=b"1234"), KeyringTrace(wrapping_key=_RAW_DATA_KEY.key_provider, flags={required_flags}), AttributeError, - "Algorithm is not set" + "Algorithm is not set", ) @@ -336,7 +339,7 @@ def test_add_encrypted_data_key_success(): "Keyring trace does not match data key encryptor.", ), ( - dict(data_encryption_key=_REMOVE), + dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE), _ENCRYPTED_DATA_KEY, KeyringTrace( wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} From 73027752fcd12e3a352a26958a064edfc8ba717c Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 2 Jul 2019 18:44:00 -0700 Subject: [PATCH 12/22] add is_complete properties to EncryptionMaterials and DecryptionMaterials --- .../materials_managers/__init__.py | 39 ++++++++++++ test/unit/test_material_managers.py | 62 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 3045d6a50..a6609199a 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -223,6 +223,9 @@ def __init__( if data_encryption_key is None and encrypted_data_keys is not None: raise TypeError("encrypted_data_keys cannot be provided without data_encryption_key") + if encrypted_data_keys is None: + encrypted_data_keys = [] + super(EncryptionMaterials, self).__init__( algorithm=algorithm, encryption_context=encryption_context, @@ -242,6 +245,24 @@ def encrypted_data_keys(self): """ return frozenset(self._encrypted_data_keys) + @property + def is_complete(self): + # type: () -> bool + """Determine whether these materials are sufficiently complete for use as decryption materials. + + :rtype: bool + """ + if self.data_encryption_key is None: + return False + + if not self.encrypted_data_keys: + return False + + if self.algorithm.signing_algorithm_info is not None and self.signing_key is None: + return False + + return True + def add_data_encryption_key(self, data_encryption_key, keyring_trace): # type: (Union[DataKey, RawDataKey], KeyringTrace) -> None """Add a plaintext data encryption key. @@ -380,6 +401,24 @@ def __init__( self._setattr("verification_key", verification_key) attr.validate(self) + @property + def is_complete(self): + # type: () -> bool + """Determine whether these materials are sufficiently complete for use as decryption materials. + + :rtype: bool + """ + if None in (self.algorithm, self.encryption_context): + return False + + if self.data_encryption_key is None: + return False + + if self.algorithm.signing_algorithm_info is not None and self.verification_key is None: + return False + + return True + @property def data_key(self): # type: () -> RawDataKey diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index e75cd7c44..2f64f3c74 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -215,6 +215,16 @@ def test_immutable_keyring_trace(material_class): materials.keyring_trace.append(42) +@pytest.mark.parametrize("material_class", (CryptographicMaterials, EncryptionMaterials, DecryptionMaterials)) +def test_empty_keyring_trace(material_class): + materials = material_class(**_copy_and_update_kwargs(material_class.__name__, dict(keyring_trace=_REMOVE))) + + trace = materials.keyring_trace + + assert isinstance(trace, tuple) + assert not trace + + def test_immutable_encrypted_data_keys(): materials = EncryptionMaterials(**_VALID_KWARGS["EncryptionMaterials"]) @@ -222,6 +232,15 @@ def test_immutable_encrypted_data_keys(): materials.encrypted_data_keys.add(42) +def test_empty_encrypted_data_keys(): + materials = EncryptionMaterials(**_copy_and_update_kwargs("EncryptionMaterials", dict(encrypted_data_keys=_REMOVE))) + + edks = materials.encrypted_data_keys + + assert isinstance(edks, frozenset) + assert not edks + + @pytest.mark.parametrize( "material_class, flag", ( @@ -415,3 +434,46 @@ def test_add_verification_key_fail(mod_kwargs, verification_key, exception_type, materials.add_verification_key(verification_key=verification_key) excinfo.match(exception_message) + + +def test_decryption_materials_is_complete(): + materials = DecryptionMaterials(**_copy_and_update_kwargs("DecryptionMaterials", {})) + + assert materials.is_complete + + +@pytest.mark.parametrize( + "mod_kwargs", + ( + dict(algorithm=_REMOVE), + dict(encryption_context=_REMOVE), + dict(data_encryption_key=_REMOVE, data_key=_REMOVE), + dict(verification_key=_REMOVE), + ), +) +def test_decryption_materials_is_not_complete(mod_kwargs): + kwargs = _copy_and_update_kwargs("DecryptionMaterials", mod_kwargs) + materials = DecryptionMaterials(**kwargs) + + assert not materials.is_complete + + +def test_encryption_materials_is_complete(): + materials = EncryptionMaterials(**_copy_and_update_kwargs("EncryptionMaterials", {})) + + assert materials.is_complete + + +@pytest.mark.parametrize( + "mod_kwargs", + ( + dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE), + dict(encrypted_data_keys=_REMOVE), + dict(signing_key=_REMOVE), + ), +) +def test_encryption_materials_is_not_complete(mod_kwargs): + kwargs = _copy_and_update_kwargs("EncryptionMaterials", mod_kwargs) + materials = EncryptionMaterials(**kwargs) + + assert not materials.is_complete From f1e7f2fa73053789308d90050334055199dadffd Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 9 Jul 2019 13:52:47 -0700 Subject: [PATCH 13/22] change KeyringTraceFlag values to bitshifted ints to match other implementations --- src/aws_encryption_sdk/identifiers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 0bec8aacd..7f5cd3f1f 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -334,7 +334,7 @@ class KeyringTraceFlag(Enum): """KeyRing Trace actions.""" WRAPPING_KEY_GENERATED_DATA_KEY = 1 - WRAPPING_KEY_ENCRYPTED_DATA_KEY = 2 - WRAPPING_KEY_DECRYPTED_DATA_KEY = 3 - WRAPPING_KEY_SIGNED_ENC_CTX = 4 - WRAPPING_KEY_VERIFIED_ENC_CTX = 5 + WRAPPING_KEY_ENCRYPTED_DATA_KEY = 1 << 1 + WRAPPING_KEY_DECRYPTED_DATA_KEY = 1 << 2 + WRAPPING_KEY_SIGNED_ENC_CTX = 1 << 3 + WRAPPING_KEY_VERIFIED_ENC_CTX = 1 << 4 From 524d84737b9873705af33c525def7547eb0ef1fc Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 9 Jul 2019 14:21:12 -0700 Subject: [PATCH 14/22] normalize EncryptionMaterials._encrypted_data_keys to list and encrypted_data_keys to tuple --- src/aws_encryption_sdk/materials_managers/__init__.py | 4 ++-- test/unit/test_material_managers.py | 10 +++++----- test/unit/test_material_managers_default.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index a6609199a..a1947b100 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -243,7 +243,7 @@ def encrypted_data_keys(self): :rtype: frozenset """ - return frozenset(self._encrypted_data_keys) + return tuple(self._encrypted_data_keys) @property def is_complete(self): @@ -305,7 +305,7 @@ def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): if keyring_trace.wrapping_key != encrypted_data_key.key_provider: raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.") - self._encrypted_data_keys.add(encrypted_data_key) + self._encrypted_data_keys.append(encrypted_data_key) self._keyring_trace.append(keyring_trace) def add_signing_key(self, signing_key): diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index 2f64f3c74..975e9ffda 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -68,11 +68,11 @@ "EncryptionMaterials": dict( algorithm=ALGORITHM, data_encryption_key=_DATA_KEY, - encrypted_data_keys=set([]), + encrypted_data_keys=[], encryption_context={}, signing_key=_SIGNING_KEY.key_bytes(), ), - "DecryptionMaterialsRequest": dict(algorithm=ALGORITHM, encrypted_data_keys=set([]), encryption_context={}), + "DecryptionMaterialsRequest": dict(algorithm=ALGORITHM, encrypted_data_keys=[], encryption_context={}), "DecryptionMaterials": dict( data_key=_DATA_KEY, verification_key=_VERIFICATION_KEY.key_bytes(), algorithm=ALGORITHM, encryption_context={} ), @@ -143,7 +143,7 @@ def test_encryption_materials_request_attributes_defaults(): def test_encryption_materials_defaults(): test = EncryptionMaterials( - algorithm=ALGORITHM, data_encryption_key=_DATA_KEY, encrypted_data_keys=set([]), encryption_context={} + algorithm=ALGORITHM, data_encryption_key=_DATA_KEY, encrypted_data_keys=[], encryption_context={} ) assert test.signing_key is None @@ -229,7 +229,7 @@ def test_immutable_encrypted_data_keys(): materials = EncryptionMaterials(**_VALID_KWARGS["EncryptionMaterials"]) with pytest.raises(AttributeError): - materials.encrypted_data_keys.add(42) + materials.encrypted_data_keys.append(42) def test_empty_encrypted_data_keys(): @@ -237,7 +237,7 @@ def test_empty_encrypted_data_keys(): edks = materials.encrypted_data_keys - assert isinstance(edks, frozenset) + assert isinstance(edks, tuple) assert not edks diff --git a/test/unit/test_material_managers_default.py b/test/unit/test_material_managers_default.py index 20aaa8dd6..32fdc953a 100644 --- a/test/unit/test_material_managers_default.py +++ b/test/unit/test_material_managers_default.py @@ -41,7 +41,7 @@ def patch_for_dcmm_encrypt(mocker): DefaultCryptoMaterialsManager._generate_signing_key_and_update_encryption_context.return_value = mock_signing_key mocker.patch.object(aws_encryption_sdk.materials_managers.default, "prepare_data_keys") mock_data_encryption_key = _DATA_KEY - mock_encrypted_data_keys = set([_ENCRYPTED_DATA_KEY]) + mock_encrypted_data_keys = (_ENCRYPTED_DATA_KEY,) result_pair = mock_data_encryption_key, mock_encrypted_data_keys aws_encryption_sdk.materials_managers.default.prepare_data_keys.return_value = result_pair yield result_pair, mock_signing_key @@ -165,7 +165,7 @@ def test_get_encryption_materials_primary_mk_not_in_mks(patch_for_dcmm_encrypt): cmm = build_cmm() cmm.master_key_provider.master_keys_for_encryption.return_value = ( sentinel.primary_mk, - set([sentinel.mk_a, sentinel.mk_b]), + {sentinel.mk_a, sentinel.mk_b}, ) with pytest.raises(MasterKeyProviderError) as excinfo: From d786409032de23e20b9476cee88d314ef48b56ce Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 9 Jul 2019 14:21:38 -0700 Subject: [PATCH 15/22] temporarily pin pydocstyle at <4.0.0 to avoid issue breaking flake8-docstrings --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index e13ea2cb8..3e5677ece 100644 --- a/tox.ini +++ b/tox.ini @@ -117,6 +117,7 @@ basepython = python3 deps = flake8 flake8-docstrings + pydocstyle < 4.0.0 # https://github.com/JBKahn/flake8-print/pull/30 flake8-print>=3.1.0 flake8-bugbear From 888fc17450a96e1f0a442a38572daf32144613f8 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 10 Jul 2019 12:22:51 -0700 Subject: [PATCH 16/22] temporarily cap pydocstyle at <4.0.0 for decrypt oracle --- decrypt_oracle/tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/decrypt_oracle/tox.ini b/decrypt_oracle/tox.ini index 60aea91c7..23a9ece86 100644 --- a/decrypt_oracle/tox.ini +++ b/decrypt_oracle/tox.ini @@ -156,6 +156,7 @@ basepython = python3 deps = flake8 flake8-docstrings + pydocstyle < 4.0.0 # https://github.com/JBKahn/flake8-print/pull/30 flake8-print>=3.1.0 commands = From 1615d6334f41f4ae61822f027ab399f2a3d67f77 Mon Sep 17 00:00:00 2001 From: MeghaShetty Date: Fri, 12 Jul 2019 12:12:43 -0700 Subject: [PATCH 17/22] Keyring base API (#161) Add keyring interface --- src/aws_encryption_sdk/keyring/base.py | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/aws_encryption_sdk/keyring/base.py diff --git a/src/aws_encryption_sdk/keyring/base.py b/src/aws_encryption_sdk/keyring/base.py new file mode 100644 index 000000000..770b53c0b --- /dev/null +++ b/src/aws_encryption_sdk/keyring/base.py @@ -0,0 +1,54 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Base class interface for Keyrings.""" +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +class Keyring(object): + """Parent interface for Keyring classes. + + .. versionadded:: 1.5.0 + """ + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key if not present and encrypt it using any available wrapping key. + + :param encryption_materials: Encryption materials for the keyring to modify. + :type encryption_materials: aws_encryption_sdk.materials_managers.EncryptionMaterials + :returns: Optionally modified encryption materials. + :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :raises NotImplementedError: if method is not implemented + """ + raise NotImplementedError("Keyring does not implement on_encrypt function") + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param decryption_materials: Decryption materials for the keyring to modify. + :type decryption_materials: aws_encryption_sdk.materials_managers.DecryptionMaterials + :param encrypted_data_keys: List of encrypted data keys. + :type: Iterable of :class:`aws_encryption_sdk.structures.EncryptedDataKey` + :returns: Optionally modified decryption materials. + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :raises NotImplementedError: if method is not implemented + """ + raise NotImplementedError("Keyring does not implement on_decrypt function") From 3b62bc3628d3e39c6704f49be95bb89ce7921395 Mon Sep 17 00:00:00 2001 From: Caitlin Tibbetts Date: Tue, 23 Jul 2019 09:48:43 -0700 Subject: [PATCH 18/22] Testing something, want AppVeyor to run --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 06564ef6a..77b46ca70 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,10 @@ commands = all: {[testenv:base-command]commands} test/ examples/test/ manual: {[testenv:base-command]commands} +[testenv:py34] +basepython = python34 +deps = c:\python35\Lib\runpy.py + # Verify that local tests work without environment variables present [testenv:nocmk] basepython = python3 @@ -252,7 +256,7 @@ commands = python setup.py check -r -s [testenv:bandit] basepython = python3 -deps = +deps = bandit>=1.5.1 commands = bandit -r src/aws_encryption_sdk/ From 626d5ba224c0cbd3f973f4d6bf4f01e3a543d6d9 Mon Sep 17 00:00:00 2001 From: Caitlin Tibbetts Date: Tue, 23 Jul 2019 09:52:40 -0700 Subject: [PATCH 19/22] Quick change --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 77b46ca70..cf91274ff 100644 --- a/tox.ini +++ b/tox.ini @@ -57,8 +57,9 @@ commands = manual: {[testenv:base-command]commands} [testenv:py34] -basepython = python34 -deps = c:\python35\Lib\runpy.py +deps = + -rtest/requirements.txt + c:\python35\Lib\runpy.py # Verify that local tests work without environment variables present [testenv:nocmk] From 83f4ff8f9f211bbfd086fea5adf2fa68957d1332 Mon Sep 17 00:00:00 2001 From: Caitlin Tibbetts Date: Tue, 23 Jul 2019 10:39:04 -0700 Subject: [PATCH 20/22] Running AppVeyor --- setup.py | 3 +++ tox.ini | 7 +------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 1afa9d869..8bfa1613e 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ """AWS Encryption SDK for Python.""" import os import re +import ast from setuptools import find_packages, setup @@ -57,3 +58,5 @@ def get_requirements(): "Topic :: Security :: Cryptography", ], ) +if not hasattr(ast, "MatMult"): + print("HERE") diff --git a/tox.ini b/tox.ini index cf91274ff..389aff177 100644 --- a/tox.ini +++ b/tox.ini @@ -55,12 +55,7 @@ commands = examples: {[testenv:base-command]commands} examples/test/ -m examples all: {[testenv:base-command]commands} test/ examples/test/ manual: {[testenv:base-command]commands} - -[testenv:py34] -deps = - -rtest/requirements.txt - c:\python35\Lib\runpy.py - + # Verify that local tests work without environment variables present [testenv:nocmk] basepython = python3 From 00959892573daaf69f7b9aea74e60e32cfc79d6f Mon Sep 17 00:00:00 2001 From: Caitlin Tibbetts Date: Tue, 23 Jul 2019 13:37:43 -0700 Subject: [PATCH 21/22] Upgrading pip --- appveyor.yml | 1 + setup.py | 2 -- test/integration/integration_test_utils.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index cfb4bdcdb..c5da2f15c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -105,6 +105,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # Check the Python version to verify the correct version was installed - "python --version" + - "python -m pip install --upgrade pip" - "python -m pip install wheel tox" build: off diff --git a/setup.py b/setup.py index 8bfa1613e..4fa9fe709 100644 --- a/setup.py +++ b/setup.py @@ -58,5 +58,3 @@ def get_requirements(): "Topic :: Security :: Cryptography", ], ) -if not hasattr(ast, "MatMult"): - print("HERE") diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index a5b4d6001..be1603391 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -30,7 +30,7 @@ def get_cmk_arn(): ) if arn.startswith("arn:") and ":alias/" not in arn: return arn - raise ValueError("KMS CMK ARN provided for integration tests much be a key not an alias") + raise ValueError("KMS CMK ARN provided for integration tests must be a key not an alias") def setup_kms_master_key_provider(cache=True): From 6c2aec652a295e06230ec10b44449d92d391478c Mon Sep 17 00:00:00 2001 From: Caitlin Tibbetts Date: Tue, 23 Jul 2019 13:49:06 -0700 Subject: [PATCH 22/22] Linting issue --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 4fa9fe709..1afa9d869 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ """AWS Encryption SDK for Python.""" import os import re -import ast from setuptools import find_packages, setup