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)