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/decrypt_oracle/tox.ini b/decrypt_oracle/tox.ini index f0a7804e5..23a9ece86 100644 --- a/decrypt_oracle/tox.ini +++ b/decrypt_oracle/tox.ini @@ -156,7 +156,7 @@ basepython = python3 deps = flake8 flake8-docstrings - pydocstyle<4.0.0 + pydocstyle < 4.0.0 # https://github.com/JBKahn/flake8-print/pull/30 flake8-print>=3.1.0 commands = 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 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/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 1bd9bb1f1..7f5cd3f1f 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 = 1 << 1 + WRAPPING_KEY_DECRYPTED_DATA_KEY = 1 << 2 + WRAPPING_KEY_SIGNED_ENC_CTX = 1 << 3 + WRAPPING_KEY_VERIFIED_ENC_CTX = 1 << 4 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") diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index bc5230c51..a1947b100 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -16,10 +16,19 @@ """ import attr import six +from attr.validators import deep_iterable, deep_mapping, instance_of, optional -from ..identifiers import Algorithm -from ..internal.utils.streams import ROStream -from ..structures import DataKey +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 + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + 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 @attr.s(hash=False) @@ -40,38 +49,285 @@ 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)) + 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 +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 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` + """ + + 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)) + ) ) - 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)) + data_encryption_key = attr.ib( + default=None, validator=optional(instance_of(RawDataKey)), converter=_data_key_to_raw_data_key ) + _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 -@attr.s(hash=False) -class EncryptionMaterials(object): + 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") + + 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) + + 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 + :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 + :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): + # type: () -> Tuple[KeyringTrace] + """Return a read-only version of the keyring trace. + + :rtype: tuple + """ + return tuple(self._keyring_trace) + + +@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))) + _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, + algorithm=None, + data_encryption_key=None, + encrypted_data_keys=None, + 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") + + 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") + + if encrypted_data_keys is None: + encrypted_data_keys = [] + + super(EncryptionMaterials, self).__init__( + algorithm=algorithm, + encryption_context=encryption_context, + data_encryption_key=data_encryption_key, + **kwargs + ) + self._setattr("signing_key", signing_key) + self._setattr("_encrypted_data_keys", encrypted_data_keys) + attr.validate(self) + + @property + def encrypted_data_keys(self): + # type: () -> FrozenSet[EncryptedDataKey] + """Return a read-only version of the encrypted data keys. + + :rtype: frozenset + """ + return tuple(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. + + .. 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 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.") + + if keyring_trace.wrapping_key != encrypted_data_key.key_provider: + raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.") + + self._encrypted_data_keys.append(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.") + + # 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) @attr.s(hash=False) @@ -87,21 +343,126 @@ 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)) + 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) + ) + ) -@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**, **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 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))) + verification_key = attr.ib(default=None, repr=False, validator=optional(instance_of(bytes))) + + def __init__( + self, data_key=_DEFAULT_SENTINEL, verification_key=None, **kwargs + ): # noqa we define this in the class docstring + + 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"): + if legacy_missing not in kwargs: + kwargs[legacy_missing] = None + + super(DecryptionMaterials, self).__init__(**kwargs) + + 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 + """Backwards-compatible shim for access to data key.""" + return self.data_encryption_key + + 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 + """ + 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, + 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.") + + # 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) diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index 8229d65fb..635f661ce 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -11,48 +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, 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. @@ -61,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) @@ -74,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) @@ -88,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) @@ -102,5 +82,72 @@ 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(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) + ), + ) + 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)) + frame_length = attr.ib(hash=True, validator=instance_of(six.integer_types)) 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): 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==", diff --git a/test/unit/test_material_managers.py b/test/unit/test_material_managers.py index fcd4977f5..975e9ffda 100644 --- a/test/unit/test_material_managers.py +++ b/test/unit/test_material_managers.py @@ -11,71 +11,129 @@ # 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.identifiers import Algorithm +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 ( + CryptographicMaterials, DecryptionMaterials, DecryptionMaterialsRequest, EncryptionMaterials, EncryptionMaterialsRequest, + _data_key_to_raw_data_key, ) -from aws_encryption_sdk.structures import DataKey +from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, KeyringTrace, 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", +) +_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( + algorithm=ALGORITHM, + encryption_context={"additional": "data"}, + data_encryption_key=_DATA_KEY, + 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), - encrypted_data_keys=set([]), + algorithm=ALGORITHM, + data_encryption_key=_DATA_KEY, + encrypted_data_keys=[], encryption_context={}, - signing_key=b"", + signing_key=_SIGNING_KEY.key_bytes(), ), - "DecryptionMaterialsRequest": dict( - algorithm=MagicMock(__class__=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={} ), - "DecryptionMaterials": dict(data_key=MagicMock(__class__=DataKey), verification_key=b"ex_verification_key"), } +_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", ( + (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")), + (EncryptionMaterials, dict(data_encryption_key=_REMOVE)), (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)), ), ) def test_attributes_fails(attr_class, invalid_kwargs): - kwargs = _VALID_KWARGS[attr_class.__name__].copy() - kwargs.update(invalid_kwargs) + 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 @@ -85,14 +143,337 @@ 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=[], 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 == _RAW_DATA_KEY + assert test.data_key == _RAW_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) + + +@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"]) + + with pytest.raises(AttributeError): + materials.encrypted_data_keys.append(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, tuple) + assert not edks + + +@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, encrypted_data_keys=_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, encrypted_data_keys=_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, encrypted_data_keys=_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, 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, + "Keyring trace does not match data key provider.", + ) + yield ( + material_class, + 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, + r"Invalid data key length *", + ) + yield ( + DecryptionMaterials, + 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", + ) + + +@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( + "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"), + flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY}, + ), + InvalidKeyringTraceError, + "Keyring trace does not match data key encryptor.", + ), + ( + 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} + ), + AttributeError, + "Data encryption key is not set.", + ), + ), +) +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: + 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=_SIGNING_KEY.key_bytes()) + + +@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=_VERIFICATION_KEY.key_bytes()) + + +@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) + + with pytest.raises(exception_type) as excinfo: + 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 diff --git a/test/unit/test_material_managers_default.py b/test/unit/test_material_managers_default.py index 9d6bd949f..32fdc953a 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 +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([mock_data_encryption_key]) + mock_data_encryption_key = _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 @@ -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,8 +134,8 @@ 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.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] @@ -158,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: @@ -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..e1070c574 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 *") diff --git a/tox.ini b/tox.ini index 06564ef6a..dee3c9be3 100644 --- a/tox.ini +++ b/tox.ini @@ -117,7 +117,7 @@ basepython = python3 deps = flake8 flake8-docstrings - pydocstyle<4.0.0 + pydocstyle < 4.0.0 # https://github.com/JBKahn/flake8-print/pull/30 flake8-print>=3.1.0 flake8-bugbear @@ -252,7 +252,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/