From a3d947d88c674825a64375008af054a0c0b56ad0 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 20 Jun 2019 17:36:09 -0700 Subject: [PATCH 01/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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/64] 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 004ea5d9b727bfce305805b4257fcc3ac2c90790 Mon Sep 17 00:00:00 2001 From: MeghaShetty Date: Thu, 1 Aug 2019 12:06:11 -0700 Subject: [PATCH 18/64] Add keyring base class (#176) * Adding Keyring API * Delete __init__.py * Delete raw_keyring.py * Added docstring to public class * Edited docstring * Edited docstring again * Changes in docstring statements * Docstring changes * Changes in docstring * Raw keyring initial * Raw keyring encrypt commit * Encrypt functions for Raw RSA and AES * Raw RSA and AES initial * Changes in raw keyrings according to new keyring materials * Updated with autoformat * Modified base * Corrected tox and flake errors * Docstring error correction * Added docstrings and corrected errors * Some more changes in docstrings * Updating base API * Made all suggested changes * Corrected tox and flake8 errors * Minor change in raw-keyrings * Adding Keyring API * Delete __init__.py * Delete raw_keyring.py * Added docstring to public class * Edited docstring * Edited docstring again * Changes in docstring statements * Docstring changes * Changes in docstring * Raw keyring initial * Raw keyring encrypt commit * Encrypt functions for Raw RSA and AES * Raw RSA and AES initial * bump attrs to 19.1.0 * add keyring trace and integrate into updated encrytion/decryption materials * s/KeyRing/Keyring/g * align cryptographic materials and add write-only interface * encrypted_data_keys must only contain EncryptedDataKey * fix test to be Python 2 compatible * Changes in raw keyrings according to new keyring materials * Updated with autoformat * Modified base * data encryption key must be set before encrypted data keys can be added to EncryptionMaterials * Corrected tox and flake errors * Docstring error correction * Added docstrings and corrected errors * Some more changes in docstrings * Updating base API * add signing/verification key checks to Encryption/DecryptionMaterials * DecryptionMaterials.algorithm must be set before DecryptionMaterials.add_data_encryption_key can be called * update materials docs and typehints * Made all suggested changes * EncryptionMaterials must not be initialized with encrypted_data_keys but no data_encryption_key * add is_complete properties to EncryptionMaterials and DecryptionMaterials * Corrected tox and flake8 errors * Minor change in raw-keyrings * change KeyringTraceFlag values to bitshifted ints to match other implementations * normalize EncryptionMaterials._encrypted_data_keys to list and encrypted_data_keys to tuple * temporarily pin pydocstyle at <4.0.0 to avoid issue breaking flake8-docstrings * temporarily cap pydocstyle at <4.0.0 for decrypt oracle * Changes to keyring trace in raw keyrings * Adding test files * Adding tests * Changed data encryption key type to RawDataKey * Added keyring trace to pytest encryption materials * Changed value of keyring_trace.wrapping_key * Few changes to match new API * Tox errors * Functional tests pass * Formatting errors corrected and functional tests pass * Corrected too broad exception error and deleted empty return statement from tests * Changed Exeception to BaseException to solve broad exception error * Added suppress broad exception * Added pylint disable broad exception * Changed wrapping keys for RSA keyrings from WrappingKey to cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey/RSAPublicKey * Fixed tox errors * More tox errors * Moved code for generation of plaintext to be before the check for key being private or public * Tox errors * Added metaclass to base API and unit tests for base API * Changed metaclass to six.add_metaclass in base API * Fixed pylint errors * Fixed more pylint errors * Removed RawAESKeyring instance * Changed on_encrypt_helper to generate_data_key and removed on_decrypt_helper. Renamed base API unit test file * Changed docstring for generate_data_key * Changed decryption_materials.data_key to decryption_materials.data_encryption_key and fixed pylint errors * Fixed pylint errors * Changed raw keyrings to have class methods for PEM and DER encoded keys * Unit tests for raw keyrings * Changes for PEM encoding * Changed base API to remove metaclass and modified tests * Delete raw_keyring.py * Delete test_f_keyring_raw_aes.py * Delete test_f_keyring_raw_rsa.py * Delete test_keyring_raw_aes.py * Delete test_keyring_raw_rsa.py * Suggested changes * Made suggested changes to base API and tests * Made suggested changes to base API tests * Ignore commit * Corrected tox and pylint errors in base API unit tests * Removed try except for Iterable * Removed try except for Iterable from test_utils * Added try except for Iterable in base API * Resolved isort errors --- requirements.txt | 1 + src/aws_encryption_sdk/keyring/__init__.py | 13 +++++ .../materials_managers/__init__.py | 4 +- test/unit/test_keyring_base.py | 45 +++++++++++++++++ test/unit/test_values.py | 49 +++++++++++++++++++ 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/aws_encryption_sdk/keyring/__init__.py create mode 100644 test/unit/test_keyring_base.py diff --git a/requirements.txt b/requirements.txt index 67be39a90..8378d126e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +six boto3>=1.4.4 cryptography>=1.8.1 attrs>=19.1.0 diff --git a/src/aws_encryption_sdk/keyring/__init__.py b/src/aws_encryption_sdk/keyring/__init__.py new file mode 100644 index 000000000..ada03b4d7 --- /dev/null +++ b/src/aws_encryption_sdk/keyring/__init__.py @@ -0,0 +1,13 @@ +# 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. +"""All provided Keyrings.""" diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index a1947b100..0a6dcd2f0 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -191,7 +191,7 @@ class EncryptionMaterials(CryptographicMaterials): 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 RawDataKey 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` @@ -370,7 +370,7 @@ class DecryptionMaterials(CryptographicMaterials): 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 RawDataKey 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) diff --git a/test/unit/test_keyring_base.py b/test/unit/test_keyring_base.py new file mode 100644 index 000000000..70863de53 --- /dev/null +++ b/test/unit/test_keyring_base.py @@ -0,0 +1,45 @@ +# Copyright 2019 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. +"""Unit tests for base keyring.""" + +import pytest + +from aws_encryption_sdk.identifiers import Algorithm +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials + +pytestmark = [pytest.mark.unit, pytest.mark.local] + +_encryption_materials = EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context={"encryption": "context", "values": "here"}, + signing_key=b"aws-crypto-public-key", +) + +_decryption_materials = DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, verification_key=b"ex_verification_key" +) + +_encrypted_data_keys = [] + + +def test_keyring_no_encrypt(): + with pytest.raises(NotImplementedError) as exc_info: + Keyring().on_encrypt(encryption_materials=_encryption_materials) + assert exc_info.match("Keyring does not implement on_encrypt function") + + +def test_keyring_no_decrypt(): + with pytest.raises(NotImplementedError) as exc_info: + Keyring().on_decrypt(decryption_materials=_decryption_materials, encrypted_data_keys=_encrypted_data_keys) + assert exc_info.match("Keyring does not implement on_decrypt function") diff --git a/test/unit/test_values.py b/test/unit/test_values.py index 26eff1341..b9aba0c02 100644 --- a/test/unit/test_values.py +++ b/test/unit/test_values.py @@ -187,6 +187,55 @@ def array_byte(source): "\xff\x8fn\x95\xf0\xf0E\x91Uj\xb0E3=\x0e\x1a\xf1'4\xf6" ), "signature_len": b"\x00h", + "private_rsa_key_bytes": [ + ( + b"-----BEGIN RSA PRIVATE KEY-----" + b"MIICXgIBAAKBgQCUjhI8YRPXV8Gfofbg/" + b"PLjWw2AzowQTPErLU2z3+xGqElMdzdiC4Ta43DFWZg34Eg0X8kQPAeoe8h3cRSMo" + b"77eSOHt2dPo7OfTfZqsH8766fivHIKVxBYPX8SZYIUhMtRnlg3uqch9BksfRop+h" + b"f8h/H3lfervJoevS2CXYB9/iwIDAQABAoGBAIqeGzQOHbaGI51yQ2zjez1dPDdiB" + b"F49fZideHEM1GuGIodgguRQ/VJGgncUSC5zcMy2SGaGrVqwznltohAtxy4rZp0eh" + b"2O3aHYi9Wehd0SPLh+qwu7mJDuh0z15hmCOue070FnUtyuSwhXLwDrbot2+5HbmF" + b"9clJLI5tv92gvIpAkEA+Bv5i8XJNPN1rao31aQFoi9bFIOEclk3b1RbLX6mpZBFS" + b"U9CNUy0RQNC0+H3KZ5CTvsyFGpMfTdiFc/Qdesk3QJBAJlHjrvoadP+PU3zXYrWR" + b"D5EryyTxaP1bOjrp9xLuQBeU8x7EVJdpoul9OmwcT3NrAqvxDE9okjha2tjCI6O2" + b"4cCQQDMyOJPYL/zaaPO5LlTKB/SPv4RT4BplYPw6xKa2XeZHhxiJv5B2f7NG6T0G" + b"AWWn16hrCoouZhKngTidfXc7motAkA/KiTgvKr3yHp86AAxWZDv1CAYD6FPqrDB3" + b"3LiLnZDd5uy1ThTJ/Kc87vUnXhdDqeKE9qWrB53SCWbMElzbd17AkEA4DMp+6ngM" + b"o6sS0dY1X6nTLqgvK3B0z5GCAdSEy3Y8jh995Lrl+hy88HzuwUkQwwPlZkFhUNCx" + b"edrC6cTKE5xLA==" + b"-----END RSA PRIVATE KEY-----" + ), + ( + b"-----BEGIN RSA PRIVATE KEY-----\n" + b"MIIEowIBAAKCAQEAo8uCyhiO4JUGZV+rtNq5DBA9Lm4xkw5kTA3v6EPybs8bVXL2\n" + b"ZE6jkbo+xT4Jg/bKzUpnp1fE+T1ruGPtsPdoEmhY/P64LDNIs3sRq5U4QV9IETU1\n" + b"vIcbNNkgGhRjV8J87YNY0tV0H7tuWuZRpqnS+gjV6V9lUMkbvjMCc5IBqQc3heut\n" + b"/+fH4JwpGlGxOVXI8QAapnSy1XpCr3+PT29kydVJnIMuAoFrurojRpOQbOuVvhtA\n" + b"gARhst1Ji4nfROGYkj6eZhvkz2Bkud4/+3lGvVU5LO1vD8oY7WoGtpin3h50VcWe\n" + b"aBT4kejx4s9/G9C4R24lTH09J9HO2UUsuCqZYQIDAQABAoIBAQCfC90bCk+qaWqF\n" + b"gymC+qOWwCn4bM28gswHQb1D5r6AtKBRD8mKywVvWs7azguFVV3Fi8sspkBA2FBC\n" + b"At5p6ULoJOTL/TauzLl6djVJTCMM701WUDm2r+ZOIctXJ5bzP4n5Q4I7b0NMEL7u\n" + b"ixib4elYGr5D1vrVQAKtZHCr8gmkqyx8Mz7wkJepzBP9EeVzETCHsmiQDd5WYlO1\n" + b"C2IQYgw6MJzgM4entJ0V/GPytkodblGY95ORVK7ZhyNtda+r5BZ6/jeMW+hA3VoK\n" + b"tHSWjHt06ueVCCieZIATmYzBNt+zEz5UA2l7ksg3eWfVORJQS7a6Ef4VvbJLM9Ca\n" + b"m1kdsjelAoGBANKgvRf39i3bSuvm5VoyJuqinSb/23IH3Zo7XOZ5G164vh49E9Cq\n" + b"dOXXVxox74ppj/kbGUoOk+AvaB48zzfzNvac0a7lRHExykPH2kVrI/NwH/1OcT/x\n" + b"2e2DnFYocXcb4gbdZQ+m6X3zkxOYcONRzPVW1uMrFTWHcJveMUm4PGx7AoGBAMcU\n" + b"IRvrT6ye5se0s27gHnPweV+3xjsNtXZcK82N7duXyHmNjxrwOAv0SOhUmTkRXArM\n" + b"6aN5D8vyZBSWma2TgUKwpQYFTI+4Sp7sdkkyojGAEixJ+c5TZJNxZFrUe0FwAoic\n" + b"c2kb7ntaiEj5G+qHvykJJro5hy6uLnjiMVbAiJDTAoGAKb67241EmHAXGEwp9sdr\n" + b"2SMjnIAnQSF39UKAthkYqJxa6elXDQtLoeYdGE7/V+J2K3wIdhoPiuY6b4vD0iX9\n" + b"JcGM+WntN7YTjX2FsC588JmvbWfnoDHR7HYiPR1E58N597xXdFOzgUgORVr4PMWQ\n" + b"pqtwaZO3X2WZlvrhr+e46hMCgYBfdIdrm6jYXFjL6RkgUNZJQUTxYGzsY+ZemlNm\n" + b"fGdQo7a8kePMRuKY2MkcnXPaqTg49YgRmjq4z8CtHokRcWjJUWnPOTs8rmEZUshk\n" + b"0KJ0mbQdCFt/Uv0mtXgpFTkEZ3DPkDTGcV4oR4CRfOCl0/EU/A5VvL/U4i/mRo7h\n" + b"ye+xgQKBgD58b+9z+PR5LAJm1tZHIwb4tnyczP28PzwknxFd2qylR4ZNgvAUqGtU\n" + b"xvpUDpzMioz6zUH9YV43YNtt+5Xnzkqj+u9Mr27/H2v9XPwORGfwQ5XPwRJz/2oC\n" + b"EnPmP1SZoY9lXKUpQXHXSpDZ2rE2Klt3RHMUMHt8Zpy36E8Vwx8o\n" + b"-----END RSA PRIVATE KEY-----\n" + ), + ], } VALUES["updated_encryption_context"] = copy.deepcopy(VALUES["encryption_context"]) VALUES["updated_encryption_context"]["aws-crypto-public-key"] = VALUES["encoded_curve_point"] From fcc05bad549a6490eb70e797b0b59d8ebd673952 Mon Sep 17 00:00:00 2001 From: MeghaShetty Date: Fri, 2 Aug 2019 14:55:04 -0700 Subject: [PATCH 19/64] Raw keyrings (#165) * Adding Keyring API * Delete __init__.py * Delete raw_keyring.py * Added docstring to public class * Edited docstring * Edited docstring again * Changes in docstring statements * Docstring changes * Changes in docstring * Raw keyring initial * Raw keyring encrypt commit * Encrypt functions for Raw RSA and AES * Raw RSA and AES initial * Changes in raw keyrings according to new keyring materials * Updated with autoformat * Modified base * Corrected tox and flake errors * Docstring error correction * Added docstrings and corrected errors * Some more changes in docstrings * Updating base API * Made all suggested changes * Corrected tox and flake8 errors * Minor change in raw-keyrings * Adding Keyring API * Delete __init__.py * Delete raw_keyring.py * Added docstring to public class * Edited docstring * Edited docstring again * Changes in docstring statements * Docstring changes * Changes in docstring * Raw keyring initial * Raw keyring encrypt commit * Encrypt functions for Raw RSA and AES * Raw RSA and AES initial * bump attrs to 19.1.0 * add keyring trace and integrate into updated encrytion/decryption materials * s/KeyRing/Keyring/g * align cryptographic materials and add write-only interface * encrypted_data_keys must only contain EncryptedDataKey * fix test to be Python 2 compatible * Changes in raw keyrings according to new keyring materials * Updated with autoformat * Modified base * data encryption key must be set before encrypted data keys can be added to EncryptionMaterials * Corrected tox and flake errors * Docstring error correction * Added docstrings and corrected errors * Some more changes in docstrings * Updating base API * add signing/verification key checks to Encryption/DecryptionMaterials * DecryptionMaterials.algorithm must be set before DecryptionMaterials.add_data_encryption_key can be called * update materials docs and typehints * Made all suggested changes * EncryptionMaterials must not be initialized with encrypted_data_keys but no data_encryption_key * add is_complete properties to EncryptionMaterials and DecryptionMaterials * Corrected tox and flake8 errors * Minor change in raw-keyrings * change KeyringTraceFlag values to bitshifted ints to match other implementations * normalize EncryptionMaterials._encrypted_data_keys to list and encrypted_data_keys to tuple * temporarily pin pydocstyle at <4.0.0 to avoid issue breaking flake8-docstrings * temporarily cap pydocstyle at <4.0.0 for decrypt oracle * Changes to keyring trace in raw keyrings * Adding test files * Adding tests * Changed data encryption key type to RawDataKey * Added keyring trace to pytest encryption materials * Changed value of keyring_trace.wrapping_key * Few changes to match new API * Tox errors * Functional tests pass * Formatting errors corrected and functional tests pass * Corrected too broad exception error and deleted empty return statement from tests * Changed Exeception to BaseException to solve broad exception error * Added suppress broad exception * Added pylint disable broad exception * Changed wrapping keys for RSA keyrings from WrappingKey to cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey/RSAPublicKey * Fixed tox errors * More tox errors * Moved code for generation of plaintext to be before the check for key being private or public * Tox errors * Added metaclass to base API and unit tests for base API * Changed metaclass to six.add_metaclass in base API * Fixed pylint errors * Fixed more pylint errors * Removed RawAESKeyring instance * Changed on_encrypt_helper to generate_data_key and removed on_decrypt_helper. Renamed base API unit test file * Changed docstring for generate_data_key * Changed decryption_materials.data_key to decryption_materials.data_encryption_key and fixed pylint errors * Fixed pylint errors * Changed raw keyrings to have class methods for PEM and DER encoded keys * Unit tests for raw keyrings * Changes for PEM encoding * Made suggested changes to raw keyrings * partial commit for raw keyrings * Made suggested changes * Changed wrapping_key_id in deserialize_wrapped_key() back to self.key_name * Decryption and PEM input now works * Adding sample * Removed test comments * Unit tests for raw aes and rsa * All unit tests working * All unit tests done. Functional tests - key_info_prefix_vectors for AES and compatibility with MKP for RSA remaining * Delete sample_aes.py * Corrected tox and pylint errors * Removed print statements used while debugging * Partial commit for changes to tests * Partial commit for tests for raw keyrings * All tests except compatibility of raw rsa with mkp and key info prefix in raw aes * Pulled from keyring branch * Updated base API * Added test for key info prefix * Changed unittest.mock to mock * Raw keyrings test partial commit * All tests for raw keyrings work * Removed unused imports * Removed unused imports --- src/aws_encryption_sdk/keyring/raw_keyring.py | 436 ++++++++++++++++++ test/functional/test_f_keyring_raw_aes.py | 202 ++++++++ test/functional/test_f_keyring_raw_rsa.py | 297 ++++++++++++ test/unit/test_keyring_raw_aes.py | 282 +++++++++++ test/unit/test_keyring_raw_rsa.py | 249 ++++++++++ test/unit/test_utils.py | 3 +- test/unit/unit_test_utils.py | 143 ++++++ 7 files changed, 1611 insertions(+), 1 deletion(-) create mode 100644 src/aws_encryption_sdk/keyring/raw_keyring.py create mode 100644 test/functional/test_f_keyring_raw_aes.py create mode 100644 test/functional/test_f_keyring_raw_rsa.py create mode 100644 test/unit/test_keyring_raw_aes.py create mode 100644 test/unit/test_keyring_raw_rsa.py diff --git a/src/aws_encryption_sdk/keyring/raw_keyring.py b/src/aws_encryption_sdk/keyring/raw_keyring.py new file mode 100644 index 000000000..513119246 --- /dev/null +++ b/src/aws_encryption_sdk/keyring/raw_keyring.py @@ -0,0 +1,436 @@ +# 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. +"""Resources required for Raw Keyrings.""" + +import logging +import os + +import attr +import six +from attr.validators import instance_of, optional +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from aws_encryption_sdk.exceptions import GenerateKeyError +from aws_encryption_sdk.identifiers import EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import EncryptedData, WrappingKey +from aws_encryption_sdk.internal.formatting.deserialize import deserialize_wrapped_key +from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix, serialize_wrapped_key +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey + +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 + +_LOGGER = logging.getLogger(__name__) + + +def _generate_data_key( + encryption_materials, # type: EncryptionMaterials + key_provider, # type: MasterKeyInfo +): + # type: (...) -> bytes + """Generates plaintext data key for the keyring. + + :param encryption_materials: Encryption materials for the keyring to modify. + :type encryption_materials: aws_encryption_sdk.materials_managers.EncryptionMaterials + :param key_provider: Information about the key in the keyring. + :type key_provider: MasterKeyInfo + :return bytes: Plaintext data key + """ + # Check if encryption materials contain data encryption key + if encryption_materials.data_encryption_key is not None: + raise TypeError("Data encryption key already exists.") + + # Generate data key + try: + plaintext_data_key = os.urandom(encryption_materials.algorithm.kdf_input_len) + except Exception: # pylint: disable=broad-except + error_message = "Unable to generate data encryption key." + _LOGGER.exception(error_message) + raise GenerateKeyError("Unable to generate data encryption key.") + + # Create a keyring trace + keyring_trace = KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}) + + # plaintext_data_key to RawDataKey + data_encryption_key = RawDataKey(key_provider=key_provider, data_key=plaintext_data_key) + + # Add generated data key to encryption_materials + encryption_materials.add_data_encryption_key(data_encryption_key, keyring_trace) + + return plaintext_data_key + + +@attr.s +class RawAESKeyring(Keyring): + """Generate an instance of Raw AES Keyring which encrypts using AES-GCM algorithm using wrapping key provided as a + byte array + + :param str key_namespace: String defining the keyring. + :param bytes key_name: Key ID + :param bytes wrapping_key: Encryption key with which to wrap plaintext data key. + :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key. + :type wrapping_algorithm: WrappingAlgorithm + + .. note:: + Only one wrapping key can be specified in a Raw AES Keyring + """ + + key_namespace = attr.ib(validator=instance_of(six.string_types)) + key_name = attr.ib(validator=instance_of(six.binary_type)) + _wrapping_key = attr.ib(repr=False, validator=instance_of(six.binary_type)) + _wrapping_algorithm = attr.ib(repr=False, validator=instance_of(WrappingAlgorithm)) + + def __attrs_post_init__(self): + # type: () -> None + """Prepares initial values not handled by attrs.""" + self._key_provider = MasterKeyInfo(provider_id=self.key_namespace, key_info=self.key_name) + + self._wrapping_key_structure = WrappingKey( + wrapping_algorithm=self._wrapping_algorithm, + wrapping_key=self._wrapping_key, + wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ) + + self._key_info_prefix = self._get_key_info_prefix( + key_namespace=self.key_namespace, key_name=self.key_name, wrapping_key=self._wrapping_key_structure + ) + + @staticmethod + def _get_key_info_prefix(key_namespace, key_name, wrapping_key): + # type: (str, bytes, WrappingKey) -> six.binary_type + """Helper function to get key info prefix + + :param str key_namespace: String defining the keyring. + :param bytes key_name: Key ID + :param wrapping_key: Encryption key with which to wrap plaintext data key. + :type wrapping_key: WrappingKey + :return: Serialized key_info prefix + :rtype: bytes + """ + key_info_prefix = serialize_raw_master_key_prefix( + RawMasterKey(provider_id=key_namespace, key_id=key_name, wrapping_key=wrapping_key) + ) + return key_info_prefix + + 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 + """ + if encryption_materials.data_encryption_key is None: + _generate_data_key(encryption_materials=encryption_materials, key_provider=self._key_provider) + + try: + # Encrypt data key + encrypted_wrapped_key = self._wrapping_key_structure.encrypt( + plaintext_data_key=encryption_materials.data_encryption_key.data_key, + encryption_context=encryption_materials.encryption_context, + ) + + # EncryptedData to EncryptedDataKey + encrypted_data_key = serialize_wrapped_key( + key_provider=self._key_provider, + wrapping_algorithm=self._wrapping_algorithm, + wrapping_key_id=self.key_name, + encrypted_wrapped_key=encrypted_wrapped_key, + ) + except Exception: # pylint: disable=broad-except + error_message = "Raw AES Keyring unable to encrypt data key" + _LOGGER.exception(error_message) + return encryption_materials + + # Update Keyring Trace + keyring_trace = KeyringTrace( + wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + ) + + # Add encrypted data key to encryption_materials + encryption_materials.add_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) + return encryption_materials + + 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: List of `aws_encryption_sdk.structures.EncryptedDataKey` + :returns: Optionally modified decryption materials + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + """ + if decryption_materials.data_encryption_key is not None: + return decryption_materials + + # Decrypt data key + expected_key_info_len = len(self._key_info_prefix) + self._wrapping_algorithm.algorithm.iv_len + for key in encrypted_data_keys: + + if decryption_materials.data_encryption_key is not None: + return decryption_materials + + if ( + key.key_provider.provider_id != self._key_provider.provider_id + or len(key.key_provider.key_info) != expected_key_info_len + or not key.key_provider.key_info.startswith(self._key_info_prefix) + ): + continue + + # Wrapped EncryptedDataKey to deserialized EncryptedData + encrypted_wrapped_key = deserialize_wrapped_key( + wrapping_algorithm=self._wrapping_algorithm, wrapping_key_id=self.key_name, wrapped_encrypted_key=key + ) + + # EncryptedData to raw key string + try: + plaintext_data_key = self._wrapping_key_structure.decrypt( + encrypted_wrapped_data_key=encrypted_wrapped_key, + encryption_context=decryption_materials.encryption_context, + ) + + except Exception: # pylint: disable=broad-except + error_message = "Raw AES Keyring unable to decrypt data key" + _LOGGER.exception(error_message) + return decryption_materials + + # Create a keyring trace + keyring_trace = KeyringTrace( + wrapping_key=self._key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY} + ) + + # Update decryption materials + data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) + decryption_materials.add_data_encryption_key(data_encryption_key, keyring_trace) + + return decryption_materials + + +@attr.s +class RawRSAKeyring(Keyring): + """Generate an instance of Raw RSA Keyring which performs asymmetric encryption and decryption using public + and private keys provided + + :param str key_namespace: String defining the keyring ID + :param bytes key_name: Key ID + :param private_wrapping_key: Private encryption key with which to wrap plaintext data key (optional) + :type private_wrapping_key: RSAPrivateKey + :param public_wrapping_key: Public encryption key with which to wrap plaintext data key (optional) + :type public_wrapping_key: RSAPublicKey + :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :type wrapping_algorithm: WrappingAlgorithm + :param key_provider: Complete information about the key in the keyring + :type key_provider: MasterKeyInfo + + .. note:: + At least one of public wrapping key or private wrapping key must be provided. + """ + + key_namespace = attr.ib(validator=instance_of(six.string_types)) + key_name = attr.ib(validator=instance_of(six.binary_type)) + _wrapping_algorithm = attr.ib(repr=False, validator=instance_of(WrappingAlgorithm)) + _private_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(rsa.RSAPrivateKey))) + _public_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(rsa.RSAPublicKey))) + + def __attrs_post_init__(self): + # type: () -> None + """Prepares initial values not handled by attrs.""" + self._key_provider = MasterKeyInfo(provider_id=self.key_namespace, key_info=self.key_name) + + if self._public_wrapping_key is None and self._private_wrapping_key is None: + raise TypeError("At least one of public key or private key must be provided.") + + if self._private_wrapping_key is not None and self._public_wrapping_key is None: + self._public_wrapping_key = self._private_wrapping_key.public_key() + + @classmethod + def from_pem_encoding( + cls, + key_namespace, # type: str + key_name, # type: bytes + wrapping_algorithm, # type: WrappingAlgorithm + public_encoded_key=None, # type: bytes + private_encoded_key=None, # type: bytes + password=None, # type: bytes + ): + # type: (...) -> RawRSAKeyring + """Generate a Raw RSA keyring using PEM Encoded public and private keys + + :param str key_namespace: String defining the keyring ID + :param bytes key_name: Key ID + :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :type wrapping_algorithm: WrappingAlgorithm + :param bytes public_encoded_key: PEM encoded public key (optional) + :param bytes private_encoded_key: PEM encoded private key (optional) + :param bytes password: Password to load private key (optional) + :return: Calls RawRSAKeyring class with required parameters + """ + loaded_private_wrapping_key = loaded_public_wrapping_key = None + if private_encoded_key is not None: + loaded_private_wrapping_key = serialization.load_pem_private_key( + data=private_encoded_key, password=password, backend=default_backend() + ) + if public_encoded_key is not None: + loaded_public_wrapping_key = serialization.load_pem_public_key( + data=public_encoded_key, backend=default_backend() + ) + + return cls( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=loaded_private_wrapping_key, + public_wrapping_key=loaded_public_wrapping_key, + ) + + @classmethod + def from_der_encoding( + cls, + key_namespace, # type: str + key_name, # type: bytes + wrapping_algorithm, # type: WrappingAlgorithm + public_encoded_key=None, # type: bytes + private_encoded_key=None, # type: bytes + password=None, # type: bytes + ): + """Generate a raw RSA keyring using DER Encoded public and private keys + + :param str key_namespace: String defining the keyring ID + :param bytes key_name: Key ID + :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :type wrapping_algorithm: WrappingAlgorithm + :param bytes public_encoded_key: DER encoded public key (optional) + :param bytes private_encoded_key: DER encoded private key (optional) + :param password: Password to load private key (optional) + :return: Calls RawRSAKeyring class with required parameters + """ + loaded_private_wrapping_key = loaded_public_wrapping_key = None + if private_encoded_key is not None: + loaded_private_wrapping_key = serialization.load_der_private_key( + data=private_encoded_key, password=password, backend=default_backend() + ) + if public_encoded_key is not None: + loaded_public_wrapping_key = serialization.load_der_public_key( + data=public_encoded_key, backend=default_backend() + ) + + return cls( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=loaded_private_wrapping_key, + public_wrapping_key=loaded_public_wrapping_key, + ) + + 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 + """ + if encryption_materials.data_encryption_key is None: + _generate_data_key(encryption_materials=encryption_materials, key_provider=self._key_provider) + + if self._public_wrapping_key is None: + return encryption_materials + + try: + # Encrypt data key + encrypted_wrapped_key = EncryptedData( + iv=None, + ciphertext=self._public_wrapping_key.encrypt( + plaintext=encryption_materials.data_encryption_key.data_key, + padding=self._wrapping_algorithm.padding, + ), + tag=None, + ) + + # EncryptedData to EncryptedDataKey + encrypted_data_key = serialize_wrapped_key( + key_provider=self._key_provider, + wrapping_algorithm=self._wrapping_algorithm, + wrapping_key_id=self.key_name, + encrypted_wrapped_key=encrypted_wrapped_key, + ) + except Exception: # pylint: disable=broad-except + error_message = "Raw RSA Keyring unable to encrypt data key" + _LOGGER.exception(error_message) + return encryption_materials + + # Update Keyring Trace + keyring_trace = KeyringTrace( + wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + ) + + # Add encrypted data key to encryption_materials + encryption_materials.add_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) + + return encryption_materials + + 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: List of `aws_encryption_sdk.structures.EncryptedDataKey` + :returns: Optionally modified decryption materials. + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + """ + if self._private_wrapping_key is None: + return decryption_materials + + # Decrypt data key + for key in encrypted_data_keys: + if decryption_materials.data_encryption_key is not None: + return decryption_materials + if key.key_provider != self._key_provider: + continue + # Wrapped EncryptedDataKey to deserialized EncryptedData + encrypted_wrapped_key = deserialize_wrapped_key( + wrapping_algorithm=self._wrapping_algorithm, wrapping_key_id=self.key_name, wrapped_encrypted_key=key + ) + try: + plaintext_data_key = self._private_wrapping_key.decrypt( + ciphertext=encrypted_wrapped_key.ciphertext, padding=self._wrapping_algorithm.padding + ) + except Exception: # pylint: disable=broad-except + error_message = "Raw RSA Keyring unable to decrypt data key" + _LOGGER.exception(error_message) + continue + + # Create a keyring trace + keyring_trace = KeyringTrace( + wrapping_key=self._key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY} + ) + + # Update decryption materials + data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) + decryption_materials.add_data_encryption_key(data_encryption_key, keyring_trace) + + return decryption_materials diff --git a/test/functional/test_f_keyring_raw_aes.py b/test/functional/test_f_keyring_raw_aes.py new file mode 100644 index 000000000..aa08b07ff --- /dev/null +++ b/test/functional/test_f_keyring_raw_aes.py @@ -0,0 +1,202 @@ +# Copyright 2019 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. +"""Functional tests for Raw AES keyring encryption decryption path.""" + +import pytest + +from aws_encryption_sdk.identifiers import ( + Algorithm, + EncryptionKeyType, + EncryptionType, + KeyringTraceFlag, + WrappingAlgorithm, +) +from aws_encryption_sdk.internal.crypto import WrappingKey +from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.functional, pytest.mark.local] + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY = b"12345678901234567890123456789012" +_SIGNING_KEY = b"aws-crypto-public-key" + +_WRAPPING_ALGORITHM = [alg for alg in WrappingAlgorithm if alg.encryption_type is EncryptionType.SYMMETRIC] + + +def sample_encryption_materials(): + return [ + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ), + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + ], + ), + ] + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +@pytest.mark.parametrize("wrapping_algorithm_samples", _WRAPPING_ALGORITHM) +def test_raw_aes_encryption_decryption(encryption_materials_samples, wrapping_algorithm_samples): + + # Initializing attributes + key_namespace = _PROVIDER_ID + key_name = _KEY_ID + _wrapping_algorithm = wrapping_algorithm_samples + + # Creating an instance of a raw AES keyring + test_raw_aes_keyring = RawAESKeyring( + key_namespace=key_namespace, + key_name=key_name, + wrapping_key=_WRAPPING_KEY, + wrapping_algorithm=_wrapping_algorithm, + ) + + # Call on_encrypt function for the keyring + encryption_materials = test_raw_aes_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + # Generate decryption materials + decryption_materials = DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + verification_key=b"ex_verification_key", + encryption_context=_ENCRYPTION_CONTEXT, + ) + + # Call on_decrypt function for the keyring + decryption_materials = test_raw_aes_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + # Check if the data keys match + assert encryption_materials.data_encryption_key.data_key == decryption_materials.data_encryption_key.data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +@pytest.mark.parametrize("wrapping_algorithm_samples", _WRAPPING_ALGORITHM) +def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_samples, wrapping_algorithm_samples): + + # Initializing attributes + key_namespace = _PROVIDER_ID + key_name = _KEY_ID + _wrapping_algorithm = wrapping_algorithm_samples + + # Creating an instance of a raw AES keyring + test_raw_aes_keyring = RawAESKeyring( + key_namespace=key_namespace, + key_name=key_name, + wrapping_key=_WRAPPING_KEY, + wrapping_algorithm=_wrapping_algorithm, + ) + + # Creating an instance of a raw master key + test_raw_master_key = RawMasterKey( + key_id=test_raw_aes_keyring.key_name, + provider_id=test_raw_aes_keyring.key_namespace, + wrapping_key=test_raw_aes_keyring._wrapping_key_structure, + ) + + # Encrypt using raw AES keyring + encryption_materials = test_raw_aes_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + # Check if plaintext data key encrypted by raw keyring is decrypted by raw master key + + raw_mkp_decrypted_data_key = test_raw_master_key.decrypt_data_key_from_list( + encrypted_data_keys=encryption_materials._encrypted_data_keys, + algorithm=encryption_materials.algorithm, + encryption_context=encryption_materials.encryption_context, + ).data_key + + assert encryption_materials.data_encryption_key.data_key == raw_mkp_decrypted_data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +@pytest.mark.parametrize("wrapping_algorithm_samples", _WRAPPING_ALGORITHM) +def test_raw_keyring_decrypts_what_raw_master_key_encrypts(encryption_materials_samples, wrapping_algorithm_samples): + + # Initializing attributes + key_namespace = _PROVIDER_ID + key_name = _KEY_ID + _wrapping_algorithm = wrapping_algorithm_samples + + # Creating an instance of a raw AES keyring + test_raw_aes_keyring = RawAESKeyring( + key_namespace=key_namespace, + key_name=key_name, + wrapping_key=_WRAPPING_KEY, + wrapping_algorithm=_wrapping_algorithm, + ) + + # Creating an instance of a raw master key + test_raw_master_key = RawMasterKey( + key_id=test_raw_aes_keyring.key_name, + provider_id=test_raw_aes_keyring.key_namespace, + wrapping_key=test_raw_aes_keyring._wrapping_key_structure, + ) + + if encryption_materials_samples.data_encryption_key is None: + return + raw_master_key_encrypted_data_key = test_raw_master_key.encrypt_data_key( + data_key=encryption_materials_samples.data_encryption_key, + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + ) + + # Check if plaintext data key encrypted by raw master key is decrypted by raw keyring + + raw_aes_keyring_decrypted_data_key = test_raw_aes_keyring.on_decrypt( + decryption_materials=DecryptionMaterials( + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + verification_key=b"ex_verification_key", + ), + encrypted_data_keys=[raw_master_key_encrypted_data_key], + ).data_encryption_key.data_key + + assert encryption_materials_samples.data_encryption_key.data_key == raw_aes_keyring_decrypted_data_key + + +@pytest.mark.parametrize("wrapping_algorithm", _WRAPPING_ALGORITHM) +def test_key_info_prefix_vectors(wrapping_algorithm): + assert ( + serialize_raw_master_key_prefix( + raw_master_key=RawMasterKey( + provider_id=_PROVIDER_ID, + key_id=_KEY_ID, + wrapping_key=WrappingKey( + wrapping_algorithm=wrapping_algorithm, + wrapping_key=_WRAPPING_KEY, + wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ), + ) + ) + == _KEY_ID + b"\x00\x00\x00\x80\x00\x00\x00\x0c" + ) diff --git a/test/functional/test_f_keyring_raw_rsa.py b/test/functional/test_f_keyring_raw_rsa.py new file mode 100644 index 000000000..bdf5bf25c --- /dev/null +++ b/test/functional/test_f_keyring_raw_rsa.py @@ -0,0 +1,297 @@ +# Copyright 2019 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. +"""Functional tests for Raw AES keyring encryption decryption path.""" + +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from aws_encryption_sdk.identifiers import ( + Algorithm, + EncryptionKeyType, + EncryptionType, + KeyringTraceFlag, + WrappingAlgorithm, +) +from aws_encryption_sdk.internal.crypto import WrappingKey +from aws_encryption_sdk.key_providers.raw import RawMasterKey +from aws_encryption_sdk.keyring.raw_keyring import RawRSAKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.functional, pytest.mark.local] + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_ALGORITHM = WrappingAlgorithm.RSA_OAEP_SHA256_MGF1 + +_PUBLIC_EXPONENT = 65537 +_KEY_SIZE = 2048 +_BACKEND = default_backend() + +_PRIVATE_WRAPPING_KEY = rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + +_PRIVATE_WRAPPING_KEY_PEM = ( + b"-----BEGIN RSA PRIVATE KEY-----\n" + b"MIIEowIBAAKCAQEAo8uCyhiO4JUGZV+rtNq5DBA9Lm4xkw5kTA3v6EPybs8bVXL2\n" + b"ZE6jkbo+xT4Jg/bKzUpnp1fE+T1ruGPtsPdoEmhY/P64LDNIs3sRq5U4QV9IETU1\n" + b"vIcbNNkgGhRjV8J87YNY0tV0H7tuWuZRpqnS+gjV6V9lUMkbvjMCc5IBqQc3heut\n" + b"/+fH4JwpGlGxOVXI8QAapnSy1XpCr3+PT29kydVJnIMuAoFrurojRpOQbOuVvhtA\n" + b"gARhst1Ji4nfROGYkj6eZhvkz2Bkud4/+3lGvVU5LO1vD8oY7WoGtpin3h50VcWe\n" + b"aBT4kejx4s9/G9C4R24lTH09J9HO2UUsuCqZYQIDAQABAoIBAQCfC90bCk+qaWqF\n" + b"gymC+qOWwCn4bM28gswHQb1D5r6AtKBRD8mKywVvWs7azguFVV3Fi8sspkBA2FBC\n" + b"At5p6ULoJOTL/TauzLl6djVJTCMM701WUDm2r+ZOIctXJ5bzP4n5Q4I7b0NMEL7u\n" + b"ixib4elYGr5D1vrVQAKtZHCr8gmkqyx8Mz7wkJepzBP9EeVzETCHsmiQDd5WYlO1\n" + b"C2IQYgw6MJzgM4entJ0V/GPytkodblGY95ORVK7ZhyNtda+r5BZ6/jeMW+hA3VoK\n" + b"tHSWjHt06ueVCCieZIATmYzBNt+zEz5UA2l7ksg3eWfVORJQS7a6Ef4VvbJLM9Ca\n" + b"m1kdsjelAoGBANKgvRf39i3bSuvm5VoyJuqinSb/23IH3Zo7XOZ5G164vh49E9Cq\n" + b"dOXXVxox74ppj/kbGUoOk+AvaB48zzfzNvac0a7lRHExykPH2kVrI/NwH/1OcT/x\n" + b"2e2DnFYocXcb4gbdZQ+m6X3zkxOYcONRzPVW1uMrFTWHcJveMUm4PGx7AoGBAMcU\n" + b"IRvrT6ye5se0s27gHnPweV+3xjsNtXZcK82N7duXyHmNjxrwOAv0SOhUmTkRXArM\n" + b"6aN5D8vyZBSWma2TgUKwpQYFTI+4Sp7sdkkyojGAEixJ+c5TZJNxZFrUe0FwAoic\n" + b"c2kb7ntaiEj5G+qHvykJJro5hy6uLnjiMVbAiJDTAoGAKb67241EmHAXGEwp9sdr\n" + b"2SMjnIAnQSF39UKAthkYqJxa6elXDQtLoeYdGE7/V+J2K3wIdhoPiuY6b4vD0iX9\n" + b"JcGM+WntN7YTjX2FsC588JmvbWfnoDHR7HYiPR1E58N597xXdFOzgUgORVr4PMWQ\n" + b"pqtwaZO3X2WZlvrhr+e46hMCgYBfdIdrm6jYXFjL6RkgUNZJQUTxYGzsY+ZemlNm\n" + b"fGdQo7a8kePMRuKY2MkcnXPaqTg49YgRmjq4z8CtHokRcWjJUWnPOTs8rmEZUshk\n" + b"0KJ0mbQdCFt/Uv0mtXgpFTkEZ3DPkDTGcV4oR4CRfOCl0/EU/A5VvL/U4i/mRo7h\n" + b"ye+xgQKBgD58b+9z+PR5LAJm1tZHIwb4tnyczP28PzwknxFd2qylR4ZNgvAUqGtU\n" + b"xvpUDpzMioz6zUH9YV43YNtt+5Xnzkqj+u9Mr27/H2v9XPwORGfwQ5XPwRJz/2oC\n" + b"EnPmP1SZoY9lXKUpQXHXSpDZ2rE2Klt3RHMUMHt8Zpy36E8Vwx8o\n" + b"-----END RSA PRIVATE KEY-----\n" +) + +_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITHOUT_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), +) + +_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITH_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(b"mypassword"), +) + +_RAW_RSA_PUBLIC_KEY_PEM_ENCODED = ( + rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + .public_key() + .public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) +) + +_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITHOUT_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), +) + +_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITH_PASSWORD = rsa.generate_private_key( + public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND +).private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.BestAvailableEncryption(b"mypassword"), +) + +_RAW_RSA_PUBLIC_KEY_DER_ENCODED = ( + rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + .public_key() + .public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo) +) + + +def sample_encryption_materials(): + return [ + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context=_ENCRYPTION_CONTEXT + ), + EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + ], + ), + ] + + +def sample_raw_rsa_keyring_using_different_wrapping_algorithm(): + for alg in WrappingAlgorithm: + if alg.encryption_type is EncryptionType.ASYMMETRIC: + yield RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=alg, + private_wrapping_key=_PRIVATE_WRAPPING_KEY, + ) + pem_and_der_encoded_raw_rsa_keyring = [ + RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITHOUT_PASSWORD, + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_PEM_ENCODED_WITH_PASSWORD, + password=b"mypassword", + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + public_encoded_key=_RAW_RSA_PUBLIC_KEY_PEM_ENCODED, + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_der_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITHOUT_PASSWORD, + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_der_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + private_encoded_key=_RAW_RSA_PRIVATE_KEY_DER_ENCODED_WITH_PASSWORD, + password=b"mypassword", + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + RawRSAKeyring.from_der_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + public_encoded_key=_RAW_RSA_PUBLIC_KEY_DER_ENCODED, + password=b"mypassword", + wrapping_algorithm=_WRAPPING_ALGORITHM, + ), + ] + for keyring in pem_and_der_encoded_raw_rsa_keyring: + yield keyring + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +@pytest.mark.parametrize("test_raw_rsa_keyring", sample_raw_rsa_keyring_using_different_wrapping_algorithm()) +def test_raw_rsa_encryption_decryption(encryption_materials_samples, test_raw_rsa_keyring): + + # Call on_encrypt function for the keyring + encryption_materials = test_raw_rsa_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + assert encryption_materials.encrypted_data_keys is not None + + # Generate decryption materials + decryption_materials = DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + verification_key=b"ex_verification_key", + encryption_context=_ENCRYPTION_CONTEXT, + ) + + # Call on_decrypt function for the keyring + decryption_materials = test_raw_rsa_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + if test_raw_rsa_keyring._private_wrapping_key is not None: + # Check if the data keys match + assert encryption_materials.data_encryption_key.data_key == decryption_materials.data_encryption_key.data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_samples): + test_raw_rsa_keyring = RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=_WRAPPING_ALGORITHM, + private_encoded_key=_PRIVATE_WRAPPING_KEY_PEM, + ) + + # Creating an instance of a raw master key + test_raw_master_key = RawMasterKey( + key_id=_KEY_ID, + provider_id=_PROVIDER_ID, + wrapping_key=WrappingKey( + wrapping_algorithm=_WRAPPING_ALGORITHM, + wrapping_key=_PRIVATE_WRAPPING_KEY_PEM, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + # Call on_encrypt function for the keyring + encryption_materials = test_raw_rsa_keyring.on_encrypt(encryption_materials=encryption_materials_samples) + + # Check if plaintext data key encrypted by raw keyring is decrypted by raw master key + raw_mkp_decrypted_data_key = test_raw_master_key.decrypt_data_key_from_list( + encrypted_data_keys=encryption_materials._encrypted_data_keys, + algorithm=encryption_materials.algorithm, + encryption_context=encryption_materials.encryption_context, + ).data_key + + assert encryption_materials.data_encryption_key.data_key == raw_mkp_decrypted_data_key + + +@pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) +def test_raw_keyring_decrypts_what_raw_master_key_encrypts(encryption_materials_samples): + + # Create instance of raw master key + test_raw_master_key = RawMasterKey( + key_id=_KEY_ID, + provider_id=_PROVIDER_ID, + wrapping_key=WrappingKey( + wrapping_algorithm=_WRAPPING_ALGORITHM, + wrapping_key=_PRIVATE_WRAPPING_KEY_PEM, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + test_raw_rsa_keyring = RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=_WRAPPING_ALGORITHM, + private_encoded_key=_PRIVATE_WRAPPING_KEY_PEM, + ) + + raw_mkp_generated_data_key = test_raw_master_key.generate_data_key( + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + ) + + raw_mkp_encrypted_data_key = test_raw_master_key.encrypt_data_key( + data_key=raw_mkp_generated_data_key, + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + ) + + decryption_materials = test_raw_rsa_keyring.on_decrypt( + decryption_materials=DecryptionMaterials( + algorithm=encryption_materials_samples.algorithm, + encryption_context=encryption_materials_samples.encryption_context, + verification_key=b"ex_verification_key", + ), + encrypted_data_keys=[raw_mkp_encrypted_data_key], + ) + + assert raw_mkp_generated_data_key.data_key == decryption_materials.data_encryption_key.data_key diff --git a/test/unit/test_keyring_raw_aes.py b/test/unit/test_keyring_raw_aes.py new file mode 100644 index 000000000..b98279d04 --- /dev/null +++ b/test/unit/test_keyring_raw_aes.py @@ -0,0 +1,282 @@ +# Copyright 2019 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. +"""Unit tests for Raw AES keyring.""" + +import os + +import mock +import pytest +from pytest_mock import mocker # noqa pylint: disable=unused-import + +import aws_encryption_sdk.key_providers.raw +import aws_encryption_sdk.keyring.raw_keyring +from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.keyring.raw_keyring import GenerateKeyError, RawAESKeyring, _generate_data_key +from aws_encryption_sdk.materials_managers import EncryptionMaterials +from aws_encryption_sdk.structures import MasterKeyInfo + +from .unit_test_utils import ( + _DATA_KEY, + _ENCRYPTED_DATA_KEY_AES, + _ENCRYPTED_DATA_KEY_NOT_IN_KEYRING, + _ENCRYPTION_CONTEXT, + _KEY_ID, + _PROVIDER_ID, + _SIGNING_KEY, + _WRAPPING_KEY, + get_decryption_materials_with_data_encryption_key, + get_decryption_materials_without_data_encryption_key, + get_encryption_materials_with_data_encryption_key, + get_encryption_materials_without_data_encryption_key, +) + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +@pytest.fixture +def raw_aes_keyring(): + return RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + wrapping_key=_WRAPPING_KEY, + ) + + +@pytest.fixture +def patch_generate_data_key(mocker): + mocker.patch.object(aws_encryption_sdk.keyring.raw_keyring, "_generate_data_key") + return aws_encryption_sdk.keyring.raw_keyring._generate_data_key + + +@pytest.fixture +def patch_decrypt_on_wrapping_key(mocker): + mocker.patch.object(WrappingKey, "decrypt") + return WrappingKey.decrypt + + +@pytest.fixture +def patch_os_urandom(mocker): + mocker.patch.object(os, "urandom") + return os.urandom + + +def test_parent(): + assert issubclass(RawAESKeyring, Keyring) + + +def test_valid_parameters(raw_aes_keyring): + test = raw_aes_keyring + assert test.key_name == _KEY_ID + assert test.key_namespace == _PROVIDER_ID + assert test._wrapping_algorithm == WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING + assert test._wrapping_key == _WRAPPING_KEY + + +@pytest.mark.parametrize( + "key_namespace, key_name, wrapping_algorithm, wrapping_key", + ( + (_PROVIDER_ID, None, WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, None), + (None, None, None, None), + ( + _PROVIDER_ID, + _KEY_ID, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ), + ( + Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, + Algorithm.AES_256_GCM_IV12_TAG16, + Algorithm.AES_128_GCM_IV12_TAG16, + Algorithm.AES_128_GCM_IV12_TAG16, + ), + ), +) +def test_invalid_parameters(key_namespace, key_name, wrapping_algorithm, wrapping_key): + with pytest.raises(TypeError): + RawAESKeyring( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + wrapping_key=wrapping_key, + ) + + +def test_on_encrypt_when_data_encryption_key_given(raw_aes_keyring, patch_generate_data_key): + test_raw_aes_keyring = raw_aes_keyring + + test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + # Check if keyring is generated + assert not patch_generate_data_key.called + + +def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_aes_keyring): + test_raw_aes_keyring = raw_aes_keyring + + test = test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + + for keyring_trace in test.keyring_trace: + if keyring_trace.wrapping_key.key_info == _KEY_ID: + # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY not in keyring_trace.flags + + +def test_on_encrypt_when_data_encryption_key_not_given(raw_aes_keyring): + + test_raw_aes_keyring = raw_aes_keyring + + original_number_of_encrypted_data_keys = len( + get_encryption_materials_without_data_encryption_key().encrypted_data_keys + ) + + test = test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + # Check if data key is generated + assert test.data_encryption_key is not None + + generated_flag_count = 0 + encrypted_flag_count = 0 + + for keyring_trace in test.keyring_trace: + if ( + keyring_trace.wrapping_key.key_info == _KEY_ID + and KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in keyring_trace.flags + ): + # Check keyring trace contains KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + generated_flag_count += 1 + if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in keyring_trace.flags: + encrypted_flag_count += 1 + + assert generated_flag_count == 1 + + assert len(test.encrypted_data_keys) == original_number_of_encrypted_data_keys + 1 + + assert encrypted_flag_count == 1 + + +@pytest.mark.parametrize( + "decryption_materials, edk", + ( + (get_decryption_materials_with_data_encryption_key(), [_ENCRYPTED_DATA_KEY_AES]), + (get_decryption_materials_with_data_encryption_key(), []), + ), +) +def test_on_decrypt_when_data_key_given(raw_aes_keyring, decryption_materials, edk, patch_decrypt_on_wrapping_key): + test_raw_aes_keyring = raw_aes_keyring + test_raw_aes_keyring.on_decrypt(decryption_materials=decryption_materials, encrypted_data_keys=edk) + assert not patch_decrypt_on_wrapping_key.called + + +def test_keyring_trace_on_decrypt_when_data_key_given(raw_aes_keyring): + test_raw_aes_keyring = raw_aes_keyring + test = test_raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_with_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + for keyring_trace in test.keyring_trace: + if keyring_trace.wrapping_key.key_info == _KEY_ID: + # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + + +@pytest.mark.parametrize( + "decryption_materials, edk", + ( + (get_decryption_materials_without_data_encryption_key(), []), + (get_encryption_materials_without_data_encryption_key(), [_ENCRYPTED_DATA_KEY_NOT_IN_KEYRING]), + ), +) +def test_on_decrypt_when_data_key_and_edk_not_provided( + raw_aes_keyring, decryption_materials, edk, patch_decrypt_on_wrapping_key +): + test_raw_aes_keyring = raw_aes_keyring + + test = test_raw_aes_keyring.on_decrypt(decryption_materials=decryption_materials, encrypted_data_keys=edk) + assert not patch_decrypt_on_wrapping_key.called + + for keyring_trace in test.keyring_trace: + if keyring_trace.wrapping_key.key_info == _KEY_ID: + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_edk_provided(raw_aes_keyring, patch_decrypt_on_wrapping_key): + patch_decrypt_on_wrapping_key.return_value = _DATA_KEY + test_raw_aes_keyring = raw_aes_keyring + test_raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + patch_decrypt_on_wrapping_key.assert_called_once_with( + encrypted_wrapped_data_key=mock.ANY, encryption_context=mock.ANY + ) + + +def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_aes_keyring): + test_raw_aes_keyring = raw_aes_keyring + + test = test_raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + decrypted_flag_count = 0 + + for keyring_trace in test.keyring_trace: + if KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in keyring_trace.flags: + decrypted_flag_count += 1 + + assert decrypted_flag_count == 1 + + +def test_error_when_data_key_not_generated(patch_os_urandom): + patch_os_urandom.side_effect = NotImplementedError + with pytest.raises(GenerateKeyError) as exc_info: + _generate_data_key( + encryption_materials=get_encryption_materials_without_data_encryption_key(), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + ) + assert exc_info.match("Unable to generate data encryption key.") + + +def test_generate_data_key_error_when_data_key_exists(): + with pytest.raises(TypeError) as exc_info: + _generate_data_key( + encryption_materials=get_encryption_materials_with_data_encryption_key(), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + ) + assert exc_info.match("Data encryption key already exists.") + + +def test_generate_data_key_keyring_trace(): + encryption_materials_without_data_key = EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ) + _generate_data_key( + encryption_materials=encryption_materials_without_data_key, + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + ) + + assert encryption_materials_without_data_key.data_encryption_key.key_provider.provider_id == _PROVIDER_ID + assert encryption_materials_without_data_key.data_encryption_key.key_provider.key_info == _KEY_ID + + generate_flag_count = 0 + + for keyring_trace in encryption_materials_without_data_key.keyring_trace: + if KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in keyring_trace.flags: + generate_flag_count += 1 + assert generate_flag_count == 1 diff --git a/test/unit/test_keyring_raw_rsa.py b/test/unit/test_keyring_raw_rsa.py new file mode 100644 index 000000000..1fcf23da5 --- /dev/null +++ b/test/unit/test_keyring_raw_rsa.py @@ -0,0 +1,249 @@ +# Copyright 2019 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. +"""Unit tests for Raw AES keyring.""" + +import pytest +from cryptography.hazmat.primitives.asymmetric import rsa +from pytest_mock import mocker # noqa pylint: disable=unused-import + +import aws_encryption_sdk.key_providers.raw +import aws_encryption_sdk.keyring.raw_keyring +from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.keyring.raw_keyring import RawRSAKeyring + +from .test_values import VALUES +from .unit_test_utils import ( + _BACKEND, + _DATA_KEY, + _ENCRYPTED_DATA_KEY_RSA, + _ENCRYPTION_CONTEXT, + _KEY_ID, + _KEY_SIZE, + _PROVIDER_ID, + _PUBLIC_EXPONENT, + get_decryption_materials_with_data_encryption_key, + get_decryption_materials_without_data_encryption_key, + get_encryption_materials_with_data_encryption_key, + get_encryption_materials_without_data_encryption_key, +) + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +@pytest.fixture +def raw_rsa_keyring(): + return RawRSAKeyring.from_pem_encoding( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_encoded_key=VALUES["private_rsa_key_bytes"][1], + ) + + +def raw_rsa_private_key(): + return rsa.generate_private_key(public_exponent=_PUBLIC_EXPONENT, key_size=_KEY_SIZE, backend=_BACKEND) + + +@pytest.fixture +def patch_generate_data_key(mocker): + mocker.patch.object(aws_encryption_sdk.keyring.raw_keyring, "_generate_data_key") + return aws_encryption_sdk.keyring.raw_keyring._generate_data_key + + +@pytest.fixture +def patch_decrypt_on_wrapping_key(mocker): + mocker.patch.object(WrappingKey, "decrypt") + return WrappingKey.decrypt + + +@pytest.fixture +def patch_os_urandom(mocker): + mocker.patch.object(aws_encryption_sdk.key_providers.raw.os, "urandom") + return aws_encryption_sdk.key_providers.raw.os.urandom + + +def test_parent(): + assert issubclass(RawRSAKeyring, Keyring) + + +def test_valid_parameters(raw_rsa_keyring): + test = raw_rsa_keyring + assert test.key_namespace == _PROVIDER_ID + assert test.key_name == _KEY_ID + assert test._wrapping_algorithm == WrappingAlgorithm.RSA_OAEP_SHA256_MGF1 + assert isinstance(test._private_wrapping_key, rsa.RSAPrivateKey) + + +@pytest.mark.parametrize( + "key_namespace, key_name, wrapping_algorithm, private_wrapping_key, public_wrapping_key", + ( + (_PROVIDER_ID, None, WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, raw_rsa_private_key(), None), + (None, None, None, None, None), + (_PROVIDER_ID, _KEY_ID, WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, None), + (None, None, None, raw_rsa_private_key(), raw_rsa_private_key().public_key()), + (len(_PROVIDER_ID), len(_KEY_ID), _PROVIDER_ID, _PROVIDER_ID, _KEY_ID), + ), +) +def test_invalid_parameters(key_namespace, key_name, wrapping_algorithm, private_wrapping_key, public_wrapping_key): + with pytest.raises(TypeError): + RawRSAKeyring( + key_namespace=key_namespace, + key_name=key_name, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=private_wrapping_key, + public_wrapping_key=public_wrapping_key, + ) + + +def test_public_and_private_key_not_provided(): + with pytest.raises(TypeError) as exc_info: + RawRSAKeyring( + key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1 + ) + assert exc_info.match("At least one of public key or private key must be provided.") + + +def test_on_encrypt_when_data_encryption_key_given(raw_rsa_keyring, patch_generate_data_key): + test_raw_rsa_keyring = raw_rsa_keyring + + test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + # Check if keyring is generated + assert not patch_generate_data_key.called + + +def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) + + for keyring_trace in test.keyring_trace: + if keyring_trace.wrapping_key.key_info == _KEY_ID: + # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY not in keyring_trace.flags + + +def test_on_encrypt_when_data_encryption_key_not_given(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + + original_number_of_encrypted_data_keys = len( + get_encryption_materials_without_data_encryption_key().encrypted_data_keys + ) + + test = test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + # Check if data key is generated + assert test.data_encryption_key is not None + + generated_flag_count = 0 + encrypted_flag_count = 0 + + for keyring_trace in test.keyring_trace: + if ( + keyring_trace.wrapping_key.key_info == _KEY_ID + and KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in keyring_trace.flags + ): + # Check keyring trace contains KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + generated_flag_count += 1 + if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in keyring_trace.flags: + encrypted_flag_count += 1 + + assert generated_flag_count == 1 + + assert len(test.encrypted_data_keys) == original_number_of_encrypted_data_keys + 1 + + assert encrypted_flag_count == 1 + + +def test_on_decrypt_when_data_key_given(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + test_raw_rsa_keyring = raw_rsa_keyring + test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_with_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + assert not patch_decrypt_on_wrapping_key.called + + +def test_keyring_trace_on_decrypt_when_data_key_given(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_with_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + for keyring_trace in test.keyring_trace: + if keyring_trace.wrapping_key.key_info == _KEY_ID: + # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + + +def test_on_decrypt_when_data_key_and_edk_not_provided(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), encrypted_data_keys=[] + ) + assert not patch_decrypt_on_wrapping_key.called + + for keyring_trace in test.keyring_trace: + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_edk_not_in_keyring(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + assert not patch_decrypt_on_wrapping_key.called + + for keyring_trace in test.keyring_trace: + if keyring_trace.wrapping_key.key_info == _KEY_ID: + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring, patch_decrypt_on_wrapping_key): + patch_decrypt_on_wrapping_key.return_value = _DATA_KEY + test_raw_rsa_keyring = raw_rsa_keyring + + test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], + ) + assert patch_decrypt_on_wrapping_key.called_once_with( + encrypted_wrapped_data_key=_ENCRYPTED_DATA_KEY_RSA, encryption_context=_ENCRYPTION_CONTEXT + ) + + +def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring): + test_raw_rsa_keyring = raw_rsa_keyring + + test = test_raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=test_raw_rsa_keyring.on_encrypt( + encryption_materials=get_encryption_materials_without_data_encryption_key() + ).encrypted_data_keys, + ) + decrypted_flag_count = 0 + + for keyring_trace in test.keyring_trace: + if KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in keyring_trace.flags: + decrypted_flag_count += 1 + + assert decrypted_flag_count == 1 + assert test.data_encryption_key is not None diff --git a/test/unit/test_utils.py b/test/unit/test_utils.py index b1374a09d..0dee36b41 100644 --- a/test/unit/test_utils.py +++ b/test/unit/test_utils.py @@ -21,7 +21,8 @@ import aws_encryption_sdk.internal.utils from aws_encryption_sdk.exceptions import InvalidDataKeyError, SerializationError, UnknownIdentityError from aws_encryption_sdk.internal.defaults import MAX_FRAME_SIZE, MESSAGE_ID_LENGTH -from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey +from aws_encryption_sdk.keyring.base import EncryptedDataKey +from aws_encryption_sdk.structures import DataKey, MasterKeyInfo, RawDataKey from .test_values import VALUES from .unit_test_utils import assert_prepped_stream_identity diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 6b0a84bdc..087c8dfa5 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -15,7 +15,150 @@ import io import itertools +from cryptography.hazmat.backends import default_backend + +from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO +from aws_encryption_sdk.keyring.base import EncryptedDataKey, Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +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 + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" +_SIGNING_KEY = b"aws-crypto-public-key" +_DATA_KEY = ( + b"\x00\xfa\x8c\xdd\x08Au\xc6\x92_4\xc5\xfb\x90\xaf\x8f\xa1D\xaf\xcc\xd25" b"\xa8\x0b\x0b\x16\x92\x91W\x01\xb7\x84" +) + +_PUBLIC_EXPONENT = 65537 +_KEY_SIZE = 2048 +_BACKEND = default_backend() + +_ENCRYPTED_DATA_KEY_AES = EncryptedDataKey( + key_provider=MasterKeyInfo( + provider_id="Random Raw Keys", + key_info=b"5325b043-5843-4629-869c-64794af77ada\x00\x00\x00\x80" + b"\x00\x00\x00\x0c\xc7\xd5d\xc9\xc5\xf21\x8d\x8b\xf9H" + b"\xbb", + ), + encrypted_data_key=b"\xf3+\x15n\xe6`\xbe\xfe\xf0\x9e1\xe5\x9b" + b"\xaf\xfe\xdaT\xbb\x17\x14\xfd} o\xdd\xf1" + b"\xbc\xe1C\xa5J\xd8\xc7\x15\xc2\x90t=\xb9" + b"\xfd;\x94lTu/6\xfe", +) + +_ENCRYPTED_DATA_KEY_NOT_IN_KEYRING = EncryptedDataKey( + key_provider=MasterKeyInfo( + provider_id="Random Raw Keys", + key_info=b"5430b043-5843-4629-869c-64794af77ada\x00\x00\x00\x80" + b"\x00\x00\x00\x0c\xc7\xd5d\xc9\xc5\xf21\x8d\x8b\xf9H" + b"\xbb", + ), + encrypted_data_key=b"\xf3+\x15n\xe6`\xbe\xfe\xf0\x9e1\xe5\x9b" + b"\xaf\xfe\xdaT\xbb\x17\x14\xfd} o\xdd\xf1" + b"\xbc\xe1C\xa5J\xd8\xc7\x15\xc2\x90t=\xb9" + b"\xfd;\x94lTu/6\xfe", +) + +_ENCRYPTED_DATA_KEY_RSA = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id="Random Raw Keys", key_info=_KEY_ID), + encrypted_data_key=b"\xf3+\x15n\xe6`\xbe\xfe\xf0\x9e1\xe5\x9b" + b"\xaf\xfe\xdaT\xbb\x17\x14\xfd} o\xdd\xf1" + b"\xbc\xe1C\xa5J\xd8\xc7\x15\xc2\x90t=\xb9" + b"\xfd;\x94lTu/6\xfe", +) + + +class IdentityKeyring(Keyring): + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return decryption_materials + + +def get_encryption_materials_with_data_encryption_key(): + return EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + ], + ) + + +def get_encryption_materials_with_encrypted_data_key_aes(): + return EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={ + KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, + }, + ) + ], + ) + + +def get_encryption_materials_without_data_encryption_key(): + return EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ) + + +def get_decryption_materials_without_data_encryption_key(): + return DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + verification_key=b"ex_verification_key", + encryption_context=_ENCRYPTION_CONTEXT, + ) + + +def get_decryption_materials_with_data_encryption_key(): + return DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + verification_key=b"ex_verification_key", + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY}, + ) + ], + ) def all_valid_kwargs(valid_kwargs): From 2e85bfd3d42965b9972506c39371e971132196e0 Mon Sep 17 00:00:00 2001 From: MeghaShetty Date: Fri, 2 Aug 2019 17:31:10 -0700 Subject: [PATCH 20/64] Multi keyrings (#166) * Adding Keyring API * Delete __init__.py * Delete raw_keyring.py * Added docstring to public class * Edited docstring * Edited docstring again * Changes in docstring statements * Docstring changes * Changes in docstring * Raw keyring initial * Raw keyring encrypt commit * Encrypt functions for Raw RSA and AES * Raw RSA and AES initial * raw keyrings first commit * Multi keyring first commit * Changes in the base file * Temporary changes in multiple files * Committing initial code * Deleted raw aes test * Multi Keyrings * Updating base API and raw keyrings * Corrected tox errors * Added typehints * Updated raw keyrings * Updated raw keyrings * Changes in error conditions for multi keyrings * Made all suggested changes in multi-keyrings * Corrected tox errors * Added docstring to __attrs_post_init__ * Changed variable name neither_generator_nor_children_defined to neither_generator_nor_children * Changed raw keyrings * Corrected tox errors * Updated raw keyrings * Updated raw keyrings and functional test for multi keyrings * Functional tests for multi-keyrings work * Autoformat errors corrected and changed Exception to BaseException to solve broad exception error * Added pylint disable broad except to raw keyrings and added multi parametrize to multi keyrings functional test * Removed duplicate import statements * Changes in functional test for multi keyrings according to change in raw keyrings * Changed RSA key structure to RSAPublicKey/RSAPrivateKey and functional test passes * Removed unwanted commented lines from test * Pylint errors * More pylint errors * Made suggested changes in multi keyring * Multi keyring unit tests * Optimized loop for decryption keyring * Unit tests for multi keyrings and added sample encryption materials and multi keyrings in test_utils * Multi keyrings unit tests * Making changes in tests and API * Almost all unit tests done * Unit tests for multi keyrings * Unit tests for multi keyrings * Unit tests for multi-keyrings working except the one to check if no further keyrings are called if data encryption key is added * Made changes in raw keyrings to match the latest version * Removed unused imports * Made suggested changes * Removed unused imports * Resolved formatting errors * Made suggested changes - partial * Made all suggested changes * apply autoformatting x_x --- .../keyring/multi_keyring.py | 105 ++++++++ test/functional/test_f_multi_keyring.py | 132 ++++++++++ test/unit/test_keyring_multi.py | 247 ++++++++++++++++++ test/unit/unit_test_utils.py | 171 +++++++++++- 4 files changed, 652 insertions(+), 3 deletions(-) create mode 100644 src/aws_encryption_sdk/keyring/multi_keyring.py create mode 100644 test/functional/test_f_multi_keyring.py create mode 100644 test/unit/test_keyring_multi.py diff --git a/src/aws_encryption_sdk/keyring/multi_keyring.py b/src/aws_encryption_sdk/keyring/multi_keyring.py new file mode 100644 index 000000000..f43820be5 --- /dev/null +++ b/src/aws_encryption_sdk/keyring/multi_keyring.py @@ -0,0 +1,105 @@ +# 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. +"""Resources required for Multi Keyrings.""" +import itertools + +import attr +from attr.validators import deep_iterable, instance_of, optional + +from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError +from aws_encryption_sdk.keyring.base import DecryptionMaterials, EncryptedDataKey, EncryptionMaterials, Keyring + +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 + + +@attr.s +class MultiKeyring(Keyring): + """Public class for Multi Keyring. + + :param generator: Generator keyring used to generate data encryption key (optional) + :type generator: Keyring + :param list children: List of keyrings used to encrypt the data encryption key (optional) + :raises EncryptKeyError: if encryption of data key fails for any reason + """ + + children = attr.ib( + default=attr.Factory(tuple), validator=optional(deep_iterable(member_validator=instance_of(Keyring))) + ) + generator = attr.ib(default=None, validator=optional(instance_of(Keyring))) + + def __attrs_post_init__(self): + # type: () -> None + """Prepares initial values not handled by attrs.""" + neither_generator_nor_children = self.generator is None and not self.children + if neither_generator_nor_children: + raise TypeError("At least one of generator or children must be provided") + + _generator = (self.generator,) if self.generator is not None else () + self._decryption_keyrings = list(itertools.chain(_generator, self.children)) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key using generator keyring + and encrypt it using any available wrapping key in any child keyring. + + :param encryption_materials: Encryption materials for 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 EncryptKeyError: if unable to encrypt data key. + """ + # Check if generator keyring is not provided and data key is not generated + if self.generator is None and encryption_materials.data_encryption_key is None: + raise EncryptKeyError( + "Generator keyring not provided " + "and encryption materials do not already contain a plaintext data key." + ) + + # Call on_encrypt on the generator keyring if it is provided + if self.generator is not None: + + encryption_materials = self.generator.on_encrypt(encryption_materials=encryption_materials) + + # Check if data key is generated + if encryption_materials.data_encryption_key is None: + raise GenerateKeyError("Unable to generate data encryption key.") + + # Call on_encrypt on all other keyrings + for keyring in self.children: + encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + + return encryption_materials + + 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 keyring to modify. + :type decryption_materials: aws_encryption_sdk.materials_managers.DecryptionMaterials + :param encrypted_data_keys: List of encrypted data keys. + :type: List of `aws_encryption_sdk.structures.EncryptedDataKey` + :returns: Optionally modified decryption materials. + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + """ + # Call on_decrypt on all keyrings till decryption is successful + for keyring in self._decryption_keyrings: + if decryption_materials.data_encryption_key is not None: + return decryption_materials + decryption_materials = keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encrypted_data_keys + ) + return decryption_materials diff --git a/test/functional/test_f_multi_keyring.py b/test/functional/test_f_multi_keyring.py new file mode 100644 index 000000000..54d519f0b --- /dev/null +++ b/test/functional/test_f_multi_keyring.py @@ -0,0 +1,132 @@ +# Copyright 2019 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. +"""Functional tests for Multi keyring encryption decryption path.""" + +import pytest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.defaults import ALGORITHM +from aws_encryption_sdk.keyring.multi_keyring import MultiKeyring +from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring, RawRSAKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey + +pytestmark = [pytest.mark.functional, pytest.mark.local] + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY_AES = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" + +_ENCRYPTION_MATERIALS_WITHOUT_DATA_KEY = EncryptionMaterials( + algorithm=ALGORITHM, encryption_context=_ENCRYPTION_CONTEXT +) + +_ENCRYPTION_MATERIALS_WITH_DATA_KEY = EncryptionMaterials( + algorithm=ALGORITHM, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + ], +) + +_MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN = MultiKeyring( + generator=RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + wrapping_key=_WRAPPING_KEY_AES, + ), + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + ], +) + +_MULTI_KEYRING_WITHOUT_CHILDREN = MultiKeyring( + generator=RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend()), + ) +) + +_MULTI_KEYRING_WITHOUT_GENERATOR = MultiKeyring( + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + wrapping_key=_WRAPPING_KEY_AES, + ), + ] +) + + +@pytest.mark.parametrize( + "multi_keyring, encryption_materials", + [ + (_MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN, _ENCRYPTION_MATERIALS_WITHOUT_DATA_KEY), + (_MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN, _ENCRYPTION_MATERIALS_WITH_DATA_KEY), + (_MULTI_KEYRING_WITHOUT_CHILDREN, _ENCRYPTION_MATERIALS_WITH_DATA_KEY), + (_MULTI_KEYRING_WITHOUT_GENERATOR, _ENCRYPTION_MATERIALS_WITH_DATA_KEY), + ], +) +def test_multi_keyring_encryption_decryption(multi_keyring, encryption_materials): + # Call on_encrypt function for the keyring + encryption_materials = multi_keyring.on_encrypt(encryption_materials) + + # Generate decryption materials + decryption_materials = DecryptionMaterials( + algorithm=ALGORITHM, verification_key=b"ex_verification_key", encryption_context=_ENCRYPTION_CONTEXT + ) + + # Call on_decrypt function for the keyring + decryption_materials = multi_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + # Check if the data keys match + assert encryption_materials.data_encryption_key == decryption_materials.data_encryption_key diff --git a/test/unit/test_keyring_multi.py b/test/unit/test_keyring_multi.py new file mode 100644 index 000000000..6b66a490f --- /dev/null +++ b/test/unit/test_keyring_multi.py @@ -0,0 +1,247 @@ +# Copyright 2019 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. +"""Unit tests for Multi keyring.""" + +import pytest +from mock import MagicMock +from pytest_mock import mocker # noqa pylint: disable=unused-import + +from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.internal.formatting import serialize +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.keyring.multi_keyring import MultiKeyring +from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring + +from .unit_test_utils import ( + IdentityKeyring, + OnlyGenerateKeyring, + get_decryption_materials_with_data_key, + get_decryption_materials_without_data_key, + get_encryption_materials_with_data_key, + get_encryption_materials_with_encrypted_data_key, + get_encryption_materials_without_data_key, + get_multi_keyring_with_generator_and_children, + get_multi_keyring_with_no_children, + get_multi_keyring_with_no_generator, +) + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +_ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} +_PROVIDER_ID = "Random Raw Keys" +_KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" +_WRAPPING_KEY_AES = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" +_SIGNING_KEY = b"aws-crypto-public-key" + + +@pytest.fixture +def identity_keyring(): + return IdentityKeyring() + + +@pytest.fixture +def keyring_which_only_generates(): + return OnlyGenerateKeyring() + + +@pytest.fixture +def mock_generator(): + mock_generator_keyring = MagicMock() + mock_generator_keyring.__class__ = RawAESKeyring + return mock_generator_keyring + + +@pytest.fixture +def mock_child_1(): + mock_child_1_keyring = MagicMock() + mock_child_1_keyring.__class__ = RawAESKeyring + return mock_child_1_keyring + + +@pytest.fixture +def mock_child_2(): + mock_child_2_keyring = MagicMock() + mock_child_2_keyring.__class__ = RawAESKeyring + return mock_child_2_keyring + + +@pytest.fixture +def mock_child_3(): + mock_child_3_keyring = MagicMock() + mock_child_3_keyring.__class__ = RawAESKeyring + mock_child_3_keyring.on_decrypt.return_value = get_decryption_materials_with_data_key() + return mock_child_3_keyring + + +@pytest.fixture +def patch_encrypt(mocker): + mocker.patch.object(serialize, "serialize_raw_master_key_prefix") + return serialize.serialize_raw_master_key_prefix + + +def test_parent(): + assert issubclass(MultiKeyring, Keyring) + + +def test_keyring_with_generator_but_no_children(): + generator_keyring = RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_key=_WRAPPING_KEY_AES, + wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ) + test_multi_keyring = MultiKeyring(generator=generator_keyring) + assert test_multi_keyring.generator is generator_keyring + assert not test_multi_keyring.children + + +def test_keyring_with_children_but_no_generator(): + children_keyring = [ + RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_key=_WRAPPING_KEY_AES, + wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ) + ] + test_multi_keyring = MultiKeyring(children=children_keyring) + assert test_multi_keyring.children is children_keyring + assert test_multi_keyring.generator is None + + +def test_keyring_with_no_generator_no_children(): + with pytest.raises(TypeError) as exc_info: + MultiKeyring() + assert exc_info.match("At least one of generator or children must be provided") + + +@pytest.mark.parametrize( + "generator, children", + ( + (WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, None), + (WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, get_multi_keyring_with_no_generator().children), + (None, [WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING]), + (get_multi_keyring_with_no_children().generator, [WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING]), + ), +) +def test_keyring_with_invalid_parameters(generator, children): + with pytest.raises(TypeError) as exc_info: + MultiKeyring(generator=generator, children=children) + assert exc_info.match("('children'|'generator') must be .*") + + +def test_decryption_keyrings(): + test_multi_keyring = get_multi_keyring_with_generator_and_children() + assert test_multi_keyring.generator in test_multi_keyring._decryption_keyrings + for child_keyring in test_multi_keyring.children: + assert child_keyring in test_multi_keyring._decryption_keyrings + assert len(test_multi_keyring._decryption_keyrings) == len(test_multi_keyring.children) + 1 + + +def test_on_encrypt_with_no_generator_no_data_encryption_key(): + test_multi_keyring = get_multi_keyring_with_no_generator() + with pytest.raises(EncryptKeyError) as exc_info: + test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_key()) + assert exc_info.match( + "Generator keyring not provided and encryption materials do not already contain a plaintext data key." + ) + + +def test_identity_keyring_as_generator_and_no_data_encryption_key(identity_keyring): + test_multi_keyring = MultiKeyring(generator=identity_keyring) + with pytest.raises(GenerateKeyError) as exc_info: + test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_key()) + assert exc_info.match("Unable to generate data encryption key.") + + +def test_number_of_encrypted_data_keys_without_generator_with_children(): + test_multi_keyring = get_multi_keyring_with_no_generator() + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + assert len(test.encrypted_data_keys) == len(test_multi_keyring.children) + + +def test_number_of_encrypted_data_keys_without_children_with_generator(): + test_multi_keyring = get_multi_keyring_with_no_children() + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + assert len(test.encrypted_data_keys) == 1 + + +def test_number_of_encrypted_data_keys_with_generator_and_children(): + test_multi_keyring = get_multi_keyring_with_generator_and_children() + number_of_children = len(test_multi_keyring.children) + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + assert len(test.encrypted_data_keys) == number_of_children + 1 + + +def test_on_encrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) + test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + for keyring in test_multi_keyring._decryption_keyrings: + keyring.on_encrypt.assert_called_once() + + +def test_on_encrypt_edk_length_when_keyring_generates_but_does_not_encrypt_encryption_materials_without_data_key(): + test_multi_keyring = MultiKeyring(generator=OnlyGenerateKeyring()) + len_edk_before_encrypt = len(get_encryption_materials_without_data_key().encrypted_data_keys) + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_key()) + assert test.data_encryption_key is not None + assert len(test.encrypted_data_keys) == len_edk_before_encrypt + + +def test_on_encrypt_edk_length_when_keyring_generates_but_does_not_encrypt_encryption_materials_with_data_key(): + test_multi_keyring = MultiKeyring(generator=OnlyGenerateKeyring()) + test = test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_encrypted_data_key()) + assert len(test.encrypted_data_keys) == len(get_encryption_materials_with_encrypted_data_key().encrypted_data_keys) + + +def test_on_decrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) + test_multi_keyring.on_decrypt(decryption_materials=get_decryption_materials_with_data_key(), encrypted_data_keys=[]) + for keyring in test_multi_keyring._decryption_keyrings: + assert not keyring.on_decrypt.called + + +def test_on_decrypt_every_keyring_called_when_data_encryption_key_not_added(mock_generator, mock_child_1, mock_child_2): + mock_generator.on_decrypt.side_effect = ( + lambda decryption_materials, encrypted_data_keys: get_decryption_materials_without_data_key() + ) + mock_child_1.on_decrypt.return_value = get_decryption_materials_without_data_key() + mock_child_2.on_decrypt.return_value = get_decryption_materials_without_data_key() + + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) + test_multi_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_key(), encrypted_data_keys=[] + ) + + for keyring in test_multi_keyring._decryption_keyrings: + assert keyring.on_decrypt.called + + +def test_no_keyring_called_after_data_encryption_key_added_when_data_encryption_key_not_given( + mock_generator, mock_child_1, mock_child_2, mock_child_3 +): + + mock_generator.on_decrypt.side_effect = ( + lambda decryption_materials, encrypted_data_keys: get_decryption_materials_without_data_key() + ) + + test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_3, mock_child_1, mock_child_2]) + test_multi_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_key(), encrypted_data_keys=[] + ) + assert mock_generator.on_decrypt.called + assert mock_child_3.on_decrypt.called + assert not mock_child_1.called + assert not mock_child_2.called diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 087c8dfa5..1e5d073e8 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -14,14 +14,18 @@ import copy import io import itertools +import os from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa -from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag +from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO -from aws_encryption_sdk.keyring.base import EncryptedDataKey, Keyring +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.keyring.multi_keyring import MultiKeyring +from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring, RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials -from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import @@ -37,6 +41,7 @@ _DATA_KEY = ( b"\x00\xfa\x8c\xdd\x08Au\xc6\x92_4\xc5\xfb\x90\xaf\x8f\xa1D\xaf\xcc\xd25" b"\xa8\x0b\x0b\x16\x92\x91W\x01\xb7\x84" ) +_WRAPPING_KEY_AES = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" _PUBLIC_EXPONENT = 65537 _KEY_SIZE = 2048 @@ -87,6 +92,45 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): return decryption_materials +class OnlyGenerateKeyring(Keyring): + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + if encryption_materials.data_encryption_key is None: + key_provider = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID) + data_encryption_key = RawDataKey( + key_provider=key_provider, data_key=os.urandom(encryption_materials.algorithm.kdf_input_len) + ) + encryption_materials.add_data_encryption_key( + data_encryption_key=data_encryption_key, + keyring_trace=KeyringTrace( + wrapping_key=key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY} + ), + ) + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return decryption_materials + + +def get_encryption_materials_with_data_key(): + return EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + ) + ], + ) + + def get_encryption_materials_with_data_encryption_key(): return EncryptionMaterials( algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, @@ -105,6 +149,42 @@ def get_encryption_materials_with_data_encryption_key(): ) +def get_encryption_materials_without_data_key(): + return EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + ) + + +def get_encryption_materials_with_encrypted_data_key(): + return EncryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encrypted_data_keys=[ + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + encrypted_data_key=b"\xde^\x97\x7f\x84\xe9\x9e\x98\xd0\xe2\xf8\xd5\xcb\xe9\x7f.}\x87\x16,\x11n#\xc8p" + b"\xdb\xbf\x94\x86*Q\x06\xd2\xf5\xdah\x08\xa4p\x81\xf7\xf4G\x07FzE\xde", + ) + ], + encryption_context=_ENCRYPTION_CONTEXT, + signing_key=_SIGNING_KEY, + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={ + KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, + KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, + }, + ) + ], + ) + + def get_encryption_materials_with_encrypted_data_key_aes(): return EncryptionMaterials( algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, @@ -143,6 +223,24 @@ def get_decryption_materials_without_data_encryption_key(): ) +def get_decryption_materials_with_data_key(): + return DecryptionMaterials( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', + ), + encryption_context=_ENCRYPTION_CONTEXT, + verification_key=b"ex_verification_key", + keyring_trace=[ + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY}, + ) + ], + ) + + def get_decryption_materials_with_data_encryption_key(): return DecryptionMaterials( algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, @@ -161,6 +259,73 @@ def get_decryption_materials_with_data_encryption_key(): ) +def get_decryption_materials_without_data_key(): + return DecryptionMaterials(encryption_context=_ENCRYPTION_CONTEXT, verification_key=b"ex_verification_key") + + +def get_multi_keyring_with_generator_and_children(): + return MultiKeyring( + generator=RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + wrapping_key=_WRAPPING_KEY_AES, + ), + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + ], + ) + + +def get_multi_keyring_with_no_children(): + return MultiKeyring( + generator=RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ) + ) + + +def get_multi_keyring_with_no_generator(): + return MultiKeyring( + children=[ + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + private_wrapping_key=rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ), + ), + RawAESKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + wrapping_key=_WRAPPING_KEY_AES, + ), + ] + ) + + def all_valid_kwargs(valid_kwargs): valid = [] for cls, kwargs_sets in valid_kwargs.items(): From 998575c576da98a904ca1aa64ed2c52f3f7d8e66 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Thu, 3 Oct 2019 18:51:16 -0700 Subject: [PATCH 21/64] Merge head of master into keyring (#195) * Update PR template * Added a check for max_age being greater than 0 (#172) * Added a check for max_age being greater than 0 * Fixed flake8 by adding missing pydocstyle dependency * Added the dependency to decrypt_oracle as well * Added test for max_age<=0 ValueError * Updated test for max_age<=0.0 ValueError * Added negative test case * Fixed KMS master key provider tests when default AWS region is configured (#179) * Fixed KMS master key provider tests for users who have their default AWS region configured * created fixture for botocore session with no region set * add auto-used fixture in KMS master key provider unit tests to test against both with and without default region * Wrote example and test for using one kms cmk with an unsigned algorithm * Update one_kms_cmk_unsigned.py * Update examples/src/one_kms_cmk_unsigned.py Co-Authored-By: Matt Bullock * isort-check now succeeds * [issue-190] Regional clients modify default botocore session (#193) * [issue-190] Creation of regional clients modifies default botocore session's region * update changelog with changes for 1.4.1 release * bump version to 1.4.1 * Updates to handle new pylint requirements (#196) * pylint max-attributes appears to be ratcheted down recently * remove unnecessary comprehensions * whitelist some pylint use-constant-test false-positives * reorganize backwards compatibility test requirements definitions attrs==19.2.0 removed a deprecated feature that aws-encryption-sdk==1.3.3 depended on. This reorganization lets us define specific requirements bounds for old versions of aws-encryption-sdk that will probably continue to be necessary as these old versions age. * remove unnecessary comprehensions * add newlines to the end of all requirements files * help pylint ignore mypy type use --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++ CHANGELOG.rst | 17 +++++++ decrypt_oracle/setup.py | 2 +- decrypt_oracle/tox.ini | 2 +- doc/requirements.txt | 2 +- examples/src/one_kms_cmk_unsigned.py | 51 +++++++++++++++++++ examples/test/test_i_one_kms_cmk_unsigned.py | 29 +++++++++++ requirements.txt | 2 +- setup.py | 2 +- src/aws_encryption_sdk/caches/__init__.py | 1 + src/aws_encryption_sdk/identifiers.py | 2 +- src/aws_encryption_sdk/key_providers/kms.py | 4 +- src/aws_encryption_sdk/keyring/base.py | 9 +++- .../keyring/multi_keyring.py | 5 +- src/aws_encryption_sdk/keyring/raw_keyring.py | 8 ++- .../materials_managers/caching.py | 3 ++ src/aws_encryption_sdk/streaming_client.py | 6 +-- src/aws_encryption_sdk/structures.py | 1 + test/integration/integration_test_utils.py | 19 +++++++ .../test_i_aws_encrytion_sdk_client.py | 15 +++++- test/requirements.txt | 2 +- test/unit/test_identifiers.py | 2 +- test/unit/test_material_managers_caching.py | 2 + .../test_providers_kms_master_key_provider.py | 35 +++++++++---- .../compatibility-requirements/1.3.3 | 2 + .../compatibility-requirements/1.3.max | 1 + .../compatibility-requirements/latest | 1 + test_vector_handlers/requirements.txt | 2 +- test_vector_handlers/setup.py | 2 +- test_vector_handlers/test/requirements.txt | 2 +- test_vector_handlers/tox.ini | 6 +-- tox.ini | 2 +- 32 files changed, 208 insertions(+), 35 deletions(-) create mode 100644 examples/src/one_kms_cmk_unsigned.py create mode 100644 examples/test/test_i_one_kms_cmk_unsigned.py create mode 100644 test_vector_handlers/compatibility-requirements/1.3.3 create mode 100644 test_vector_handlers/compatibility-requirements/1.3.max create mode 100644 test_vector_handlers/compatibility-requirements/latest diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ab40d21d7..176df025b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,3 +4,7 @@ By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. + +# Check any applicable: +- [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files. + diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 80b7fc304..d9bca1f73 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,23 @@ Changelog ********* +1.4.1 -- 2019-09-20 +=================== + +Bugfixes +-------- + +* Fix region configuration override in botocore sessions. + `#190 `_ + `#193 `_ + +Minor +----- + +* Caching CMM must require that max age configuration value is greater than 0. + `#147 `_ + `#172 `_ + 1.4.0 -- 2019-05-23 =================== diff --git a/decrypt_oracle/setup.py b/decrypt_oracle/setup.py index e5d9ec84f..e8dfb2aac 100644 --- a/decrypt_oracle/setup.py +++ b/decrypt_oracle/setup.py @@ -22,7 +22,7 @@ def get_version(): def get_requirements(): """Read the requirements file.""" requirements = read("requirements-actual.txt") - return [r for r in requirements.strip().splitlines()] + return list(requirements.strip().splitlines()) setup( diff --git a/decrypt_oracle/tox.ini b/decrypt_oracle/tox.ini index 23a9ece86..f0a7804e5 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/doc/requirements.txt b/doc/requirements.txt index 29e319455..9a54370d6 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,2 +1,2 @@ sphinx>=1.3.0 -sphinx_rtd_theme \ No newline at end of file +sphinx_rtd_theme diff --git a/examples/src/one_kms_cmk_unsigned.py b/examples/src/one_kms_cmk_unsigned.py new file mode 100644 index 000000000..df2f4373d --- /dev/null +++ b/examples/src/one_kms_cmk_unsigned.py @@ -0,0 +1,51 @@ +# Copyright 2019 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. +"""Example showing basic encryption and decryption of a value already in memory +using one KMS CMK with an unsigned algorithm. +""" +from aws_encryption_sdk import KMSMasterKeyProvider, decrypt, encrypt +from aws_encryption_sdk.identifiers import Algorithm + + +def encrypt_decrypt(key_arn, source_plaintext, botocore_session=None): + """Encrypts and then decrypts a string under one KMS customer master key (CMK) with an unsigned algorithm. + + :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param bytes source_plaintext: Data to encrypt + :param botocore_session: existing botocore session instance + :type botocore_session: botocore.session.Session + """ + kwargs = dict(key_ids=[key_arn]) + + if botocore_session is not None: + kwargs["botocore_session"] = botocore_session + + # Create master key provider using the ARN of the key and the session (botocore_session) + kms_key_provider = KMSMasterKeyProvider(**kwargs) + + # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header + ciphertext, encrypted_message_header = encrypt( + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, source=source_plaintext, key_provider=kms_key_provider + ) + + # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header + plaintext, decrypted_message_header = decrypt(source=ciphertext, key_provider=kms_key_provider) + + # Check if the original message and the decrypted message are the same + assert source_plaintext == plaintext + + # Check if the headers of the encrypted message and decrypted message match + assert all( + pair in encrypted_message_header.encryption_context.items() + for pair in decrypted_message_header.encryption_context.items() + ) diff --git a/examples/test/test_i_one_kms_cmk_unsigned.py b/examples/test/test_i_one_kms_cmk_unsigned.py new file mode 100644 index 000000000..8a2758c96 --- /dev/null +++ b/examples/test/test_i_one_kms_cmk_unsigned.py @@ -0,0 +1,29 @@ +# Copyright 2019 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. +"""Unit test suite for the encryption and decryption using one KMS CMK with an unsigned algorithm example.""" + +import botocore.session +import pytest + +from ..src.one_kms_cmk_unsigned import encrypt_decrypt +from .examples_test_utils import get_cmk_arn +from .examples_test_utils import static_plaintext + + +pytestmark = [pytest.mark.examples] + + +def test_one_kms_cmk_unsigned(): + plaintext = static_plaintext + cmk_arn = get_cmk_arn() + encrypt_decrypt(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) diff --git a/requirements.txt b/requirements.txt index 8378d126e..08e1d1a72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ six boto3>=1.4.4 cryptography>=1.8.1 attrs>=19.1.0 -wrapt>=1.10.11 \ No newline at end of file +wrapt>=1.10.11 diff --git a/setup.py b/setup.py index 1afa9d869..6ceb2d8fb 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def get_version(): def get_requirements(): """Reads the requirements file.""" requirements = read("requirements.txt") - return [r for r in requirements.strip().splitlines()] + return list(requirements.strip().splitlines()) setup( diff --git a/src/aws_encryption_sdk/caches/__init__.py b/src/aws_encryption_sdk/caches/__init__.py index 3d3189441..9d2afa485 100644 --- a/src/aws_encryption_sdk/caches/__init__.py +++ b/src/aws_encryption_sdk/caches/__init__.py @@ -143,6 +143,7 @@ class CryptoMaterialsCacheEntryHints(object): @attr.s(hash=False) class CryptoMaterialsCacheEntry(object): + # pylint: disable=too-many-instance-attributes """Value and metadata store for cryptographic materials cache entries. :param bytes cache_key: Identifier for entries in cache diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 7f5cd3f1f..87d5919a1 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -27,7 +27,7 @@ # We only actually need these imports when running the mypy checks pass -__version__ = "1.4.0" +__version__ = "1.4.1" USER_AGENT_SUFFIX = "AwsEncryptionSdkPython/{}".format(__version__) diff --git a/src/aws_encryption_sdk/key_providers/kms.py b/src/aws_encryption_sdk/key_providers/kms.py index df089e3b1..c0a2dc46e 100644 --- a/src/aws_encryption_sdk/key_providers/kms.py +++ b/src/aws_encryption_sdk/key_providers/kms.py @@ -161,8 +161,8 @@ def add_regional_client(self, region_name): :param str region_name: AWS Region ID (ex: us-east-1) """ if region_name not in self._regional_clients: - session = boto3.session.Session(region_name=region_name, botocore_session=self.config.botocore_session) - client = session.client("kms", config=self._user_agent_adding_config) + session = boto3.session.Session(botocore_session=self.config.botocore_session) + client = session.client("kms", region_name=region_name, config=self._user_agent_adding_config) self._register_client(client, region_name) self._regional_clients[region_name] = client diff --git a/src/aws_encryption_sdk/keyring/base.py b/src/aws_encryption_sdk/keyring/base.py index 770b53c0b..236037b4e 100644 --- a/src/aws_encryption_sdk/keyring/base.py +++ b/src/aws_encryption_sdk/keyring/base.py @@ -11,8 +11,13 @@ # 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 +from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import,duplicate-code + DecryptionMaterials, + EncryptionMaterials, +) +from aws_encryption_sdk.structures import ( # only used for mypy; pylint: disable=unused-import,duplicate-code + EncryptedDataKey, +) try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import diff --git a/src/aws_encryption_sdk/keyring/multi_keyring.py b/src/aws_encryption_sdk/keyring/multi_keyring.py index f43820be5..cb2b5cc79 100644 --- a/src/aws_encryption_sdk/keyring/multi_keyring.py +++ b/src/aws_encryption_sdk/keyring/multi_keyring.py @@ -17,7 +17,10 @@ from attr.validators import deep_iterable, instance_of, optional from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError -from aws_encryption_sdk.keyring.base import DecryptionMaterials, EncryptedDataKey, EncryptionMaterials, Keyring +from aws_encryption_sdk.keyring.base import DecryptionMaterials # only used for mypy so pylint: disable=unused-import +from aws_encryption_sdk.keyring.base import EncryptionMaterials # only used for mypy so pylint: disable=unused-import +from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy so pylint: disable=unused-import try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import diff --git a/src/aws_encryption_sdk/keyring/raw_keyring.py b/src/aws_encryption_sdk/keyring/raw_keyring.py index 513119246..922f6ae0c 100644 --- a/src/aws_encryption_sdk/keyring/raw_keyring.py +++ b/src/aws_encryption_sdk/keyring/raw_keyring.py @@ -29,8 +29,12 @@ from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix, serialize_wrapped_key from aws_encryption_sdk.key_providers.raw import RawMasterKey from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials -from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey +from aws_encryption_sdk.materials_managers import ( # only used for mypy so pylint: disable=unused-import + DecryptionMaterials, + EncryptionMaterials, +) +from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy so pylint: disable=unused-import +from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import diff --git a/src/aws_encryption_sdk/materials_managers/caching.py b/src/aws_encryption_sdk/materials_managers/caching.py index b2bdcba9f..992a39a7a 100644 --- a/src/aws_encryption_sdk/materials_managers/caching.py +++ b/src/aws_encryption_sdk/materials_managers/caching.py @@ -108,6 +108,9 @@ def __attrs_post_init__(self): if self.max_bytes_encrypted > MAX_BYTES_PER_KEY: raise ValueError("max_bytes_encrypted cannot exceed {}".format(MAX_BYTES_PER_KEY)) + if self.max_age <= 0.0: + raise ValueError("max_age cannot be less than or equal to 0") + if self.backing_materials_manager is None: if self.master_key_provider is None: raise TypeError("Either backing_materials_manager or master_key_provider must be defined") diff --git a/src/aws_encryption_sdk/streaming_client.py b/src/aws_encryption_sdk/streaming_client.py index 90dc9d25c..504f68977 100644 --- a/src/aws_encryption_sdk/streaming_client.py +++ b/src/aws_encryption_sdk/streaming_client.py @@ -235,7 +235,7 @@ def read(self, b=-1): if not self._message_prepped: self._prep_message() - if self.closed: + if self.closed: # dynamic values confuse pylint: disable=using-constant-test raise ValueError("I/O operation on closed file") if b >= 0: @@ -283,7 +283,7 @@ def readline(self): def readlines(self): """Reads all chunks of output, outputting a list as defined in the IOBase specification.""" - return [line for line in self] + return list(self) def __iter__(self): """Make this class and subclasses identify as iterators.""" @@ -292,7 +292,7 @@ def __iter__(self): def next(self): """Provides hook for Python2 iterator functionality.""" _LOGGER.debug("reading next") - if self.closed: + if self.closed: # dynamic values confuse pylint: disable=using-constant-test _LOGGER.debug("stream is closed") raise StopIteration() diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index 635f661ce..ea9253b39 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -117,6 +117,7 @@ class KeyringTrace(object): @attr.s(hash=True) class MessageHeader(object): + # pylint: disable=too-many-instance-attributes """Deserialized message header object. :param version: Message format version, per spec diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index a5b4d6001..b65d93570 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -13,10 +13,13 @@ """Utility functions to handle configuration and credentials setup for integration tests.""" import os +import botocore.session + from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider AWS_KMS_KEY_ID = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID" _KMS_MKP = None +_KMS_MKP_BOTO = None def get_cmk_arn(): @@ -47,3 +50,19 @@ def setup_kms_master_key_provider(cache=True): _KMS_MKP = kms_master_key_provider return kms_master_key_provider + + +def setup_kms_master_key_provider_with_botocore_session(cache=True): + """Reads the test_values config file and builds the requested KMS Master Key Provider with botocore_session.""" + global _KMS_MKP_BOTO # pylint: disable=global-statement + if cache and _KMS_MKP_BOTO is not None: + return _KMS_MKP_BOTO + + cmk_arn = get_cmk_arn() + kms_master_key_provider = KMSMasterKeyProvider(botocore_session=botocore.session.Session()) + kms_master_key_provider.add_master_key(cmk_arn) + + if cache: + _KMS_MKP_BOTO = kms_master_key_provider + + return kms_master_key_provider diff --git a/test/integration/test_i_aws_encrytion_sdk_client.py b/test/integration/test_i_aws_encrytion_sdk_client.py index 56b0536fd..26df431dc 100644 --- a/test/integration/test_i_aws_encrytion_sdk_client.py +++ b/test/integration/test_i_aws_encrytion_sdk_client.py @@ -21,7 +21,11 @@ from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX, Algorithm from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider -from .integration_test_utils import get_cmk_arn, setup_kms_master_key_provider +from .integration_test_utils import ( + get_cmk_arn, + setup_kms_master_key_provider, + setup_kms_master_key_provider_with_botocore_session, +) pytestmark = [pytest.mark.integ] @@ -68,6 +72,15 @@ def test_remove_bad_client(): assert not test._regional_clients +def test_regional_client_does_not_modify_botocore_session(caplog): + mkp = setup_kms_master_key_provider_with_botocore_session() + fake_region = "us-fakey-12" + + assert mkp.config.botocore_session.get_config_variable("region") != fake_region + mkp.add_regional_client(fake_region) + assert mkp.config.botocore_session.get_config_variable("region") != fake_region + + class TestKMSThickClientIntegration(object): @pytest.fixture(autouse=True) def apply_fixtures(self): diff --git a/test/requirements.txt b/test/requirements.txt index d1b878ce2..152b5dbf4 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,4 +1,4 @@ mock pytest>=3.3.1 pytest-cov -pytest-mock \ No newline at end of file +pytest-mock diff --git a/test/unit/test_identifiers.py b/test/unit/test_identifiers.py index 8f13b1314..3fd421b65 100644 --- a/test/unit/test_identifiers.py +++ b/test/unit/test_identifiers.py @@ -41,7 +41,7 @@ def test_algorithm_safe_to_cache(check_algorithm, safe_to_cache): assert not check_algorithm.safe_to_cache() -@pytest.mark.parametrize("suite", [suite for suite in EncryptionSuite]) +@pytest.mark.parametrize("suite", list(EncryptionSuite)) def test_encryption_suite_invalid_kdf(suite): mock_kdf = Mock() mock_kdf.input_length.return_value = 1 diff --git a/test/unit/test_material_managers_caching.py b/test/unit/test_material_managers_caching.py index 426fe3348..833d6aa53 100644 --- a/test/unit/test_material_managers_caching.py +++ b/test/unit/test_material_managers_caching.py @@ -117,6 +117,8 @@ def test_mkp_to_default_cmm(mocker): dict(max_bytes_encrypted=MAX_BYTES_PER_KEY + 1), r"max_bytes_encrypted cannot exceed {}".format(MAX_BYTES_PER_KEY), ), + (dict(max_age=0.0), r"max_age cannot be less than or equal to 0"), + (dict(max_age=-1.0), r"max_age cannot be less than or equal to 0"), ), ) def test_invalid_values(invalid_kwargs, error_message): diff --git a/test/unit/test_providers_kms_master_key_provider.py b/test/unit/test_providers_kms_master_key_provider.py index 48802a36f..f8d8dc453 100644 --- a/test/unit/test_providers_kms_master_key_provider.py +++ b/test/unit/test_providers_kms_master_key_provider.py @@ -12,6 +12,7 @@ # language governing permissions and limitations under the License. """Unit test suite from aws_encryption_sdk.key_providers.kms.KMSMasterKeyProvider""" import botocore.client +import botocore.session import pytest from mock import ANY, MagicMock, call, patch, sentinel @@ -22,6 +23,19 @@ pytestmark = [pytest.mark.unit, pytest.mark.local] +@pytest.fixture(autouse=True, params=[True, False], ids=["default region", "no default region"]) +def patch_default_region(request, monkeypatch): + """Run all tests in this module both with a default region set and no default region set. + + This ensures that we do not regress on default region handling. + https://github.com/aws/aws-encryption-sdk-python/issues/31 + """ + if request.param: + monkeypatch.setenv("AWS_DEFAULT_REGION", "us-west-2") + else: + monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False) + + def test_init_with_regionless_key_ids_and_region_names(): key_ids = ("alias/key_1",) region_names = ("test-region-1",) @@ -32,6 +46,7 @@ def test_init_with_regionless_key_ids_and_region_names(): class TestKMSMasterKeyProvider(object): @pytest.fixture(autouse=True) def apply_fixtures(self): + self.botocore_no_region_session = botocore.session.Session(session_vars={"region": (None, None, None, None)}) self.mock_botocore_session_patcher = patch("aws_encryption_sdk.key_providers.kms.botocore.session.Session") self.mock_botocore_session = self.mock_botocore_session_patcher.start() self.mock_boto3_session_patcher = patch("aws_encryption_sdk.key_providers.kms.boto3.session.Session") @@ -69,7 +84,7 @@ def test_init_with_region_names(self, mock_add_clients): @patch("aws_encryption_sdk.key_providers.kms.KMSMasterKeyProvider.add_regional_client") def test_init_with_default_region_found(self, mock_add_regional_client): - test = KMSMasterKeyProvider() + test = KMSMasterKeyProvider(botocore_session=self.botocore_no_region_session) assert test.default_region is None with patch.object( test.config.botocore_session, "get_config_variable", return_value=sentinel.default_region @@ -77,11 +92,11 @@ def test_init_with_default_region_found(self, mock_add_regional_client): test._process_config() mock_get_config.assert_called_once_with("region") assert test.default_region is sentinel.default_region - mock_add_regional_client.assert_called_once_with(sentinel.default_region) + mock_add_regional_client.assert_called_with(sentinel.default_region) @patch("aws_encryption_sdk.key_providers.kms.KMSMasterKeyProvider.add_regional_client") def test_init_with_default_region_not_found(self, mock_add_regional_client): - test = KMSMasterKeyProvider() + test = KMSMasterKeyProvider(botocore_session=self.botocore_no_region_session) assert test.default_region is None with patch.object(test.config.botocore_session, "get_config_variable", return_value=None) as mock_get_config: test._process_config() @@ -93,12 +108,14 @@ def test_add_regional_client_new(self): test = KMSMasterKeyProvider() test._regional_clients = {} test.add_regional_client("ex_region_name") - self.mock_boto3_session.assert_called_once_with(region_name="ex_region_name", botocore_session=ANY) - self.mock_boto3_session_instance.client.assert_called_once_with("kms", config=test._user_agent_adding_config) + self.mock_boto3_session.assert_called_with(botocore_session=ANY) + self.mock_boto3_session_instance.client.assert_called_with( + "kms", region_name="ex_region_name", config=test._user_agent_adding_config + ) assert test._regional_clients["ex_region_name"] is self.mock_boto3_client_instance def test_add_regional_client_exists(self): - test = KMSMasterKeyProvider() + test = KMSMasterKeyProvider(botocore_session=self.botocore_no_region_session) test._regional_clients["ex_region_name"] = sentinel.existing_client test.add_regional_client("ex_region_name") assert not self.mock_boto3_session.called @@ -114,7 +131,7 @@ def test_client_valid_region_name(self, mock_add_client): test = KMSMasterKeyProvider() test._regional_clients["us-east-1"] = self.mock_boto3_client_instance client = test._client("arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb") - mock_add_client.assert_called_once_with("us-east-1") + mock_add_client.assert_called_with("us-east-1") assert client is self.mock_boto3_client_instance @patch("aws_encryption_sdk.key_providers.kms.KMSMasterKeyProvider.add_regional_client") @@ -124,10 +141,10 @@ def test_client_no_region_name_with_default(self, mock_add_client): test._regional_clients[sentinel.default_region] = sentinel.default_client client = test._client("") assert client is sentinel.default_client - mock_add_client.assert_called_once_with(sentinel.default_region) + mock_add_client.assert_called_with(sentinel.default_region) def test_client_no_region_name_without_default(self): - test = KMSMasterKeyProvider() + test = KMSMasterKeyProvider(botocore_session=self.botocore_no_region_session) with pytest.raises(UnknownRegionError) as excinfo: test._client("") excinfo.match("No default region found and no region determinable from key id: *") diff --git a/test_vector_handlers/compatibility-requirements/1.3.3 b/test_vector_handlers/compatibility-requirements/1.3.3 new file mode 100644 index 000000000..7aa43f6be --- /dev/null +++ b/test_vector_handlers/compatibility-requirements/1.3.3 @@ -0,0 +1,2 @@ +aws-encryption-sdk==1.3.3 +attrs<19.2.0 diff --git a/test_vector_handlers/compatibility-requirements/1.3.max b/test_vector_handlers/compatibility-requirements/1.3.max new file mode 100644 index 000000000..72c946bf5 --- /dev/null +++ b/test_vector_handlers/compatibility-requirements/1.3.max @@ -0,0 +1 @@ +aws-encryption-sdk >= 1.3.3, < 1.4.0 diff --git a/test_vector_handlers/compatibility-requirements/latest b/test_vector_handlers/compatibility-requirements/latest new file mode 100644 index 000000000..d0197da7f --- /dev/null +++ b/test_vector_handlers/compatibility-requirements/latest @@ -0,0 +1 @@ +aws-encryption-sdk diff --git a/test_vector_handlers/requirements.txt b/test_vector_handlers/requirements.txt index 21e4de2f0..9de0ee532 100644 --- a/test_vector_handlers/requirements.txt +++ b/test_vector_handlers/requirements.txt @@ -1,3 +1,3 @@ attrs >= 17.4.0 aws-encryption-sdk -six \ No newline at end of file +six diff --git a/test_vector_handlers/setup.py b/test_vector_handlers/setup.py index f191b3869..bd16c76c8 100644 --- a/test_vector_handlers/setup.py +++ b/test_vector_handlers/setup.py @@ -22,7 +22,7 @@ def get_version(): def get_requirements(): """Read the requirements file.""" requirements = read("requirements.txt") - return [r for r in requirements.strip().splitlines()] + return list(requirements.strip().splitlines()) setup( diff --git a/test_vector_handlers/test/requirements.txt b/test_vector_handlers/test/requirements.txt index d1b878ce2..152b5dbf4 100644 --- a/test_vector_handlers/test/requirements.txt +++ b/test_vector_handlers/test/requirements.txt @@ -1,4 +1,4 @@ mock pytest>=3.3.1 pytest-cov -pytest-mock \ No newline at end of file +pytest-mock diff --git a/test_vector_handlers/tox.ini b/test_vector_handlers/tox.ini index 748ba4881..fc2c8d5a7 100644 --- a/test_vector_handlers/tox.ini +++ b/test_vector_handlers/tox.ini @@ -46,9 +46,9 @@ passenv = sitepackages = False deps = -rtest/requirements.txt - awses_1.3.3: aws-encryption-sdk==1.3.3 - awses_1.3.max: aws-encryption-sdk >= 1.3.3, < 1.4.0 - awses_latest: aws-encryption-sdk + awses_1.3.3: -rcompatibility-requirements/1.3.3 + awses_1.3.max: -rcompatibility-requirements/1.3.max + awses_latest: -rcompatibility-requirements/latest commands = {[testenv:base-command]commands} [testenv:full-encrypt] diff --git a/tox.ini b/tox.ini index 3e5677ece..06564ef6a 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 From 2d4b2c41cdcfaf7b0fc6075ab7bc465118512db8 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Mon, 7 Oct 2019 14:34:57 -0700 Subject: [PATCH 22/64] Keyring reorg (#198) * reorganize keyring tests to match #100 and keyring namespace to simplify names * change aws_encryption_sdk.keyring to aws_encryption_sdk.keyrings for consistency in namespace names * autoformat * disable pylint similarity checks on imports Because we provide multiple implementations of certain types of things (keyrings, etc), this was triggering pylint due to them all needing to import the same types in order to function. * remove aws_encryption_sdk namespace layer in test reorg --- .../{keyring => keyrings}/__init__.py | 0 .../{keyring => keyrings}/base.py | 6 ++---- .../multi_keyring.py => keyrings/multi.py} | 11 +++++++---- .../{keyring/raw_keyring.py => keyrings/raw.py} | 14 +++++++++----- src/pylintrc | 3 +++ test/__init__.py | 0 test/functional/keyrings/__init__.py | 13 +++++++++++++ test/functional/keyrings/raw/__init__.py | 13 +++++++++++++ .../raw/test_raw_aes.py} | 2 +- .../raw/test_raw_rsa.py} | 2 +- .../test_multi.py} | 4 ++-- test/unit/keyrings/__init__.py | 13 +++++++++++++ test/unit/keyrings/raw/__init__.py | 13 +++++++++++++ .../raw/test_raw_aes.py} | 12 ++++++------ .../raw/test_raw_rsa.py} | 14 +++++++------- .../test_base.py} | 2 +- .../test_multi.py} | 12 ++++++------ test/unit/test_utils.py | 2 +- test/unit/unit_test_utils.py | 6 +++--- 19 files changed, 101 insertions(+), 41 deletions(-) rename src/aws_encryption_sdk/{keyring => keyrings}/__init__.py (100%) rename src/aws_encryption_sdk/{keyring => keyrings}/base.py (93%) rename src/aws_encryption_sdk/{keyring/multi_keyring.py => keyrings/multi.py} (93%) rename src/aws_encryption_sdk/{keyring/raw_keyring.py => keyrings/raw.py} (98%) create mode 100644 test/__init__.py create mode 100644 test/functional/keyrings/__init__.py create mode 100644 test/functional/keyrings/raw/__init__.py rename test/functional/{test_f_keyring_raw_aes.py => keyrings/raw/test_raw_aes.py} (99%) rename test/functional/{test_f_keyring_raw_rsa.py => keyrings/raw/test_raw_rsa.py} (99%) rename test/functional/{test_f_multi_keyring.py => keyrings/test_multi.py} (97%) create mode 100644 test/unit/keyrings/__init__.py create mode 100644 test/unit/keyrings/raw/__init__.py rename test/unit/{test_keyring_raw_aes.py => keyrings/raw/test_raw_aes.py} (96%) rename test/unit/{test_keyring_raw_rsa.py => keyrings/raw/test_raw_rsa.py} (96%) rename test/unit/{test_keyring_base.py => keyrings/test_base.py} (97%) rename test/unit/{test_keyring_multi.py => keyrings/test_multi.py} (97%) diff --git a/src/aws_encryption_sdk/keyring/__init__.py b/src/aws_encryption_sdk/keyrings/__init__.py similarity index 100% rename from src/aws_encryption_sdk/keyring/__init__.py rename to src/aws_encryption_sdk/keyrings/__init__.py diff --git a/src/aws_encryption_sdk/keyring/base.py b/src/aws_encryption_sdk/keyrings/base.py similarity index 93% rename from src/aws_encryption_sdk/keyring/base.py rename to src/aws_encryption_sdk/keyrings/base.py index 236037b4e..32f810e47 100644 --- a/src/aws_encryption_sdk/keyring/base.py +++ b/src/aws_encryption_sdk/keyrings/base.py @@ -11,13 +11,11 @@ # 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 ( # only used for mypy; pylint: disable=unused-import,duplicate-code +from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import DecryptionMaterials, EncryptionMaterials, ) -from aws_encryption_sdk.structures import ( # only used for mypy; pylint: disable=unused-import,duplicate-code - EncryptedDataKey, -) +from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy; pylint: disable=unused-import try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import diff --git a/src/aws_encryption_sdk/keyring/multi_keyring.py b/src/aws_encryption_sdk/keyrings/multi.py similarity index 93% rename from src/aws_encryption_sdk/keyring/multi_keyring.py rename to src/aws_encryption_sdk/keyrings/multi.py index cb2b5cc79..4274c1c4d 100644 --- a/src/aws_encryption_sdk/keyring/multi_keyring.py +++ b/src/aws_encryption_sdk/keyrings/multi.py @@ -17,10 +17,13 @@ from attr.validators import deep_iterable, instance_of, optional from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError -from aws_encryption_sdk.keyring.base import DecryptionMaterials # only used for mypy so pylint: disable=unused-import -from aws_encryption_sdk.keyring.base import EncryptionMaterials # only used for mypy so pylint: disable=unused-import -from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy so pylint: disable=unused-import +from aws_encryption_sdk.keyrings.base import Keyring + +from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import + DecryptionMaterials, + EncryptionMaterials, +) +from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy; pylint: disable=unused-import try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import diff --git a/src/aws_encryption_sdk/keyring/raw_keyring.py b/src/aws_encryption_sdk/keyrings/raw.py similarity index 98% rename from src/aws_encryption_sdk/keyring/raw_keyring.py rename to src/aws_encryption_sdk/keyrings/raw.py index 922f6ae0c..dea924eae 100644 --- a/src/aws_encryption_sdk/keyring/raw_keyring.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -11,7 +11,6 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Resources required for Raw Keyrings.""" - import logging import os @@ -28,13 +27,18 @@ from aws_encryption_sdk.internal.formatting.deserialize import deserialize_wrapped_key from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix, serialize_wrapped_key from aws_encryption_sdk.key_providers.raw import RawMasterKey -from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.materials_managers import ( # only used for mypy so pylint: disable=unused-import +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.structures import ( # pylint: disable=unused-import + EncryptedDataKey, + KeyringTrace, + MasterKeyInfo, + RawDataKey, +) + +from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import DecryptionMaterials, EncryptionMaterials, ) -from aws_encryption_sdk.structures import EncryptedDataKey # only used for mypy so pylint: disable=unused-import -from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import diff --git a/src/pylintrc b/src/pylintrc index af00ced56..1722f208c 100644 --- a/src/pylintrc +++ b/src/pylintrc @@ -33,6 +33,9 @@ additional-builtins = raw_input [DESIGN] max-args = 10 +[SIMILARITIES] +ignore-imports = yes + [FORMAT] max-line-length = 120 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/functional/keyrings/__init__.py b/test/functional/keyrings/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/functional/keyrings/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/keyrings/raw/__init__.py b/test/functional/keyrings/raw/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/functional/keyrings/raw/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/test_f_keyring_raw_aes.py b/test/functional/keyrings/raw/test_raw_aes.py similarity index 99% rename from test/functional/test_f_keyring_raw_aes.py rename to test/functional/keyrings/raw/test_raw_aes.py index aa08b07ff..0cfad9d2c 100644 --- a/test/functional/test_f_keyring_raw_aes.py +++ b/test/functional/keyrings/raw/test_raw_aes.py @@ -24,7 +24,7 @@ from aws_encryption_sdk.internal.crypto import WrappingKey from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix from aws_encryption_sdk.key_providers.raw import RawMasterKey -from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey diff --git a/test/functional/test_f_keyring_raw_rsa.py b/test/functional/keyrings/raw/test_raw_rsa.py similarity index 99% rename from test/functional/test_f_keyring_raw_rsa.py rename to test/functional/keyrings/raw/test_raw_rsa.py index bdf5bf25c..d900c5cdf 100644 --- a/test/functional/test_f_keyring_raw_rsa.py +++ b/test/functional/keyrings/raw/test_raw_rsa.py @@ -26,7 +26,7 @@ ) from aws_encryption_sdk.internal.crypto import WrappingKey from aws_encryption_sdk.key_providers.raw import RawMasterKey -from aws_encryption_sdk.keyring.raw_keyring import RawRSAKeyring +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey diff --git a/test/functional/test_f_multi_keyring.py b/test/functional/keyrings/test_multi.py similarity index 97% rename from test/functional/test_f_multi_keyring.py rename to test/functional/keyrings/test_multi.py index 54d519f0b..c6de06f0e 100644 --- a/test/functional/test_f_multi_keyring.py +++ b/test/functional/keyrings/test_multi.py @@ -18,8 +18,8 @@ from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.defaults import ALGORITHM -from aws_encryption_sdk.keyring.multi_keyring import MultiKeyring -from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring, RawRSAKeyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring, RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials from aws_encryption_sdk.structures import KeyringTrace, MasterKeyInfo, RawDataKey diff --git a/test/unit/keyrings/__init__.py b/test/unit/keyrings/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/keyrings/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/keyrings/raw/__init__.py b/test/unit/keyrings/raw/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/keyrings/raw/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_keyring_raw_aes.py b/test/unit/keyrings/raw/test_raw_aes.py similarity index 96% rename from test/unit/test_keyring_raw_aes.py rename to test/unit/keyrings/raw/test_raw_aes.py index b98279d04..43de2a697 100644 --- a/test/unit/test_keyring_raw_aes.py +++ b/test/unit/keyrings/raw/test_raw_aes.py @@ -19,15 +19,15 @@ from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.key_providers.raw -import aws_encryption_sdk.keyring.raw_keyring +import aws_encryption_sdk.keyrings.raw from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey -from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.keyring.raw_keyring import GenerateKeyError, RawAESKeyring, _generate_data_key +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.raw import GenerateKeyError, RawAESKeyring, _generate_data_key from aws_encryption_sdk.materials_managers import EncryptionMaterials from aws_encryption_sdk.structures import MasterKeyInfo -from .unit_test_utils import ( +from ...unit_test_utils import ( _DATA_KEY, _ENCRYPTED_DATA_KEY_AES, _ENCRYPTED_DATA_KEY_NOT_IN_KEYRING, @@ -57,8 +57,8 @@ def raw_aes_keyring(): @pytest.fixture def patch_generate_data_key(mocker): - mocker.patch.object(aws_encryption_sdk.keyring.raw_keyring, "_generate_data_key") - return aws_encryption_sdk.keyring.raw_keyring._generate_data_key + mocker.patch.object(aws_encryption_sdk.keyrings.raw, "_generate_data_key") + return aws_encryption_sdk.keyrings.raw._generate_data_key @pytest.fixture diff --git a/test/unit/test_keyring_raw_rsa.py b/test/unit/keyrings/raw/test_raw_rsa.py similarity index 96% rename from test/unit/test_keyring_raw_rsa.py rename to test/unit/keyrings/raw/test_raw_rsa.py index 1fcf23da5..e460a4221 100644 --- a/test/unit/test_keyring_raw_rsa.py +++ b/test/unit/keyrings/raw/test_raw_rsa.py @@ -17,14 +17,14 @@ from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.key_providers.raw -import aws_encryption_sdk.keyring.raw_keyring +import aws_encryption_sdk.keyrings.raw from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey -from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.keyring.raw_keyring import RawRSAKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring -from .test_values import VALUES -from .unit_test_utils import ( +from ...test_values import VALUES +from ...unit_test_utils import ( _BACKEND, _DATA_KEY, _ENCRYPTED_DATA_KEY_RSA, @@ -58,8 +58,8 @@ def raw_rsa_private_key(): @pytest.fixture def patch_generate_data_key(mocker): - mocker.patch.object(aws_encryption_sdk.keyring.raw_keyring, "_generate_data_key") - return aws_encryption_sdk.keyring.raw_keyring._generate_data_key + mocker.patch.object(aws_encryption_sdk.keyrings.raw, "_generate_data_key") + return aws_encryption_sdk.keyrings.raw._generate_data_key @pytest.fixture diff --git a/test/unit/test_keyring_base.py b/test/unit/keyrings/test_base.py similarity index 97% rename from test/unit/test_keyring_base.py rename to test/unit/keyrings/test_base.py index 70863de53..08522de0a 100644 --- a/test/unit/test_keyring_base.py +++ b/test/unit/keyrings/test_base.py @@ -15,7 +15,7 @@ import pytest from aws_encryption_sdk.identifiers import Algorithm -from aws_encryption_sdk.keyring.base import Keyring +from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_keyring_multi.py b/test/unit/keyrings/test_multi.py similarity index 97% rename from test/unit/test_keyring_multi.py rename to test/unit/keyrings/test_multi.py index 6b66a490f..c0bdc78d9 100644 --- a/test/unit/test_keyring_multi.py +++ b/test/unit/keyrings/test_multi.py @@ -19,11 +19,11 @@ from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError from aws_encryption_sdk.identifiers import WrappingAlgorithm from aws_encryption_sdk.internal.formatting import serialize -from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.keyring.multi_keyring import MultiKeyring -from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring -from .unit_test_utils import ( +from ..unit_test_utils import ( IdentityKeyring, OnlyGenerateKeyring, get_decryption_materials_with_data_key, @@ -139,10 +139,10 @@ def test_keyring_with_no_generator_no_children(): def test_keyring_with_invalid_parameters(generator, children): with pytest.raises(TypeError) as exc_info: MultiKeyring(generator=generator, children=children) - assert exc_info.match("('children'|'generator') must be .*") + assert exc_info.match("('children'|'generator') must be .*") -def test_decryption_keyrings(): +def test_decryption_keyring(): test_multi_keyring = get_multi_keyring_with_generator_and_children() assert test_multi_keyring.generator in test_multi_keyring._decryption_keyrings for child_keyring in test_multi_keyring.children: diff --git a/test/unit/test_utils.py b/test/unit/test_utils.py index 0dee36b41..b83e5de1d 100644 --- a/test/unit/test_utils.py +++ b/test/unit/test_utils.py @@ -21,7 +21,7 @@ import aws_encryption_sdk.internal.utils from aws_encryption_sdk.exceptions import InvalidDataKeyError, SerializationError, UnknownIdentityError from aws_encryption_sdk.internal.defaults import MAX_FRAME_SIZE, MESSAGE_ID_LENGTH -from aws_encryption_sdk.keyring.base import EncryptedDataKey +from aws_encryption_sdk.keyrings.base import EncryptedDataKey from aws_encryption_sdk.structures import DataKey, MasterKeyInfo, RawDataKey from .test_values import VALUES diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 1e5d073e8..2bf1bc838 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -21,9 +21,9 @@ from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO -from aws_encryption_sdk.keyring.base import Keyring -from aws_encryption_sdk.keyring.multi_keyring import MultiKeyring -from aws_encryption_sdk.keyring.raw_keyring import RawAESKeyring, RawRSAKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawAESKeyring, RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey From a446f379c816e01416475c1b2c44f702066cf53e Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Mon, 7 Oct 2019 16:32:08 -0700 Subject: [PATCH 23/64] Reorganize tests to easy readability and further development (#199) * reorganize keyring tests to match #100 and keyring namespace to simplify names * change aws_encryption_sdk.keyring to aws_encryption_sdk.keyrings for consistency in namespace names * autoformat * disable pylint similarity checks on imports Because we provide multiple implementations of certain types of things (keyrings, etc), this was triggering pylint due to them all needing to import the same types in order to function. * reorganize functional and integration tests * define structure for unit tests reorg * reorganize unit tests * rename test_values to vectors to avoid it being confused for a test file * remove aws_encryption_sdk from functional test namespace * remove aws_encryption_sdk from integration test namespace * remove aws_encryption_sdk from unit test namespace * rename test_crypto to vectors to avoid confusion * autoformat --- test/functional/__init__.py | 3 ++- test/functional/internal/__init__.py | 13 +++++++++++++ test/functional/internal/crypto/__init__.py | 13 +++++++++++++ .../crypto/test_crypto.py} | 0 .../crypto/test_iv.py} | 0 ..._aws_encryption_sdk_client.py => test_client.py} | 0 test/integration/__init__.py | 3 ++- ...i_aws_encrytion_sdk_client.py => test_client.py} | 0 ...est_i_thread_safety.py => test_thread_safety.py} | 0 test/unit/__init__.py | 3 ++- test/unit/caches/__init__.py | 13 +++++++++++++ .../{test_caches_base.py => caches/test_base.py} | 0 test/unit/{ => caches}/test_caches.py | 0 .../test_crypto_cache_entry.py} | 0 .../{test_caches_local.py => caches/test_local.py} | 0 .../{test_caches_null.py => caches/test_null.py} | 0 test/unit/internal/__init__.py | 13 +++++++++++++ test/unit/internal/crypto/__init__.py | 13 +++++++++++++ .../unit/internal/crypto/authentication/__init__.py | 13 +++++++++++++ .../test_prehashing_authenticator.py} | 0 .../crypto/authentication/test_signer.py} | 2 +- .../crypto/authentication/test_verifier.py} | 2 +- test/unit/internal/crypto/encryption/__init__.py | 13 +++++++++++++ .../crypto/encryption/test_decryptor.py} | 0 .../crypto/encryption/test_encryptor.py} | 0 .../crypto/test_data_keys.py} | 0 .../crypto/test_elliptic_curve.py} | 3 +-- .../crypto/test_wrapping_keys.py} | 2 +- .../{test_crypto.py => internal/crypto/vectors.py} | 0 test/unit/internal/formatting/__init__.py | 13 +++++++++++++ .../{ => internal/formatting}/test_deserialize.py | 2 +- .../formatting}/test_encryption_context.py | 2 +- .../{ => internal/formatting}/test_serialize.py | 2 +- test/unit/{ => internal}/test_defaults.py | 0 .../test_structures.py} | 2 +- test/unit/internal/utils/__init__.py | 13 +++++++++++++ .../utils/test_str_ops.py} | 0 .../utils/test_streams.py} | 2 +- test/unit/{ => internal/utils}/test_utils.py | 4 ++-- test/unit/key_providers/__init__.py | 13 +++++++++++++ test/unit/key_providers/base/__init__.py | 13 +++++++++++++ .../base/test_base_master_key.py} | 2 +- .../base/test_base_master_key_config.py} | 2 +- .../base/test_base_master_key_provider.py} | 2 +- .../base/test_base_master_key_provider_config.py} | 0 test/unit/key_providers/kms/__init__.py | 13 +++++++++++++ .../kms/test_kms_master_key.py} | 2 +- .../kms/test_kms_master_key_config.py} | 2 +- .../kms/test_kms_master_key_provider.py} | 0 .../kms/test_kms_master_key_provider_config.py} | 2 +- test/unit/key_providers/raw/__init__.py | 13 +++++++++++++ .../raw/test_raw_master_key.py} | 2 +- .../raw/test_raw_master_key_config.py} | 2 +- .../raw/test_raw_master_key_provider.py} | 2 +- test/unit/keyrings/raw/test_raw_rsa.py | 2 +- test/unit/materials_managers/__init__.py | 13 +++++++++++++ .../test_base.py} | 0 .../test_caching.py} | 0 .../test_default.py} | 0 .../test_material_managers.py | 0 test/unit/streaming_client/__init__.py | 13 +++++++++++++ .../test_configs.py} | 2 +- .../test_encryption_stream.py} | 4 ++-- .../test_stream_decryptor.py} | 2 +- .../test_stream_encryptor.py} | 2 +- .../{test_aws_encryption_sdk.py => test_client.py} | 0 test/unit/{test_values.py => vectors.py} | 0 67 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 test/functional/internal/__init__.py create mode 100644 test/functional/internal/crypto/__init__.py rename test/functional/{test_f_crypto.py => internal/crypto/test_crypto.py} (100%) rename test/functional/{test_f_crypto_iv.py => internal/crypto/test_iv.py} (100%) rename test/functional/{test_f_aws_encryption_sdk_client.py => test_client.py} (100%) rename test/integration/{test_i_aws_encrytion_sdk_client.py => test_client.py} (100%) rename test/integration/{test_i_thread_safety.py => test_thread_safety.py} (100%) create mode 100644 test/unit/caches/__init__.py rename test/unit/{test_caches_base.py => caches/test_base.py} (100%) rename test/unit/{ => caches}/test_caches.py (100%) rename test/unit/{test_caches_crypto_cache_entry.py => caches/test_crypto_cache_entry.py} (100%) rename test/unit/{test_caches_local.py => caches/test_local.py} (100%) rename test/unit/{test_caches_null.py => caches/test_null.py} (100%) create mode 100644 test/unit/internal/__init__.py create mode 100644 test/unit/internal/crypto/__init__.py create mode 100644 test/unit/internal/crypto/authentication/__init__.py rename test/unit/{test_crypto_prehashing_authenticator.py => internal/crypto/authentication/test_prehashing_authenticator.py} (100%) rename test/unit/{test_crypto_authentication_signer.py => internal/crypto/authentication/test_signer.py} (99%) rename test/unit/{test_crypto_authentication_verifier.py => internal/crypto/authentication/test_verifier.py} (99%) create mode 100644 test/unit/internal/crypto/encryption/__init__.py rename test/unit/{test_crypto_encryption_decryptor.py => internal/crypto/encryption/test_decryptor.py} (100%) rename test/unit/{test_crypto_encryption_encryptor.py => internal/crypto/encryption/test_encryptor.py} (100%) rename test/unit/{test_crypto_data_keys.py => internal/crypto/test_data_keys.py} (100%) rename test/unit/{test_crypto_elliptic_curve.py => internal/crypto/test_elliptic_curve.py} (99%) rename test/unit/{test_crypto_wrapping_keys.py => internal/crypto/test_wrapping_keys.py} (99%) rename test/unit/{test_crypto.py => internal/crypto/vectors.py} (100%) create mode 100644 test/unit/internal/formatting/__init__.py rename test/unit/{ => internal/formatting}/test_deserialize.py (99%) rename test/unit/{ => internal/formatting}/test_encryption_context.py (99%) rename test/unit/{ => internal/formatting}/test_serialize.py (99%) rename test/unit/{ => internal}/test_defaults.py (100%) rename test/unit/{test_internal_structures.py => internal/test_structures.py} (97%) create mode 100644 test/unit/internal/utils/__init__.py rename test/unit/{test_util_str_ops.py => internal/utils/test_str_ops.py} (100%) rename test/unit/{test_util_streams.py => internal/utils/test_streams.py} (96%) rename test/unit/{ => internal/utils}/test_utils.py (99%) create mode 100644 test/unit/key_providers/__init__.py create mode 100644 test/unit/key_providers/base/__init__.py rename test/unit/{test_providers_base_master_key.py => key_providers/base/test_base_master_key.py} (99%) rename test/unit/{test_providers_base_master_key_config.py => key_providers/base/test_base_master_key_config.py} (96%) rename test/unit/{test_providers_base_master_key_provider.py => key_providers/base/test_base_master_key_provider.py} (99%) rename test/unit/{test_providers_base_master_key_provider_config.py => key_providers/base/test_base_master_key_provider_config.py} (100%) create mode 100644 test/unit/key_providers/kms/__init__.py rename test/unit/{test_providers_kms_master_key.py => key_providers/kms/test_kms_master_key.py} (99%) rename test/unit/{test_providers_kms_master_key_config.py => key_providers/kms/test_kms_master_key_config.py} (97%) rename test/unit/{test_providers_kms_master_key_provider.py => key_providers/kms/test_kms_master_key_provider.py} (100%) rename test/unit/{test_providers_kms_master_key_provider_config.py => key_providers/kms/test_kms_master_key_provider_config.py} (97%) create mode 100644 test/unit/key_providers/raw/__init__.py rename test/unit/{test_providers_raw_master_key.py => key_providers/raw/test_raw_master_key.py} (99%) rename test/unit/{test_providers_raw_master_key_config.py => key_providers/raw/test_raw_master_key_config.py} (96%) rename test/unit/{test_providers_raw_master_key_provider.py => key_providers/raw/test_raw_master_key_provider.py} (98%) create mode 100644 test/unit/materials_managers/__init__.py rename test/unit/{test_material_managers_base.py => materials_managers/test_base.py} (100%) rename test/unit/{test_material_managers_caching.py => materials_managers/test_caching.py} (100%) rename test/unit/{test_material_managers_default.py => materials_managers/test_default.py} (100%) rename test/unit/{ => materials_managers}/test_material_managers.py (100%) create mode 100644 test/unit/streaming_client/__init__.py rename test/unit/{test_streaming_client_configs.py => streaming_client/test_configs.py} (98%) rename test/unit/{test_streaming_client_encryption_stream.py => streaming_client/test_encryption_stream.py} (99%) rename test/unit/{test_streaming_client_stream_decryptor.py => streaming_client/test_stream_decryptor.py} (99%) rename test/unit/{test_streaming_client_stream_encryptor.py => streaming_client/test_stream_encryptor.py} (99%) rename test/unit/{test_aws_encryption_sdk.py => test_client.py} (100%) rename test/unit/{test_values.py => vectors.py} (100%) diff --git a/test/functional/__init__.py b/test/functional/__init__.py index 53a960891..ad0e71d6c 100644 --- a/test/functional/__init__.py +++ b/test/functional/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2019 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 @@ -10,3 +10,4 @@ # 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. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/internal/__init__.py b/test/functional/internal/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/functional/internal/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/internal/crypto/__init__.py b/test/functional/internal/crypto/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/functional/internal/crypto/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/functional/test_f_crypto.py b/test/functional/internal/crypto/test_crypto.py similarity index 100% rename from test/functional/test_f_crypto.py rename to test/functional/internal/crypto/test_crypto.py diff --git a/test/functional/test_f_crypto_iv.py b/test/functional/internal/crypto/test_iv.py similarity index 100% rename from test/functional/test_f_crypto_iv.py rename to test/functional/internal/crypto/test_iv.py diff --git a/test/functional/test_f_aws_encryption_sdk_client.py b/test/functional/test_client.py similarity index 100% rename from test/functional/test_f_aws_encryption_sdk_client.py rename to test/functional/test_client.py diff --git a/test/integration/__init__.py b/test/integration/__init__.py index 53a960891..ad0e71d6c 100644 --- a/test/integration/__init__.py +++ b/test/integration/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2019 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 @@ -10,3 +10,4 @@ # 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. +"""Dummy stub to make linters work better.""" diff --git a/test/integration/test_i_aws_encrytion_sdk_client.py b/test/integration/test_client.py similarity index 100% rename from test/integration/test_i_aws_encrytion_sdk_client.py rename to test/integration/test_client.py diff --git a/test/integration/test_i_thread_safety.py b/test/integration/test_thread_safety.py similarity index 100% rename from test/integration/test_i_thread_safety.py rename to test/integration/test_thread_safety.py diff --git a/test/unit/__init__.py b/test/unit/__init__.py index 53a960891..ad0e71d6c 100644 --- a/test/unit/__init__.py +++ b/test/unit/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright 2019 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 @@ -10,3 +10,4 @@ # 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/caches/__init__.py b/test/unit/caches/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/caches/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_caches_base.py b/test/unit/caches/test_base.py similarity index 100% rename from test/unit/test_caches_base.py rename to test/unit/caches/test_base.py diff --git a/test/unit/test_caches.py b/test/unit/caches/test_caches.py similarity index 100% rename from test/unit/test_caches.py rename to test/unit/caches/test_caches.py diff --git a/test/unit/test_caches_crypto_cache_entry.py b/test/unit/caches/test_crypto_cache_entry.py similarity index 100% rename from test/unit/test_caches_crypto_cache_entry.py rename to test/unit/caches/test_crypto_cache_entry.py diff --git a/test/unit/test_caches_local.py b/test/unit/caches/test_local.py similarity index 100% rename from test/unit/test_caches_local.py rename to test/unit/caches/test_local.py diff --git a/test/unit/test_caches_null.py b/test/unit/caches/test_null.py similarity index 100% rename from test/unit/test_caches_null.py rename to test/unit/caches/test_null.py diff --git a/test/unit/internal/__init__.py b/test/unit/internal/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/internal/crypto/__init__.py b/test/unit/internal/crypto/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/crypto/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/internal/crypto/authentication/__init__.py b/test/unit/internal/crypto/authentication/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/crypto/authentication/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_crypto_prehashing_authenticator.py b/test/unit/internal/crypto/authentication/test_prehashing_authenticator.py similarity index 100% rename from test/unit/test_crypto_prehashing_authenticator.py rename to test/unit/internal/crypto/authentication/test_prehashing_authenticator.py diff --git a/test/unit/test_crypto_authentication_signer.py b/test/unit/internal/crypto/authentication/test_signer.py similarity index 99% rename from test/unit/test_crypto_authentication_signer.py rename to test/unit/internal/crypto/authentication/test_signer.py index eae064130..0a55b2e48 100644 --- a/test/unit/test_crypto_authentication_signer.py +++ b/test/unit/internal/crypto/authentication/test_signer.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.internal.crypto.authentication import Signer from aws_encryption_sdk.internal.defaults import ALGORITHM -from .test_crypto import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_crypto_authentication_verifier.py b/test/unit/internal/crypto/authentication/test_verifier.py similarity index 99% rename from test/unit/test_crypto_authentication_verifier.py rename to test/unit/internal/crypto/authentication/test_verifier.py index a55e8f517..e25fb78f3 100644 --- a/test/unit/test_crypto_authentication_verifier.py +++ b/test/unit/internal/crypto/authentication/test_verifier.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.internal.crypto.authentication import Verifier from aws_encryption_sdk.internal.defaults import ALGORITHM -from .test_crypto import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/internal/crypto/encryption/__init__.py b/test/unit/internal/crypto/encryption/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/crypto/encryption/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_crypto_encryption_decryptor.py b/test/unit/internal/crypto/encryption/test_decryptor.py similarity index 100% rename from test/unit/test_crypto_encryption_decryptor.py rename to test/unit/internal/crypto/encryption/test_decryptor.py diff --git a/test/unit/test_crypto_encryption_encryptor.py b/test/unit/internal/crypto/encryption/test_encryptor.py similarity index 100% rename from test/unit/test_crypto_encryption_encryptor.py rename to test/unit/internal/crypto/encryption/test_encryptor.py diff --git a/test/unit/test_crypto_data_keys.py b/test/unit/internal/crypto/test_data_keys.py similarity index 100% rename from test/unit/test_crypto_data_keys.py rename to test/unit/internal/crypto/test_data_keys.py diff --git a/test/unit/test_crypto_elliptic_curve.py b/test/unit/internal/crypto/test_elliptic_curve.py similarity index 99% rename from test/unit/test_crypto_elliptic_curve.py rename to test/unit/internal/crypto/test_elliptic_curve.py index b030db5c2..16dcd2686 100644 --- a/test/unit/test_crypto_elliptic_curve.py +++ b/test/unit/internal/crypto/test_elliptic_curve.py @@ -17,7 +17,6 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.utils import InterfaceNotImplemented from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.internal.crypto.elliptic_curve from aws_encryption_sdk.exceptions import NotSupportedError @@ -31,7 +30,7 @@ generate_ecc_signing_key, ) -from .test_crypto import VALUES +from .vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_crypto_wrapping_keys.py b/test/unit/internal/crypto/test_wrapping_keys.py similarity index 99% rename from test/unit/test_crypto_wrapping_keys.py rename to test/unit/internal/crypto/test_wrapping_keys.py index cb7b4489a..2bdcb0983 100644 --- a/test/unit/test_crypto_wrapping_keys.py +++ b/test/unit/internal/crypto/test_wrapping_keys.py @@ -21,7 +21,7 @@ from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.structures import EncryptedData -from .test_crypto import VALUES +from .vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_crypto.py b/test/unit/internal/crypto/vectors.py similarity index 100% rename from test/unit/test_crypto.py rename to test/unit/internal/crypto/vectors.py diff --git a/test/unit/internal/formatting/__init__.py b/test/unit/internal/formatting/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/formatting/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_deserialize.py b/test/unit/internal/formatting/test_deserialize.py similarity index 99% rename from test/unit/test_deserialize.py rename to test/unit/internal/formatting/test_deserialize.py index 8a96ea4ca..d19093320 100644 --- a/test/unit/test_deserialize.py +++ b/test/unit/internal/formatting/test_deserialize.py @@ -23,7 +23,7 @@ from aws_encryption_sdk.identifiers import AlgorithmSuite from aws_encryption_sdk.internal.structures import EncryptedData -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_encryption_context.py b/test/unit/internal/formatting/test_encryption_context.py similarity index 99% rename from test/unit/test_encryption_context.py rename to test/unit/internal/formatting/test_encryption_context.py index 187365783..443df4065 100644 --- a/test/unit/test_encryption_context.py +++ b/test/unit/internal/formatting/test_encryption_context.py @@ -18,7 +18,7 @@ from aws_encryption_sdk.exceptions import SerializationError from aws_encryption_sdk.identifiers import ContentAADString -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_serialize.py b/test/unit/internal/formatting/test_serialize.py similarity index 99% rename from test/unit/test_serialize.py rename to test/unit/internal/formatting/test_serialize.py index 511048d80..8dbe9bd05 100644 --- a/test/unit/test_serialize.py +++ b/test/unit/internal/formatting/test_serialize.py @@ -21,7 +21,7 @@ from aws_encryption_sdk.internal.structures import EncryptedData from aws_encryption_sdk.structures import EncryptedDataKey, MasterKeyInfo -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_defaults.py b/test/unit/internal/test_defaults.py similarity index 100% rename from test/unit/test_defaults.py rename to test/unit/internal/test_defaults.py diff --git a/test/unit/test_internal_structures.py b/test/unit/internal/test_structures.py similarity index 97% rename from test/unit/test_internal_structures.py rename to test/unit/internal/test_structures.py index d57166982..04f4e737a 100644 --- a/test/unit/test_internal_structures.py +++ b/test/unit/internal/test_structures.py @@ -21,7 +21,7 @@ MessageNoFrameBody, ) -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ..unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/internal/utils/__init__.py b/test/unit/internal/utils/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/internal/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_util_str_ops.py b/test/unit/internal/utils/test_str_ops.py similarity index 100% rename from test/unit/test_util_str_ops.py rename to test/unit/internal/utils/test_str_ops.py diff --git a/test/unit/test_util_streams.py b/test/unit/internal/utils/test_streams.py similarity index 96% rename from test/unit/test_util_streams.py rename to test/unit/internal/utils/test_streams.py index ab7b05152..660e4623c 100644 --- a/test/unit/test_util_streams.py +++ b/test/unit/internal/utils/test_streams.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.internal.str_ops import to_bytes, to_str from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO, ROStream, TeeStream -from .unit_test_utils import ExactlyTwoReads, NothingButRead, SometimesIncompleteReaderIO +from ...unit_test_utils import ExactlyTwoReads, NothingButRead, SometimesIncompleteReaderIO pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_utils.py b/test/unit/internal/utils/test_utils.py similarity index 99% rename from test/unit/test_utils.py rename to test/unit/internal/utils/test_utils.py index b83e5de1d..c118ba375 100644 --- a/test/unit/test_utils.py +++ b/test/unit/internal/utils/test_utils.py @@ -24,8 +24,8 @@ from aws_encryption_sdk.keyrings.base import EncryptedDataKey from aws_encryption_sdk.structures import DataKey, MasterKeyInfo, RawDataKey -from .test_values import VALUES -from .unit_test_utils import assert_prepped_stream_identity +from ...unit_test_utils import assert_prepped_stream_identity +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/key_providers/__init__.py b/test/unit/key_providers/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/key_providers/base/__init__.py b/test/unit/key_providers/base/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/base/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_providers_base_master_key.py b/test/unit/key_providers/base/test_base_master_key.py similarity index 99% rename from test/unit/test_providers_base_master_key.py rename to test/unit/key_providers/base/test_base_master_key.py index 26a90ced8..4ee6c2661 100644 --- a/test/unit/test_providers_base_master_key.py +++ b/test/unit/key_providers/base/test_base_master_key.py @@ -20,7 +20,7 @@ from aws_encryption_sdk.key_providers.base import MasterKey, MasterKeyConfig, MasterKeyProvider from aws_encryption_sdk.structures import MasterKeyInfo -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_base_master_key_config.py b/test/unit/key_providers/base/test_base_master_key_config.py similarity index 96% rename from test/unit/test_providers_base_master_key_config.py rename to test/unit/key_providers/base/test_base_master_key_config.py index 8b6c8731a..3eb0ce406 100644 --- a/test/unit/test_providers_base_master_key_config.py +++ b/test/unit/key_providers/base/test_base_master_key_config.py @@ -15,7 +15,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_base_master_key_provider.py b/test/unit/key_providers/base/test_base_master_key_provider.py similarity index 99% rename from test/unit/test_providers_base_master_key_provider.py rename to test/unit/key_providers/base/test_base_master_key_provider.py index 44385ea17..778068dd5 100644 --- a/test/unit/test_providers_base_master_key_provider.py +++ b/test/unit/key_providers/base/test_base_master_key_provider.py @@ -23,7 +23,7 @@ ) from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_base_master_key_provider_config.py b/test/unit/key_providers/base/test_base_master_key_provider_config.py similarity index 100% rename from test/unit/test_providers_base_master_key_provider_config.py rename to test/unit/key_providers/base/test_base_master_key_provider_config.py diff --git a/test/unit/key_providers/kms/__init__.py b/test/unit/key_providers/kms/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/kms/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_providers_kms_master_key.py b/test/unit/key_providers/kms/test_kms_master_key.py similarity index 99% rename from test/unit/test_providers_kms_master_key.py rename to test/unit/key_providers/kms/test_kms_master_key.py index c0ab9a968..862888ad8 100644 --- a/test/unit/test_providers_kms_master_key.py +++ b/test/unit/key_providers/kms/test_kms_master_key.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyConfig from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_kms_master_key_config.py b/test/unit/key_providers/kms/test_kms_master_key_config.py similarity index 97% rename from test/unit/test_providers_kms_master_key_config.py rename to test/unit/key_providers/kms/test_kms_master_key_config.py index 1501c951f..224e43c7c 100644 --- a/test/unit/test_providers_kms_master_key_config.py +++ b/test/unit/key_providers/kms/test_kms_master_key_config.py @@ -17,7 +17,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyConfig from aws_encryption_sdk.key_providers.kms import _PROVIDER_ID, KMSMasterKeyConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_kms_master_key_provider.py b/test/unit/key_providers/kms/test_kms_master_key_provider.py similarity index 100% rename from test/unit/test_providers_kms_master_key_provider.py rename to test/unit/key_providers/kms/test_kms_master_key_provider.py diff --git a/test/unit/test_providers_kms_master_key_provider_config.py b/test/unit/key_providers/kms/test_kms_master_key_provider_config.py similarity index 97% rename from test/unit/test_providers_kms_master_key_provider_config.py rename to test/unit/key_providers/kms/test_kms_master_key_provider_config.py index affa74102..9b8f9fd74 100644 --- a/test/unit/test_providers_kms_master_key_provider_config.py +++ b/test/unit/key_providers/kms/test_kms_master_key_provider_config.py @@ -17,7 +17,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProviderConfig from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProviderConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/key_providers/raw/__init__.py b/test/unit/key_providers/raw/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/key_providers/raw/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_providers_raw_master_key.py b/test/unit/key_providers/raw/test_raw_master_key.py similarity index 99% rename from test/unit/test_providers_raw_master_key.py rename to test/unit/key_providers/raw/test_raw_master_key.py index 9abcd14c6..8b9ba658d 100644 --- a/test/unit/test_providers_raw_master_key.py +++ b/test/unit/key_providers/raw/test_raw_master_key.py @@ -20,7 +20,7 @@ from aws_encryption_sdk.key_providers.raw import RawMasterKey, RawMasterKeyConfig from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_raw_master_key_config.py b/test/unit/key_providers/raw/test_raw_master_key_config.py similarity index 96% rename from test/unit/test_providers_raw_master_key_config.py rename to test/unit/key_providers/raw/test_raw_master_key_config.py index d06feae87..bbdbc5bef 100644 --- a/test/unit/test_providers_raw_master_key_config.py +++ b/test/unit/key_providers/raw/test_raw_master_key_config.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs +from ...unit_test_utils import all_invalid_kwargs, all_valid_kwargs pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_providers_raw_master_key_provider.py b/test/unit/key_providers/raw/test_raw_master_key_provider.py similarity index 98% rename from test/unit/test_providers_raw_master_key_provider.py rename to test/unit/key_providers/raw/test_raw_master_key_provider.py index 5128b1e22..9205d3563 100644 --- a/test/unit/test_providers_raw_master_key_provider.py +++ b/test/unit/key_providers/raw/test_raw_master_key_provider.py @@ -18,7 +18,7 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider -from .test_values import VALUES +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/keyrings/raw/test_raw_rsa.py b/test/unit/keyrings/raw/test_raw_rsa.py index e460a4221..460b7280f 100644 --- a/test/unit/keyrings/raw/test_raw_rsa.py +++ b/test/unit/keyrings/raw/test_raw_rsa.py @@ -23,7 +23,6 @@ from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.keyrings.raw import RawRSAKeyring -from ...test_values import VALUES from ...unit_test_utils import ( _BACKEND, _DATA_KEY, @@ -38,6 +37,7 @@ get_encryption_materials_with_data_encryption_key, get_encryption_materials_without_data_encryption_key, ) +from ...vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/materials_managers/__init__.py b/test/unit/materials_managers/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/materials_managers/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_material_managers_base.py b/test/unit/materials_managers/test_base.py similarity index 100% rename from test/unit/test_material_managers_base.py rename to test/unit/materials_managers/test_base.py diff --git a/test/unit/test_material_managers_caching.py b/test/unit/materials_managers/test_caching.py similarity index 100% rename from test/unit/test_material_managers_caching.py rename to test/unit/materials_managers/test_caching.py diff --git a/test/unit/test_material_managers_default.py b/test/unit/materials_managers/test_default.py similarity index 100% rename from test/unit/test_material_managers_default.py rename to test/unit/materials_managers/test_default.py diff --git a/test/unit/test_material_managers.py b/test/unit/materials_managers/test_material_managers.py similarity index 100% rename from test/unit/test_material_managers.py rename to test/unit/materials_managers/test_material_managers.py diff --git a/test/unit/streaming_client/__init__.py b/test/unit/streaming_client/__init__.py new file mode 100644 index 000000000..ad0e71d6c --- /dev/null +++ b/test/unit/streaming_client/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 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. +"""Dummy stub to make linters work better.""" diff --git a/test/unit/test_streaming_client_configs.py b/test/unit/streaming_client/test_configs.py similarity index 98% rename from test/unit/test_streaming_client_configs.py rename to test/unit/streaming_client/test_configs.py index 98e5cb13c..a98a38957 100644 --- a/test/unit/test_streaming_client_configs.py +++ b/test/unit/streaming_client/test_configs.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager from aws_encryption_sdk.streaming_client import DecryptorConfig, EncryptorConfig, _ClientConfig -from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs, build_valid_kwargs_list +from ..unit_test_utils import all_invalid_kwargs, all_valid_kwargs, build_valid_kwargs_list pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_streaming_client_encryption_stream.py b/test/unit/streaming_client/test_encryption_stream.py similarity index 99% rename from test/unit/test_streaming_client_encryption_stream.py rename to test/unit/streaming_client/test_encryption_stream.py index e3a06347a..345e9939e 100644 --- a/test/unit/test_streaming_client_encryption_stream.py +++ b/test/unit/streaming_client/test_encryption_stream.py @@ -23,8 +23,8 @@ from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.streaming_client import _ClientConfig, _EncryptionStream -from .test_values import VALUES -from .unit_test_utils import assert_prepped_stream_identity +from ..unit_test_utils import assert_prepped_stream_identity +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_streaming_client_stream_decryptor.py b/test/unit/streaming_client/test_stream_decryptor.py similarity index 99% rename from test/unit/test_streaming_client_stream_decryptor.py rename to test/unit/streaming_client/test_stream_decryptor.py index 6a3ccb56d..06e7f0816 100644 --- a/test/unit/test_streaming_client_stream_decryptor.py +++ b/test/unit/streaming_client/test_stream_decryptor.py @@ -22,7 +22,7 @@ from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.streaming_client import StreamDecryptor -from .test_values import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_streaming_client_stream_encryptor.py b/test/unit/streaming_client/test_stream_encryptor.py similarity index 99% rename from test/unit/test_streaming_client_stream_encryptor.py rename to test/unit/streaming_client/test_stream_encryptor.py index 5cb2b8e37..42516444a 100644 --- a/test/unit/test_streaming_client_stream_encryptor.py +++ b/test/unit/streaming_client/test_stream_encryptor.py @@ -30,7 +30,7 @@ from aws_encryption_sdk.streaming_client import StreamEncryptor from aws_encryption_sdk.structures import MessageHeader -from .test_values import VALUES +from ..vectors import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] diff --git a/test/unit/test_aws_encryption_sdk.py b/test/unit/test_client.py similarity index 100% rename from test/unit/test_aws_encryption_sdk.py rename to test/unit/test_client.py diff --git a/test/unit/test_values.py b/test/unit/vectors.py similarity index 100% rename from test/unit/test_values.py rename to test/unit/vectors.py From 952bce44db630754de6a0dad451f72665f83b1c7 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Wed, 9 Oct 2019 13:07:14 -0700 Subject: [PATCH 24/64] Refactor MasterKeyProvider.master_keys_for_data_key (#200) * refactor MasterKeyProvider.decrypt_data_key and break out master key location to master_keys_for_data_key * simplify MasterKey.owns_data_key * autoformat * fix linting issues --- src/aws_encryption_sdk/key_providers/base.py | 95 ++++++++++++-------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/aws_encryption_sdk/key_providers/base.py b/src/aws_encryption_sdk/key_providers/base.py index 3112cba6d..9ee9b7d1c 100644 --- a/src/aws_encryption_sdk/key_providers/base.py +++ b/src/aws_encryption_sdk/key_providers/base.py @@ -26,8 +26,17 @@ MasterKeyProviderError, ) from aws_encryption_sdk.internal.str_ops import to_bytes +from aws_encryption_sdk.structures import DataKey # pylint: disable=unused-import +from aws_encryption_sdk.structures import EncryptedDataKey # pylint: disable=unused-import +from aws_encryption_sdk.structures import RawDataKey # pylint: disable=unused-import from aws_encryption_sdk.structures import MasterKeyInfo +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 + _LOGGER = logging.getLogger(__name__) @@ -212,6 +221,35 @@ def master_key_for_decrypt(self, key_info): self._decrypt_key_index[key_info] = decrypt_master_key return decrypt_master_key + def master_keys_for_data_key(self, data_key): + # type: (Union[DataKey, EncryptedDataKey, RawDataKey]) -> Iterable[MasterKey] + """Locates the correct master keys from children for the specified data key. + + :param data_key: Data key for which to locate owning master keys + :type data_key: :class:`EncryptedDataKey`, :class:`RawDataKey`, or :class:`DataKey` + :returns: Masters key that own data key + :rtype: iterator of :class:`MasterKey` + :raises UnknownIdentityError: if unable to locate the correct master key + """ + for member in [self] + self._members: + if member.provider_id != data_key.key_provider.provider_id: + continue + + _LOGGER.debug("attempting to locate master key from key provider: %s", member.provider_id) + + if isinstance(member, MasterKey): + if member.owns_data_key(data_key): + _LOGGER.debug("using existing master key") + yield member + + if self.vend_masterkey_on_decrypt: + try: + _LOGGER.debug("attempting to add master key: %s", data_key.key_provider.key_info) + yield member.master_key_for_decrypt(data_key.key_provider.key_info) + except InvalidKeyIdError: + _LOGGER.debug("master key %s not available in provider", data_key.key_provider.key_info) + continue + def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): """Iterates through all currently added Master Keys and Master Key Providers to attempt to decrypt data key. @@ -225,42 +263,25 @@ def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): :rtype: aws_encryption_sdk.structures.DataKey :raises DecryptKeyError: if unable to decrypt encrypted data key """ - data_key = None - master_key = None _LOGGER.debug("starting decrypt data key attempt") - for member in [self] + self._members: - if member.provider_id == encrypted_data_key.key_provider.provider_id: - _LOGGER.debug("attempting to locate master key from key provider: %s", member.provider_id) - if isinstance(member, MasterKey): - _LOGGER.debug("using existing master key") - master_key = member - elif self.vend_masterkey_on_decrypt: - try: - _LOGGER.debug("attempting to add master key: %s", encrypted_data_key.key_provider.key_info) - master_key = member.master_key_for_decrypt(encrypted_data_key.key_provider.key_info) - except InvalidKeyIdError: - _LOGGER.debug( - "master key %s not available in provider", encrypted_data_key.key_provider.key_info - ) - continue - else: - continue - try: - _LOGGER.debug( - "attempting to decrypt data key with provider %s", encrypted_data_key.key_provider.key_info - ) - data_key = master_key.decrypt_data_key(encrypted_data_key, algorithm, encryption_context) - except (IncorrectMasterKeyError, DecryptKeyError) as error: - _LOGGER.debug( - "%s raised when attempting to decrypt data key with master key %s", - repr(error), - master_key.key_provider, - ) - continue - break # If this point is reached without throwing any errors, the data key has been decrypted - if not data_key: - raise DecryptKeyError("Unable to decrypt data key") - return data_key + for master_key in self.master_keys_for_data_key(encrypted_data_key): + try: + _LOGGER.debug( + "attempting to decrypt data key with provider %s", encrypted_data_key.key_provider.key_info + ) + return master_key.decrypt_data_key(encrypted_data_key, algorithm, encryption_context) + + # MasterKeyProvider.decrypt_data_key throws DecryptKeyError + # but MasterKey.decrypt_data_key throws IncorrectMasterKeyError + except (IncorrectMasterKeyError, DecryptKeyError) as error: + _LOGGER.debug( + "%s raised when attempting to decrypt data key with master key %s", + repr(error), + master_key.key_provider, + ) + continue + + raise DecryptKeyError("Unable to decrypt data key") def decrypt_data_key_from_list(self, encrypted_data_keys, algorithm, encryption_context): """Receives a list of encrypted data keys and returns the first one which this provider is able to decrypt. @@ -356,9 +377,7 @@ def owns_data_key(self, data_key): :returns: Boolean statement of ownership :rtype: bool """ - if data_key.key_provider == self.key_provider: - return True - return False + return data_key.key_provider == self.key_provider def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): """Returns self and a list containing self, to match the format of output for a Master Key Provider. From 9ec35ef6b12c9db0cb8147fb638dd02168d0359f Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Tue, 3 Mar 2020 12:29:59 -0800 Subject: [PATCH 25/64] feat: enable use of keyrings (#216) * chore: add __all__ values for keyring modules * chore: add versionadded tags for keyrings * feat: plump keyrings into stream handlers and default CMM * chore: incorporate keyrings into tests * add raw AES keyring-MKP compatibility tests * add raw RSA MKP-keyring compatibility tests * fix: fix integ test to run when default region is set * chore: add public-private keyring-MKP compat tests * fix: fix typo * add DefaultCryptographicMaterialsProvider tests for keyrings that return incomplete or broken materials * feat: enable caching CMM to accept either MKP or keyring * fix: rename test keyring to avoid name collision * chore: caching CMM has too many instance attributes and that's ok * docs: add versionadded flags to docstrings for keyring values * docs: update docs on encrypt/decrypt helper functions to match underlying docs * chore: update copyright notices on modified files * docs: render keyring docs * docs: clean up keyring method docs * fix: fix linting issues * fix: autoformat * docs: fix typo * fix: fix docs and error message inconsistency * fix: re-order checks to avoid misleading error messages if materials are invalid * chore: test broken paths in default CMM with more algorithm suites * docs: make docs correctly link to pyca/cryptography docs --- doc/conf.py | 5 +- doc/index.rst | 3 + src/aws_encryption_sdk/__init__.py | 48 +-- src/aws_encryption_sdk/exceptions.py | 21 +- src/aws_encryption_sdk/keyrings/base.py | 29 +- src/aws_encryption_sdk/keyrings/multi.py | 38 +-- src/aws_encryption_sdk/keyrings/raw.py | 107 +++---- .../materials_managers/__init__.py | 2 +- .../materials_managers/caching.py | 94 +++--- .../materials_managers/default.py | 168 ++++++++-- src/aws_encryption_sdk/streaming_client.py | 130 ++++---- test/functional/test_client.py | 217 ++++++++++--- test/integration/test_client.py | 21 +- test/unit/materials_managers/test_caching.py | 50 ++- test/unit/materials_managers/test_default.py | 138 ++++++-- test/unit/unit_test_utils.py | 296 ++++++++++++++++-- 16 files changed, 956 insertions(+), 411 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 2164a52a6..42a7771b4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -67,7 +67,10 @@ def get_version(): htmlhelp_basename = "%sdoc" % project # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"http://docs.python.org/": None} +intersphinx_mapping = { + "python": ("http://docs.python.org/3/", None), + "cryptography": ("https://cryptography.io/en/latest/", None), +} # autosummary autosummary_generate = True diff --git a/doc/index.rst b/doc/index.rst index 10957074e..b4a0476f9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,6 +14,9 @@ Modules aws_encryption_sdk.caches.base aws_encryption_sdk.caches.local aws_encryption_sdk.caches.null + aws_encryption_sdk.keyrings.base + aws_encryption_sdk.keyrings.multi + aws_encryption_sdk.keyrings.raw aws_encryption_sdk.key_providers.base aws_encryption_sdk.key_providers.kms aws_encryption_sdk.key_providers.raw diff --git a/src/aws_encryption_sdk/__init__.py b/src/aws_encryption_sdk/__init__.py index 3f6d86e2e..8cadd51b8 100644 --- a/src/aws_encryption_sdk/__init__.py +++ b/src/aws_encryption_sdk/__init__.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """High level AWS Encryption SDK client functions.""" # Below are imported for ease of use by implementors from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache # noqa @@ -33,6 +23,9 @@ def encrypt(**kwargs): When using this function, the entire ciphertext message is encrypted into memory before returning any data. If streaming is desired, see :class:`aws_encryption_sdk.stream`. + .. versionadded:: 1.5.0 + The *keyring* parameter. + .. code:: python >>> import aws_encryption_sdk @@ -49,12 +42,14 @@ def encrypt(**kwargs): :type config: aws_encryption_sdk.streaming_client.EncryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -87,6 +82,9 @@ def decrypt(**kwargs): When using this function, the entire ciphertext message is decrypted into memory before returning any data. If streaming is desired, see :class:`aws_encryption_sdk.stream`. + .. versionadded:: 1.5.0 + The *keyring* parameter. + .. code:: python >>> import aws_encryption_sdk @@ -103,12 +101,14 @@ def decrypt(**kwargs): :type config: aws_encryption_sdk.streaming_client.DecryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for decryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: diff --git a/src/aws_encryption_sdk/exceptions.py b/src/aws_encryption_sdk/exceptions.py index cd60ab6bd..3c58dcea1 100644 --- a/src/aws_encryption_sdk/exceptions.py +++ b/src/aws_encryption_sdk/exceptions.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Contains exception classes for AWS Encryption SDK.""" @@ -87,6 +77,13 @@ class SignatureKeyError(AWSEncryptionSDKClientError): """ +class InvalidCryptographicMaterialsError(AWSEncryptionSDKClientError): + """Exception class for errors encountered when attempting to validate cryptographic materials. + + .. 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/keyrings/base.py b/src/aws_encryption_sdk/keyrings/base.py index 32f810e47..c854faf27 100644 --- a/src/aws_encryption_sdk/keyrings/base.py +++ b/src/aws_encryption_sdk/keyrings/base.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Base class interface for Keyrings.""" from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import DecryptionMaterials, @@ -23,6 +13,8 @@ # We only actually need these imports when running the mypy checks pass +__all__ = ("Keyring",) + class Keyring(object): """Parent interface for Keyring classes. @@ -34,10 +26,9 @@ 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 + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. :returns: Optionally modified encryption materials. - :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :rtype: EncryptionMaterials :raises NotImplementedError: if method is not implemented """ raise NotImplementedError("Keyring does not implement on_encrypt function") @@ -46,12 +37,10 @@ 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` + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. :returns: Optionally modified decryption materials. - :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :rtype: DecryptionMaterials :raises NotImplementedError: if method is not implemented """ raise NotImplementedError("Keyring does not implement on_decrypt function") diff --git a/src/aws_encryption_sdk/keyrings/multi.py b/src/aws_encryption_sdk/keyrings/multi.py index 4274c1c4d..d42ea365d 100644 --- a/src/aws_encryption_sdk/keyrings/multi.py +++ b/src/aws_encryption_sdk/keyrings/multi.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Resources required for Multi Keyrings.""" import itertools @@ -31,21 +21,24 @@ # We only actually need these imports when running the mypy checks pass +__all__ = ("MultiKeyring",) + @attr.s class MultiKeyring(Keyring): """Public class for Multi Keyring. - :param generator: Generator keyring used to generate data encryption key (optional) - :type generator: Keyring - :param list children: List of keyrings used to encrypt the data encryption key (optional) + .. versionadded:: 1.5.0 + + :param Keyring generator: Generator keyring used to generate data encryption key (optional) + :param List[Keyring] children: List of keyrings used to encrypt the data encryption key (optional) :raises EncryptKeyError: if encryption of data key fails for any reason """ + generator = attr.ib(default=None, validator=optional(instance_of(Keyring))) children = attr.ib( default=attr.Factory(tuple), validator=optional(deep_iterable(member_validator=instance_of(Keyring))) ) - generator = attr.ib(default=None, validator=optional(instance_of(Keyring))) def __attrs_post_init__(self): # type: () -> None @@ -62,10 +55,9 @@ def on_encrypt(self, encryption_materials): """Generate a data key using generator keyring and encrypt it using any available wrapping key in any child keyring. - :param encryption_materials: Encryption materials for keyring to modify. - :type encryption_materials: aws_encryption_sdk.materials_managers.EncryptionMaterials + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. :returns: Optionally modified encryption materials. - :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :rtype: EncryptionMaterials :raises EncryptKeyError: if unable to encrypt data key. """ # Check if generator keyring is not provided and data key is not generated @@ -94,12 +86,10 @@ 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 keyring to modify. - :type decryption_materials: aws_encryption_sdk.materials_managers.DecryptionMaterials - :param encrypted_data_keys: List of encrypted data keys. - :type: List of `aws_encryption_sdk.structures.EncryptedDataKey` + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. :returns: Optionally modified decryption materials. - :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :rtype: DecryptionMaterials """ # Call on_decrypt on all keyrings till decryption is successful for keyring in self._decryption_keyrings: diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index dea924eae..1f4eebe14 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Resources required for Raw Keyrings.""" import logging import os @@ -19,7 +9,7 @@ from attr.validators import instance_of, optional from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey from aws_encryption_sdk.exceptions import GenerateKeyError from aws_encryption_sdk.identifiers import EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm @@ -28,17 +18,8 @@ from aws_encryption_sdk.internal.formatting.serialize import serialize_raw_master_key_prefix, serialize_wrapped_key from aws_encryption_sdk.key_providers.raw import RawMasterKey from aws_encryption_sdk.keyrings.base import Keyring -from aws_encryption_sdk.structures import ( # pylint: disable=unused-import - EncryptedDataKey, - KeyringTrace, - MasterKeyInfo, - RawDataKey, -) - -from aws_encryption_sdk.materials_managers import ( # only used for mypy; pylint: disable=unused-import - DecryptionMaterials, - EncryptionMaterials, -) +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Iterable # noqa pylint: disable=unused-import @@ -46,6 +27,7 @@ # We only actually need these imports when running the mypy checks pass +__all__ = ("RawAESKeyring", "RawRSAKeyring") _LOGGER = logging.getLogger(__name__) @@ -56,10 +38,8 @@ def _generate_data_key( # type: (...) -> bytes """Generates plaintext data key for the keyring. - :param encryption_materials: Encryption materials for the keyring to modify. - :type encryption_materials: aws_encryption_sdk.materials_managers.EncryptionMaterials - :param key_provider: Information about the key in the keyring. - :type key_provider: MasterKeyInfo + :param EncryptionMaterials encryption_materials: Encryption materials for the keyring to modify. + :param MasterKeyInfo key_provider: Information about the key in the keyring. :return bytes: Plaintext data key """ # Check if encryption materials contain data encryption key @@ -91,11 +71,12 @@ class RawAESKeyring(Keyring): """Generate an instance of Raw AES Keyring which encrypts using AES-GCM algorithm using wrapping key provided as a byte array + .. versionadded:: 1.5.0 + :param str key_namespace: String defining the keyring. :param bytes key_name: Key ID :param bytes wrapping_key: Encryption key with which to wrap plaintext data key. - :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key. - :type wrapping_algorithm: WrappingAlgorithm + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key. .. note:: Only one wrapping key can be specified in a Raw AES Keyring @@ -128,8 +109,7 @@ def _get_key_info_prefix(key_namespace, key_name, wrapping_key): :param str key_namespace: String defining the keyring. :param bytes key_name: Key ID - :param wrapping_key: Encryption key with which to wrap plaintext data key. - :type wrapping_key: WrappingKey + :param WrappingKey wrapping_key: Encryption key with which to wrap plaintext data key. :return: Serialized key_info prefix :rtype: bytes """ @@ -142,10 +122,9 @@ 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 + :param EncryptionMaterials encryption_materials: Encryption materials for the keyring to modify :returns: Optionally modified encryption materials - :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :rtype: EncryptionMaterials """ if encryption_materials.data_encryption_key is None: _generate_data_key(encryption_materials=encryption_materials, key_provider=self._key_provider) @@ -182,12 +161,10 @@ 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: List of `aws_encryption_sdk.structures.EncryptedDataKey` + :param DecryptionMaterials decryption_materials: Decryption materials for the keyring to modify + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys :returns: Optionally modified decryption materials - :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :rtype: DecryptionMaterials """ if decryption_materials.data_encryption_key is not None: return decryption_materials @@ -240,26 +217,27 @@ class RawRSAKeyring(Keyring): """Generate an instance of Raw RSA Keyring which performs asymmetric encryption and decryption using public and private keys provided + .. versionadded:: 1.5.0 + :param str key_namespace: String defining the keyring ID :param bytes key_name: Key ID - :param private_wrapping_key: Private encryption key with which to wrap plaintext data key (optional) - :type private_wrapping_key: RSAPrivateKey - :param public_wrapping_key: Public encryption key with which to wrap plaintext data key (optional) - :type public_wrapping_key: RSAPublicKey - :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key - :type wrapping_algorithm: WrappingAlgorithm - :param key_provider: Complete information about the key in the keyring - :type key_provider: MasterKeyInfo + :param cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey private_wrapping_key: + Private encryption key with which to wrap plaintext data key (optional) + :param cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey public_wrapping_key: + Public encryption key with which to wrap plaintext data key (optional) + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key + :param MasterKeyInfo key_provider: Complete information about the key in the keyring .. note:: - At least one of public wrapping key or private wrapping key must be provided. + + At least one of public wrapping key or private wrapping key must be provided. """ key_namespace = attr.ib(validator=instance_of(six.string_types)) key_name = attr.ib(validator=instance_of(six.binary_type)) _wrapping_algorithm = attr.ib(repr=False, validator=instance_of(WrappingAlgorithm)) - _private_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(rsa.RSAPrivateKey))) - _public_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(rsa.RSAPublicKey))) + _private_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(RSAPrivateKey))) + _public_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(RSAPublicKey))) def __attrs_post_init__(self): # type: () -> None @@ -287,12 +265,11 @@ def from_pem_encoding( :param str key_namespace: String defining the keyring ID :param bytes key_name: Key ID - :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key - :type wrapping_algorithm: WrappingAlgorithm + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key :param bytes public_encoded_key: PEM encoded public key (optional) :param bytes private_encoded_key: PEM encoded private key (optional) :param bytes password: Password to load private key (optional) - :return: Calls RawRSAKeyring class with required parameters + :return: :class:`RawRSAKeyring` constructed using required parameters """ loaded_private_wrapping_key = loaded_public_wrapping_key = None if private_encoded_key is not None: @@ -326,12 +303,11 @@ def from_der_encoding( :param str key_namespace: String defining the keyring ID :param bytes key_name: Key ID - :param wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key - :type wrapping_algorithm: WrappingAlgorithm + :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key :param bytes public_encoded_key: DER encoded public key (optional) :param bytes private_encoded_key: DER encoded private key (optional) - :param password: Password to load private key (optional) - :return: Calls RawRSAKeyring class with required parameters + :param bytes password: Password to load private key (optional) + :return: :class:`RawRSAKeyring` constructed using required parameters """ loaded_private_wrapping_key = loaded_public_wrapping_key = None if private_encoded_key is not None: @@ -353,12 +329,12 @@ def from_der_encoding( def on_encrypt(self, encryption_materials): # type: (EncryptionMaterials) -> EncryptionMaterials - """Generate a data key if not present and encrypt it using any available wrapping key. + """Generate a data key using generator keyring + and encrypt it using any available wrapping key in any child keyring. - :param encryption_materials: Encryption materials for the keyring to modify. - :type encryption_materials: aws_encryption_sdk.materials_managers.EncryptionMaterials + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. :returns: Optionally modified encryption materials. - :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :rtype: EncryptionMaterials """ if encryption_materials.data_encryption_key is None: _generate_data_key(encryption_materials=encryption_materials, key_provider=self._key_provider) @@ -403,12 +379,11 @@ 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 DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. :param encrypted_data_keys: List of encrypted data keys. - :type: List of `aws_encryption_sdk.structures.EncryptedDataKey` + :type: List[EncryptedDataKey] :returns: Optionally modified decryption materials. - :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :rtype: DecryptionMaterials """ if self._private_wrapping_key is None: return decryption_materials diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 0a6dcd2f0..e6e86c5cd 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -248,7 +248,7 @@ def encrypted_data_keys(self): @property def is_complete(self): # type: () -> bool - """Determine whether these materials are sufficiently complete for use as decryption materials. + """Determine whether these materials are sufficiently complete for use as encryption materials. :rtype: bool """ diff --git a/src/aws_encryption_sdk/materials_managers/caching.py b/src/aws_encryption_sdk/materials_managers/caching.py index 992a39a7a..5a8e9e9bd 100644 --- a/src/aws_encryption_sdk/materials_managers/caching.py +++ b/src/aws_encryption_sdk/materials_managers/caching.py @@ -1,32 +1,25 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Caching crypto material manager.""" import logging import uuid import attr import six +from attr.validators import instance_of, optional -from ..caches import ( +from aws_encryption_sdk.caches import ( CryptoMaterialsCacheEntryHints, build_decryption_materials_cache_key, build_encryption_materials_cache_key, ) -from ..caches.base import CryptoMaterialsCache -from ..exceptions import CacheKeyError -from ..internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY -from ..internal.str_ops import to_bytes -from ..key_providers.base import MasterKeyProvider +from aws_encryption_sdk.caches.base import CryptoMaterialsCache +from aws_encryption_sdk.exceptions import CacheKeyError +from aws_encryption_sdk.internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY +from aws_encryption_sdk.internal.str_ops import to_bytes +from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring + from . import EncryptionMaterialsRequest from .base import CryptoMaterialsManager from .default import DefaultCryptoMaterialsManager @@ -36,10 +29,14 @@ @attr.s(hash=False) class CachingCryptoMaterialsManager(CryptoMaterialsManager): + # pylint: disable=too-many-instance-attributes """Crypto material manager which caches results from an underlying material manager. .. versionadded:: 1.3.0 + .. versionadded:: 1.5.0 + The *keyring* parameter. + >>> import aws_encryption_sdk >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', @@ -59,17 +56,16 @@ class CachingCryptoMaterialsManager(CryptoMaterialsManager): value. If no partition name is provided, a random UUID will be used. .. note:: - Either `backing_materials_manager` or `master_key_provider` must be provided. - `backing_materials_manager` will always be used if present. - - :param cache: Crypto cache to use with material manager - :type cache: aws_encryption_sdk.caches.base.CryptoMaterialsCache - :param backing_materials_manager: Crypto material manager to back this caching material manager - (either `backing_materials_manager` or `master_key_provider` required) - :type backing_materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param master_key_provider: Master key provider to use (either `backing_materials_manager` or - `master_key_provider` required) - :type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + Exactly one of ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` must be provided. + + :param CryptoMaterialsCache cache: Crypto cache to use with material manager + :param CryptoMaterialsManager backing_materials_manager: + Crypto material manager to back this caching material manager + (either ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` required) + :param MasterKeyProvider master_key_provider: Master key provider to use + (either ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` required) + :param Keyring keyring: Keyring to use + (either ``backing_materials_manager``, ``keyring``, or ``master_key_provider`` required) :param float max_age: Maximum time in seconds that a cache entry may be kept in the cache :param int max_messages_encrypted: Maximum number of messages that may be encrypted under a cache entry (optional) @@ -78,21 +74,14 @@ class CachingCryptoMaterialsManager(CryptoMaterialsManager): :param bytes partition_name: Partition name to use for this instance (optional) """ - cache = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsCache)) - max_age = attr.ib(validator=attr.validators.instance_of(float)) - max_messages_encrypted = attr.ib( - default=MAX_MESSAGES_PER_KEY, validator=attr.validators.instance_of(six.integer_types) - ) - max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=attr.validators.instance_of(six.integer_types)) - partition_name = attr.ib( - default=None, converter=to_bytes, validator=attr.validators.optional(attr.validators.instance_of(bytes)) - ) - master_key_provider = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(MasterKeyProvider)) - ) - backing_materials_manager = attr.ib( - default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager)) - ) + cache = attr.ib(validator=instance_of(CryptoMaterialsCache)) + max_age = attr.ib(validator=instance_of(float)) + max_messages_encrypted = attr.ib(default=MAX_MESSAGES_PER_KEY, validator=instance_of(six.integer_types)) + max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=instance_of(six.integer_types)) + partition_name = attr.ib(default=None, converter=to_bytes, validator=optional(instance_of(bytes))) + master_key_provider = attr.ib(default=None, validator=optional(instance_of(MasterKeyProvider))) + backing_materials_manager = attr.ib(default=None, validator=optional(instance_of(CryptoMaterialsManager))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) def __attrs_post_init__(self): """Applies post-processing which cannot be handled by attrs.""" @@ -111,10 +100,21 @@ def __attrs_post_init__(self): if self.max_age <= 0.0: raise ValueError("max_age cannot be less than or equal to 0") + options_provided = [ + option is not None for option in (self.backing_materials_manager, self.keyring, self.master_key_provider) + ] + provided_count = len([is_set for is_set in options_provided if is_set]) + + if provided_count != 1: + raise TypeError("Exactly one of 'backing_materials_manager', 'keyring', or 'key_provider' must be provided") + if self.backing_materials_manager is None: - if self.master_key_provider is None: - raise TypeError("Either backing_materials_manager or master_key_provider must be defined") - self.backing_materials_manager = DefaultCryptoMaterialsManager(self.master_key_provider) + if self.master_key_provider is not None: + self.backing_materials_manager = DefaultCryptoMaterialsManager( + master_key_provider=self.master_key_provider + ) + else: + self.backing_materials_manager = DefaultCryptoMaterialsManager(keyring=self.keyring) if self.partition_name is None: self.partition_name = to_bytes(str(uuid.uuid4())) diff --git a/src/aws_encryption_sdk/materials_managers/default.py b/src/aws_encryption_sdk/materials_managers/default.py index 6d10465a9..336e5243f 100644 --- a/src/aws_encryption_sdk/materials_managers/default.py +++ b/src/aws_encryption_sdk/materials_managers/default.py @@ -1,29 +1,21 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Default crypto material manager class.""" import logging import attr +from attr.validators import instance_of, optional -from ..exceptions import MasterKeyProviderError, SerializationError -from ..internal.crypto.authentication import Signer, Verifier -from ..internal.crypto.elliptic_curve import generate_ecc_signing_key -from ..internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY -from ..internal.str_ops import to_str -from ..internal.utils import prepare_data_keys -from ..key_providers.base import MasterKeyProvider -from . import DecryptionMaterials, EncryptionMaterials -from .base import CryptoMaterialsManager +from aws_encryption_sdk.exceptions import InvalidCryptographicMaterialsError, MasterKeyProviderError, SerializationError +from aws_encryption_sdk.internal.crypto.authentication import Signer, Verifier +from aws_encryption_sdk.internal.crypto.elliptic_curve import generate_ecc_signing_key +from aws_encryption_sdk.internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY +from aws_encryption_sdk.internal.str_ops import to_str +from aws_encryption_sdk.internal.utils import prepare_data_keys +from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager _LOGGER = logging.getLogger(__name__) @@ -34,12 +26,26 @@ class DefaultCryptoMaterialsManager(CryptoMaterialsManager): .. versionadded:: 1.3.0 - :param master_key_provider: Master key provider to use - :type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + .. versionadded:: 1.5.0 + The *keyring* parameter. + + :param MasterKeyProvider master_key_provider: Master key provider to use + (either ``keyring`` or ``master_key_provider`` is required) + :param Keyring keyring: Keyring to use + (either ``keyring`` or ``master_key_provider`` is required) """ algorithm = ALGORITHM - master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) + master_key_provider = attr.ib(default=None, validator=optional(instance_of(MasterKeyProvider))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) + + def __attrs_post_init__(self): + """Apply input requirements.""" + both = self.master_key_provider is not None and self.keyring is not None + neither = self.master_key_provider is None and self.keyring is None + + if both or neither: + raise TypeError("Exactly one of 'keyring' or 'master_key_provider' must be supplied.") def _generate_signing_key_and_update_encryption_context(self, algorithm, encryption_context): """Generates a signing key based on the provided algorithm. @@ -58,7 +64,7 @@ def _generate_signing_key_and_update_encryption_context(self, algorithm, encrypt encryption_context[ENCODED_SIGNER_KEY] = to_str(signer.encoded_public_key()) return signer.key_bytes() - def get_encryption_materials(self, request): + def _get_encryption_materials_using_master_key_provider(self, request): """Creates encryption materials using underlying master key provider. :param request: encryption materials request @@ -101,6 +107,64 @@ def get_encryption_materials(self, request): signing_key=signing_key, ) + def _get_encryption_materials_using_keyring(self, request): + """Creates encryption materials using underlying keyring. + + :param request: encryption materials request + :type request: aws_encryption_sdk.materials_managers.EncryptionMaterialsRequest + :returns: encryption materials + :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :raises InvalidCryptographicMaterialsError: if keyring cannot complete encryption materials + :raises InvalidCryptographicMaterialsError: + if encryption materials received from keyring do not match request + """ + algorithm = request.algorithm if request.algorithm is not None else self.algorithm + encryption_context = request.encryption_context.copy() + + signing_key = self._generate_signing_key_and_update_encryption_context(algorithm, encryption_context) + + expected_encryption_context = encryption_context.copy() + + encryption_materials = EncryptionMaterials( + algorithm=algorithm, encryption_context=encryption_context, signing_key=signing_key, + ) + + final_materials = self.keyring.on_encrypt(encryption_materials=encryption_materials) + + materials_are_valid = ( + final_materials.algorithm is algorithm, + final_materials.encryption_context == expected_encryption_context, + final_materials.signing_key is signing_key, + ) + if not all(materials_are_valid): + raise InvalidCryptographicMaterialsError("Encryption materials do not match request!") + + if not final_materials.is_complete: + raise InvalidCryptographicMaterialsError("Encryption materials are incomplete!") + + _LOGGER.debug("Post-encrypt encryption context: %s", encryption_context) + + return final_materials + + def get_encryption_materials(self, request): + """Creates encryption materials using underlying master key provider. + + :param request: encryption materials request + :type request: aws_encryption_sdk.materials_managers.EncryptionMaterialsRequest + :returns: encryption materials + :rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials + :raises InvalidCryptographicMaterialsError: if keyring cannot complete encryption materials + :raises InvalidCryptographicMaterialsError: + if encryption materials received from keyring do not match request + :raises MasterKeyProviderError: if no master keys are available from the underlying master key provider + :raises MasterKeyProviderError: if the primary master key provided by the underlying master key provider + is not included in the full set of master keys provided by that provider + """ + if self.master_key_provider is not None: + return self._get_encryption_materials_using_master_key_provider(request) + + return self._get_encryption_materials_using_keyring(request) + def _load_verification_key_from_encryption_context(self, algorithm, encryption_context): """Loads the verification key from the encryption context if used by algorithm suite. @@ -124,7 +188,7 @@ def _load_verification_key_from_encryption_context(self, algorithm, encryption_c verifier = Verifier.from_encoded_point(algorithm=algorithm, encoded_point=encoded_verification_key) return verifier.key_bytes() - def decrypt_materials(self, request): + def _decrypt_materials_using_master_key_provider(self, request): """Obtains a plaintext data key from one or more encrypted data keys using underlying master key provider. @@ -143,3 +207,55 @@ def decrypt_materials(self, request): ) return DecryptionMaterials(data_key=data_key, verification_key=verification_key) + + def _decrypt_materials_using_keyring(self, request): + """Obtains a plaintext data key from one or more encrypted data keys + using underlying keyring. + + :param request: decrypt materials request + :type request: aws_encryption_sdk.materials_managers.DecryptionMaterialsRequest + :returns: decryption materials + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + :raises InvalidCryptographicMaterialsError: if keyring cannot complete decryption materials + :raises InvalidCryptographicMaterialsError: + if decryption materials received from keyring do not match request + """ + verification_key = self._load_verification_key_from_encryption_context( + algorithm=request.algorithm, encryption_context=request.encryption_context + ) + decryption_materials = DecryptionMaterials( + algorithm=request.algorithm, + encryption_context=request.encryption_context.copy(), + verification_key=verification_key, + ) + + final_materials = self.keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=request.encrypted_data_keys + ) + + materials_are_valid = ( + final_materials.algorithm is request.algorithm, + final_materials.encryption_context == request.encryption_context, + final_materials.verification_key is verification_key, + ) + if not all(materials_are_valid): + raise InvalidCryptographicMaterialsError("Decryption materials do not match request!") + + if not final_materials.is_complete: + raise InvalidCryptographicMaterialsError("Decryption materials are incomplete!") + + return final_materials + + def decrypt_materials(self, request): + """Obtains a plaintext data key from one or more encrypted data keys + using underlying master key provider. + + :param request: decrypt materials request + :type request: aws_encryption_sdk.materials_managers.DecryptionMaterialsRequest + :returns: decryption materials + :rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials + """ + if self.master_key_provider is not None: + return self._decrypt_materials_using_master_key_provider(request) + + return self._decrypt_materials_using_keyring(request) diff --git a/src/aws_encryption_sdk/streaming_client.py b/src/aws_encryption_sdk/streaming_client.py index 504f68977..d22fb7c70 100644 --- a/src/aws_encryption_sdk/streaming_client.py +++ b/src/aws_encryption_sdk/streaming_client.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """High level AWS Encryption SDK client for streaming objects.""" from __future__ import division @@ -20,6 +10,7 @@ import attr import six +from attr.validators import instance_of, optional import aws_encryption_sdk.internal.utils from aws_encryption_sdk.exceptions import ( @@ -54,6 +45,7 @@ serialize_non_framed_open, ) from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager @@ -67,14 +59,19 @@ class _ClientConfig(object): """Parent configuration object for StreamEncryptor and StreamDecryptor objects. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -83,28 +80,27 @@ class _ClientConfig(object): """ source = attr.ib(hash=True, converter=aws_encryption_sdk.internal.utils.prep_stream_data) - materials_manager = attr.ib( - hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager)) - ) - key_provider = attr.ib( - hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(MasterKeyProvider)) - ) - source_length = attr.ib( - hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(six.integer_types)) - ) + materials_manager = attr.ib(hash=True, default=None, validator=optional(instance_of(CryptoMaterialsManager))) + keyring = attr.ib(default=None, validator=optional(instance_of(Keyring))) + key_provider = attr.ib(hash=True, default=None, validator=optional(instance_of(MasterKeyProvider))) + source_length = attr.ib(hash=True, default=None, validator=optional(instance_of(six.integer_types))) line_length = attr.ib( - hash=True, default=LINE_LENGTH, validator=attr.validators.instance_of(six.integer_types) + hash=True, default=LINE_LENGTH, validator=instance_of(six.integer_types) ) # DEPRECATED: Value is no longer configurable here. Parameter left here to avoid breaking consumers. def __attrs_post_init__(self): """Normalize inputs to crypto material manager.""" - both_cmm_and_mkp_defined = self.materials_manager is not None and self.key_provider is not None - neither_cmm_nor_mkp_defined = self.materials_manager is None and self.key_provider is None + options_provided = [option is not None for option in (self.materials_manager, self.keyring, self.key_provider)] + provided_count = len([is_set for is_set in options_provided if is_set]) + + if provided_count != 1: + raise TypeError("Exactly one of 'materials_manager', 'keyring', or 'key_provider' must be provided") - if both_cmm_and_mkp_defined or neither_cmm_nor_mkp_defined: - raise TypeError("Exactly one of materials_manager or key_provider must be provided") if self.materials_manager is None: - self.materials_manager = DefaultCryptoMaterialsManager(master_key_provider=self.key_provider) + if self.key_provider is not None: + self.materials_manager = DefaultCryptoMaterialsManager(master_key_provider=self.key_provider) + else: + self.materials_manager = DefaultCryptoMaterialsManager(keyring=self.keyring) class _EncryptionStream(io.IOBase): @@ -311,14 +307,19 @@ def next(self): class EncryptorConfig(_ClientConfig): """Configuration object for StreamEncryptor class. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -360,16 +361,21 @@ class StreamEncryptor(_EncryptionStream): # pylint: disable=too-many-instance-a .. note:: If config is provided, all other parameters are ignored. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.EncryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for encryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -667,14 +673,19 @@ def close(self): class DecryptorConfig(_ClientConfig): """Configuration object for StreamDecryptor class. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for decryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: @@ -701,16 +712,21 @@ class StreamDecryptor(_EncryptionStream): # pylint: disable=too-many-instance-a .. note:: If config is provided, all other parameters are ignored. + .. versionadded:: 1.5.0 + The *keyring* parameter. + :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.DecryptorConfig :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file - :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials - (either `materials_manager` or `key_provider` required) - :type materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager - :param key_provider: `MasterKeyProvider` from which to obtain data keys for decryption - (either `materials_manager` or `key_provider` required) - :type key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider + :param CryptoMaterialsManager materials_manager: + Cryptographic materials manager to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param Keyring keyring: Keyring to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) + :param MasterKeyProvider key_provider: + Master key provider to use for encryption + (either ``materials_manager``, ``keyring``, ``key_provider`` required) :param int source_length: Length of source data (optional) .. note:: diff --git a/test/functional/test_client.py b/test/functional/test_client.py index fb19e868a..3888479d4 100644 --- a/test/functional/test_client.py +++ b/test/functional/test_client.py @@ -1,19 +1,10 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Functional test suite for aws_encryption_sdk.kms_thick_client""" from __future__ import division import io +import itertools import logging import attr @@ -34,10 +25,18 @@ from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.defaults import LINE_LENGTH from aws_encryption_sdk.internal.formatting.encryption_context import serialize_encryption_context -from aws_encryption_sdk.key_providers.base import MasterKeyProviderConfig +from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest +from ..unit.unit_test_utils import ( + ephemeral_raw_aes_keyring, + ephemeral_raw_aes_master_key, + ephemeral_raw_rsa_keyring, + raw_rsa_mkps_from_keyring, +) + pytestmark = [pytest.mark.functional, pytest.mark.local] VALUES = { @@ -315,58 +314,176 @@ def test_encrypt_ciphertext_message(frame_length, algorithm, encryption_context) assert len(ciphertext) == results_length -@pytest.mark.parametrize( - "wrapping_algorithm, encryption_key_type, decryption_key_type", - ( - (WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, EncryptionKeyType.SYMMETRIC, EncryptionKeyType.SYMMETRIC), - (WrappingAlgorithm.RSA_PKCS1, EncryptionKeyType.PRIVATE, EncryptionKeyType.PRIVATE), - (WrappingAlgorithm.RSA_PKCS1, EncryptionKeyType.PUBLIC, EncryptionKeyType.PRIVATE), - (WrappingAlgorithm.RSA_OAEP_SHA1_MGF1, EncryptionKeyType.PRIVATE, EncryptionKeyType.PRIVATE), - (WrappingAlgorithm.RSA_OAEP_SHA1_MGF1, EncryptionKeyType.PUBLIC, EncryptionKeyType.PRIVATE), - ), -) -def test_encryption_cycle_raw_mkp(caplog, wrapping_algorithm, encryption_key_type, decryption_key_type): - caplog.set_level(logging.DEBUG) +def _raw_aes(): + for symmetric_algorithm in ( + WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ): + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(symmetric_algorithm, EncryptionKeyType.SYMMETRIC), + "key_provider", + build_fake_raw_key_provider(symmetric_algorithm, EncryptionKeyType.SYMMETRIC), + id="raw AES master key provider -- {}".format(symmetric_algorithm.name), + ) + keyring = ephemeral_raw_aes_keyring(symmetric_algorithm) + yield pytest.param( + "keyring", keyring, "keyring", keyring, id="raw AES keyring -- {}".format(symmetric_algorithm.name) + ) + + mkp = ephemeral_raw_aes_master_key(wrapping_algorithm=symmetric_algorithm, key=keyring._wrapping_key) + yield pytest.param( + "key_provider", + mkp, + "keyring", + keyring, + id="raw AES -- encrypt with master key provider and decrypt with keyring -- {}".format(symmetric_algorithm), + ) + yield pytest.param( + "keyring", + keyring, + "key_provider", + mkp, + id="raw AES -- encrypt with keyring and decrypt with master key provider -- {}".format(symmetric_algorithm), + ) + + +def _raw_rsa(include_pre_sha2=True, include_sha2=True): + wrapping_algorithms = [] + if include_pre_sha2: + wrapping_algorithms.extend([WrappingAlgorithm.RSA_PKCS1, WrappingAlgorithm.RSA_OAEP_SHA1_MGF1]) + if include_sha2: + wrapping_algorithms.extend( + [ + WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA384_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA512_MGF1, + ] + ) + for wrapping_algorithm in wrapping_algorithms: + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + id="raw RSA master key provider -- private encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PUBLIC), + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + id="raw RSA master key provider -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + + private_keyring = ephemeral_raw_rsa_keyring(wrapping_algorithm=wrapping_algorithm) + public_keyring = RawRSAKeyring( + key_namespace=private_keyring.key_namespace, + key_name=private_keyring.key_name, + wrapping_algorithm=wrapping_algorithm, + public_wrapping_key=private_keyring._private_wrapping_key.public_key(), + ) + yield pytest.param( + "keyring", + private_keyring, + "keyring", + private_keyring, + id="raw RSA keyring -- private encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + yield pytest.param( + "keyring", + public_keyring, + "keyring", + private_keyring, + id="raw RSA keyring -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + private_mkp, public_mkp = raw_rsa_mkps_from_keyring(private_keyring) + + yield pytest.param( + "key_provider", + private_mkp, + "keyring", + private_keyring, + id="raw RSA keyring -- private master key provider encrypt and private keyring decrypt -- {}".format( + wrapping_algorithm + ), + ) + yield pytest.param( + "key_provider", + public_mkp, + "keyring", + private_keyring, + id="raw RSA keyring -- public master key provider encrypt and private keyring decrypt -- {}".format( + wrapping_algorithm + ), + ) + yield pytest.param( + "keyring", + private_keyring, + "key_provider", + private_mkp, + id="raw RSA keyring -- private keyring encrypt and private master key provider decrypt -- {}".format( + wrapping_algorithm + ), + ) + yield pytest.param( + "keyring", + public_keyring, + "key_provider", + private_mkp, + id="raw RSA keyring -- public keyring encrypt and private master key provider decrypt -- {}".format( + wrapping_algorithm + ), + ) + + +def assert_key_not_logged(provider, log_capture): + if isinstance(provider, MasterKeyProvider): + for member in provider._members: + assert repr(member.config.wrapping_key._wrapping_key)[2:-1] not in log_capture + + +def run_raw_provider_check( + log_capturer, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider +): + log_capturer.set_level(logging.DEBUG) + + encrypt_kwargs = {encrypt_param_name: encrypting_provider} + decrypt_kwargs = {decrypt_param_name: decrypting_provider} - encrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, encryption_key_type) - decrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, decryption_key_type) ciphertext, _ = aws_encryption_sdk.encrypt( source=VALUES["plaintext_128"], - key_provider=encrypting_key_provider, encryption_context=VALUES["encryption_context"], frame_length=0, + **encrypt_kwargs ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=decrypting_key_provider) + plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, **decrypt_kwargs) assert plaintext == VALUES["plaintext_128"] - for member in encrypting_key_provider._members: - assert repr(member.config.wrapping_key._wrapping_key)[2:-1] not in caplog.text + assert_key_not_logged(encrypting_provider, log_capturer.text) + + +@pytest.mark.parametrize( + "encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider", + itertools.chain.from_iterable((_raw_aes(), _raw_rsa(include_sha2=False))), +) +def test_encryption_cycle_raw_mkp( + caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider +): + run_raw_provider_check(caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider) @pytest.mark.skipif( not _mgf1_sha256_supported(), reason="MGF1-SHA2 not supported by this backend: OpenSSL required v1.0.2+" ) @pytest.mark.parametrize( - "wrapping_algorithm", - ( - WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, - WrappingAlgorithm.RSA_OAEP_SHA384_MGF1, - WrappingAlgorithm.RSA_OAEP_SHA512_MGF1, - ), + "encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider", _raw_rsa(include_pre_sha2=False) ) -@pytest.mark.parametrize("encryption_key_type", (EncryptionKeyType.PUBLIC, EncryptionKeyType.PRIVATE)) -def test_encryption_cycle_raw_mkp_openssl_102_plus(wrapping_algorithm, encryption_key_type): - decryption_key_type = EncryptionKeyType.PRIVATE - encrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, encryption_key_type) - decrypting_key_provider = build_fake_raw_key_provider(wrapping_algorithm, decryption_key_type) - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=encrypting_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=decrypting_key_provider) - assert plaintext == VALUES["plaintext_128"] +def test_encryption_cycle_raw_mkp_openssl_102_plus( + caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider +): + run_raw_provider_check(caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider) @pytest.mark.parametrize( diff --git a/test/integration/test_client.py b/test/integration/test_client.py index 26df431dc..2b66fb408 100644 --- a/test/integration/test_client.py +++ b/test/integration/test_client.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Integration test suite for `aws_encryption_sdk`.""" import io import logging @@ -64,12 +54,13 @@ def test_encrypt_verify_user_agent_kms_master_key(caplog): def test_remove_bad_client(): test = KMSMasterKeyProvider() - test.add_regional_client("us-fakey-12") + fake_region = "us-fakey-12" + test.add_regional_client(fake_region) with pytest.raises(BotoCoreError): - test._regional_clients["us-fakey-12"].list_keys() + test._regional_clients[fake_region].list_keys() - assert not test._regional_clients + assert fake_region not in test._regional_clients def test_regional_client_does_not_modify_botocore_session(caplog): diff --git a/test/unit/materials_managers/test_caching.py b/test/unit/materials_managers/test_caching.py index 833d6aa53..cea3e86b6 100644 --- a/test/unit/materials_managers/test_caching.py +++ b/test/unit/materials_managers/test_caching.py @@ -1,28 +1,20 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Unit test suite for CachingCryptoMaterialsManager""" import pytest from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.materials_managers.caching from aws_encryption_sdk.caches.base import CryptoMaterialsCache +from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache from aws_encryption_sdk.exceptions import CacheKeyError from aws_encryption_sdk.internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY from aws_encryption_sdk.internal.str_ops import to_bytes -from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.materials_managers.caching import CachingCryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + +from ..unit_test_utils import ephemeral_raw_aes_keyring, ephemeral_raw_aes_master_key pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -54,7 +46,7 @@ def fake_encryption_request(): dict(max_messages_encrypted=None), dict(max_bytes_encrypted=None), dict(partition_name=55), - dict(master_key_provider=None, backing_materials_manager=None), + dict(master_key_provider=None, backing_materials_manager=None, keyring=None), ), ) def test_attrs_fail(invalid_kwargs): @@ -88,20 +80,26 @@ def test_custom_partition_name(patch_uuid4): assert test.partition_name == custom_partition_name -def test_mkp_to_default_cmm(mocker): - mocker.patch.object(aws_encryption_sdk.materials_managers.caching, "DefaultCryptoMaterialsManager") - mock_mkp = MagicMock(__class__=MasterKeyProvider) +def test_mkp_to_default_cmm(): + mkp = ephemeral_raw_aes_master_key() + test = CachingCryptoMaterialsManager( - cache=MagicMock(__class__=CryptoMaterialsCache), max_age=10.0, master_key_provider=mock_mkp + cache=LocalCryptoMaterialsCache(capacity=10), max_age=10.0, master_key_provider=mkp ) - aws_encryption_sdk.materials_managers.caching.DefaultCryptoMaterialsManager.assert_called_once_with( - mock_mkp - ) # noqa pylint: disable=line-too-long - assert ( - test.backing_materials_manager - is aws_encryption_sdk.materials_managers.caching.DefaultCryptoMaterialsManager.return_value - ) # noqa pylint: disable=line-too-long + assert isinstance(test.backing_materials_manager, DefaultCryptoMaterialsManager) + assert test.backing_materials_manager.master_key_provider is mkp + assert test.backing_materials_manager.keyring is None + + +def test_keyring_to_default_cmm(): + keyring = ephemeral_raw_aes_keyring() + + test = CachingCryptoMaterialsManager(cache=LocalCryptoMaterialsCache(capacity=10), max_age=10.0, keyring=keyring) + + assert isinstance(test.backing_materials_manager, DefaultCryptoMaterialsManager) + assert test.backing_materials_manager.keyring is keyring + assert test.backing_materials_manager.master_key_provider is None @pytest.mark.parametrize( diff --git a/test/unit/materials_managers/test_default.py b/test/unit/materials_managers/test_default.py index 32fdc953a..9a86e59b8 100644 --- a/test/unit/materials_managers/test_default.py +++ b/test/unit/materials_managers/test_default.py @@ -1,29 +1,29 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Test suite for aws_encryption_sdk.materials_managers.default""" import pytest from mock import MagicMock, sentinel -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.materials_managers.default -from aws_encryption_sdk.exceptions import MasterKeyProviderError, SerializationError -from aws_encryption_sdk.identifiers import Algorithm +from aws_encryption_sdk.exceptions import InvalidCryptographicMaterialsError, MasterKeyProviderError, SerializationError +from aws_encryption_sdk.identifiers import Algorithm, WrappingAlgorithm from aws_encryption_sdk.internal.defaults import ALGORITHM, ENCODED_SIGNER_KEY from aws_encryption_sdk.key_providers.base import MasterKeyProvider -from aws_encryption_sdk.materials_managers import EncryptionMaterials +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, RawDataKey +from ..unit_test_utils import ( + BrokenKeyring, + NoEncryptedDataKeysKeyring, + ephemeral_raw_aes_keyring, + ephemeral_raw_aes_master_key, +) + pytestmark = [pytest.mark.unit, pytest.mark.local] _DATA_KEY = DataKey( @@ -60,14 +60,25 @@ def build_cmm(): 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]), + {sentinel.primary_mk, sentinel.mk_a, sentinel.mk_b}, ) return DefaultCryptoMaterialsManager(master_key_provider=mock_mkp) -def test_attributes_fail(): +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(), id="no parameters"), + pytest.param(dict(master_key_provider=None, keyring=None), id="explicit None for both"), + pytest.param( + dict(master_key_provider=ephemeral_raw_aes_master_key(), keyring=ephemeral_raw_aes_keyring()), + id="both provided", + ), + ), +) +def test_attributes_fail(kwargs): with pytest.raises(TypeError): - DefaultCryptoMaterialsManager(master_key_provider=None) + DefaultCryptoMaterialsManager(**kwargs) def test_attributes_default(): @@ -241,3 +252,92 @@ def test_decrypt_materials(mocker, patch_for_dcmm_decrypt): ) 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 + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +def test_encrypt_with_keyring_materials_incomplete(algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=NoEncryptedDataKeysKeyring(inner_keyring=raw_aes256_keyring)) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + encrypt_cmm.get_encryption_materials(encryption_materials_request) + + excinfo.match("Encryption materials are incomplete!") + + +def _broken_materials_scenarios(): + yield pytest.param(dict(break_algorithm=True), id="broken algorithm") + yield pytest.param(dict(break_encryption_context=True), id="broken encryption context") + yield pytest.param(dict(break_signing=True), id="broken signing/verification key") + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +@pytest.mark.parametrize("kwargs", _broken_materials_scenarios()) +def test_encrypt_with_keyring_materials_do_not_match_request(kwargs, algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=BrokenKeyring(inner_keyring=raw_aes256_keyring, **kwargs)) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + encrypt_cmm.get_encryption_materials(encryption_materials_request) + + excinfo.match("Encryption materials do not match request!") + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +def test_decrypt_with_keyring_materials_incomplete(algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + raw_aes128_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=raw_aes256_keyring) + decrypt_cmm = DefaultCryptoMaterialsManager(keyring=raw_aes128_keyring) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + encryption_materials = encrypt_cmm.get_encryption_materials(encryption_materials_request) + + decryption_materials_request = DecryptionMaterialsRequest( + algorithm=encryption_materials.algorithm, + encrypted_data_keys=encryption_materials.encrypted_data_keys, + encryption_context=encryption_materials.encryption_context, + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + decrypt_cmm.decrypt_materials(decryption_materials_request) + + excinfo.match("Decryption materials are incomplete!") + + +@pytest.mark.parametrize("algorithm_suite", Algorithm) +@pytest.mark.parametrize("kwargs", _broken_materials_scenarios()) +def test_decrypt_with_keyring_materials_do_not_match_request(kwargs, algorithm_suite): + raw_aes256_keyring = ephemeral_raw_aes_keyring(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING) + + encrypt_cmm = DefaultCryptoMaterialsManager(keyring=raw_aes256_keyring) + decrypt_cmm = DefaultCryptoMaterialsManager(keyring=BrokenKeyring(inner_keyring=raw_aes256_keyring, **kwargs)) + + encryption_materials_request = EncryptionMaterialsRequest( + encryption_context={}, frame_length=1024, algorithm=algorithm_suite + ) + encryption_materials = encrypt_cmm.get_encryption_materials(encryption_materials_request) + + decryption_materials_request = DecryptionMaterialsRequest( + algorithm=encryption_materials.algorithm, + encrypted_data_keys=encryption_materials.encrypted_data_keys, + encryption_context=encryption_materials.encryption_context, + ) + + with pytest.raises(InvalidCryptographicMaterialsError) as excinfo: + decrypt_cmm.decrypt_materials(decryption_materials_request) + + excinfo.match("Decryption materials do not match request!") diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 2bf1bc838..ff1a1b158 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -1,26 +1,24 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Utility functions to handle common test framework functions.""" +import base64 import copy import io import itertools import os +import attr +from attr.validators import instance_of from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa -from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.exceptions import DecryptKeyError +from aws_encryption_sdk.identifiers import AlgorithmSuite, EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm +from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.internal.utils.streams import InsistentReaderBytesIO +from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig +from aws_encryption_sdk.key_providers.raw import RawMasterKey, RawMasterKeyProvider from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.keyrings.multi import MultiKeyring from aws_encryption_sdk.keyrings.raw import RawAESKeyring, RawRSAKeyring @@ -28,7 +26,7 @@ from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Iterable # noqa pylint: disable=unused-import + from typing import Dict, Iterable, Optional # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -115,7 +113,7 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): def get_encryption_materials_with_data_key(): return EncryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', @@ -133,7 +131,7 @@ def get_encryption_materials_with_data_key(): def get_encryption_materials_with_data_encryption_key(): return EncryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', @@ -151,7 +149,7 @@ def get_encryption_materials_with_data_encryption_key(): def get_encryption_materials_without_data_key(): return EncryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context=_ENCRYPTION_CONTEXT, signing_key=_SIGNING_KEY, ) @@ -159,7 +157,7 @@ def get_encryption_materials_without_data_key(): def get_encryption_materials_with_encrypted_data_key(): return EncryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', @@ -187,7 +185,7 @@ def get_encryption_materials_with_encrypted_data_key(): def get_encryption_materials_with_encrypted_data_key_aes(): return EncryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', @@ -209,7 +207,7 @@ def get_encryption_materials_with_encrypted_data_key_aes(): def get_encryption_materials_without_data_encryption_key(): return EncryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context=_ENCRYPTION_CONTEXT, signing_key=_SIGNING_KEY, ) @@ -217,7 +215,7 @@ def get_encryption_materials_without_data_encryption_key(): def get_decryption_materials_without_data_encryption_key(): return DecryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, verification_key=b"ex_verification_key", encryption_context=_ENCRYPTION_CONTEXT, ) @@ -225,7 +223,7 @@ def get_decryption_materials_without_data_encryption_key(): def get_decryption_materials_with_data_key(): return DecryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', @@ -243,7 +241,7 @@ def get_decryption_materials_with_data_key(): def get_decryption_materials_with_data_encryption_key(): return DecryptionMaterials( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', @@ -401,3 +399,255 @@ def assert_prepped_stream_identity(prepped_stream, wrapped_type): assert isinstance(prepped_stream, wrapped_type) # Check the wrapping streams assert isinstance(prepped_stream, InsistentReaderBytesIO) + + +def _generate_rsa_key_bytes(size): + # type: (int) -> bytes + private_key = rsa.generate_private_key(public_exponent=65537, key_size=size, backend=default_backend()) + return private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + +def ephemeral_raw_rsa_master_key(size=4096): + # type: (int) -> RawMasterKey + key_bytes = _generate_rsa_key_bytes(size) + return RawMasterKey( + provider_id="fake", + key_id="rsa-{}".format(size).encode("utf-8"), + wrapping_key=WrappingKey( + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + wrapping_key=key_bytes, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + + +def ephemeral_raw_rsa_keyring(size=4096, wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1): + # type: (int, WrappingAlgorithm) -> RawRSAKeyring + key_bytes = _generate_rsa_key_bytes(size) + return RawRSAKeyring.from_pem_encoding( + key_namespace="fake", + key_name="rsa-{}".format(size).encode("utf-8"), + wrapping_algorithm=wrapping_algorithm, + private_encoded_key=key_bytes, + ) + + +def raw_rsa_mkps_from_keyring(keyring): + # type: (RawRSAKeyring) -> (MasterKeyProvider, MasterKeyProvider) + """Constructs a private and public raw RSA MKP using the private key in the raw RSA keyring.""" + private_key = keyring._private_wrapping_key + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + public_pem = private_key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + private_key_mkp = RawMasterKey( + provider_id=keyring.key_namespace, + key_id=keyring.key_name, + wrapping_key=WrappingKey( + wrapping_algorithm=keyring._wrapping_algorithm, + wrapping_key=private_pem, + wrapping_key_type=EncryptionKeyType.PRIVATE, + ), + ) + public_key_mkp = RawMasterKey( + provider_id=keyring.key_namespace, + key_id=keyring.key_name, + wrapping_key=WrappingKey( + wrapping_algorithm=keyring._wrapping_algorithm, + wrapping_key=public_pem, + wrapping_key_type=EncryptionKeyType.PUBLIC, + ), + ) + return private_key_mkp, public_key_mkp + + +def ephemeral_raw_aes_master_key(wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, key=None): + # type: (WrappingAlgorithm, Optional[bytes]) -> RawMasterKey + key_length = wrapping_algorithm.algorithm.data_key_len + if key is None: + key = os.urandom(key_length) + return RawMasterKey( + provider_id="fake", + key_id="aes-{}".format(key_length * 8).encode("utf-8"), + wrapping_key=WrappingKey( + wrapping_algorithm=wrapping_algorithm, wrapping_key=key, wrapping_key_type=EncryptionKeyType.SYMMETRIC, + ), + ) + + +def ephemeral_raw_aes_keyring(wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, key=None): + # type: (WrappingAlgorithm, Optional[bytes]) -> RawAESKeyring + key_length = wrapping_algorithm.algorithm.data_key_len + if key is None: + key = os.urandom(key_length) + return RawAESKeyring( + key_namespace="fake", + key_name="aes-{}".format(key_length * 8).encode("utf-8"), + wrapping_algorithm=wrapping_algorithm, + wrapping_key=key, + ) + + +class EphemeralRawMasterKeyProvider(RawMasterKeyProvider): + """Master key provider with raw master keys that are generated on each initialization.""" + + provider_id = "fake" + + def __init__(self): + self.__keys = { + b"aes-256": ephemeral_raw_aes_master_key(WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING), + b"rsa-4096": ephemeral_raw_rsa_master_key(4096), + } + + def _get_raw_key(self, key_id): + return self.__keys[key_id].config.wrapping_key + + +class EmptyMasterKeyProvider(MasterKeyProvider): + """Master key provider that provides no master keys.""" + + provider_id = "empty" + _config_class = MasterKeyProviderConfig + vend_masterkey_on_decrypt = False + + def _new_master_key(self, key_id): + raise Exception("How did this happen??") + + def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): + return ephemeral_raw_aes_master_key(), [] + + +class DisjointMasterKeyProvider(MasterKeyProvider): + """Master key provider that does not provide the primary master key in the additional master keys.""" + + provider_id = "disjoint" + _config_class = MasterKeyProviderConfig + vend_masterkey_on_decrypt = False + + def _new_master_key(self, key_id): + raise Exception("How did this happen??") + + def master_keys_for_encryption(self, encryption_context, plaintext_rostream, plaintext_length=None): + return ephemeral_raw_aes_master_key(), [ephemeral_raw_rsa_master_key()] + + +class FailingDecryptMasterKeyProvider(EphemeralRawMasterKeyProvider): + """EphemeralRawMasterKeyProvider that cannot decrypt.""" + + def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): + raise DecryptKeyError("FailingDecryptMasterKeyProvider cannot decrypt!") + + +@attr.s +class BrokenKeyring(Keyring): + """Keyring that wraps another keyring and selectively breaks the returned values.""" + + _inner_keyring = attr.ib(validator=instance_of(Keyring)) + _break_algorithm = attr.ib(default=False, validator=instance_of(bool)) + _break_encryption_context = attr.ib(default=False, validator=instance_of(bool)) + _break_signing = attr.ib(default=False, validator=instance_of(bool)) + + @staticmethod + def _random_string(bytes_len): + # type: (int) -> str + return base64.b64encode(os.urandom(bytes_len)).decode("utf-8") + + def _broken_algorithm(self, algorithm): + # type: (AlgorithmSuite) -> AlgorithmSuite + if not self._break_algorithm: + return algorithm + + # We want to make sure that we return something different, + # so find this suite in all suites and grab the next one, + # whatever that is. + all_suites = list(AlgorithmSuite) + suite_index = all_suites.index(algorithm) + next_index = (suite_index + 1) % (len(all_suites) - 1) + + return all_suites[next_index] + + def _broken_encryption_context(self, encryption_context): + # type: (Dict[str, str]) -> Dict[str, str] + broken_ec = encryption_context.copy() + + if not self._break_encryption_context: + return broken_ec + + # Remove a random value + try: + broken_ec.popitem() + except KeyError: + pass + + # add a random value + broken_ec[self._random_string(5)] = self._random_string(10) + + return broken_ec + + def _broken_key(self, key): + # type: (bytes) -> bytes + if not self._break_signing: + return key + + return self._random_string(32).encode("utf-8") + + def _break_encryption_materials(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return EncryptionMaterials( + algorithm=self._broken_algorithm(encryption_materials.algorithm), + data_encryption_key=encryption_materials.data_encryption_key, + encrypted_data_keys=encryption_materials.encrypted_data_keys, + encryption_context=self._broken_encryption_context(encryption_materials.encryption_context), + signing_key=self._broken_key(encryption_materials.signing_key), + keyring_trace=encryption_materials.keyring_trace, + ) + + def _break_decryption_materials(self, decryption_materials): + # type: (DecryptionMaterials) -> DecryptionMaterials + return DecryptionMaterials( + algorithm=self._broken_algorithm(decryption_materials.algorithm), + data_encryption_key=decryption_materials.data_encryption_key, + encryption_context=self._broken_encryption_context(decryption_materials.encryption_context), + verification_key=self._broken_key(decryption_materials.verification_key), + keyring_trace=decryption_materials.keyring_trace, + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return self._break_encryption_materials(self._inner_keyring.on_encrypt(encryption_materials)) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return self._break_decryption_materials( + self._inner_keyring.on_decrypt(decryption_materials, encrypted_data_keys) + ) + + +@attr.s +class NoEncryptedDataKeysKeyring(Keyring): + """Keyring that wraps another keyring and removes any encrypted data keys.""" + + _inner_keyring = attr.ib(validator=instance_of(Keyring)) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + materials = self._inner_keyring.on_encrypt(encryption_materials) + return EncryptionMaterials( + algorithm=materials.algorithm, + data_encryption_key=materials.data_encryption_key, + encryption_context=materials.encryption_context, + signing_key=materials.signing_key, + keyring_trace=materials.keyring_trace, + ) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + return self._inner_keyring.on_decrypt(decryption_materials, encrypted_data_keys) From 71b795dccb280b2b25b92c359d80c85623f2bd27 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Thu, 5 Mar 2020 00:51:33 -0800 Subject: [PATCH 26/64] chore: remove CPython3.4 advertised support and add CPython3.8 (#217) * chore: remove CPython3.4 advertised support and add CPython3.8 * docs: update readmes to reflect 3.5 minimum --- .travis.yml | 74 ++++++++++++++++++++------------- README.rst | 2 +- appveyor.yml | 40 ++++++++---------- setup.py | 2 +- test_vector_handlers/README.rst | 2 +- test_vector_handlers/setup.py | 2 +- test_vector_handlers/tox.ini | 2 +- tox.ini | 2 +- 8 files changed, 69 insertions(+), 57 deletions(-) diff --git a/.travis.yml b/.travis.yml index 86702c536..cf290d719 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,19 +15,6 @@ matrix: - python: 2.7 env: TOXENV=py27-examples stage: Client Tests - # CPython 3.4 - - python: 3.4 - env: TOXENV=py34-local - stage: Client Tests - - python: 3.4 - env: TOXENV=py34-integ - stage: Client Tests - - python: 3.4 - env: TOXENV=py34-accept - stage: Client Tests - - python: 3.4 - env: TOXENV=py34-examples - stage: Client Tests # CPython 3.5 - python: 3.5 env: TOXENV=py35-local @@ -77,6 +64,29 @@ matrix: dist: xenial sudo: true stage: Client Tests + # CPython 3.8 + # xenial + sudo are currently needed to get 3.8 + # https://github.com/travis-ci/travis-ci/issues/9815 + - python: 3.8 + env: TOXENV=py38-local + dist: xenial + sudo: true + stage: Client Tests + - python: 3.8 + env: TOXENV=py38-integ + dist: xenial + sudo: true + stage: Client Tests + - python: 3.8 + env: TOXENV=py38-accept + dist: xenial + sudo: true + stage: Client Tests + - python: 3.8 + env: TOXENV=py38-examples + dist: xenial + sudo: true + stage: Client Tests # Upstream tests - python: 3.6 env: TOXENV=nocmk @@ -148,22 +158,6 @@ matrix: TEST_VECTOR_HANDLERS=1 TOXENV=py27-awses_latest stage: Test Vector Handler Tests - # CPython 3.4 - - python: 3.4 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=py34-awses_1.3.3 - stage: Test Vector Handler Tests - - python: 3.4 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=py34-awses_1.3.max - stage: Test Vector Handler Tests - - python: 3.4 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=py34-awses_latest - stage: Test Vector Handler Tests # CPython 3.5 - python: 3.5 env: @@ -218,6 +212,28 @@ matrix: dist: xenial sudo: true stage: Test Vector Handler Tests + # CPython 3.8 + - python: 3.8 + env: + TEST_VECTOR_HANDLERS=1 + TOXENV=py38-awses_1.3.3 + dist: xenial + sudo: true + stage: Test Vector Handler Tests + - python: 3.8 + env: + TEST_VECTOR_HANDLERS=1 + TOXENV=py38-awses_1.3.max + dist: xenial + sudo: true + stage: Test Vector Handler Tests + - python: 3.8 + env: + TEST_VECTOR_HANDLERS=1 + TOXENV=py38-awses_latest + dist: xenial + sudo: true + stage: Test Vector Handler Tests # Linters - python: 3.6 env: diff --git a/README.rst b/README.rst index 092b3b109..ae373f8e3 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ Getting Started Required Prerequisites ====================== -* Python 2.7+ or 3.4+ +* Python 2.7+ or 3.5+ * cryptography >= 1.8.1 * boto3 * attrs diff --git a/appveyor.yml b/appveyor.yml index cfb4bdcdb..ceb9318bd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,28 +24,6 @@ environment: - PYTHON: "C:\\Python27-x64" TOXENV: "py27-examples" - # Python 3.4 - - PYTHON: "C:\\Python34" - TOXENV: "py34-local" - - PYTHON: "C:\\Python34" - TOXENV: "py34-integ" - - PYTHON: "C:\\Python34" - TOXENV: "py34-accept" - - PYTHON: "C:\\Python34" - TOXENV: "py34-examples" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-local" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-integ" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-accept" - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - TOXENV: "py34-examples" - # Python 3.5 - PYTHON: "C:\\Python35" TOXENV: "py35-local" @@ -100,6 +78,24 @@ environment: - PYTHON: "C:\\Python37-x64" TOXENV: "py37-examples" + # Python 3.8 + - PYTHON: "C:\\Python38" + TOXENV: "py38-local" + - PYTHON: "C:\\Python38" + TOXENV: "py38-integ" + - PYTHON: "C:\\Python38" + TOXENV: "py38-accept" + - PYTHON: "C:\\Python38" + TOXENV: "py38-examples" + - PYTHON: "C:\\Python38-x64" + TOXENV: "py38-local" + - PYTHON: "C:\\Python38-x64" + TOXENV: "py38-integ" + - PYTHON: "C:\\Python38-x64" + TOXENV: "py38-accept" + - PYTHON: "C:\\Python38-x64" + TOXENV: "py38-examples" + install: # Prepend newly installed Python to the PATH of this build - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" diff --git a/setup.py b/setup.py index 6ceb2d8fb..52ef4ff7f 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,10 @@ def get_requirements(): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Security :: Cryptography", diff --git a/test_vector_handlers/README.rst b/test_vector_handlers/README.rst index a2d188dd6..b1d10c2c5 100644 --- a/test_vector_handlers/README.rst +++ b/test_vector_handlers/README.rst @@ -12,7 +12,7 @@ Getting Started Required Prerequisites ====================== -* Python 2.7 or 3.4+ +* Python 2.7 or 3.5+ * aws-encryption-sdk Use diff --git a/test_vector_handlers/setup.py b/test_vector_handlers/setup.py index bd16c76c8..c7ad742ad 100644 --- a/test_vector_handlers/setup.py +++ b/test_vector_handlers/setup.py @@ -48,10 +48,10 @@ def get_requirements(): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Security", "Topic :: Security :: Cryptography", diff --git a/test_vector_handlers/tox.ini b/test_vector_handlers/tox.ini index fc2c8d5a7..d62bc38ea 100644 --- a/test_vector_handlers/tox.ini +++ b/test_vector_handlers/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36,37}-awses_{1.3.3,1.3.max,latest}, + py{27,35,36,37,38}-awses_{1.3.3,1.3.max,latest}, # 1.2.0 and 1.2.max are being difficult because of attrs bandit, doc8, readme, docs, {flake8,pylint}{,-tests}, diff --git a/tox.ini b/tox.ini index 06564ef6a..1df161ccd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36,37}-{local,integ,accept,examples}, nocmk, + py{27,35,36,37,38}-{local,integ,accept,examples}, nocmk, bandit, doc8, readme, docs, {flake8,pylint}{,-tests,-examples}, isort-check, black-check, From 45a5632f6d179724687ca757029b48cdbcde35c2 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 4 Mar 2020 16:26:32 -0800 Subject: [PATCH 27/64] feat: add AWS KMS keyring and tests --- doc/index.rst | 4 + setup.cfg | 2 +- src/aws_encryption_sdk/internal/validators.py | 21 + .../keyrings/aws_kms/__init__.py | 376 ++++++++++++++ .../keyrings/aws_kms/client_cache.py | 120 +++++ .../keyrings/aws_kms/client_suppliers.py | 136 +++++ test/functional/functional_test_utils.py | 29 ++ test/functional/keyrings/aws_kms/__init__.py | 3 + .../keyrings/aws_kms/test_aws_kms.py | 490 ++++++++++++++++++ .../keyrings/aws_kms/test_client_cache.py | 31 ++ .../keyrings/aws_kms/test_client_suppliers.py | 113 ++++ .../test_base_master_key_provider_config.py | 4 + test/unit/keyrings/test_aws_kms.py | 198 +++++++ tox.ini | 2 + 14 files changed, 1528 insertions(+), 1 deletion(-) create mode 100644 src/aws_encryption_sdk/internal/validators.py create mode 100644 src/aws_encryption_sdk/keyrings/aws_kms/__init__.py create mode 100644 src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py create mode 100644 src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py create mode 100644 test/functional/functional_test_utils.py create mode 100644 test/functional/keyrings/aws_kms/__init__.py create mode 100644 test/functional/keyrings/aws_kms/test_aws_kms.py create mode 100644 test/functional/keyrings/aws_kms/test_client_cache.py create mode 100644 test/functional/keyrings/aws_kms/test_client_suppliers.py create mode 100644 test/unit/keyrings/test_aws_kms.py diff --git a/doc/index.rst b/doc/index.rst index b4a0476f9..5e6181904 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,6 +15,9 @@ Modules aws_encryption_sdk.caches.local aws_encryption_sdk.caches.null aws_encryption_sdk.keyrings.base + aws_encryption_sdk.keyrings.aws_kms + aws_encryption_sdk.keyrings.aws_kms.client_suppliers + aws_encryption_sdk.keyrings.aws_kms.client_cache aws_encryption_sdk.keyrings.multi aws_encryption_sdk.keyrings.raw aws_encryption_sdk.key_providers.base @@ -40,6 +43,7 @@ Modules aws_encryption_sdk.internal.formatting.serialize aws_encryption_sdk.internal.str_ops aws_encryption_sdk.internal.structures + aws_encryption_sdk.internal.validators aws_encryption_sdk.internal.utils .. include:: ../CHANGELOG.rst diff --git a/setup.cfg b/setup.cfg index 038fc5924..0671c3c64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,4 +52,4 @@ force_grid_wrap = 0 combine_as_imports = True not_skip = __init__.py known_first_party = aws_encryption_sdk -known_third_party = attr,awses_test_vectors,basic_encryption,basic_file_encryption_with_multiple_providers,basic_file_encryption_with_raw_key_provider,boto3,botocore,cryptography,data_key_caching_basic,integration_test_utils,mock,pytest,pytest_mock,setuptools,six,typing,wrapt +known_third_party = attr,awacs,aws_encryption_sdk_decrypt_oracle,awses_test_vectors,boto3,botocore,chalice,cryptography,integration_test_utils,mock,moto,pytest,pytest_mock,requests,setuptools,six,troposphere,wrapt diff --git a/src/aws_encryption_sdk/internal/validators.py b/src/aws_encryption_sdk/internal/validators.py new file mode 100644 index 000000000..66056602e --- /dev/null +++ b/src/aws_encryption_sdk/internal/validators.py @@ -0,0 +1,21 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Common ``attrs`` validators.""" +import attr # only used by mypy, so pylint: disable=unused-import +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +def value_is_not_a_string(instance, attribute, value): + # type: (Any, attr.Attribute, Any) -> None + """Technically a string is an iterable containing strings. + + This validator lets you accept other iterators but not strings. + """ + if isinstance(value, six.string_types): + raise TypeError("'{}' must not a string".format(attribute.name)) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py new file mode 100644 index 000000000..5cec52bdc --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -0,0 +1,376 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Keyring for use with AWS Key Management Service (KMS). + +.. versionadded:: 1.5.0 + +""" +import logging + +import attr +import six +from attr.validators import deep_iterable, instance_of, optional + +from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.internal.validators import value_is_not_a_string +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, KeyringTraceFlag, MasterKeyInfo, RawDataKey + +from .client_suppliers import ClientSupplier, DefaultClientSupplier + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Dict, Iterable, Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ("KmsKeyring",) + +_LOGGER = logging.getLogger(__name__) +_PROVIDER_ID = "aws-kms" +_GENERATE_FLAGS = {KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY} +_ENCRYPT_FLAGS = {KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX} +_DECRYPT_FLAGS = {KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX} + + +@attr.s +class KmsKeyring(Keyring): + """Keyring that uses AWS Key Management Service (KMS) Customer Master Keys (CMKs) to manage wrapping keys. + + Set ``generator_key_id`` to require that the keyring use that CMK to generate the data key. + If you do not set ``generator_key_id``, the keyring will not generate a data key. + + Set ``child_key_ids`` to specify additional CMKs that the keyring will use to encrypt the data key. + + The keyring will attempt to use any CMKs + identified by CMK ARN in either ``generator_key_id`` or ``child_key_ids`` on decrypt. + + You can identify CMKs by any `valid key ID`_ for the keyring to use on encrypt, + but for the keyring to attempt to use them on decrypt + you MUST specify the CMK ARN. + + If you specify neither ``generator_key_id`` nor ``child_key_ids`` + then the keyring will operate in `discovery mode`_, + doing nothing on encrypt and attempting to decrypt any AWS KMS-encrypted data key on decrypt. + + You can use the :class:`ClientSupplier` to customize behavior further, + such as to provide different credentials for different regions + or to restrict which regions are allowed. + + See the `AWS KMS Keyring specification`_ for more details. + + .. _AWS KMS Keyring specification: + https://github.com/awslabs/aws-encryption-sdk-specification/blob/master/framework/kms-keyring.md + .. _valid key ID: + https://docs.aws.amazon.com/kms/latest/APIReference/API_GenerateDataKey.html#API_GenerateDataKey_RequestSyntax + .. _discovery mode: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#kms-keyring-discovery + + .. versionadded:: 1.5.0 + + :param ClientSupplier client_supplier: Client supplier that provides AWS KMS clients (optional) + :param str generator_key_id: Key ID of AWS KMS CMK to use when generating data keys (optional) + :param List[str] child_key_ids: Key IDs that will be used to encrypt and decrypt data keys (optional) + :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) + """ + + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=instance_of(ClientSupplier)) + _generator_key_id = attr.ib(default=None, validator=optional(instance_of(six.string_types))) + _child_key_ids = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + _grant_tokens = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + + def __attrs_post_init__(self): + """Configure internal keyring.""" + if self._generator_key_id is None: + generator_keyring = None + else: + generator_keyring = _AwsKmsSingleCmkKeyring( + key_id=self._generator_key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + + child_keyrings = [ + _AwsKmsSingleCmkKeyring( + key_id=key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + for key_id in self._child_key_ids + ] + + self._is_discovery = generator_keyring is None and not child_keyrings + + if self._is_discovery: + self._inner_keyring = _AwsKmsDiscoveryKeyring( + client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + else: + self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + """Generate a data key using generator keyring + and encrypt it using any available wrapping key in any child keyring. + + :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. + :returns: Optionally modified encryption materials. + :rtype: EncryptionMaterials + :raises EncryptKeyError: if unable to encrypt data key. + """ + return self._inner_keyring.on_encrypt(encryption_materials=encryption_materials) + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + """Attempt to decrypt the encrypted data keys. + + :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. + :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys. + :returns: Optionally modified decryption materials. + :rtype: DecryptionMaterials + """ + return self._inner_keyring.on_decrypt( + decryption_materials=decryption_materials, encrypted_data_keys=encrypted_data_keys + ) + + +@attr.s +class _AwsKmsSingleCmkKeyring(Keyring): + """AWS KMS keyring that only works with a single AWS KMS CMK. + + This keyring should never be used directly. + It should only ever be used internally by :class:`KmsKeyring`. + + .. versionadded:: 1.5.0 + + :param str key_id: CMK key ID + :param ClientSupplier client_supplier: Client supplier to use when asking for clients + :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) + """ + + _key_id = attr.ib(validator=instance_of(six.string_types)) + _client_supplier = attr.ib(validator=instance_of(ClientSupplier)) + _grant_tokens = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + trace_info = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=self._key_id) + try: + if encryption_materials.data_encryption_key is None: + plaintext_key, encrypted_key = _do_aws_kms_generate_data_key( + client_supplier=self._client_supplier, + key_name=self._key_id, + encryption_context=encryption_materials.encryption_context, + algorithm=encryption_materials.algorithm, + grant_tokens=self._grant_tokens, + ) + encryption_materials.add_data_encryption_key( + data_encryption_key=plaintext_key, + keyring_trace=KeyringTrace(wrapping_key=trace_info, flags=_GENERATE_FLAGS), + ) + else: + encrypted_key = _do_aws_kms_encrypt( + client_supplier=self._client_supplier, + key_name=self._key_id, + plaintext_data_key=encryption_materials.data_encryption_key, + encryption_context=encryption_materials.encryption_context, + grant_tokens=self._grant_tokens, + ) + except Exception: # pylint: disable=broad-except + # We intentionally WANT to catch all exceptions here + message = "Unable to generate or encrypt data key using {}".format(trace_info) + _LOGGER.exception(message) + raise EncryptKeyError(message) + + encryption_materials.add_encrypted_data_key( + encrypted_data_key=encrypted_key, keyring_trace=KeyringTrace(wrapping_key=trace_info, flags=_ENCRYPT_FLAGS) + ) + + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + for edk in encrypted_data_keys: + if decryption_materials.data_encryption_key is not None: + return decryption_materials + + if not ( + edk.key_provider.provider_id == _PROVIDER_ID + and edk.key_provider.key_info.decode("utf-8") == self._key_id + ): + continue + + decryption_materials = _try_aws_kms_decrypt( + client_supplier=self._client_supplier, + decryption_materials=decryption_materials, + grant_tokens=self._grant_tokens, + encrypted_data_key=edk, + ) + + return decryption_materials + + +@attr.s +class _AwsKmsDiscoveryKeyring(Keyring): + """AWS KMS discovery keyring that will attempt to decrypt any AWS KMS encrypted data key. + + This keyring should never be used directly. + It should only ever be used internally by :class:`KmsKeyring`. + + .. versionadded:: 1.5.0 + + :param ClientSupplier client_supplier: Client supplier to use when asking for clients + :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) + """ + + _client_supplier = attr.ib(validator=instance_of(ClientSupplier)) + _grant_tokens = attr.ib( + default=attr.Factory(tuple), + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), + ) + + def on_encrypt(self, encryption_materials): + # type: (EncryptionMaterials) -> EncryptionMaterials + return encryption_materials + + def on_decrypt(self, decryption_materials, encrypted_data_keys): + # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + for edk in encrypted_data_keys: + if decryption_materials.data_encryption_key is not None: + return decryption_materials + + if edk.key_provider.provider_id != _PROVIDER_ID: + continue + + decryption_materials = _try_aws_kms_decrypt( + client_supplier=self._client_supplier, + decryption_materials=decryption_materials, + grant_tokens=self._grant_tokens, + encrypted_data_key=edk, + ) + + return decryption_materials + + +def _try_aws_kms_decrypt(client_supplier, decryption_materials, grant_tokens, encrypted_data_key): + # type: (ClientSupplier, DecryptionMaterials, Iterable[str], EncryptedDataKey) -> DecryptionMaterials + """Attempt to call ``kms:Decrypt`` and return the resulting plaintext data key. + + Any errors encountered are caught and logged. + + .. versionadded:: 1.5.0 + + """ + try: + plaintext_key = _do_aws_kms_decrypt( + client_supplier=client_supplier, + key_name=encrypted_data_key.key_provider.key_info.decode("utf-8"), + encrypted_data_key=encrypted_data_key, + encryption_context=decryption_materials.encryption_context, + grant_tokens=grant_tokens, + ) + except Exception: # pylint: disable=broad-except + # We intentionally WANT to catch all exceptions here + _LOGGER.exception("Unable to decrypt encrypted data key from %s", encrypted_data_key.key_provider) + else: + decryption_materials.add_data_encryption_key( + data_encryption_key=plaintext_key, + keyring_trace=KeyringTrace(wrapping_key=encrypted_data_key.key_provider, flags=_DECRYPT_FLAGS), + ) + return decryption_materials + + return decryption_materials + + +def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryption_context, grant_tokens): + # type: (ClientSupplier, str, EncryptedDataKey, Dict[str, str], Iterable[str]) -> RawDataKey + """Attempt to call ``kms:Decrypt`` and return the resulting plaintext data key. + + Any errors encountered are passed up the chain without comment. + + .. versionadded:: 1.5.0 + + """ + region = _region_from_key_id(encrypted_data_key.key_provider.key_info.decode("utf-8")) + client = client_supplier.client(region) + response = client.decrypt( + CiphertextBlob=encrypted_data_key.encrypted_data_key, + EncryptionContext=encryption_context, + GrantTokens=grant_tokens, + ) + response_key_id = response["KeyId"] + if response_key_id != key_name: + raise DecryptKeyError( + "Decryption results from AWS KMS are for an unexpected key ID!" + " actual '{actual}' != expected '{expected}'".format(actual=response_key_id, expected=key_name) + ) + return RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response_key_id), data_key=response["Plaintext"] + ) + + +def _do_aws_kms_encrypt(client_supplier, key_name, plaintext_data_key, encryption_context, grant_tokens): + # type: (ClientSupplier, str, RawDataKey, Dict[str, str], Iterable[str]) -> EncryptedDataKey + """Attempt to call ``kms:Encrypt`` and return the resulting encrypted data key. + + Any errors encountered are passed up the chain without comment. + """ + region = _region_from_key_id(key_name) + client = client_supplier.client(region) + response = client.encrypt( + KeyId=key_name, + Plaintext=plaintext_data_key.data_key, + EncryptionContext=encryption_context, + GrantTokens=grant_tokens, + ) + return EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + +def _do_aws_kms_generate_data_key(client_supplier, key_name, encryption_context, algorithm, grant_tokens): + # type: (ClientSupplier, str, Dict[str, str], AlgorithmSuite, Iterable[str]) -> (RawDataKey, EncryptedDataKey) + """Attempt to call ``kms:GenerateDataKey`` and return the resulting plaintext and encrypted data keys. + + Any errors encountered are passed up the chain without comment. + + .. versionadded:: 1.5.0 + + """ + region = _region_from_key_id(key_name) + client = client_supplier.client(region) + response = client.generate_data_key( + KeyId=key_name, + NumberOfBytes=algorithm.kdf_input_len, + EncryptionContext=encryption_context, + GrantTokens=grant_tokens, + ) + provider = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]) + plaintext_key = RawDataKey(key_provider=provider, data_key=response["Plaintext"]) + encrypted_key = EncryptedDataKey(key_provider=provider, encrypted_data_key=response["CiphertextBlob"]) + return plaintext_key, encrypted_key + + +def _region_from_key_id(key_id): + # type: (str) -> Union[None, str] + """Attempt to determine the region from the key ID. + + If the region cannot be found, ``None`` is returned instead. + + .. versionadded:: 1.5.0 + + """ + parts = key_id.split(":", 4) + try: + return parts[3] + except IndexError: + return None diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py new file mode 100644 index 000000000..e809ab894 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py @@ -0,0 +1,120 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""boto3 client cache for use by client suppliers. + +.. versionadded:: 1.5.0 + +""" +import functools +import logging + +import attr +from attr.validators import instance_of +from boto3.session import Session as Boto3Session +from botocore.client import BaseClient +from botocore.config import Config as BotocoreConfig +from botocore.exceptions import BotoCoreError +from botocore.session import Session as BotocoreSession + +from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +_LOGGER = logging.getLogger(__name__) +__all__ = ("ClientCache",) + + +@attr.s +class ClientCache(object): + """Provide boto3 clients regional clients, caching by region. + + Any clients that throw an error when used are immediately removed from the cache. + + .. versionadded:: 1.5.0 + + :param botocore_session: Botocore session to use when creating clients (optional) + :type botocore_session: botocore.session.Session + :param client_config: Config to use when creating client + :type client_config: botocore.config.Config + """ + + _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) + _client_config = attr.ib( + default=attr.Factory(functools.partial(BotocoreConfig, user_agent_extra=USER_AGENT_SUFFIX)), + validator=instance_of(BotocoreConfig), + ) + + def __attrs_post_init__(self): + """Set up internal cache.""" + self._cache = {} # type: Dict[str, BaseClient] + + def _wrap_client_method(self, region_name, method, *args, **kwargs): + """Proxy a call to a boto3 client method and remove any misbehaving clients from the cache. + + :param str region_name: Client region name + :param Callable method: Method on the boto3 client to proxy + :param Tuple args: Positional arguments to pass to ``method`` + :param Dict kwargs: Named arguments to pass to ``method`` + :returns: result of + """ + try: + return method(*args, **kwargs) + except BotoCoreError as error: + try: + del self._cache[region_name] + except KeyError: + pass + _LOGGER.exception( + 'Removing client "%s" from cache due to BotoCoreError on %s call', region_name, method.__name__ + ) + raise error + + def _patch_client(self, client): + # type: (BaseClient) -> BaseClient + """Patch a boto3 client, wrapping every API call in ``_wrap_client_method``. + + :param BaseClient client: boto3 client to patch + :returns: patched client + """ + for method_name in client.meta.method_to_api_mapping: + method = getattr(client, method_name) + wrapped_method = functools.partial(self._wrap_client_method, client.meta.region_name, method) + setattr(client, method_name, wrapped_method) + + return client + + def _add_client(self, region_name, service): + # type: (str, str) -> BaseClient + """Make a new client and add it to the internal cache. + + :param str region_name: Client region + :param str service: Client service + :returns: New client, now in cache + :rtype: botocore.client.BaseClient + """ + client = Boto3Session(botocore_session=self._botocore_session).client( + service_name=service, region_name=region_name, config=self._client_config + ) + patched_client = self._patch_client(client) + self._cache[region_name] = patched_client + return client + + def client(self, region_name, service): + # type: (str, str) -> BaseClient + """Get a client for the specified region and service. + + Generate a new client if needed. + Otherwise, retrieve an existing client from the internal cache. + + :param str region_name: Client region + :param str service: Client service + :rtype: botocore.client.BaseClient + """ + try: + return self._cache[region_name] + except KeyError: + return self._add_client(region_name, service) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py new file mode 100644 index 000000000..7fcdb4fc5 --- /dev/null +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -0,0 +1,136 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""AWS KMS client suppliers for use with AWS KMS keyring. + +.. versionadded:: 1.5.0 + +""" +import logging + +import attr +import six +from attr.validators import deep_iterable, instance_of +from botocore.client import BaseClient +from botocore.session import Session as BotocoreSession + +from aws_encryption_sdk.exceptions import UnknownRegionError +from aws_encryption_sdk.internal.validators import value_is_not_a_string + +from .client_cache import ClientCache + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Dict, Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +_LOGGER = logging.getLogger(__name__) +__all__ = ("ClientSupplier", "DefaultClientSupplier", "AllowRegionsClientSupplier", "DenyRegionsClientSupplier") + + +class ClientSupplier(object): + """Base class for client suppliers. + + .. versionadded:: 1.5.0 + + """ + + def client(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + """ + raise NotImplementedError("'ClientSupplier' does not implement 'client'") + + +@attr.s +class DefaultClientSupplier(ClientSupplier): + """The default AWS KMS client supplier. + Creates and caches clients for any region. + + .. versionadded:: 1.5.0 + + :param botocore_session: botocore session to use when creating clients (optional) + :type botocore_session: botocore.session.Session + """ + + _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) + + def __attrs_post_init__(self): + """Set up internal client cache.""" + self._cache = ClientCache(botocore_session=self._botocore_session) + + def client(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + """ + return self._cache.client(region_name=region_name, service="kms") + + +@attr.s +class AllowRegionsClientSupplier(ClientSupplier): + """AWS KMS client supplier that only supplies clients for the specified regions. + + .. versionadded:: 1.5.0 + + :param List[str] allowed_regions: Regions to allow + :param botocore_session: botocore session to use when creating clients (optional) + :type botocore_session: botocore.session.Session + """ + + allowed_regions = attr.ib( + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) + ) + _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) + + def __attrs_post_init__(self): + """Set up internal client supplier.""" + self._supplier = DefaultClientSupplier(botocore_session=self._botocore_session) + + def client(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + :raises UnknownRegionError: if a region is requested that is not in ``allowed_regions`` + """ + if region_name not in self.allowed_regions: + raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) + + return self._supplier.client(region_name) + + +@attr.s +class DenyRegionsClientSupplier(ClientSupplier): + """AWS KMS client supplier that supplies clients for any region except for the specified regions. + + .. versionadded:: 1.5.0 + + :param List[str] denied_regions: Regions to deny + :param botocore_session: Botocore session to use when creating clients (optional) + :type botocore_session: botocore.session.Session + """ + + denied_regions = attr.ib( + validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) + ) + _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) + + def __attrs_post_init__(self): + """Set up internal client supplier.""" + self._supplier = DefaultClientSupplier(botocore_session=self._botocore_session) + + def client(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + :raises UnknownRegionError: if a region is requested that is in ``denied_regions`` + """ + if region_name in self.denied_regions: + raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) + + return self._supplier.client(region_name) diff --git a/test/functional/functional_test_utils.py b/test/functional/functional_test_utils.py new file mode 100644 index 000000000..3822ef5fa --- /dev/null +++ b/test/functional/functional_test_utils.py @@ -0,0 +1,29 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Utility functions to handle configuration and credentials setup for functional tests.""" +import boto3 +import pytest +from moto.kms import mock_kms + +FAKE_REGION = "us-west-2" + + +def _create_cmk(): + # type: () -> str + kms = boto3.client("kms", region_name=FAKE_REGION) + response = kms.create_key() + return response["KeyMetadata"]["Arn"] + + +@pytest.fixture +def fake_generator(): + with mock_kms(): + yield _create_cmk() + + +@pytest.fixture +def fake_generator_and_child(): + with mock_kms(): + generator = _create_cmk() + child = _create_cmk() + yield generator, child diff --git a/test/functional/keyrings/aws_kms/__init__.py b/test/functional/keyrings/aws_kms/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/functional/keyrings/aws_kms/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/functional/keyrings/aws_kms/test_aws_kms.py b/test/functional/keyrings/aws_kms/test_aws_kms.py new file mode 100644 index 000000000..19d0d68ad --- /dev/null +++ b/test/functional/keyrings/aws_kms/test_aws_kms.py @@ -0,0 +1,490 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Functional tests for ``aws_encryption_sdk.keyrings.aws_kms``.""" +import itertools +import logging +import os + +import boto3 +import pytest +from moto.kms import mock_kms + +from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError +from aws_encryption_sdk.identifiers import KeyringTraceFlag +from aws_encryption_sdk.internal.defaults import ALGORITHM +from aws_encryption_sdk.keyrings.aws_kms import ( + _PROVIDER_ID, + KmsKeyring, + _AwsKmsDiscoveryKeyring, + _AwsKmsSingleCmkKeyring, + _do_aws_kms_decrypt, + _do_aws_kms_encrypt, + _do_aws_kms_generate_data_key, + _try_aws_kms_decrypt, +) +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import DefaultClientSupplier +from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials +from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, MasterKeyInfo, RawDataKey + +# used as fixtures +from ...functional_test_utils import fake_generator # noqa pylint: disable=unused-import +from ...functional_test_utils import fake_generator_and_child # noqa pylint: disable=unused-import +from ...functional_test_utils import FAKE_REGION + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Iterable, List # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def _matching_flags(wrapping_key, keyring_trace): + # type: (MasterKeyInfo, Iterable[KeyringTrace]) -> List[KeyringTraceFlag] + return list( + itertools.chain.from_iterable([entry.flags for entry in keyring_trace if entry.wrapping_key == wrapping_key]) + ) + + +def test_aws_kms_single_cmk_keyring_on_encrypt_empty_materials(fake_generator): + keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + result_materials = keyring.on_encrypt(initial_materials) + + assert result_materials.data_encryption_key is not None + assert len(result_materials.encrypted_data_keys) == 1 + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX in generator_flags + + +def test_aws_kms_single_cmk_keyring_on_encrypt_existing_data_key(fake_generator): + keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=os.urandom(ALGORITHM.kdf_input_len) + ), + ) + + result_materials = keyring.on_encrypt(initial_materials) + + assert result_materials.data_encryption_key is not None + assert len(result_materials.encrypted_data_keys) == 1 + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY not in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX in generator_flags + + +@mock_kms +def test_aws_kms_single_cmk_keyring_on_encrypt_fail(): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + keyring = _AwsKmsSingleCmkKeyring(key_id="foo", client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + with pytest.raises(EncryptKeyError) as excinfo: + keyring.on_encrypt(initial_materials) + + excinfo.match(r"Unable to generate or encrypt data key using *") + + +@mock_kms +def test_aws_kms_single_cmk_keyring_on_decrypt_existing_datakey(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsSingleCmkKeyring(key_id="foo", client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=os.urandom(ALGORITHM.kdf_input_len) + ), + ) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + ), + ), + ) + + assert result_materials.data_encryption_key == initial_materials.data_encryption_key + + log_data = caplog.text + # This means that it did NOT try to decrypt the EDK. + assert "Unable to decrypt encrypted data key from" not in log_data + + +def test_aws_kms_single_cmk_keyring_on_decrypt_single_cmk(fake_generator): + keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + encryption_materials = keyring.on_encrypt(initial_encryption_materials) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + assert result_materials.data_encryption_key is not None + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in generator_flags + + +def test_aws_kms_single_cmk_keyring_on_decrypt_multiple_cmk(fake_generator_and_child): + generator, child = fake_generator_and_child + + encrypting_keyring = KmsKeyring(generator_key_id=generator, child_key_ids=(child,)) + decrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=child, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + encryption_materials = encrypting_keyring.on_encrypt(initial_encryption_materials) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = decrypting_keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=generator), result_materials.keyring_trace + ) + assert len(generator_flags) == 0 + + child_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=child), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in child_flags + assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in child_flags + + +def test_aws_kms_single_cmk_keyring_on_decrypt_no_match(fake_generator_and_child): + generator, child = fake_generator_and_child + + encrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=generator, client_supplier=DefaultClientSupplier()) + decrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=child, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + encryption_materials = encrypting_keyring.on_encrypt(initial_encryption_materials) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = decrypting_keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + assert result_materials.data_encryption_key is None + + +@mock_kms +def test_aws_kms_single_cmk_keyring_on_decrypt_fail(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsSingleCmkKeyring(key_id="foo", client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + ), + ), + ) + + assert not result_materials.data_encryption_key + + log_data = caplog.text + + # This means that it did actually try to decrypt the EDK but encountered an error talking to KMS. + assert "Unable to decrypt encrypted data key from" in log_data + + +def test_aws_kms_discovery_keyring_on_encrypt(): + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + result_materials = keyring.on_encrypt(initial_materials) + + assert len(result_materials.encrypted_data_keys) == 0 + + +@pytest.fixture +def encryption_materials_for_discovery_decrypt(fake_generator): + encrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=fake_generator, client_supplier=DefaultClientSupplier()) + + initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) + + return fake_generator, encrypting_keyring.on_encrypt(initial_encryption_materials) + + +def test_aws_kms_discovery_keyring_on_decrypt(encryption_materials_for_discovery_decrypt): + generator_key_id, encryption_materials = encryption_materials_for_discovery_decrypt + + decrypting_keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_decryption_materials = DecryptionMaterials( + algorithm=encryption_materials.algorithm, encryption_context=encryption_materials.encryption_context + ) + + result_materials = decrypting_keyring.on_decrypt( + decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys + ) + + assert result_materials.data_encryption_key is not None + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=generator_key_id), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in generator_flags + + +@mock_kms +def test_aws_kms_discovery_keyring_on_decrypt_existing_data_key(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials( + algorithm=ALGORITHM, + encryption_context={}, + data_encryption_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), data_key=os.urandom(ALGORITHM.kdf_input_len) + ), + ) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + ), + ), + ) + + assert result_materials.data_encryption_key == initial_materials.data_encryption_key + + log_data = caplog.text + # This means that it did NOT try to decrypt the EDK. + assert "Unable to decrypt encrypted data key from" not in log_data + + +@mock_kms +def test_aws_kms_discovery_keyring_on_decrypt_no_matching_edk(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey(key_provider=MasterKeyInfo(provider_id="foo", key_info=b"bar"), encrypted_data_key=b"bar"), + ), + ) + + assert result_materials.data_encryption_key is None + + log_data = caplog.text + # This means that it did NOT try to decrypt the EDK. + assert "Unable to decrypt encrypted data key from" not in log_data + + +@mock_kms +def test_aws_kms_discovery_keyring_on_decrypt_fail(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + keyring = _AwsKmsDiscoveryKeyring(client_supplier=DefaultClientSupplier()) + + initial_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) + + result_materials = keyring.on_decrypt( + decryption_materials=initial_materials, + encrypted_data_keys=( + EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"bar"), encrypted_data_key=b"bar" + ), + ), + ) + + assert result_materials.data_encryption_key is None + + log_data = caplog.text + # This means that it did actually try to decrypt the EDK but encountered an error talking to KMS. + assert "Unable to decrypt encrypted data key from" in log_data + + +def test_try_aws_kms_decrypt_succeed(fake_generator): + encryption_context = {"foo": "bar"} + kms = boto3.client("kms", region_name=FAKE_REGION) + plaintext = b"0123" * 8 + response = kms.encrypt(KeyId=fake_generator, Plaintext=plaintext, EncryptionContext=encryption_context) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + initial_decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context=encryption_context,) + + result_materials = _try_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + decryption_materials=initial_decryption_materials, + grant_tokens=[], + encrypted_data_key=encrypted_data_key, + ) + + assert result_materials.data_encryption_key.data_key == plaintext + + generator_flags = _matching_flags( + MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + ) + + assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in generator_flags + + +@mock_kms +def test_try_aws_kms_decrypt_error(caplog): + # In this context there are no KMS CMKs, so any calls to KMS will fail. + caplog.set_level(logging.DEBUG) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + ) + + initial_decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) + + result_materials = _try_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + decryption_materials=initial_decryption_materials, + grant_tokens=[], + encrypted_data_key=encrypted_data_key, + ) + + assert result_materials.data_encryption_key is None + + log_data = caplog.text + # This means that it did actually try to decrypt the EDK but encountered an error talking to KMS. + assert "Unable to decrypt encrypted data key from" in log_data + + +def test_do_aws_kms_decrypt(fake_generator): + encryption_context = {"foo": "bar"} + kms = boto3.client("kms", region_name=FAKE_REGION) + plaintext = b"0123" * 8 + response = kms.encrypt(KeyId=fake_generator, Plaintext=plaintext, EncryptionContext=encryption_context) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + decrypted_data_key = _do_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + key_name=fake_generator, + encrypted_data_key=encrypted_data_key, + encryption_context=encryption_context, + grant_tokens=[], + ) + assert decrypted_data_key.data_key == plaintext + + +def test_do_aws_kms_decrypt_unexpected_key_id(fake_generator_and_child): + encryptor, decryptor = fake_generator_and_child + encryption_context = {"foo": "bar"} + kms = boto3.client("kms", region_name=FAKE_REGION) + plaintext = b"0123" * 8 + response = kms.encrypt(KeyId=encryptor, Plaintext=plaintext, EncryptionContext=encryption_context) + + encrypted_data_key = EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + encrypted_data_key=response["CiphertextBlob"], + ) + + with pytest.raises(DecryptKeyError) as excinfo: + _do_aws_kms_decrypt( + client_supplier=DefaultClientSupplier(), + key_name=decryptor, + encrypted_data_key=encrypted_data_key, + encryption_context=encryption_context, + grant_tokens=[], + ) + + excinfo.match(r"Decryption results from AWS KMS are for an unexpected key ID*") + + +def test_do_aws_kms_encrypt(fake_generator): + encryption_context = {"foo": "bar"} + plaintext = b"0123" * 8 + + encrypted_key = _do_aws_kms_encrypt( + client_supplier=DefaultClientSupplier(), + key_name=fake_generator, + plaintext_data_key=RawDataKey( + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), data_key=plaintext + ), + encryption_context=encryption_context, + grant_tokens=[], + ) + + kms = boto3.client("kms", region_name=FAKE_REGION) + response = kms.decrypt(CiphertextBlob=encrypted_key.encrypted_data_key, EncryptionContext=encryption_context) + + assert response["Plaintext"] == plaintext + + +def test_do_aws_kms_generate_data_key(fake_generator): + encryption_context = {"foo": "bar"} + plaintext_key, encrypted_key = _do_aws_kms_generate_data_key( + client_supplier=DefaultClientSupplier(), + key_name=fake_generator, + encryption_context=encryption_context, + algorithm=ALGORITHM, + grant_tokens=[], + ) + + kms = boto3.client("kms", region_name=FAKE_REGION) + response = kms.decrypt(CiphertextBlob=encrypted_key.encrypted_data_key, EncryptionContext=encryption_context) + + assert response["Plaintext"] == plaintext_key.data_key diff --git a/test/functional/keyrings/aws_kms/test_client_cache.py b/test/functional/keyrings/aws_kms/test_client_cache.py new file mode 100644 index 000000000..e2187b2dc --- /dev/null +++ b/test/functional/keyrings/aws_kms/test_client_cache.py @@ -0,0 +1,31 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_cache``.""" +import pytest + +from aws_encryption_sdk.keyrings.aws_kms.client_cache import ClientCache + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def test_client_cache_caches_clients(): + cache = ClientCache() + + initial_client = cache.client("us-west-2", "kms") + + test = cache.client("us-west-2", "kms") + + assert "us-west-2" in cache._cache + assert test is initial_client + + +def test_client_cache_new_client(): + cache = ClientCache() + + initial_client = cache.client("us-west-2", "kms") + + cache._cache.pop("us-west-2") + + test = cache.client("us-west-2", "kms") + + assert test is not initial_client diff --git a/test/functional/keyrings/aws_kms/test_client_suppliers.py b/test/functional/keyrings/aws_kms/test_client_suppliers.py new file mode 100644 index 000000000..eebe986c4 --- /dev/null +++ b/test/functional/keyrings/aws_kms/test_client_suppliers.py @@ -0,0 +1,113 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_suppliers``.""" +import pytest +from botocore.session import Session + +from aws_encryption_sdk.exceptions import UnknownRegionError +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import ( + AllowRegionsClientSupplier, + ClientSupplier, + DefaultClientSupplier, + DenyRegionsClientSupplier, +) + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def test_default_supplier_not_implemented(): + test = ClientSupplier() + + with pytest.raises(NotImplementedError) as excinfo: + test.client("region") + + excinfo.match("'ClientSupplier' does not implement 'client'") + + +def test_default_supplier_uses_cache(): + supplier = DefaultClientSupplier() + + region = "us-west-2" + expected = supplier._cache.client(region_name=region, service="kms") + + test = supplier.client(region) + + assert test is expected + + +def test_default_supplier_passes_session(): + botocore_session = Session() + + test = DefaultClientSupplier(botocore_session=botocore_session) + + assert test._cache._botocore_session is botocore_session + + +def test_allow_regions_supplier_passes_session(): + botocore_session = Session() + + test = AllowRegionsClientSupplier(allowed_regions=["us-west-2"], botocore_session=botocore_session) + + assert test._supplier._botocore_session is botocore_session + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(allowed_regions="foo"), id="allowed_regions is a string"), + pytest.param(dict(allowed_regions=["foo", 5]), id="allowed_regions contains invalid type"), + ), +) +def test_allow_regions_supplier_invalid_parameters(kwargs): + with pytest.raises(TypeError): + AllowRegionsClientSupplier(**kwargs) + + +def test_allow_regions_supplier_allows_allowed_region(): + test = AllowRegionsClientSupplier(allowed_regions=["us-west-2", "us-east-2"]) + + assert test.client("us-west-2") + + +def test_allow_regions_supplier_denied_not_allowed_region(): + test = AllowRegionsClientSupplier(allowed_regions=["us-west-2", "us-east-2"]) + + with pytest.raises(UnknownRegionError) as excinfo: + test.client("ap-northeast-2") + + excinfo.match("Unable to provide client for region 'ap-northeast-2'") + + +def test_deny_regions_supplier_passes_session(): + botocore_session = Session() + + test = DenyRegionsClientSupplier(denied_regions=["us-west-2"], botocore_session=botocore_session) + + assert test._supplier._botocore_session is botocore_session + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(denied_regions="foo"), id="denied_regions is a string"), + pytest.param(dict(denied_regions=["foo", 5]), id="denied_regions contains invalid type"), + ), +) +def test_deny_regions_supplier_invalid_parameters(kwargs): + with pytest.raises(TypeError): + DenyRegionsClientSupplier(**kwargs) + + +def test_deny_regions_supplier_denies_denied_region(): + test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) + + with pytest.raises(UnknownRegionError) as excinfo: + test.client("us-west-2") + + excinfo.match("Unable to provide client for region 'us-west-2'") + + +def test_deny_regions_supplier_allows_not_denied_region(): + test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) + + assert test.client("ap-northeast-2") diff --git a/test/unit/key_providers/base/test_base_master_key_provider_config.py b/test/unit/key_providers/base/test_base_master_key_provider_config.py index 0e21ded80..9604b84a8 100644 --- a/test/unit/key_providers/base/test_base_master_key_provider_config.py +++ b/test/unit/key_providers/base/test_base_master_key_provider_config.py @@ -11,7 +11,11 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Unit test suite to validate aws_encryption_sdk.key_providers.base.MasterKeyProviderConfig""" +import pytest + from aws_encryption_sdk.key_providers.base import MasterKeyProviderConfig # noqa pylint: disable=unused-import +pytestmark = [pytest.mark.unit, pytest.mark.local] + # Nothing to test at this time, but import will ensure that it exists. # If this MasterKeyProviderConfig has attributes added in the future, they should be tested here. diff --git a/test/unit/keyrings/test_aws_kms.py b/test/unit/keyrings/test_aws_kms.py new file mode 100644 index 000000000..ad2e4b526 --- /dev/null +++ b/test/unit/keyrings/test_aws_kms.py @@ -0,0 +1,198 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for ``aws_encryption_sdk.keyrings.aws_kms``.""" +import pytest + +from aws_encryption_sdk.keyrings.aws_kms import ( + KmsKeyring, + _AwsKmsDiscoveryKeyring, + _AwsKmsSingleCmkKeyring, + _region_from_key_id, +) +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import DefaultClientSupplier +from aws_encryption_sdk.keyrings.multi import MultiKeyring + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(client_supplier=None), id="client_supplier is invalid"), + pytest.param(dict(generator_key_id=5), id="generator_id is invalid"), + pytest.param(dict(child_key_ids=("foo", 5)), id="child_key_ids contains invalid values"), + pytest.param(dict(child_key_ids="some stuff"), id="child_key_ids is a string"), + pytest.param(dict(grant_tokens=("foo", 5)), id="grant_tokens contains invalid values"), + pytest.param(dict(grant_tokens="some stuff"), id="grant_tokens is a string"), + ), +) +def test_kms_keyring_invalid_parameters(kwargs): + with pytest.raises(TypeError): + KmsKeyring(**kwargs) + + +def test_kms_keyring_builds_correct_inner_keyring_multikeyring(): + generator_id = "foo" + child_id_1 = "bar" + child_id_2 = "baz" + grants = ("asdf", "fdsa") + supplier = DefaultClientSupplier() + + test = KmsKeyring( + generator_key_id=generator_id, + child_key_ids=(child_id_1, child_id_2), + grant_tokens=grants, + client_supplier=supplier, + ) + + # We specified a generator and child IDs, so the inner keyring MUST be a multikeyring + assert isinstance(test._inner_keyring, MultiKeyring) + + # Verify that the generator is configured correctly + assert isinstance(test._inner_keyring.generator, _AwsKmsSingleCmkKeyring) + assert test._inner_keyring.generator._key_id == generator_id + assert test._inner_keyring.generator._grant_tokens == grants + assert test._inner_keyring.generator._client_supplier is supplier + + # We specified two child IDs, so there MUST be exactly two children + assert len(test._inner_keyring.children) == 2 + + # Verify that the first child is configured correctly + assert isinstance(test._inner_keyring.children[0], _AwsKmsSingleCmkKeyring) + assert test._inner_keyring.children[0]._key_id == child_id_1 + assert test._inner_keyring.children[0]._grant_tokens == grants + assert test._inner_keyring.children[0]._client_supplier is supplier + + # Verify that the second child is configured correctly + assert isinstance(test._inner_keyring.children[1], _AwsKmsSingleCmkKeyring) + assert test._inner_keyring.children[1]._key_id == child_id_2 + assert test._inner_keyring.children[1]._grant_tokens == grants + assert test._inner_keyring.children[1]._client_supplier is supplier + + +def test_kms_keyring_builds_correct_inner_keyring_multikeyring_no_generator(): + test = KmsKeyring(child_key_ids=("bar", "baz")) + + # We specified child IDs, so the inner keyring MUST be a multikeyring + assert isinstance(test._inner_keyring, MultiKeyring) + + # We did not specify a generator ID, so the generator MUST NOT be set + assert test._inner_keyring.generator is None + + # We specified two child IDs, so there MUST be exactly two children + assert len(test._inner_keyring.children) == 2 + + +def test_kms_keyring_builds_correct_inner_keyring_multikeyring_no_children(): + test = KmsKeyring(generator_key_id="foo") + + # We specified a generator ID, so the inner keyring MUST be a multikeyring + assert isinstance(test._inner_keyring, MultiKeyring) + + # We specified a generator ID, so the generator MUST be set + assert test._inner_keyring.generator is not None + + # We did not specify any child IDs, so the multikeyring MUST NOT contain any children + assert len(test._inner_keyring.children) == 0 + + +def test_kms_keyring_builds_correct_inner_keyring_discovery(): + grants = ("asdf", "fdas") + supplier = DefaultClientSupplier() + + test = KmsKeyring(grant_tokens=grants, client_supplier=supplier) + + # We specified neither a generator nor children, so the inner keyring MUST be a discovery keyring + assert isinstance(test._inner_keyring, _AwsKmsDiscoveryKeyring) + + # Verify that the discovery keyring is configured correctly + assert test._inner_keyring._grant_tokens == grants + assert test._inner_keyring._client_supplier is supplier + + +def test_kms_keyring_on_encrypt(mocker): + mock_keyring = mocker.Mock() + + keyring = KmsKeyring() + keyring._inner_keyring = mock_keyring + + test = keyring.on_encrypt(encryption_materials=mocker.sentinel.encryption_materials) + + # on_encrypt MUST be a straight passthrough to the inner keyring + assert mock_keyring.on_encrypt.called_once_with(encryption_materials=mocker.sentinel.encryption_materials) + assert test is mock_keyring.on_encrypt.return_value + + +def test_kms_keyring_on_decrypt(mocker): + mock_keyring = mocker.Mock() + + keyring = KmsKeyring() + keyring._inner_keyring = mock_keyring + + test = keyring.on_decrypt( + decryption_materials=mocker.sentinel.decryption_materials, + encrypted_data_keys=mocker.sentinel.encrypted_data_keys, + ) + + # on_decrypt MUST be a straight passthrough to the inner keyring + assert mock_keyring.on_decrypt.called_once_with( + decryption_materials=mocker.sentinel.decryption_materials, + encrypted_data_keys=mocker.sentinel.encrypted_data_keys, + ) + assert test is mock_keyring.on_decrypt.return_value + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(key_id=None, client_supplier=DefaultClientSupplier()), id="key_id is invalid"), + pytest.param(dict(key_id="foo", client_supplier=None), id="client_supplier is invalid"), + pytest.param( + dict(key_id="foo", client_supplier=DefaultClientSupplier(), grant_tokens=("bar", 5)), + id="grant_tokens contains invalid values", + ), + pytest.param( + dict(key_id="foo", client_supplier=DefaultClientSupplier(), grant_tokens="some stuff"), + id="grant_tokens is a string", + ), + ), +) +def test_aws_kms_single_cmk_keyring_invalid_parameters(kwargs): + with pytest.raises(TypeError): + _AwsKmsSingleCmkKeyring(**kwargs) + + +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(client_supplier=None), id="client_supplier is invalid"), + pytest.param( + dict(client_supplier=DefaultClientSupplier(), grant_tokens=("bar", 5)), + id="grant_tokens contains invalid values", + ), + pytest.param( + dict(client_supplier=DefaultClientSupplier(), grant_tokens="some stuff"), id="grant_tokens is a string", + ), + ), +) +def test_aws_kms_discovery_keyring_invalid_parameters(kwargs): + with pytest.raises(TypeError): + _AwsKmsDiscoveryKeyring(**kwargs) + + +@pytest.mark.parametrize( + "key_id, expected", + ( + pytest.param("foo", None, id="invalid format"), + pytest.param("alias/foo", None, id="alias name"), + pytest.param("880e7651-6f87-4c68-b84b-3220da5a7a02", None, id="key ID"), + pytest.param("arn:aws:kms:moon-base-1:111222333444:alias/foo", "moon-base-1", id="alias ARN"), + pytest.param( + "arn:aws:kms:moon-base-1:111222333444:key/880e7651-6f87-4c68-b84b-3220da5a7a02", "moon-base-1", id="CMK ARN" + ), + ), +) +def test_region_from_key_id(key_id, expected): + actual = _region_from_key_id(key_id=key_id) + + assert actual == expected diff --git a/tox.ini b/tox.ini index 1df161ccd..d9d60d756 100644 --- a/tox.ini +++ b/tox.ini @@ -232,9 +232,11 @@ commands = {[testenv:isort]commands} -c [testenv:autoformat] basepython = python3 deps = + {[testenv:isort-seed]deps} {[testenv:blacken]deps} {[testenv:isort]deps} commands = + {[testenv:isort-seed]commands} {[testenv:blacken]commands} {[testenv:isort]commands} From e56b3ed0dbf9ee6e4264ad9a148e14e970591c32 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 4 Mar 2020 16:27:50 -0800 Subject: [PATCH 28/64] chore: add helpers for integration tests --- test/integration/integration_test_utils.py | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index b65d93570..37e0243b7 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -14,12 +14,15 @@ import os import botocore.session +import pytest from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring AWS_KMS_KEY_ID = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID" _KMS_MKP = None _KMS_MKP_BOTO = None +_KMS_KEYRING = None def get_cmk_arn(): @@ -37,7 +40,7 @@ def get_cmk_arn(): def setup_kms_master_key_provider(cache=True): - """Reads the test_values config file and builds the requested KMS Master Key Provider.""" + """Build an AWS KMS Master Key Provider.""" global _KMS_MKP # pylint: disable=global-statement if cache and _KMS_MKP is not None: return _KMS_MKP @@ -53,7 +56,7 @@ def setup_kms_master_key_provider(cache=True): def setup_kms_master_key_provider_with_botocore_session(cache=True): - """Reads the test_values config file and builds the requested KMS Master Key Provider with botocore_session.""" + """Build an AWS KMS Master Key Provider with an explicit botocore_session.""" global _KMS_MKP_BOTO # pylint: disable=global-statement if cache and _KMS_MKP_BOTO is not None: return _KMS_MKP_BOTO @@ -66,3 +69,29 @@ def setup_kms_master_key_provider_with_botocore_session(cache=True): _KMS_MKP_BOTO = kms_master_key_provider return kms_master_key_provider + + +def build_aws_kms_keyring(generate=True, cache=True): + """Build an AWS KMS keyring.""" + global _KMS_KEYRING # pylint: disable=global-statement + if cache and _KMS_KEYRING is not None: + return _KMS_KEYRING + + cmk_arn = get_cmk_arn() + + if generate: + kwargs = dict(generator_key_id=cmk_arn) + else: + kwargs = dict(child_key_ids=[cmk_arn]) + + keyring = KmsKeyring(**kwargs) + + if cache: + _KMS_KEYRING = keyring + + return keyring + + +@pytest.fixture +def aws_kms_keyring(): + return build_aws_kms_keyring() From f7801c7c15f96d07c064dc43057bc220d89eb18c Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 4 Mar 2020 16:28:17 -0800 Subject: [PATCH 29/64] chore: refactor end-to-end integration tests with parametrization --- test/integration/test_client.py | 491 ++++++-------------------------- 1 file changed, 82 insertions(+), 409 deletions(-) diff --git a/test/integration/test_client.py b/test/integration/test_client.py index 2b66fb408..7f5c1e983 100644 --- a/test/integration/test_client.py +++ b/test/integration/test_client.py @@ -5,17 +5,11 @@ import logging import pytest -from botocore.exceptions import BotoCoreError import aws_encryption_sdk from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX, Algorithm -from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider -from .integration_test_utils import ( - get_cmk_arn, - setup_kms_master_key_provider, - setup_kms_master_key_provider_with_botocore_session, -) +from .integration_test_utils import build_aws_kms_keyring, get_cmk_arn, setup_kms_master_key_provider pytestmark = [pytest.mark.integ] @@ -33,411 +27,90 @@ } -def test_encrypt_verify_user_agent_kms_master_key_provider(caplog): - caplog.set_level(level=logging.DEBUG) - mkp = setup_kms_master_key_provider() - mk = mkp.master_key(get_cmk_arn()) - - mk.generate_data_key(algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context={}) - - assert USER_AGENT_SUFFIX in caplog.text - - -def test_encrypt_verify_user_agent_kms_master_key(caplog): +@pytest.mark.parametrize( + "kwargs", + ( + pytest.param(dict(key_provider=setup_kms_master_key_provider()), id="AWS KMS master key provider"), + pytest.param( + dict(key_provider=setup_kms_master_key_provider().master_key(get_cmk_arn())), id="AWS KMS master key" + ), + pytest.param(dict(keyring=build_aws_kms_keyring()), id="AWS KMS keyring"), + ), +) +def test_encrypt_verify_user_agent_in_logs(caplog, kwargs): caplog.set_level(level=logging.DEBUG) - mk = KMSMasterKey(key_id=get_cmk_arn()) - mk.generate_data_key(algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, encryption_context={}) + aws_encryption_sdk.encrypt(source=VALUES["plaintext_128"], **kwargs) assert USER_AGENT_SUFFIX in caplog.text -def test_remove_bad_client(): - test = KMSMasterKeyProvider() - fake_region = "us-fakey-12" - test.add_regional_client(fake_region) - - with pytest.raises(BotoCoreError): - test._regional_clients[fake_region].list_keys() - - assert fake_region not in test._regional_clients - - -def test_regional_client_does_not_modify_botocore_session(caplog): - mkp = setup_kms_master_key_provider_with_botocore_session() - fake_region = "us-fakey-12" - - assert mkp.config.botocore_session.get_config_variable("region") != fake_region - mkp.add_regional_client(fake_region) - assert mkp.config.botocore_session.get_config_variable("region") != fake_region - - -class TestKMSThickClientIntegration(object): - @pytest.fixture(autouse=True) - def apply_fixtures(self): - self.kms_master_key_provider = setup_kms_master_key_provider() - - def test_encryption_cycle_default_algorithm_framed_stream(self): - """Test that the enrypt/decrypt cycle completes successfully - for a framed message using the default algorithm. - """ - with aws_encryption_sdk.stream( - source=io.BytesIO(VALUES["plaintext_128"]), - key_provider=self.kms_master_key_provider, - mode="e", - encryption_context=VALUES["encryption_context"], - ) as encryptor: - ciphertext = encryptor.read() - header_1 = encryptor.header - with aws_encryption_sdk.stream( - source=io.BytesIO(ciphertext), key_provider=self.kms_master_key_provider, mode="d" - ) as decryptor: - plaintext = decryptor.read() - header_2 = decryptor.header - assert plaintext == VALUES["plaintext_128"] - assert header_1.encryption_context == header_2.encryption_context - - def test_encryption_cycle_default_algorithm_framed_stream_many_lines(self): - """Test that the enrypt/decrypt cycle completes successfully - for a framed message with many frames using the default algorithm. - """ - ciphertext = b"" - with aws_encryption_sdk.stream( - source=io.BytesIO(VALUES["plaintext_128"] * 10), - key_provider=self.kms_master_key_provider, - mode="e", - encryption_context=VALUES["encryption_context"], - frame_length=128, - ) as encryptor: - for chunk in encryptor: - ciphertext += chunk - header_1 = encryptor.header - plaintext = b"" - with aws_encryption_sdk.stream( - source=io.BytesIO(ciphertext), key_provider=self.kms_master_key_provider, mode="d" - ) as decryptor: - for chunk in decryptor: - plaintext += chunk - header_2 = decryptor.header - assert plaintext == VALUES["plaintext_128"] * 10 - assert header_1.encryption_context == header_2.encryption_context - - def test_encryption_cycle_default_algorithm_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_default_algorithm_non_framed_no_encryption_context(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], key_provider=self.kms_master_key_provider, frame_length=0 - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_default_algorithm_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_default_algorithm_multiple_frames(self): - """Test that the enrypt/decrypt cycle completes successfully - for a framed message with multiple frames using the - default algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"] * 100, - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] * 100 - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the aes_128_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the aes_128_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the aes_192_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the aes_192_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully - for a single frame message using the aes_256_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully - for a non-framed message using the aes_256_gcm_iv12_tag16 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a - single frame message using the aes_128_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a - non-framed message using the aes_128_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a - single frame message using the aes_192_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a - non-framed message using the aes_192_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a - single frame message using the aes_256_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a - non-framed message using the aes_256_gcm_iv12_tag16_hkdf_sha256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - frame message using the aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - block message using the aes_128_gcm_iv12_tag16_hkdf_sha256_ecdsa_p256 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - frame message using the aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - block message using the aes_192_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_192_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_single_frame(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - frame message using the aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=1024, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] - - def test_encryption_cycle_aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384_non_framed(self): - """Test that the enrypt/decrypt cycle completes successfully for a single - block message using the aes_256_gcm_iv12_tag16_hkdf_sha384_ecdsa_p384 - algorithm. - """ - ciphertext, _ = aws_encryption_sdk.encrypt( - source=VALUES["plaintext_128"], - key_provider=self.kms_master_key_provider, - encryption_context=VALUES["encryption_context"], - frame_length=0, - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, - ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=self.kms_master_key_provider) - assert plaintext == VALUES["plaintext_128"] +@pytest.mark.parametrize("frame_size", (pytest.param(0, id="unframed"), pytest.param(1024, id="1024 byte frame"))) +@pytest.mark.parametrize("algorithm_suite", Algorithm) +@pytest.mark.parametrize( + "encrypt_key_provider_kwargs", + ( + pytest.param(dict(key_provider=setup_kms_master_key_provider()), id="encrypt with MKP"), + pytest.param(dict(keyring=build_aws_kms_keyring()), id="encrypt with keyring"), + ), +) +@pytest.mark.parametrize( + "decrypt_key_provider_kwargs", + ( + pytest.param(dict(key_provider=setup_kms_master_key_provider()), id="decrypt with MKP"), + pytest.param(dict(keyring=build_aws_kms_keyring()), id="decrypt with keyring"), + ), +) +@pytest.mark.parametrize( + "encryption_context", + ( + pytest.param({}, id="empty encryption context"), + pytest.param(VALUES["encryption_context"], id="non-empty encryption context"), + ), +) +@pytest.mark.parametrize( + "plaintext", + ( + pytest.param(VALUES["plaintext_128"], id="plaintext smaller than frame"), + pytest.param(VALUES["plaintext_128"] * 100, id="plaintext larger than frame"), + ), +) +def test_encrypt_decrypt_cycle_aws_kms( + frame_size, algorithm_suite, encrypt_key_provider_kwargs, decrypt_key_provider_kwargs, encryption_context, plaintext +): + ciphertext, _ = aws_encryption_sdk.encrypt( + source=plaintext, + encryption_context=encryption_context, + frame_length=frame_size, + algorithm=algorithm_suite, + **encrypt_key_provider_kwargs + ) + decrypted, _ = aws_encryption_sdk.decrypt(source=ciphertext, **decrypt_key_provider_kwargs) + assert decrypted == plaintext + + +@pytest.mark.parametrize( + "plaintext", + ( + pytest.param(VALUES["plaintext_128"], id="plaintext smaller than frame"), + pytest.param(VALUES["plaintext_128"] * 100, id="plaintext larger than frame"), + ), +) +def test_encrypt_decrypt_cycle_aws_kms_streaming(plaintext): + keyring = build_aws_kms_keyring() + ciphertext = b"" + with aws_encryption_sdk.stream( + source=io.BytesIO(plaintext), keyring=keyring, mode="e", encryption_context=VALUES["encryption_context"], + ) as encryptor: + for chunk in encryptor: + ciphertext += chunk + header_1 = encryptor.header + + decrypted = b"" + with aws_encryption_sdk.stream(source=io.BytesIO(ciphertext), keyring=keyring, mode="d") as decryptor: + for chunk in decryptor: + decrypted += chunk + header_2 = decryptor.header + + assert decrypted == plaintext + assert header_1.encryption_context == header_2.encryption_context From 16b652801f58337946f9e06e715afb355ce5138d Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 4 Mar 2020 16:28:31 -0800 Subject: [PATCH 30/64] chore: add integration tests for AWS KMS keyrings --- test/integration/key_providers/__init__.py | 3 ++ test/integration/key_providers/test_kms.py | 31 ++++++++++++ test/integration/keyrings/__init__.py | 3 ++ test/integration/keyrings/aws_kms/__init__.py | 3 ++ .../keyrings/aws_kms/test_client_cache.py | 48 +++++++++++++++++++ test/requirements.txt | 1 + 6 files changed, 89 insertions(+) create mode 100644 test/integration/key_providers/__init__.py create mode 100644 test/integration/key_providers/test_kms.py create mode 100644 test/integration/keyrings/__init__.py create mode 100644 test/integration/keyrings/aws_kms/__init__.py create mode 100644 test/integration/keyrings/aws_kms/test_client_cache.py diff --git a/test/integration/key_providers/__init__.py b/test/integration/key_providers/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/integration/key_providers/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/integration/key_providers/test_kms.py b/test/integration/key_providers/test_kms.py new file mode 100644 index 000000000..59c699f70 --- /dev/null +++ b/test/integration/key_providers/test_kms.py @@ -0,0 +1,31 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Integration tests for ``aws_encryption_sdk.key_provider.kms``.""" +from test.integration.integration_test_utils import setup_kms_master_key_provider_with_botocore_session + +import pytest +from botocore.exceptions import BotoCoreError + +from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider + +pytestmark = [pytest.mark.integ] + + +def test_remove_bad_client(): + test = KMSMasterKeyProvider() + fake_region = "us-fakey-12" + test.add_regional_client(fake_region) + + with pytest.raises(BotoCoreError): + test._regional_clients[fake_region].list_keys() + + assert fake_region not in test._regional_clients + + +def test_regional_client_does_not_modify_botocore_session(caplog): + mkp = setup_kms_master_key_provider_with_botocore_session() + fake_region = "us-fakey-12" + + assert mkp.config.botocore_session.get_config_variable("region") != fake_region + mkp.add_regional_client(fake_region) + assert mkp.config.botocore_session.get_config_variable("region") != fake_region diff --git a/test/integration/keyrings/__init__.py b/test/integration/keyrings/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/integration/keyrings/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/integration/keyrings/aws_kms/__init__.py b/test/integration/keyrings/aws_kms/__init__.py new file mode 100644 index 000000000..d548f9b1f --- /dev/null +++ b/test/integration/keyrings/aws_kms/__init__.py @@ -0,0 +1,3 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Dummy stub to make linters work better.""" diff --git a/test/integration/keyrings/aws_kms/test_client_cache.py b/test/integration/keyrings/aws_kms/test_client_cache.py new file mode 100644 index 000000000..61b4bf61b --- /dev/null +++ b/test/integration/keyrings/aws_kms/test_client_cache.py @@ -0,0 +1,48 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Integration tests for ``aws_encryption_sdk.keyrings.aws_kms.client_cache``.""" +import pytest +from botocore.exceptions import BotoCoreError +from botocore.session import Session + +from aws_encryption_sdk.keyrings.aws_kms.client_cache import ClientCache + +pytestmark = [pytest.mark.integ] + + +def test_client_cache_removes_bad_client(): + cache = ClientCache() + fake_region = "us-fake-1" + + initial_client = cache.client(fake_region, "kms") + + assert fake_region in cache._cache + + with pytest.raises(BotoCoreError): + initial_client.encrypt(KeyId="foo", Plaintext=b"bar") + + assert fake_region not in cache._cache + + +def test_regional_client_does_not_modify_botocore_session(): + cache = ClientCache(botocore_session=Session()) + fake_region = "us-fake-1" + + assert cache._botocore_session.get_config_variable("region") != fake_region + cache.client(fake_region, "kms") + assert cache._botocore_session.get_config_variable("region") != fake_region + + +def test_client_cache_remove_bad_client_when_already_removed(): + cache = ClientCache() + fake_region = "us-fake-1" + + initial_client = cache.client(fake_region, "kms") + + assert fake_region in cache._cache + del cache._cache[fake_region] + + with pytest.raises(BotoCoreError): + initial_client.encrypt(KeyId="foo", Plaintext=b"bar") + + assert fake_region not in cache._cache diff --git a/test/requirements.txt b/test/requirements.txt index 152b5dbf4..ff9311dc4 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -2,3 +2,4 @@ mock pytest>=3.3.1 pytest-cov pytest-mock +moto>=1.3.14 From 929c5b98860ec28ddb311c1eb33b06cd6226fa3c Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 5 Mar 2020 20:15:15 -0800 Subject: [PATCH 31/64] feat: convert client suppliers to callables --- src/aws_encryption_sdk/keyrings/aws_kms/__init__.py | 6 +++--- .../keyrings/aws_kms/client_suppliers.py | 12 ++++++------ .../keyrings/aws_kms/test_client_suppliers.py | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 5cec52bdc..788c055af 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -300,7 +300,7 @@ def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryptio """ region = _region_from_key_id(encrypted_data_key.key_provider.key_info.decode("utf-8")) - client = client_supplier.client(region) + client = client_supplier(region) response = client.decrypt( CiphertextBlob=encrypted_data_key.encrypted_data_key, EncryptionContext=encryption_context, @@ -324,7 +324,7 @@ def _do_aws_kms_encrypt(client_supplier, key_name, plaintext_data_key, encryptio Any errors encountered are passed up the chain without comment. """ region = _region_from_key_id(key_name) - client = client_supplier.client(region) + client = client_supplier(region) response = client.encrypt( KeyId=key_name, Plaintext=plaintext_data_key.data_key, @@ -347,7 +347,7 @@ def _do_aws_kms_generate_data_key(client_supplier, key_name, encryption_context, """ region = _region_from_key_id(key_name) - client = client_supplier.client(region) + client = client_supplier(region) response = client.generate_data_key( KeyId=key_name, NumberOfBytes=algorithm.kdf_input_len, diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py index 7fcdb4fc5..e701b386a 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -35,13 +35,13 @@ class ClientSupplier(object): """ - def client(self, region_name): + def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient """Return a client for the requested region. :rtype: BaseClient """ - raise NotImplementedError("'ClientSupplier' does not implement 'client'") + raise NotImplementedError("'ClientSupplier' is not callable") @attr.s @@ -61,7 +61,7 @@ def __attrs_post_init__(self): """Set up internal client cache.""" self._cache = ClientCache(botocore_session=self._botocore_session) - def client(self, region_name): + def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient """Return a client for the requested region. @@ -100,7 +100,7 @@ def client(self, region_name): if region_name not in self.allowed_regions: raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) - return self._supplier.client(region_name) + return self._supplier(region_name) @attr.s @@ -123,7 +123,7 @@ def __attrs_post_init__(self): """Set up internal client supplier.""" self._supplier = DefaultClientSupplier(botocore_session=self._botocore_session) - def client(self, region_name): + def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient """Return a client for the requested region. @@ -133,4 +133,4 @@ def client(self, region_name): if region_name in self.denied_regions: raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) - return self._supplier.client(region_name) + return self._supplier(region_name) diff --git a/test/functional/keyrings/aws_kms/test_client_suppliers.py b/test/functional/keyrings/aws_kms/test_client_suppliers.py index eebe986c4..c2c04287d 100644 --- a/test/functional/keyrings/aws_kms/test_client_suppliers.py +++ b/test/functional/keyrings/aws_kms/test_client_suppliers.py @@ -19,7 +19,7 @@ def test_default_supplier_not_implemented(): test = ClientSupplier() with pytest.raises(NotImplementedError) as excinfo: - test.client("region") + test("region") excinfo.match("'ClientSupplier' does not implement 'client'") @@ -30,7 +30,7 @@ def test_default_supplier_uses_cache(): region = "us-west-2" expected = supplier._cache.client(region_name=region, service="kms") - test = supplier.client(region) + test = supplier(region) assert test is expected @@ -66,14 +66,14 @@ def test_allow_regions_supplier_invalid_parameters(kwargs): def test_allow_regions_supplier_allows_allowed_region(): test = AllowRegionsClientSupplier(allowed_regions=["us-west-2", "us-east-2"]) - assert test.client("us-west-2") + assert test("us-west-2") def test_allow_regions_supplier_denied_not_allowed_region(): test = AllowRegionsClientSupplier(allowed_regions=["us-west-2", "us-east-2"]) with pytest.raises(UnknownRegionError) as excinfo: - test.client("ap-northeast-2") + test("ap-northeast-2") excinfo.match("Unable to provide client for region 'ap-northeast-2'") @@ -102,7 +102,7 @@ def test_deny_regions_supplier_denies_denied_region(): test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) with pytest.raises(UnknownRegionError) as excinfo: - test.client("us-west-2") + test("us-west-2") excinfo.match("Unable to provide client for region 'us-west-2'") @@ -110,4 +110,4 @@ def test_deny_regions_supplier_denies_denied_region(): def test_deny_regions_supplier_allows_not_denied_region(): test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) - assert test.client("ap-northeast-2") + assert test("ap-northeast-2") From 2bac0de51017e9cb76e73dca0f3cd7c47b29afc7 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 15:24:30 -0800 Subject: [PATCH 32/64] feat: convert AWS KMS keyring and helpers to require a callable rather than an instance of ClientSupplier --- .../keyrings/aws_kms/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 788c055af..1d580eee1 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -9,7 +9,7 @@ import attr import six -from attr.validators import deep_iterable, instance_of, optional +from attr.validators import deep_iterable, instance_of, is_callable, optional from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError from aws_encryption_sdk.identifiers import AlgorithmSuite @@ -23,6 +23,7 @@ try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Any, Dict, Iterable, Union # noqa pylint: disable=unused-import + from .client_suppliers import ClientSupplierType # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -77,7 +78,7 @@ class KmsKeyring(Keyring): :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) """ - _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=instance_of(ClientSupplier)) + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=is_callable()) _generator_key_id = attr.ib(default=None, validator=optional(instance_of(six.string_types))) _child_key_ids = attr.ib( default=attr.Factory(tuple), @@ -154,7 +155,7 @@ class _AwsKmsSingleCmkKeyring(Keyring): """ _key_id = attr.ib(validator=instance_of(six.string_types)) - _client_supplier = attr.ib(validator=instance_of(ClientSupplier)) + _client_supplier = attr.ib(validator=is_callable()) _grant_tokens = attr.ib( default=attr.Factory(tuple), validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), @@ -231,7 +232,7 @@ class _AwsKmsDiscoveryKeyring(Keyring): :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) """ - _client_supplier = attr.ib(validator=instance_of(ClientSupplier)) + _client_supplier = attr.ib(validator=is_callable()) _grant_tokens = attr.ib( default=attr.Factory(tuple), validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), @@ -261,7 +262,7 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): def _try_aws_kms_decrypt(client_supplier, decryption_materials, grant_tokens, encrypted_data_key): - # type: (ClientSupplier, DecryptionMaterials, Iterable[str], EncryptedDataKey) -> DecryptionMaterials + # type: (ClientSupplierType, DecryptionMaterials, Iterable[str], EncryptedDataKey) -> DecryptionMaterials """Attempt to call ``kms:Decrypt`` and return the resulting plaintext data key. Any errors encountered are caught and logged. @@ -291,7 +292,7 @@ def _try_aws_kms_decrypt(client_supplier, decryption_materials, grant_tokens, en def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryption_context, grant_tokens): - # type: (ClientSupplier, str, EncryptedDataKey, Dict[str, str], Iterable[str]) -> RawDataKey + # type: (ClientSupplierType, str, EncryptedDataKey, Dict[str, str], Iterable[str]) -> RawDataKey """Attempt to call ``kms:Decrypt`` and return the resulting plaintext data key. Any errors encountered are passed up the chain without comment. @@ -318,7 +319,7 @@ def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryptio def _do_aws_kms_encrypt(client_supplier, key_name, plaintext_data_key, encryption_context, grant_tokens): - # type: (ClientSupplier, str, RawDataKey, Dict[str, str], Iterable[str]) -> EncryptedDataKey + # type: (ClientSupplierType, str, RawDataKey, Dict[str, str], Iterable[str]) -> EncryptedDataKey """Attempt to call ``kms:Encrypt`` and return the resulting encrypted data key. Any errors encountered are passed up the chain without comment. @@ -338,7 +339,7 @@ def _do_aws_kms_encrypt(client_supplier, key_name, plaintext_data_key, encryptio def _do_aws_kms_generate_data_key(client_supplier, key_name, encryption_context, algorithm, grant_tokens): - # type: (ClientSupplier, str, Dict[str, str], AlgorithmSuite, Iterable[str]) -> (RawDataKey, EncryptedDataKey) + # type: (ClientSupplierType, str, Dict[str, str], AlgorithmSuite, Iterable[str]) -> (RawDataKey, EncryptedDataKey) """Attempt to call ``kms:GenerateDataKey`` and return the resulting plaintext and encrypted data keys. Any errors encountered are passed up the chain without comment. From 7265c84be3d4bf6402439a0fc251ac210441b428 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 15:41:49 -0800 Subject: [PATCH 33/64] feat: restructure client supplier configuration --- .../keyrings/aws_kms/__init__.py | 6 ++- .../keyrings/aws_kms/client_suppliers.py | 51 +++++++++---------- .../keyrings/aws_kms/test_client_suppliers.py | 45 +++++++--------- 3 files changed, 46 insertions(+), 56 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 1d580eee1..1cfbd16da 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -19,10 +19,12 @@ from aws_encryption_sdk.materials_managers import DecryptionMaterials, EncryptionMaterials from aws_encryption_sdk.structures import EncryptedDataKey, KeyringTrace, KeyringTraceFlag, MasterKeyInfo, RawDataKey -from .client_suppliers import ClientSupplier, DefaultClientSupplier +from .client_suppliers import DefaultClientSupplier + +from .client_suppliers import ClientSupplier # noqa - only used in docstring params; this confuses flake8 try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Any, Dict, Iterable, Union # noqa pylint: disable=unused-import + from typing import Dict, Iterable, Union # noqa pylint: disable=unused-import from .client_suppliers import ClientSupplierType # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py index e701b386a..08b1a48f7 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -9,9 +9,8 @@ import attr import six -from attr.validators import deep_iterable, instance_of +from attr.validators import deep_iterable, instance_of, optional from botocore.client import BaseClient -from botocore.session import Session as BotocoreSession from aws_encryption_sdk.exceptions import UnknownRegionError from aws_encryption_sdk.internal.validators import value_is_not_a_string @@ -19,13 +18,21 @@ from .client_cache import ClientCache try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Any, Dict, Union # noqa pylint: disable=unused-import + from typing import Callable, Union # noqa pylint: disable=unused-import + + ClientSupplierType = Callable[[Union[None, str]], BaseClient] except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass _LOGGER = logging.getLogger(__name__) -__all__ = ("ClientSupplier", "DefaultClientSupplier", "AllowRegionsClientSupplier", "DenyRegionsClientSupplier") +__all__ = ( + "ClientSupplier", + "ClientSupplierType", + "DefaultClientSupplier", + "AllowRegionsClientSupplier", + "DenyRegionsClientSupplier", +) class ClientSupplier(object): @@ -55,11 +62,7 @@ class DefaultClientSupplier(ClientSupplier): :type botocore_session: botocore.session.Session """ - _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) - - def __attrs_post_init__(self): - """Set up internal client cache.""" - self._cache = ClientCache(botocore_session=self._botocore_session) + _client_cache = attr.ib(default=attr.Factory(ClientCache), validator=instance_of(ClientCache)) def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient @@ -67,7 +70,7 @@ def __call__(self, region_name): :rtype: BaseClient """ - return self._cache.client(region_name=region_name, service="kms") + return self._client_cache.client(region_name=region_name, service="kms") @attr.s @@ -77,20 +80,17 @@ class AllowRegionsClientSupplier(ClientSupplier): .. versionadded:: 1.5.0 :param List[str] allowed_regions: Regions to allow - :param botocore_session: botocore session to use when creating clients (optional) - :type botocore_session: botocore.session.Session + :param ClientSupplier client_supplier: Client supplier to wrap (optional) """ allowed_regions = attr.ib( validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) ) - _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) - - def __attrs_post_init__(self): - """Set up internal client supplier.""" - self._supplier = DefaultClientSupplier(botocore_session=self._botocore_session) + _client_supplier = attr.ib( + default=attr.Factory(DefaultClientSupplier), validator=optional(instance_of(ClientSupplier)) + ) - def client(self, region_name): + def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient """Return a client for the requested region. @@ -100,7 +100,7 @@ def client(self, region_name): if region_name not in self.allowed_regions: raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) - return self._supplier(region_name) + return self._client_supplier(region_name) @attr.s @@ -110,18 +110,15 @@ class DenyRegionsClientSupplier(ClientSupplier): .. versionadded:: 1.5.0 :param List[str] denied_regions: Regions to deny - :param botocore_session: Botocore session to use when creating clients (optional) - :type botocore_session: botocore.session.Session + :param ClientSupplier client_supplier: Client supplier to wrap (optional) """ denied_regions = attr.ib( validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) ) - _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) - - def __attrs_post_init__(self): - """Set up internal client supplier.""" - self._supplier = DefaultClientSupplier(botocore_session=self._botocore_session) + _client_supplier = attr.ib( + default=attr.Factory(DefaultClientSupplier), validator=optional(instance_of(ClientSupplier)) + ) def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient @@ -133,4 +130,4 @@ def __call__(self, region_name): if region_name in self.denied_regions: raise UnknownRegionError("Unable to provide client for region '{}'".format(region_name)) - return self._supplier(region_name) + return self._client_supplier(region_name) diff --git a/test/functional/keyrings/aws_kms/test_client_suppliers.py b/test/functional/keyrings/aws_kms/test_client_suppliers.py index c2c04287d..bede8f0ab 100644 --- a/test/functional/keyrings/aws_kms/test_client_suppliers.py +++ b/test/functional/keyrings/aws_kms/test_client_suppliers.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 """Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_suppliers``.""" import pytest -from botocore.session import Session from aws_encryption_sdk.exceptions import UnknownRegionError from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import ( @@ -21,36 +20,20 @@ def test_default_supplier_not_implemented(): with pytest.raises(NotImplementedError) as excinfo: test("region") - excinfo.match("'ClientSupplier' does not implement 'client'") + excinfo.match("'ClientSupplier' is not callable") def test_default_supplier_uses_cache(): supplier = DefaultClientSupplier() region = "us-west-2" - expected = supplier._cache.client(region_name=region, service="kms") + expected = supplier._client_cache.client(region_name=region, service="kms") test = supplier(region) assert test is expected -def test_default_supplier_passes_session(): - botocore_session = Session() - - test = DefaultClientSupplier(botocore_session=botocore_session) - - assert test._cache._botocore_session is botocore_session - - -def test_allow_regions_supplier_passes_session(): - botocore_session = Session() - - test = AllowRegionsClientSupplier(allowed_regions=["us-west-2"], botocore_session=botocore_session) - - assert test._supplier._botocore_session is botocore_session - - @pytest.mark.parametrize( "kwargs", ( @@ -78,14 +61,6 @@ def test_allow_regions_supplier_denied_not_allowed_region(): excinfo.match("Unable to provide client for region 'ap-northeast-2'") -def test_deny_regions_supplier_passes_session(): - botocore_session = Session() - - test = DenyRegionsClientSupplier(denied_regions=["us-west-2"], botocore_session=botocore_session) - - assert test._supplier._botocore_session is botocore_session - - @pytest.mark.parametrize( "kwargs", ( @@ -111,3 +86,19 @@ def test_deny_regions_supplier_allows_not_denied_region(): test = DenyRegionsClientSupplier(denied_regions=["us-west-2", "us-east-2"]) assert test("ap-northeast-2") + + +def test_allow_deny_nested_supplier(): + test_allow = AllowRegionsClientSupplier( + allowed_regions=["us-west-2", "us-east-2"], client_supplier=DefaultClientSupplier() + ) + test_deny = DenyRegionsClientSupplier(denied_regions=["us-west-2"], client_supplier=test_allow) + + # test_allow allows us-west-2 + test_allow("us-west-2") + + # test_deny denies us-west-2 even though its internal supplier (test_allow) allows it + with pytest.raises(UnknownRegionError) as excinfo: + test_deny("us-west-2") + + excinfo.match("Unable to provide client for region 'us-west-2'") From 6c2881a26fb74c037d62e0e4152710ca73e6df6c Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 15:43:40 -0800 Subject: [PATCH 34/64] fix: client suppliers should only require a callable not specifically a ClientSupplier --- .../keyrings/aws_kms/client_suppliers.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py index 08b1a48f7..5c419c506 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -9,7 +9,7 @@ import attr import six -from attr.validators import deep_iterable, instance_of, optional +from attr.validators import deep_iterable, instance_of, is_callable, optional from botocore.client import BaseClient from aws_encryption_sdk.exceptions import UnknownRegionError @@ -86,9 +86,7 @@ class AllowRegionsClientSupplier(ClientSupplier): allowed_regions = attr.ib( validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) ) - _client_supplier = attr.ib( - default=attr.Factory(DefaultClientSupplier), validator=optional(instance_of(ClientSupplier)) - ) + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=optional(is_callable())) def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient @@ -116,9 +114,7 @@ class DenyRegionsClientSupplier(ClientSupplier): denied_regions = attr.ib( validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string) ) - _client_supplier = attr.ib( - default=attr.Factory(DefaultClientSupplier), validator=optional(instance_of(ClientSupplier)) - ) + _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=optional(is_callable())) def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient From 4686fbf93d571432ff25835272945b8aac2f3178 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 17:02:49 -0800 Subject: [PATCH 35/64] feat: restructure default client supplier to pass through config to cache rather than accepting a cache --- .../keyrings/aws_kms/client_cache.py | 11 ++----- .../keyrings/aws_kms/client_suppliers.py | 33 +++++++++++++++++-- .../keyrings/aws_kms/test_client_cache.py | 6 ++-- .../keyrings/aws_kms/test_client_suppliers.py | 12 +++++++ .../keyrings/aws_kms/test_client_cache.py | 7 ++-- 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py index e809ab894..c09eeab15 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py @@ -16,8 +16,6 @@ from botocore.exceptions import BotoCoreError from botocore.session import Session as BotocoreSession -from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX - try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Dict # noqa pylint: disable=unused-import except ImportError: # pragma: no cover @@ -36,17 +34,14 @@ class ClientCache(object): .. versionadded:: 1.5.0 - :param botocore_session: Botocore session to use when creating clients (optional) + :param botocore_session: Botocore session to use when creating clients :type botocore_session: botocore.session.Session :param client_config: Config to use when creating client :type client_config: botocore.config.Config """ - _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) - _client_config = attr.ib( - default=attr.Factory(functools.partial(BotocoreConfig, user_agent_extra=USER_AGENT_SUFFIX)), - validator=instance_of(BotocoreConfig), - ) + _botocore_session = attr.ib(validator=instance_of(BotocoreSession)) + _client_config = attr.ib(validator=instance_of(BotocoreConfig)) def __attrs_post_init__(self): """Set up internal cache.""" diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py index 5c419c506..52747caae 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -5,14 +5,18 @@ .. versionadded:: 1.5.0 """ +import functools import logging import attr import six from attr.validators import deep_iterable, instance_of, is_callable, optional from botocore.client import BaseClient +from botocore.config import Config as BotocoreConfig +from botocore.session import Session as BotocoreSession from aws_encryption_sdk.exceptions import UnknownRegionError +from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX from aws_encryption_sdk.internal.validators import value_is_not_a_string from .client_cache import ClientCache @@ -58,11 +62,36 @@ class DefaultClientSupplier(ClientSupplier): .. versionadded:: 1.5.0 - :param botocore_session: botocore session to use when creating clients (optional) + If you want clients to have special credentials or other configuration, + you can provide those with custom ``botocore`` Session and/or `Config`_ instances. + + .. _Config: https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html + + .. code-block:: python + + from aws_encryption_sdk.keyrings.aws_kms.client_supplier import DefaultClientSupplier + from botocore.session import Session + from botocore.config import Config + + my_client_supplier = DefaultClientSupplier( + botocore_session=Session(**_get_custom_credentials()), + client_config=Config(connect_timeout=10), + ) + + :param botocore_session: Botocore session to use when creating clients (optional) :type botocore_session: botocore.session.Session + :param client_config: Config to use when creating client (optional) + :type client_config: botocore.config.Config """ - _client_cache = attr.ib(default=attr.Factory(ClientCache), validator=instance_of(ClientCache)) + _botocore_session = attr.ib(default=attr.Factory(BotocoreSession), validator=instance_of(BotocoreSession)) + _client_config = attr.ib( + default=attr.Factory(functools.partial(BotocoreConfig, user_agent_extra=USER_AGENT_SUFFIX)), + validator=instance_of(BotocoreConfig), + ) + + def __attrs_post_init__(self): + self._client_cache = ClientCache(botocore_session=self._botocore_session, client_config=self._client_config) def __call__(self, region_name): # type: (Union[None, str]) -> BaseClient diff --git a/test/functional/keyrings/aws_kms/test_client_cache.py b/test/functional/keyrings/aws_kms/test_client_cache.py index e2187b2dc..01c27fa55 100644 --- a/test/functional/keyrings/aws_kms/test_client_cache.py +++ b/test/functional/keyrings/aws_kms/test_client_cache.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 """Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_cache``.""" import pytest +from botocore.config import Config +from botocore.session import Session from aws_encryption_sdk.keyrings.aws_kms.client_cache import ClientCache @@ -9,7 +11,7 @@ def test_client_cache_caches_clients(): - cache = ClientCache() + cache = ClientCache(botocore_session=Session(), client_config=Config()) initial_client = cache.client("us-west-2", "kms") @@ -20,7 +22,7 @@ def test_client_cache_caches_clients(): def test_client_cache_new_client(): - cache = ClientCache() + cache = ClientCache(botocore_session=Session(), client_config=Config()) initial_client = cache.client("us-west-2", "kms") diff --git a/test/functional/keyrings/aws_kms/test_client_suppliers.py b/test/functional/keyrings/aws_kms/test_client_suppliers.py index bede8f0ab..2d63fcc02 100644 --- a/test/functional/keyrings/aws_kms/test_client_suppliers.py +++ b/test/functional/keyrings/aws_kms/test_client_suppliers.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 """Functional tests for ``aws_encryption_sdk.keyrings.aws_kms.client_suppliers``.""" import pytest +from botocore.config import Config +from botocore.session import Session from aws_encryption_sdk.exceptions import UnknownRegionError from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import ( @@ -34,6 +36,16 @@ def test_default_supplier_uses_cache(): assert test is expected +def test_default_supplier_passes_through_configs(): + session = Session() + config = Config() + + test = DefaultClientSupplier(botocore_session=session, client_config=config) + + assert test._client_cache._botocore_session is session + assert test._client_cache._client_config is config + + @pytest.mark.parametrize( "kwargs", ( diff --git a/test/integration/keyrings/aws_kms/test_client_cache.py b/test/integration/keyrings/aws_kms/test_client_cache.py index 61b4bf61b..a49900d44 100644 --- a/test/integration/keyrings/aws_kms/test_client_cache.py +++ b/test/integration/keyrings/aws_kms/test_client_cache.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 """Integration tests for ``aws_encryption_sdk.keyrings.aws_kms.client_cache``.""" import pytest +from botocore.config import Config from botocore.exceptions import BotoCoreError from botocore.session import Session @@ -11,7 +12,7 @@ def test_client_cache_removes_bad_client(): - cache = ClientCache() + cache = ClientCache(botocore_session=Session(), client_config=Config()) fake_region = "us-fake-1" initial_client = cache.client(fake_region, "kms") @@ -25,7 +26,7 @@ def test_client_cache_removes_bad_client(): def test_regional_client_does_not_modify_botocore_session(): - cache = ClientCache(botocore_session=Session()) + cache = ClientCache(botocore_session=Session(), client_config=Config()) fake_region = "us-fake-1" assert cache._botocore_session.get_config_variable("region") != fake_region @@ -34,7 +35,7 @@ def test_regional_client_does_not_modify_botocore_session(): def test_client_cache_remove_bad_client_when_already_removed(): - cache = ClientCache() + cache = ClientCache(botocore_session=Session(), client_config=Config()) fake_region = "us-fake-1" initial_client = cache.client(fake_region, "kms") From ac516028c3909eccd49a1dd3565f471f664652c5 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 17:07:26 -0800 Subject: [PATCH 36/64] chore: move client cache into a "private" module --- doc/index.rst | 2 +- .../keyrings/aws_kms/{client_cache.py => _client_cache.py} | 4 ++++ src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py | 2 +- test/functional/keyrings/aws_kms/test_client_cache.py | 2 +- test/integration/keyrings/aws_kms/test_client_cache.py | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) rename src/aws_encryption_sdk/keyrings/aws_kms/{client_cache.py => _client_cache.py} (96%) diff --git a/doc/index.rst b/doc/index.rst index 5e6181904..2e21bccbd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -17,7 +17,6 @@ Modules aws_encryption_sdk.keyrings.base aws_encryption_sdk.keyrings.aws_kms aws_encryption_sdk.keyrings.aws_kms.client_suppliers - aws_encryption_sdk.keyrings.aws_kms.client_cache aws_encryption_sdk.keyrings.multi aws_encryption_sdk.keyrings.raw aws_encryption_sdk.key_providers.base @@ -45,5 +44,6 @@ Modules aws_encryption_sdk.internal.structures aws_encryption_sdk.internal.validators aws_encryption_sdk.internal.utils + aws_encryption_sdk.keyrings.aws_kms._client_cache .. include:: ../CHANGELOG.rst diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py b/src/aws_encryption_sdk/keyrings/aws_kms/_client_cache.py similarity index 96% rename from src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py rename to src/aws_encryption_sdk/keyrings/aws_kms/_client_cache.py index c09eeab15..9eb8d3e82 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_cache.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/_client_cache.py @@ -4,6 +4,10 @@ .. versionadded:: 1.5.0 +.. warning:: + No guarantee is provided on the modules and APIs within this + namespace staying consistent. Directly reference at your own risk. + """ import functools import logging diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py index 52747caae..4fdbbc234 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -19,7 +19,7 @@ from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX from aws_encryption_sdk.internal.validators import value_is_not_a_string -from .client_cache import ClientCache +from ._client_cache import ClientCache try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import Callable, Union # noqa pylint: disable=unused-import diff --git a/test/functional/keyrings/aws_kms/test_client_cache.py b/test/functional/keyrings/aws_kms/test_client_cache.py index 01c27fa55..06c6c51c0 100644 --- a/test/functional/keyrings/aws_kms/test_client_cache.py +++ b/test/functional/keyrings/aws_kms/test_client_cache.py @@ -5,7 +5,7 @@ from botocore.config import Config from botocore.session import Session -from aws_encryption_sdk.keyrings.aws_kms.client_cache import ClientCache +from aws_encryption_sdk.keyrings.aws_kms._client_cache import ClientCache pytestmark = [pytest.mark.functional, pytest.mark.local] diff --git a/test/integration/keyrings/aws_kms/test_client_cache.py b/test/integration/keyrings/aws_kms/test_client_cache.py index a49900d44..6ab1a05d5 100644 --- a/test/integration/keyrings/aws_kms/test_client_cache.py +++ b/test/integration/keyrings/aws_kms/test_client_cache.py @@ -6,7 +6,7 @@ from botocore.exceptions import BotoCoreError from botocore.session import Session -from aws_encryption_sdk.keyrings.aws_kms.client_cache import ClientCache +from aws_encryption_sdk.keyrings.aws_kms._client_cache import ClientCache pytestmark = [pytest.mark.integ] From dd4764a41f1be9785c8b07e5ea96f7f18c882cf4 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 17:18:12 -0800 Subject: [PATCH 37/64] chore: update upstream requirements files --- test/upstream-requirements-py27.txt | 94 +++++++++++++++++++++-------- test/upstream-requirements-py37.txt | 80 ++++++++++++++++-------- 2 files changed, 124 insertions(+), 50 deletions(-) diff --git a/test/upstream-requirements-py27.txt b/test/upstream-requirements-py27.txt index 082a6198e..92e723b9c 100644 --- a/test/upstream-requirements-py27.txt +++ b/test/upstream-requirements-py27.txt @@ -1,30 +1,72 @@ -asn1crypto==0.24.0 atomicwrites==1.3.0 -attrs==19.1.0 -boto3==1.9.133 -botocore==1.12.133 -cffi==1.12.3 -coverage==4.5.3 -cryptography==2.6.1 -docutils==0.14 -enum34==1.1.6 +attrs==19.3.0 +aws-sam-translator==1.21.0 +aws-xray-sdk==2.4.3 +backports.ssl-match-hostname==3.7.0.1 +backports.tempfile==1.0 +backports.weakref==1.0.post1 +boto==2.49.0 +boto3==1.12.16 +botocore==1.15.16 +certifi==2019.11.28 +cffi==1.14.0 +cfn-lint==0.28.3 +chardet==3.0.4 +configparser==4.0.2 +contextlib2==0.6.0.post1 +cookies==2.2.1 +coverage==5.0.3 +cryptography==2.8 +docker==4.2.0 +docutils==0.15.2 +ecdsa==0.15 +enum34==1.1.9 funcsigs==1.0.2 -futures==3.2.0 -ipaddress==1.0.22 -jmespath==0.9.4 -mock==2.0.0 +functools32==3.2.3.post2 +future==0.18.2 +futures==3.3.0 +idna==2.8 +importlib-metadata==1.5.0 +importlib-resources==1.0.2 +ipaddress==1.0.23 +Jinja2==2.11.1 +jmespath==0.9.5 +jsondiff==1.1.2 +jsonpatch==1.25 +jsonpickle==1.3 +jsonpointer==2.0 +jsonschema==3.2.0 +MarkupSafe==1.1.1 +mock==3.0.5 more-itertools==5.0.0 -pathlib2==2.3.3 -pbr==5.1.3 -pluggy==0.9.0 -py==1.8.0 -pycparser==2.19 -pytest==4.4.1 -pytest-cov==2.6.1 -pytest-mock==1.10.4 -python-dateutil==2.8.0 -s3transfer==0.2.0 +moto==1.3.14 +packaging==20.3 +pathlib2==2.3.5 +pluggy==0.13.1 +py==1.8.1 +pyasn1==0.4.8 +pycparser==2.20 +pyparsing==2.4.6 +pyrsistent==0.15.7 +pytest==4.6.9 +pytest-cov==2.8.1 +pytest-mock==2.0.0 +python-dateutil==2.8.1 +python-jose==3.1.0 +pytz==2019.3 +PyYAML==5.3 +requests==2.23.0 +responses==0.10.12 +rsa==4.0 +s3transfer==0.3.3 scandir==1.10.0 -six==1.12.0 -urllib3==1.24.2 -wrapt==1.11.1 +six==1.14.0 +sshpubkeys==3.1.0 +typing==3.7.4.1 +urllib3==1.25.8 +wcwidth==0.1.8 +websocket-client==0.57.0 +Werkzeug==1.0.0 +wrapt==1.12.0 +xmltodict==0.12.0 +zipp==1.2.0 diff --git a/test/upstream-requirements-py37.txt b/test/upstream-requirements-py37.txt index 238746675..03a6eb36e 100644 --- a/test/upstream-requirements-py37.txt +++ b/test/upstream-requirements-py37.txt @@ -1,24 +1,56 @@ -asn1crypto==0.24.0 -atomicwrites==1.3.0 -attrs==19.1.0 -boto3==1.9.133 -botocore==1.12.133 -cffi==1.12.3 -coverage==4.5.3 -cryptography==2.6.1 -docutils==0.14 -jmespath==0.9.4 -mock==2.0.0 -more-itertools==7.0.0 -pbr==5.1.3 -pluggy==0.9.0 -py==1.8.0 -pycparser==2.19 -pytest==4.4.1 -pytest-cov==2.6.1 -pytest-mock==1.10.4 -python-dateutil==2.8.0 -s3transfer==0.2.0 -six==1.12.0 -urllib3==1.24.2 -wrapt==1.11.1 +attrs==19.3.0 +aws-sam-translator==1.21.0 +aws-xray-sdk==2.4.3 +boto==2.49.0 +boto3==1.12.16 +botocore==1.15.16 +certifi==2019.11.28 +cffi==1.14.0 +cfn-lint==0.28.3 +chardet==3.0.4 +coverage==5.0.3 +cryptography==2.8 +docker==4.2.0 +docutils==0.15.2 +ecdsa==0.15 +future==0.18.2 +idna==2.8 +importlib-metadata==1.5.0 +Jinja2==2.11.1 +jmespath==0.9.5 +jsondiff==1.1.2 +jsonpatch==1.25 +jsonpickle==1.3 +jsonpointer==2.0 +jsonschema==3.2.0 +MarkupSafe==1.1.1 +mock==4.0.1 +more-itertools==8.2.0 +moto==1.3.14 +packaging==20.3 +pluggy==0.13.1 +py==1.8.1 +pyasn1==0.4.8 +pycparser==2.20 +pyparsing==2.4.6 +pyrsistent==0.15.7 +pytest==5.3.5 +pytest-cov==2.8.1 +pytest-mock==2.0.0 +python-dateutil==2.8.1 +python-jose==3.1.0 +pytz==2019.3 +PyYAML==5.3 +requests==2.23.0 +responses==0.10.12 +rsa==4.0 +s3transfer==0.3.3 +six==1.14.0 +sshpubkeys==3.1.0 +urllib3==1.25.8 +wcwidth==0.1.8 +websocket-client==0.57.0 +Werkzeug==1.0.0 +wrapt==1.12.0 +xmltodict==0.12.0 +zipp==3.1.0 From f46df5f3897813a1bcaf06ca0f09355308dad26b Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 18:18:37 -0800 Subject: [PATCH 38/64] docs: add docstring --- src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py index 4fdbbc234..c8a0af696 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/client_suppliers.py @@ -91,6 +91,7 @@ class DefaultClientSupplier(ClientSupplier): ) def __attrs_post_init__(self): + """Set up the internal cache.""" self._client_cache = ClientCache(botocore_session=self._botocore_session, client_config=self._client_config) def __call__(self, region_name): From d2bbb7b25ed8869c5911dd18a08b4f5faa8bbe24 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 6 Mar 2020 19:07:52 -0800 Subject: [PATCH 39/64] chore: make negative logic positive --- .../keyrings/aws_kms/__init__.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 1cfbd16da..808dde163 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -205,18 +205,16 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): if decryption_materials.data_encryption_key is not None: return decryption_materials - if not ( + if ( edk.key_provider.provider_id == _PROVIDER_ID and edk.key_provider.key_info.decode("utf-8") == self._key_id ): - continue - - decryption_materials = _try_aws_kms_decrypt( - client_supplier=self._client_supplier, - decryption_materials=decryption_materials, - grant_tokens=self._grant_tokens, - encrypted_data_key=edk, - ) + decryption_materials = _try_aws_kms_decrypt( + client_supplier=self._client_supplier, + decryption_materials=decryption_materials, + grant_tokens=self._grant_tokens, + encrypted_data_key=edk, + ) return decryption_materials @@ -250,15 +248,13 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): if decryption_materials.data_encryption_key is not None: return decryption_materials - if edk.key_provider.provider_id != _PROVIDER_ID: - continue - - decryption_materials = _try_aws_kms_decrypt( - client_supplier=self._client_supplier, - decryption_materials=decryption_materials, - grant_tokens=self._grant_tokens, - encrypted_data_key=edk, - ) + if edk.key_provider.provider_id == _PROVIDER_ID: + decryption_materials = _try_aws_kms_decrypt( + client_supplier=self._client_supplier, + decryption_materials=decryption_materials, + grant_tokens=self._grant_tokens, + encrypted_data_key=edk, + ) return decryption_materials From 94028267d359fe97ea8ff5cb263b32d88d56b73f Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Thu, 12 Mar 2020 12:03:28 -0700 Subject: [PATCH 40/64] Examples refresh, take 1 (#219) * Update PR template * Added a check for max_age being greater than 0 (#172) * Added a check for max_age being greater than 0 * Fixed flake8 by adding missing pydocstyle dependency * Added the dependency to decrypt_oracle as well * Added test for max_age<=0 ValueError * Updated test for max_age<=0.0 ValueError * Added negative test case * Testing something, want AppVeyor to run * Quick change * Running AppVeyor * Added example for using multiple keyrings in multiple regions * Undid something quickly * Fixed importerror * Formatting fix * Update tox.ini * Update tox.ini * Made some changes to the multiple_kms_cmk_regions example/test * This is my next interation of the code for the example; however, I am still working on populating the tests correctly, so the CI will fail, but I tested the code with my own KMS CMK ARNs, so I know it will work once the tests are populated (working with Tejeswini on this) * Changed the example to test two CMKs in the same region until Issue #178 is cleared up * Found out how to make a new valid test key, so now there are two valid test keys in different regions for this example * Ran autoformat * Added some docstrings * Formatting will be the death of me * Used correct keys in test * Updated some comments * Fixed KMS master key provider tests when default AWS region is configured (#179) * Fixed KMS master key provider tests for users who have their default AWS region configured * created fixture for botocore session with no region set * add auto-used fixture in KMS master key provider unit tests to test against both with and without default region * Wrote example and test for using one kms cmk with an unsigned algorithm * Update the integration tests * Small changes * Update one_kms_cmk_unsigned.py * Update examples/src/one_kms_cmk_unsigned.py Co-Authored-By: Matt Bullock * isort-check now succeeds * chore: move existing examples into "legacy" directory * chore: add automatic test runner for examples * chore: convert existing examples to work with automatic test runner * chore: move examples kwarg building into utils module * chore: convert multi-CMK test runners to new configuration format * fix: fix multi-CMK example logic * chore: convert multi-CMK example to new test runner signature and move into legacy examples * chore: make examples linting always run across both source and tests * fix: linting fixes * docs: add docstring description for legacy examples module * chore: add initial new-format examples * docs: add examples readme * docs: add instructions for writing examples * chore: address PR comments * chore: change examples parameter from aws_kms_cmk_arn to aws_kms_cmk for consistency * docs: clarify integration tests readme * Apply suggestions from code review Co-Authored-By: June Blender * Apply suggestions from code review Co-Authored-By: June Blender * docs: change from "one-shot" term to "one-step" * chore: apply changes based on PR comments * docs: reword parameter description * Apply suggestions from code review Co-Authored-By: June Blender * chore: rename examples input parameters to move from "child" to "additional" naming * docs: clarify configuration intro * docs: apply examples comments consistently * chore: fix line length * fix: fix typo Co-authored-by: John Walker Co-authored-by: Caitlin Tibbetts Co-authored-by: Caitlin Tibbetts Co-authored-by: Ryan Ragona Co-authored-by: lizroth <30636882+lizroth@users.noreply.github.com> Co-authored-by: June Blender --- examples/README.md | 94 +++++++++++++++++++ examples/src/file_streaming_defaults.py | 82 ++++++++++++++++ examples/src/in_memory_streaming_defaults.py | 79 ++++++++++++++++ examples/src/legacy/__init__.py | 9 ++ examples/src/{ => legacy}/basic_encryption.py | 22 ++--- ...file_encryption_with_multiple_providers.py | 20 +--- ...c_file_encryption_with_raw_key_provider.py | 16 +--- .../{ => legacy}/data_key_caching_basic.py | 20 +--- .../src/legacy/multiple_kms_cmk_regions.py | 65 +++++++++++++ examples/src/{ => legacy}/one_kms_cmk.py | 20 +--- .../one_kms_cmk_streaming_data.py | 20 +--- .../src/{ => legacy}/one_kms_cmk_unsigned.py | 20 +--- examples/src/onestep_defaults.py | 57 +++++++++++ examples/src/onestep_unsigned.py | 77 +++++++++++++++ examples/test/examples_test_utils.py | 92 +++++++++++++++--- examples/test/test_i_basic_encryption.py | 27 ------ ...file_encryption_with_multiple_providers.py | 41 -------- ...c_file_encryption_with_raw_key_provider.py | 36 ------- .../test/test_i_data_key_caching_basic.py | 25 ----- examples/test/test_i_one_kms_cmk.py | 29 ------ .../test/test_i_one_kms_cmk_streaming_data.py | 40 -------- examples/test/test_i_one_kms_cmk_unsigned.py | 29 ------ examples/test/test_run_examples.py | 24 +++++ test/integration/README.rst | 5 +- test/integration/integration_test_utils.py | 26 +++-- tox.ini | 11 ++- 26 files changed, 632 insertions(+), 354 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/src/file_streaming_defaults.py create mode 100644 examples/src/in_memory_streaming_defaults.py create mode 100644 examples/src/legacy/__init__.py rename examples/src/{ => legacy}/basic_encryption.py (63%) rename examples/src/{ => legacy}/basic_file_encryption_with_multiple_providers.py (88%) rename examples/src/{ => legacy}/basic_file_encryption_with_raw_key_provider.py (83%) rename examples/src/{ => legacy}/data_key_caching_basic.py (67%) create mode 100644 examples/src/legacy/multiple_kms_cmk_regions.py rename examples/src/{ => legacy}/one_kms_cmk.py (66%) rename examples/src/{ => legacy}/one_kms_cmk_streaming_data.py (72%) rename examples/src/{ => legacy}/one_kms_cmk_unsigned.py (69%) create mode 100644 examples/src/onestep_defaults.py create mode 100644 examples/src/onestep_unsigned.py delete mode 100644 examples/test/test_i_basic_encryption.py delete mode 100644 examples/test/test_i_basic_file_encryption_with_multiple_providers.py delete mode 100644 examples/test/test_i_basic_file_encryption_with_raw_key_provider.py delete mode 100644 examples/test/test_i_data_key_caching_basic.py delete mode 100644 examples/test/test_i_one_kms_cmk.py delete mode 100644 examples/test/test_i_one_kms_cmk_streaming_data.py delete mode 100644 examples/test/test_i_one_kms_cmk_unsigned.py create mode 100644 examples/test/test_run_examples.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..f370f7407 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,94 @@ +# AWS Encryption SDK Examples + +This section features examples that show you +how to use the AWS Encryption SDK. +We demonstrate how to use the encryption and decryption APIs +and how to set up some common configuration patterns. + +## APIs + +The AWS Encryption SDK provides two high-level APIs: +one-step APIs that process the entire operation in memory +and streaming APIs. + +You can find examples that demonstrate these APIs +in the [`examples/src/`](./src) directory. + +## Configuration + +To use the library APIs, +you need to describe how you want the library to protect your data keys. +You can do this using +[keyrings][#keyrings] or [cryptographic materials managers][#cryptographic-materials-managers], +or using [master key providers][#master-key-providers]. +These examples will show you how. + +### Keyrings + +Keyrings are the most common way for you to configure the AWS Encryption SDK. +They determine how the AWS Encryption SDK protects your data. +You can find these examples in [`examples/src/keyring`](./src/keyring). + +### Cryptographic Materials Managers + +Keyrings define how your data keys are protected, +but there is more going on here than just protecting data keys. + +Cryptographic materials managers give you higher-level controls +over how the AWS Encryption SDK protects your data. +This can include things like +enforcing the use of certain algorithm suites or encryption context settings, +reusing data keys across messages, +or changing how you interact with keyrings. +You can find these examples in +[`examples/src/crypto_materials_manager`](./src/crypto_materials_manager). + +### Master Key Providers + +Before there were keyrings, there were master key providers. +Master key providers were the original configuration structure +that we provided for defining how you want to protect your data keys. +Keyrings provide a simpler experience and often more powerful configuration options, +but if you need to use master key providers, +need help migrating from master key providers to keyrings, +or simply want to see the difference between these configuration experiences, +you can find these examples in [`examples/src/master_key_provider`](./src/master_key_provider). + +## Legacy + +This section includes older examples, including examples of using master keys and master key providers in Java and Python. +You can use them as a reference, +but we recommend looking at the newer examples, which explain the preferred ways of using this library. +You can find these examples in [`examples/src/legacy`](./src/legacy). + +# Writing Examples + +If you want to contribute a new example, that's awesome! +To make sure that your example is tested in our CI, +please make sure that it meets the following requirements: + +1. The example MUST be a distinct module in the [`examples/src/`](./src) directory. +1. The example MAY be nested arbitrarily deeply, + but every intermediate directory MUST contain a `__init__.py` file + so that CPython 2.7 will recognize it as a module. +1. Every example MUST be CPython 2.7 compatible. +1. Each example file MUST contain exactly one example. +1. Each example file MUST contain a function called `run` that runs the example. +1. If your `run` function needs any of the following inputs, + the parameters MUST have the following names: + * `aws_kms_cmk` (`str`) : A single AWS KMS CMK ARN. + * NOTE: You can assume that automatically discovered credentials have + `kms:GenerateDataKey`, `kms:Encrypt`, and `kms:Decrypt` permissions on this CMK. + * `aws_kms_generator_cmk` (`str`) : A single AWS KMS CMK ARN to use as a generator key. + * NOTE: You can assume that automatically discovered credentials have + `kms:GenerateDataKey`, `kms:Encrypt`, and `kms:Decrypt` permissions on this CMK. + * `aws_kms_additional_cmks` (`List[str]`) : + A list of AWS KMS CMK ARNs to use for encrypting and decrypting data keys. + * NOTE: You can assume that automatically discovered credentials have + `kms:Encrypt` and `kms:Decrypt` permissions on these CMKs. + * `source_plaintext` (`bytes`) : Plaintext data to encrypt. + * `source_plaintext_filename` (`str`) : A path to a file containing plaintext to encrypt. + * NOTE: You can assume that you have write access to the parent directory + and that anything you do in that directory will be cleaned up + by our test runners. +1. Any additional parameters MUST be optional. diff --git a/examples/src/file_streaming_defaults.py b/examples/src/file_streaming_defaults.py new file mode 100644 index 000000000..983bfc90d --- /dev/null +++ b/examples/src/file_streaming_defaults.py @@ -0,0 +1,82 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to use the streaming encrypt and decrypt APIs when working with files. + +One benefit of using the streaming API is that +we can check the encryption context in the header before we start decrypting. + +In this example, we use an AWS KMS customer master key (CMK), +but you can use other key management options with the AWS Encryption SDK. +For examples that demonstrate how to use other key management configurations, +see the ``keyring`` and ``master_key_provider`` directories. +""" +import filecmp + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext_filename): + # type: (str, str) -> None + """Demonstrate an encrypt/decrypt cycle using the streaming encrypt/decrypt APIs with files. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param str source_plaintext_filename: Path to plaintext file to encrypt + """ + # We assume that you can also write to the directory containing the plaintext file, + # so that is where we will put all of the results. + ciphertext_filename = source_plaintext_filename + ".encrypted" + decrypted_filename = ciphertext_filename + ".decrypted" + + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Open the files you want to work with. + with open(source_plaintext_filename, "rb") as plaintext, open(ciphertext_filename, "wb") as ciphertext: + # The streaming API provides a context manager. + # You can read from it just as you read from a file. + with aws_encryption_sdk.stream( + mode="encrypt", source=plaintext, encryption_context=encryption_context, keyring=keyring + ) as encryptor: + # Iterate through the segments in the context manager + # and write the results to the ciphertext. + for segment in encryptor: + ciphertext.write(segment) + + # Verify that the ciphertext and plaintext are different. + assert not filecmp.cmp(source_plaintext_filename, ciphertext_filename) + + # Open the files you want to work with. + with open(ciphertext_filename, "rb") as ciphertext, open(decrypted_filename, "wb") as decrypted: + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # We do not need to specify the encryption context on decrypt + # because the message header includes the encryption context. + with aws_encryption_sdk.stream(mode="decrypt", source=ciphertext, keyring=keyring) as decryptor: + # Check the encryption context in the header before we start decrypting. + # + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decryptor.header.encryption_context.items()) + + # Now that we are more confident that we will decrypt the right message, + # we can start decrypting. + for segment in decryptor: + decrypted.write(segment) + + # Verify that the decrypted plaintext is identical to the original plaintext. + assert filecmp.cmp(source_plaintext_filename, decrypted_filename) diff --git a/examples/src/in_memory_streaming_defaults.py b/examples/src/in_memory_streaming_defaults.py new file mode 100644 index 000000000..1a2824b94 --- /dev/null +++ b/examples/src/in_memory_streaming_defaults.py @@ -0,0 +1,79 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to use the streaming encrypt and decrypt APIs on data in memory. + +One benefit of using the streaming API is that +we can check the encryption context in the header before we start decrypting. + +In this example, we use an AWS KMS customer master key (CMK), +but you can use other key management options with the AWS Encryption SDK. +For examples that demonstrate how to use other key management configurations, +see the ``keyring`` and ``mater_key_provider`` directories. +""" +import io + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using the streaming encrypt/decrypt APIs in-memory. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + ciphertext = io.BytesIO() + + # The streaming API provides a context manager. + # You can read from it just as you read from a file. + with aws_encryption_sdk.stream( + mode="encrypt", source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) as encryptor: + # Iterate through the segments in the context manager + # and write the results to the ciphertext. + for segment in encryptor: + ciphertext.write(segment) + + # Verify that the ciphertext and plaintext are different. + assert ciphertext.getvalue() != source_plaintext + + # Reset the ciphertext stream position so that we can read from the beginning. + ciphertext.seek(0) + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # We do not need to specify the encryption context on decrypt + # because the header message includes the encryption context. + decrypted = io.BytesIO() + with aws_encryption_sdk.stream(mode="decrypt", source=ciphertext, keyring=keyring) as decryptor: + # Check the encryption context in the header before we start decrypting. + # + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decryptor.header.encryption_context.items()) + + # Now that we are more confident that we will decrypt the right message, + # we can start decrypting. + for segment in decryptor: + decrypted.write(segment) + + # Verify that the decrypted plaintext is identical to the original plaintext. + assert decrypted.getvalue() == source_plaintext diff --git a/examples/src/legacy/__init__.py b/examples/src/legacy/__init__.py new file mode 100644 index 000000000..e4646257c --- /dev/null +++ b/examples/src/legacy/__init__.py @@ -0,0 +1,9 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Legacy examples. + +We keep these older examples as reference material, +but we recommend that you use the new examples. +The new examples reflect our current guidance for using the library. +""" diff --git a/examples/src/basic_encryption.py b/examples/src/legacy/basic_encryption.py similarity index 63% rename from examples/src/basic_encryption.py rename to examples/src/legacy/basic_encryption.py index 6c194e45d..81db7a102 100644 --- a/examples/src/basic_encryption.py +++ b/examples/src/legacy/basic_encryption.py @@ -1,29 +1,19 @@ -# 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. -"""Example showing basic encryption and decryption of a value already in memory.""" +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Example showing how to encrypt and decrypt a value in memory.""" import aws_encryption_sdk -def cycle_string(key_arn, source_plaintext, botocore_session=None): +def run(aws_kms_cmk, source_plaintext, botocore_session=None): """Encrypts and then decrypts a string under a KMS customer master key (CMK). - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the AWS KMS CMK :param bytes source_plaintext: Data to encrypt :param botocore_session: existing botocore session instance :type botocore_session: botocore.session.Session """ # Create a KMS master key provider - kms_kwargs = dict(key_ids=[key_arn]) + kms_kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kms_kwargs["botocore_session"] = botocore_session master_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(**kms_kwargs) diff --git a/examples/src/basic_file_encryption_with_multiple_providers.py b/examples/src/legacy/basic_file_encryption_with_multiple_providers.py similarity index 88% rename from examples/src/basic_file_encryption_with_multiple_providers.py rename to examples/src/legacy/basic_file_encryption_with_multiple_providers.py index 9edceef9e..1319025d9 100644 --- a/examples/src/basic_file_encryption_with_multiple_providers.py +++ b/examples/src/legacy/basic_file_encryption_with_multiple_providers.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing creation of a RawMasterKeyProvider, how to use multiple master key providers to encrypt, and demonstrating that each master key provider can then be used independently to decrypt the same encrypted message. @@ -60,12 +50,12 @@ def _get_raw_key(self, key_id): ) -def cycle_file(key_arn, source_plaintext_filename, botocore_session=None): +def run(aws_kms_cmk, source_plaintext_filename, botocore_session=None): """Encrypts and then decrypts a file using a KMS master key provider and a custom static master key provider. Both master key providers are used to encrypt the plaintext file, so either one alone can decrypt it. - :param str key_arn: Amazon Resource Name (ARN) of the KMS Customer Master Key (CMK) + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS Customer Master Key (CMK) (http://docs.aws.amazon.com/kms/latest/developerguide/viewing-keys.html) :param str source_plaintext_filename: Filename of file to encrypt :param botocore_session: existing botocore session instance @@ -77,7 +67,7 @@ def cycle_file(key_arn, source_plaintext_filename, botocore_session=None): cycled_static_plaintext_filename = source_plaintext_filename + ".static.decrypted" # Create a KMS master key provider - kms_kwargs = dict(key_ids=[key_arn]) + kms_kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kms_kwargs["botocore_session"] = botocore_session kms_master_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(**kms_kwargs) diff --git a/examples/src/basic_file_encryption_with_raw_key_provider.py b/examples/src/legacy/basic_file_encryption_with_raw_key_provider.py similarity index 83% rename from examples/src/basic_file_encryption_with_raw_key_provider.py rename to examples/src/legacy/basic_file_encryption_with_raw_key_provider.py index 91e2a7e9a..1e3eff8e0 100644 --- a/examples/src/basic_file_encryption_with_raw_key_provider.py +++ b/examples/src/legacy/basic_file_encryption_with_raw_key_provider.py @@ -1,15 +1,5 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing creation and use of a RawMasterKeyProvider.""" import filecmp import os @@ -48,7 +38,7 @@ def _get_raw_key(self, key_id): ) -def cycle_file(source_plaintext_filename): +def run(source_plaintext_filename): """Encrypts and then decrypts a file under a custom static master key provider. :param str source_plaintext_filename: Filename of file to encrypt diff --git a/examples/src/data_key_caching_basic.py b/examples/src/legacy/data_key_caching_basic.py similarity index 67% rename from examples/src/data_key_caching_basic.py rename to examples/src/legacy/data_key_caching_basic.py index 1d5445615..899aed294 100644 --- a/examples/src/data_key_caching_basic.py +++ b/examples/src/legacy/data_key_caching_basic.py @@ -1,23 +1,13 @@ -# 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example of encryption with data key caching.""" import aws_encryption_sdk -def encrypt_with_caching(kms_cmk_arn, max_age_in_cache, cache_capacity): +def run(aws_kms_cmk, max_age_in_cache=10.0, cache_capacity=10): """Encrypts a string using an AWS KMS customer master key (CMK) and data key caching. - :param str kms_cmk_arn: Amazon Resource Name (ARN) of the KMS customer master key + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS customer master key :param float max_age_in_cache: Maximum time in seconds that a cached entry can be used :param int cache_capacity: Maximum number of entries to retain in cache at once """ @@ -32,7 +22,7 @@ def encrypt_with_caching(kms_cmk_arn, max_age_in_cache, cache_capacity): encryption_context = {"purpose": "test"} # Create a master key provider for the KMS customer master key (CMK) - key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[kms_cmk_arn]) + key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[aws_kms_cmk]) # Create a local cache cache = aws_encryption_sdk.LocalCryptoMaterialsCache(cache_capacity) diff --git a/examples/src/legacy/multiple_kms_cmk_regions.py b/examples/src/legacy/multiple_kms_cmk_regions.py new file mode 100644 index 000000000..deefd73e9 --- /dev/null +++ b/examples/src/legacy/multiple_kms_cmk_regions.py @@ -0,0 +1,65 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Example showing basic encryption and decryption of a value already in memory +using multiple KMS CMKs in multiple regions. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider + + +def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext, botocore_session=None): + """Encrypts and then decrypts a string under multiple KMS customer master keys (CMKs) in multiple regions. + + :param str aws_kms_generator_cmk: Amazon Resource Name (ARN) of the primary KMS CMK + :param List[str] aws_kms_additional_cmks: Additional Amazon Resource Names (ARNs) of secondary KMS CMKs + :param bytes source_plaintext: Data to encrypt + :param botocore_session: existing botocore session instance + :type botocore_session: botocore.session.Session + """ + encrypt_cmk = aws_kms_additional_cmks[0] + + # Check that these keys are in different regions + assert not aws_kms_generator_cmk.split(":")[3] == encrypt_cmk.split(":")[3] + + kwargs = dict(key_ids=[aws_kms_generator_cmk, encrypt_cmk]) + + if botocore_session is not None: + kwargs["botocore_session"] = botocore_session + + # Create master key provider using the ARNs of the keys and the session (botocore_session) + kms_key_provider = KMSMasterKeyProvider(**kwargs) + + # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header + ciphertext, encrypted_message_header = aws_encryption_sdk.encrypt( + key_provider=kms_key_provider, source=source_plaintext + ) + + # Check that both key ARNs are in the message headers + assert len(encrypted_message_header.encrypted_data_keys) == 2 + + # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header + # Either of our keys can be used to decrypt the message + plaintext_1, decrypted_message_header_1 = aws_encryption_sdk.decrypt( + key_provider=KMSMasterKey(key_id=aws_kms_generator_cmk), source=ciphertext + ) + plaintext_2, decrypted_message_header_2 = aws_encryption_sdk.decrypt( + key_provider=KMSMasterKey(key_id=encrypt_cmk), source=ciphertext + ) + + # Check that the original message and the decrypted message are the same + if not isinstance(source_plaintext, bytes): + plaintext_1 = plaintext_1.decode("utf-8") + plaintext_2 = plaintext_2.decode("utf-8") + assert source_plaintext == plaintext_1 + assert source_plaintext == plaintext_2 + + # Check that the headers of the encrypted message and decrypted message match + assert all( + pair in encrypted_message_header.encryption_context.items() + for pair in decrypted_message_header_1.encryption_context.items() + ) + assert all( + pair in encrypted_message_header.encryption_context.items() + for pair in decrypted_message_header_2.encryption_context.items() + ) diff --git a/examples/src/one_kms_cmk.py b/examples/src/legacy/one_kms_cmk.py similarity index 66% rename from examples/src/one_kms_cmk.py rename to examples/src/legacy/one_kms_cmk.py index 1ba1d869f..7fb5f1431 100644 --- a/examples/src/one_kms_cmk.py +++ b/examples/src/legacy/one_kms_cmk.py @@ -1,28 +1,18 @@ -# Copyright 2019 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing basic encryption and decryption of a value already in memory using one KMS CMK.""" import aws_encryption_sdk -def encrypt_decrypt(key_arn, source_plaintext, botocore_session=None): +def run(aws_kms_cmk, source_plaintext, botocore_session=None): """Encrypts and then decrypts a string under one KMS customer master key (CMK). - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS CMK :param bytes source_plaintext: Data to encrypt :param botocore_session: existing botocore session instance :type botocore_session: botocore.session.Session """ - kwargs = dict(key_ids=[key_arn]) + kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kwargs["botocore_session"] = botocore_session diff --git a/examples/src/one_kms_cmk_streaming_data.py b/examples/src/legacy/one_kms_cmk_streaming_data.py similarity index 72% rename from examples/src/one_kms_cmk_streaming_data.py rename to examples/src/legacy/one_kms_cmk_streaming_data.py index 3edfa82ab..90854398d 100644 --- a/examples/src/one_kms_cmk_streaming_data.py +++ b/examples/src/legacy/one_kms_cmk_streaming_data.py @@ -1,32 +1,22 @@ -# Copyright 2019 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing basic encryption and decryption of streaming data in memory using one KMS CMK.""" import filecmp import aws_encryption_sdk -def encrypt_decrypt_stream(key_arn, source_plaintext_filename, botocore_session=None): +def run(aws_kms_cmk, source_plaintext_filename, botocore_session=None): """Encrypts and then decrypts streaming data under one KMS customer master key (CMK). - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS CMK :param str source_plaintext_filename: Filename of file to encrypt :param botocore_session: existing botocore session instance :type botocore_session: botocore.session.Session """ kwargs = dict() - kwargs["key_ids"] = [key_arn] + kwargs["key_ids"] = [aws_kms_cmk] if botocore_session is not None: kwargs["botocore_session"] = botocore_session diff --git a/examples/src/one_kms_cmk_unsigned.py b/examples/src/legacy/one_kms_cmk_unsigned.py similarity index 69% rename from examples/src/one_kms_cmk_unsigned.py rename to examples/src/legacy/one_kms_cmk_unsigned.py index df2f4373d..a127261b7 100644 --- a/examples/src/one_kms_cmk_unsigned.py +++ b/examples/src/legacy/one_kms_cmk_unsigned.py @@ -1,15 +1,5 @@ -# Copyright 2019 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Example showing basic encryption and decryption of a value already in memory using one KMS CMK with an unsigned algorithm. """ @@ -17,15 +7,15 @@ from aws_encryption_sdk.identifiers import Algorithm -def encrypt_decrypt(key_arn, source_plaintext, botocore_session=None): +def run(aws_kms_cmk, source_plaintext, botocore_session=None): """Encrypts and then decrypts a string under one KMS customer master key (CMK) with an unsigned algorithm. - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK + :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS CMK :param bytes source_plaintext: Data to encrypt :param botocore_session: existing botocore session instance :type botocore_session: botocore.session.Session """ - kwargs = dict(key_ids=[key_arn]) + kwargs = dict(key_ids=[aws_kms_cmk]) if botocore_session is not None: kwargs["botocore_session"] = botocore_session diff --git a/examples/src/onestep_defaults.py b/examples/src/onestep_defaults.py new file mode 100644 index 000000000..a1d6e3dbd --- /dev/null +++ b/examples/src/onestep_defaults.py @@ -0,0 +1,57 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to use the one-step encrypt and decrypt APIs. + +In this example, we use an AWS KMS customer master key (CMK), +but you can use other key management options with the AWS Encryption SDK. +For examples that demonstrate how to use other key management configurations, +see the ``keyring`` and ``mater_key_provider`` directories. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using the one-step encrypt/decrypt APIs. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Verify that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # We do not need to specify the encryption context on decrypt + # because the header message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Verify that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/onestep_unsigned.py b/examples/src/onestep_unsigned.py new file mode 100644 index 000000000..aafb09feb --- /dev/null +++ b/examples/src/onestep_unsigned.py @@ -0,0 +1,77 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to specify an algorithm suite +when using the one-step encrypt and decrypt APIs. + +In this example, we use an AWS KMS customer master key (CMK), +but you can use other key management options with the AWS Encryption SDK. +For examples that demonstrate how to use other key management configurations, +see the ``keyring`` and ``mater_key_provider`` directories. + +The default algorithm suite includes a message-level signature +that protects you from an attacker who has *decrypt* but not *encrypt* capability +for a wrapping key that you used when encrypting a message +under multiple wrapping keys. + +However, if all of your readers and writers have the same permissions, +then this additional protection does not always add value. +This example shows you how to select another algorithm suite +that has all of the other properties of the default suite +but does not include a message-level signature. +""" +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate requesting a specific algorithm suite through the one-step encrypt/decrypt APIs. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=encryption_context, + keyring=keyring, + # Here we can specify the algorithm suite that we want to use. + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, + ) + + # Verify that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # We do not need to specify the encryption context on decrypt + # because the header message includes the encryption context. + # + # We do not need to specify the algorithm suite on decrypt + # because the header message includes the algorithm suite identifier. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Verify that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/test/examples_test_utils.py b/examples/test/examples_test_utils.py index 0984ee684..4f9aadef5 100644 --- a/examples/test/examples_test_utils.py +++ b/examples/test/examples_test_utils.py @@ -1,18 +1,29 @@ -# Copyright 2019 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. +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """Helper utilities for use while testing examples.""" import os import sys +import inspect + +import pytest +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Callable, Dict, Iterable, List # noqa pylint: disable=unused-import + + # we only need pathlib here for typehints + from pathlib import Path +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +HERE = os.path.abspath(os.path.dirname(__file__)) +EXAMPLES_SOURCE = os.path.join(HERE, "..", "src") +SINGLE_CMK_ARG = "aws_kms_cmk" +GENERATOR_CMK_ARG = "aws_kms_generator_cmk" +ADDITIONAL_CMKS_ARG = "aws_kms_additional_cmks" +PLAINTEXT_ARG = "source_plaintext" +PLAINTEXT_FILE_ARG = "source_plaintext_filename" os.environ["AWS_ENCRYPTION_SDK_EXAMPLES_TESTING"] = "yes" sys.path.extend([os.sep.join([os.path.dirname(__file__), "..", "..", "test", "integration"])]) @@ -47,4 +58,59 @@ ) -from integration_test_utils import get_cmk_arn # noqa pylint: disable=unused-import,import-error +from integration_test_utils import get_all_cmk_arns # noqa pylint: disable=unused-import,import-error + + +def all_examples(): + # type: () -> Iterable[pytest.param] + for (dirpath, _dirnames, filenames) in os.walk(EXAMPLES_SOURCE): + for testfile in filenames: + split_path = testfile.rsplit(".", 1) + if len(split_path) != 2: + continue + stem, suffix = split_path + if suffix == "py" and stem != "__init__": + module_parent = dirpath[len(EXAMPLES_SOURCE) + 1 :].replace("/", ".") + module_name = stem + if module_parent: + import_path = "..src.{base}.{name}".format(base=module_parent, name=module_name) + else: + import_path = "..src.{name}".format(name=module_name) + + yield pytest.param(import_path, id="{base}.{name}".format(base=module_parent, name=module_name)) + + +def get_arg_names(function): + # type: (Callable) -> List[str] + if six.PY2: + # getargspec was deprecated in CPython 3.0 but 2.7 does not have either of the new options + spec = inspect.getargspec(function) # pylint: disable=deprecated-method + return spec.args + + spec = inspect.getfullargspec(function) + return spec.args + + +def build_kwargs(function, temp_dir): + # type: (Callable, Path) -> Dict[str, str] + + plaintext_file = temp_dir / "plaintext" + plaintext_file.write_bytes(static_plaintext) + + cmk_arns = get_all_cmk_arns() + + args = get_arg_names(function) + possible_kwargs = { + SINGLE_CMK_ARG: cmk_arns[0], + GENERATOR_CMK_ARG: cmk_arns[0], + ADDITIONAL_CMKS_ARG: cmk_arns[1:], + PLAINTEXT_ARG: static_plaintext, + PLAINTEXT_FILE_ARG: str(plaintext_file.absolute()), + } + kwargs = {} + for name in args: + try: + kwargs[name] = possible_kwargs[name] + except KeyError: + pass + return kwargs diff --git a/examples/test/test_i_basic_encryption.py b/examples/test/test_i_basic_encryption.py deleted file mode 100644 index f2a4fab51..000000000 --- a/examples/test/test_i_basic_encryption.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2017-2019 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. -"""Unit test suite for the Strings examples in the AWS-hosted documentation.""" -import botocore.session -import pytest - -from ..src.basic_encryption import cycle_string -from .examples_test_utils import get_cmk_arn, static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_cycle_string(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - cycle_string(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) diff --git a/examples/test/test_i_basic_file_encryption_with_multiple_providers.py b/examples/test/test_i_basic_file_encryption_with_multiple_providers.py deleted file mode 100644 index 282a272ab..000000000 --- a/examples/test/test_i_basic_file_encryption_with_multiple_providers.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2017-2019 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. -"""Unit test suite for the Bytes Streams Multiple Providers examples in the AWS-hosted documentation.""" -import os -import tempfile - -import botocore.session -import pytest - -from ..src.basic_file_encryption_with_multiple_providers import cycle_file -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_cycle_file(): - cmk_arn = get_cmk_arn() - handle, filename = tempfile.mkstemp() - with open(filename, "wb") as f: - f.write(static_plaintext) - try: - new_files = cycle_file( - key_arn=cmk_arn, source_plaintext_filename=filename, botocore_session=botocore.session.Session() - ) - for f in new_files: - os.remove(f) - finally: - os.close(handle) - os.remove(filename) diff --git a/examples/test/test_i_basic_file_encryption_with_raw_key_provider.py b/examples/test/test_i_basic_file_encryption_with_raw_key_provider.py deleted file mode 100644 index 710c0ccac..000000000 --- a/examples/test/test_i_basic_file_encryption_with_raw_key_provider.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2017-2019 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. -"""Unit test suite for the Bytes Streams examples in the AWS-hosted documentation.""" -import os -import tempfile - -import pytest - -from ..src.basic_file_encryption_with_raw_key_provider import cycle_file -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_cycle_file(): - handle, filename = tempfile.mkstemp() - with open(filename, "wb") as f: - f.write(static_plaintext) - try: - new_files = cycle_file(source_plaintext_filename=filename) - for f in new_files: - os.remove(f) - finally: - os.close(handle) - os.remove(filename) diff --git a/examples/test/test_i_data_key_caching_basic.py b/examples/test/test_i_data_key_caching_basic.py deleted file mode 100644 index 734c35692..000000000 --- a/examples/test/test_i_data_key_caching_basic.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2017-2019 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. -"""Unit test suite for the basic data key caching example in the AWS-hosted documentation.""" -import pytest - -from ..src.data_key_caching_basic import encrypt_with_caching -from .examples_test_utils import get_cmk_arn - - -pytestmark = [pytest.mark.examples] - - -def test_encrypt_with_caching(): - cmk_arn = get_cmk_arn() - encrypt_with_caching(kms_cmk_arn=cmk_arn, max_age_in_cache=10.0, cache_capacity=10) diff --git a/examples/test/test_i_one_kms_cmk.py b/examples/test/test_i_one_kms_cmk.py deleted file mode 100644 index 71ce74d3d..000000000 --- a/examples/test/test_i_one_kms_cmk.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2019 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. -"""Unit test suite for the encryption and decryption using one KMS CMK example.""" - -import botocore.session -import pytest - -from ..src.one_kms_cmk import encrypt_decrypt -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - encrypt_decrypt(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) diff --git a/examples/test/test_i_one_kms_cmk_streaming_data.py b/examples/test/test_i_one_kms_cmk_streaming_data.py deleted file mode 100644 index b22fa4232..000000000 --- a/examples/test/test_i_one_kms_cmk_streaming_data.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2017-2019 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. -"""Unit test suite for the encryption and decryption of streaming data using one KMS CMK example.""" -import os -import tempfile - -import botocore.session -import pytest - -from ..src.one_kms_cmk_streaming_data import encrypt_decrypt_stream -from .examples_test_utils import get_cmk_arn, static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk_streaming_data(): - cmk_arn = get_cmk_arn() - handle, filename = tempfile.mkstemp() - with open(filename, "wb") as f: - f.write(static_plaintext) - try: - new_files = encrypt_decrypt_stream( - key_arn=cmk_arn, source_plaintext_filename=filename, botocore_session=botocore.session.Session() - ) - for f in new_files: - os.remove(f) - finally: - os.close(handle) - os.remove(filename) diff --git a/examples/test/test_i_one_kms_cmk_unsigned.py b/examples/test/test_i_one_kms_cmk_unsigned.py deleted file mode 100644 index 8a2758c96..000000000 --- a/examples/test/test_i_one_kms_cmk_unsigned.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2019 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. -"""Unit test suite for the encryption and decryption using one KMS CMK with an unsigned algorithm example.""" - -import botocore.session -import pytest - -from ..src.one_kms_cmk_unsigned import encrypt_decrypt -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk_unsigned(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - encrypt_decrypt(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) diff --git a/examples/test/test_run_examples.py b/examples/test/test_run_examples.py new file mode 100644 index 000000000..781f341ad --- /dev/null +++ b/examples/test/test_run_examples.py @@ -0,0 +1,24 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Test all examples.""" +from importlib import import_module + +import pytest + +from .examples_test_utils import all_examples, build_kwargs + +pytestmark = [pytest.mark.examples] + + +@pytest.mark.parametrize("import_path", all_examples()) +def test_examples(import_path, tmp_path): + module = import_module(name=import_path, package=__package__) + try: + run_function = module.run + except AttributeError: + pytest.skip("Module lacks 'run' function.") + return + + kwargs = build_kwargs(function=run_function, temp_dir=tmp_path) + + run_function(**kwargs) diff --git a/test/integration/README.rst b/test/integration/README.rst index 33ecbbedd..a7dcdd5ac 100644 --- a/test/integration/README.rst +++ b/test/integration/README.rst @@ -5,8 +5,11 @@ aws-encryption-sdk Integration Tests In order to run these integration tests successfully, these things must be configured. #. Ensure that AWS credentials are available in one of the `automatically discoverable credential locations`_. -#. Set environment variable ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID`` to valid +#. Set environment variable ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID`` + and ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2`` to valid `AWS KMS key id`_ to use for integration tests. + These should be AWS KMS CMK ARNs in two different regions. + They will be used for integration tests. .. _automatically discoverable credential locations: http://boto3.readthedocs.io/en/latest/guide/configuration.html .. _AWS KMS key id: http://docs.aws.amazon.com/kms/latest/APIReference/API_Encrypt.html diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index 37e0243b7..4928f3637 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -20,25 +20,35 @@ from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring AWS_KMS_KEY_ID = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID" +AWS_KMS_KEY_ID_2 = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2" _KMS_MKP = None _KMS_MKP_BOTO = None _KMS_KEYRING = None -def get_cmk_arn(): - """Retrieves the target CMK ARN from environment variable.""" - arn = os.environ.get(AWS_KMS_KEY_ID, None) +def _get_single_cmk_arn(name): + # type: (str) -> str + """Retrieve a single target AWS KMS CMK ARN from the specified environment variable name.""" + arn = os.environ.get(name, None) if arn is None: raise ValueError( - 'Environment variable "{}" must be set to a valid KMS CMK ARN for integration tests to run'.format( - AWS_KMS_KEY_ID - ) + 'Environment variable "{}" must be set to a valid KMS CMK ARN for integration tests to run'.format(name) ) 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") +def get_cmk_arn(): + """Retrieves the target AWS KMS CMK ARN from environment variable.""" + return _get_single_cmk_arn(AWS_KMS_KEY_ID) + + +def get_all_cmk_arns(): + """Retrieve all known target AWS KMS CMK ARNs from environment variables.""" + return [_get_single_cmk_arn(AWS_KMS_KEY_ID), _get_single_cmk_arn(AWS_KMS_KEY_ID_2)] + + def setup_kms_master_key_provider(cache=True): """Build an AWS KMS Master Key Provider.""" global _KMS_MKP # pylint: disable=global-statement @@ -47,7 +57,7 @@ def setup_kms_master_key_provider(cache=True): cmk_arn = get_cmk_arn() kms_master_key_provider = KMSMasterKeyProvider() - kms_master_key_provider.add_master_key(cmk_arn) + kms_master_key_provider.add_master_key(cmk_arn.encode("utf-8")) if cache: _KMS_MKP = kms_master_key_provider @@ -63,7 +73,7 @@ def setup_kms_master_key_provider_with_botocore_session(cache=True): cmk_arn = get_cmk_arn() kms_master_key_provider = KMSMasterKeyProvider(botocore_session=botocore.session.Session()) - kms_master_key_provider.add_master_key(cmk_arn) + kms_master_key_provider.add_master_key(cmk_arn.encode("utf-8")) if cache: _KMS_MKP_BOTO = kms_master_key_provider diff --git a/tox.ini b/tox.ini index d9d60d756..7895b3a0d 100644 --- a/tox.ini +++ b/tox.ini @@ -40,8 +40,9 @@ commands = pytest --basetemp={envtmpdir} -l --cov aws_encryption_sdk {posargs} [testenv] passenv = - # Identifies AWS KMS key id to use in integration tests + # Identifies AWS KMS key ids to use in integration tests AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID \ + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2 \ # Pass through AWS credentials AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN \ # Pass through AWS profile name (useful for local testing) @@ -140,6 +141,10 @@ commands = [testenv:flake8-examples] basepython = {[testenv:flake8]basepython} +# This does not actually ignore errors, +# it just runs all commands regardless of whether any fail. +# If any fail, the final result will still fail. +ignore_errors = true deps = {[testenv:flake8]deps} commands = flake8 examples/src/ @@ -164,6 +169,10 @@ commands = [testenv:pylint-examples] basepython = {[testenv:pylint]basepython} +# This does not actually ignore errors, +# it just runs all commands regardless of whether any fail. +# If any fail, the final result will still fail. +ignore_errors = true deps = {[testenv:pylint]deps} commands = pylint --rcfile=examples/src/pylintrc examples/src/ From 1bd5fa6ccb184ddc768c57a76869284ce3f6919c Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Fri, 20 Mar 2020 11:06:03 -0700 Subject: [PATCH 41/64] feat: expose keyring trace in results (#224) * feat: add CryptoResult and plumb in use * chore: simplify test parametrization * chore: adjust RawKeyring functional tests to use CryptoResult and check keyring trace * chore: add unit tests for CryptoResult * docs: fix docstrings and typehints in structures * chore: verify that CryptoResult casts to a tuple as expected * docs: add keyrings link and clarify how CryptoResult could break you --- CHANGELOG.rst | 20 ++++ src/aws_encryption_sdk/__init__.py | 34 ++++-- .../materials_managers/__init__.py | 6 +- src/aws_encryption_sdk/streaming_client.py | 4 + src/aws_encryption_sdk/structures.py | 66 ++++++++--- test/functional/test_client.py | 103 +++++++++++------- test/unit/test_client.py | 21 ++-- test/unit/test_structures.py | 71 +++++++++++- 8 files changed, 247 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9bca1f73..c02fb8017 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,25 @@ Changelog ********* +1.5.0 -- 2020-xx-xx +=================== + +Major Features +-------------- + +* Add `keyrings`_. +* Change one-step APIs to return a :class:`CryptoResult` rather than a tuple. + * Modified APIs: ``aws_encryption_sdk.encrypt`` and ``aws_encryption_sdk.decrypt``. + +.. note:: + + For backwards compatibility, + :class:`CryptoResult` also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs + so this change should not break any existing consumers + unless you are specifically relying on the output being an instance of :class:`tuple`. + + 1.4.1 -- 2019-09-20 =================== @@ -193,3 +212,4 @@ Minor .. _pylint: https://www.pylint.org/ .. _flake8: http://flake8.pycqa.org/en/latest/ .. _doc8: https://launchpad.net/doc8 +.. _keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html diff --git a/src/aws_encryption_sdk/__init__.py b/src/aws_encryption_sdk/__init__.py index 8cadd51b8..cc9b7b9f7 100644 --- a/src/aws_encryption_sdk/__init__.py +++ b/src/aws_encryption_sdk/__init__.py @@ -14,6 +14,9 @@ StreamDecryptor, StreamEncryptor, ) +from aws_encryption_sdk.structures import CryptoResult + +__all__ = ("encrypt", "decrypt", "stream") def encrypt(**kwargs): @@ -26,6 +29,13 @@ def encrypt(**kwargs): .. versionadded:: 1.5.0 The *keyring* parameter. + .. versionadded:: 1.5.0 + + For backwards compatibility, + the new :class:`CryptoResult` return value also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs + so this change should not break any existing consumers. + .. code:: python >>> import aws_encryption_sdk @@ -67,12 +77,13 @@ def encrypt(**kwargs): :param algorithm: Algorithm to use for encryption :type algorithm: aws_encryption_sdk.identifiers.Algorithm :param int frame_length: Frame length in bytes - :returns: Tuple containing the encrypted ciphertext and the message header object - :rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader` + :returns: Encrypted message, message metadata (header), and keyring trace + :rtype: CryptoResult """ with StreamEncryptor(**kwargs) as encryptor: ciphertext = encryptor.read() - return ciphertext, encryptor.header + + return CryptoResult(result=ciphertext, header=encryptor.header, keyring_trace=encryptor.keyring_trace) def decrypt(**kwargs): @@ -85,6 +96,13 @@ def decrypt(**kwargs): .. versionadded:: 1.5.0 The *keyring* parameter. + .. versionadded:: 1.5.0 + + For backwards compatibility, + the new :class:`CryptoResult` return value also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs + so this change should not break any existing consumers. + .. code:: python >>> import aws_encryption_sdk @@ -117,12 +135,13 @@ def decrypt(**kwargs): :param int max_body_length: Maximum frame size (or content length for non-framed messages) in bytes to read from ciphertext message. - :returns: Tuple containing the decrypted plaintext and the message header object - :rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader` + :returns: Decrypted plaintext, message metadata (header), and keyring trace + :rtype: CryptoResult """ with StreamDecryptor(**kwargs) as decryptor: plaintext = decryptor.read() - return plaintext, decryptor.header + + return CryptoResult(result=plaintext, header=decryptor.header, keyring_trace=decryptor.keyring_trace) def stream(**kwargs): @@ -182,6 +201,3 @@ def stream(**kwargs): return _stream_map[mode.lower()](**kwargs) except KeyError: raise ValueError("Unsupported mode: {}".format(mode)) - - -__all__ = ("encrypt", "decrypt", "stream") diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index e6e86c5cd..bd9ffb19e 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 Any, FrozenSet, Iterable, Tuple, Union # noqa pylint: disable=unused-import + from typing import Any, 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 @@ -238,10 +238,10 @@ def __init__( @property def encrypted_data_keys(self): - # type: () -> FrozenSet[EncryptedDataKey] + # type: () -> Tuple[EncryptedDataKey] """Return a read-only version of the encrypted data keys. - :rtype: frozenset + :rtype: Tuple[EncryptedDataKey] """ return tuple(self._encrypted_data_keys) diff --git a/src/aws_encryption_sdk/streaming_client.py b/src/aws_encryption_sdk/streaming_client.py index d22fb7c70..ff549563d 100644 --- a/src/aws_encryption_sdk/streaming_client.py +++ b/src/aws_encryption_sdk/streaming_client.py @@ -137,6 +137,7 @@ class _EncryptionStream(io.IOBase): _message_prepped = None # type: bool source_stream = None _stream_length = None # type: int + keyring_trace = () def __new__(cls, **kwargs): """Perform necessary handling for _EncryptionStream instances that should be @@ -443,6 +444,7 @@ def _prep_message(self): self._encryption_materials = self.config.materials_manager.get_encryption_materials( request=encryption_materials_request ) + self.keyring_trace = self._encryption_materials.keyring_trace if self.config.algorithm is not None and self._encryption_materials.algorithm != self.config.algorithm: raise ActionNotAllowedError( @@ -779,6 +781,8 @@ def _read_header(self): encryption_context=header.encryption_context, ) decryption_materials = self.config.materials_manager.decrypt_materials(request=decrypt_materials_request) + self.keyring_trace = decryption_materials.keyring_trace + if decryption_materials.verification_key is None: self.verifier = None else: diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index ea9253b39..35eab24e6 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -20,6 +20,12 @@ from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion from aws_encryption_sdk.internal.str_ops import to_bytes, to_str +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Tuple # 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=True) class MasterKeyInfo(object): @@ -107,8 +113,7 @@ class KeyringTrace(object): .. versionadded:: 1.5.0 :param MasterKeyInfo wrapping_key: Wrapping key used - :param flags: Actions performed - :type flags: set of :class:`KeyringTraceFlag` + :param Set[KeyringTraceFlag] flags: Actions performed """ wrapping_key = attr.ib(validator=instance_of(MasterKeyInfo)) @@ -120,19 +125,14 @@ class MessageHeader(object): # pylint: disable=too-many-instance-attributes """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 SerializationVersion version: Message format version, per spec + :param ObjectType type: Message content type, per spec + :param AlgorithmSuite algorithm: Algorithm to use for encryption :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 Dict[str,str] encryption_context: Dictionary defining encryption context + :param Sequence[EncryptedDataKey] encrypted_data_keys: Encrypted data keys + :param ContentType content_type: Message content framing type (framed/non-framed) + :param int 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 """ @@ -152,3 +152,41 @@ class MessageHeader(object): 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)) + + +@attr.s +class CryptoResult(object): + """Result container for one-shot cryptographic API results. + + .. versionadded:: 1.5.0 + + .. note:: + + For backwards compatibility, + this container also unpacks like a 2-member tuple. + This allows for backwards compatibility with the previous outputs. + + :param bytes result: Binary results of the cryptographic operation + :param MessageHeader header: Encrypted message metadata + :param Tuple[KeyringTrace] keyring_trace: Keyring trace entries + """ + + result = attr.ib(validator=instance_of(bytes)) + header = attr.ib(validator=instance_of(MessageHeader)) + keyring_trace = attr.ib(validator=deep_iterable(member_validator=instance_of(KeyringTrace))) + + def __attrs_post_init__(self): + """Construct the inner tuple for backwards compatibility.""" + self._legacy_container = (self.result, self.header) + + def __len__(self): + """Emulate the inner tuple.""" + return self._legacy_container.__len__() + + def __iter__(self): + """Emulate the inner tuple.""" + return self._legacy_container.__iter__() + + def __getitem__(self, key): + """Emulate the inner tuple.""" + return self._legacy_container.__getitem__(key) diff --git a/test/functional/test_client.py b/test/functional/test_client.py index 3888479d4..ebe7e14d1 100644 --- a/test/functional/test_client.py +++ b/test/functional/test_client.py @@ -27,6 +27,7 @@ from aws_encryption_sdk.internal.formatting.encryption_context import serialize_encryption_context from aws_encryption_sdk.key_providers.base import MasterKeyProvider, MasterKeyProviderConfig from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider +from aws_encryption_sdk.keyrings.base import Keyring from aws_encryption_sdk.keyrings.raw import RawRSAKeyring from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest @@ -314,12 +315,20 @@ def test_encrypt_ciphertext_message(frame_length, algorithm, encryption_context) assert len(ciphertext) == results_length -def _raw_aes(): +def _raw_aes(include_mkp=True): for symmetric_algorithm in ( WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, ): + keyring = ephemeral_raw_aes_keyring(symmetric_algorithm) + yield pytest.param( + "keyring", keyring, "keyring", keyring, id="raw AES keyring -- {}".format(symmetric_algorithm.name) + ) + + if not include_mkp: + continue + yield pytest.param( "key_provider", build_fake_raw_key_provider(symmetric_algorithm, EncryptionKeyType.SYMMETRIC), @@ -327,10 +336,6 @@ def _raw_aes(): build_fake_raw_key_provider(symmetric_algorithm, EncryptionKeyType.SYMMETRIC), id="raw AES master key provider -- {}".format(symmetric_algorithm.name), ) - keyring = ephemeral_raw_aes_keyring(symmetric_algorithm) - yield pytest.param( - "keyring", keyring, "keyring", keyring, id="raw AES keyring -- {}".format(symmetric_algorithm.name) - ) mkp = ephemeral_raw_aes_master_key(wrapping_algorithm=symmetric_algorithm, key=keyring._wrapping_key) yield pytest.param( @@ -349,7 +354,7 @@ def _raw_aes(): ) -def _raw_rsa(include_pre_sha2=True, include_sha2=True): +def _raw_rsa(include_pre_sha2=True, include_sha2=True, include_mkp=True): wrapping_algorithms = [] if include_pre_sha2: wrapping_algorithms.extend([WrappingAlgorithm.RSA_PKCS1, WrappingAlgorithm.RSA_OAEP_SHA1_MGF1]) @@ -362,21 +367,6 @@ def _raw_rsa(include_pre_sha2=True, include_sha2=True): ] ) for wrapping_algorithm in wrapping_algorithms: - yield pytest.param( - "key_provider", - build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), - "key_provider", - build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), - id="raw RSA master key provider -- private encrypt, private decrypt -- {}".format(wrapping_algorithm.name), - ) - yield pytest.param( - "key_provider", - build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PUBLIC), - "key_provider", - build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), - id="raw RSA master key provider -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), - ) - private_keyring = ephemeral_raw_rsa_keyring(wrapping_algorithm=wrapping_algorithm) public_keyring = RawRSAKeyring( key_namespace=private_keyring.key_namespace, @@ -398,8 +388,27 @@ def _raw_rsa(include_pre_sha2=True, include_sha2=True): private_keyring, id="raw RSA keyring -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), ) + + if not include_mkp: + continue + private_mkp, public_mkp = raw_rsa_mkps_from_keyring(private_keyring) + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + id="raw RSA master key provider -- private encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + yield pytest.param( + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PUBLIC), + "key_provider", + build_fake_raw_key_provider(wrapping_algorithm, EncryptionKeyType.PRIVATE), + id="raw RSA master key provider -- public encrypt, private decrypt -- {}".format(wrapping_algorithm.name), + ) + yield pytest.param( "key_provider", private_mkp, @@ -452,17 +461,39 @@ def run_raw_provider_check( encrypt_kwargs = {encrypt_param_name: encrypting_provider} decrypt_kwargs = {decrypt_param_name: decrypting_provider} - ciphertext, _ = aws_encryption_sdk.encrypt( + encrypt_result = aws_encryption_sdk.encrypt( source=VALUES["plaintext_128"], encryption_context=VALUES["encryption_context"], frame_length=0, **encrypt_kwargs ) - plaintext, _ = aws_encryption_sdk.decrypt(source=ciphertext, **decrypt_kwargs) + decrypt_result = aws_encryption_sdk.decrypt(source=encrypt_result.result, **decrypt_kwargs) + + if isinstance(encrypting_provider, Keyring): + trace_entries = ( + entry + for entry in encrypt_result.keyring_trace + if ( + entry.wrapping_key.provider_id == encrypting_provider.key_namespace + and entry.wrapping_key.key_info == encrypting_provider.key_name + ) + ) + assert trace_entries - assert plaintext == VALUES["plaintext_128"] + assert decrypt_result.result == VALUES["plaintext_128"] assert_key_not_logged(encrypting_provider, log_capturer.text) + if isinstance(decrypting_provider, Keyring): + trace_entries = ( + entry + for entry in decrypt_result.keyring_trace + if ( + entry.wrapping_key.provider_id == decrypting_provider.key_namespace + and entry.wrapping_key.key_info == decrypting_provider.key_name + ) + ) + assert trace_entries + @pytest.mark.parametrize( "encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider", @@ -486,15 +517,9 @@ def test_encryption_cycle_raw_mkp_openssl_102_plus( run_raw_provider_check(caplog, encrypt_param_name, encrypting_provider, decrypt_param_name, decrypting_provider) -@pytest.mark.parametrize( - "frame_length, algorithm, encryption_context", - [ - [frame_length, algorithm_suite, encryption_context] - for frame_length in VALUES["frame_lengths"] - for algorithm_suite in Algorithm - for encryption_context in [{}, VALUES["encryption_context"]] - ], -) +@pytest.mark.parametrize("frame_length", VALUES["frame_lengths"]) +@pytest.mark.parametrize("algorithm", Algorithm) +@pytest.mark.parametrize("encryption_context", [{}, VALUES["encryption_context"]]) def test_encryption_cycle_oneshot_kms(frame_length, algorithm, encryption_context): key_provider = fake_kms_key_provider(algorithm.kdf_input_len) @@ -511,15 +536,9 @@ def test_encryption_cycle_oneshot_kms(frame_length, algorithm, encryption_contex assert plaintext == VALUES["plaintext_128"] * 10 -@pytest.mark.parametrize( - "frame_length, algorithm, encryption_context", - [ - [frame_length, algorithm_suite, encryption_context] - for frame_length in VALUES["frame_lengths"] - for algorithm_suite in Algorithm - for encryption_context in [{}, VALUES["encryption_context"]] - ], -) +@pytest.mark.parametrize("frame_length", VALUES["frame_lengths"]) +@pytest.mark.parametrize("algorithm", Algorithm) +@pytest.mark.parametrize("encryption_context", [{}, VALUES["encryption_context"]]) def test_encryption_cycle_stream_kms(frame_length, algorithm, encryption_context): key_provider = fake_kms_key_provider(algorithm.kdf_input_len) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 38dfff85a..d6b763b49 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -17,7 +17,12 @@ import aws_encryption_sdk import aws_encryption_sdk.internal.defaults +from .vectors import VALUES + pytestmark = [pytest.mark.unit, pytest.mark.local] +_CIPHERTEXT = b"CIPHERTEXT" +_PLAINTEXT = b"PLAINTEXT" +_HEADER = VALUES["deserialized_header_frame"] class TestAwsEncryptionSdk(object): @@ -27,16 +32,16 @@ def apply_fixtures(self): self.mock_stream_encryptor_patcher = patch("aws_encryption_sdk.StreamEncryptor") self.mock_stream_encryptor = self.mock_stream_encryptor_patcher.start() self.mock_stream_encryptor_instance = MagicMock() - self.mock_stream_encryptor_instance.read.return_value = sentinel.ciphertext - self.mock_stream_encryptor_instance.header = sentinel.header + self.mock_stream_encryptor_instance.read.return_value = _CIPHERTEXT + self.mock_stream_encryptor_instance.header = _HEADER self.mock_stream_encryptor.return_value = self.mock_stream_encryptor_instance self.mock_stream_encryptor_instance.__enter__.return_value = self.mock_stream_encryptor_instance # Set up StreamDecryptor patch self.mock_stream_decryptor_patcher = patch("aws_encryption_sdk.StreamDecryptor") self.mock_stream_decryptor = self.mock_stream_decryptor_patcher.start() self.mock_stream_decryptor_instance = MagicMock() - self.mock_stream_decryptor_instance.read.return_value = sentinel.plaintext - self.mock_stream_decryptor_instance.header = sentinel.header + self.mock_stream_decryptor_instance.read.return_value = _PLAINTEXT + self.mock_stream_decryptor_instance.header = _HEADER self.mock_stream_decryptor.return_value = self.mock_stream_decryptor_instance self.mock_stream_decryptor_instance.__enter__.return_value = self.mock_stream_decryptor_instance yield @@ -47,14 +52,14 @@ def apply_fixtures(self): def test_encrypt(self): test_ciphertext, test_header = aws_encryption_sdk.encrypt(a=sentinel.a, b=sentinel.b, c=sentinel.b) self.mock_stream_encryptor.called_once_with(a=sentinel.a, b=sentinel.b, c=sentinel.b) - assert test_ciphertext is sentinel.ciphertext - assert test_header is sentinel.header + assert test_ciphertext is _CIPHERTEXT + assert test_header is _HEADER def test_decrypt(self): test_plaintext, test_header = aws_encryption_sdk.decrypt(a=sentinel.a, b=sentinel.b, c=sentinel.b) self.mock_stream_encryptor.called_once_with(a=sentinel.a, b=sentinel.b, c=sentinel.b) - assert test_plaintext is sentinel.plaintext - assert test_header is sentinel.header + assert test_plaintext is _PLAINTEXT + assert test_header is _HEADER def test_stream_encryptor_e(self): test = aws_encryption_sdk.stream(mode="e", a=sentinel.a, b=sentinel.b, c=sentinel.b) diff --git a/test/unit/test_structures.py b/test/unit/test_structures.py index e1070c574..f047d5f11 100644 --- a/test/unit/test_structures.py +++ b/test/unit/test_structures.py @@ -13,8 +13,16 @@ """Unit test suite for aws_encryption_sdk.structures""" import pytest -from aws_encryption_sdk.identifiers import Algorithm, ContentType, ObjectType, SerializationVersion -from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo, MessageHeader, RawDataKey +from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion +from aws_encryption_sdk.structures import ( + CryptoResult, + DataKey, + EncryptedDataKey, + KeyringTrace, + MasterKeyInfo, + MessageHeader, + RawDataKey, +) from .unit_test_utils import all_invalid_kwargs, all_valid_kwargs @@ -57,6 +65,35 @@ key_provider=MasterKeyInfo(provider_id="asjnoa", key_info=b"aosjfoaiwej"), encrypted_data_key=b"aisofiawjef" ) ], + KeyringTrace: [ + dict( + wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), + flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY}, + ) + ], + CryptoResult: [ + dict( + result=b"super secret stuff", + header=MessageHeader( + version=SerializationVersion.V1, + type=ObjectType.CUSTOMER_AE_DATA, + algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, + message_id=b"aosiejfoaiwej", + encryption_context={}, + encrypted_data_keys=set([]), + content_type=ContentType.FRAMED_DATA, + content_aad_length=32456, + header_iv_length=32456, + frame_length=234567, + ), + keyring_trace=( + KeyringTrace( + wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), + flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY}, + ), + ), + ) + ], } @@ -134,3 +171,33 @@ def test_raw_and_encrypted_data_key_from_data_key_fail(data_key_class): data_key_class.from_data_key(b"ahjseofij") excinfo.match(r"data_key must be type DataKey not *") + + +@pytest.fixture +def ex_result(): + return CryptoResult(**VALID_KWARGS[CryptoResult][0]) + + +def test_cryptoresult_len(ex_result): + assert len(ex_result) == 2 + + +def test_cryptoresult_unpack(ex_result): + data, header = ex_result + + assert data is ex_result.result + assert header is ex_result.header + + +def test_cryptoresult_getitem(ex_result): + data = ex_result[0] + header = ex_result[1] + + assert data is ex_result.result + assert header is ex_result.header + + +def test_cryptoresult_to_tuple(ex_result): + test = tuple(ex_result) + + assert test == ex_result._legacy_container From 31e4e4940bc4343dede6a3229567968eb63b7a23 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Mon, 23 Mar 2020 13:30:17 -0700 Subject: [PATCH 42/64] feat: remove specific value definition for keyring trace flags #215 (#225) * feat: remove specific value definition for keyring trace flags #215 * chore: autoformat * feat: rename keyring trace flags to match names in specification * docs: add docs explaining each keyring trace flag * chore: fix linting --- src/aws_encryption_sdk/identifiers.py | 25 +++++++++++++---- .../keyrings/aws_kms/__init__.py | 6 ++-- src/aws_encryption_sdk/keyrings/raw.py | 14 ++++------ .../materials_managers/__init__.py | 6 ++-- .../keyrings/aws_kms/test_aws_kms.py | 28 +++++++++---------- test/functional/keyrings/raw/test_raw_aes.py | 2 +- test/functional/keyrings/raw/test_raw_rsa.py | 2 +- test/functional/keyrings/test_multi.py | 2 +- test/unit/keyrings/raw/test_raw_aes.py | 20 ++++++------- test/unit/keyrings/raw/test_raw_rsa.py | 20 ++++++------- .../test_material_managers.py | 18 ++++++------ test/unit/test_structures.py | 5 ++-- test/unit/unit_test_utils.py | 22 +++++---------- 13 files changed, 85 insertions(+), 85 deletions(-) diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 87d5919a1..269afd702 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -14,6 +14,7 @@ import struct from enum import Enum +import attr from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa from cryptography.hazmat.primitives.ciphers import algorithms, modes @@ -333,8 +334,22 @@ class ContentAADString(Enum): 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 + @attr.s + class KeyringTraceFlagValue(object): + """Keyring trace flags do not have defined serializable values.""" + + name = attr.ib() + + #: A flag to represent that a keyring has generated a plaintext data key. + GENERATED_DATA_KEY = KeyringTraceFlagValue("GENERATED_DATA_KEY") + #: A flag to represent that a keyring has created an encrypted data key. + ENCRYPTED_DATA_KEY = KeyringTraceFlagValue("ENCRYPTED_DATA_KEY") + #: A flag to represent that a keyring has obtained + #: the corresponding plaintext data key from an encrypted data key. + DECRYPTED_DATA_KEY = KeyringTraceFlagValue("DECRYPTED_DATA_KEY") + #: A flag to represent that the keyring has cryptographically + #: bound the encryption context to a newly created encrypted data key. + SIGNED_ENCRYPTION_CONTEXT = KeyringTraceFlagValue("SIGNED_ENCRYPTION_CONTEXT") + #: A flag to represent that the keyring has verified that an encrypted + #: data key was originally created with a particular encryption context. + VERIFIED_ENCRYPTION_CONTEXT = KeyringTraceFlagValue("VERIFIED_ENCRYPTION_CONTEXT") diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 808dde163..b8cb94b81 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -34,9 +34,9 @@ _LOGGER = logging.getLogger(__name__) _PROVIDER_ID = "aws-kms" -_GENERATE_FLAGS = {KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY} -_ENCRYPT_FLAGS = {KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX} -_DECRYPT_FLAGS = {KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY, KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX} +_GENERATE_FLAGS = {KeyringTraceFlag.GENERATED_DATA_KEY} +_ENCRYPT_FLAGS = {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} +_DECRYPT_FLAGS = {KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT} @attr.s diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index 1f4eebe14..fcf793f5f 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -55,7 +55,7 @@ def _generate_data_key( raise GenerateKeyError("Unable to generate data encryption key.") # Create a keyring trace - keyring_trace = KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}) + keyring_trace = KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.GENERATED_DATA_KEY}) # plaintext_data_key to RawDataKey data_encryption_key = RawDataKey(key_provider=key_provider, data_key=plaintext_data_key) @@ -150,7 +150,7 @@ def on_encrypt(self, encryption_materials): # Update Keyring Trace keyring_trace = KeyringTrace( - wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} ) # Add encrypted data key to encryption_materials @@ -201,9 +201,7 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): return decryption_materials # Create a keyring trace - keyring_trace = KeyringTrace( - wrapping_key=self._key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY} - ) + keyring_trace = KeyringTrace(wrapping_key=self._key_provider, flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}) # Update decryption materials data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) @@ -367,7 +365,7 @@ def on_encrypt(self, encryption_materials): # Update Keyring Trace keyring_trace = KeyringTrace( - wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY} + wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} ) # Add encrypted data key to encryption_materials @@ -408,9 +406,7 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): continue # Create a keyring trace - keyring_trace = KeyringTrace( - wrapping_key=self._key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY} - ) + keyring_trace = KeyringTrace(wrapping_key=self._key_provider, flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}) # Update decryption materials data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index bd9ffb19e..28ea40c0d 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -280,7 +280,7 @@ def add_data_encryption_key(self, data_encryption_key, keyring_trace): self._add_data_encryption_key( data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, - required_flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + required_flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): @@ -299,7 +299,7 @@ def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): 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: + if KeyringTraceFlag.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: @@ -445,7 +445,7 @@ def add_data_encryption_key(self, data_encryption_key, keyring_trace): self._add_data_encryption_key( data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, - required_flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY}, + required_flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, ) def add_verification_key(self, verification_key): diff --git a/test/functional/keyrings/aws_kms/test_aws_kms.py b/test/functional/keyrings/aws_kms/test_aws_kms.py index 19d0d68ad..7a5b50d51 100644 --- a/test/functional/keyrings/aws_kms/test_aws_kms.py +++ b/test/functional/keyrings/aws_kms/test_aws_kms.py @@ -61,9 +61,9 @@ def test_aws_kms_single_cmk_keyring_on_encrypt_empty_materials(fake_generator): MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace ) - assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX in generator_flags + assert KeyringTraceFlag.GENERATED_DATA_KEY in generator_flags + assert KeyringTraceFlag.ENCRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT in generator_flags def test_aws_kms_single_cmk_keyring_on_encrypt_existing_data_key(fake_generator): @@ -86,9 +86,9 @@ def test_aws_kms_single_cmk_keyring_on_encrypt_existing_data_key(fake_generator) MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace ) - assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY not in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_SIGNED_ENC_CTX in generator_flags + assert KeyringTraceFlag.GENERATED_DATA_KEY not in generator_flags + assert KeyringTraceFlag.ENCRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT in generator_flags @mock_kms @@ -155,8 +155,8 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_single_cmk(fake_generator): MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace ) - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in generator_flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in generator_flags def test_aws_kms_single_cmk_keyring_on_decrypt_multiple_cmk(fake_generator_and_child): @@ -186,8 +186,8 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_multiple_cmk(fake_generator_and_c MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=child), result_materials.keyring_trace ) - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in child_flags - assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in child_flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in child_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in child_flags def test_aws_kms_single_cmk_keyring_on_decrypt_no_match(fake_generator_and_child): @@ -274,8 +274,8 @@ def test_aws_kms_discovery_keyring_on_decrypt(encryption_materials_for_discovery MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=generator_key_id), result_materials.keyring_trace ) - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in generator_flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in generator_flags @mock_kms @@ -380,8 +380,8 @@ def test_try_aws_kms_decrypt_succeed(fake_generator): MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace ) - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in generator_flags - assert KeyringTraceFlag.WRAPPING_KEY_VERIFIED_ENC_CTX in generator_flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags + assert KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT in generator_flags @mock_kms diff --git a/test/functional/keyrings/raw/test_raw_aes.py b/test/functional/keyrings/raw/test_raw_aes.py index 0cfad9d2c..5e181f803 100644 --- a/test/functional/keyrings/raw/test_raw_aes.py +++ b/test/functional/keyrings/raw/test_raw_aes.py @@ -57,7 +57,7 @@ def sample_encryption_materials(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], ), diff --git a/test/functional/keyrings/raw/test_raw_rsa.py b/test/functional/keyrings/raw/test_raw_rsa.py index d900c5cdf..b7a278d2b 100644 --- a/test/functional/keyrings/raw/test_raw_rsa.py +++ b/test/functional/keyrings/raw/test_raw_rsa.py @@ -133,7 +133,7 @@ def sample_encryption_materials(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], ), diff --git a/test/functional/keyrings/test_multi.py b/test/functional/keyrings/test_multi.py index c6de06f0e..db98eb5a8 100644 --- a/test/functional/keyrings/test_multi.py +++ b/test/functional/keyrings/test_multi.py @@ -44,7 +44,7 @@ keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], ) diff --git a/test/unit/keyrings/raw/test_raw_aes.py b/test/unit/keyrings/raw/test_raw_aes.py index 43de2a697..33a882918 100644 --- a/test/unit/keyrings/raw/test_raw_aes.py +++ b/test/unit/keyrings/raw/test_raw_aes.py @@ -129,8 +129,8 @@ def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_aes_keyring for keyring_trace in test.keyring_trace: if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY - assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY not in keyring_trace.flags + # Check keyring trace does not contain KeyringTraceFlag.GENERATED_DATA_KEY + assert KeyringTraceFlag.GENERATED_DATA_KEY not in keyring_trace.flags def test_on_encrypt_when_data_encryption_key_not_given(raw_aes_keyring): @@ -152,11 +152,11 @@ def test_on_encrypt_when_data_encryption_key_not_given(raw_aes_keyring): for keyring_trace in test.keyring_trace: if ( keyring_trace.wrapping_key.key_info == _KEY_ID - and KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in keyring_trace.flags + and KeyringTraceFlag.GENERATED_DATA_KEY in keyring_trace.flags ): - # Check keyring trace contains KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + # Check keyring trace contains KeyringTraceFlag.GENERATED_DATA_KEY generated_flag_count += 1 - if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in keyring_trace.flags: + if KeyringTraceFlag.ENCRYPTED_DATA_KEY in keyring_trace.flags: encrypted_flag_count += 1 assert generated_flag_count == 1 @@ -187,8 +187,8 @@ def test_keyring_trace_on_decrypt_when_data_key_given(raw_aes_keyring): ) for keyring_trace in test.keyring_trace: if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + # Check keyring trace does not contain KeyringTraceFlag.DECRYPTED_DATA_KEY + assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags @pytest.mark.parametrize( @@ -208,7 +208,7 @@ def test_on_decrypt_when_data_key_and_edk_not_provided( for keyring_trace in test.keyring_trace: if keyring_trace.wrapping_key.key_info == _KEY_ID: - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags assert test.data_encryption_key is None @@ -235,7 +235,7 @@ def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_aes_keyri decrypted_flag_count = 0 for keyring_trace in test.keyring_trace: - if KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in keyring_trace.flags: + if KeyringTraceFlag.DECRYPTED_DATA_KEY in keyring_trace.flags: decrypted_flag_count += 1 assert decrypted_flag_count == 1 @@ -277,6 +277,6 @@ def test_generate_data_key_keyring_trace(): generate_flag_count = 0 for keyring_trace in encryption_materials_without_data_key.keyring_trace: - if KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in keyring_trace.flags: + if KeyringTraceFlag.GENERATED_DATA_KEY in keyring_trace.flags: generate_flag_count += 1 assert generate_flag_count == 1 diff --git a/test/unit/keyrings/raw/test_raw_rsa.py b/test/unit/keyrings/raw/test_raw_rsa.py index 460b7280f..d9aa7b266 100644 --- a/test/unit/keyrings/raw/test_raw_rsa.py +++ b/test/unit/keyrings/raw/test_raw_rsa.py @@ -130,8 +130,8 @@ def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_rsa_keyring for keyring_trace in test.keyring_trace: if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY - assert KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY not in keyring_trace.flags + # Check keyring trace does not contain KeyringTraceFlag.GENERATED_DATA_KEY + assert KeyringTraceFlag.GENERATED_DATA_KEY not in keyring_trace.flags def test_on_encrypt_when_data_encryption_key_not_given(raw_rsa_keyring): @@ -152,11 +152,11 @@ def test_on_encrypt_when_data_encryption_key_not_given(raw_rsa_keyring): for keyring_trace in test.keyring_trace: if ( keyring_trace.wrapping_key.key_info == _KEY_ID - and KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY in keyring_trace.flags + and KeyringTraceFlag.GENERATED_DATA_KEY in keyring_trace.flags ): - # Check keyring trace contains KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY + # Check keyring trace contains KeyringTraceFlag.GENERATED_DATA_KEY generated_flag_count += 1 - if KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY in keyring_trace.flags: + if KeyringTraceFlag.ENCRYPTED_DATA_KEY in keyring_trace.flags: encrypted_flag_count += 1 assert generated_flag_count == 1 @@ -183,8 +183,8 @@ def test_keyring_trace_on_decrypt_when_data_key_given(raw_rsa_keyring): ) for keyring_trace in test.keyring_trace: if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + # Check keyring trace does not contain KeyringTraceFlag.DECRYPTED_DATA_KEY + assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags def test_on_decrypt_when_data_key_and_edk_not_provided(raw_rsa_keyring, patch_decrypt_on_wrapping_key): @@ -196,7 +196,7 @@ def test_on_decrypt_when_data_key_and_edk_not_provided(raw_rsa_keyring, patch_de assert not patch_decrypt_on_wrapping_key.called for keyring_trace in test.keyring_trace: - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags assert test.data_encryption_key is None @@ -212,7 +212,7 @@ def test_on_decrypt_when_data_key_not_provided_and_edk_not_in_keyring(raw_rsa_ke for keyring_trace in test.keyring_trace: if keyring_trace.wrapping_key.key_info == _KEY_ID: - assert KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY not in keyring_trace.flags + assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags assert test.data_encryption_key is None @@ -242,7 +242,7 @@ def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_rsa_keyri decrypted_flag_count = 0 for keyring_trace in test.keyring_trace: - if KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY in keyring_trace.flags: + if KeyringTraceFlag.DECRYPTED_DATA_KEY in keyring_trace.flags: decrypted_flag_count += 1 assert decrypted_flag_count == 1 diff --git a/test/unit/materials_managers/test_material_managers.py b/test/unit/materials_managers/test_material_managers.py index 975e9ffda..499f7ba0d 100644 --- a/test/unit/materials_managers/test_material_managers.py +++ b/test/unit/materials_managers/test_material_managers.py @@ -54,7 +54,7 @@ keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id="Provider", key_info=b"Info"), - flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], ), @@ -244,8 +244,8 @@ def test_empty_encrypted_data_keys(): @pytest.mark.parametrize( "material_class, flag", ( - (EncryptionMaterials, KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY), - (DecryptionMaterials, KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY), + (EncryptionMaterials, KeyringTraceFlag.GENERATED_DATA_KEY), + (DecryptionMaterials, KeyringTraceFlag.DECRYPTED_DATA_KEY), ), ) def test_add_data_encryption_key_success(material_class, flag): @@ -264,8 +264,8 @@ def test_add_data_encryption_key_success(material_class, 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), + (EncryptionMaterials, KeyringTraceFlag.GENERATED_DATA_KEY), + (DecryptionMaterials, KeyringTraceFlag.DECRYPTED_DATA_KEY), ): yield ( material_class, @@ -332,7 +332,7 @@ def test_add_encrypted_data_key_success(): 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} + wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} ), ) @@ -352,7 +352,7 @@ def test_add_encrypted_data_key_success(): 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}, + flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ), InvalidKeyringTraceError, "Keyring trace does not match data key encryptor.", @@ -360,9 +360,7 @@ def test_add_encrypted_data_key_success(): ( 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} - ), + KeyringTrace(wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}), AttributeError, "Data encryption key is not set.", ), diff --git a/test/unit/test_structures.py b/test/unit/test_structures.py index f047d5f11..26cef17ec 100644 --- a/test/unit/test_structures.py +++ b/test/unit/test_structures.py @@ -67,8 +67,7 @@ ], KeyringTrace: [ dict( - wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), - flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY}, + wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ) ], CryptoResult: [ @@ -89,7 +88,7 @@ keyring_trace=( KeyringTrace( wrapping_key=MasterKeyInfo(provider_id="foo", key_info=b"bar"), - flags={KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY}, + flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ), ), ) diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index ff1a1b158..ddde3e975 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -100,9 +100,7 @@ def on_encrypt(self, encryption_materials): ) encryption_materials.add_data_encryption_key( data_encryption_key=data_encryption_key, - keyring_trace=KeyringTrace( - wrapping_key=key_provider, flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY} - ), + keyring_trace=KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.GENERATED_DATA_KEY}), ) return encryption_materials @@ -123,7 +121,7 @@ def get_encryption_materials_with_data_key(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], ) @@ -141,7 +139,7 @@ def get_encryption_materials_with_data_encryption_key(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), - flags={KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY}, + flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], ) @@ -174,10 +172,7 @@ def get_encryption_materials_with_encrypted_data_key(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={ - KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, - KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, - }, + flags={KeyringTraceFlag.GENERATED_DATA_KEY, KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ) ], ) @@ -196,10 +191,7 @@ def get_encryption_materials_with_encrypted_data_key_aes(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={ - KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, - KeyringTraceFlag.WRAPPING_KEY_ENCRYPTED_DATA_KEY, - }, + flags={KeyringTraceFlag.GENERATED_DATA_KEY, KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ) ], ) @@ -233,7 +225,7 @@ def get_decryption_materials_with_data_key(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), - flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY}, + flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, ) ], ) @@ -251,7 +243,7 @@ def get_decryption_materials_with_data_encryption_key(): keyring_trace=[ KeyringTrace( wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), - flags={KeyringTraceFlag.WRAPPING_KEY_DECRYPTED_DATA_KEY}, + flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, ) ], ) From 1dac2831ea6cd88c006b8483a6cf825bf19410a5 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Wed, 25 Mar 2020 14:26:58 -0700 Subject: [PATCH 43/64] feat: change KMS discovery keyring configuration path (#228) * feat: change KMS discovery keyring configuration path * fix: clarify error message when insufficient configuration is provided to KmsKeyring --- .../keyrings/aws_kms/__init__.py | 34 +++++++++++++------ test/unit/keyrings/test_aws_kms.py | 13 ++++--- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index b8cb94b81..3630644c6 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -55,10 +55,13 @@ class KmsKeyring(Keyring): but for the keyring to attempt to use them on decrypt you MUST specify the CMK ARN. - If you specify neither ``generator_key_id`` nor ``child_key_ids`` - then the keyring will operate in `discovery mode`_, + If you specify ``is_discovery=True`` the keyring will be a KMS discovery keyring, doing nothing on encrypt and attempting to decrypt any AWS KMS-encrypted data key on decrypt. + .. notice:: + + You must either set ``is_discovery=True`` or provide key IDs. + You can use the :class:`ClientSupplier` to customize behavior further, such as to provide different credentials for different regions or to restrict which regions are allowed. @@ -75,12 +78,14 @@ class KmsKeyring(Keyring): .. versionadded:: 1.5.0 :param ClientSupplier client_supplier: Client supplier that provides AWS KMS clients (optional) + :param bool is_discovery: Should this be a discovery keyring (optional) :param str generator_key_id: Key ID of AWS KMS CMK to use when generating data keys (optional) :param List[str] child_key_ids: Key IDs that will be used to encrypt and decrypt data keys (optional) :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) """ _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=is_callable()) + _is_discovery = attr.ib(default=False, validator=instance_of(bool)) _generator_key_id = attr.ib(default=None, validator=optional(instance_of(six.string_types))) _child_key_ids = attr.ib( default=attr.Factory(tuple), @@ -93,6 +98,22 @@ class KmsKeyring(Keyring): def __attrs_post_init__(self): """Configure internal keyring.""" + key_ids_provided = self._generator_key_id is not None or self._child_key_ids + both = key_ids_provided and self._is_discovery + neither = not key_ids_provided and not self._is_discovery + + if both: + raise TypeError("is_discovery cannot be True if key IDs are provided") + + if neither: + raise TypeError("is_discovery cannot be False if no key IDs are provided") + + if self._is_discovery: + self._inner_keyring = _AwsKmsDiscoveryKeyring( + client_supplier=self._client_supplier, grant_tokens=self._grant_tokens + ) + return + if self._generator_key_id is None: generator_keyring = None else: @@ -107,14 +128,7 @@ def __attrs_post_init__(self): for key_id in self._child_key_ids ] - self._is_discovery = generator_keyring is None and not child_keyrings - - if self._is_discovery: - self._inner_keyring = _AwsKmsDiscoveryKeyring( - client_supplier=self._client_supplier, grant_tokens=self._grant_tokens - ) - else: - self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings) + self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings) def on_encrypt(self, encryption_materials): # type: (EncryptionMaterials) -> EncryptionMaterials diff --git a/test/unit/keyrings/test_aws_kms.py b/test/unit/keyrings/test_aws_kms.py index ad2e4b526..113ffc98f 100644 --- a/test/unit/keyrings/test_aws_kms.py +++ b/test/unit/keyrings/test_aws_kms.py @@ -24,6 +24,9 @@ pytest.param(dict(child_key_ids="some stuff"), id="child_key_ids is a string"), pytest.param(dict(grant_tokens=("foo", 5)), id="grant_tokens contains invalid values"), pytest.param(dict(grant_tokens="some stuff"), id="grant_tokens is a string"), + pytest.param(dict(generator_key_id="foo", is_discovery=True), id="generator and discovery"), + pytest.param(dict(child_key_ids=("foo",), is_discovery=True), id="child_key_ids and discovery"), + pytest.param(dict(), id="nothing"), ), ) def test_kms_keyring_invalid_parameters(kwargs): @@ -100,7 +103,7 @@ def test_kms_keyring_builds_correct_inner_keyring_discovery(): grants = ("asdf", "fdas") supplier = DefaultClientSupplier() - test = KmsKeyring(grant_tokens=grants, client_supplier=supplier) + test = KmsKeyring(is_discovery=True, grant_tokens=grants, client_supplier=supplier) # We specified neither a generator nor children, so the inner keyring MUST be a discovery keyring assert isinstance(test._inner_keyring, _AwsKmsDiscoveryKeyring) @@ -110,10 +113,10 @@ def test_kms_keyring_builds_correct_inner_keyring_discovery(): assert test._inner_keyring._client_supplier is supplier -def test_kms_keyring_on_encrypt(mocker): +def test_kms_keyring_inner_keyring_on_encrypt(mocker): mock_keyring = mocker.Mock() - keyring = KmsKeyring() + keyring = KmsKeyring(is_discovery=True) keyring._inner_keyring = mock_keyring test = keyring.on_encrypt(encryption_materials=mocker.sentinel.encryption_materials) @@ -123,10 +126,10 @@ def test_kms_keyring_on_encrypt(mocker): assert test is mock_keyring.on_encrypt.return_value -def test_kms_keyring_on_decrypt(mocker): +def test_kms_keyring_inner_keyring_on_decrypt(mocker): mock_keyring = mocker.Mock() - keyring = KmsKeyring() + keyring = KmsKeyring(is_discovery=True) keyring._inner_keyring = mock_keyring test = keyring.on_decrypt( From daed717268985f56529dcbad761e1d726af7a861 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Mon, 30 Mar 2020 18:43:31 -0700 Subject: [PATCH 44/64] feat: immutable cryptographic materials (for keyrings) (#231) * feat: change keyring materials modifiers to immutable constructors * feat: add key_name parameter and key_namespace accessor to MasterKeyInfo * feat: adopt immutable materials for KMS keyring * feat: adopt immutable materials for multi-keyring * fix: revise test helpers to use key IDs that are not used in tests * feat: add key_name parameter and key_namespace accessor to MasterKeyInfo pt2 * feat: fix raw keyrings and move to immutable materials * adopt immutable materials * add encryption context entries to keyring trace https://github.com/awslabs/aws-encryption-sdk-specification/issues/32 * use the key name, not the provider info, in the keyring trace * fix the keyring trace tests to accurately fail when trace is wrong * fix: fix incorrectly placed test assertions * chore: clean up linting * docs: update src/aws_encryption_sdk/keyrings/raw.py Co-Authored-By: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> * chore: remove unnecessary copies on encrypt and avoid overwriting pointers on decrypt * chore: finish copy cleanup Co-authored-by: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> --- .../internal/formatting/serialize.py | 2 +- .../keyrings/aws_kms/__init__.py | 51 +++---- src/aws_encryption_sdk/keyrings/multi.py | 24 ++-- src/aws_encryption_sdk/keyrings/raw.py | 115 ++++++++------- .../materials_managers/__init__.py | 120 ++++++++++++---- src/aws_encryption_sdk/structures.py | 29 +++- .../keyrings/aws_kms/test_aws_kms.py | 4 + .../internal/formatting/test_serialize.py | 4 +- test/unit/keyrings/raw/test_raw_aes.py | 121 ++++++++++------ test/unit/keyrings/raw/test_raw_rsa.py | 132 ++++++++++++------ test/unit/keyrings/test_multi.py | 19 ++- .../test_material_managers.py | 45 +++--- test/unit/unit_test_utils.py | 29 ++-- 13 files changed, 458 insertions(+), 237 deletions(-) diff --git a/src/aws_encryption_sdk/internal/formatting/serialize.py b/src/aws_encryption_sdk/internal/formatting/serialize.py index e7c86a0cb..bd71b3a1a 100644 --- a/src/aws_encryption_sdk/internal/formatting/serialize.py +++ b/src/aws_encryption_sdk/internal/formatting/serialize.py @@ -316,6 +316,6 @@ def serialize_wrapped_key(key_provider, wrapping_algorithm, wrapping_key_id, enc ) key_ciphertext = encrypted_wrapped_key.ciphertext + encrypted_wrapped_key.tag return EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=key_provider.provider_id, key_info=key_info), + key_provider=MasterKeyInfo(provider_id=key_provider.provider_id, key_info=key_info, key_name=wrapping_key_id), encrypted_data_key=key_ciphertext, ) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 3630644c6..b65a5a8d2 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -180,16 +180,17 @@ class _AwsKmsSingleCmkKeyring(Keyring): def on_encrypt(self, encryption_materials): # type: (EncryptionMaterials) -> EncryptionMaterials trace_info = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=self._key_id) + new_materials = encryption_materials try: - if encryption_materials.data_encryption_key is None: + if new_materials.data_encryption_key is None: plaintext_key, encrypted_key = _do_aws_kms_generate_data_key( client_supplier=self._client_supplier, key_name=self._key_id, - encryption_context=encryption_materials.encryption_context, - algorithm=encryption_materials.algorithm, + encryption_context=new_materials.encryption_context, + algorithm=new_materials.algorithm, grant_tokens=self._grant_tokens, ) - encryption_materials.add_data_encryption_key( + new_materials = new_materials.with_data_encryption_key( data_encryption_key=plaintext_key, keyring_trace=KeyringTrace(wrapping_key=trace_info, flags=_GENERATE_FLAGS), ) @@ -197,8 +198,8 @@ def on_encrypt(self, encryption_materials): encrypted_key = _do_aws_kms_encrypt( client_supplier=self._client_supplier, key_name=self._key_id, - plaintext_data_key=encryption_materials.data_encryption_key, - encryption_context=encryption_materials.encryption_context, + plaintext_data_key=new_materials.data_encryption_key, + encryption_context=new_materials.encryption_context, grant_tokens=self._grant_tokens, ) except Exception: # pylint: disable=broad-except @@ -207,30 +208,30 @@ def on_encrypt(self, encryption_materials): _LOGGER.exception(message) raise EncryptKeyError(message) - encryption_materials.add_encrypted_data_key( + return new_materials.with_encrypted_data_key( encrypted_data_key=encrypted_key, keyring_trace=KeyringTrace(wrapping_key=trace_info, flags=_ENCRYPT_FLAGS) ) - return encryption_materials - def on_decrypt(self, decryption_materials, encrypted_data_keys): # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + new_materials = decryption_materials + for edk in encrypted_data_keys: - if decryption_materials.data_encryption_key is not None: - return decryption_materials + if new_materials.data_encryption_key is not None: + return new_materials if ( edk.key_provider.provider_id == _PROVIDER_ID and edk.key_provider.key_info.decode("utf-8") == self._key_id ): - decryption_materials = _try_aws_kms_decrypt( + new_materials = _try_aws_kms_decrypt( client_supplier=self._client_supplier, - decryption_materials=decryption_materials, + decryption_materials=new_materials, grant_tokens=self._grant_tokens, encrypted_data_key=edk, ) - return decryption_materials + return new_materials @attr.s @@ -258,19 +259,21 @@ def on_encrypt(self, encryption_materials): def on_decrypt(self, decryption_materials, encrypted_data_keys): # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials + new_materials = decryption_materials + for edk in encrypted_data_keys: - if decryption_materials.data_encryption_key is not None: - return decryption_materials + if new_materials.data_encryption_key is not None: + return new_materials if edk.key_provider.provider_id == _PROVIDER_ID: - decryption_materials = _try_aws_kms_decrypt( + new_materials = _try_aws_kms_decrypt( client_supplier=self._client_supplier, - decryption_materials=decryption_materials, + decryption_materials=new_materials, grant_tokens=self._grant_tokens, encrypted_data_key=edk, ) - return decryption_materials + return new_materials def _try_aws_kms_decrypt(client_supplier, decryption_materials, grant_tokens, encrypted_data_key): @@ -293,14 +296,12 @@ def _try_aws_kms_decrypt(client_supplier, decryption_materials, grant_tokens, en except Exception: # pylint: disable=broad-except # We intentionally WANT to catch all exceptions here _LOGGER.exception("Unable to decrypt encrypted data key from %s", encrypted_data_key.key_provider) - else: - decryption_materials.add_data_encryption_key( - data_encryption_key=plaintext_key, - keyring_trace=KeyringTrace(wrapping_key=encrypted_data_key.key_provider, flags=_DECRYPT_FLAGS), - ) return decryption_materials - return decryption_materials + return decryption_materials.with_data_encryption_key( + data_encryption_key=plaintext_key, + keyring_trace=KeyringTrace(wrapping_key=encrypted_data_key.key_provider, flags=_DECRYPT_FLAGS), + ) def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryption_context, grant_tokens): diff --git a/src/aws_encryption_sdk/keyrings/multi.py b/src/aws_encryption_sdk/keyrings/multi.py index d42ea365d..27e90c3c8 100644 --- a/src/aws_encryption_sdk/keyrings/multi.py +++ b/src/aws_encryption_sdk/keyrings/multi.py @@ -67,20 +67,21 @@ def on_encrypt(self, encryption_materials): "and encryption materials do not already contain a plaintext data key." ) + new_materials = encryption_materials + # Call on_encrypt on the generator keyring if it is provided if self.generator is not None: - - encryption_materials = self.generator.on_encrypt(encryption_materials=encryption_materials) + new_materials = self.generator.on_encrypt(encryption_materials=new_materials) # Check if data key is generated - if encryption_materials.data_encryption_key is None: + if new_materials.data_encryption_key is None: raise GenerateKeyError("Unable to generate data encryption key.") # Call on_encrypt on all other keyrings for keyring in self.children: - encryption_materials = keyring.on_encrypt(encryption_materials=encryption_materials) + new_materials = keyring.on_encrypt(encryption_materials=new_materials) - return encryption_materials + return new_materials def on_decrypt(self, decryption_materials, encrypted_data_keys): # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials @@ -92,10 +93,13 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): :rtype: DecryptionMaterials """ # Call on_decrypt on all keyrings till decryption is successful + new_materials = decryption_materials for keyring in self._decryption_keyrings: - if decryption_materials.data_encryption_key is not None: - return decryption_materials - decryption_materials = keyring.on_decrypt( - decryption_materials=decryption_materials, encrypted_data_keys=encrypted_data_keys + if new_materials.data_encryption_key is not None: + return new_materials + + new_materials = keyring.on_decrypt( + decryption_materials=new_materials, encrypted_data_keys=encrypted_data_keys ) - return decryption_materials + + return new_materials diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index fcf793f5f..deab1a122 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -11,7 +11,7 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey -from aws_encryption_sdk.exceptions import GenerateKeyError +from aws_encryption_sdk.exceptions import EncryptKeyError, GenerateKeyError from aws_encryption_sdk.identifiers import EncryptionKeyType, KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.crypto.wrapping_keys import EncryptedData, WrappingKey from aws_encryption_sdk.internal.formatting.deserialize import deserialize_wrapped_key @@ -35,12 +35,13 @@ def _generate_data_key( encryption_materials, # type: EncryptionMaterials key_provider, # type: MasterKeyInfo ): - # type: (...) -> bytes + # type: (...) -> EncryptionMaterials """Generates plaintext data key for the keyring. :param EncryptionMaterials encryption_materials: Encryption materials for the keyring to modify. :param MasterKeyInfo key_provider: Information about the key in the keyring. - :return bytes: Plaintext data key + :rtype: EncryptionMaterials + :returns: Encryption materials containing a data encryption key """ # Check if encryption materials contain data encryption key if encryption_materials.data_encryption_key is not None: @@ -60,10 +61,9 @@ def _generate_data_key( # plaintext_data_key to RawDataKey data_encryption_key = RawDataKey(key_provider=key_provider, data_key=plaintext_data_key) - # Add generated data key to encryption_materials - encryption_materials.add_data_encryption_key(data_encryption_key, keyring_trace) - - return plaintext_data_key + return encryption_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace + ) @attr.s @@ -123,17 +123,20 @@ def on_encrypt(self, encryption_materials): """Generate a data key if not present and encrypt it using any available wrapping key :param EncryptionMaterials encryption_materials: Encryption materials for the keyring to modify - :returns: Optionally modified encryption materials + :returns: Encryption materials containing data key and encrypted data key :rtype: EncryptionMaterials """ - if encryption_materials.data_encryption_key is None: - _generate_data_key(encryption_materials=encryption_materials, key_provider=self._key_provider) + new_materials = encryption_materials + + if new_materials.data_encryption_key is None: + # Get encryption materials with a new data key. + new_materials = _generate_data_key(encryption_materials=new_materials, key_provider=self._key_provider) try: # Encrypt data key encrypted_wrapped_key = self._wrapping_key_structure.encrypt( - plaintext_data_key=encryption_materials.data_encryption_key.data_key, - encryption_context=encryption_materials.encryption_context, + plaintext_data_key=new_materials.data_encryption_key.data_key, + encryption_context=new_materials.encryption_context, ) # EncryptedData to EncryptedDataKey @@ -144,18 +147,17 @@ def on_encrypt(self, encryption_materials): encrypted_wrapped_key=encrypted_wrapped_key, ) except Exception: # pylint: disable=broad-except - error_message = "Raw AES Keyring unable to encrypt data key" + error_message = "Raw AES keyring unable to encrypt data key" _LOGGER.exception(error_message) - return encryption_materials + raise EncryptKeyError(error_message) # Update Keyring Trace keyring_trace = KeyringTrace( - wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} + wrapping_key=self._key_provider, + flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT}, ) - # Add encrypted data key to encryption_materials - encryption_materials.add_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) - return encryption_materials + return new_materials.with_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) def on_decrypt(self, decryption_materials, encrypted_data_keys): # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials @@ -163,19 +165,18 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): :param DecryptionMaterials decryption_materials: Decryption materials for the keyring to modify :param List[EncryptedDataKey] encrypted_data_keys: List of encrypted data keys - :returns: Optionally modified decryption materials + :returns: Decryption materials that MAY include a plaintext data key :rtype: DecryptionMaterials """ - if decryption_materials.data_encryption_key is not None: - return decryption_materials + new_materials = decryption_materials + + if new_materials.data_encryption_key is not None: + return new_materials # Decrypt data key expected_key_info_len = len(self._key_info_prefix) + self._wrapping_algorithm.algorithm.iv_len for key in encrypted_data_keys: - if decryption_materials.data_encryption_key is not None: - return decryption_materials - if ( key.key_provider.provider_id != self._key_provider.provider_id or len(key.key_provider.key_info) != expected_key_info_len @@ -192,22 +193,31 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): try: plaintext_data_key = self._wrapping_key_structure.decrypt( encrypted_wrapped_data_key=encrypted_wrapped_key, - encryption_context=decryption_materials.encryption_context, + encryption_context=new_materials.encryption_context, ) except Exception: # pylint: disable=broad-except + # We intentionally WANT to catch all exceptions here error_message = "Raw AES Keyring unable to decrypt data key" _LOGGER.exception(error_message) - return decryption_materials + # The Raw AES keyring MUST evaluate every encrypted data key + # until it either succeeds or runs out of encrypted data keys. + continue # Create a keyring trace - keyring_trace = KeyringTrace(wrapping_key=self._key_provider, flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}) + keyring_trace = KeyringTrace( + wrapping_key=self._key_provider, + flags={KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT}, + ) # Update decryption materials data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) - decryption_materials.add_data_encryption_key(data_encryption_key, keyring_trace) - return decryption_materials + return new_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace + ) + + return new_materials @attr.s @@ -331,22 +341,24 @@ def on_encrypt(self, encryption_materials): and encrypt it using any available wrapping key in any child keyring. :param EncryptionMaterials encryption_materials: Encryption materials for keyring to modify. - :returns: Optionally modified encryption materials. + :returns: Encryption materials containing data key and encrypted data key :rtype: EncryptionMaterials """ - if encryption_materials.data_encryption_key is None: - _generate_data_key(encryption_materials=encryption_materials, key_provider=self._key_provider) + new_materials = encryption_materials + + if new_materials.data_encryption_key is None: + new_materials = _generate_data_key(encryption_materials=new_materials, key_provider=self._key_provider) if self._public_wrapping_key is None: - return encryption_materials + # This should be impossible, but just in case, give a useful error message. + raise EncryptKeyError("Raw RSA keyring unable to encrypt data key: no public key available") try: # Encrypt data key encrypted_wrapped_key = EncryptedData( iv=None, ciphertext=self._public_wrapping_key.encrypt( - plaintext=encryption_materials.data_encryption_key.data_key, - padding=self._wrapping_algorithm.padding, + plaintext=new_materials.data_encryption_key.data_key, padding=self._wrapping_algorithm.padding, ), tag=None, ) @@ -359,19 +371,15 @@ def on_encrypt(self, encryption_materials): encrypted_wrapped_key=encrypted_wrapped_key, ) except Exception: # pylint: disable=broad-except - error_message = "Raw RSA Keyring unable to encrypt data key" + error_message = "Raw RSA keyring unable to encrypt data key" _LOGGER.exception(error_message) - return encryption_materials + raise EncryptKeyError(error_message) # Update Keyring Trace - keyring_trace = KeyringTrace( - wrapping_key=encrypted_data_key.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} - ) + keyring_trace = KeyringTrace(wrapping_key=self._key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY}) # Add encrypted data key to encryption_materials - encryption_materials.add_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) - - return encryption_materials + return new_materials.with_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) def on_decrypt(self, decryption_materials, encrypted_data_keys): # type: (DecryptionMaterials, Iterable[EncryptedDataKey]) -> DecryptionMaterials @@ -380,18 +388,22 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): :param DecryptionMaterials decryption_materials: Decryption materials for keyring to modify. :param encrypted_data_keys: List of encrypted data keys. :type: List[EncryptedDataKey] - :returns: Optionally modified decryption materials. + :returns: Decryption materials that MAY include a plaintext data key :rtype: DecryptionMaterials """ + new_materials = decryption_materials + + if new_materials.data_encryption_key is not None: + return new_materials + if self._private_wrapping_key is None: - return decryption_materials + return new_materials # Decrypt data key for key in encrypted_data_keys: - if decryption_materials.data_encryption_key is not None: - return decryption_materials if key.key_provider != self._key_provider: continue + # Wrapped EncryptedDataKey to deserialized EncryptedData encrypted_wrapped_key = deserialize_wrapped_key( wrapping_algorithm=self._wrapping_algorithm, wrapping_key_id=self.key_name, wrapped_encrypted_key=key @@ -403,6 +415,8 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): except Exception: # pylint: disable=broad-except error_message = "Raw RSA Keyring unable to decrypt data key" _LOGGER.exception(error_message) + # The Raw RSA keyring MUST evaluate every encrypted data key + # until it either succeeds or runs out of encrypted data keys. continue # Create a keyring trace @@ -410,6 +424,9 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): # Update decryption materials data_encryption_key = RawDataKey(key_provider=self._key_provider, data_key=plaintext_data_key) - decryption_materials.add_data_encryption_key(data_encryption_key, keyring_trace) - return decryption_materials + return new_materials.with_data_encryption_key( + data_encryption_key=data_encryption_key, keyring_trace=keyring_trace + ) + + return new_materials diff --git a/src/aws_encryption_sdk/materials_managers/__init__.py b/src/aws_encryption_sdk/materials_managers/__init__.py index 28ea40c0d..8c8c33886 100644 --- a/src/aws_encryption_sdk/materials_managers/__init__.py +++ b/src/aws_encryption_sdk/materials_managers/__init__.py @@ -14,6 +14,8 @@ .. versionadded:: 1.3.0 """ +import copy + import attr import six from attr.validators import deep_iterable, deep_mapping, instance_of, optional @@ -117,6 +119,8 @@ def _validate_data_encryption_key(self, data_encryption_key, keyring_trace, requ # 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. + .. versionadded:: 1.5.0 + :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 @@ -143,9 +147,11 @@ def _validate_data_encryption_key(self, data_encryption_key, keyring_trace, requ ) ) - 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. + def _with_data_encryption_key(self, data_encryption_key, keyring_trace, required_flags): + # type: (Union[DataKey, RawDataKey], KeyringTrace, Iterable[KeyringTraceFlag]) -> CryptographicMaterials + """Get new cryptographic materials that include this 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 @@ -161,10 +167,15 @@ def _add_data_encryption_key(self, data_encryption_key, keyring_trace, required_ data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, required_flags=required_flags ) + new_materials = copy.copy(self) + data_key = _data_key_to_raw_data_key(data_key=data_encryption_key) + new_materials._setattr( # simplify access to copies pylint: disable=protected-access + "data_encryption_key", data_key + ) + new_materials._keyring_trace.append(keyring_trace) # simplify access to copies pylint: disable=protected-access - super(CryptographicMaterials, self).__setattr__("data_encryption_key", data_key) - self._keyring_trace.append(keyring_trace) + return new_materials @property def keyring_trace(self): @@ -220,7 +231,8 @@ 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: + if data_encryption_key is None and encrypted_data_keys: + # If data_encryption_key is not set, encrypted_data_keys MUST be either None or empty raise TypeError("encrypted_data_keys cannot be provided without data_encryption_key") if encrypted_data_keys is None: @@ -236,6 +248,18 @@ def __init__( self._setattr("_encrypted_data_keys", encrypted_data_keys) attr.validate(self) + def __copy__(self): + # type: () -> EncryptionMaterials + """Do a shallow copy of this instance.""" + return EncryptionMaterials( + algorithm=self.algorithm, + data_encryption_key=self.data_encryption_key, + encrypted_data_keys=copy.copy(self._encrypted_data_keys), + encryption_context=self.encryption_context.copy(), + signing_key=self.signing_key, + keyring_trace=copy.copy(self._keyring_trace), + ) + @property def encrypted_data_keys(self): # type: () -> Tuple[EncryptedDataKey] @@ -263,35 +287,37 @@ def is_complete(self): 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. + def with_data_encryption_key(self, data_encryption_key, keyring_trace): + # type: (Union[DataKey, RawDataKey], KeyringTrace) -> EncryptionMaterials + """Get new encryption materials that also include this 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 + :rtype: EncryptionMaterials :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( + return self._with_data_encryption_key( data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, required_flags={KeyringTraceFlag.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. + def with_encrypted_data_key(self, encrypted_data_key, keyring_trace): + # type: (EncryptedDataKey, KeyringTrace) -> EncryptionMaterials + """Get new encryption materials that also include this 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 + :rtype: EncryptionMaterials :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 @@ -302,19 +328,30 @@ def add_encrypted_data_key(self, encrypted_data_key, keyring_trace): if KeyringTraceFlag.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: + if not all( + ( + keyring_trace.wrapping_key.provider_id == encrypted_data_key.key_provider.provider_id, + keyring_trace.wrapping_key.key_name == encrypted_data_key.key_provider.key_name, + ) + ): raise InvalidKeyringTraceError("Keyring trace does not match data key encryptor.") - self._encrypted_data_keys.append(encrypted_data_key) - self._keyring_trace.append(keyring_trace) + new_materials = copy.copy(self) - def add_signing_key(self, signing_key): - # type: (bytes) -> None - """Add a signing key. + new_materials._encrypted_data_keys.append( # simplify access to copies pylint: disable=protected-access + encrypted_data_key + ) + new_materials._keyring_trace.append(keyring_trace) # simplify access to copies pylint: disable=protected-access + return new_materials + + def with_signing_key(self, signing_key): + # type: (bytes) -> EncryptionMaterials + """Get new encryption materials that also include this signing key. .. versionadded:: 1.5.0 :param bytes signing_key: Signing key + :rtype: EncryptionMaterials :raises AttributeError: if signing key is already set :raises SignatureKeyError: if algorithm suite does not support signing keys """ @@ -324,10 +361,14 @@ def add_signing_key(self, signing_key): if self.algorithm.signing_algorithm_info is None: raise SignatureKeyError("Algorithm suite does not support signing keys.") + new_materials = copy.copy(self) + # Verify that the signing key matches the algorithm - Signer.from_key_bytes(algorithm=self.algorithm, key_bytes=signing_key) + Signer.from_key_bytes(algorithm=new_materials.algorithm, key_bytes=signing_key) - self._setattr("signing_key", signing_key) + new_materials._setattr("signing_key", signing_key) # simplify access to copies pylint: disable=protected-access + + return new_materials @attr.s(hash=False) @@ -401,6 +442,17 @@ def __init__( self._setattr("verification_key", verification_key) attr.validate(self) + def __copy__(self): + # type: () -> DecryptionMaterials + """Do a shallow copy of this instance.""" + return DecryptionMaterials( + algorithm=self.algorithm, + data_encryption_key=self.data_encryption_key, + encryption_context=copy.copy(self.encryption_context), + verification_key=self.verification_key, + keyring_trace=copy.copy(self._keyring_trace), + ) + @property def is_complete(self): # type: () -> bool @@ -425,15 +477,16 @@ def data_key(self): """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. + def with_data_encryption_key(self, data_encryption_key, keyring_trace): + # type: (Union[DataKey, RawDataKey], KeyringTrace) -> DecryptionMaterials + """Get new decryption materials that also include this 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 + :rtype: DecryptionMaterials :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 @@ -442,19 +495,20 @@ def add_data_encryption_key(self, data_encryption_key, keyring_trace): if self.algorithm is None: raise AttributeError("Algorithm is not set") - self._add_data_encryption_key( + return self._with_data_encryption_key( data_encryption_key=data_encryption_key, keyring_trace=keyring_trace, required_flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, ) - def add_verification_key(self, verification_key): - # type: (bytes) -> None - """Add a verification key. + def with_verification_key(self, verification_key): + # type: (bytes) -> DecryptionMaterials + """Get new decryption materials that also include this verification key. .. versionadded:: 1.5.0 :param bytes verification_key: Verification key + :rtype: DecryptionMaterials """ if self.verification_key is not None: raise AttributeError("Verification key is already set.") @@ -462,7 +516,13 @@ def add_verification_key(self, verification_key): if self.algorithm.signing_algorithm_info is None: raise SignatureKeyError("Algorithm suite does not support signing keys.") + new_materials = copy.copy(self) + # Verify that the verification key matches the algorithm - Verifier.from_key_bytes(algorithm=self.algorithm, key_bytes=verification_key) + Verifier.from_key_bytes(algorithm=new_materials.algorithm, key_bytes=verification_key) - self._setattr("verification_key", verification_key) + new_materials._setattr( # simplify access to copies pylint: disable=protected-access + "verification_key", verification_key + ) + + return new_materials diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index 35eab24e6..ea0af94e9 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -15,7 +15,7 @@ import attr import six -from attr.validators import deep_iterable, deep_mapping, instance_of +from attr.validators import deep_iterable, deep_mapping, instance_of, optional from aws_encryption_sdk.identifiers import Algorithm, ContentType, KeyringTraceFlag, ObjectType, SerializationVersion from aws_encryption_sdk.internal.str_ops import to_bytes, to_str @@ -31,12 +31,39 @@ class MasterKeyInfo(object): """Contains information necessary to identify a Master Key. + .. notice:: + + The only keyring or master key that should need to set ``key_name`` is the Raw AES keyring/master key. + For all other keyrings and master keys, ``key_info`` and ``key_name`` should always be the same. + + + .. versionadded:: 1.5.0 + ``key_name`` + :param str provider_id: MasterKey provider_id value :param bytes key_info: MasterKey key_info value + :param bytes key_name: Key name if different than key_info (optional) """ 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) + key_name = attr.ib( + hash=True, default=None, validator=optional(instance_of((six.string_types, bytes))), converter=to_bytes + ) + + def __attrs_post_init__(self): + """Set ``key_name`` if not already set.""" + if self.key_name is None: + self.key_name = self.key_info + + @property + def key_namespace(self): + """Access the key namespace value (previously, provider ID). + + .. versionadded:: 1.5.0 + + """ + return self.provider_id @attr.s(hash=True) diff --git a/test/functional/keyrings/aws_kms/test_aws_kms.py b/test/functional/keyrings/aws_kms/test_aws_kms.py index 7a5b50d51..c84174f69 100644 --- a/test/functional/keyrings/aws_kms/test_aws_kms.py +++ b/test/functional/keyrings/aws_kms/test_aws_kms.py @@ -79,6 +79,7 @@ def test_aws_kms_single_cmk_keyring_on_encrypt_existing_data_key(fake_generator) result_materials = keyring.on_encrypt(initial_materials) + assert result_materials is not initial_materials assert result_materials.data_encryption_key is not None assert len(result_materials.encrypted_data_keys) == 1 @@ -149,6 +150,7 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_single_cmk(fake_generator): decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys ) + assert result_materials is not initial_decryption_materials assert result_materials.data_encryption_key is not None generator_flags = _matching_flags( @@ -243,6 +245,7 @@ def test_aws_kms_discovery_keyring_on_encrypt(): result_materials = keyring.on_encrypt(initial_materials) + assert result_materials is initial_materials assert len(result_materials.encrypted_data_keys) == 0 @@ -268,6 +271,7 @@ def test_aws_kms_discovery_keyring_on_decrypt(encryption_materials_for_discovery decryption_materials=initial_decryption_materials, encrypted_data_keys=encryption_materials.encrypted_data_keys ) + assert result_materials is not initial_decryption_materials assert result_materials.data_encryption_key is not None generator_flags = _matching_flags( diff --git a/test/unit/internal/formatting/test_serialize.py b/test/unit/internal/formatting/test_serialize.py index 8dbe9bd05..7a4063472 100644 --- a/test/unit/internal/formatting/test_serialize.py +++ b/test/unit/internal/formatting/test_serialize.py @@ -325,7 +325,9 @@ def test_serialize_wrapped_key_symmetric(self): ) assert test == EncryptedDataKey( key_provider=MasterKeyInfo( - provider_id=VALUES["provider_id"], key_info=VALUES["wrapped_keys"]["serialized"]["key_info"] + provider_id=self.mock_key_provider.provider_id, + key_info=VALUES["wrapped_keys"]["serialized"]["key_info"], + key_name=VALUES["wrapped_keys"]["raw"]["key_info"], ), encrypted_data_key=VALUES["wrapped_keys"]["serialized"]["key_ciphertext"], ) diff --git a/test/unit/keyrings/raw/test_raw_aes.py b/test/unit/keyrings/raw/test_raw_aes.py index 33a882918..1f4322e09 100644 --- a/test/unit/keyrings/raw/test_raw_aes.py +++ b/test/unit/keyrings/raw/test_raw_aes.py @@ -16,10 +16,10 @@ import mock import pytest -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.key_providers.raw import aws_encryption_sdk.keyrings.raw +from aws_encryption_sdk.exceptions import EncryptKeyError from aws_encryption_sdk.identifiers import Algorithm, KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.keyrings.base import Keyring @@ -67,6 +67,12 @@ def patch_decrypt_on_wrapping_key(mocker): return WrappingKey.decrypt +@pytest.fixture +def patch_encrypt_on_wrapping_key(mocker): + mocker.patch.object(WrappingKey, "encrypt") + return WrappingKey.encrypt + + @pytest.fixture def patch_os_urandom(mocker): mocker.patch.object(os, "urandom") @@ -127,10 +133,18 @@ def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_aes_keyring test = test_raw_aes_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) - for keyring_trace in test.keyring_trace: - if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.GENERATED_DATA_KEY - assert KeyringTraceFlag.GENERATED_DATA_KEY not in keyring_trace.flags + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 1 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 0 + + encrypt_traces = [ + entry + for entry in trace_entries + if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} + ] + assert len(encrypt_traces) == 1 def test_on_encrypt_when_data_encryption_key_not_given(raw_aes_keyring): @@ -146,24 +160,29 @@ def test_on_encrypt_when_data_encryption_key_not_given(raw_aes_keyring): # Check if data key is generated assert test.data_encryption_key is not None - generated_flag_count = 0 - encrypted_flag_count = 0 + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 2 - for keyring_trace in test.keyring_trace: - if ( - keyring_trace.wrapping_key.key_info == _KEY_ID - and KeyringTraceFlag.GENERATED_DATA_KEY in keyring_trace.flags - ): - # Check keyring trace contains KeyringTraceFlag.GENERATED_DATA_KEY - generated_flag_count += 1 - if KeyringTraceFlag.ENCRYPTED_DATA_KEY in keyring_trace.flags: - encrypted_flag_count += 1 + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 1 - assert generated_flag_count == 1 + encrypt_traces = [ + entry + for entry in trace_entries + if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} + ] + assert len(encrypt_traces) == 1 assert len(test.encrypted_data_keys) == original_number_of_encrypted_data_keys + 1 - assert encrypted_flag_count == 1 + +def test_on_encrypt_cannot_encrypt(patch_encrypt_on_wrapping_key, raw_aes_keyring): + patch_encrypt_on_wrapping_key.side_effect = Exception("ENCRYPT FAIL") + + with pytest.raises(EncryptKeyError) as excinfo: + raw_aes_keyring.on_encrypt(get_encryption_materials_without_data_encryption_key()) + + excinfo.match("Raw AES keyring unable to encrypt data key") @pytest.mark.parametrize( @@ -179,16 +198,15 @@ def test_on_decrypt_when_data_key_given(raw_aes_keyring, decryption_materials, e assert not patch_decrypt_on_wrapping_key.called -def test_keyring_trace_on_decrypt_when_data_key_given(raw_aes_keyring): +def test_on_decrypt_keyring_trace_when_data_key_given(raw_aes_keyring): test_raw_aes_keyring = raw_aes_keyring test = test_raw_aes_keyring.on_decrypt( decryption_materials=get_decryption_materials_with_data_encryption_key(), encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], ) - for keyring_trace in test.keyring_trace: - if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.DECRYPTED_DATA_KEY - assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 0 @pytest.mark.parametrize( @@ -206,9 +224,8 @@ def test_on_decrypt_when_data_key_and_edk_not_provided( test = test_raw_aes_keyring.on_decrypt(decryption_materials=decryption_materials, encrypted_data_keys=edk) assert not patch_decrypt_on_wrapping_key.called - for keyring_trace in test.keyring_trace: - if keyring_trace.wrapping_key.key_info == _KEY_ID: - assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 0 assert test.data_encryption_key is None @@ -225,23 +242,38 @@ def test_on_decrypt_when_data_key_not_provided_and_edk_provided(raw_aes_keyring, ) -def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_aes_keyring): +def test_on_decrypt_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_aes_keyring): test_raw_aes_keyring = raw_aes_keyring test = test_raw_aes_keyring.on_decrypt( decryption_materials=get_decryption_materials_without_data_encryption_key(), encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], ) - decrypted_flag_count = 0 - for keyring_trace in test.keyring_trace: - if KeyringTraceFlag.DECRYPTED_DATA_KEY in keyring_trace.flags: - decrypted_flag_count += 1 + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_aes_keyring._key_provider] + assert len(trace_entries) == 1 - assert decrypted_flag_count == 1 + decrypt_traces = [ + entry + for entry in trace_entries + if entry.flags == {KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT} + ] + assert len(decrypt_traces) == 1 -def test_error_when_data_key_not_generated(patch_os_urandom): +def test_on_decrypt_continues_through_edks_on_failure(raw_aes_keyring, patch_decrypt_on_wrapping_key): + patch_decrypt_on_wrapping_key.side_effect = (Exception("DECRYPT FAIL"), _DATA_KEY) + + test = raw_aes_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=(_ENCRYPTED_DATA_KEY_AES, _ENCRYPTED_DATA_KEY_AES), + ) + + assert test.data_encryption_key is not None + assert patch_decrypt_on_wrapping_key.call_count == 2 + + +def test_generate_data_key_error_when_data_key_not_generated(patch_os_urandom): patch_os_urandom.side_effect = NotImplementedError with pytest.raises(GenerateKeyError) as exc_info: _generate_data_key( @@ -266,17 +298,20 @@ def test_generate_data_key_keyring_trace(): encryption_context=_ENCRYPTION_CONTEXT, signing_key=_SIGNING_KEY, ) - _generate_data_key( - encryption_materials=encryption_materials_without_data_key, - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + key_provider_info = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID) + new_materials = _generate_data_key( + encryption_materials=encryption_materials_without_data_key, key_provider=key_provider_info, ) - assert encryption_materials_without_data_key.data_encryption_key.key_provider.provider_id == _PROVIDER_ID - assert encryption_materials_without_data_key.data_encryption_key.key_provider.key_info == _KEY_ID + assert new_materials is not encryption_materials_without_data_key + assert encryption_materials_without_data_key.data_encryption_key is None + assert not encryption_materials_without_data_key.keyring_trace + + assert new_materials.data_encryption_key is not None + assert new_materials.data_encryption_key.key_provider == key_provider_info - generate_flag_count = 0 + trace_entries = [entry for entry in new_materials.keyring_trace if entry.wrapping_key == key_provider_info] + assert len(trace_entries) == 1 - for keyring_trace in encryption_materials_without_data_key.keyring_trace: - if KeyringTraceFlag.GENERATED_DATA_KEY in keyring_trace.flags: - generate_flag_count += 1 - assert generate_flag_count == 1 + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 1 diff --git a/test/unit/keyrings/raw/test_raw_rsa.py b/test/unit/keyrings/raw/test_raw_rsa.py index d9aa7b266..4b0a8d506 100644 --- a/test/unit/keyrings/raw/test_raw_rsa.py +++ b/test/unit/keyrings/raw/test_raw_rsa.py @@ -14,10 +14,10 @@ import pytest from cryptography.hazmat.primitives.asymmetric import rsa -from pytest_mock import mocker # noqa pylint: disable=unused-import import aws_encryption_sdk.key_providers.raw import aws_encryption_sdk.keyrings.raw +from aws_encryption_sdk.exceptions import EncryptKeyError from aws_encryption_sdk.identifiers import KeyringTraceFlag, WrappingAlgorithm from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.keyrings.base import Keyring @@ -26,6 +26,7 @@ from ...unit_test_utils import ( _BACKEND, _DATA_KEY, + _ENCRYPTED_DATA_KEY_AES, _ENCRYPTED_DATA_KEY_RSA, _ENCRYPTION_CONTEXT, _KEY_ID, @@ -123,15 +124,28 @@ def test_on_encrypt_when_data_encryption_key_given(raw_rsa_keyring, patch_genera assert not patch_generate_data_key.called -def test_keyring_trace_on_encrypt_when_data_encryption_key_given(raw_rsa_keyring): - test_raw_rsa_keyring = raw_rsa_keyring +def test_on_encrypt_no_public_key(raw_rsa_keyring): + raw_rsa_keyring._public_wrapping_key = None + + with pytest.raises(EncryptKeyError) as excinfo: + raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + excinfo.match("Raw RSA keyring unable to encrypt data key: no public key available") + - test = test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_encryption_key()) +def test_on_encrypt_keyring_trace_when_data_encryption_key_given(raw_rsa_keyring): + materials = get_encryption_materials_with_data_encryption_key() + test = raw_rsa_keyring.on_encrypt(encryption_materials=materials) + assert test is not materials - for keyring_trace in test.keyring_trace: - if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.GENERATED_DATA_KEY - assert KeyringTraceFlag.GENERATED_DATA_KEY not in keyring_trace.flags + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 1 + + encrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY}] + assert len(encrypt_traces) == 1 + + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 0 def test_on_encrypt_when_data_encryption_key_not_given(raw_rsa_keyring): @@ -143,27 +157,28 @@ def test_on_encrypt_when_data_encryption_key_not_given(raw_rsa_keyring): test = test_raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) - # Check if data key is generated - assert test.data_encryption_key is not None + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 2 - generated_flag_count = 0 - encrypted_flag_count = 0 + encrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.ENCRYPTED_DATA_KEY}] + assert len(encrypt_traces) == 1 - for keyring_trace in test.keyring_trace: - if ( - keyring_trace.wrapping_key.key_info == _KEY_ID - and KeyringTraceFlag.GENERATED_DATA_KEY in keyring_trace.flags - ): - # Check keyring trace contains KeyringTraceFlag.GENERATED_DATA_KEY - generated_flag_count += 1 - if KeyringTraceFlag.ENCRYPTED_DATA_KEY in keyring_trace.flags: - encrypted_flag_count += 1 + generate_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.GENERATED_DATA_KEY}] + assert len(generate_traces) == 1 - assert generated_flag_count == 1 + assert test.data_encryption_key.data_key is not None assert len(test.encrypted_data_keys) == original_number_of_encrypted_data_keys + 1 - assert encrypted_flag_count == 1 + +def test_on_encrypt_cannot_encrypt(raw_rsa_keyring, mocker): + encrypt_patch = mocker.patch.object(raw_rsa_keyring._public_wrapping_key, "encrypt") + encrypt_patch.side_effect = Exception("ENCRYPT FAIL") + + with pytest.raises(EncryptKeyError) as excinfo: + raw_rsa_keyring.on_encrypt(encryption_materials=get_encryption_materials_without_data_encryption_key()) + + excinfo.match("Raw RSA keyring unable to encrypt data key") def test_on_decrypt_when_data_key_given(raw_rsa_keyring, patch_decrypt_on_wrapping_key): @@ -175,16 +190,23 @@ def test_on_decrypt_when_data_key_given(raw_rsa_keyring, patch_decrypt_on_wrappi assert not patch_decrypt_on_wrapping_key.called -def test_keyring_trace_on_decrypt_when_data_key_given(raw_rsa_keyring): +def test_on_decrypt_no_private_key(raw_rsa_keyring): + raw_rsa_keyring._private_wrapping_key = None + + materials = get_decryption_materials_without_data_encryption_key() + test = raw_rsa_keyring.on_decrypt(decryption_materials=materials, encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA],) + + assert test is materials + + +def test_on_decrypt_keyring_trace_when_data_key_given(raw_rsa_keyring): test_raw_rsa_keyring = raw_rsa_keyring test = test_raw_rsa_keyring.on_decrypt( decryption_materials=get_decryption_materials_with_data_encryption_key(), encrypted_data_keys=[_ENCRYPTED_DATA_KEY_RSA], ) - for keyring_trace in test.keyring_trace: - if keyring_trace.wrapping_key.key_info == _KEY_ID: - # Check keyring trace does not contain KeyringTraceFlag.DECRYPTED_DATA_KEY - assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 0 def test_on_decrypt_when_data_key_and_edk_not_provided(raw_rsa_keyring, patch_decrypt_on_wrapping_key): @@ -195,8 +217,21 @@ def test_on_decrypt_when_data_key_and_edk_not_provided(raw_rsa_keyring, patch_de ) assert not patch_decrypt_on_wrapping_key.called - for keyring_trace in test.keyring_trace: - assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 0 + + assert test.data_encryption_key is None + + +def test_on_decrypt_when_data_key_not_provided_and_no_know_edks(raw_rsa_keyring, mocker): + patched_wrapping_key_decrypt = mocker.patch.object(raw_rsa_keyring._private_wrapping_key, "decrypt") + + test = raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], + ) + + assert not patched_wrapping_key_decrypt.called assert test.data_encryption_key is None @@ -210,9 +245,8 @@ def test_on_decrypt_when_data_key_not_provided_and_edk_not_in_keyring(raw_rsa_ke ) assert not patch_decrypt_on_wrapping_key.called - for keyring_trace in test.keyring_trace: - if keyring_trace.wrapping_key.key_info == _KEY_ID: - assert KeyringTraceFlag.DECRYPTED_DATA_KEY not in keyring_trace.flags + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert not trace_entries assert test.data_encryption_key is None @@ -230,7 +264,7 @@ def test_on_decrypt_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring, ) -def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring): +def test_on_decrypt_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_rsa_keyring): test_raw_rsa_keyring = raw_rsa_keyring test = test_raw_rsa_keyring.on_decrypt( @@ -239,11 +273,31 @@ def test_keyring_trace_when_data_key_not_provided_and_edk_provided(raw_rsa_keyri encryption_materials=get_encryption_materials_without_data_encryption_key() ).encrypted_data_keys, ) - decrypted_flag_count = 0 - for keyring_trace in test.keyring_trace: - if KeyringTraceFlag.DECRYPTED_DATA_KEY in keyring_trace.flags: - decrypted_flag_count += 1 + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 1 + + decrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.DECRYPTED_DATA_KEY}] + assert len(decrypt_traces) == 1 - assert decrypted_flag_count == 1 assert test.data_encryption_key is not None + + +def test_on_decrypt_continues_through_edks_on_failure(raw_rsa_keyring, mocker): + patched_wrapping_key_decrypt = mocker.patch.object(raw_rsa_keyring._private_wrapping_key, "decrypt") + patched_wrapping_key_decrypt.side_effect = (Exception("DECRYPT FAIL"), _DATA_KEY) + + test = raw_rsa_keyring.on_decrypt( + decryption_materials=get_decryption_materials_without_data_encryption_key(), + encrypted_data_keys=(_ENCRYPTED_DATA_KEY_RSA, _ENCRYPTED_DATA_KEY_RSA), + ) + + assert patched_wrapping_key_decrypt.call_count == 2 + + trace_entries = [entry for entry in test.keyring_trace if entry.wrapping_key == raw_rsa_keyring._key_provider] + assert len(trace_entries) == 1 + + decrypt_traces = [entry for entry in trace_entries if entry.flags == {KeyringTraceFlag.DECRYPTED_DATA_KEY}] + assert len(decrypt_traces) == 1 + + assert test.data_encryption_key.data_key == _DATA_KEY diff --git a/test/unit/keyrings/test_multi.py b/test/unit/keyrings/test_multi.py index c0bdc78d9..747ef5c37 100644 --- a/test/unit/keyrings/test_multi.py +++ b/test/unit/keyrings/test_multi.py @@ -187,7 +187,11 @@ def test_number_of_encrypted_data_keys_with_generator_and_children(): def test_on_encrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) - test_multi_keyring.on_encrypt(encryption_materials=get_encryption_materials_with_data_key()) + initial_materials = get_encryption_materials_with_data_key() + new_materials = test_multi_keyring.on_encrypt(encryption_materials=initial_materials) + + assert new_materials is not initial_materials + for keyring in test_multi_keyring._decryption_keyrings: keyring.on_encrypt.assert_called_once() @@ -208,7 +212,11 @@ def test_on_encrypt_edk_length_when_keyring_generates_but_does_not_encrypt_encry def test_on_decrypt_when_data_encryption_key_given(mock_generator, mock_child_1, mock_child_2): test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_1, mock_child_2]) - test_multi_keyring.on_decrypt(decryption_materials=get_decryption_materials_with_data_key(), encrypted_data_keys=[]) + initial_materials = get_decryption_materials_with_data_key() + new_materials = test_multi_keyring.on_decrypt(decryption_materials=initial_materials, encrypted_data_keys=[]) + + assert new_materials is initial_materials + for keyring in test_multi_keyring._decryption_keyrings: assert not keyring.on_decrypt.called @@ -238,9 +246,10 @@ def test_no_keyring_called_after_data_encryption_key_added_when_data_encryption_ ) test_multi_keyring = MultiKeyring(generator=mock_generator, children=[mock_child_3, mock_child_1, mock_child_2]) - test_multi_keyring.on_decrypt( - decryption_materials=get_decryption_materials_without_data_key(), encrypted_data_keys=[] - ) + initial_materials = get_decryption_materials_without_data_key() + new_materials = test_multi_keyring.on_decrypt(decryption_materials=initial_materials, encrypted_data_keys=[]) + + assert new_materials is not initial_materials assert mock_generator.on_decrypt.called assert mock_child_3.on_decrypt.called assert not mock_child_1.called diff --git a/test/unit/materials_managers/test_material_managers.py b/test/unit/materials_managers/test_material_managers.py index 499f7ba0d..62314298e 100644 --- a/test/unit/materials_managers/test_material_managers.py +++ b/test/unit/materials_managers/test_material_managers.py @@ -16,7 +16,6 @@ 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 @@ -104,7 +103,6 @@ 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)), @@ -123,6 +121,8 @@ def test_attributes_fails(attr_class, invalid_kwargs): ( (CryptographicMaterials, {}), (EncryptionMaterials, {}), + (EncryptionMaterials, dict(data_encryption_key=_REMOVE, encrypted_data_keys=[])), + (EncryptionMaterials, dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE)), (DecryptionMaterials, {}), (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_REMOVE)), (DecryptionMaterials, dict(data_key=_REMOVE, data_encryption_key=_RAW_DATA_KEY)), @@ -248,18 +248,19 @@ def test_empty_encrypted_data_keys(): (DecryptionMaterials, KeyringTraceFlag.DECRYPTED_DATA_KEY), ), ) -def test_add_data_encryption_key_success(material_class, flag): +def test_with_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( + new_materials = materials.with_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}), ) + assert new_materials is not materials def _add_data_encryption_key_test_cases(): @@ -313,28 +314,29 @@ def _add_data_encryption_key_test_cases(): "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( +def test_with_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) + materials.with_data_encryption_key(data_encryption_key=data_encryption_key, keyring_trace=keyring_trace) excinfo.match(exception_message) -def test_add_encrypted_data_key_success(): +def test_with_encrypted_data_key_success(): kwargs = _copy_and_update_kwargs("EncryptionMaterials", {}) materials = EncryptionMaterials(**kwargs) - materials.add_encrypted_data_key( + new_materials = materials.with_encrypted_data_key( _ENCRYPTED_DATA_KEY, keyring_trace=KeyringTrace( wrapping_key=_ENCRYPTED_DATA_KEY.key_provider, flags={KeyringTraceFlag.ENCRYPTED_DATA_KEY} ), ) + assert new_materials is not materials @pytest.mark.parametrize( @@ -366,21 +368,22 @@ def test_add_encrypted_data_key_success(): ), ), ) -def test_add_encrypted_data_key_fail(mod_kwargs, encrypted_data_key, keyring_trace, exception_type, exception_message): +def test_with_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) + materials.with_encrypted_data_key(encrypted_data_key=encrypted_data_key, keyring_trace=keyring_trace) excinfo.match(exception_message) -def test_add_signing_key_success(): +def test_with_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()) + new_materials = materials.with_signing_key(signing_key=_SIGNING_KEY.key_bytes()) + assert new_materials is not materials @pytest.mark.parametrize( @@ -395,21 +398,22 @@ def test_add_signing_key_success(): ), ), ) -def test_add_signing_key_fail(mod_kwargs, signing_key, exception_type, exception_message): +def test_with_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) + materials.with_signing_key(signing_key=signing_key) excinfo.match(exception_message) -def test_add_verification_key_success(): +def test_with_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()) + new_materials = materials.with_verification_key(verification_key=_VERIFICATION_KEY.key_bytes()) + assert new_materials is not materials @pytest.mark.parametrize( @@ -424,12 +428,12 @@ def test_add_verification_key_success(): ), ), ) -def test_add_verification_key_fail(mod_kwargs, verification_key, exception_type, exception_message): +def test_with_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) + materials.with_verification_key(verification_key=verification_key) excinfo.match(exception_message) @@ -457,7 +461,9 @@ def test_decryption_materials_is_not_complete(mod_kwargs): def test_encryption_materials_is_complete(): - materials = EncryptionMaterials(**_copy_and_update_kwargs("EncryptionMaterials", {})) + materials = EncryptionMaterials( + **_copy_and_update_kwargs("EncryptionMaterials", dict(encrypted_data_keys=[_ENCRYPTED_DATA_KEY])) + ) assert materials.is_complete @@ -466,6 +472,7 @@ def test_encryption_materials_is_complete(): "mod_kwargs", ( dict(data_encryption_key=_REMOVE, encrypted_data_keys=_REMOVE), + dict(encrypted_data_keys=[]), dict(encrypted_data_keys=_REMOVE), dict(signing_key=_REMOVE), ), diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index ddde3e975..9063badc6 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -33,6 +33,7 @@ _ENCRYPTION_CONTEXT = {"encryption": "context", "values": "here"} _PROVIDER_ID = "Random Raw Keys" +_EXISTING_KEY_ID = b"pre-seeded key id" _KEY_ID = b"5325b043-5843-4629-869c-64794af77ada" _WRAPPING_KEY = b"\xeby-\x80A6\x15rA8\x83#,\xe4\xab\xac`\xaf\x99Z\xc1\xce\xdb\xb6\x0f\xb7\x805\xb2\x14J3" _SIGNING_KEY = b"aws-crypto-public-key" @@ -98,7 +99,7 @@ def on_encrypt(self, encryption_materials): data_encryption_key = RawDataKey( key_provider=key_provider, data_key=os.urandom(encryption_materials.algorithm.kdf_input_len) ) - encryption_materials.add_data_encryption_key( + encryption_materials = encryption_materials.with_data_encryption_key( data_encryption_key=data_encryption_key, keyring_trace=KeyringTrace(wrapping_key=key_provider, flags={KeyringTraceFlag.GENERATED_DATA_KEY}), ) @@ -113,14 +114,14 @@ def get_encryption_materials_with_data_key(): return EncryptionMaterials( algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', ), encryption_context=_ENCRYPTION_CONTEXT, signing_key=_SIGNING_KEY, keyring_trace=[ KeyringTrace( - wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], @@ -131,14 +132,14 @@ def get_encryption_materials_with_data_encryption_key(): return EncryptionMaterials( algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', ), encryption_context=_ENCRYPTION_CONTEXT, signing_key=_SIGNING_KEY, keyring_trace=[ KeyringTrace( - wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), flags={KeyringTraceFlag.GENERATED_DATA_KEY}, ) ], @@ -157,12 +158,12 @@ def get_encryption_materials_with_encrypted_data_key(): return EncryptionMaterials( algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', ), encrypted_data_keys=[ EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), encrypted_data_key=b"\xde^\x97\x7f\x84\xe9\x9e\x98\xd0\xe2\xf8\xd5\xcb\xe9\x7f.}\x87\x16,\x11n#\xc8p" b"\xdb\xbf\x94\x86*Q\x06\xd2\xf5\xdah\x08\xa4p\x81\xf7\xf4G\x07FzE\xde", ) @@ -171,7 +172,7 @@ def get_encryption_materials_with_encrypted_data_key(): signing_key=_SIGNING_KEY, keyring_trace=[ KeyringTrace( - wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), flags={KeyringTraceFlag.GENERATED_DATA_KEY, KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ) ], @@ -182,7 +183,7 @@ def get_encryption_materials_with_encrypted_data_key_aes(): return EncryptionMaterials( algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', ), encrypted_data_keys=[_ENCRYPTED_DATA_KEY_AES], @@ -190,7 +191,7 @@ def get_encryption_materials_with_encrypted_data_key_aes(): signing_key=_SIGNING_KEY, keyring_trace=[ KeyringTrace( - wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), flags={KeyringTraceFlag.GENERATED_DATA_KEY, KeyringTraceFlag.ENCRYPTED_DATA_KEY}, ) ], @@ -217,14 +218,14 @@ def get_decryption_materials_with_data_key(): return DecryptionMaterials( algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', ), encryption_context=_ENCRYPTION_CONTEXT, verification_key=b"ex_verification_key", keyring_trace=[ KeyringTrace( - wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_KEY_ID), + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, ) ], @@ -235,14 +236,14 @@ def get_decryption_materials_with_data_encryption_key(): return DecryptionMaterials( algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, data_encryption_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), data_key=b'*!\xa1"^-(\xf3\x105\x05i@B\xc2\xa2\xb7\xdd\xd5\xd5\xa9\xddm\xfae\xa8\\$\xf9d\x1e(', ), encryption_context=_ENCRYPTION_CONTEXT, verification_key=b"ex_verification_key", keyring_trace=[ KeyringTrace( - wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"5430b043-5843-4629-869c-64794af77ada"), + wrapping_key=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=_EXISTING_KEY_ID), flags={KeyringTraceFlag.DECRYPTED_DATA_KEY}, ) ], From 8d84ff531558821fa177c7482e403869baf1340e Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Tue, 31 Mar 2020 11:48:23 -0700 Subject: [PATCH 45/64] docs: add keyring examples (#221) * docs: add raw RSA keyring examples * fix: fix type signatures in raw RSA keyring * docs: add raw AES keyring example * chore: move single-CMK example to new location * docs: update single-CMK example to match new template * fix: fix docstring in raw AES keyring example * chore: move multi-region KMS example to new location * chore: update multi-region AWS keyring example to new format * docs: add multi-keyring example * docs: add custom KMS client config keyring example * docs: add multi-partition client supplier example * docs: clarify custom client supplier example intro * docs: add reference to in-region-discovery example * docs: add basic KMS discovery keyring example * docs: add examples showing region-scoped discovery keyrings * docs: add listing of keyring examples * chore: remove reminder to write more because I already did * docs: clean up multikeyring description * docs: rearrange examples readme * fix: set AWS_DEFAULT_REGION to region of primary CMK for examples tets * Apply suggestions from code review Co-Authored-By: June Blender Co-Authored-By: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> * fix: fix examples test runner for Windows * docs: update examples docs and comments with feedback * docs: re-order operations in RSA example for clarity * docs: add framing for examples in readme * docs: adjust wording * docs: remove reference to Java * docs: derive allowed discovery region from ARN rather than hard-coding it * docs: change examples to talk about "KMS discovery keyring" rather than "KMS keyring in discovery mode" * docs: add link to NIST docs explaining RSA key size and fix typos * docs: apply suggestions from code review Co-Authored-By: June Blender * docs: carrying over copyedits * feat: update KMS discovery keyring examples to use new discovery configuration * docs: fix typo * docs: remove trailing whitespace Co-authored-by: June Blender Co-authored-by: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> --- examples/README.md | 44 +++++- examples/src/file_streaming_defaults.py | 8 +- examples/src/in_memory_streaming_defaults.py | 10 +- examples/src/keyring/__init__.py | 7 + examples/src/keyring/aws_kms/__init__.py | 7 + .../keyring/aws_kms/custom_client_supplier.py | 115 ++++++++++++++++ .../aws_kms/custom_kms_client_config.py | 88 ++++++++++++ .../src/keyring/aws_kms/discovery_decrypt.py | 76 +++++++++++ .../discovery_decrypt_in_region_only.py | 89 ++++++++++++ ...iscovery_decrypt_with_preferred_regions.py | 110 +++++++++++++++ .../src/keyring/aws_kms/multiple_regions.py | 93 +++++++++++++ examples/src/keyring/aws_kms/single_cmk.py | 66 +++++++++ examples/src/keyring/multi/__init__.py | 7 + .../src/keyring/multi/aws_kms_with_escrow.py | 128 ++++++++++++++++++ examples/src/keyring/raw_aes/__init__.py | 7 + examples/src/keyring/raw_aes/raw_aes.py | 78 +++++++++++ examples/src/keyring/raw_rsa/__init__.py | 7 + .../src/keyring/raw_rsa/private_key_only.py | 88 ++++++++++++ .../raw_rsa/private_key_only_from_pem.py | 101 ++++++++++++++ .../raw_rsa/public_private_key_separate.py | 125 +++++++++++++++++ .../src/legacy/multiple_kms_cmk_regions.py | 65 --------- examples/src/legacy/one_kms_cmk.py | 38 ------ examples/src/onestep_defaults.py | 10 +- examples/src/onestep_unsigned.py | 12 +- examples/test/examples_test_utils.py | 12 +- examples/test/test_run_examples.py | 6 +- src/aws_encryption_sdk/keyrings/raw.py | 1 + 27 files changed, 1263 insertions(+), 135 deletions(-) create mode 100644 examples/src/keyring/__init__.py create mode 100644 examples/src/keyring/aws_kms/__init__.py create mode 100644 examples/src/keyring/aws_kms/custom_client_supplier.py create mode 100644 examples/src/keyring/aws_kms/custom_kms_client_config.py create mode 100644 examples/src/keyring/aws_kms/discovery_decrypt.py create mode 100644 examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py create mode 100644 examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py create mode 100644 examples/src/keyring/aws_kms/multiple_regions.py create mode 100644 examples/src/keyring/aws_kms/single_cmk.py create mode 100644 examples/src/keyring/multi/__init__.py create mode 100644 examples/src/keyring/multi/aws_kms_with_escrow.py create mode 100644 examples/src/keyring/raw_aes/__init__.py create mode 100644 examples/src/keyring/raw_aes/raw_aes.py create mode 100644 examples/src/keyring/raw_rsa/__init__.py create mode 100644 examples/src/keyring/raw_rsa/private_key_only.py create mode 100644 examples/src/keyring/raw_rsa/private_key_only_from_pem.py create mode 100644 examples/src/keyring/raw_rsa/public_private_key_separate.py delete mode 100644 examples/src/legacy/multiple_kms_cmk_regions.py delete mode 100644 examples/src/legacy/one_kms_cmk.py diff --git a/examples/README.md b/examples/README.md index f370f7407..592872232 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,14 +14,45 @@ and streaming APIs. You can find examples that demonstrate these APIs in the [`examples/src/`](./src) directory. +* [How to encrypt and decrypt](./src/onestep_defaults.py) +* [How to change the algorithm suite](./src/onestep_unsigned.py) +* [How to encrypt and decrypt data streams in memory](./src/in_memory_streaming_defaults.py) +* [How to encrypt and decrypt data streamed between files](./src/file_streaming_defaults.py) + ## Configuration -To use the library APIs, +To use the encryption and decryption APIs, you need to describe how you want the library to protect your data keys. -You can do this using -[keyrings][#keyrings] or [cryptographic materials managers][#cryptographic-materials-managers], -or using [master key providers][#master-key-providers]. -These examples will show you how. +You can do this by configuring +[keyrings](#keyrings) or [cryptographic materials managers](#cryptographic-materials-managers), +or by configuring [master key providers](#master-key-providers). +These examples will show you how to use the configuration tools that we include for you +and how to create some of your own. +We start with AWS KMS examples, then show how to use other wrapping keys. + +* Using AWS Key Management Service (AWS KMS) + * How to use one AWS KMS CMK + * [with keyrings](./src/keyring/aws_kms/single_cmk.py) + * How to use multiple AWS KMS CMKs in different regions + * [with keyrings](./src/keyring/aws_kms/multiple_regions.py) + * How to decrypt when you don't know the CMK + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt.py) + * How to decrypt within a region + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt_in_region_only.py) + * How to decrypt with a preferred region but failover to others + * [with keyrings](./src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py) +* Using raw wrapping keys + * How to use a raw AES wrapping key + * [with keyrings](./src/keyring/raw_aes/raw_aes.py) + * How to use a raw RSA wrapping key + * [with keyrings](./src/keyring/raw_rsa/private_key_only.py) + * How to use a raw RSA wrapping key when the key is PEM or DER encoded + * [with keyrings](./src/keyring/raw_rsa/private_key_only_from_pem.py) + * How to encrypt with a raw RSA public key wrapping key without access to the private key + * [with keyrings](./src/keyring/raw_rsa/public_private_key_separate.py) +* Combining wrapping keys + * How to combine AWS KMS with an offline escrow key + * [with keyrings](./src/keyring/multi/aws_kms_with_escrow.py) ### Keyrings @@ -56,7 +87,8 @@ you can find these examples in [`examples/src/master_key_provider`](./src/master ## Legacy -This section includes older examples, including examples of using master keys and master key providers in Java and Python. +This section includes older examples, +including examples of using master keys and master key providers. You can use them as a reference, but we recommend looking at the newer examples, which explain the preferred ways of using this library. You can find these examples in [`examples/src/legacy`](./src/legacy). diff --git a/examples/src/file_streaming_defaults.py b/examples/src/file_streaming_defaults.py index 983bfc90d..84bba1d31 100644 --- a/examples/src/file_streaming_defaults.py +++ b/examples/src/file_streaming_defaults.py @@ -54,15 +54,15 @@ def run(aws_kms_cmk, source_plaintext_filename): for segment in encryptor: ciphertext.write(segment) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert not filecmp.cmp(source_plaintext_filename, ciphertext_filename) # Open the files you want to work with. with open(ciphertext_filename, "rb") as ciphertext, open(decrypted_filename, "wb") as decrypted: # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the message header includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. with aws_encryption_sdk.stream(mode="decrypt", source=ciphertext, keyring=keyring) as decryptor: # Check the encryption context in the header before we start decrypting. # @@ -78,5 +78,5 @@ def run(aws_kms_cmk, source_plaintext_filename): for segment in decryptor: decrypted.write(segment) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert filecmp.cmp(source_plaintext_filename, decrypted_filename) diff --git a/examples/src/in_memory_streaming_defaults.py b/examples/src/in_memory_streaming_defaults.py index 1a2824b94..984848bce 100644 --- a/examples/src/in_memory_streaming_defaults.py +++ b/examples/src/in_memory_streaming_defaults.py @@ -9,7 +9,7 @@ In this example, we use an AWS KMS customer master key (CMK), but you can use other key management options with the AWS Encryption SDK. For examples that demonstrate how to use other key management configurations, -see the ``keyring`` and ``mater_key_provider`` directories. +see the ``keyring`` and ``master_key_provider`` directories. """ import io @@ -49,7 +49,7 @@ def run(aws_kms_cmk, source_plaintext): for segment in encryptor: ciphertext.write(segment) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert ciphertext.getvalue() != source_plaintext # Reset the ciphertext stream position so that we can read from the beginning. @@ -57,8 +57,8 @@ def run(aws_kms_cmk, source_plaintext): # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the header message includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. decrypted = io.BytesIO() with aws_encryption_sdk.stream(mode="decrypt", source=ciphertext, keyring=keyring) as decryptor: # Check the encryption context in the header before we start decrypting. @@ -75,5 +75,5 @@ def run(aws_kms_cmk, source_plaintext): for segment in decryptor: decrypted.write(segment) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted.getvalue() == source_plaintext diff --git a/examples/src/keyring/__init__.py b/examples/src/keyring/__init__.py new file mode 100644 index 000000000..c718f08e8 --- /dev/null +++ b/examples/src/keyring/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Keyring examples. + +These examples show how to use keyrings. +""" diff --git a/examples/src/keyring/aws_kms/__init__.py b/examples/src/keyring/aws_kms/__init__.py new file mode 100644 index 000000000..e9d9452c8 --- /dev/null +++ b/examples/src/keyring/aws_kms/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +AWS KMS keyring examples. + +These examples show how to use the KMS keyring. +""" diff --git a/examples/src/keyring/aws_kms/custom_client_supplier.py b/examples/src/keyring/aws_kms/custom_client_supplier.py new file mode 100644 index 000000000..159f8a2ab --- /dev/null +++ b/examples/src/keyring/aws_kms/custom_client_supplier.py @@ -0,0 +1,115 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +By default, the KMS keyring uses a client supplier that +supplies a client with the same configuration for every region. +If you need different behavior, you can write your own client supplier. + +You might use this +if you need different credentials in different AWS regions. +This might be because you are crossing partitions (ex: ``aws`` and ``aws-cn``) +or if you are working with regions that have separate authentication silos +like ``ap-east-1`` and ``me-south-1``. + +This example shows how to create a client supplier +that will supply KMS clients with valid credentials for the target region +even when working with regions that need different credentials. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For another example of how to use the KMS keyring with a custom client configuration, +see the ``keyring/aws_kms/custom_kms_client_config`` example. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +from botocore.client import BaseClient +from botocore.session import Session + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import ClientSupplier, DefaultClientSupplier + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only need these imports when running the mypy checks + pass + + +class MultiPartitionClientSupplier(ClientSupplier): + """Client supplier that supplies clients across AWS partitions and identity silos.""" + + def __init__(self): + """Set up default client suppliers for identity silos.""" + self._china_supplier = DefaultClientSupplier(botocore_session=Session(profile="china")) + self._middle_east_supplier = DefaultClientSupplier(botocore_session=Session(profile="middle-east")) + self._hong_kong_supplier = DefaultClientSupplier(botocore_session=Session(profile="hong-kong")) + self._default_supplier = DefaultClientSupplier() + + def __call__(self, region_name): + # type: (Union[None, str]) -> BaseClient + """Return a client for the requested region. + + :rtype: BaseClient + """ + if region_name.startswith("cn-"): + return self._china_supplier(region_name) + + if region_name.startswith("me-"): + return self._middle_east_supplier(region_name) + + if region_name == "ap-east-1": + return self._hong_kong_supplier(region_name) + + return self._default_supplier(region_name) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with a custom client supplier. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk, client_supplier=MultiPartitionClientSupplier()) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/custom_kms_client_config.py b/examples/src/keyring/aws_kms/custom_kms_client_config.py new file mode 100644 index 000000000..2a8e7c759 --- /dev/null +++ b/examples/src/keyring/aws_kms/custom_kms_client_config.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +By default, the KMS keyring uses the default configurations +for all KMS clients and uses the default discoverable credentials. +If you need to change this configuration, +you can configure the client supplier. + +This example shows how to use custom-configured clients with the KMS keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For another example of how to use the KMS keyring with custom client configuration, +see the ``keyring/aws_kms/custom_client_supplier`` example. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +from botocore.config import Config +from botocore.session import Session + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import DefaultClientSupplier + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with custom KMS client configuration. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Prepare your custom configuration values. + # + # Set your custom connection timeout value. + # https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html + custom_client_config = Config(connect_timeout=10.0, user_agent_extra=USER_AGENT_SUFFIX) + # For this example we will just use the default botocore session configuration + # but if you need to, you can set custom credentials in the botocore session. + custom_session = Session() + + # Use your custom configuration values to configure your client supplier. + client_supplier = DefaultClientSupplier(botocore_session=custom_session, client_config=custom_client_config) + + # Create the keyring that determines how your data keys are protected, + # providing the client supplier that you created. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk, client_supplier=client_supplier) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/discovery_decrypt.py b/examples/src/keyring/aws_kms/discovery_decrypt.py new file mode 100644 index 000000000..85f0e9c2c --- /dev/null +++ b/examples/src/keyring/aws_kms/discovery_decrypt.py @@ -0,0 +1,76 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. +This is true both on encrypt and on decrypt. +However, sometimes you need more flexibility on decrypt, +especially when you don't know which CMKs were used to encrypt a message. +To address this need, you can use a KMS discovery keyring. +The KMS discovery keyring does nothing on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + +This example shows how to configure and use a KMS discovery keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS discovery keyring on decrypt, +see the ``keyring/aws_kms/discovery_decrypt_in_region_only`` +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS discovery keyring for decryption. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + encrypt_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create a KMS discovery keyring to use on decrypt. + decrypt_keyring = KmsKeyring(is_discovery=True) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the KMS discovery keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=decrypt_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py b/examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py new file mode 100644 index 000000000..326f512ec --- /dev/null +++ b/examples/src/keyring/aws_kms/discovery_decrypt_in_region_only.py @@ -0,0 +1,89 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. +This is true both on encrypt and on decrypt. +However, sometimes you need more flexibility on decrypt, +especially when you don't know which CMKs were used to encrypt a message. +To address this need, you can use a KMS discovery keyring. +The KMS discovery keyring does nothing on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + +However, sometimes you need to be a *bit* more restrictive than that. +To address this need, you can use a client supplier that restricts the regions a KMS keyring can talk to. + +This example shows how to configure and use a KMS regional discovery keyring that is restricted to one region. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS discovery keyring on decrypt, +see the ``keyring/aws_kms/discovery_decrypt`` +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import AllowRegionsClientSupplier + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS discovery keyring to only work within a single region. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + encrypt_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Extract the region from the CMK ARN. + decrypt_region = aws_kms_cmk.split(":", 4)[3] + + # Create the KMS discovery keyring that we will use on decrypt. + # + # The client supplier that we specify here will only supply clients for the specified region. + # The keyring only attempts to decrypt data keys if it can get a client for that region, + # so this keyring will now ignore any data keys that were encrypted under a CMK in another region. + decrypt_keyring = KmsKeyring( + is_discovery=True, client_supplier=AllowRegionsClientSupplier(allowed_regions=[decrypt_region]) + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the KMS discovery keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=decrypt_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py b/examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py new file mode 100644 index 000000000..ebe8a4953 --- /dev/null +++ b/examples/src/keyring/aws_kms/discovery_decrypt_with_preferred_regions.py @@ -0,0 +1,110 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you give the KMS keyring specific key IDs it will use those CMKs and nothing else. +This is true both on encrypt and on decrypt. +However, sometimes you need more flexibility on decrypt, +especially when you don't know which CMKs were used to encrypt a message. +To address this need, you can use a KMS discovery keyring. +The KMS discovery keyring does nothing on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. + +However, sometimes you need to be a *bit* more restrictive than that. +To address this need, you can use a client supplier to restrict what regions a KMS keyring can talk to. + +A more complex but more common use-case is that you would *prefer* to stay within a region, +but you would rather make calls to other regions than fail to decrypt the message. +In this case, you want a keyring that will try to decrypt data keys in this region first, +then try other regions. + +This example shows how to configure and use a multi-keyring with the KMS keyring +to prefer the current AWS region while also failing over to other AWS regions. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS discovery keyring on decrypt, +see the ``keyring/aws_kms/discovery_decrypt`` +and ``keyring/aws_kms/discovery_decrypt_in_region_only`` examples. +""" +from boto3.session import Session + +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.aws_kms.client_suppliers import AllowRegionsClientSupplier, DenyRegionsClientSupplier +from aws_encryption_sdk.keyrings.multi import MultiKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS discovery-like keyring a particular AWS region and failover to others. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + encrypt_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # To create our decrypt keyring, we need to know our current default AWS region. + # + # Create a throw-away boto3 session to discover the default region. + local_region = Session().region_name + + # Now, use that region name to create two KMS discovery keyrings: + # + # One that only works in the local region + local_region_decrypt_keyring = KmsKeyring( + is_discovery=True, client_supplier=AllowRegionsClientSupplier(allowed_regions=[local_region]) + ) + # and one that will work in any other region but NOT the local region. + other_regions_decrypt_keyring = KmsKeyring( + is_discovery=True, client_supplier=DenyRegionsClientSupplier(denied_regions=[local_region]) + ) + + # Finally, combine those two keyrings into a multi-keyring. + # + # The multi-keyring steps through its member keyrings in the order that you provide them, + # attempting to decrypt every encrypted data key with each keyring before moving on to the next keyring. + # Because of this, other_regions_decrypt_keyring will not be called + # unless local_region_decrypt_keyring fails to decrypt every encrypted data key. + decrypt_keyring = MultiKeyring(children=[local_region_decrypt_keyring, other_regions_decrypt_keyring]) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the multi-keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=decrypt_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/multiple_regions.py b/examples/src/keyring/aws_kms/multiple_regions.py new file mode 100644 index 000000000..f466796c3 --- /dev/null +++ b/examples/src/keyring/aws_kms/multiple_regions.py @@ -0,0 +1,93 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to configure and use a KMS keyring with with CMKs in multiple regions. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with a single CMK, +see the ``keyring/aws_kms/single_cmk`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Sequence # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext): + # type: (str, Sequence[str], bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with CMKs in multiple regions. + + :param str aws_kms_generator_cmk: The ARN of the primary AWS KMS CMK + :param List[str] aws_kms_additional_cmks: Additional ARNs of secondary KMS CMKs + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that will encrypt your data keys under all requested CMKs. + many_cmks_keyring = KmsKeyring(generator_key_id=aws_kms_generator_cmk, child_key_ids=aws_kms_additional_cmks) + + # Create keyrings that each only use one of the CMKs. + # We will use these later to demonstrate that any of the CMKs can be used to decrypt the message. + # + # We provide these in "child_key_ids" rather than "generator_key_id" + # so that these keyrings cannot be used to generate a new data key. + # We will only be using them on decrypt. + single_cmk_keyring_that_generated = KmsKeyring(child_key_ids=[aws_kms_generator_cmk]) + single_cmk_keyring_that_encrypted = KmsKeyring(child_key_ids=[aws_kms_additional_cmks[0]]) + + # Encrypt your plaintext data using the keyring that uses all requests CMKs. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=many_cmks_keyring + ) + + # Verify that the header contains the expected number of encrypted data keys (EDKs). + # It should contain one EDK for each CMK. + assert len(encrypt_header.encrypted_data_keys) == len(aws_kms_additional_cmks) + 1 + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data separately using the single-CMK keyrings. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted_1, decrypt_header_1 = aws_encryption_sdk.decrypt( + source=ciphertext, keyring=single_cmk_keyring_that_generated + ) + decrypted_2, decrypt_header_2 = aws_encryption_sdk.decrypt( + source=ciphertext, keyring=single_cmk_keyring_that_encrypted + ) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted_1 == source_plaintext + assert decrypted_2 == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header_1.encryption_context.items()) + assert set(encryption_context.items()) <= set(decrypt_header_2.encryption_context.items()) diff --git a/examples/src/keyring/aws_kms/single_cmk.py b/examples/src/keyring/aws_kms/single_cmk.py new file mode 100644 index 000000000..b186f5056 --- /dev/null +++ b/examples/src/keyring/aws_kms/single_cmk.py @@ -0,0 +1,66 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example shows how to configure and use a KMS keyring with a single KMS CMK. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-kms-keyring + +For an example of how to use the KMS keyring with CMKs in multiple regions, +see the ``keyring/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS keyring with custom client configurations, +see the ``keyring/aws_kms/custom_client_supplier`` +and ``keyring/aws_kms/custom_kms_client_config`` examples. + +For examples of how to use the KMS keyring in discovery mode on decrypt, +see the ``keyring/aws_kms/discovery_decrypt``, +``keyring/aws_kms/discovery_decrypt_in_region_only``, +and ``keyring/aws_kms/discovery_decrypt_with_preferred_region`` examples. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS keyring with a single CMK. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/multi/__init__.py b/examples/src/keyring/multi/__init__.py new file mode 100644 index 000000000..e5703355a --- /dev/null +++ b/examples/src/keyring/multi/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Multi-keyring examples. + +These examples show how to use the multi-keyring. +""" diff --git a/examples/src/keyring/multi/aws_kms_with_escrow.py b/examples/src/keyring/multi/aws_kms_with_escrow.py new file mode 100644 index 000000000..dac35b9d9 --- /dev/null +++ b/examples/src/keyring/multi/aws_kms_with_escrow.py @@ -0,0 +1,128 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +One use-case that we have seen customers need is +the ability to enjoy the benefits of AWS KMS during normal operation +but retain the ability to decrypt encrypted messages without access to AWS KMS. +This example shows how you can use the multi-keyring to achieve this +by combining a KMS keyring with a raw RSA keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-multi-keyring + +For more examples of how to use the KMS keyring, see the ``keyring/aws_kms`` examples. + +For more examples of how to use the raw RSA keyring, see the ``keyring/raw_rsa`` examples. + +In this example we generate a RSA keypair +but in practice you would want to keep your private key in an HSM +or other key management system. + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.multi import MultiKeyring +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a keyring to use an AWS KMS CMK and a RSA wrapping key. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Collect the public key from the private key. + public_key = private_key.public_key() + + # Create the encrypt keyring that only has access to the public key. + escrow_encrypt_keyring = RawRSAKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + public_wrapping_key=public_key, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Create the decrypt keyring that has access to the private key. + escrow_decrypt_keyring = RawRSAKeyring( + # The key namespace and key name MUST match the encrypt keyring. + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_wrapping_key=private_key, + # The wrapping algorithm MUST match the encrypt keyring. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Create the KMS keyring that you will use for decryption during normal operations. + kms_keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Combine the KMS keyring and the escrow encrypt keyring using the multi-keyring. + encrypt_keyring = MultiKeyring(generator=kms_keyring, children=[escrow_encrypt_keyring]) + + # Encrypt your plaintext data using the multi-keyring. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=encrypt_keyring + ) + + # Verify that the header contains the expected number of encrypted data keys (EDKs). + # It should contain one EDK for KMS and one for the escrow key. + assert len(encrypt_header.encrypted_data_keys) == 2 + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data separately using the KMS keyring and the escrow decrypt keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted_kms, decrypt_header_kms = aws_encryption_sdk.decrypt(source=ciphertext, keyring=kms_keyring) + decrypted_escrow, decrypt_header_escrow = aws_encryption_sdk.decrypt( + source=ciphertext, keyring=escrow_decrypt_keyring + ) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted_kms == source_plaintext + assert decrypted_escrow == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header_kms.encryption_context.items()) + assert set(encryption_context.items()) <= set(decrypt_header_escrow.encryption_context.items()) diff --git a/examples/src/keyring/raw_aes/__init__.py b/examples/src/keyring/raw_aes/__init__.py new file mode 100644 index 000000000..2159bf30d --- /dev/null +++ b/examples/src/keyring/raw_aes/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Raw AES keyring examples. + +These examples show how to use the raw AES keyring. +""" diff --git a/examples/src/keyring/raw_aes/raw_aes.py b/examples/src/keyring/raw_aes/raw_aes.py new file mode 100644 index 000000000..58aa697c9 --- /dev/null +++ b/examples/src/keyring/raw_aes/raw_aes.py @@ -0,0 +1,78 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This examples shows how to configure and use a raw AES keyring. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-aes-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import os + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawAESKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw AES keyring. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Choose the wrapping algorithm for the keyring to use. + wrapping_algorithm = WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING + + # Generate an AES key to use with your keyring. + # The key size depends on the wrapping algorithm. + # + # In practice, you should get this key from a secure key management system such as an HSM. + key = os.urandom(wrapping_algorithm.algorithm.kdf_input_len) + + # Create the keyring that determines how your data keys are protected. + keyring = RawAESKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-aes-keyring + key_namespace="some managed raw keys", + key_name=b"my AES wrapping key", + wrapping_key=key, + wrapping_algorithm=wrapping_algorithm, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/raw_rsa/__init__.py b/examples/src/keyring/raw_rsa/__init__.py new file mode 100644 index 000000000..742761bbe --- /dev/null +++ b/examples/src/keyring/raw_rsa/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Raw RSA keyring examples. + +These examples show how to use the raw RSA keyring. +""" diff --git a/examples/src/keyring/raw_rsa/private_key_only.py b/examples/src/keyring/raw_rsa/private_key_only.py new file mode 100644 index 000000000..db7705735 --- /dev/null +++ b/examples/src/keyring/raw_rsa/private_key_only.py @@ -0,0 +1,88 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This examples shows how to configure and use a raw RSA keyring using a pre-loaded RSA private key. + +If your RSA key is in PEM or DER format, +see the ``keyring/raw_rsa/private_key_only_from_pem`` example. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw RSA keyring. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Create the keyring that determines how your data keys are protected. + keyring = RawRSAKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_wrapping_key=private_key, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/raw_rsa/private_key_only_from_pem.py b/examples/src/keyring/raw_rsa/private_key_only_from_pem.py new file mode 100644 index 000000000..397a79274 --- /dev/null +++ b/examples/src/keyring/raw_rsa/private_key_only_from_pem.py @@ -0,0 +1,101 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +When you store RSA keys, you have to serialize them somehow. + +This example shows how to configure and use a raw RSA keyring using a PEM-encoded RSA private key. + +The most commonly used encodings for RSA keys tend to be PEM and DER. +The raw RSA keyring supports loading both public and private keys from these encodings. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw RSA keyring loaded from a PEM-encoded key. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Serialize the RSA private key to PEM encoding. + # This or DER encoding is likely to be what you get from your key management system in practice. + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Create the keyring that determines how your data keys are protected. + # + # If your key is encoded using DER, you can use RawRSAKeyring.from_der_encoding + keyring = RawRSAKeyring.from_pem_encoding( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_encoded_key=private_key_pem, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same keyring you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/keyring/raw_rsa/public_private_key_separate.py b/examples/src/keyring/raw_rsa/public_private_key_separate.py new file mode 100644 index 000000000..b86b86ece --- /dev/null +++ b/examples/src/keyring/raw_rsa/public_private_key_separate.py @@ -0,0 +1,125 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +One of the benefits of asymmetric encryption +is that you can encrypt with just the public key. +This means that you can give someone the ability to encrypt +without giving them the ability to decrypt. + +The raw RSA keyring supports encrypt-only operations +when it only has access to a public key. + +This example shows how to construct and use the raw RSA keyring +to encrypt with only the public key and decrypt with the private key. + +If your RSA key is in PEM or DER format, +see the ``keyring/raw_rsa/private_key_only_from_pem`` example. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.exceptions import AWSEncryptionSDKClientError +from aws_encryption_sdk.identifiers import WrappingAlgorithm +from aws_encryption_sdk.keyrings.raw import RawRSAKeyring + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using separate public and private raw RSA keyrings. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your keyring. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Collect the public key from the private key. + public_key = private_key.public_key() + + # The keyring determines how your data keys are protected. + # + # Create the encrypt keyring that only has access to the public key. + public_key_keyring = RawRSAKeyring( + # The key namespace and key name are defined by you + # and are used by the raw RSA keyring + # to determine whether it should attempt to decrypt + # an encrypted data key. + # + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/choose-keyring.html#use-raw-rsa-keyring + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + public_wrapping_key=public_key, + # The wrapping algorithm tells the raw RSA keyring + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Create the decrypt keyring that has access to the private key. + private_key_keyring = RawRSAKeyring( + # The key namespace and key name MUST match the encrypt keyring. + key_namespace="some managed raw keys", + key_name=b"my RSA wrapping key", + private_wrapping_key=private_key, + # The wrapping algorithm MUST match the encrypt keyring. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ) + + # Encrypt your plaintext data using the encrypt keyring. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=public_key_keyring + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Try to decrypt your encrypted data using the *encrypt* keyring. + # This demonstrates that you cannot decrypt using the public key. + try: + aws_encryption_sdk.decrypt(source=ciphertext, keyring=public_key_keyring) + except AWSEncryptionSDKClientError: + # The public key cannot decrypt. + # Reaching this point means everything is working as expected. + pass + else: + # Show that the public keyring could not decrypt. + raise AssertionError("The public key can never decrypt!") + + # Decrypt your encrypted data using the decrypt keyring. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=private_key_keyring) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/legacy/multiple_kms_cmk_regions.py b/examples/src/legacy/multiple_kms_cmk_regions.py deleted file mode 100644 index deefd73e9..000000000 --- a/examples/src/legacy/multiple_kms_cmk_regions.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -""" -Example showing basic encryption and decryption of a value already in memory -using multiple KMS CMKs in multiple regions. -""" -import aws_encryption_sdk -from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider - - -def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext, botocore_session=None): - """Encrypts and then decrypts a string under multiple KMS customer master keys (CMKs) in multiple regions. - - :param str aws_kms_generator_cmk: Amazon Resource Name (ARN) of the primary KMS CMK - :param List[str] aws_kms_additional_cmks: Additional Amazon Resource Names (ARNs) of secondary KMS CMKs - :param bytes source_plaintext: Data to encrypt - :param botocore_session: existing botocore session instance - :type botocore_session: botocore.session.Session - """ - encrypt_cmk = aws_kms_additional_cmks[0] - - # Check that these keys are in different regions - assert not aws_kms_generator_cmk.split(":")[3] == encrypt_cmk.split(":")[3] - - kwargs = dict(key_ids=[aws_kms_generator_cmk, encrypt_cmk]) - - if botocore_session is not None: - kwargs["botocore_session"] = botocore_session - - # Create master key provider using the ARNs of the keys and the session (botocore_session) - kms_key_provider = KMSMasterKeyProvider(**kwargs) - - # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header - ciphertext, encrypted_message_header = aws_encryption_sdk.encrypt( - key_provider=kms_key_provider, source=source_plaintext - ) - - # Check that both key ARNs are in the message headers - assert len(encrypted_message_header.encrypted_data_keys) == 2 - - # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header - # Either of our keys can be used to decrypt the message - plaintext_1, decrypted_message_header_1 = aws_encryption_sdk.decrypt( - key_provider=KMSMasterKey(key_id=aws_kms_generator_cmk), source=ciphertext - ) - plaintext_2, decrypted_message_header_2 = aws_encryption_sdk.decrypt( - key_provider=KMSMasterKey(key_id=encrypt_cmk), source=ciphertext - ) - - # Check that the original message and the decrypted message are the same - if not isinstance(source_plaintext, bytes): - plaintext_1 = plaintext_1.decode("utf-8") - plaintext_2 = plaintext_2.decode("utf-8") - assert source_plaintext == plaintext_1 - assert source_plaintext == plaintext_2 - - # Check that the headers of the encrypted message and decrypted message match - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header_1.encryption_context.items() - ) - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header_2.encryption_context.items() - ) diff --git a/examples/src/legacy/one_kms_cmk.py b/examples/src/legacy/one_kms_cmk.py deleted file mode 100644 index 7fb5f1431..000000000 --- a/examples/src/legacy/one_kms_cmk.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 -"""Example showing basic encryption and decryption of a value already in memory using one KMS CMK.""" -import aws_encryption_sdk - - -def run(aws_kms_cmk, source_plaintext, botocore_session=None): - """Encrypts and then decrypts a string under one KMS customer master key (CMK). - - :param str aws_kms_cmk: Amazon Resource Name (ARN) of the KMS CMK - :param bytes source_plaintext: Data to encrypt - :param botocore_session: existing botocore session instance - :type botocore_session: botocore.session.Session - """ - kwargs = dict(key_ids=[aws_kms_cmk]) - - if botocore_session is not None: - kwargs["botocore_session"] = botocore_session - - # Create master key provider using the ARN of the key and the session (botocore_session) - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(**kwargs) - - # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header - ciphertext, encrypted_message_header = aws_encryption_sdk.encrypt( - source=source_plaintext, key_provider=kms_key_provider - ) - - # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header - plaintext, decrypted_message_header = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=kms_key_provider) - - # Check if the original message and the decrypted message are the same - assert source_plaintext == plaintext - - # Check if the headers of the encrypted message and decrypted message match - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header.encryption_context.items() - ) diff --git a/examples/src/onestep_defaults.py b/examples/src/onestep_defaults.py index a1d6e3dbd..3ded2b3ae 100644 --- a/examples/src/onestep_defaults.py +++ b/examples/src/onestep_defaults.py @@ -6,7 +6,7 @@ In this example, we use an AWS KMS customer master key (CMK), but you can use other key management options with the AWS Encryption SDK. For examples that demonstrate how to use other key management configurations, -see the ``keyring`` and ``mater_key_provider`` directories. +see the ``keyring`` and ``master_key_provider`` directories. """ import aws_encryption_sdk from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring @@ -37,16 +37,16 @@ def run(aws_kms_cmk, source_plaintext): source=source_plaintext, encryption_context=encryption_context, keyring=keyring ) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the header message includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted == source_plaintext # Verify that the encryption context used in the decrypt operation includes diff --git a/examples/src/onestep_unsigned.py b/examples/src/onestep_unsigned.py index aafb09feb..72623087b 100644 --- a/examples/src/onestep_unsigned.py +++ b/examples/src/onestep_unsigned.py @@ -7,7 +7,7 @@ In this example, we use an AWS KMS customer master key (CMK), but you can use other key management options with the AWS Encryption SDK. For examples that demonstrate how to use other key management configurations, -see the ``keyring`` and ``mater_key_provider`` directories. +see the ``keyring`` and ``master_key_provider`` directories. The default algorithm suite includes a message-level signature that protects you from an attacker who has *decrypt* but not *encrypt* capability @@ -54,19 +54,19 @@ def run(aws_kms_cmk, source_plaintext): algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, ) - # Verify that the ciphertext and plaintext are different. + # Demonstrate that the ciphertext and plaintext are different. assert ciphertext != source_plaintext # Decrypt your encrypted data using the same keyring you used on encrypt. # - # We do not need to specify the encryption context on decrypt - # because the header message includes the encryption context. + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. # - # We do not need to specify the algorithm suite on decrypt + # You do not need to specify the algorithm suite on decrypt # because the header message includes the algorithm suite identifier. decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, keyring=keyring) - # Verify that the decrypted plaintext is identical to the original plaintext. + # Demonstrate that the decrypted plaintext is identical to the original plaintext. assert decrypted == source_plaintext # Verify that the encryption context used in the decrypt operation includes diff --git a/examples/test/examples_test_utils.py b/examples/test/examples_test_utils.py index 4f9aadef5..49379ff1b 100644 --- a/examples/test/examples_test_utils.py +++ b/examples/test/examples_test_utils.py @@ -27,6 +27,7 @@ os.environ["AWS_ENCRYPTION_SDK_EXAMPLES_TESTING"] = "yes" sys.path.extend([os.sep.join([os.path.dirname(__file__), "..", "..", "test", "integration"])]) +from integration_test_utils import get_all_cmk_arns # noqa pylint: disable=unused-import,import-error static_plaintext = ( b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. " @@ -58,9 +59,6 @@ ) -from integration_test_utils import get_all_cmk_arns # noqa pylint: disable=unused-import,import-error - - def all_examples(): # type: () -> Iterable[pytest.param] for (dirpath, _dirnames, filenames) in os.walk(EXAMPLES_SOURCE): @@ -70,7 +68,7 @@ def all_examples(): continue stem, suffix = split_path if suffix == "py" and stem != "__init__": - module_parent = dirpath[len(EXAMPLES_SOURCE) + 1 :].replace("/", ".") + module_parent = dirpath[len(EXAMPLES_SOURCE) + 1 :].replace(os.path.sep, ".") module_name = stem if module_parent: import_path = "..src.{base}.{name}".format(base=module_parent, name=module_name) @@ -114,3 +112,9 @@ def build_kwargs(function, temp_dir): except KeyError: pass return kwargs + + +def default_region(): + # type: () -> str + primary_cmk = get_all_cmk_arns()[0] + return primary_cmk.split(":", 4)[3] diff --git a/examples/test/test_run_examples.py b/examples/test/test_run_examples.py index 781f341ad..210c0119c 100644 --- a/examples/test/test_run_examples.py +++ b/examples/test/test_run_examples.py @@ -5,13 +5,13 @@ import pytest -from .examples_test_utils import all_examples, build_kwargs +from .examples_test_utils import all_examples, build_kwargs, default_region pytestmark = [pytest.mark.examples] @pytest.mark.parametrize("import_path", all_examples()) -def test_examples(import_path, tmp_path): +def test_examples(import_path, tmp_path, monkeypatch): module = import_module(name=import_path, package=__package__) try: run_function = module.run @@ -21,4 +21,6 @@ def test_examples(import_path, tmp_path): kwargs = build_kwargs(function=run_function, temp_dir=tmp_path) + monkeypatch.setenv("AWS_DEFAULT_REGION", default_region()) + run_function(**kwargs) diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index deab1a122..dfcc5294b 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -307,6 +307,7 @@ def from_der_encoding( private_encoded_key=None, # type: bytes password=None, # type: bytes ): + # type: (...) -> RawRSAKeyring """Generate a raw RSA keyring using DER Encoded public and private keys :param str key_namespace: String defining the keyring ID From 52794b37ab3d6c1cb19cff0c7a3cee851fa27283 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 31 Mar 2020 11:56:53 -0700 Subject: [PATCH 46/64] chore: add GitHub Actions workflows for all CI that does not require AWS credentials --- .github/workflows/ci_decrypt-oracle.yaml | 53 ++++++++++ .github/workflows/ci_static-analysis.yaml | 40 +++++++ .github/workflows/ci_test-vector-handler.yaml | 88 +++++++++++++++ .github/workflows/ci_tests.yaml | 100 ++++++++++++++++++ ci-requirements.txt | 1 + 5 files changed, 282 insertions(+) create mode 100644 .github/workflows/ci_decrypt-oracle.yaml create mode 100644 .github/workflows/ci_static-analysis.yaml create mode 100644 .github/workflows/ci_test-vector-handler.yaml create mode 100644 .github/workflows/ci_tests.yaml create mode 100644 ci-requirements.txt diff --git a/.github/workflows/ci_decrypt-oracle.yaml b/.github/workflows/ci_decrypt-oracle.yaml new file mode 100644 index 000000000..d8ecff117 --- /dev/null +++ b/.github/workflows/ci_decrypt-oracle.yaml @@ -0,0 +1,53 @@ +name: Continuous Integration tests for the decrypt oracle + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + # The oracle runs in a Python 3.6 Lamba + python-version: 3.6 + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: local + run: | + cd decrypt_oracle + tox -- -vv + static-analysis: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - bandit + - readme + - flake8 + - pylint + - flake8-tests + - pylint-tests + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: | + cd decrypt_oracle + tox -- -vv diff --git a/.github/workflows/ci_static-analysis.yaml b/.github/workflows/ci_static-analysis.yaml new file mode 100644 index 000000000..f80c429fe --- /dev/null +++ b/.github/workflows/ci_static-analysis.yaml @@ -0,0 +1,40 @@ +name: Static analysis checks + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +jobs: + analysis: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - bandit + - doc8 + - docs + - readme + - flake8 + - pylint + - flake8-tests + - pylint-tests + - flake8-examples + - pylint-examples + - black-check + - isort-check + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv diff --git a/.github/workflows/ci_test-vector-handler.yaml b/.github/workflows/ci_test-vector-handler.yaml new file mode 100644 index 000000000..f2efa30ce --- /dev/null +++ b/.github/workflows/ci_test-vector-handler.yaml @@ -0,0 +1,88 @@ +name: Continuous Integration tests for the test vector handler + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +jobs: + tests: + # Until we address the credentials problem, + # do not run for pull requests. + if: github.event != 'pull_request' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.x + architecture: + - x64 + - x86 + category: + - awses_1.3.3 + - awses_1.3.max + - awses_latest + exclude: + # x86 builds are only meaningful for Windows + - os: ubuntu-latest + architecture: x86 + - os: macos-latest + architecture: x86 + steps: + - uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.INTEG_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.INTEG_AWS_SECRET_ACCESS_KEY }} + aws-region: us-west-2 + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.architecture }} + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: | + cd test_vector_handlers + tox -- -vv + static-analysis: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + category: + - bandit + - readme + - flake8 + - pylint + - flake8-tests + - pylint-tests + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.x + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: | + cd test_vector_handlers + tox -- -vv diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml new file mode 100644 index 000000000..c2f297ea2 --- /dev/null +++ b/.github/workflows/ci_tests.yaml @@ -0,0 +1,100 @@ +name: Continuous Integration tests + +on: + pull_request: + push: + # Run once a day + schedule: + - cron: '0 0 * * *' + +env: + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: | + arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: | + arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + - 3.8 + - 3.x + architecture: + - x64 + - x86 + category: + - local + - accept +# These require credentials. +# Enable them once we sort how to provide them. +# - integ +# - examples + exclude: + # x86 builds are only meaningful for Windows + - os: ubuntu-latest + architecture: x86 + - os: macos-latest + architecture: x86 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + architecture: ${{ matrix.architecture }} + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv + upstream-py3: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + category: + - nocmk + - test-upstream-requirements-py37 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 3.7 + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv + upstream-py2: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + category: + - test-upstream-requirements-py27 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: 2.7 + - run: | + python -m pip install --upgrade pip + pip install --upgrade -r ci-requirements.txt + - name: run test + env: + TOXENV: ${{ matrix.category }} + run: tox -- -vv diff --git a/ci-requirements.txt b/ci-requirements.txt new file mode 100644 index 000000000..053148f84 --- /dev/null +++ b/ci-requirements.txt @@ -0,0 +1 @@ +tox From 6a20af6d4e0f5bd9ce8f6fbe4e16725c0b941f9f Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Tue, 31 Mar 2020 11:58:25 -0700 Subject: [PATCH 47/64] chore: remove checks from travis and appveyor that were moved to GitHub Actions workflows --- .travis.yml | 177 --------------------------------------------------- appveyor.yml | 40 ------------ 2 files changed, 217 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf290d719..a8ca00f68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,62 +3,34 @@ language: python matrix: include: # CPython 2.7 - - python: 2.7 - env: TOXENV=py27-local - stage: Client Tests - python: 2.7 env: TOXENV=py27-integ stage: Client Tests - - python: 2.7 - env: TOXENV=py27-accept - stage: Client Tests - python: 2.7 env: TOXENV=py27-examples stage: Client Tests # CPython 3.5 - - python: 3.5 - env: TOXENV=py35-local - stage: Client Tests - python: 3.5 env: TOXENV=py35-integ stage: Client Tests - - python: 3.5 - env: TOXENV=py35-accept - stage: Client Tests - python: 3.5 env: TOXENV=py35-examples stage: Client Tests # CPython 3.6 - - python: 3.6 - env: TOXENV=py36-local - stage: Client Tests - python: 3.6 env: TOXENV=py36-integ stage: Client Tests - - python: 3.6 - env: TOXENV=py36-accept - stage: Client Tests - python: 3.6 env: TOXENV=py36-examples stage: Client Tests # CPython 3.7 # xenial + sudo are currently needed to get 3.7 # https://github.com/travis-ci/travis-ci/issues/9815 - - python: 3.7 - env: TOXENV=py37-local - dist: xenial - sudo: true - stage: Client Tests - python: 3.7 env: TOXENV=py37-integ dist: xenial sudo: true stage: Client Tests - - python: 3.7 - env: TOXENV=py37-accept - dist: xenial - sudo: true - stage: Client Tests - python: 3.7 env: TOXENV=py37-examples dist: xenial @@ -67,78 +39,16 @@ matrix: # CPython 3.8 # xenial + sudo are currently needed to get 3.8 # https://github.com/travis-ci/travis-ci/issues/9815 - - python: 3.8 - env: TOXENV=py38-local - dist: xenial - sudo: true - stage: Client Tests - python: 3.8 env: TOXENV=py38-integ dist: xenial sudo: true stage: Client Tests - - python: 3.8 - env: TOXENV=py38-accept - dist: xenial - sudo: true - stage: Client Tests - python: 3.8 env: TOXENV=py38-examples dist: xenial sudo: true stage: Client Tests - # Upstream tests - - python: 3.6 - env: TOXENV=nocmk - stage: Upstream Tests - - python: 2.7 - env: TOXENV=test-upstream-requirements-py27 - stage: Upstream Tests - # xenial + sudo are currently needed to get 3.7 - # https://github.com/travis-ci/travis-ci/issues/9815 - - python: 3.7 - env: TOXENV=test-upstream-requirements-py37 - dist: xenial - sudo: true - stage: Upstream Tests - # Security - - python: 3.6 - env: TOXENV=bandit - stage: Security Checks - # Linting and autoformatting - - python: 3.6 - env: TOXENV=doc8 - stage: Formatting Checks - - python: 3.6 - env: TOXENV=docs - stage: Formatting Checks - - python: 3.6 - env: TOXENV=readme - stage: Formatting Checks - - python: 3.6 - env: TOXENV=flake8 - stage: Formatting Checks - - python: 3.6 - env: TOXENV=pylint - stage: Formatting Checks - - python: 3.6 - env: TOXENV=flake8-tests - stage: Formatting Checks - - python: 3.6 - env: TOXENV=pylint-tests - stage: Formatting Checks - - python: 3.6 - env: TOXENV=flake8-examples - stage: Formatting Checks - - python: 3.6 - env: TOXENV=pylint-examples - stage: Formatting Checks - - python: 3.6 - env: TOXENV=black-check - stage: Formatting Checks - - python: 3.6 - env: TOXENV=isort-check - stage: Formatting Checks ######################## # Test Vector Handlers # ######################## @@ -234,93 +144,6 @@ matrix: dist: xenial sudo: true stage: Test Vector Handler Tests - # Linters - - python: 3.6 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=bandit - stage: Test Vector Handler Formatting Checks - - python: 3.6 - env: - TEST_VECTOR_HANDLERS=1 - TOXENV=readme - stage: Test Vector Handler Formatting Checks - # Pending buildout of docs - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=docs - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=doc8 - # Pending linting cleanup - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=flake8 - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=pylint - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=flake8-tests - #- python: 3.6 - # env: - # TEST_VECTOR_HANDLERS=1 - # TOXENV=pylint-tests - ################## - # Decrypt Oracle # - ################## - # CPython 3.6 - # Because this build as Python 3.6 Lambda, this is the only runtime we are targetting. - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=py36-local - stage: Decrypt Oracle Tests - # Linters - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=bandit - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=readme - stage: Decrypt Oracle Formatting Checks - # Pending buildout of docs - #- python: 3.6 - # env: - # DECRYPT_ORACLE=1 - # TOXENV=docs - #- python: 3.6 - # env: - # DECRYPT_ORACLE=1 - # TOXENV=doc8 - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=flake8 - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=pylint - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=flake8-tests - stage: Decrypt Oracle Formatting Checks - - python: 3.6 - env: - DECRYPT_ORACLE=1 - TOXENV=pylint-tests - stage: Decrypt Oracle Formatting Checks install: pip install tox script: - | diff --git a/appveyor.yml b/appveyor.yml index ceb9318bd..d7791f6ae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,92 +7,52 @@ environment: # analysis, etc are only run on Linux (via Travis CI). # Python 2.7 - - PYTHON: "C:\\Python27" - TOXENV: "py27-local" - PYTHON: "C:\\Python27" TOXENV: "py27-integ" - - PYTHON: "C:\\Python27" - TOXENV: "py27-accept" - PYTHON: "C:\\Python27" TOXENV: "py27-examples" - - PYTHON: "C:\\Python27-x64" - TOXENV: "py27-local" - PYTHON: "C:\\Python27-x64" TOXENV: "py27-integ" - - PYTHON: "C:\\Python27-x64" - TOXENV: "py27-accept" - PYTHON: "C:\\Python27-x64" TOXENV: "py27-examples" # Python 3.5 - - PYTHON: "C:\\Python35" - TOXENV: "py35-local" - PYTHON: "C:\\Python35" TOXENV: "py35-integ" - - PYTHON: "C:\\Python35" - TOXENV: "py35-accept" - PYTHON: "C:\\Python35" TOXENV: "py35-examples" - - PYTHON: "C:\\Python35-x64" - TOXENV: "py35-local" - PYTHON: "C:\\Python35-x64" TOXENV: "py35-integ" - - PYTHON: "C:\\Python35-x64" - TOXENV: "py35-accept" - PYTHON: "C:\\Python35-x64" TOXENV: "py35-examples" # Python 3.6 - - PYTHON: "C:\\Python36" - TOXENV: "py36-local" - PYTHON: "C:\\Python36" TOXENV: "py36-integ" - - PYTHON: "C:\\Python36" - TOXENV: "py36-accept" - PYTHON: "C:\\Python36" TOXENV: "py36-examples" - - PYTHON: "C:\\Python36-x64" - TOXENV: "py36-local" - PYTHON: "C:\\Python36-x64" TOXENV: "py36-integ" - - PYTHON: "C:\\Python36-x64" - TOXENV: "py36-accept" - PYTHON: "C:\\Python36-x64" TOXENV: "py36-examples" # Python 3.7 - - PYTHON: "C:\\Python37" - TOXENV: "py37-local" - PYTHON: "C:\\Python37" TOXENV: "py37-integ" - - PYTHON: "C:\\Python37" - TOXENV: "py37-accept" - PYTHON: "C:\\Python37" TOXENV: "py37-examples" - - PYTHON: "C:\\Python37-x64" - TOXENV: "py37-local" - PYTHON: "C:\\Python37-x64" TOXENV: "py37-integ" - - PYTHON: "C:\\Python37-x64" - TOXENV: "py37-accept" - PYTHON: "C:\\Python37-x64" TOXENV: "py37-examples" # Python 3.8 - - PYTHON: "C:\\Python38" - TOXENV: "py38-local" - PYTHON: "C:\\Python38" TOXENV: "py38-integ" - - PYTHON: "C:\\Python38" - TOXENV: "py38-accept" - PYTHON: "C:\\Python38" TOXENV: "py38-examples" - - PYTHON: "C:\\Python38-x64" - TOXENV: "py38-local" - PYTHON: "C:\\Python38-x64" TOXENV: "py38-integ" - - PYTHON: "C:\\Python38-x64" - TOXENV: "py38-accept" - PYTHON: "C:\\Python38-x64" TOXENV: "py38-examples" From 1d9189d1c65c74986897dbb8d7abf25c81136ae1 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Wed, 1 Apr 2020 12:51:59 -0700 Subject: [PATCH 48/64] fix: expose the KMS keyring key namespace value for public access (#234) --- .../keyrings/aws_kms/__init__.py | 18 +++++----- .../keyrings/aws_kms/test_aws_kms.py | 34 +++++++++---------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index b65a5a8d2..006871786 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -30,14 +30,16 @@ # We only actually need these imports when running the mypy checks pass -__all__ = ("KmsKeyring",) +__all__ = ("KmsKeyring", "KEY_NAMESPACE") _LOGGER = logging.getLogger(__name__) -_PROVIDER_ID = "aws-kms" _GENERATE_FLAGS = {KeyringTraceFlag.GENERATED_DATA_KEY} _ENCRYPT_FLAGS = {KeyringTraceFlag.ENCRYPTED_DATA_KEY, KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT} _DECRYPT_FLAGS = {KeyringTraceFlag.DECRYPTED_DATA_KEY, KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT} +#: Key namespace used for all encrypted data keys created by the KMS keyring. +KEY_NAMESPACE = "aws-kms" + @attr.s class KmsKeyring(Keyring): @@ -179,7 +181,7 @@ class _AwsKmsSingleCmkKeyring(Keyring): def on_encrypt(self, encryption_materials): # type: (EncryptionMaterials) -> EncryptionMaterials - trace_info = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=self._key_id) + trace_info = MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=self._key_id) new_materials = encryption_materials try: if new_materials.data_encryption_key is None: @@ -221,7 +223,7 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): return new_materials if ( - edk.key_provider.provider_id == _PROVIDER_ID + edk.key_provider.provider_id == KEY_NAMESPACE and edk.key_provider.key_info.decode("utf-8") == self._key_id ): new_materials = _try_aws_kms_decrypt( @@ -265,7 +267,7 @@ def on_decrypt(self, decryption_materials, encrypted_data_keys): if new_materials.data_encryption_key is not None: return new_materials - if edk.key_provider.provider_id == _PROVIDER_ID: + if edk.key_provider.provider_id == KEY_NAMESPACE: new_materials = _try_aws_kms_decrypt( client_supplier=self._client_supplier, decryption_materials=new_materials, @@ -327,7 +329,7 @@ def _do_aws_kms_decrypt(client_supplier, key_name, encrypted_data_key, encryptio " actual '{actual}' != expected '{expected}'".format(actual=response_key_id, expected=key_name) ) return RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response_key_id), data_key=response["Plaintext"] + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response_key_id), data_key=response["Plaintext"] ) @@ -346,7 +348,7 @@ def _do_aws_kms_encrypt(client_supplier, key_name, plaintext_data_key, encryptio GrantTokens=grant_tokens, ) return EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), encrypted_data_key=response["CiphertextBlob"], ) @@ -368,7 +370,7 @@ def _do_aws_kms_generate_data_key(client_supplier, key_name, encryption_context, EncryptionContext=encryption_context, GrantTokens=grant_tokens, ) - provider = MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]) + provider = MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]) plaintext_key = RawDataKey(key_provider=provider, data_key=response["Plaintext"]) encrypted_key = EncryptedDataKey(key_provider=provider, encrypted_data_key=response["CiphertextBlob"]) return plaintext_key, encrypted_key diff --git a/test/functional/keyrings/aws_kms/test_aws_kms.py b/test/functional/keyrings/aws_kms/test_aws_kms.py index c84174f69..c1af0ed6d 100644 --- a/test/functional/keyrings/aws_kms/test_aws_kms.py +++ b/test/functional/keyrings/aws_kms/test_aws_kms.py @@ -13,7 +13,7 @@ from aws_encryption_sdk.identifiers import KeyringTraceFlag from aws_encryption_sdk.internal.defaults import ALGORITHM from aws_encryption_sdk.keyrings.aws_kms import ( - _PROVIDER_ID, + KEY_NAMESPACE, KmsKeyring, _AwsKmsDiscoveryKeyring, _AwsKmsSingleCmkKeyring, @@ -58,7 +58,7 @@ def test_aws_kms_single_cmk_keyring_on_encrypt_empty_materials(fake_generator): assert len(result_materials.encrypted_data_keys) == 1 generator_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace ) assert KeyringTraceFlag.GENERATED_DATA_KEY in generator_flags @@ -84,7 +84,7 @@ def test_aws_kms_single_cmk_keyring_on_encrypt_existing_data_key(fake_generator) assert len(result_materials.encrypted_data_keys) == 1 generator_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace ) assert KeyringTraceFlag.GENERATED_DATA_KEY not in generator_flags @@ -123,7 +123,7 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_existing_datakey(caplog): decryption_materials=initial_materials, encrypted_data_keys=( EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" ), ), ) @@ -154,7 +154,7 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_single_cmk(fake_generator): assert result_materials.data_encryption_key is not None generator_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace ) assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags @@ -180,12 +180,12 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_multiple_cmk(fake_generator_and_c ) generator_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=generator), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=generator), result_materials.keyring_trace ) assert len(generator_flags) == 0 child_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=child), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=child), result_materials.keyring_trace ) assert KeyringTraceFlag.DECRYPTED_DATA_KEY in child_flags @@ -225,7 +225,7 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_fail(caplog): decryption_materials=initial_materials, encrypted_data_keys=( EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" ), ), ) @@ -275,7 +275,7 @@ def test_aws_kms_discovery_keyring_on_decrypt(encryption_materials_for_discovery assert result_materials.data_encryption_key is not None generator_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=generator_key_id), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=generator_key_id), result_materials.keyring_trace ) assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags @@ -300,7 +300,7 @@ def test_aws_kms_discovery_keyring_on_decrypt_existing_data_key(caplog): decryption_materials=initial_materials, encrypted_data_keys=( EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" ), ), ) @@ -346,7 +346,7 @@ def test_aws_kms_discovery_keyring_on_decrypt_fail(caplog): decryption_materials=initial_materials, encrypted_data_keys=( EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"bar"), encrypted_data_key=b"bar" + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"bar"), encrypted_data_key=b"bar" ), ), ) @@ -365,7 +365,7 @@ def test_try_aws_kms_decrypt_succeed(fake_generator): response = kms.encrypt(KeyId=fake_generator, Plaintext=plaintext, EncryptionContext=encryption_context) encrypted_data_key = EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), encrypted_data_key=response["CiphertextBlob"], ) @@ -381,7 +381,7 @@ def test_try_aws_kms_decrypt_succeed(fake_generator): assert result_materials.data_encryption_key.data_key == plaintext generator_flags = _matching_flags( - MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), result_materials.keyring_trace + MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), result_materials.keyring_trace ) assert KeyringTraceFlag.DECRYPTED_DATA_KEY in generator_flags @@ -394,7 +394,7 @@ def test_try_aws_kms_decrypt_error(caplog): caplog.set_level(logging.DEBUG) encrypted_data_key = EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=b"foo"), encrypted_data_key=b"bar" + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=b"foo"), encrypted_data_key=b"bar" ) initial_decryption_materials = DecryptionMaterials(algorithm=ALGORITHM, encryption_context={},) @@ -420,7 +420,7 @@ def test_do_aws_kms_decrypt(fake_generator): response = kms.encrypt(KeyId=fake_generator, Plaintext=plaintext, EncryptionContext=encryption_context) encrypted_data_key = EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), encrypted_data_key=response["CiphertextBlob"], ) @@ -442,7 +442,7 @@ def test_do_aws_kms_decrypt_unexpected_key_id(fake_generator_and_child): response = kms.encrypt(KeyId=encryptor, Plaintext=plaintext, EncryptionContext=encryption_context) encrypted_data_key = EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=response["KeyId"]), + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=response["KeyId"]), encrypted_data_key=response["CiphertextBlob"], ) @@ -466,7 +466,7 @@ def test_do_aws_kms_encrypt(fake_generator): client_supplier=DefaultClientSupplier(), key_name=fake_generator, plaintext_data_key=RawDataKey( - key_provider=MasterKeyInfo(provider_id=_PROVIDER_ID, key_info=fake_generator), data_key=plaintext + key_provider=MasterKeyInfo(provider_id=KEY_NAMESPACE, key_info=fake_generator), data_key=plaintext ), encryption_context=encryption_context, grant_tokens=[], From cbce22495b13dfda297dfb7903b2753d9a2ed5b1 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Wed, 1 Apr 2020 16:26:51 -0700 Subject: [PATCH 49/64] docs: fix sphinx formatting and syntax (#235) --- src/aws_encryption_sdk/keyrings/aws_kms/__init__.py | 2 +- src/aws_encryption_sdk/keyrings/raw.py | 3 ++- src/aws_encryption_sdk/structures.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 006871786..4d51bd573 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -60,7 +60,7 @@ class KmsKeyring(Keyring): If you specify ``is_discovery=True`` the keyring will be a KMS discovery keyring, doing nothing on encrypt and attempting to decrypt any AWS KMS-encrypted data key on decrypt. - .. notice:: + .. note:: You must either set ``is_discovery=True`` or provide key IDs. diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index dfcc5294b..1e5978d42 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -79,7 +79,8 @@ class RawAESKeyring(Keyring): :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key. .. note:: - Only one wrapping key can be specified in a Raw AES Keyring + + Only one wrapping key can be specified in a Raw AES Keyring """ key_namespace = attr.ib(validator=instance_of(six.string_types)) diff --git a/src/aws_encryption_sdk/structures.py b/src/aws_encryption_sdk/structures.py index ea0af94e9..4e8275a2c 100644 --- a/src/aws_encryption_sdk/structures.py +++ b/src/aws_encryption_sdk/structures.py @@ -31,7 +31,7 @@ class MasterKeyInfo(object): """Contains information necessary to identify a Master Key. - .. notice:: + .. note:: The only keyring or master key that should need to set ``key_name`` is the Raw AES keyring/master key. For all other keyrings and master keys, ``key_info`` and ``key_name`` should always be the same. From cbfe9b0fc9a1048b5779fdf48c677d734d65690b Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 1 Apr 2020 18:51:33 -0700 Subject: [PATCH 50/64] docs: add raw AES MKP example --- examples/src/keyring/raw_aes/raw_aes.py | 2 +- .../master_key_provider/raw_aes/__init__.py | 7 ++ .../master_key_provider/raw_aes/raw_aes.py | 81 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 examples/src/master_key_provider/raw_aes/__init__.py create mode 100644 examples/src/master_key_provider/raw_aes/raw_aes.py diff --git a/examples/src/keyring/raw_aes/raw_aes.py b/examples/src/keyring/raw_aes/raw_aes.py index 58aa697c9..c688141f2 100644 --- a/examples/src/keyring/raw_aes/raw_aes.py +++ b/examples/src/keyring/raw_aes/raw_aes.py @@ -42,7 +42,7 @@ def run(source_plaintext): # Create the keyring that determines how your data keys are protected. keyring = RawAESKeyring( # The key namespace and key name are defined by you - # and are used by the raw RSA keyring + # and are used by the raw AES keyring # to determine whether it should attempt to decrypt # an encrypted data key. # diff --git a/examples/src/master_key_provider/raw_aes/__init__.py b/examples/src/master_key_provider/raw_aes/__init__.py new file mode 100644 index 000000000..5572015a7 --- /dev/null +++ b/examples/src/master_key_provider/raw_aes/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Raw AES master key provider examples. + +These examples show how to use the raw AES master key. +""" diff --git a/examples/src/master_key_provider/raw_aes/raw_aes.py b/examples/src/master_key_provider/raw_aes/raw_aes.py new file mode 100644 index 000000000..035278120 --- /dev/null +++ b/examples/src/master_key_provider/raw_aes/raw_aes.py @@ -0,0 +1,81 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is provided as a reference for users migrating away from master key providers. +We recommend that all new use should use keyrings. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This examples shows how to configure and use a raw AES master key. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import os + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.key_providers.raw import RawMasterKey, WrappingKey + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw AES master key. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Choose the wrapping algorithm for your master key to use. + wrapping_algorithm = WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING + + # Generate an AES key to use with your master key. + # The key size depends on the wrapping algorithm. + # + # In practice, you should get this key from a secure key management system such as an HSM. + key = os.urandom(wrapping_algorithm.algorithm.kdf_input_len) + + # Create the master key that determines how your data keys are protected. + master_key = RawMasterKey( + # The provider ID and key ID are defined by you + # and are used by the raw AES master key + # to determine whether it should attempt to decrypt + # an encrypted data key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my AES wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_algorithm=wrapping_algorithm, wrapping_key_type=EncryptionKeyType.SYMMETRIC, wrapping_key=key, + ), + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same master key you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=master_key) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) From 97c60baf8377486609c12ae32b7d6bd3062e626b Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 1 Apr 2020 18:51:55 -0700 Subject: [PATCH 51/64] docs: add raw RSA MKP example --- .../master_key_provider/raw_rsa/__init__.py | 7 ++ .../raw_rsa/private_key_only_from_pem.py | 104 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 examples/src/master_key_provider/raw_rsa/__init__.py create mode 100644 examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py diff --git a/examples/src/master_key_provider/raw_rsa/__init__.py b/examples/src/master_key_provider/raw_rsa/__init__.py new file mode 100644 index 000000000..374a606fb --- /dev/null +++ b/examples/src/master_key_provider/raw_rsa/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Raw RSA master key provider examples. + +These examples show how to use the raw RSA master key. +""" diff --git a/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py b/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py new file mode 100644 index 000000000..a4c6eaf3d --- /dev/null +++ b/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py @@ -0,0 +1,104 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is provided as a reference for users migrating away from master key providers. +We recommend that all new use should use keyrings. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This example shows how to configure and use a raw RSA master key using a PEM-encoded RSA private key. + +The most commonly used encodings for RSA keys tend to be PEM and DER. +The raw RSA master key supports loading both public and private keys from PEM encoding. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.key_providers.raw import RawMasterKey, WrappingKey + + +def run(source_plaintext): + # type: (bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a raw RSA master key loaded from a PEM-encoded key. + + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your master key. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Serialize the RSA private key to PEM encoding. + # This or DER encoding is likely to be what you get from your key management system in practice. + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Create the master key that determines how your data keys are protected. + # + # WrappingKey can only load PEM-encoded keys. + master_key = RawMasterKey( + # The provider ID and key ID are defined by you + # and are used by the raw RSA master key + # to determine whether it should attempt to decrypt + # an encrypted data key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my RSA wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_key=private_key_pem, + wrapping_key_type=EncryptionKeyType.PRIVATE, + # The wrapping algorithm tells the raw RSA master key + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ), + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same master key you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=master_key) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) From d6b86c79f7a956f8a4adcb9a4558e31bba91a434 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 1 Apr 2020 18:52:15 -0700 Subject: [PATCH 52/64] docs: add KMS MK/P examples --- .../master_key_provider/aws_kms/__init__.py | 7 ++ .../aws_kms/discovery_decrypt.py | 68 ++++++++++++++ .../aws_kms/multiple_regions.py | 91 +++++++++++++++++++ .../master_key_provider/aws_kms/single_cmk.py | 64 +++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 examples/src/master_key_provider/aws_kms/__init__.py create mode 100644 examples/src/master_key_provider/aws_kms/discovery_decrypt.py create mode 100644 examples/src/master_key_provider/aws_kms/multiple_regions.py create mode 100644 examples/src/master_key_provider/aws_kms/single_cmk.py diff --git a/examples/src/master_key_provider/aws_kms/__init__.py b/examples/src/master_key_provider/aws_kms/__init__.py new file mode 100644 index 000000000..5da30d818 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +AWS KMS master key provider examples. + +These examples show how to use the KMS master key provider. +""" diff --git a/examples/src/master_key_provider/aws_kms/discovery_decrypt.py b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py new file mode 100644 index 000000000..77c1a65c6 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py @@ -0,0 +1,68 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is provided as a reference for users migrating away from master key providers. +We recommend that all new use should use keyrings. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +The KMS master key provider uses any key IDs that you specify on encrypt, +but attempts to decrypt *any* data keys that were encrypted under a KMS CMK. +This means that you do not need to know which CMKs were used to encrypt a message. + +This example shows how to configure and use a KMS master key provider to decrypt without provider key IDs. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For an example of how to use the KMS master key provider with CMKs in multiple regions, +see the ``master_key_provider/aws_kms/multiple_regions`` example. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a KMS master key provider for decryption. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the master key that determines how your data keys are protected. + encrypt_master_key = KMSMasterKey(key_id=aws_kms_cmk) + + # Create a KMS master key provider to use on decrypt. + decrypt_master_key_provider = KMSMasterKeyProvider() + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=encrypt_master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the KMS master key provider. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=decrypt_master_key_provider) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/master_key_provider/aws_kms/multiple_regions.py b/examples/src/master_key_provider/aws_kms/multiple_regions.py new file mode 100644 index 000000000..476975c57 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/multiple_regions.py @@ -0,0 +1,91 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is provided as a reference for users migrating away from master key providers. +We recommend that all new use should use keyrings. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This example shows how to configure and use a KMS master key provider with with CMKs in multiple regions. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For an example of how to use the KMS master key with a single CMK, +see the ``master_key_provider/aws_kms/single_cmk`` example. + +For examples of how to use the KMS master key provider in discovery mode on decrypt, +see the ``master_key_provider/aws_kms/discovery_decrypt``. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Sequence # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext): + # type: (str, Sequence[str], bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS master key provider with CMKs in multiple regions. + + :param str aws_kms_generator_cmk: The ARN of the primary AWS KMS CMK + :param List[str] aws_kms_additional_cmks: Additional ARNs of secondary KMS CMKs + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the master key provider that will encrypt your data keys under all requested CMKs. + # + # The KMS master key provider generates the data key using the first key ID in the list. + key_ids = [aws_kms_generator_cmk] + key_ids.extend(aws_kms_additional_cmks) + master_key_provider = KMSMasterKeyProvider(key_ids=key_ids) + + # Create master keys that each only use one of the CMKs. + # We will use these later to demonstrate that any of the CMKs can be used to decrypt the message. + single_cmk_master_key_that_generated = KMSMasterKey(key_id=aws_kms_generator_cmk) + single_cmk_master_key_that_encrypted = KMSMasterKey(key_id=aws_kms_additional_cmks[0]) + + # Encrypt your plaintext data using the master key provider that uses all requests CMKs. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key_provider + ) + + # Verify that the header contains the expected number of encrypted data keys (EDKs). + # It should contain one EDK for each CMK. + assert len(encrypt_header.encrypted_data_keys) == len(aws_kms_additional_cmks) + 1 + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data separately using the single-CMK master keys. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted_1, decrypt_header_1 = aws_encryption_sdk.decrypt( + source=ciphertext, key_provider=single_cmk_master_key_that_generated + ) + decrypted_2, decrypt_header_2 = aws_encryption_sdk.decrypt( + source=ciphertext, key_provider=single_cmk_master_key_that_encrypted + ) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted_1 == source_plaintext + assert decrypted_2 == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header_1.encryption_context.items()) + assert set(encryption_context.items()) <= set(decrypt_header_2.encryption_context.items()) diff --git a/examples/src/master_key_provider/aws_kms/single_cmk.py b/examples/src/master_key_provider/aws_kms/single_cmk.py new file mode 100644 index 000000000..4ddfd69c1 --- /dev/null +++ b/examples/src/master_key_provider/aws_kms/single_cmk.py @@ -0,0 +1,64 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is provided as a reference for users migrating away from master key providers. +We recommend that all new use should use keyrings. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +This example shows how to configure and use a KMS master key with a single KMS CMK. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For an example of how to use the KMS master key provider with CMKs in multiple regions, +see the ``master_key_provider/aws_kms/multiple_regions`` example. + +For examples of how to use the KMS master key provider in discovery mode on decrypt, +see the ``master_key_provider/aws_kms/discovery_decrypt``. +""" +import aws_encryption_sdk +from aws_encryption_sdk.key_providers.kms import KMSMasterKey + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a KMS master key with a single CMK. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the master key that determines how your data keys are protected. + master_key = KMSMasterKey(key_id=aws_kms_cmk) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=master_key + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same master key you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=master_key) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) From 1a01d325bd9e9dddeae691d2bf3b81099710a0ec Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Wed, 1 Apr 2020 18:52:30 -0700 Subject: [PATCH 53/64] docs: add MKP combination example --- examples/src/master_key_provider/__init__.py | 7 + .../src/master_key_provider/multi/__init__.py | 7 + .../multi/aws_kms_with_escrow.py | 150 ++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 examples/src/master_key_provider/__init__.py create mode 100644 examples/src/master_key_provider/multi/__init__.py create mode 100644 examples/src/master_key_provider/multi/aws_kms_with_escrow.py diff --git a/examples/src/master_key_provider/__init__.py b/examples/src/master_key_provider/__init__.py new file mode 100644 index 000000000..0edf699c5 --- /dev/null +++ b/examples/src/master_key_provider/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Master key provider examples. + +These examples show how to use master key providers. +""" diff --git a/examples/src/master_key_provider/multi/__init__.py b/examples/src/master_key_provider/multi/__init__.py new file mode 100644 index 000000000..22f5195fc --- /dev/null +++ b/examples/src/master_key_provider/multi/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Multi-master key provider examples. + +These examples show how to combine master key providers. +""" diff --git a/examples/src/master_key_provider/multi/aws_kms_with_escrow.py b/examples/src/master_key_provider/multi/aws_kms_with_escrow.py new file mode 100644 index 000000000..73fb626d8 --- /dev/null +++ b/examples/src/master_key_provider/multi/aws_kms_with_escrow.py @@ -0,0 +1,150 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +This example is provided as a reference for users migrating away from master key providers. +We recommend that all new use should use keyrings. +For examples using keyrings, see the ``examples/src/keyrings`` directory. + +One use-case that we have seen customers need is +the ability to enjoy the benefits of AWS KMS during normal operation +but retain the ability to decrypt encrypted messages without access to AWS KMS. +This example shows how you can achieve this +by combining a KMS master key with a raw RSA master key. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider + +For more examples of how to use the KMS master key provider, see the ``master_key_provider/aws_kms`` examples. + +For more examples of how to use the raw RSA master key, see the ``master_key_provider/raw_rsa`` examples. + +In this example we generate a RSA keypair +but in practice you would want to keep your private key in an HSM +or other key management system. + +In this example, we use the one-step encrypt and decrypt APIs. +""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.key_providers.kms import KMSMasterKeyProvider +from aws_encryption_sdk.key_providers.raw import RawMasterKey, WrappingKey + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate configuring a master key provider to use an AWS KMS CMK and a RSA wrapping key. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Generate an RSA private key to use with your master key. + # In practice, you should get this key from a secure key management system such as an HSM. + # + # The National Institute of Standards and Technology (NIST) recommends a minimum of 2048-bit keys for RSA. + # https://www.nist.gov/publications/transitioning-use-cryptographic-algorithms-and-key-lengths + # + # Why did we use this public exponent? + # https://crypto.stanford.edu/~dabo/pubs/papers/RSA-survey.pdf + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend()) + + # Serialize the RSA private key to PEM encoding. + # This or DER encoding is likely to be what you get from your key management system in practice. + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Collect the public key from the private key. + public_key = private_key.public_key() + + # Serialize the RSA public key to PEM encoding. + # This or DER encoding is likely to be what you get from your key management system in practice. + public_key_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + # Create the encrypt master key that only has access to the public key. + escrow_encrypt_master_key = RawMasterKey( + # The provider ID and key ID are defined by you + # and are used by the raw RSA master key + # to determine whether it should attempt to decrypt + # an encrypted data key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my RSA wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_key=public_key_pem, + wrapping_key_type=EncryptionKeyType.PUBLIC, + # The wrapping algorithm tells the raw RSA master key + # how to use your wrapping key to encrypt data keys. + # + # We recommend using RSA_OAEP_SHA256_MGF1. + # You should not use RSA_PKCS1 unless you require it for backwards compatibility. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ), + ) + + # Create the decrypt master key that has access to the private key. + escrow_decrypt_master_key = RawMasterKey( + # The key namespace and key name MUST match the encrypt master key. + provider_id="some managed raw keys", # provider ID corresponds to key namespace for keyrings + key_id=b"my RSA wrapping key", # key ID corresponds to key name for keyrings + wrapping_key=WrappingKey( + wrapping_key=private_key_pem, + wrapping_key_type=EncryptionKeyType.PRIVATE, + # The wrapping algorithm MUST match the encrypt master key. + wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + ), + ) + + # Create the KMS master key that you will use for decryption during normal operations. + kms_master_key = KMSMasterKeyProvider(key_ids=[aws_kms_cmk]) + + # Add the escrow encrypt master key to the KMS master key. + kms_master_key.add_master_key_provider(escrow_encrypt_master_key) + + # Encrypt your plaintext data using the combined master keys. + ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, key_provider=kms_master_key + ) + + # Verify that the header contains the expected number of encrypted data keys (EDKs). + # It should contain one EDK for KMS and one for the escrow key. + assert len(encrypt_header.encrypted_data_keys) == 2 + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data separately using the KMS master key and the escrow decrypt master key. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted_kms, decrypt_header_kms = aws_encryption_sdk.decrypt(source=ciphertext, key_provider=kms_master_key) + decrypted_escrow, decrypt_header_escrow = aws_encryption_sdk.decrypt( + source=ciphertext, key_provider=escrow_decrypt_master_key + ) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted_kms == source_plaintext + assert decrypted_escrow == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header_kms.encryption_context.items()) + assert set(encryption_context.items()) <= set(decrypt_header_escrow.encryption_context.items()) From 8fb27416414e0e006a98c48a8c414a215fde941a Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Thu, 2 Apr 2020 11:21:21 -0700 Subject: [PATCH 54/64] docs: apply suggestions from code review Co-Authored-By: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> --- examples/src/master_key_provider/aws_kms/discovery_decrypt.py | 3 +++ examples/src/master_key_provider/aws_kms/multiple_regions.py | 4 ++-- examples/src/master_key_provider/aws_kms/single_cmk.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/src/master_key_provider/aws_kms/discovery_decrypt.py b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py index 77c1a65c6..9ffc291e7 100644 --- a/examples/src/master_key_provider/aws_kms/discovery_decrypt.py +++ b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py @@ -13,6 +13,9 @@ https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#master-key-provider +For an example of how to use the KMS master key with a single CMK, +see the ``master_key_provider/aws_kms/single_cmk`` example. + For an example of how to use the KMS master key provider with CMKs in multiple regions, see the ``master_key_provider/aws_kms/multiple_regions`` example. """ diff --git a/examples/src/master_key_provider/aws_kms/multiple_regions.py b/examples/src/master_key_provider/aws_kms/multiple_regions.py index 476975c57..ae9b34b77 100644 --- a/examples/src/master_key_provider/aws_kms/multiple_regions.py +++ b/examples/src/master_key_provider/aws_kms/multiple_regions.py @@ -12,8 +12,8 @@ For an example of how to use the KMS master key with a single CMK, see the ``master_key_provider/aws_kms/single_cmk`` example. -For examples of how to use the KMS master key provider in discovery mode on decrypt, -see the ``master_key_provider/aws_kms/discovery_decrypt``. +For an example of how to use the KMS master key provider in discovery mode on decrypt, +see the ``master_key_provider/aws_kms/discovery_decrypt`` example. """ import aws_encryption_sdk from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyProvider diff --git a/examples/src/master_key_provider/aws_kms/single_cmk.py b/examples/src/master_key_provider/aws_kms/single_cmk.py index 4ddfd69c1..65d4a2ae7 100644 --- a/examples/src/master_key_provider/aws_kms/single_cmk.py +++ b/examples/src/master_key_provider/aws_kms/single_cmk.py @@ -12,8 +12,8 @@ For an example of how to use the KMS master key provider with CMKs in multiple regions, see the ``master_key_provider/aws_kms/multiple_regions`` example. -For examples of how to use the KMS master key provider in discovery mode on decrypt, -see the ``master_key_provider/aws_kms/discovery_decrypt``. +For an example of how to use the KMS master key provider in discovery mode on decrypt, +see the ``master_key_provider/aws_kms/discovery_decrypt`` example. """ import aws_encryption_sdk from aws_encryption_sdk.key_providers.kms import KMSMasterKey From 0de7cdcc52088a543a1604a77e1e850dba7b4037 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 2 Apr 2020 18:38:28 -0700 Subject: [PATCH 55/64] docs: revise MKP examples intro --- examples/src/master_key_provider/aws_kms/discovery_decrypt.py | 4 ++-- examples/src/master_key_provider/aws_kms/multiple_regions.py | 4 ++-- examples/src/master_key_provider/aws_kms/single_cmk.py | 4 ++-- examples/src/master_key_provider/multi/aws_kms_with_escrow.py | 4 ++-- examples/src/master_key_provider/raw_aes/raw_aes.py | 4 ++-- .../master_key_provider/raw_rsa/private_key_only_from_pem.py | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/src/master_key_provider/aws_kms/discovery_decrypt.py b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py index 9ffc291e7..1b7e09452 100644 --- a/examples/src/master_key_provider/aws_kms/discovery_decrypt.py +++ b/examples/src/master_key_provider/aws_kms/discovery_decrypt.py @@ -1,8 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -This example is provided as a reference for users migrating away from master key providers. -We recommend that all new use should use keyrings. +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. For examples using keyrings, see the ``examples/src/keyrings`` directory. The KMS master key provider uses any key IDs that you specify on encrypt, diff --git a/examples/src/master_key_provider/aws_kms/multiple_regions.py b/examples/src/master_key_provider/aws_kms/multiple_regions.py index ae9b34b77..81fee9ec5 100644 --- a/examples/src/master_key_provider/aws_kms/multiple_regions.py +++ b/examples/src/master_key_provider/aws_kms/multiple_regions.py @@ -1,8 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -This example is provided as a reference for users migrating away from master key providers. -We recommend that all new use should use keyrings. +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. For examples using keyrings, see the ``examples/src/keyrings`` directory. This example shows how to configure and use a KMS master key provider with with CMKs in multiple regions. diff --git a/examples/src/master_key_provider/aws_kms/single_cmk.py b/examples/src/master_key_provider/aws_kms/single_cmk.py index 65d4a2ae7..5a23493d6 100644 --- a/examples/src/master_key_provider/aws_kms/single_cmk.py +++ b/examples/src/master_key_provider/aws_kms/single_cmk.py @@ -1,8 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -This example is provided as a reference for users migrating away from master key providers. -We recommend that all new use should use keyrings. +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. For examples using keyrings, see the ``examples/src/keyrings`` directory. This example shows how to configure and use a KMS master key with a single KMS CMK. diff --git a/examples/src/master_key_provider/multi/aws_kms_with_escrow.py b/examples/src/master_key_provider/multi/aws_kms_with_escrow.py index 73fb626d8..a9c4a2130 100644 --- a/examples/src/master_key_provider/multi/aws_kms_with_escrow.py +++ b/examples/src/master_key_provider/multi/aws_kms_with_escrow.py @@ -1,8 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -This example is provided as a reference for users migrating away from master key providers. -We recommend that all new use should use keyrings. +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. For examples using keyrings, see the ``examples/src/keyrings`` directory. One use-case that we have seen customers need is diff --git a/examples/src/master_key_provider/raw_aes/raw_aes.py b/examples/src/master_key_provider/raw_aes/raw_aes.py index 035278120..943fcb67b 100644 --- a/examples/src/master_key_provider/raw_aes/raw_aes.py +++ b/examples/src/master_key_provider/raw_aes/raw_aes.py @@ -1,8 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -This example is provided as a reference for users migrating away from master key providers. -We recommend that all new use should use keyrings. +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. For examples using keyrings, see the ``examples/src/keyrings`` directory. This examples shows how to configure and use a raw AES master key. diff --git a/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py b/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py index a4c6eaf3d..5ad78c00b 100644 --- a/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py +++ b/examples/src/master_key_provider/raw_rsa/private_key_only_from_pem.py @@ -1,8 +1,8 @@ # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """ -This example is provided as a reference for users migrating away from master key providers. -We recommend that all new use should use keyrings. +This example is intended to serve as reference material for users migrating away from master key providers. +We recommend using keyrings rather than master key providers. For examples using keyrings, see the ``examples/src/keyrings`` directory. This example shows how to configure and use a raw RSA master key using a PEM-encoded RSA private key. From c5d3a94d79979e46e4df183cb5cdc9f5c62f6886 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Thu, 2 Apr 2020 18:40:11 -0700 Subject: [PATCH 56/64] docs: add MKP examples to readme map --- examples/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/README.md b/examples/README.md index 592872232..c4c0e67ec 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,10 +33,13 @@ We start with AWS KMS examples, then show how to use other wrapping keys. * Using AWS Key Management Service (AWS KMS) * How to use one AWS KMS CMK * [with keyrings](./src/keyring/aws_kms/single_cmk.py) + * [with master key providers](./src/master_key_provider/aws_kms/single_cmk.py) * How to use multiple AWS KMS CMKs in different regions * [with keyrings](./src/keyring/aws_kms/multiple_regions.py) + * [with master key providers](./src/master_key_provider/aws_kms/multiple_regions.py) * How to decrypt when you don't know the CMK * [with keyrings](./src/keyring/aws_kms/discovery_decrypt.py) + * [with master key providers](./src/master_key_provider/aws_kms/discovery_decrypt.py) * How to decrypt within a region * [with keyrings](./src/keyring/aws_kms/discovery_decrypt_in_region_only.py) * How to decrypt with a preferred region but failover to others @@ -44,15 +47,18 @@ We start with AWS KMS examples, then show how to use other wrapping keys. * Using raw wrapping keys * How to use a raw AES wrapping key * [with keyrings](./src/keyring/raw_aes/raw_aes.py) + * [with master key providers](./src/master_key_provider/raw_aes/raw_aes.py) * How to use a raw RSA wrapping key * [with keyrings](./src/keyring/raw_rsa/private_key_only.py) * How to use a raw RSA wrapping key when the key is PEM or DER encoded * [with keyrings](./src/keyring/raw_rsa/private_key_only_from_pem.py) + * [with master key providers](./src/master_key_provider/raw_rsa/private_key_only_from_pem.py) * How to encrypt with a raw RSA public key wrapping key without access to the private key * [with keyrings](./src/keyring/raw_rsa/public_private_key_separate.py) * Combining wrapping keys * How to combine AWS KMS with an offline escrow key * [with keyrings](./src/keyring/multi/aws_kms_with_escrow.py) + * [with master key providers](./src/master_key_provider/multi/aws_kms_with_escrow.py) ### Keyrings From 3d98f47eedd58fbde57dad0d8cf465a421545691 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Tue, 7 Apr 2020 17:24:16 -0700 Subject: [PATCH 57/64] fix: correct KMS keyring parameter name from child_key_ids to key_ids (#240) * fix: correct KMS keyring parameter name from child_key_ids to additional_key_ids * feat: rename kms keyring parameter from additional_key_ids to key_ids --- examples/src/keyring/aws_kms/multiple_regions.py | 8 ++++---- src/aws_encryption_sdk/keyrings/aws_kms/__init__.py | 12 ++++++------ test/functional/keyrings/aws_kms/test_aws_kms.py | 2 +- test/integration/integration_test_utils.py | 2 +- test/unit/keyrings/test_aws_kms.py | 10 +++++----- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/src/keyring/aws_kms/multiple_regions.py b/examples/src/keyring/aws_kms/multiple_regions.py index f466796c3..b90d8ccc3 100644 --- a/examples/src/keyring/aws_kms/multiple_regions.py +++ b/examples/src/keyring/aws_kms/multiple_regions.py @@ -46,16 +46,16 @@ def run(aws_kms_generator_cmk, aws_kms_additional_cmks, source_plaintext): } # Create the keyring that will encrypt your data keys under all requested CMKs. - many_cmks_keyring = KmsKeyring(generator_key_id=aws_kms_generator_cmk, child_key_ids=aws_kms_additional_cmks) + many_cmks_keyring = KmsKeyring(generator_key_id=aws_kms_generator_cmk, key_ids=aws_kms_additional_cmks) # Create keyrings that each only use one of the CMKs. # We will use these later to demonstrate that any of the CMKs can be used to decrypt the message. # - # We provide these in "child_key_ids" rather than "generator_key_id" + # We provide these in "key_ids" rather than "generator_key_id" # so that these keyrings cannot be used to generate a new data key. # We will only be using them on decrypt. - single_cmk_keyring_that_generated = KmsKeyring(child_key_ids=[aws_kms_generator_cmk]) - single_cmk_keyring_that_encrypted = KmsKeyring(child_key_ids=[aws_kms_additional_cmks[0]]) + single_cmk_keyring_that_generated = KmsKeyring(key_ids=[aws_kms_generator_cmk]) + single_cmk_keyring_that_encrypted = KmsKeyring(key_ids=[aws_kms_additional_cmks[0]]) # Encrypt your plaintext data using the keyring that uses all requests CMKs. ciphertext, encrypt_header = aws_encryption_sdk.encrypt( diff --git a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py index 4d51bd573..678ac722b 100644 --- a/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py +++ b/src/aws_encryption_sdk/keyrings/aws_kms/__init__.py @@ -48,10 +48,10 @@ class KmsKeyring(Keyring): Set ``generator_key_id`` to require that the keyring use that CMK to generate the data key. If you do not set ``generator_key_id``, the keyring will not generate a data key. - Set ``child_key_ids`` to specify additional CMKs that the keyring will use to encrypt the data key. + Set ``key_ids`` to specify additional CMKs that the keyring will use to encrypt the data key. The keyring will attempt to use any CMKs - identified by CMK ARN in either ``generator_key_id`` or ``child_key_ids`` on decrypt. + identified by CMK ARN in either ``generator_key_id`` or ``key_ids`` on decrypt. You can identify CMKs by any `valid key ID`_ for the keyring to use on encrypt, but for the keyring to attempt to use them on decrypt @@ -82,14 +82,14 @@ class KmsKeyring(Keyring): :param ClientSupplier client_supplier: Client supplier that provides AWS KMS clients (optional) :param bool is_discovery: Should this be a discovery keyring (optional) :param str generator_key_id: Key ID of AWS KMS CMK to use when generating data keys (optional) - :param List[str] child_key_ids: Key IDs that will be used to encrypt and decrypt data keys (optional) + :param List[str] key_ids: Key IDs that will be used to encrypt and decrypt data keys (optional) :param List[str] grant_tokens: AWS KMS grant tokens to include in requests (optional) """ _client_supplier = attr.ib(default=attr.Factory(DefaultClientSupplier), validator=is_callable()) _is_discovery = attr.ib(default=False, validator=instance_of(bool)) _generator_key_id = attr.ib(default=None, validator=optional(instance_of(six.string_types))) - _child_key_ids = attr.ib( + _key_ids = attr.ib( default=attr.Factory(tuple), validator=(deep_iterable(member_validator=instance_of(six.string_types)), value_is_not_a_string), ) @@ -100,7 +100,7 @@ class KmsKeyring(Keyring): def __attrs_post_init__(self): """Configure internal keyring.""" - key_ids_provided = self._generator_key_id is not None or self._child_key_ids + key_ids_provided = self._generator_key_id is not None or self._key_ids both = key_ids_provided and self._is_discovery neither = not key_ids_provided and not self._is_discovery @@ -127,7 +127,7 @@ def __attrs_post_init__(self): _AwsKmsSingleCmkKeyring( key_id=key_id, client_supplier=self._client_supplier, grant_tokens=self._grant_tokens ) - for key_id in self._child_key_ids + for key_id in self._key_ids ] self._inner_keyring = MultiKeyring(generator=generator_keyring, children=child_keyrings) diff --git a/test/functional/keyrings/aws_kms/test_aws_kms.py b/test/functional/keyrings/aws_kms/test_aws_kms.py index c1af0ed6d..5a5a82bd9 100644 --- a/test/functional/keyrings/aws_kms/test_aws_kms.py +++ b/test/functional/keyrings/aws_kms/test_aws_kms.py @@ -164,7 +164,7 @@ def test_aws_kms_single_cmk_keyring_on_decrypt_single_cmk(fake_generator): def test_aws_kms_single_cmk_keyring_on_decrypt_multiple_cmk(fake_generator_and_child): generator, child = fake_generator_and_child - encrypting_keyring = KmsKeyring(generator_key_id=generator, child_key_ids=(child,)) + encrypting_keyring = KmsKeyring(generator_key_id=generator, key_ids=(child,)) decrypting_keyring = _AwsKmsSingleCmkKeyring(key_id=child, client_supplier=DefaultClientSupplier()) initial_encryption_materials = EncryptionMaterials(algorithm=ALGORITHM, encryption_context={}) diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index 4928f3637..3407b4f1c 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -92,7 +92,7 @@ def build_aws_kms_keyring(generate=True, cache=True): if generate: kwargs = dict(generator_key_id=cmk_arn) else: - kwargs = dict(child_key_ids=[cmk_arn]) + kwargs = dict(key_ids=[cmk_arn]) keyring = KmsKeyring(**kwargs) diff --git a/test/unit/keyrings/test_aws_kms.py b/test/unit/keyrings/test_aws_kms.py index 113ffc98f..78f84635e 100644 --- a/test/unit/keyrings/test_aws_kms.py +++ b/test/unit/keyrings/test_aws_kms.py @@ -20,12 +20,12 @@ ( pytest.param(dict(client_supplier=None), id="client_supplier is invalid"), pytest.param(dict(generator_key_id=5), id="generator_id is invalid"), - pytest.param(dict(child_key_ids=("foo", 5)), id="child_key_ids contains invalid values"), - pytest.param(dict(child_key_ids="some stuff"), id="child_key_ids is a string"), + pytest.param(dict(key_ids=("foo", 5)), id="key_ids contains invalid values"), + pytest.param(dict(key_ids="some stuff"), id="key_ids is a string"), pytest.param(dict(grant_tokens=("foo", 5)), id="grant_tokens contains invalid values"), pytest.param(dict(grant_tokens="some stuff"), id="grant_tokens is a string"), pytest.param(dict(generator_key_id="foo", is_discovery=True), id="generator and discovery"), - pytest.param(dict(child_key_ids=("foo",), is_discovery=True), id="child_key_ids and discovery"), + pytest.param(dict(key_ids=("foo",), is_discovery=True), id="key_ids and discovery"), pytest.param(dict(), id="nothing"), ), ) @@ -43,7 +43,7 @@ def test_kms_keyring_builds_correct_inner_keyring_multikeyring(): test = KmsKeyring( generator_key_id=generator_id, - child_key_ids=(child_id_1, child_id_2), + key_ids=(child_id_1, child_id_2), grant_tokens=grants, client_supplier=supplier, ) @@ -74,7 +74,7 @@ def test_kms_keyring_builds_correct_inner_keyring_multikeyring(): def test_kms_keyring_builds_correct_inner_keyring_multikeyring_no_generator(): - test = KmsKeyring(child_key_ids=("bar", "baz")) + test = KmsKeyring(key_ids=("bar", "baz")) # We specified child IDs, so the inner keyring MUST be a multikeyring assert isinstance(test._inner_keyring, MultiKeyring) From 5ac45bf9c1cfc3f3eeb1eb386085edede5d2e09c Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Wed, 8 Apr 2020 16:03:44 -0700 Subject: [PATCH 58/64] chore: disable the test vector handler tests in GitHub Actions (#243) --- .github/workflows/ci_test-vector-handler.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_test-vector-handler.yaml b/.github/workflows/ci_test-vector-handler.yaml index f2efa30ce..570133231 100644 --- a/.github/workflows/ci_test-vector-handler.yaml +++ b/.github/workflows/ci_test-vector-handler.yaml @@ -9,9 +9,9 @@ on: jobs: tests: - # Until we address the credentials problem, - # do not run for pull requests. - if: github.event != 'pull_request' + # Leaving this defined but disabled + # until we address the credentials problem. + if: 1 == 0 runs-on: ${{ matrix.os }} strategy: fail-fast: true From cd2a17183d70f3292c64dfd201cb6cc2e96c341a Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Thu, 9 Apr 2020 16:25:40 -0700 Subject: [PATCH 59/64] docs: add CMM examples (#239) * docs: add CMM examples * docs: add CMM examples to readme * docs: apply suggestions from code review Co-Authored-By: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> * docs: fix and refine examples docs * chore: remove redundant example * docs: clarify "filtering CMM" references in examples comments * chore: autoformat * docs: refactor algorithm restricting example focus * docs: revise framing of encryption context example and KMS references * docs: apply suggestions from code review Co-Authored-By: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> Co-authored-by: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> --- examples/README.md | 6 + .../src/crypto_materials_manager/__init__.py | 7 + .../caching/__init__.py | 7 + .../caching/simple_cache.py | 94 +++++++++++ .../custom/__init__.py | 10 ++ .../custom/algorithm_suite_enforcement.py | 138 ++++++++++++++++ .../requiring_encryption_context_fields.py | 153 ++++++++++++++++++ test/unit/keyrings/test_aws_kms.py | 5 +- 8 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 examples/src/crypto_materials_manager/__init__.py create mode 100644 examples/src/crypto_materials_manager/caching/__init__.py create mode 100644 examples/src/crypto_materials_manager/caching/simple_cache.py create mode 100644 examples/src/crypto_materials_manager/custom/__init__.py create mode 100644 examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py create mode 100644 examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py diff --git a/examples/README.md b/examples/README.md index c4c0e67ec..081e62fab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -59,6 +59,12 @@ We start with AWS KMS examples, then show how to use other wrapping keys. * How to combine AWS KMS with an offline escrow key * [with keyrings](./src/keyring/multi/aws_kms_with_escrow.py) * [with master key providers](./src/master_key_provider/multi/aws_kms_with_escrow.py) +* How to reuse data keys across multiple messages + * [with the caching cryptographic materials manager](./src/crypto_materials_manager/caching/simple_cache.py) +* How to restrict algorithm suites + * [with a custom cryptographic materials manager](src/crypto_materials_manager/custom/algorithm_suite_enforcement.py) +* How to require encryption context fields + * [with a custom cryptographic materials manager](src/crypto_materials_manager/custom/requiring_encryption_context_fields.py) ### Keyrings diff --git a/examples/src/crypto_materials_manager/__init__.py b/examples/src/crypto_materials_manager/__init__.py new file mode 100644 index 000000000..f413e63bd --- /dev/null +++ b/examples/src/crypto_materials_manager/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Cryptographic materials manager examples. + +These examples show how to create and use cryptographic materials managers. +""" diff --git a/examples/src/crypto_materials_manager/caching/__init__.py b/examples/src/crypto_materials_manager/caching/__init__.py new file mode 100644 index 000000000..2c55faad8 --- /dev/null +++ b/examples/src/crypto_materials_manager/caching/__init__.py @@ -0,0 +1,7 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Caching cryptographic materials manager examples. + +These examples show how to configure and use the caching cryptographic materials manager. +""" diff --git a/examples/src/crypto_materials_manager/caching/simple_cache.py b/examples/src/crypto_materials_manager/caching/simple_cache.py new file mode 100644 index 000000000..bb14b25fe --- /dev/null +++ b/examples/src/crypto_materials_manager/caching/simple_cache.py @@ -0,0 +1,94 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +The default cryptographic materials manager (CMM) +creates new encryption and decryption materials +on every call. +This means every encrypted message is protected by a unique data key, +but it also means that you need to interact with your key management system +in order to process any message. +If this causes performance, operations, or cost issues for you, +you might benefit from data key caching. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/data-key-caching.html + +This example shows how to configure the caching CMM +to reuse data keys across multiple encrypted messages. + +In this example, we use an AWS KMS customer master key (CMK), +but you can use other key management options with the AWS Encryption SDK. +For examples that demonstrate how to use other key management configurations, +see the ``keyring`` and ``master_key_provider`` directories. + +In this example, we use the one-step encrypt and decrypt APIs. +""" +import aws_encryption_sdk +from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.materials_managers.caching import CachingCryptoMaterialsManager + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using the caching cryptographic materials manager. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create the caching cryptographic materials manager using your keyring. + cmm = CachingCryptoMaterialsManager( + keyring=keyring, + # The cache is where the caching CMM stores the materials. + # + # LocalCryptoMaterialsCache gives you a local, in-memory, cache. + cache=LocalCryptoMaterialsCache(capacity=100), + # max_age determines how long the caching CMM will reuse materials. + # + # This example uses two minutes. + # In production, always choose as small a value as possible + # that works for your requirements. + max_age=120.0, + # max_messages_encrypted determines how many messages + # the caching CMM will protect with the same materials. + # + # In production, always choose as small a value as possible + # that works for your requirements. + max_messages_encrypted=10, + ) + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, materials_manager=cmm) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/crypto_materials_manager/custom/__init__.py b/examples/src/crypto_materials_manager/custom/__init__.py new file mode 100644 index 000000000..202647480 --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/__init__.py @@ -0,0 +1,10 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Custom cryptographic materials manager (CMM) examples. + +The AWS Encryption SDK includes CMMs for common use cases, +but you might need to do something else. + +These examples show how you could create your own CMM for some specific requirements. +""" diff --git a/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py b/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py new file mode 100644 index 000000000..d7385fc76 --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/algorithm_suite_enforcement.py @@ -0,0 +1,138 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +The AWS Encryption SDK supports several different algorithm suites +that offer different security properties. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html + +By default, the AWS Encryption SDK will let you use any of these, +but you might want to restrict that further. + +We recommend that you use the default algorithm suite, +which uses AES-GCM with 256-bit keys, HKDF, and ECDSA message signing. +If your readers and writers have the same permissions, +you might want to omit the message signature for faster operation. +For more information about choosing a signed or unsigned algorithm suite, +see the AWS Encryption SDK developer guide: + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/supported-algorithms.html#other-algorithms + +This example shows how you can make a custom cryptographic materials manager (CMM) +that only allows encrypt requests that either specify one of these two algorithm suites +or do not specify an algorithm suite, in which case the default CMM uses the default algorithm suite. +""" +import aws_encryption_sdk +from aws_encryption_sdk.identifiers import AlgorithmSuite +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + + +class UnapprovedAlgorithmSuite(Exception): + """Indicate that an unsupported algorithm suite was requested.""" + + +class RequireApprovedAlgorithmSuitesCryptoMaterialsManager(CryptoMaterialsManager): + """Only allow encryption requests for approved algorithm suites.""" + + def __init__(self, keyring): + # type: (Keyring) -> None + """Set up the inner cryptographic materials manager using the provided keyring. + + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager + """ + self._allowed_algorithm_suites = { + None, # no algorithm suite in the request + AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384, # the default algorithm suite + AlgorithmSuite.AES_256_GCM_IV12_TAG16_HKDF_SHA256, # the recommended unsigned algorithm suite + } + # Wrap the provided keyring in the default cryptographic materials manager (CMM). + # + # This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + # do if you provide a keyring instead of a CMM. + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials + """Block any requests that include an unapproved algorithm suite.""" + if request.algorithm not in self._allowed_algorithm_suites: + raise UnapprovedAlgorithmSuite("Unapproved algorithm suite requested!") + + return self._cmm.get_encryption_materials(request) + + def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials + """Be more permissive on decrypt and just pass through.""" + return self._cmm.decrypt_materials(request) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create the algorithm suite restricting cryptographic materials manager using your keyring. + cmm = RequireApprovedAlgorithmSuitesCryptoMaterialsManager(keyring=keyring) + + # Demonstrate that the algorithm suite restricting CMM will not let you use an unapproved algorithm suite. + try: + aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=encryption_context, + materials_manager=cmm, + algorithm=AlgorithmSuite.AES_256_GCM_IV12_TAG16, + ) + except UnapprovedAlgorithmSuite: + # You asked for an unapproved algorithm suite. + # Reaching this point means everything is working as expected. + pass + else: + # The algorithm suite restricting CMM keeps this from happening. + raise AssertionError("The algorithm suite restricting CMM does not let this happen!") + + # Encrypt your plaintext data. + ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=ciphertext, materials_manager=cmm) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) diff --git a/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py b/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py new file mode 100644 index 000000000..c797849bb --- /dev/null +++ b/examples/src/crypto_materials_manager/custom/requiring_encryption_context_fields.py @@ -0,0 +1,153 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +""" +Encryption context is a powerful tool for access and audit controls +because it lets you tie *non-secret* metadata about a plaintext value to the encrypted message. +Within the AWS Encryption SDK, +you can use cryptographic materials managers to analyse the encryption context +to provide logical controls and additional metadata. + +https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + +If you are using the AWS Encryption SDK with AWS KMS, +you can use AWS KMS to provide additional powerful controls using the encryption context. +For more information on that, see the KMS developer guide: + +https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context + +This example shows how to create a custom cryptographic materials manager (CMM) +that requires a particular field in the encryption context. +""" +import aws_encryption_sdk +from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring +from aws_encryption_sdk.keyrings.base import Keyring +from aws_encryption_sdk.materials_managers import ( + DecryptionMaterials, + DecryptionMaterialsRequest, + EncryptionMaterials, + EncryptionMaterialsRequest, +) +from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager + + +class MissingClassificationError(Exception): + """Indicates that an encryption context was found that lacked a classification identifier.""" + + +class ClassificationRequiringCryptoMaterialsManager(CryptoMaterialsManager): + """Only allow requests when the encryption context contains a classification identifier.""" + + def __init__(self, keyring): + # type: (Keyring) -> None + """Set up the inner cryptographic materials manager using the provided keyring. + + :param Keyring keyring: Keyring to use in the inner cryptographic materials manager + """ + self._classification_field = "classification" + self._classification_error = MissingClassificationError("Encryption context does not contain classification!") + # Wrap the provided keyring in the default cryptographic materials manager (CMM). + # + # This is the same thing that the encrypt and decrypt APIs, as well as the caching CMM, + # do if you provide a keyring instead of a CMM. + self._cmm = DefaultCryptoMaterialsManager(keyring=keyring) + + def get_encryption_materials(self, request): + # type: (EncryptionMaterialsRequest) -> EncryptionMaterials + """Block any requests that do not contain a classification identifier in the encryption context.""" + if self._classification_field not in request.encryption_context: + raise self._classification_error + + return self._cmm.get_encryption_materials(request) + + def decrypt_materials(self, request): + # type: (DecryptionMaterialsRequest) -> DecryptionMaterials + """Block any requests that do not contain a classification identifier in the encryption context.""" + if self._classification_field not in request.encryption_context: + raise self._classification_error + + return self._cmm.decrypt_materials(request) + + +def run(aws_kms_cmk, source_plaintext): + # type: (str, bytes) -> None + """Demonstrate an encrypt/decrypt cycle using a custom cryptographic materials manager that filters requests. + + :param str aws_kms_cmk: The ARN of an AWS KMS CMK that protects data keys + :param bytes source_plaintext: Plaintext to encrypt + """ + # Prepare your encryption context. + # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html#encryption-context + encryption_context = { + "encryption": "context", + "is not": "secret", + "but adds": "useful metadata", + "that can help you": "be confident that", + "the data you are handling": "is what you think it is", + } + + # Create the keyring that determines how your data keys are protected. + keyring = KmsKeyring(generator_key_id=aws_kms_cmk) + + # Create the classification requiring cryptographic materials manager using your keyring. + cmm = ClassificationRequiringCryptoMaterialsManager(keyring=keyring) + + # Demonstrate that the classification requiring CMM will not let you encrypt without a classification identifier. + try: + aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, materials_manager=cmm, + ) + except MissingClassificationError: + # Your encryption context did not contain a classification identifier. + # Reaching this point means everything is working as expected. + pass + else: + # The classification requiring CMM keeps this from happening. + raise AssertionError("The classification requiring CMM does not let this happen!") + + # Encrypt your plaintext data. + classified_ciphertext, _encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, + encryption_context=dict(classification="secret", **encryption_context), + materials_manager=cmm, + ) + + # Demonstrate that the ciphertext and plaintext are different. + assert classified_ciphertext != source_plaintext + + # Decrypt your encrypted data using the same cryptographic materials manager you used on encrypt. + # + # You do not need to specify the encryption context on decrypt + # because the header of the encrypted message includes the encryption context. + decrypted, decrypt_header = aws_encryption_sdk.decrypt(source=classified_ciphertext, materials_manager=cmm) + + # Demonstrate that the decrypted plaintext is identical to the original plaintext. + assert decrypted == source_plaintext + + # Verify that the encryption context used in the decrypt operation includes + # the encryption context that you specified when encrypting. + # The AWS Encryption SDK can add pairs, so don't require an exact match. + # + # In production, always use a meaningful encryption context. + assert set(encryption_context.items()) <= set(decrypt_header.encryption_context.items()) + + # Now demonstrate the decrypt path of the classification requiring cryptographic materials manager. + + # Encrypt your plaintext using the keyring and do not include a classification identifier. + unclassified_ciphertext, encrypt_header = aws_encryption_sdk.encrypt( + source=source_plaintext, encryption_context=encryption_context, keyring=keyring + ) + + assert "classification" not in encrypt_header.encryption_context + + # Demonstrate that the classification requiring CMM + # will not let you decrypt messages without classification identifiers. + try: + aws_encryption_sdk.decrypt(source=unclassified_ciphertext, materials_manager=cmm) + except MissingClassificationError: + # Your encryption context did not contain a classification identifier. + # Reaching this point means everything is working as expected. + pass + else: + # The classification requiring CMM keeps this from happening. + raise AssertionError("The classification requiring CMM does not let this happen!") diff --git a/test/unit/keyrings/test_aws_kms.py b/test/unit/keyrings/test_aws_kms.py index 78f84635e..2adf0f656 100644 --- a/test/unit/keyrings/test_aws_kms.py +++ b/test/unit/keyrings/test_aws_kms.py @@ -42,10 +42,7 @@ def test_kms_keyring_builds_correct_inner_keyring_multikeyring(): supplier = DefaultClientSupplier() test = KmsKeyring( - generator_key_id=generator_id, - key_ids=(child_id_1, child_id_2), - grant_tokens=grants, - client_supplier=supplier, + generator_key_id=generator_id, key_ids=(child_id_1, child_id_2), grant_tokens=grants, client_supplier=supplier, ) # We specified a generator and child IDs, so the inner keyring MUST be a multikeyring From b5be88ff46b17b7f7b5adc136a3414d8ebb760eb Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Thu, 9 Apr 2020 16:38:39 -0700 Subject: [PATCH 60/64] feat: deprecate master key providers (#242) * feat: deprecate master key providers * docs: change main API examples from MKP to keyring * docs: add deprecation notices in MKP docstrings * docs: add future guidance in MKP deprecation comment * docs: remove examples from readme and replace concepts with dev guide concepts * docs: add link to dev guide concepts section * docs: update CHANGELOG.rst Co-Authored-By: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> * docs: refine wording in MKP removal statements * docs: replace copy-paste concepts descriptions with links to developer guide Co-authored-by: Wesley Rosenblum <55108558+WesleyRosenblum@users.noreply.github.com> --- CHANGELOG.rst | 14 ++ README.rst | 194 +++--------------- src/aws_encryption_sdk/__init__.py | 39 ++-- src/aws_encryption_sdk/key_providers/base.py | 19 ++ src/aws_encryption_sdk/key_providers/kms.py | 8 + src/aws_encryption_sdk/key_providers/raw.py | 10 + .../base/test_base_master_key_provider.py | 6 + 7 files changed, 103 insertions(+), 187 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c02fb8017..6c51b214d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ Major Features * Add `keyrings`_. * Change one-step APIs to return a :class:`CryptoResult` rather than a tuple. + * Modified APIs: ``aws_encryption_sdk.encrypt`` and ``aws_encryption_sdk.decrypt``. .. note:: @@ -20,6 +21,19 @@ Major Features so this change should not break any existing consumers unless you are specifically relying on the output being an instance of :class:`tuple`. +Deprecations +------------ + +* Deprecate master key providers in favor of keyrings. + + * We still support using master key providers and are not removing them yet. + When we decide to remove them, + we will communicate that as defined in our versioning policy. + +* Deprecate support for Python 3.4. + + * This does not mean that this library will no longer work or install with 3.4, + but we are no longer testing against or advertising support for 3.4. 1.4.1 -- 2019-09-20 =================== diff --git a/README.rst b/README.rst index ae373f8e3..c0dfb8d8f 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ Getting Started Required Prerequisites ====================== -* Python 2.7+ or 3.5+ +* Python 2.7 or 3.5+ * cryptography >= 1.8.1 * boto3 * attrs @@ -55,189 +55,42 @@ Installation Concepts ======== -There are four main concepts that you need to understand to use this library: +There are three main concepts that are helpful to understand when using the AWS Encryption SDK. + +For further information, see the `AWS Encryption SDK developer guide concepts`_. Cryptographic Materials Managers -------------------------------- -Cryptographic materials managers (CMMs) are resources that collect cryptographic materials and prepare them for -use by the Encryption SDK core logic. - -An example of a CMM is the default CMM, which is automatically generated anywhere a caller provides a master -key provider. The default CMM collects encrypted data keys from all master keys referenced by the master key -provider. - -An example of a more advanced CMM is the caching CMM, which caches cryptographic materials provided by another CMM. +The cryptographic materials manager (CMM) assembles the cryptographic materials +that are used to encrypt and decrypt data. -Master Key Providers --------------------- -Master key providers are resources that provide master keys. -An example of a master key provider is `AWS KMS`_. +`For more details, +see the AWS Encryption SDK developer guide cryptographic materials manager concept. +`_ -To encrypt data in this client, a ``MasterKeyProvider`` object must contain at least one ``MasterKey`` object. +Keyrings +-------- -``MasterKeyProvider`` objects can also contain other ``MasterKeyProvider`` objects. +A keyring generates, encrypts, and decrypts data keys. -Master Keys ------------ -Master keys generate, encrypt, and decrypt data keys. -An example of a master key is a `KMS customer master key (CMK)`_. +`For more details, +see the AWS Encryption SDK developer guide keyring concept. +`_ Data Keys --------- -Data keys are the encryption keys that are used to encrypt your data. If your algorithm suite -uses a key derivation function, the data key is used to generate the key that directly encrypts the data. + +A data key is an encryption key that the AWS Encryption SDK uses to encrypt your data. + +`For more details, +see the AWS Encryption SDK developer guide data key concept. +`_ ***** Usage ***** -To use this client, you (the caller) must provide an instance of either a master key provider -or a CMM. The examples in this readme use the ``KMSMasterKeyProvider`` class. - -KMSMasterKeyProvider -==================== -Because the ``KMSMasterKeyProvider`` uses the `boto3 SDK`_ to interact with `AWS KMS`_, it requires AWS Credentials. -To provide these credentials, use the `standard means by which boto3 locates credentials`_ or provide a -pre-existing instance of a ``botocore session`` to the ``KMSMasterKeyProvider``. -This latter option can be useful if you have an alternate way to store your AWS credentials or -you want to reuse an existing instance of a botocore session in order to decrease startup costs. - -.. code:: python - - import aws_encryption_sdk - import botocore.session - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider() - - existing_botocore_session = botocore.session.Session() - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(botocore_session=existing_botocore_session) - - -You can pre-load the ``KMSMasterKeyProvider`` with one or more CMKs. -To encrypt data, you must configure the ``KMSMasterKeyProvider`` with as least one CMK. -If you configure the the ``KMSMasterKeyProvider`` with multiple CMKs, the `final message`_ -will include a copy of the data key encrypted by each configured CMK. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - -You can add CMKs from multiple regions to the ``KMSMasterKeyProvider``. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-west-2:3333333333333:key/33333333-3333-3333-3333-333333333333', - 'arn:aws:kms:ap-northeast-1:4444444444444:key/44444444-4444-4444-4444-444444444444' - ]) - - -Encryption and Decryption -========================= -After you create an instance of a ``MasterKeyProvider``, you can use either of the two -high-level ``encrypt``/``decrypt`` functions to encrypt and decrypt your data. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - my_plaintext = b'This is some super secret data! Yup, sure is!' - - my_ciphertext, encryptor_header = aws_encryption_sdk.encrypt( - source=my_plaintext, - key_provider=kms_key_provider - ) - - decrypted_plaintext, decryptor_header = aws_encryption_sdk.decrypt( - source=my_ciphertext, - key_provider=kms_key_provider - ) - - assert my_plaintext == decrypted_plaintext - assert encryptor_header.encryption_context == decryptor_header.encryption_context - -You can provide an `encryption context`_: a form of additional authenticating information. - -.. code:: python - - import aws_encryption_sdk - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - my_plaintext = b'This is some super secret data! Yup, sure is!' - - my_ciphertext, encryptor_header = aws_encryption_sdk.encrypt( - source=my_plaintext, - key_provider=kms_key_provider, - encryption_context={ - 'not really': 'a secret', - 'but adds': 'some authentication' - } - ) - - decrypted_plaintext, decryptor_header = aws_encryption_sdk.decrypt( - source=my_ciphertext, - key_provider=kms_key_provider - ) - - assert my_plaintext == decrypted_plaintext - assert encryptor_header.encryption_context == decryptor_header.encryption_context - - -Streaming -========= -If you are handling large files or simply do not want to put the entire plaintext or ciphertext in -memory at once, you can use this library's streaming clients directly. The streaming clients are -file-like objects, and behave exactly as you would expect a Python file object to behave, -offering context manager and iteration support. - -.. code:: python - - import aws_encryption_sdk - import filecmp - - kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ]) - plaintext_filename = 'my-secret-data.dat' - ciphertext_filename = 'my-encrypted-data.ct' - - with open(plaintext_filename, 'rb') as pt_file, open(ciphertext_filename, 'wb') as ct_file: - with aws_encryption_sdk.stream( - mode='e', - source=pt_file, - key_provider=kms_key_provider - ) as encryptor: - for chunk in encryptor: - ct_file.write(chunk) - - new_plaintext_filename = 'my-decrypted-data.dat' - with open(ciphertext_filename, 'rb') as ct_file, open(new_plaintext_filename, 'wb') as pt_file: - with aws_encryption_sdk.stream( - mode='d', - source=ct_file, - key_provider=kms_key_provider - ) as decryptor: - for chunk in decryptor: - pt_file.write(chunk) - - assert filecmp.cmp(plaintext_filename, new_plaintext_filename) - assert encryptor.header.encryption_context == decryptor.header.encryption_context +For examples of how to use these concepts to accomplish different tasks, see our `examples`_. Performance Considerations ========================== @@ -249,6 +102,8 @@ to your use-case in order to obtain peak performance. .. _AWS Encryption SDK: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/introduction.html +.. _AWS Encryption SDK developer guide concepts: + https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/concepts.html .. _cryptography: https://cryptography.io/en/latest/ .. _cryptography installation guide: https://cryptography.io/en/latest/installation/ .. _Read the Docs: http://aws-encryption-sdk-python.readthedocs.io/en/latest/ @@ -259,3 +114,4 @@ to your use-case in order to obtain peak performance. .. _standard means by which boto3 locates credentials: https://boto3.readthedocs.io/en/latest/guide/configuration.html .. _final message: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html .. _encryption context: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context +.. _examples: https://github.com/aws/aws-encryption-sdk-python/tree/master/examples diff --git a/src/aws_encryption_sdk/__init__.py b/src/aws_encryption_sdk/__init__.py index cc9b7b9f7..b6a47537c 100644 --- a/src/aws_encryption_sdk/__init__.py +++ b/src/aws_encryption_sdk/__init__.py @@ -39,14 +39,15 @@ def encrypt(**kwargs): .. code:: python >>> import aws_encryption_sdk - >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - ... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ... ]) + >>> from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + >>> keyring = KmsKeyring( + ... generator_key_id="arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222", + ... key_ids=["arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333"], + ... ) >>> my_ciphertext, encryptor_header = aws_encryption_sdk.encrypt( ... source=my_plaintext, - ... key_provider=kms_key_provider - ... ) + ... keyring=keyring, + >>> ) :param config: Client configuration object (config or individual parameters required) :type config: aws_encryption_sdk.streaming_client.EncryptorConfig @@ -106,13 +107,14 @@ def decrypt(**kwargs): .. code:: python >>> import aws_encryption_sdk - >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - ... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ... ]) - >>> my_ciphertext, encryptor_header = aws_encryption_sdk.decrypt( + >>> from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + >>> keyring = KmsKeyring( + ... generator_key_id="arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222", + ... key_ids=["arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333"], + ... ) + >>> my_ciphertext, decryptor_header = aws_encryption_sdk.decrypt( ... source=my_ciphertext, - ... key_provider=kms_key_provider + ... keyring=keyring, ... ) :param config: Client configuration object (config or individual parameters required) @@ -164,17 +166,18 @@ def stream(**kwargs): .. code:: python >>> import aws_encryption_sdk - >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ - ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', - ... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333' - ... ]) + >>> from aws_encryption_sdk.keyrings.aws_kms import KmsKeyring + >>> keyring = KmsKeyring( + ... generator_key_id="arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222", + ... key_ids=["arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333"], + ... ) >>> plaintext_filename = 'my-secret-data.dat' >>> ciphertext_filename = 'my-encrypted-data.ct' >>> with open(plaintext_filename, 'rb') as pt_file, open(ciphertext_filename, 'wb') as ct_file: ... with aws_encryption_sdk.stream( ... mode='e', ... source=pt_file, - ... key_provider=kms_key_provider + ... keyring=keyring, ... ) as encryptor: ... for chunk in encryptor: ... ct_file.write(chunk) @@ -183,7 +186,7 @@ def stream(**kwargs): ... with aws_encryption_sdk.stream( ... mode='d', ... source=ct_file, - ... key_provider=kms_key_provider + ... keyring=keyring, ... ) as decryptor: ... for chunk in decryptor: ... pt_file.write(chunk) diff --git a/src/aws_encryption_sdk/key_providers/base.py b/src/aws_encryption_sdk/key_providers/base.py index 9ee9b7d1c..4ba10585b 100644 --- a/src/aws_encryption_sdk/key_providers/base.py +++ b/src/aws_encryption_sdk/key_providers/base.py @@ -13,6 +13,7 @@ """Base class interface for Master Key Providers.""" import abc import logging +import warnings import attr import six @@ -51,6 +52,10 @@ class MasterKeyProviderConfig(object): class MasterKeyProvider(object): """Parent interface for Master Key Provider classes. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.base.Keyring` instead. + :param config: Configuration object :type config: aws_encryption_sdk.key_providers.base.MasterKeyProviderConfig """ @@ -78,6 +83,16 @@ def __new__(cls, **kwargs): """Set key index and member set for all new instances here to avoid requiring child classes to call super init. """ + # DeprecationWarning are ignored by default, + # but because we are not yet removing master key providers, + # I think this is the correct level of visibility. + # + # Once we decide that we are one X or Y version away from removing master key providers, + # we should upgrade this to a UserWarning. + warnings.warn( + "Master key providers are deprecated as of 1.5.0. You should migrate to keyrings.", DeprecationWarning + ) + instance = super(MasterKeyProvider, cls).__new__(cls) config = kwargs.pop("config", None) if not isinstance(config, instance._config_class): # pylint: disable=protected-access @@ -329,6 +344,10 @@ def __attrs_post_init__(self): class MasterKey(MasterKeyProvider): """Parent interface for Master Key classes. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.base.Keyring` instead. + :param bytes key_id: Key ID for Master Key :param config: Configuration object :type config: aws_encryption_sdk.key_providers.base.MasterKeyConfig diff --git a/src/aws_encryption_sdk/key_providers/kms.py b/src/aws_encryption_sdk/key_providers/kms.py index c0a2dc46e..08398b568 100644 --- a/src/aws_encryption_sdk/key_providers/kms.py +++ b/src/aws_encryption_sdk/key_providers/kms.py @@ -78,6 +78,10 @@ class KMSMasterKeyProviderConfig(MasterKeyProviderConfig): class KMSMasterKeyProvider(MasterKeyProvider): """Master Key Provider for KMS. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.aws_kms.KmsKeyring` instead. + >>> import aws_encryption_sdk >>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[ ... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222', @@ -226,6 +230,10 @@ def client_default(self): class KMSMasterKey(MasterKey): """Master Key class for KMS CMKs. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.aws_kms.KmsKeyring` instead. + :param config: Configuration object (config or individual parameters required) :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyConfig :param bytes key_id: KMS CMK ID diff --git a/src/aws_encryption_sdk/key_providers/raw.py b/src/aws_encryption_sdk/key_providers/raw.py index 57a1d5edf..fff3487e2 100644 --- a/src/aws_encryption_sdk/key_providers/raw.py +++ b/src/aws_encryption_sdk/key_providers/raw.py @@ -49,6 +49,11 @@ class RawMasterKeyConfig(MasterKeyConfig): class RawMasterKey(MasterKey): """Raw Master Key. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.raw.RawAESKeyring` + or :class:`aws_encryption_sdk.keyrings.raw.RawRSAKeyring` instead. + :param config: Configuration object (config or individual parameters required) :type config: aws_encryption_sdk.key_providers.raw.RawMasterKeyConfig :param bytes key_id: Key ID for Master Key @@ -192,6 +197,11 @@ def _decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): class RawMasterKeyProvider(MasterKeyProvider): """Raw Master Key Provider. + .. versionadded:: 1.5.0 + Master key providers are deprecated. + Use :class:`aws_encryption_sdk.keyrings.raw.RawAESKeyring` + or :class:`aws_encryption_sdk.keyrings.raw.RawRSAKeyring` instead. + :param config: Configuration object (optional) :type config: aws_encryption_sdk.key_providers.base.MasterKeyProviderConfig """ diff --git a/test/unit/key_providers/base/test_base_master_key_provider.py b/test/unit/key_providers/base/test_base_master_key_provider.py index 778068dd5..87e9a924c 100644 --- a/test/unit/key_providers/base/test_base_master_key_provider.py +++ b/test/unit/key_providers/base/test_base_master_key_provider.py @@ -60,6 +60,12 @@ def test_repr(): ) +def test_deprecated(): + + with pytest.warns(DeprecationWarning): + MockMasterKeyProvider(provider_id="ex_provider_id", mock_new_master_key="ex_new_master_key") + + class TestBaseMasterKeyProvider(object): def test_provider_id_enforcement(self): class TestProvider(MasterKeyProvider): From 9cc19b173e7e1353b9116db6bca43209ac422152 Mon Sep 17 00:00:00 2001 From: mattsb42-aws Date: Fri, 10 Apr 2020 13:58:08 -0700 Subject: [PATCH 61/64] chore: remove example collected from merge from master this example is replaced by the onestep_unsigned example --- examples/src/one_kms_cmk_unsigned.py | 51 -------------------- examples/test/test_i_one_kms_cmk_unsigned.py | 29 ----------- 2 files changed, 80 deletions(-) delete mode 100644 examples/src/one_kms_cmk_unsigned.py delete mode 100644 examples/test/test_i_one_kms_cmk_unsigned.py diff --git a/examples/src/one_kms_cmk_unsigned.py b/examples/src/one_kms_cmk_unsigned.py deleted file mode 100644 index df2f4373d..000000000 --- a/examples/src/one_kms_cmk_unsigned.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2019 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. -"""Example showing basic encryption and decryption of a value already in memory -using one KMS CMK with an unsigned algorithm. -""" -from aws_encryption_sdk import KMSMasterKeyProvider, decrypt, encrypt -from aws_encryption_sdk.identifiers import Algorithm - - -def encrypt_decrypt(key_arn, source_plaintext, botocore_session=None): - """Encrypts and then decrypts a string under one KMS customer master key (CMK) with an unsigned algorithm. - - :param str key_arn: Amazon Resource Name (ARN) of the KMS CMK - :param bytes source_plaintext: Data to encrypt - :param botocore_session: existing botocore session instance - :type botocore_session: botocore.session.Session - """ - kwargs = dict(key_ids=[key_arn]) - - if botocore_session is not None: - kwargs["botocore_session"] = botocore_session - - # Create master key provider using the ARN of the key and the session (botocore_session) - kms_key_provider = KMSMasterKeyProvider(**kwargs) - - # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header - ciphertext, encrypted_message_header = encrypt( - algorithm=Algorithm.AES_256_GCM_IV12_TAG16_HKDF_SHA256, source=source_plaintext, key_provider=kms_key_provider - ) - - # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header - plaintext, decrypted_message_header = decrypt(source=ciphertext, key_provider=kms_key_provider) - - # Check if the original message and the decrypted message are the same - assert source_plaintext == plaintext - - # Check if the headers of the encrypted message and decrypted message match - assert all( - pair in encrypted_message_header.encryption_context.items() - for pair in decrypted_message_header.encryption_context.items() - ) diff --git a/examples/test/test_i_one_kms_cmk_unsigned.py b/examples/test/test_i_one_kms_cmk_unsigned.py deleted file mode 100644 index 8a2758c96..000000000 --- a/examples/test/test_i_one_kms_cmk_unsigned.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2019 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. -"""Unit test suite for the encryption and decryption using one KMS CMK with an unsigned algorithm example.""" - -import botocore.session -import pytest - -from ..src.one_kms_cmk_unsigned import encrypt_decrypt -from .examples_test_utils import get_cmk_arn -from .examples_test_utils import static_plaintext - - -pytestmark = [pytest.mark.examples] - - -def test_one_kms_cmk_unsigned(): - plaintext = static_plaintext - cmk_arn = get_cmk_arn() - encrypt_decrypt(key_arn=cmk_arn, source_plaintext=plaintext, botocore_session=botocore.session.Session()) From 8b7a609a86ee563695b8f57d5d7b1b6d84287932 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Fri, 10 Apr 2020 16:43:09 -0700 Subject: [PATCH 62/64] docs: fix security issue notifications link in readme (#245) * docs: fix security issue notifications link in readme * docs: update README.rst Co-Authored-By: seebees Co-authored-by: seebees --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e4bcbe7ac..3407dc177 100644 --- a/README.rst +++ b/README.rst @@ -117,4 +117,4 @@ to your use-case in order to obtain peak performance. .. _final message: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html .. _encryption context: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context .. _examples: https://github.com/aws/aws-encryption-sdk-python/tree/master/examples -.. _Security issue notifications: ./CONTRIBUTING.md#security-issue-notifications +.. _Security issue notifications: https://github.com/aws/aws-encryption-sdk-python/tree/master/CONTRIBUTING.md#security-issue-notifications From a6433d4bb5834c3aff2ea5ff9588e195c1e1b170 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Tue, 14 Apr 2020 12:27:58 -0700 Subject: [PATCH 63/64] feat: remove wrapping_algorithm input parameter from RawAESKeyring (#247) * feat: remove wrapping_algorithm input parameter from RawAESKeyring * docs: make RawAESKeyring key material length error message deterministic and check for exact allowed options * docs: tweak wording explaining wrapping key generation --- examples/src/keyring/raw_aes/raw_aes.py | 10 ++---- src/aws_encryption_sdk/keyrings/raw.py | 35 +++++++++++++++++--- test/functional/keyrings/raw/test_raw_aes.py | 33 ++++-------------- test/functional/keyrings/test_multi.py | 14 ++------ test/unit/keyrings/raw/test_raw_aes.py | 21 ++++++------ test/unit/keyrings/raw/test_raw_rsa.py | 18 ++++++++++ test/unit/keyrings/test_multi.py | 16 ++------- test/unit/unit_test_utils.py | 19 ++--------- 8 files changed, 75 insertions(+), 91 deletions(-) diff --git a/examples/src/keyring/raw_aes/raw_aes.py b/examples/src/keyring/raw_aes/raw_aes.py index c688141f2..5abe28ea4 100644 --- a/examples/src/keyring/raw_aes/raw_aes.py +++ b/examples/src/keyring/raw_aes/raw_aes.py @@ -10,7 +10,6 @@ import os import aws_encryption_sdk -from aws_encryption_sdk.identifiers import WrappingAlgorithm from aws_encryption_sdk.keyrings.raw import RawAESKeyring @@ -30,14 +29,10 @@ def run(source_plaintext): "the data you are handling": "is what you think it is", } - # Choose the wrapping algorithm for the keyring to use. - wrapping_algorithm = WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING - - # Generate an AES key to use with your keyring. - # The key size depends on the wrapping algorithm. + # Generate a 256-bit (32 byte) AES key to use with your keyring. # # In practice, you should get this key from a secure key management system such as an HSM. - key = os.urandom(wrapping_algorithm.algorithm.kdf_input_len) + key = os.urandom(32) # Create the keyring that determines how your data keys are protected. keyring = RawAESKeyring( @@ -50,7 +45,6 @@ def run(source_plaintext): key_namespace="some managed raw keys", key_name=b"my AES wrapping key", wrapping_key=key, - wrapping_algorithm=wrapping_algorithm, ) # Encrypt your plaintext data. diff --git a/src/aws_encryption_sdk/keyrings/raw.py b/src/aws_encryption_sdk/keyrings/raw.py index 1e5978d42..450111ad7 100644 --- a/src/aws_encryption_sdk/keyrings/raw.py +++ b/src/aws_encryption_sdk/keyrings/raw.py @@ -6,7 +6,7 @@ import attr import six -from attr.validators import instance_of, optional +from attr.validators import in_, instance_of, optional from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey @@ -76,7 +76,6 @@ class RawAESKeyring(Keyring): :param str key_namespace: String defining the keyring. :param bytes key_name: Key ID :param bytes wrapping_key: Encryption key with which to wrap plaintext data key. - :param WrappingAlgorithm wrapping_algorithm: Wrapping Algorithm with which to wrap plaintext data key. .. note:: @@ -86,11 +85,28 @@ class RawAESKeyring(Keyring): key_namespace = attr.ib(validator=instance_of(six.string_types)) key_name = attr.ib(validator=instance_of(six.binary_type)) _wrapping_key = attr.ib(repr=False, validator=instance_of(six.binary_type)) - _wrapping_algorithm = attr.ib(repr=False, validator=instance_of(WrappingAlgorithm)) def __attrs_post_init__(self): # type: () -> None """Prepares initial values not handled by attrs.""" + key_size_to_wrapping_algorithm = { + wrapper.algorithm.kdf_input_len: wrapper + for wrapper in ( + WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ) + } + + try: + self._wrapping_algorithm = key_size_to_wrapping_algorithm[len(self._wrapping_key)] + except KeyError: + raise ValueError( + "Invalid wrapping key length. Must be one of {} bytes.".format( + sorted(key_size_to_wrapping_algorithm.keys()) + ) + ) + self._key_provider = MasterKeyInfo(provider_id=self.key_namespace, key_info=self.key_name) self._wrapping_key_structure = WrappingKey( @@ -244,7 +260,18 @@ class RawRSAKeyring(Keyring): key_namespace = attr.ib(validator=instance_of(six.string_types)) key_name = attr.ib(validator=instance_of(six.binary_type)) - _wrapping_algorithm = attr.ib(repr=False, validator=instance_of(WrappingAlgorithm)) + _wrapping_algorithm = attr.ib( + repr=False, + validator=in_( + ( + WrappingAlgorithm.RSA_PKCS1, + WrappingAlgorithm.RSA_OAEP_SHA1_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA256_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA384_MGF1, + WrappingAlgorithm.RSA_OAEP_SHA512_MGF1, + ) + ), + ) _private_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(RSAPrivateKey))) _public_wrapping_key = attr.ib(default=None, repr=False, validator=optional(instance_of(RSAPublicKey))) diff --git a/test/functional/keyrings/raw/test_raw_aes.py b/test/functional/keyrings/raw/test_raw_aes.py index 5e181f803..9759f2ce9 100644 --- a/test/functional/keyrings/raw/test_raw_aes.py +++ b/test/functional/keyrings/raw/test_raw_aes.py @@ -65,21 +65,14 @@ def sample_encryption_materials(): @pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) -@pytest.mark.parametrize("wrapping_algorithm_samples", _WRAPPING_ALGORITHM) -def test_raw_aes_encryption_decryption(encryption_materials_samples, wrapping_algorithm_samples): +def test_raw_aes_encryption_decryption(encryption_materials_samples): # Initializing attributes key_namespace = _PROVIDER_ID key_name = _KEY_ID - _wrapping_algorithm = wrapping_algorithm_samples # Creating an instance of a raw AES keyring - test_raw_aes_keyring = RawAESKeyring( - key_namespace=key_namespace, - key_name=key_name, - wrapping_key=_WRAPPING_KEY, - wrapping_algorithm=_wrapping_algorithm, - ) + test_raw_aes_keyring = RawAESKeyring(key_namespace=key_namespace, key_name=key_name, wrapping_key=_WRAPPING_KEY,) # Call on_encrypt function for the keyring encryption_materials = test_raw_aes_keyring.on_encrypt(encryption_materials=encryption_materials_samples) @@ -101,21 +94,14 @@ def test_raw_aes_encryption_decryption(encryption_materials_samples, wrapping_al @pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) -@pytest.mark.parametrize("wrapping_algorithm_samples", _WRAPPING_ALGORITHM) -def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_samples, wrapping_algorithm_samples): +def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_samples): # Initializing attributes key_namespace = _PROVIDER_ID key_name = _KEY_ID - _wrapping_algorithm = wrapping_algorithm_samples # Creating an instance of a raw AES keyring - test_raw_aes_keyring = RawAESKeyring( - key_namespace=key_namespace, - key_name=key_name, - wrapping_key=_WRAPPING_KEY, - wrapping_algorithm=_wrapping_algorithm, - ) + test_raw_aes_keyring = RawAESKeyring(key_namespace=key_namespace, key_name=key_name, wrapping_key=_WRAPPING_KEY,) # Creating an instance of a raw master key test_raw_master_key = RawMasterKey( @@ -139,21 +125,14 @@ def test_raw_master_key_decrypts_what_raw_keyring_encrypts(encryption_materials_ @pytest.mark.parametrize("encryption_materials_samples", sample_encryption_materials()) -@pytest.mark.parametrize("wrapping_algorithm_samples", _WRAPPING_ALGORITHM) -def test_raw_keyring_decrypts_what_raw_master_key_encrypts(encryption_materials_samples, wrapping_algorithm_samples): +def test_raw_keyring_decrypts_what_raw_master_key_encrypts(encryption_materials_samples): # Initializing attributes key_namespace = _PROVIDER_ID key_name = _KEY_ID - _wrapping_algorithm = wrapping_algorithm_samples # Creating an instance of a raw AES keyring - test_raw_aes_keyring = RawAESKeyring( - key_namespace=key_namespace, - key_name=key_name, - wrapping_key=_WRAPPING_KEY, - wrapping_algorithm=_wrapping_algorithm, - ) + test_raw_aes_keyring = RawAESKeyring(key_namespace=key_namespace, key_name=key_name, wrapping_key=_WRAPPING_KEY,) # Creating an instance of a raw master key test_raw_master_key = RawMasterKey( diff --git a/test/functional/keyrings/test_multi.py b/test/functional/keyrings/test_multi.py index db98eb5a8..ff9eb2440 100644 --- a/test/functional/keyrings/test_multi.py +++ b/test/functional/keyrings/test_multi.py @@ -50,12 +50,7 @@ ) _MULTI_KEYRING_WITH_GENERATOR_AND_CHILDREN = MultiKeyring( - generator=RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, - wrapping_key=_WRAPPING_KEY_AES, - ), + generator=RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), children=[ RawRSAKeyring( key_namespace=_PROVIDER_ID, @@ -95,12 +90,7 @@ public_exponent=65537, key_size=2048, backend=default_backend() ), ), - RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_algorithm=WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, - wrapping_key=_WRAPPING_KEY_AES, - ), + RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), ] ) diff --git a/test/unit/keyrings/raw/test_raw_aes.py b/test/unit/keyrings/raw/test_raw_aes.py index 1f4322e09..72961c7d4 100644 --- a/test/unit/keyrings/raw/test_raw_aes.py +++ b/test/unit/keyrings/raw/test_raw_aes.py @@ -47,12 +47,7 @@ @pytest.fixture def raw_aes_keyring(): - return RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, - wrapping_key=_WRAPPING_KEY, - ) + return RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY,) @pytest.fixture @@ -113,12 +108,18 @@ def test_valid_parameters(raw_aes_keyring): def test_invalid_parameters(key_namespace, key_name, wrapping_algorithm, wrapping_key): with pytest.raises(TypeError): RawAESKeyring( - key_namespace=key_namespace, - key_name=key_name, - wrapping_algorithm=wrapping_algorithm, - wrapping_key=wrapping_key, + key_namespace=key_namespace, key_name=key_name, wrapping_key=wrapping_key, + ) + + +def test_invalid_key_length(): + with pytest.raises(ValueError) as excinfo: + RawAESKeyring( + key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=b"012345", ) + excinfo.match(r"Invalid wrapping key length. Must be one of \[16, 24, 32\] bytes.") + def test_on_encrypt_when_data_encryption_key_given(raw_aes_keyring, patch_generate_data_key): test_raw_aes_keyring = raw_aes_keyring diff --git a/test/unit/keyrings/raw/test_raw_rsa.py b/test/unit/keyrings/raw/test_raw_rsa.py index 4b0a8d506..5416ae24d 100644 --- a/test/unit/keyrings/raw/test_raw_rsa.py +++ b/test/unit/keyrings/raw/test_raw_rsa.py @@ -108,6 +108,24 @@ def test_invalid_parameters(key_namespace, key_name, wrapping_algorithm, private ) +@pytest.mark.parametrize( + "wrapping_algorithm", + ( + WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_192_GCM_IV12_TAG16_NO_PADDING, + WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, + ), +) +def test_invalid_wrapping_algorithm_suite(wrapping_algorithm): + with pytest.raises(ValueError): + RawRSAKeyring( + key_namespace=_PROVIDER_ID, + key_name=_KEY_ID, + wrapping_algorithm=wrapping_algorithm, + private_wrapping_key=raw_rsa_private_key(), + ) + + def test_public_and_private_key_not_provided(): with pytest.raises(TypeError) as exc_info: RawRSAKeyring( diff --git a/test/unit/keyrings/test_multi.py b/test/unit/keyrings/test_multi.py index 747ef5c37..97948ef63 100644 --- a/test/unit/keyrings/test_multi.py +++ b/test/unit/keyrings/test_multi.py @@ -96,26 +96,14 @@ def test_parent(): def test_keyring_with_generator_but_no_children(): - generator_keyring = RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_key=_WRAPPING_KEY_AES, - wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, - ) + generator_keyring = RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,) test_multi_keyring = MultiKeyring(generator=generator_keyring) assert test_multi_keyring.generator is generator_keyring assert not test_multi_keyring.children def test_keyring_with_children_but_no_generator(): - children_keyring = [ - RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_key=_WRAPPING_KEY_AES, - wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, - ) - ] + children_keyring = [RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,)] test_multi_keyring = MultiKeyring(children=children_keyring) assert test_multi_keyring.children is children_keyring assert test_multi_keyring.generator is None diff --git a/test/unit/unit_test_utils.py b/test/unit/unit_test_utils.py index 9063badc6..d175f0b3e 100644 --- a/test/unit/unit_test_utils.py +++ b/test/unit/unit_test_utils.py @@ -256,12 +256,7 @@ def get_decryption_materials_without_data_key(): def get_multi_keyring_with_generator_and_children(): return MultiKeyring( - generator=RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_IV12_TAG16_NO_PADDING, - wrapping_key=_WRAPPING_KEY_AES, - ), + generator=RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), children=[ RawRSAKeyring( key_namespace=_PROVIDER_ID, @@ -307,12 +302,7 @@ def get_multi_keyring_with_no_generator(): public_exponent=65537, key_size=2048, backend=default_backend() ), ), - RawAESKeyring( - key_namespace=_PROVIDER_ID, - key_name=_KEY_ID, - wrapping_algorithm=WrappingAlgorithm.AES_128_GCM_IV12_TAG16_NO_PADDING, - wrapping_key=_WRAPPING_KEY_AES, - ), + RawAESKeyring(key_namespace=_PROVIDER_ID, key_name=_KEY_ID, wrapping_key=_WRAPPING_KEY_AES,), ] ) @@ -482,10 +472,7 @@ def ephemeral_raw_aes_keyring(wrapping_algorithm=WrappingAlgorithm.AES_256_GCM_I if key is None: key = os.urandom(key_length) return RawAESKeyring( - key_namespace="fake", - key_name="aes-{}".format(key_length * 8).encode("utf-8"), - wrapping_algorithm=wrapping_algorithm, - wrapping_key=key, + key_namespace="fake", key_name="aes-{}".format(key_length * 8).encode("utf-8"), wrapping_key=key, ) From fef2ad75c367aa03cd346979dcd86274beffca79 Mon Sep 17 00:00:00 2001 From: Matt Bullock Date: Wed, 15 Apr 2020 10:11:52 -0700 Subject: [PATCH 64/64] docs: add mention of new examples to changelog (#250) --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6c51b214d..aaaed8044 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,15 @@ Deprecations * This does not mean that this library will no longer work or install with 3.4, but we are no longer testing against or advertising support for 3.4. +Documentation +------------- + +* Added new examples demonstrating how to use + APIs, keyrings, cryptographic materials managers, and master key providers. + `#221 `_ + `#236 `_ + `#239 `_ + 1.4.1 -- 2019-09-20 ===================