diff --git a/doc/index.rst b/doc/index.rst index b0de37c2..e13cbc81 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -21,8 +21,11 @@ Modules dynamodb_encryption_sdk.encrypted.table dynamodb_encryption_sdk.material_providers dynamodb_encryption_sdk.material_providers.aws_kms + dynamodb_encryption_sdk.material_providers.most_recent dynamodb_encryption_sdk.material_providers.static dynamodb_encryption_sdk.material_providers.wrapped + dynamodb_encryption_sdk.material_providers.store + dynamodb_encryption_sdk.material_providers.store.meta dynamodb_encryption_sdk.materials dynamodb_encryption_sdk.materials.raw dynamodb_encryption_sdk.materials.wrapped diff --git a/src/dynamodb_encryption_sdk/delegated_keys/__init__.py b/src/dynamodb_encryption_sdk/delegated_keys/__init__.py index 9c2ad28e..ac57a207 100644 --- a/src/dynamodb_encryption_sdk/delegated_keys/__init__.py +++ b/src/dynamodb_encryption_sdk/delegated_keys/__init__.py @@ -13,7 +13,7 @@ """Delegated keys.""" import abc try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Dict, Text # noqa pylint: disable=unused-import + from typing import Dict, Optional, Text # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -43,17 +43,24 @@ class DelegatedKey(object): a ``NotImplementedError`` detailing this. """ - #: Most delegated keys should not be used with RawCryptographicMaterials. - allowed_for_raw_materials = False - @abc.abstractproperty def algorithm(self): # type: () -> Text """Text description of algorithm used by this delegated key.""" + @property + def allowed_for_raw_materials(self): + # type: () -> bool + """Most delegated keys should not be used with RawCryptographicMaterials. + + :returns: False + :rtype: bool + """ + return False + @classmethod - def generate(cls, algorithm, key_length): - # type: (Text, int) -> None + def generate(cls, algorithm, key_length): # type: ignore + # type: (Text, int) -> DelegatedKey # pylint: disable=unused-argument,no-self-use """Generate an instance of this DelegatedKey using the specified algorithm and key length. @@ -64,8 +71,8 @@ def generate(cls, algorithm, key_length): """ _raise_not_implemented('generate') - def encrypt(self, algorithm, name, plaintext, additional_associated_data=None): - # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + def encrypt(self, algorithm, name, plaintext, additional_associated_data=None): # type: ignore + # type: (Text, Text, bytes, Optional[Dict[Text, Text]]) -> bytes # pylint: disable=unused-argument,no-self-use """Encrypt data. @@ -79,8 +86,8 @@ def encrypt(self, algorithm, name, plaintext, additional_associated_data=None): """ _raise_not_implemented('encrypt') - def decrypt(self, algorithm, name, ciphertext, additional_associated_data=None): - # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + def decrypt(self, algorithm, name, ciphertext, additional_associated_data=None): # type: ignore + # type: (Text, Text, bytes, Optional[Dict[Text, Text]]) -> bytes # pylint: disable=unused-argument,no-self-use """Encrypt data. @@ -94,8 +101,8 @@ def decrypt(self, algorithm, name, ciphertext, additional_associated_data=None): """ _raise_not_implemented('decrypt') - def wrap(self, algorithm, content_key, additional_associated_data=None): - # type: (Text, bytes, Dict[Text, Text]) -> bytes + def wrap(self, algorithm, content_key, additional_associated_data=None): # type: ignore + # type: (Text, bytes, Optional[Dict[Text, Text]]) -> bytes # pylint: disable=unused-argument,no-self-use """Wrap content key. @@ -108,8 +115,15 @@ def wrap(self, algorithm, content_key, additional_associated_data=None): """ _raise_not_implemented('wrap') - def unwrap(self, algorithm, wrapped_key, wrapped_key_algorithm, wrapped_key_type, additional_associated_data=None): - # type: (Text, bytes, Text, EncryptionKeyType, Dict[Text, Text]) -> DelegatedKey + def unwrap( # type: ignore + self, + algorithm, + wrapped_key, + wrapped_key_algorithm, + wrapped_key_type, + additional_associated_data=None + ): + # type: (Text, bytes, Text, EncryptionKeyType, Optional[Dict[Text, Text]]) -> DelegatedKey # pylint: disable=unused-argument,no-self-use """Wrap content key. @@ -125,7 +139,7 @@ def unwrap(self, algorithm, wrapped_key, wrapped_key_algorithm, wrapped_key_type """ _raise_not_implemented('unwrap') - def sign(self, algorithm, data): + def sign(self, algorithm, data): # type: ignore # type: (Text, bytes) -> bytes # pylint: disable=unused-argument,no-self-use """Sign data. @@ -137,7 +151,7 @@ def sign(self, algorithm, data): """ _raise_not_implemented('sign') - def verify(self, algorithm, signature, data): + def verify(self, algorithm, signature, data): # type: ignore # type: (Text, bytes, bytes) -> None # pylint: disable=unused-argument,no-self-use """Sign data. @@ -148,10 +162,10 @@ def verify(self, algorithm, signature, data): """ _raise_not_implemented('verify') - def signing_algorithm(self): + def signing_algorithm(self): # type: ignore # type: () -> Text # pylint: disable=no-self-use - """Provides a description that can inform an appropriate cryptographic materials + """Provide a description that can inform an appropriate cryptographic materials provider about how to build a DelegatedKey for signature verification. If implemented, the return value of this method is included in the material description written to the encrypted item. diff --git a/src/dynamodb_encryption_sdk/delegated_keys/jce.py b/src/dynamodb_encryption_sdk/delegated_keys/jce.py index ad71fdc3..4ee676a9 100644 --- a/src/dynamodb_encryption_sdk/delegated_keys/jce.py +++ b/src/dynamodb_encryption_sdk/delegated_keys/jce.py @@ -22,6 +22,12 @@ from cryptography.hazmat.primitives.asymmetric import rsa import six +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Optional, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.exceptions import JceTransformationError, UnwrappingError from dynamodb_encryption_sdk.identifiers import EncryptionKeyType, KeyEncodingType, LOGGER_NAME from dynamodb_encryption_sdk.internal.crypto.jce_bridge import authentication, encryption, primitives @@ -67,7 +73,7 @@ def _generate_rsa_key(key_length): } -@attr.s +@attr.s(init=False) class JceNameLocalDelegatedKey(DelegatedKey): # pylint: disable=too-many-instance-attributes """Delegated key that uses JCE StandardName algorithm values to determine behavior. @@ -114,6 +120,25 @@ class JceNameLocalDelegatedKey(DelegatedKey): _key_type = attr.ib(validator=attr.validators.instance_of(EncryptionKeyType)) _key_encoding = attr.ib(validator=attr.validators.instance_of(KeyEncodingType)) + def __init__( + self, + key, # type: bytes + algorithm, # type: Text + key_type, # type: EncryptionKeyType + key_encoding # type: KeyEncodingType + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.key = key + self._algorithm = algorithm + self._key_type = key_type + self._key_encoding = key_encoding + attr.validate(self) + self.__attrs_post_init__() + @property def algorithm(self): # type: () -> Text @@ -203,6 +228,7 @@ def generate(cls, algorithm, key_length=None): @property def allowed_for_raw_materials(self): + # type: () -> bool """Only ``JceNameLocalDelegatedKey`` backed by AES keys are allowed to be used with ``RawCryptographicMaterials``. @@ -212,7 +238,7 @@ def allowed_for_raw_materials(self): return self.algorithm == 'AES' def _encrypt(self, algorithm, name, plaintext, additional_associated_data=None): - # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + # type: (Text, Text, bytes, Optional[Dict[Text, Text]]) -> bytes # pylint: disable=unused-argument """ Encrypt data. @@ -230,7 +256,7 @@ def _encrypt(self, algorithm, name, plaintext, additional_associated_data=None): return encryptor.encrypt(self.__key, plaintext) def _decrypt(self, algorithm, name, ciphertext, additional_associated_data=None): - # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + # type: (Text, Text, bytes, Optional[Dict[Text, Text]]) -> bytes # pylint: disable=unused-argument """Encrypt data. @@ -246,7 +272,7 @@ def _decrypt(self, algorithm, name, ciphertext, additional_associated_data=None) return decryptor.decrypt(self.__key, ciphertext) def _wrap(self, algorithm, content_key, additional_associated_data=None): - # type: (Text, bytes, Dict[Text, Text]) -> bytes + # type: (Text, bytes, Optional[Dict[Text, Text]]) -> bytes # pylint: disable=unused-argument """Wrap content key. @@ -263,7 +289,7 @@ def _wrap(self, algorithm, content_key, additional_associated_data=None): ) def _unwrap(self, algorithm, wrapped_key, wrapped_key_algorithm, wrapped_key_type, additional_associated_data=None): - # type: (Text, bytes, Text, EncryptionKeyType, Dict[Text, Text]) -> DelegatedKey + # type: (Text, bytes, Text, EncryptionKeyType, Optional[Dict[Text, Text]]) -> DelegatedKey # pylint: disable=unused-argument """Wrap content key. @@ -316,7 +342,7 @@ def _verify(self, algorithm, signature, data): def _signing_algorithm(self): # type: () -> Text - """Provides a description that can inform an appropriate cryptographic materials + """Provide a description that can inform an appropriate cryptographic materials provider about how to build a ``JceNameLocalDelegatedKey`` for signature verification. The return value of this method is included in the material description written to the encrypted item. diff --git a/src/dynamodb_encryption_sdk/encrypted/__init__.py b/src/dynamodb_encryption_sdk/encrypted/__init__.py index ec2051de..35befc81 100644 --- a/src/dynamodb_encryption_sdk/encrypted/__init__.py +++ b/src/dynamodb_encryption_sdk/encrypted/__init__.py @@ -30,7 +30,7 @@ __all__ = ('CryptoConfig',) -@attr.s +@attr.s(init=False) class CryptoConfig(object): """Container for all configuration needed to encrypt or decrypt an item. @@ -46,6 +46,23 @@ class CryptoConfig(object): encryption_context = attr.ib(validator=attr.validators.instance_of(EncryptionContext)) attribute_actions = attr.ib(validator=attr.validators.instance_of(AttributeActions)) + def __init__( + self, + materials_provider, # type: CryptographicMaterialsProvider + encryption_context, # type: EncryptionContext + attribute_actions # type: AttributeActions + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.materials_provider = materials_provider + self.encryption_context = encryption_context + self.attribute_actions = attribute_actions + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): # type: () -> None """Make sure that primary index attributes are not being encrypted.""" diff --git a/src/dynamodb_encryption_sdk/encrypted/client.py b/src/dynamodb_encryption_sdk/encrypted/client.py index a6edd29f..9e093963 100644 --- a/src/dynamodb_encryption_sdk/encrypted/client.py +++ b/src/dynamodb_encryption_sdk/encrypted/client.py @@ -16,6 +16,12 @@ import attr import botocore +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Callable, Dict, Iterator, Optional # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.internal.utils import ( crypto_config_from_cache, crypto_config_from_kwargs, decrypt_batch_get_item, decrypt_get_item, decrypt_multi_get, @@ -30,7 +36,7 @@ __all__ = ('EncryptedClient',) -@attr.s +@attr.s(init=False) class EncryptedPaginator(object): """Paginator that decrypts returned items before returning them. @@ -44,6 +50,22 @@ class EncryptedPaginator(object): _decrypt_method = attr.ib() _crypto_config_method = attr.ib(validator=callable_validator) + def __init__( + self, + paginator, # type: botocore.paginate.Paginator + decrypt_method, # type: Callable + crypto_config_method # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self._paginator = paginator + self._decrypt_method = decrypt_method + self._crypto_config_method = crypto_config_method + attr.validate(self) + @_decrypt_method.validator def validate_decrypt_method(self, attribute, value): # pylint: disable=unused-argument @@ -66,7 +88,7 @@ def __getattr__(self, name): return getattr(self._paginator, name) def paginate(self, **kwargs): - # type: (**Any) -> Dict + # type: (**Any) -> Iterator[Dict] # TODO: narrow this down """Create an iterator that will paginate through responses from the underlying paginator, transparently decrypting any returned items. @@ -84,7 +106,7 @@ def paginate(self, **kwargs): yield page -@attr.s +@attr.s(init=False) class EncryptedClient(object): # pylint: disable=too-few-public-methods,too-many-instance-attributes """High-level helper class to provide a familiar interface to encrypted tables. @@ -143,6 +165,30 @@ class EncryptedClient(object): default=False ) + def __init__( + self, + client, # type: botocore.client.BaseClient + materials_provider, # type: CryptographicMaterialsProvider + attribute_actions=None, # type: Optional[AttributeActions] + auto_refresh_table_indexes=True, # type: Optional[bool] + expect_standard_dictionaries=False # type: Optional[bool] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if attribute_actions is None: + attribute_actions = AttributeActions() + + self._client = client + self._materials_provider = materials_provider + self._attribute_actions = attribute_actions + self._auto_refresh_table_indexes = auto_refresh_table_indexes + self._expect_standard_dictionaries = expect_standard_dictionaries + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Set up the table info cache and translation methods.""" if self._expect_standard_dictionaries: diff --git a/src/dynamodb_encryption_sdk/encrypted/resource.py b/src/dynamodb_encryption_sdk/encrypted/resource.py index e2bec983..18dad1e5 100644 --- a/src/dynamodb_encryption_sdk/encrypted/resource.py +++ b/src/dynamodb_encryption_sdk/encrypted/resource.py @@ -17,6 +17,12 @@ from boto3.resources.base import ServiceResource from boto3.resources.collection import CollectionManager +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Optional # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.internal.utils import ( crypto_config_from_cache, decrypt_batch_get_item, encrypt_batch_write_item, TableInfoCache ) @@ -28,7 +34,7 @@ __all__ = ('EncryptedResource',) -@attr.s +@attr.s(init=False) class EncryptedTablesCollectionManager(object): # pylint: disable=too-few-public-methods """Tables collection manager that provides EncryptedTable objects. @@ -50,6 +56,25 @@ class EncryptedTablesCollectionManager(object): _attribute_actions = attr.ib(validator=attr.validators.instance_of(AttributeActions)) _table_info_cache = attr.ib(validator=attr.validators.instance_of(TableInfoCache)) + def __init__( + self, + collection, # type: CollectionManager + materials_provider, # type: CryptographicMaterialsProvider + attribute_actions, # type: AttributeActions + table_info_cache # type: TableInfoCache + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self._collection = collection + self._materials_provider = materials_provider + self._attribute_actions = attribute_actions + self._table_info_cache = table_info_cache + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Set up the translation methods.""" self.all = partial( # attrs confuses pylint: disable=attribute-defined-outside-init @@ -95,7 +120,7 @@ def _transform_table(self, method, **kwargs): ) -@attr.s +@attr.s(init=False) class EncryptedResource(object): # pylint: disable=too-few-public-methods """High-level helper class to provide a familiar interface to encrypted tables. @@ -142,6 +167,28 @@ class EncryptedResource(object): default=True ) + def __init__( + self, + resource, # type: ServiceResource + materials_provider, # type: CryptographicMaterialsProvider + attribute_actions=None, # type: Optional[AttributeActions] + auto_refresh_table_indexes=True # type: Optional[bool] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if attribute_actions is None: + attribute_actions = AttributeActions() + + self._resource = resource + self._materials_provider = materials_provider + self._attribute_actions = attribute_actions + self._auto_refresh_table_indexes = auto_refresh_table_indexes + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Set up the table info cache, encrypted tables collection manager, and translation methods.""" self._table_info_cache = TableInfoCache( # attrs confuses pylint: disable=attribute-defined-outside-init diff --git a/src/dynamodb_encryption_sdk/encrypted/table.py b/src/dynamodb_encryption_sdk/encrypted/table.py index 38a7af7b..fa87d575 100644 --- a/src/dynamodb_encryption_sdk/encrypted/table.py +++ b/src/dynamodb_encryption_sdk/encrypted/table.py @@ -17,6 +17,12 @@ from boto3.dynamodb.table import BatchWriter from boto3.resources.base import ServiceResource +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Optional # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.internal.utils import ( crypto_config_from_kwargs, crypto_config_from_table_info, decrypt_get_item, decrypt_multi_get, encrypt_put_item @@ -29,7 +35,7 @@ __all__ = ('EncryptedTable',) -@attr.s +@attr.s(init=False) class EncryptedTable(object): # pylint: disable=too-few-public-methods """High-level helper class to provide a familiar interface to encrypted tables. @@ -86,6 +92,30 @@ class EncryptedTable(object): default=True ) + def __init__( + self, + table, # type: ServiceResource + materials_provider, # type: CryptographicMaterialsProvider + table_info=None, # type: Optional[TableInfo] + attribute_actions=None, # type: Optional[AttributeActions] + auto_refresh_table_indexes=True # type: Optional[bool] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if attribute_actions is None: + attribute_actions = AttributeActions() + + self._table = table + self._materials_provider = materials_provider + self._table_info = table_info + self._attribute_actions = attribute_actions + self._auto_refresh_table_indexes = auto_refresh_table_indexes + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Prepare table info is it was not set and set up translation methods.""" if self._table_info is None: diff --git a/src/dynamodb_encryption_sdk/exceptions.py b/src/dynamodb_encryption_sdk/exceptions.py index 1191f09f..d845adfd 100644 --- a/src/dynamodb_encryption_sdk/exceptions.py +++ b/src/dynamodb_encryption_sdk/exceptions.py @@ -87,3 +87,19 @@ class SigningError(DynamodbEncryptionSdkError): class SignatureVerificationError(DynamodbEncryptionSdkError): """Otherwise undifferentiated error encountered while verifying a signature.""" + + +class ProviderStoreError(DynamodbEncryptionSdkError): + """Otherwise undifferentiated error encountered by a provider store.""" + + +class NoKnownVersionError(ProviderStoreError): + """Raised if a provider store cannot locate any version of the requested material.""" + + +class InvalidVersionError(ProviderStoreError): + """Raised if an invalid version of a material is requested.""" + + +class VersionAlreadyExistsError(ProviderStoreError): + """Raised if a version that is being added to a provider store already exists.""" diff --git a/src/dynamodb_encryption_sdk/internal/crypto/authentication.py b/src/dynamodb_encryption_sdk/internal/crypto/authentication.py index 4cd77083..61844ee9 100644 --- a/src/dynamodb_encryption_sdk/internal/crypto/authentication.py +++ b/src/dynamodb_encryption_sdk/internal/crypto/authentication.py @@ -14,6 +14,13 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Text # noqa pylint: disable=unused-import + from dynamodb_encryption_sdk.internal import dynamodb_types # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.delegated_keys import DelegatedKey # noqa pylint: disable=unused-import from dynamodb_encryption_sdk.encrypted import CryptoConfig # noqa pylint: disable=unused-import from dynamodb_encryption_sdk.identifiers import CryptoAction @@ -71,7 +78,7 @@ def verify_item_signature(signature_attribute, encrypted_item, verification_key, def _string_to_sign(item, table_name, attribute_actions): - # type: (dynamodb_type.ITEM, Text, AttributeActions) -> bytes + # type: (dynamodb_types.ITEM, Text, AttributeActions) -> bytes """Generate the string to sign from an encrypted item and configuration. :param dict item: Encrypted DynamoDB item diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py index fa587bbc..973375c4 100644 --- a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py @@ -21,7 +21,7 @@ import six try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Text # noqa pylint: disable=unused-import + from typing import Any, Callable, Text # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -87,7 +87,7 @@ def verify(self, key, signature, data): """ -@attr.s +@attr.s(init=False) class JavaMac(JavaAuthenticator): """Symmetric MAC authenticators. @@ -99,6 +99,22 @@ class JavaMac(JavaAuthenticator): algorithm_type = attr.ib(validator=callable_validator) hash_type = attr.ib(validator=callable_validator) + def __init__( + self, + java_name, # type: Text + algorithm_type, # type: Callable + hash_type # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.algorithm_type = algorithm_type + self.hash_type = hash_type + attr.validate(self) + def _build_hmac_signer(self, key): # type: (bytes) -> Any """Build HMAC signer using instance algorithm and hash type and ``key``. @@ -182,7 +198,7 @@ def verify(self, key, signature, data): raise SignatureVerificationError(message) -@attr.s +@attr.s(init=False) class JavaSignature(JavaAuthenticator): """Asymmetric signature authenticators. @@ -195,6 +211,24 @@ class JavaSignature(JavaAuthenticator): hash_type = attr.ib(validator=callable_validator) padding_type = attr.ib(validator=callable_validator) + def __init__( + self, + java_name, # type: Text + algorithm_type, + hash_type, # type: Callable + padding_type # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.algorithm_type = algorithm_type + self.hash_type = hash_type + self.padding_type = padding_type + attr.validate(self) + def validate_algorithm(self, algorithm): # type: (Text) -> None """Determine whether the requested algorithm name is compatible with this authenticator. diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py index fa23e03f..4dbf32be 100644 --- a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py @@ -21,7 +21,7 @@ __all__ = ('JavaCipher',) -@attr.s +@attr.s(init=False) class JavaCipher(object): """Defines the encryption cipher, mode, and padding type to use for encryption. @@ -36,6 +36,22 @@ class JavaCipher(object): mode = attr.ib(validator=attr.validators.instance_of(JavaMode)) padding = attr.ib(validator=attr.validators.instance_of(JavaPadding)) + def __init__( + self, + cipher, # type: JavaEncryptionAlgorithm + mode, # type: JavaMode + padding # type: JavaPadding + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.cipher = cipher + self.mode = mode + self.padding = padding + attr.validate(self) + def encrypt(self, key, data): """Encrypt data using loaded key. diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py index 675349cc..9519cc86 100644 --- a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py @@ -22,6 +22,12 @@ from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes import six +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Callable, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.exceptions import ( DecryptionError, EncryptionError, InvalidAlgorithmError, UnwrappingError, WrappingError ) @@ -89,7 +95,7 @@ def build(self, block_size): """Build an instance of this padding type.""" -@attr.s +@attr.s(init=False) class SimplePadding(JavaPadding): # pylint: disable=too-few-public-methods """Padding types that do not require any preparation.""" @@ -97,8 +103,22 @@ class SimplePadding(JavaPadding): java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) padding = attr.ib(validator=callable_validator) + def __init__( + self, + java_name, # type: Text + padding # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.padding = padding + attr.validate(self) + def build(self, block_size=None): - # type: (int) -> ANY + # type: (int) -> Any """Build an instance of this padding type. :param int block_size: Not used by SimplePadding. Ignored and not required. @@ -107,7 +127,7 @@ def build(self, block_size=None): return self.padding() -@attr.s +@attr.s(init=False) class BlockSizePadding(JavaPadding): # pylint: disable=too-few-public-methods """Padding types that require a block size input.""" @@ -115,8 +135,22 @@ class BlockSizePadding(JavaPadding): java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) padding = attr.ib(validator=callable_validator) + def __init__( + self, + java_name, # type: Text + padding # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.padding = padding + attr.validate(self) + def build(self, block_size): - # type: (int) -> ANY + # type: (int) -> Any """Build an instance of this padding type. :param int block_size: Block size of algorithm for which to build padder. @@ -125,7 +159,7 @@ def build(self, block_size): return self.padding(block_size) -@attr.s +@attr.s(init=False) class OaepPadding(JavaPadding): # pylint: disable=too-few-public-methods """OAEP padding types. These require more complex setup. @@ -143,8 +177,28 @@ class OaepPadding(JavaPadding): mgf = attr.ib(validator=callable_validator) mgf_digest = attr.ib(validator=callable_validator) + def __init__( + self, + java_name, # type: Text + padding, # type: Callable + digest, # type: Callable + mgf, # type: Callable + mgf_digest # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.padding = padding + self.digest = digest + self.mgf = mgf + self.mgf_digest = mgf_digest + attr.validate(self) + def build(self, block_size=None): - # type: (int) -> ANY + # type: (int) -> Any """Build an instance of this padding type. :param int block_size: Not used by OaepPadding. Ignored and not required. @@ -157,7 +211,7 @@ def build(self, block_size=None): ) -@attr.s +@attr.s(init=False) class JavaMode(object): # pylint: disable=too-few-public-methods """Bridge the gap from the Java encryption mode names and Python resources. @@ -167,8 +221,22 @@ class JavaMode(object): java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) mode = attr.ib(validator=callable_validator) + def __init__( + self, + java_name, # type: Text + mode # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.mode = mode + attr.validate(self) + def build(self, iv): - # type: (int) -> ANY + # type: (int) -> Any """Build an instance of this mode type. :param bytes iv: Initialization vector bytes @@ -177,7 +245,7 @@ def build(self, iv): return self.mode(iv) -@attr.s +@attr.s(init=False) class JavaEncryptionAlgorithm(object): # pylint: disable=too-few-public-methods """Bridge the gap from the Java encryption algorithm names and Python resources. @@ -187,6 +255,24 @@ class JavaEncryptionAlgorithm(object): java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) cipher = attr.ib() + def __init__( + self, + java_name, # type: Text + cipher # type: Callable + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.java_name = java_name + self.cipher = cipher + attr.validate(self) + self.__attrs_post_init__() + + def __attrs_post_init__(self): + """No-op stub to standardize API.""" + def validate_algorithm(self, algorithm): # type: (Text) -> None """Determine whether the requested algorithm name is compatible with this cipher""" diff --git a/src/dynamodb_encryption_sdk/internal/dynamodb_types.py b/src/dynamodb_encryption_sdk/internal/dynamodb_types.py index abac1d3b..fd87ae92 100644 --- a/src/dynamodb_encryption_sdk/internal/dynamodb_types.py +++ b/src/dynamodb_encryption_sdk/internal/dynamodb_types.py @@ -1,14 +1,14 @@ """Types used with mypy for DynamoDB items and attributes.""" try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Any, ByteString, Dict, List, Text, Union + from typing import Any, AnyStr, ByteString, Dict, List, Text - ATTRIBUTE = Any # TODO: narrow this down + ATTRIBUTE = Dict[Text, Any] # TODO: narrow this down ITEM = Dict[Text, ATTRIBUTE] RAW_ATTRIBUTE = ITEM NULL = bool # DynamoDB TypeSerializer converts none to {'NULL': True} BOOLEAN = bool NUMBER = int # TODO: This misses long on Python 2...figure out something for this - STRING = Union[Text, Text] # TODO: can be unicode but should not be bytes + STRING = AnyStr # TODO: can be unicode but should not be bytes BINARY = ByteString BINARY_ATTRIBUTE = Dict[Text, BINARY] SET = List # DynamoDB TypeSerializer converts sets into lists diff --git a/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py b/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py index 6af16605..b4f1327c 100644 --- a/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py +++ b/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py @@ -18,7 +18,7 @@ import struct try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Callable, Dict, List, Union # noqa pylint: disable=unused-import + from typing import Callable, Dict, List, Text, Union # noqa pylint: disable=unused-import from dynamodb_encryption_sdk.internal import dynamodb_types # noqa pylint: disable=unused-import,ungrouped-imports except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks @@ -52,7 +52,7 @@ def _transform_binary_value(value): return value def _deserialize_binary(stream): - # type: (io.BytesIO) -> Dict[str, bytes] + # type: (io.BytesIO) -> Dict[Text, bytes] """Deserializes a binary object. :param stream: Stream containing serialized object @@ -72,7 +72,7 @@ def _transform_string_value(value): return codecs.decode(value, TEXT_ENCODING) def _deserialize_string(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.STRING] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.STRING] """Deserializes a string object. :param stream: Stream containing serialized object @@ -94,7 +94,7 @@ def _transform_number_value(value): return '{0:f}'.format(decimal_value) def _deserialize_number(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.STRING] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.STRING] """Deserializes a number object. :param stream: Stream containing serialized object @@ -110,7 +110,7 @@ def _deserialize_number(stream): } def _deserialize_boolean(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.BOOLEAN] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.BOOLEAN] """Deserializes a boolean object. :param stream: Stream containing serialized object @@ -121,7 +121,7 @@ def _deserialize_boolean(stream): return {Tag.BOOLEAN.dynamodb_tag: _boolean_map[value]} def _deserialize_null(stream): # we want a consistent API but don't use stream, so pylint: disable=unused-argument - # type: (io.BytesIO) -> Dict[str, dynamodb_types.BOOLEAN] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.BOOLEAN] """Deserializes a null object. :param stream: Stream containing serialized object @@ -145,7 +145,7 @@ def _deserialize_set(stream, member_transform): ]) def _deserialize_binary_set(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.SET[dynamodb_types.BINARY]] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.SET[dynamodb_types.BINARY]] """Deserializes a binary set object. :param stream: Stream containing serialized object @@ -155,7 +155,7 @@ def _deserialize_binary_set(stream): return {Tag.BINARY_SET.dynamodb_tag: _deserialize_set(stream, _transform_binary_value)} def _deserialize_string_set(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.SET[dynamodb_types.STRING]] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.SET[dynamodb_types.STRING]] """Deserializes a string set object. :param stream: Stream containing serialized object @@ -165,7 +165,7 @@ def _deserialize_string_set(stream): return {Tag.STRING_SET.dynamodb_tag: _deserialize_set(stream, _transform_string_value)} def _deserialize_number_set(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.SET[dynamodb_types.STRING]] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.SET[dynamodb_types.STRING]] """Deserializes a number set object. :param stream: Stream containing serialized object @@ -175,7 +175,7 @@ def _deserialize_number_set(stream): return {Tag.NUMBER_SET.dynamodb_tag: _deserialize_set(stream, _transform_number_value)} def _deserialize_list(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.LIST] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.LIST] """Deserializes a list object. :param stream: Stream containing serialized object @@ -189,7 +189,7 @@ def _deserialize_list(stream): ]} def _deserialize_map(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.MAP] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.MAP] """Deserializes a map object. :param stream: Stream containing serialized object @@ -197,7 +197,7 @@ def _deserialize_map(stream): :rtype: dict """ member_count = decode_length(stream) - members = {} + members = {} # type: dynamodb_types.MAP for _ in range(member_count): key = _deserialize(stream) if Tag.STRING.dynamodb_tag not in key: @@ -236,7 +236,7 @@ def _deserialize_function(tag): raise DeserializationError('Unsupported tag: "{}"'.format(tag)) def _deserialize(stream): - # type: (io.BytesIO) -> Dict[str, dynamodb_types.RAW_ATTRIBUTE] + # type: (io.BytesIO) -> Dict[Text, dynamodb_types.RAW_ATTRIBUTE] """Deserializes a serialized object. :param stream: Stream containing serialized object diff --git a/src/dynamodb_encryption_sdk/internal/formatting/material_description.py b/src/dynamodb_encryption_sdk/internal/formatting/material_description.py index ed3d76fa..3abeab8a 100644 --- a/src/dynamodb_encryption_sdk/internal/formatting/material_description.py +++ b/src/dynamodb_encryption_sdk/internal/formatting/material_description.py @@ -15,6 +15,13 @@ import logging import struct +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Text # noqa pylint: disable=unused-import + from dynamodb_encryption_sdk.internal import dynamodb_types # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.exceptions import InvalidMaterialDescriptionError, InvalidMaterialDescriptionVersionError from dynamodb_encryption_sdk.identifiers import LOGGER_NAME from dynamodb_encryption_sdk.internal.identifiers import Tag diff --git a/src/dynamodb_encryption_sdk/internal/identifiers.py b/src/dynamodb_encryption_sdk/internal/identifiers.py index d0acd67a..3abbb970 100644 --- a/src/dynamodb_encryption_sdk/internal/identifiers.py +++ b/src/dynamodb_encryption_sdk/internal/identifiers.py @@ -14,7 +14,7 @@ from enum import Enum try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Text # noqa pylint: disable=unused-import + from typing import Optional, Text # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass diff --git a/src/dynamodb_encryption_sdk/internal/utils.py b/src/dynamodb_encryption_sdk/internal/utils.py index b188424f..880b52de 100644 --- a/src/dynamodb_encryption_sdk/internal/utils.py +++ b/src/dynamodb_encryption_sdk/internal/utils.py @@ -20,7 +20,7 @@ from dynamodb_encryption_sdk.structures import EncryptionContext, TableInfo try: # Python 3.5.0 and 3.5.1 have incompatible typing modules - from typing import Callable, Dict, Text # noqa pylint: disable=unused-import + from typing import Any, Callable, Dict, Text # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -50,7 +50,7 @@ def sorted_key_map(item, transform=to_bytes): return sorted_items -@attr.s +@attr.s(init=False) class TableInfoCache(object): # pylint: disable=too-few-public-methods """Very simple cache of TableInfo objects, providing configuration information about DynamoDB tables. @@ -64,6 +64,21 @@ class TableInfoCache(object): _client = attr.ib(validator=attr.validators.instance_of(botocore.client.BaseClient)) _auto_refresh_table_indexes = attr.ib(validator=attr.validators.instance_of(bool)) + def __init__( + self, + client, # type: botocore.client.BaseClient + auto_refresh_table_indexes # type: bool + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self._client = client + self._auto_refresh_table_indexes = auto_refresh_table_indexes + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Set up the empty cache.""" self._all_tables_info = {} # type: Dict[Text, TableInfo] # pylint: disable=attribute-defined-outside-init diff --git a/src/dynamodb_encryption_sdk/material_providers/__init__.py b/src/dynamodb_encryption_sdk/material_providers/__init__.py index 82735d17..56799235 100644 --- a/src/dynamodb_encryption_sdk/material_providers/__init__.py +++ b/src/dynamodb_encryption_sdk/material_providers/__init__.py @@ -11,7 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Cryptographic materials providers.""" -from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials # noqa pylint: disable=unused-import +from dynamodb_encryption_sdk.materials import CryptographicMaterials # noqa pylint: disable=unused-import from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import __all__ = ('CryptographicMaterialsProvider',) @@ -21,7 +21,7 @@ class CryptographicMaterialsProvider(object): """Base class for all cryptographic materials providers.""" def decryption_materials(self, encryption_context): - # type: (EncryptionContext) -> DecryptionMaterials + # type: (EncryptionContext) -> CryptographicMaterials # pylint: disable=unused-argument,no-self-use """Return decryption materials. @@ -32,7 +32,7 @@ def decryption_materials(self, encryption_context): raise AttributeError('No decryption materials available') def encryption_materials(self, encryption_context): - # type: (EncryptionContext) -> EncryptionMaterials + # type: (EncryptionContext) -> CryptographicMaterials # pylint: disable=unused-argument,no-self-use """Return encryption materials. diff --git a/src/dynamodb_encryption_sdk/material_providers/aws_kms.py b/src/dynamodb_encryption_sdk/material_providers/aws_kms.py index 5c7a9f65..3b42be9f 100644 --- a/src/dynamodb_encryption_sdk/material_providers/aws_kms.py +++ b/src/dynamodb_encryption_sdk/material_providers/aws_kms.py @@ -27,6 +27,7 @@ try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from dynamodb_encryption_sdk.internal import dynamodb_types # noqa pylint: disable=unused-import + from typing import Dict, Optional, Text, Tuple # noqa pylint: disable=unused-import except ImportError: # pragma: no cover # We only actually need these imports when running the mypy checks pass @@ -69,7 +70,7 @@ class EncryptionContextKeys(Enum): TABLE_NAME = '*aws-kms-table*' -@attr.s +@attr.s(init=False) class KeyInfo(object): # pylint: disable=too-few-public-methods """Identifying information for a specific key and how it should be used. @@ -83,9 +84,25 @@ class KeyInfo(object): algorithm = attr.ib(validator=attr.validators.instance_of(six.string_types)) length = attr.ib(validator=attr.validators.instance_of(six.integer_types)) + def __init__( + self, + description, # type: Text + algorithm, # type: Text + length # type: int + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.description = description + self.algorithm = algorithm + self.length = length + attr.validate(self) + @classmethod def from_description(cls, description, default_key_length=None): - # type: (Text) -> KeyInfo + # type: (Text, Optional[int]) -> KeyInfo """Load key info from key info description. :param str description: Key info description @@ -122,7 +139,7 @@ def from_material_description(cls, material_description, description_key, defaul return cls.from_description(description, default_key_length) -@attr.s +@attr.s(init=False) class AwsKmsCryptographicMaterialsProvider(CryptographicMaterialsProvider): """Cryptographic materials provider for use with the AWS Key Management Service (KMS). @@ -159,6 +176,37 @@ class AwsKmsCryptographicMaterialsProvider(CryptographicMaterialsProvider): default=attr.Factory(dict) ) + def __init__( + self, + key_id, # type: Text + botocore_session=None, # type: Optional[botocore.session.Session] + grant_tokens=None, # type: Optional[Tuple[Text]] + material_description=None, # type: Optional[Dict[Text, Text]] + regional_clients=None # type: Optional[Dict[Text, botocore.client.BaseClient]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if botocore_session is None: + botocore_session = botocore.session.Session() + if grant_tokens is None: + # reassignment confuses mypy + grant_tokens = () # type: ignore + if material_description is None: + material_description = {} + if regional_clients is None: + regional_clients = {} + + self._key_id = key_id + self._botocore_session = botocore_session + self._grant_tokens = grant_tokens + self._material_description = material_description + self._regional_clients = regional_clients + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): # type: () -> None """Load the content and signing key info.""" @@ -230,7 +278,7 @@ def _select_key_id(self, encryption_context): return self._key_id def _validate_key_id(self, key_id, encryption_context): - # type: (EncryptionContext) -> None + # type: (Text, EncryptionContext) -> None # pylint: disable=unused-argument,no-self-use """Validate the selected key id. @@ -245,7 +293,7 @@ def _validate_key_id(self, key_id, encryption_context): """ def _attribute_to_value(self, attribute): - # type: (dynamodb_types.ITEM) -> str + # type: (dynamodb_types.ITEM) -> Text """Convert a DynamoDB attribute to a value that can be added to the KMS encryption context. :param dict attribute: Attribute to convert @@ -260,7 +308,7 @@ def _attribute_to_value(self, attribute): raise ValueError('Attribute of type "{}" cannot be used in KMS encryption context.'.format(attribute_type)) def _kms_encryption_context(self, encryption_context, encryption_description, signing_description): - # type: (EncryptionContext, Text, Text) -> Dict[str, str] + # type: (EncryptionContext, Text, Text) -> Dict[Text, Text] """Build the KMS encryption context from the encryption context and key descriptions. :param encryption_context: Encryption context providing information about request @@ -299,7 +347,7 @@ def _kms_encryption_context(self, encryption_context, encryption_description, si return kms_encryption_context def _generate_initial_material(self, encryption_context): - # type: () -> (bytes, bytes) + # type: (EncryptionContext) -> Tuple[bytes, bytes] """Generate the initial cryptographic material for use with HKDF. :param encryption_context: Encryption context providing information about request @@ -332,7 +380,7 @@ def _generate_initial_material(self, encryption_context): raise WrappingError(message) def _decrypt_initial_material(self, encryption_context): - # type: () -> bytes + # type: (EncryptionContext) -> bytes """Decrypt an encrypted initial cryptographic material value. :param encryption_context: Encryption context providing information about request diff --git a/src/dynamodb_encryption_sdk/material_providers/most_recent.py b/src/dynamodb_encryption_sdk/material_providers/most_recent.py new file mode 100644 index 00000000..22dcd1c4 --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/most_recent.py @@ -0,0 +1,298 @@ +# Copyright 2018 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. +"""Cryptographic materials provider that uses a provider store to obtain cryptographic materials.""" +from collections import OrderedDict +from enum import Enum +import logging +from threading import Lock, RLock +import time + +import attr +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError +from dynamodb_encryption_sdk.identifiers import LOGGER_NAME +from dynamodb_encryption_sdk.materials import CryptographicMaterials # noqa pylint: disable=unused-import +from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import +from . import CryptographicMaterialsProvider +from .store import ProviderStore + +__all__ = ('MostRecentProvider',) +_LOGGER = logging.getLogger(LOGGER_NAME) +#: Grace period during which we will return the latest local materials. This allows multiple +#: threads to be using this same provider without risking lock contention or many threads +#: all attempting to create new versions simultaneously. +_GRACE_PERIOD = 0.5 + + +class TtlActions(Enum): + """Actions that can be taken based on the version TTl state.""" + EXPIRED = 0 + GRACE_PERIOD = 1 + LIVE = 2 + + +def _min_capacity_validator(instance, attribute, value): + """Attrs validator to require that value is at least 1.""" + if value < 1: + raise ValueError('Cache capacity must be at least 1') + + +@attr.s(init=False) +class BasicCache(object): + """Most basic LRU cache.""" + + capacity = attr.ib(validator=( + attr.validators.instance_of(int), + _min_capacity_validator + )) + + def __init__(self, capacity): + # type: (int) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.capacity = capacity + attr.validate(self) + self.__attrs_post_init__() + + def __attrs_post_init__(self): + # type; () -> None + """Initialize the internal cache.""" + self._cache_lock = RLock() # attrs confuses pylint: disable=attribute-defined-outside-init + self.clear() + + def _prune(self): + # type: () -> None + """Prunes internal cache until internal cache is within the defined limit.""" + with self._cache_lock: + while len(self._cache) > self.capacity: + self._cache.popitem(last=False) + + def put(self, name, value): + # type: (Any, Any) -> None + """Add a value to the cache. + + :param name: Hashable object to identify the value in the cache + :param value: Value to add to cache + """ + with self._cache_lock: + self._cache[name] = value + self._prune() + + def get(self, name): + # type: (Any) -> Any + """Get a value from the cache.""" + with self._cache_lock: + value = self._cache.pop(name) + self.put(name, value) # bump to the from of the LRU + return value + + def clear(self): + # type: () -> None + """Clear the cache.""" + with self._cache_lock: + self._cache = OrderedDict() # type: OrderedDict[Any, Any] + + +@attr.s(init=False) +class MostRecentProvider(CryptographicMaterialsProvider): + """Cryptographic materials provider that uses a provider store to obtain cryptography + materials. + + When encrypting, the most recent provider that the provider store knows about will always + be used. + + :param provider_store: Provider store to use + :type provider_store: dynamodb_encryption_sdk.material_providers.store.ProviderStore + :param str material_name: Name of materials for which to ask the provider store + :param float version_ttl: Max time in seconds to go until checking with provider store + for a more recent version + """ + + _provider_store = attr.ib(validator=attr.validators.instance_of(ProviderStore)) + _material_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + _version_ttl = attr.ib(validator=attr.validators.instance_of(float)) + + def __init__( + self, + provider_store, # type: ProviderStore + material_name, # type: Text + version_ttl # type: float + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self._provider_store = provider_store + self._material_name = material_name + self._version_ttl = version_ttl + attr.validate(self) + self.__attrs_post_init__() + + def __attrs_post_init__(self): + # type: () -> None + """Initialize the cache.""" + self._lock = Lock() + self._cache = BasicCache(1000) + self.refresh() + + def decryption_materials(self, encryption_context): + # type: (EncryptionContext) -> CryptographicMaterials + """Return decryption materials. + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :raises AttributeError: if no decryption materials are available + """ + version = self._provider_store.version_from_material_description(encryption_context.material_description) + try: + provider = self._cache.get(version) + except KeyError: + try: + provider = self._provider_store.provider(self._material_name, version) + except InvalidVersionError: + _LOGGER.exception('Unable to get decryption materials from provider store.') + raise AttributeError('No decryption materials available') + + self._cache.put(version, provider) + + return provider.decryption_materials(encryption_context) + + def _ttl_action(self): + # type: () -> bool + """Determine the correct action to take based on the local resources and TTL. + + :returns: decision + :rtype: TtlActions + """ + if self._version is None: + return TtlActions.EXPIRED + + time_since_updated = time.time() - self._last_updated + + if time_since_updated < self._version_ttl: + return TtlActions.LIVE + + elif time_since_updated < self._version_ttl + _GRACE_PERIOD: + return TtlActions.GRACE_PERIOD + + return TtlActions.EXPIRED + + def _get_max_version(self): + # type: () -> int + """Ask the provider store for the most recent version of this material. + + :returns: Latest version in the provider store (0 if not found) + :rtype: int + """ + try: + return self._provider_store.max_version(self._material_name) + except NoKnownVersionError: + return 0 + + def _get_provider(self, version): + # type: (int) -> CryptographicMaterialsProvider + """Ask the provider for a specific version of this material. + + :param int version: Version to request + :returns: Cryptographic materials provider for the requested version + :rtype: CryptographicMaterialsProvider + :raises AttributeError: if provider could not locate version + """ + try: + return self._provider_store.get_or_create_provider(self._material_name, version) + except InvalidVersionError: + _LOGGER.exception('Unable to get encryption materials from provider store.') + raise AttributeError('No encryption materials available') + + def _get_most_recent_version(self, allow_local): + # type: (bool) -> CryptographicMaterialsProvider + """Get the most recent version of the provider. + + If allowing local and we cannot obtain the lock, just return the most recent local + version. Otherwise, wait for the lock and ask the provider store for the most recent + version of the provider. + + :param bool allow_local: Should we allow returning the local version if we cannot obtain the lock? + :returns: version and corresponding cryptographic materials provider + :rtype: CryptographicMaterialsProvider + """ + acquired = self._lock.acquire(blocking=not allow_local) + + if not acquired: + # We failed to acquire the lock. + # If blocking, we will never reach this point. + # If not blocking, we want whatever the latest local version is. + version = self._version + return version, self._cache.get(version) + + try: + max_version = self._get_max_version() + try: + provider = self._cache.get(max_version) + except KeyError: + provider = self._get_provider(max_version) + received_version = self._provider_store.version_from_material_description(provider._material_description) + # TODO: ^ should we promote material description from hidden? + + self._version = received_version + self._last_updated = time.time() + self._cache.put(received_version, provider) + finally: + self._lock.release() + + return provider + + def encryption_materials(self, encryption_context): + # type: (EncryptionContext) -> CryptographicMaterials + """Return encryption materials. + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :raises AttributeError: if no encryption materials are available + """ + ttl_action = self._ttl_action() + + if ttl_action is TtlActions.LIVE: + try: + return self._cache.get(self._version) + except KeyError: + ttl_action = TtlActions.EXPIRED + + if ttl_action is TtlActions.GRACE_PERIOD: + # Just get the latest local version if we cannot acquire the lock. + allow_local = True + else: + # Block until we can acquire the lock. + allow_local = False + + provider = self._get_most_recent_version(allow_local) + + return provider.encryption_materials(encryption_context) + + def refresh(self): + # type: () -> None + """Clear all local caches for this provider.""" + with self._lock: + self._cache.clear() + self._version = None # type: int + self._last_updated = None # type: float diff --git a/src/dynamodb_encryption_sdk/material_providers/static.py b/src/dynamodb_encryption_sdk/material_providers/static.py index 3c1ab28a..f629e333 100644 --- a/src/dynamodb_encryption_sdk/material_providers/static.py +++ b/src/dynamodb_encryption_sdk/material_providers/static.py @@ -13,6 +13,13 @@ """Cryptographic materials provider for use with pre-configured encryption and decryption materials.""" import attr +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Optional # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from dynamodb_encryption_sdk.materials import CryptographicMaterials # noqa pylint: disable=unused-import from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import from . import CryptographicMaterialsProvider @@ -20,7 +27,7 @@ __all__ = ('StaticCryptographicMaterialsProvider',) -@attr.s +@attr.s(init=False) class StaticCryptographicMaterialsProvider(CryptographicMaterialsProvider): """Manually combine encryption and decryption materials for use as a cryptographic materials provider. @@ -39,8 +46,22 @@ class StaticCryptographicMaterialsProvider(CryptographicMaterialsProvider): default=None ) + def __init__( + self, + decryption_materials=None, # type: Optional[DecryptionMaterials] + encryption_materials=None # type: Optional[EncryptionMaterials] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self._decryption_materials = decryption_materials + self._encryption_materials = encryption_materials + attr.validate(self) + def decryption_materials(self, encryption_context): - # type: (EncryptionContext) -> DecryptionMaterials + # type: (EncryptionContext) -> CryptographicMaterials """Return the static decryption materials. :param encryption_context: Encryption context for request (not used by ``StaticCryptographicMaterialsProvider``) @@ -53,7 +74,7 @@ def decryption_materials(self, encryption_context): return self._decryption_materials def encryption_materials(self, encryption_context): - # type: (EncryptionContext) -> EncryptionMaterials + # type: (EncryptionContext) -> CryptographicMaterials """Return the static encryption materials. :param encryption_context: Encryption context for request (not used by ``StaticCryptographicMaterialsProvider``) diff --git a/src/dynamodb_encryption_sdk/material_providers/store/__init__.py b/src/dynamodb_encryption_sdk/material_providers/store/__init__.py new file mode 100644 index 00000000..3c2179e6 --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/store/__init__.py @@ -0,0 +1,101 @@ +# Copyright 2018 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. +"""Cryptographic materials provider stores.""" +import abc + +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Text, Optional # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError +from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider + +__all__ = ('ProviderStore',) + + +@six.add_metaclass(abc.ABCMeta) +class ProviderStore(object): + """Provide a standard way to retrieve and/or create cryptographic materials providers.""" + + @abc.abstractmethod + def get_or_create_provider(self, material_name, version): + # type: (Text, int) -> CryptographicMaterialsProvider + """Obtain a cryptographic materials provider identified by a name and version. + + If the requested version does not exist, a new one might be created. + + :param str material_name: Material to locate + :param int version: Version of material to locate (optional) + :returns: cryptographic materials provider + :rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + :raises InvalidVersionError: if the requested version is not available and cannot be created + """ + + @abc.abstractmethod + def version_from_material_description(self, material_description): + # (Dict[Text, Text]) -> int + """Determine the version from the provided material description. + + :param dict material_description: Material description to use with this request + :returns: version to use + :rtype: int + """ + + def max_version(self, material_name): + # (Text) -> int + """Find the maximum known version of the specified material. + + .. note:: + + Child classes should usually override this method. + + :param str material_name: Material to locate + :returns: Maximum known version + :rtype: int + :raises NoKnownVersion: if no version can be found + """ + raise NoKnownVersionError('No known version for name: "{}"'.format(material_name)) + + def provider(self, material_name, version=None): + # type: (Text, Optional[int]) -> CryptographicMaterialsProvider + """Obtain a cryptographic materials provider identified by a name and version. + + If the version is not provided, the maximum version will be used. + + :param str material_name: Material to locate + :param int version: Version of material to locate (optional) + :returns: cryptographic materials provider + :rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + :raises InvalidVersionError: if the requested version is not found + """ + if version is None: + try: + version = self.max_version(material_name) + except NoKnownVersionError: + version = 0 + return self.get_or_create_provider(material_name, version) + + def new_provider(self, material_name): + # type: (Text) -> CryptographicMaterialsProvider + """Create a new provider with a version one greater than the current known maximum. + + :param str material_name: Material to locate + :returns: cryptographic materials provider + :rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + """ + version = self.max_version(material_name) + 1 + return self.get_or_create_provider(material_name, version) diff --git a/src/dynamodb_encryption_sdk/material_providers/store/meta.py b/src/dynamodb_encryption_sdk/material_providers/store/meta.py new file mode 100644 index 00000000..8db064e5 --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/store/meta.py @@ -0,0 +1,347 @@ +# Copyright 2018 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. +"""Meta cryptographic provider store.""" +from enum import Enum + +import attr +from boto3.dynamodb.conditions import Attr, Key +from boto3.dynamodb.types import Binary +from boto3.resources.base import ServiceResource +import botocore + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Optional, Text, Tuple # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey +from dynamodb_encryption_sdk.encrypted.table import EncryptedTable +from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError, VersionAlreadyExistsError +from dynamodb_encryption_sdk.identifiers import EncryptionKeyType, KeyEncodingType +from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider +from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider +from . import ProviderStore + +__all__ = ('MetaStore',) + + +class MetaStoreAttributeNames(Enum): + """Names of attributes in the MetaStore table.""" + + PARTITION = 'N' + SORT = 'V' + INTEGRITY_ALGORITHM = 'intAlg' + INTEGRITY_KEY = 'int' + ENCRYPTION_ALGORITHM = 'encAlg' + ENCRYPTION_KEY = 'enc' + MATERIAL_TYPE_VERSION = 't' + + +class MetaStoreValues(Enum): + """Static values for use by MetaStore.""" + + INTEGRITY_ALGORITHM = 'HmacSHA256' + ENCRYPTION_ALGORITHM = 'AES' + MATERIAL_TYPE_VERSION = '0' + KEY_BITS = 256 + + +#: Field in material description to use for the MetaStore material name and version. +_MATERIAL_DESCRIPTION_META_FIELD = 'amzn-ddb-meta-id' + + +@attr.s(init=False) +class MetaStore(ProviderStore): + """Create and retrieve wrapped cryptographic materials providers, storing their cryptographic + materials using the provided encrypted table. + + :param table: Pre-configured boto3 DynamoDB Table object + :type table: boto3.resources.base.ServiceResource + :param materials_provider: Cryptographic materials provider to use + :type materials_provider: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + """ + + _table = attr.ib(validator=attr.validators.instance_of(ServiceResource)) + _materials_provider = attr.ib(validator=attr.validators.instance_of(CryptographicMaterialsProvider)) + + def __init__(self, table, materials_provider): + # type: (ServiceResource, CryptographicMaterialsProvider) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self._table = table + self._materials_provider = materials_provider + attr.validate(self) + self.__attrs_post_init__() + + def __attrs_post_init__(self): + # type: () -> None + """Prepare the encrypted table resource from the provided table and materials provider.""" + self._encrypted_table = EncryptedTable( # attrs confuses pylint: disable=attribute-defined-outside-init + table=self._table, + materials_provider=self._materials_provider + ) + + @classmethod + def create_table(cls, client, table_name, read_units, write_units): + # type: (botocore.client.BaseClient, Text, int, int) -> None + """Create the table for this MetaStore. + + :param table: Pre-configured boto3 DynamoDB client object + :type table: boto3.resources.base.BaseClient + :param str table_name: Name of table to create + :param int read_units: Read capacity units to provision + :param int write_units: Write capacity units to provision + """ + try: + client.create_table( + TableName=table_name, + AttributeDefinitions=[ + { + 'AttributeName': MetaStoreAttributeNames.PARTITION.value, + 'AttributeType': 'S' + }, + { + 'AttributeName': MetaStoreAttributeNames.SORT.value, + 'AttributeType': 'N' + } + ], + KeySchema=[ + { + 'AttributeName': MetaStoreAttributeNames.PARTITION.value, + 'KeyType': 'HASH' + }, + { + 'AttributeName': MetaStoreAttributeNames.SORT.value, + 'KeyType': 'RANGE' + } + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': read_units, + 'WriteCapacityUnits': write_units + } + ) + except botocore.exceptions.ClientError: + raise Exception('TODO: Could not create table') + + def _load_materials(self, material_name, version): + # type: (Text, int) -> Tuple[JceNameLocalDelegatedKey, JceNameLocalDelegatedKey] + """Load materials from table. + + :returns: Materials loaded into delegated keys + :rtype: tuple of JceNameLocalDelegatedKey + """ + key = { + MetaStoreAttributeNames.PARTITION.value: material_name, + MetaStoreAttributeNames.SORT.value: version + } + response = self._encrypted_table.get_item(Key=key) + try: + item = response['Item'] + except KeyError: + raise InvalidVersionError('Version not found: "{}#{}"'.format(material_name, version)) + + try: + encryption_key_kwargs = dict( + key=item[MetaStoreAttributeNames.ENCRYPTION_KEY.value].value, + algorithm=item[MetaStoreAttributeNames.ENCRYPTION_ALGORITHM.value], + key_type=EncryptionKeyType.SYMMETRIC, + key_encoding=KeyEncodingType.RAW + ) + signing_key_kwargs = dict( + key=item[MetaStoreAttributeNames.INTEGRITY_KEY.value].value, + algorithm=item[MetaStoreAttributeNames.INTEGRITY_ALGORITHM.value], + key_type=EncryptionKeyType.SYMMETRIC, + key_encoding=KeyEncodingType.RAW + ) + except KeyError: + raise Exception('TODO: Invalid record') + + # TODO: handle if the material type version is not in the item + if item[MetaStoreAttributeNames.MATERIAL_TYPE_VERSION.value] != MetaStoreValues.MATERIAL_TYPE_VERSION.value: + raise InvalidVersionError('Unsupported material type: "{}"'.format( + item[MetaStoreAttributeNames.MATERIAL_TYPE_VERSION.value] + )) + + encryption_key = JceNameLocalDelegatedKey(**encryption_key_kwargs) + signing_key = JceNameLocalDelegatedKey(**signing_key_kwargs) + return encryption_key, signing_key + + def _save_materials(self, material_name, version, encryption_key, signing_key): + # type: (Text, int, JceNameLocalDelegatedKey, JceNameLocalDelegatedKey) -> None + """Save materials to the table, raising an error if the version already exists. + + :param str material_name: Material to locate + :param int version: Version of material to locate + :raises VersionAlreadyExistsError: if the specified version already exists + """ + item = { + MetaStoreAttributeNames.PARTITION.value: material_name, + MetaStoreAttributeNames.SORT.value: version, + MetaStoreAttributeNames.MATERIAL_TYPE_VERSION.value: MetaStoreValues.MATERIAL_TYPE_VERSION.value, + MetaStoreAttributeNames.ENCRYPTION_ALGORITHM.value: encryption_key.algorithm, + MetaStoreAttributeNames.ENCRYPTION_KEY.value: Binary(encryption_key.key), + MetaStoreAttributeNames.INTEGRITY_ALGORITHM.value: signing_key.algorithm, + MetaStoreAttributeNames.INTEGRITY_KEY.value: Binary(signing_key.key) + } + try: + self._encrypted_table.put_item( + Item=item, + ConditionExpression=( + Attr(MetaStoreAttributeNames.PARTITION.value).not_exists() & + Attr(MetaStoreAttributeNames.SORT.value).not_exists() + ) + ) + except botocore.exceptions.ClientError as error: + if error.response['Error']['Code'] == 'ConditionalCheckFailedException': + raise VersionAlreadyExistsError('Version already exists: "{}#{}"'.format(material_name, version)) + + def _save_or_load_materials( + self, + material_name, # type: Text + version, # type: int + encryption_key, # type: JceNameLocalDelegatedKey + signing_key # type: JceNameLocalDelegatedKey + ): + # type: (...) -> Tuple[JceNameLocalDelegatedKey, JceNameLocalDelegatedKey] + """Attempt to save the materials to the table. + + If the specified version already exists, the existing materials will be loaded from + the table and returned. Otherwise, the provided materials will be returned. + + :param str material_name: Material to locate + :param int version: Version of material to locate + :param encryption_key: Loaded encryption key + :type encryption_key: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey + :param signing_key: Loaded signing key + :type signing_key: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey + """ + try: + self._save_materials(material_name, version, encryption_key, signing_key) + return encryption_key, signing_key + except VersionAlreadyExistsError: + return self._load_materials(material_name, version) + + @staticmethod + def _material_description(material_name, version): + # type: (Text, int) -> Dict[Text, Text] + """Build a material description from a material name and version. + + :param str material_name: Material to locate + :param int version: Version of material to locate + """ + return {_MATERIAL_DESCRIPTION_META_FIELD: '{name}#{version}'.format(name=material_name, version=version)} + + def _load_provider_from_table(self, material_name, version): + # type: (Text, int) -> CryptographicMaterialsProvider + """Load a provider from the table. + + If the requested version does not exist, an error will be raised. + + :param str material_name: Material to locate + :param int version: Version of material to locate + """ + encryption_key, signing_key = self._load_materials(material_name, version) + return WrappedCryptographicMaterialsProvider( + signing_key=signing_key, + wrapping_key=encryption_key, + unwrapping_key=encryption_key, + material_description=self._material_description(material_name, version) + ) + + def get_or_create_provider(self, material_name, version): + # type: (Text, int) -> CryptographicMaterialsProvider + """Obtain a cryptographic materials provider identified by a name and version. + + If the requested version does not exist, a new one will be created. + + :param str material_name: Material to locate + :param int version: Version of material to locate + :returns: cryptographic materials provider + :rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + :raises InvalidVersionError: if the requested version is not available and cannot be created + """ + encryption_key = JceNameLocalDelegatedKey.generate( + MetaStoreValues.ENCRYPTION_ALGORITHM.value, + MetaStoreValues.KEY_BITS.value + ) + signing_key = JceNameLocalDelegatedKey.generate( + MetaStoreValues.INTEGRITY_ALGORITHM.value, + MetaStoreValues.KEY_BITS.value + ) + encryption_key, signing_key = self._save_or_load_materials(material_name, version, encryption_key, signing_key) + return WrappedCryptographicMaterialsProvider( + signing_key=signing_key, + wrapping_key=encryption_key, + unwrapping_key=encryption_key, + material_description=self._material_description(material_name, version) + ) + + def provider(self, material_name, version=None): + # type: (Text, Optional[int]) -> CryptographicMaterialsProvider + """Obtain a cryptographic materials provider identified by a name and version. + + If the version is provided, an error will be raised if that version is not found. + + If the version is not provided, the maximum version will be used. + + :param str material_name: Material to locate + :param int version: Version of material to locate (optional) + :returns: cryptographic materials provider + :rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + :raises InvalidVersionError: if the requested version is not found + """ + if version is not None: + return self._load_provider_from_table(material_name, version) + + return super(MetaStore, self).provider(material_name, version) + + def version_from_material_description(self, material_description): + # (Dict[Text, Text]) -> int + """Determine the version from the provided material description. + + :param dict material_description: Material description to use with this request + :returns: version to use + :rtype: int + """ + try: + info = material_description[_MATERIAL_DESCRIPTION_META_FIELD] + except KeyError: + raise Exception('TODO: No info found') + + try: + return int(info.split('#', 1)[1]) + except (IndexError, ValueError): + raise Exception('TODO: Malformed info') + + def max_version(self, material_name): + # (Text) -> int + """Find the maximum known version of the specified material. + + :param str material_name: Material to locate + :returns: Maximum known version + :rtype: int + :raises NoKnownVersion: if no version can be found + """ + response = self._encrypted_table.query( + KeyConditionExpression=Key(MetaStoreAttributeNames.PARTITION.value).eq(material_name), + ScanIndexForward=False, + Limit=1 + ) + + if not response['Items']: + raise NoKnownVersionError('No known version for name: "{}"'.format(material_name)) + + return int(response['Items'][0][MetaStoreAttributeNames.SORT.value]) diff --git a/src/dynamodb_encryption_sdk/material_providers/wrapped.py b/src/dynamodb_encryption_sdk/material_providers/wrapped.py index 96c3c025..2263c870 100644 --- a/src/dynamodb_encryption_sdk/material_providers/wrapped.py +++ b/src/dynamodb_encryption_sdk/material_providers/wrapped.py @@ -12,9 +12,17 @@ # language governing permissions and limitations under the License. """Cryptographic materials provider to use ephemeral content encryption keys wrapped by delegated keys.""" import attr +import six + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Optional, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass from dynamodb_encryption_sdk.delegated_keys import DelegatedKey from dynamodb_encryption_sdk.exceptions import UnwrappingError, WrappingError +from dynamodb_encryption_sdk.internal.validators import dictionary_validator from dynamodb_encryption_sdk.materials.wrapped import WrappedCryptographicMaterials from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import from . import CryptographicMaterialsProvider @@ -22,7 +30,7 @@ __all__ = ('WrappedCryptographicMaterialsProvider',) -@attr.s +@attr.s(init=False) class WrappedCryptographicMaterialsProvider(CryptographicMaterialsProvider): """Cryptographic materials provider to use ephemeral content encryption keys wrapped by delegated keys. @@ -53,6 +61,31 @@ class WrappedCryptographicMaterialsProvider(CryptographicMaterialsProvider): validator=attr.validators.optional(attr.validators.instance_of(DelegatedKey)), default=None ) + _material_description = attr.ib( + validator=attr.validators.optional(dictionary_validator(six.string_types, six.string_types)), + default=attr.Factory(dict) + ) + + def __init__( + self, + signing_key, # type: DelegatedKey + wrapping_key=None, # type: Optional[DelegatedKey] + unwrapping_key=None, # type: Optional[DelegatedKey] + material_description=None # type: Optional[Dict[Text, Text]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if material_description is None: + material_description = {} + + self._signing_key = signing_key + self._wrapping_key = wrapping_key + self._unwrapping_key = unwrapping_key + self._material_description = material_description + attr.validate(self) def _build_materials(self, encryption_context): # type: (EncryptionContext) -> WrappedCryptographicMaterials @@ -63,11 +96,13 @@ def _build_materials(self, encryption_context): :returns: Wrapped cryptographic materials :rtype: dynamodb_encryption_sdk.materials.wrapped.WrappedCryptographicMaterials """ + material_description = self._material_description.copy() + material_description.update(encryption_context.material_description) return WrappedCryptographicMaterials( wrapping_key=self._wrapping_key, unwrapping_key=self._unwrapping_key, signing_key=self._signing_key, - material_description=encryption_context.material_description.copy() + material_description=material_description ) def encryption_materials(self, encryption_context): diff --git a/src/dynamodb_encryption_sdk/materials/__init__.py b/src/dynamodb_encryption_sdk/materials/__init__.py index a1c73e82..5b0754e3 100644 --- a/src/dynamodb_encryption_sdk/materials/__init__.py +++ b/src/dynamodb_encryption_sdk/materials/__init__.py @@ -14,6 +14,7 @@ import abc try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Text # noqa pylint: disable=unused-import from mypy_extensions import NoReturn # 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/dynamodb_encryption_sdk/materials/raw.py b/src/dynamodb_encryption_sdk/materials/raw.py index 70f4acff..fa982263 100644 --- a/src/dynamodb_encryption_sdk/materials/raw.py +++ b/src/dynamodb_encryption_sdk/materials/raw.py @@ -27,6 +27,12 @@ import attr import six +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Optional, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.delegated_keys import DelegatedKey from dynamodb_encryption_sdk.internal.validators import dictionary_validator from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials @@ -34,7 +40,7 @@ __all__ = ('RawEncryptionMaterials', 'RawDecryptionMaterials') -@attr.s +@attr.s(init=False) class RawEncryptionMaterials(EncryptionMaterials): # inheritance confuses pylint: disable=abstract-method """Encryption materials for use directly with delegated keys. @@ -61,6 +67,26 @@ class RawEncryptionMaterials(EncryptionMaterials): default=attr.Factory(dict) ) + def __init__( + self, + signing_key, # type: DelegatedKey + encryption_key=None, # type: Optional[DelegatedKey] + material_description=None # type: Optional[Dict[Text, Text]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if material_description is None: + material_description = {} + + self._signing_key = signing_key + self._encryption_key = encryption_key + self._material_description = material_description + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Verify that the encryption key is allowed be used for raw materials.""" if self._encryption_key is not None and not self._encryption_key.allowed_for_raw_materials: @@ -80,7 +106,7 @@ def material_description(self): @property def signing_key(self): - # type: () -> Dict[Text, Text] + # type: () -> DelegatedKey """Delegated key used for calculating digital signatures. :returns: Signing key @@ -90,7 +116,7 @@ def signing_key(self): @property def encryption_key(self): - # type: () -> Dict[Text, Text] + # type: () -> DelegatedKey """Delegated key used for encrypting attributes. :returns: Encryption key @@ -102,7 +128,7 @@ def encryption_key(self): return self._encryption_key -@attr.s +@attr.s(init=False) class RawDecryptionMaterials(DecryptionMaterials): # inheritance confuses pylint: disable=abstract-method """Encryption materials for use directly with delegated keys. @@ -129,6 +155,26 @@ class RawDecryptionMaterials(DecryptionMaterials): default=attr.Factory(dict) ) + def __init__( + self, + verification_key, # type: DelegatedKey + decryption_key=None, # type: Optional[DelegatedKey] + material_description=None # type: Optional[Dict[Text, Text]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if material_description is None: + material_description = {} + + self._verification_key = verification_key + self._decryption_key = decryption_key + self._material_description = material_description + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Verify that the encryption key is allowed be used for raw materials.""" if self._decryption_key is not None and not self._decryption_key.allowed_for_raw_materials: @@ -148,7 +194,7 @@ def material_description(self): @property def verification_key(self): - # type: () -> Dict[Text, Text] + # type: () -> DelegatedKey """Delegated key used for verifying digital signatures. :returns: Verification key @@ -158,7 +204,7 @@ def verification_key(self): @property def decryption_key(self): - # type: () -> Dict[Text, Text] + # type: () -> DelegatedKey """Delegated key used for decrypting attributes. :returns: Decryption key diff --git a/src/dynamodb_encryption_sdk/materials/wrapped.py b/src/dynamodb_encryption_sdk/materials/wrapped.py index 937ec9f5..0e4412a7 100644 --- a/src/dynamodb_encryption_sdk/materials/wrapped.py +++ b/src/dynamodb_encryption_sdk/materials/wrapped.py @@ -17,6 +17,12 @@ import attr import six +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Optional, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.delegated_keys import DelegatedKey from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey from dynamodb_encryption_sdk.exceptions import UnwrappingError, WrappingError @@ -33,7 +39,7 @@ } -@attr.s +@attr.s(init=False) class WrappedCryptographicMaterials(CryptographicMaterials): """Encryption/decryption key is a content key stored in the material description, wrapped by the wrapping key. @@ -72,6 +78,28 @@ class WrappedCryptographicMaterials(CryptographicMaterials): default=attr.Factory(dict) ) + def __init__( + self, + signing_key, # type: DelegatedKey + wrapping_key=None, # type: Optional[DelegatedKey] + unwrapping_key=None, # type: Optional[DelegatedKey] + material_description=None # type: Optional[Dict[Text, Text]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if material_description is None: + material_description = {} + + self._signing_key = signing_key + self._wrapping_key = wrapping_key + self._unwrapping_key = unwrapping_key + self._material_description = material_description + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Prepare the content key.""" self._content_key_algorithm = self.material_description.get( # pylint: disable=attribute-defined-outside-init diff --git a/src/dynamodb_encryption_sdk/structures.py b/src/dynamodb_encryption_sdk/structures.py index b35596bb..85382f82 100644 --- a/src/dynamodb_encryption_sdk/structures.py +++ b/src/dynamodb_encryption_sdk/structures.py @@ -16,6 +16,12 @@ import attr import six +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Iterable, Optional, Set, Text # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + from dynamodb_encryption_sdk.exceptions import InvalidArgumentError from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes from dynamodb_encryption_sdk.internal.validators import dictionary_validator, iterable_validator @@ -38,7 +44,7 @@ def _validate_attribute_values_are_ddb_items(instance, attribute, value): # pyl raise TypeError('"{}" values do not look like DynamoDB items'.format(attribute.name)) -@attr.s +@attr.s(init=False) class EncryptionContext(object): # pylint: disable=too-few-public-methods """Additional information about an encryption request. @@ -75,8 +81,33 @@ class EncryptionContext(object): default=attr.Factory(dict) ) + def __init__( + self, + table_name=None, # type: Optional[Text] + partition_key_name=None, # type: Optional[Text] + sort_key_name=None, # type: Optional[Text] + attributes=None, # type: Optional[Dict[Text, Dict]] + material_description=None # type: Optional[Dict[Text, Text]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if attributes is None: + attributes = {} + if material_description is None: + material_description = {} + + self.table_name = table_name + self.partition_key_name = partition_key_name + self.sort_key_name = sort_key_name + self.attributes = attributes + self.material_description = material_description + attr.validate(self) -@attr.s + +@attr.s(init=False) class AttributeActions(object): """Configuration resource used to determine what action should be taken for a specific attribute. @@ -94,6 +125,24 @@ class AttributeActions(object): default=attr.Factory(dict) ) + def __init__( + self, + default_action=CryptoAction.ENCRYPT_AND_SIGN, # type: Optional[CryptoAction] + attribute_actions=None # type: Optional[Dict[Text, CryptoAction]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + if attribute_actions is None: + attribute_actions = {} + + self.default_action = default_action + self.attribute_actions = attribute_actions + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): # () -> None """Determine if any actions should ever be taken with this configuration and record that for reference.""" @@ -170,7 +219,7 @@ def __add__(self, other): ) -@attr.s +@attr.s(init=False) class TableIndex(object): # pylint: disable=too-few-public-methods """Describes a table index. @@ -185,6 +234,21 @@ class TableIndex(object): default=None ) + def __init__( + self, + partition, # type: Text + sort=None # type: Optional[Text] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.partition = partition + self.sort = sort + attr.validate(self) + self.__attrs_post_init__() + def __attrs_post_init__(self): """Set the ``attributes`` attribute for ease of access later.""" self.attributes = set([self.partition]) # attrs confuses pylint: disable=attribute-defined-outside-init @@ -217,7 +281,7 @@ def from_key_schema(cls, key_schema): ) -@attr.s +@attr.s(init=False) class TableInfo(object): """Describes a DynamoDB table. @@ -239,6 +303,22 @@ class TableInfo(object): default=None ) + def __init__( + self, + name, # type: Text + primary_index=None, # type: Optional[TableIndex] + secondary_indexes=None # type: Optional[Set[TableIndex]] + ): + # type: (...) -> None + """Workaround pending resolution of attrs/mypy interaction. + https://github.com/python/mypy/issues/2088 + https://github.com/python-attrs/attrs/issues/215 + """ + self.name = name + self._primary_index = primary_index + self._secondary_indexes = secondary_indexes + attr.validate(self) + @property def primary_index(self): # type: () -> TableIndex diff --git a/test/acceptance/acceptance_test_utils.py b/test/acceptance/acceptance_test_utils.py index fc0ff1a5..fb525cdf 100644 --- a/test/acceptance/acceptance_test_utils.py +++ b/test/acceptance/acceptance_test_utils.py @@ -13,17 +13,22 @@ """Helper tools for use with acceptance tests.""" import base64 from collections import defaultdict +from functools import partial import json import os import sys +import boto3 +from moto import mock_dynamodb2 import pytest from six.moves.urllib.parse import urlparse # moves confuse pylint: disable=wrong-import-order from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey from dynamodb_encryption_sdk.identifiers import EncryptionKeyType, KeyEncodingType from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider +from dynamodb_encryption_sdk.material_providers.most_recent import MostRecentProvider from dynamodb_encryption_sdk.material_providers.static import StaticCryptographicMaterialsProvider +from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider from dynamodb_encryption_sdk.materials.raw import RawDecryptionMaterials from dynamodb_encryption_sdk.structures import AttributeActions @@ -177,19 +182,65 @@ def _build_aws_kms_cmp(decrypt_key, verify_key): return AwsKmsCryptographicMaterialsProvider(key_id=key_id) +def _meta_table_prep(table_name, items_filename): + if table_name is None: + return + + client = boto3.client('dynamodb', region_name='us-west-2') + MetaStore.create_table(client, table_name, 100, 100) + + with open(_filename_from_uri(items_filename)) as f: + table_data = json.load(f) + request_items = {} + + for table_name, items in table_data.items(): + requests = [] + for item in items: + _decode_item(item) + requests.append({'PutRequest': {'Item': item}}) + request_items[table_name] = requests + client.batch_write_item(RequestItems=request_items) + + +def _build_most_recent_cmp(scenario, keys): + table = boto3.resource('dynamodb', region_name='us-west-2').Table(scenario['metastore']['table_name']) + meta_cmp, _, _ = _build_cmp(scenario['metastore'], keys) + metastore = MetaStore(table=table, materials_provider=meta_cmp()) + + most_recent_cmp = MostRecentProvider( + provider_store=metastore, + material_name=scenario['material_name'], + version_ttl=600.0 + ) + return most_recent_cmp + + _CMP_TYPE_MAP = { 'STATIC': _build_static_cmp, 'WRAPPED': _build_wrapped_cmp, - 'AWSKMS': _build_aws_kms_cmp + 'AWSKMS': _build_aws_kms_cmp, + 'MOST_RECENT': _build_most_recent_cmp } -def _build_cmp(provider_type, decrypt_key, verify_key): +def _build_cmp(scenario, keys): try: - cmp_builder = _CMP_TYPE_MAP[provider_type.upper()] + cmp_builder = _CMP_TYPE_MAP[scenario['provider'].upper()] except KeyError: - raise ValueError('Unsupported cryptographic materials provider type: "{}"'.format(provider_type)) - return cmp_builder(decrypt_key, verify_key) + raise ValueError('Unsupported cryptographic materials provider type: "{}"'.format(scenario['provider'])) + + if cmp_builder is _build_most_recent_cmp: + return ( + partial(cmp_builder, scenario, keys), + scenario['metastore']['keys']['decrypt'], + scenario['metastore']['keys']['verify'] + ) + + return ( + partial(cmp_builder, keys[scenario['keys']['decrypt']], keys[scenario['keys']['verify']]), + scenario['keys']['decrypt'], + scenario['keys']['verify'] + ) def _index(item, keys): @@ -216,23 +267,28 @@ def _expand_items(ciphertext_items, plaintext_items): yield table_name, table_index, ciphertext_item, pt_item['item'], pt_item['action'] -def load_scenarios(): +def load_scenarios(online): # pylint: disable=too-many-locals with open(_SCENARIO_FILE) as f: scenarios = json.load(f) keys_file = _filename_from_uri(scenarios['keys']) keys = _load_keys(keys_file) for scenario in scenarios['scenarios']: + if (not online and scenario['network']) or (online and not scenario['network']): + continue + plaintext_file = _filename_from_uri(scenario['plaintext']) - ciphertext_file = _filename_from_uri(scenario['ciphertext']) plaintext_items = _build_plaintext_items(plaintext_file, scenario['version']) + + ciphertext_file = _filename_from_uri(scenario['ciphertext']) ciphertext_items = _load_ciphertext_items(ciphertext_file) - materials_provider = _build_cmp( - provider_type=scenario['provider'], - decrypt_key=keys[scenario['keys']['decrypt']], - verify_key=keys[scenario['keys']['verify']] - ) + + materials_provider, decrypt_key_name, verify_key_name = _build_cmp(scenario, keys) + items = _expand_items(ciphertext_items, plaintext_items) + + metastore_info = scenario.get('metastore', {'table_name': None, 'ciphertext': None}) + for table_name, table_index, ciphertext_item, plaintext_item, attribute_actions in items: item_index = _index(ciphertext_item, table_index.values()) yield pytest.param( @@ -242,11 +298,12 @@ def load_scenarios(): ciphertext_item, plaintext_item, attribute_actions, + partial(_meta_table_prep, metastore_info['table_name'], metastore_info['ciphertext']), id='{version}-{provider}-{decrypt_key}-{verify_key}-{table}-{index}'.format( version=scenario['version'], provider=scenario['provider'], - decrypt_key=scenario['keys']['decrypt'], - verify_key=scenario['keys']['verify'], + decrypt_key=decrypt_key_name, + verify_key=verify_key_name, table=table_name, index=str(item_index) ) diff --git a/test/acceptance/encrypted/test_item.py b/test/acceptance/encrypted/test_item.py index 50fbbf98..2fe3a745 100644 --- a/test/acceptance/encrypted/test_item.py +++ b/test/acceptance/encrypted/test_item.py @@ -11,6 +11,7 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Acceptance tests for ``dynamodb_encryption_sdk.encrypted.item``.""" +from moto import mock_dynamodb2 import pytest from dynamodb_encryption_sdk.encrypted import CryptoConfig @@ -18,21 +19,20 @@ from dynamodb_encryption_sdk.structures import EncryptionContext from ..acceptance_test_utils import load_scenarios -pytestmark = [pytest.mark.accept, pytest.mark.integ] +pytestmark = [pytest.mark.accept] -@pytest.mark.parametrize( - 'materials_provider, table_name, table_index, ciphertext_item, plaintext_item, attribute_actions', - load_scenarios() -) -def test_item_encryptor( +def _item_check( materials_provider, table_name, table_index, ciphertext_item, plaintext_item, - attribute_actions + attribute_actions, + prep ): + prep() # Test scenario setup that needs to happen inside the test + cmp = materials_provider() # Some of the materials providers need to be constructed inside the test encryption_context = EncryptionContext( table_name=table_name, partition_key_name=table_index['partition'], @@ -40,7 +40,7 @@ def test_item_encryptor( attributes=ciphertext_item ) crypto_config = CryptoConfig( - materials_provider=materials_provider, + materials_provider=cmp, encryption_context=encryption_context, attribute_actions=attribute_actions ) @@ -50,3 +50,54 @@ def test_item_encryptor( if key == 'version': continue assert decrypted_item[key] == plaintext_item[key] + + +@mock_dynamodb2 +@pytest.mark.local +@pytest.mark.parametrize( + 'materials_provider, table_name, table_index, ciphertext_item, plaintext_item, attribute_actions, prep', + load_scenarios(online=False) +) +def test_item_encryptor_offline( + materials_provider, + table_name, + table_index, + ciphertext_item, + plaintext_item, + attribute_actions, + prep +): + return _item_check( + materials_provider, + table_name, + table_index, + ciphertext_item, + plaintext_item, + attribute_actions, + prep + ) + + +@pytest.mark.integ +@pytest.mark.parametrize( + 'materials_provider, table_name, table_index, ciphertext_item, plaintext_item, attribute_actions, prep', + load_scenarios(online=True) +) +def test_item_encryptor_online( + materials_provider, + table_name, + table_index, + ciphertext_item, + plaintext_item, + attribute_actions, + prep +): + return _item_check( + materials_provider, + table_name, + table_index, + ciphertext_item, + plaintext_item, + attribute_actions, + prep + ) diff --git a/test/functional/material_providers/__init__.py b/test/functional/material_providers/__init__.py new file mode 100644 index 00000000..2add15ef --- /dev/null +++ b/test/functional/material_providers/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 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/material_providers/store/__init__.py b/test/functional/material_providers/store/__init__.py new file mode 100644 index 00000000..2add15ef --- /dev/null +++ b/test/functional/material_providers/store/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 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/material_providers/store/test_meta.py b/test/functional/material_providers/store/test_meta.py new file mode 100644 index 00000000..6eee7a13 --- /dev/null +++ b/test/functional/material_providers/store/test_meta.py @@ -0,0 +1,37 @@ +# Copyright 2018 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. +"""Integration tests for ``dynamodb_encryption_sdk.material_providers.store.meta``.""" +import base64 +import os + +import boto3 +from moto import mock_dynamodb2 +import pytest + +from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +@mock_dynamodb2 +def test_create_table(): + client = boto3.client('dynamodb', region_name='us-west-2') + table_name = base64.b64encode(os.urandom(32)).decode('utf-8') + + MetaStore.create_table(client, table_name, 1, 1) + waiter = client.get_waiter('table_exists') + waiter.wait(TableName=table_name) + + client.delete_table(TableName=table_name) + waiter = client.get_waiter('table_not_exists') + waiter.wait(TableName=table_name) diff --git a/test/integration/material_providers/store/__init__.py b/test/integration/material_providers/store/__init__.py new file mode 100644 index 00000000..2add15ef --- /dev/null +++ b/test/integration/material_providers/store/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2018 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/integration/material_providers/store/test_meta.py b/test/integration/material_providers/store/test_meta.py new file mode 100644 index 00000000..075f0c38 --- /dev/null +++ b/test/integration/material_providers/store/test_meta.py @@ -0,0 +1,23 @@ +# Copyright 2018 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. +"""Integration tests for ``dynamodb_encryption_sdk.material_providers.store.meta``.""" +import os + +import boto3 +import pytest + +from dynamodb_encryption_sdk.material_providers.store.meta import MetaStore + +pytestmark = [pytest.mark.integ] + + diff --git a/test/vectors/encrypted_item/ciphertext/metastore-data-tables-1.json b/test/vectors/encrypted_item/ciphertext/metastore-data-tables-1.json new file mode 100644 index 00000000..19923ca0 --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/metastore-data-tables-1.json @@ -0,0 +1,351 @@ +{ + "HashKeyOnly": [ + { + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAODNvNHkzTTB3UUFCRXl1UXN0SFVQUGF1NkpPMUhiNk1OWGxXQW5aWDhYdmFYTlUwNUluTFUxZz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "iZXCp3s7VEMYdf01YEWqMlXOBHv3+e8gKbECrPUW47I=" + } + }, + { + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAODlZdEVSWXVDT3A4MHlKVnJOYytYREFoaVN6UHdlRnNJQk1YRXMxSEQ2eGdvdmYveldabmMrQT09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "zh74eH/yJQFzkm5mq52iFAlSDpXAFe3ZP2nv7X/xY1w=" + } + }, + { + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOEw2YkExbWszYTZxek1YNUkyMkYyYzRvU0FmZ2VZdCtjQmtFYndDTzhYUzlkL0ZqV20wekpZUT09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "HR5P6kozMSqqs+rnDMaCiymH8++OwEVzx2Y13ZMp5P8=" + } + } + ], + "TableName": [ + { + "rangeKey": { + "N": "1" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOEpKNDk2UGRpcDViOHlldTVxbEE0STNOUjFTVHdtZEd2REJwQWowNXprUmN0OFh6T3E1TmRJZz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "yT2ehLcx/a609Ez6laLkTAqCtp0IYzzKV8Amv8jdQMw=" + } + }, + { + "rangeKey": { + "N": "2" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOHNVQzNEekp5Tk1tZ3ZUSE1EVnh2Sng1OCtDT1h0UStwRzR4ZlVQL0pJTkRHOGI1M00wOFRBZz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "YAai32/7MVrGjSzgcVxkFDqU+G9HcmuiNSWZHcnvfjg=" + } + }, + { + "rangeKey": { + "N": "3" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOEZGdjVQNjAxZzF0eXhoaDhxQmlCdDB1d2JoODlRaDdyeTcxL2lJdWxvSWNvQzFBV3JHczhtdz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "0iwjbBLCdtSosmDTDYzKxu3Q5qda0Ok9q3VbIJczBV0=" + } + }, + { + "rangeKey": { + "N": "1" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOHJ3OU5qdU53dkhENTZPTmlqWC9nbUlGZ051ZDk3OS94QXhlaTVjbmdJbmxhajdpSVg0RDdadz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "Gl1jMNLZl/B70Hz2B4K4K46kir+hE6AeX8azZfFi8GA=" + } + }, + { + "rangeKey": { + "N": "7" + }, + "hashKey": { + "N": "5" + }, + "stringValue": { + "B": "MyVrAzOuKFS+hAiVq0jlmIJcwMP2w62LdWChncBN0q0HMB3WpADYK2BF1q+oQP83" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOHlUK09JWlpaWkE5VmU0dTdwRE1zNG9TUVZTNlFYZEFmQjZkVjlMMUg4QzBrRXliQ0Nad0JRQT09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "intSet": { + "B": "0iLxGtvyDaUNXY1iYwcDlZX3zIs+QOMsBQ+RbX6YlAgFdMK/k57OXPH3jMIptzkNAKNWFea+NAz+AXFd2jPC8w==" + }, + "doubleSet": { + "B": "0nazy+tnY85GZpSANJzBLXZHPKzCvN4ggpopjujfAOO37wDi6zrSwhurLpjFIJGR27pn5azaroZWYA8GLfiGIw==" + }, + "byteArrayValue": { + "B": "w9sfXioZCE9luCt4qiOixyRJVlJ6zbTwFoFg0wQNJbA=" + }, + "stringSet": { + "B": "8057NGIAJADqX/KzkjZl7XzFMI/6j7vAbp5F83tZjOQhguhp8hheXAzcsrCmM6sME1oGEmJEran4Svs1qT5ChA==" + }, + "intValue": { + "B": "LFHv7oLor2SoKypi/gubI0IsipoLd/I20qPr2wHOgOs=" + }, + "doubleValue": { + "B": "uq8MBbPKDskxhyJ6VCmd9EC6+tD3EuiqhgFUpxckzdk=" + }, + "version": { + "N": "1" + }, + "*amzn-ddb-map-sig*": { + "B": "FhpaX3jXqz+Pg4QETqcNBULC+OBOTkux2BFGCdnr5PY=" + } + }, + { + "rangeKey": { + "N": "10" + }, + "hashKey": { + "N": "8" + }, + "stringValue": { + "S": "Hello world!" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAODRhOGRFc01ybDR6ODlVM1RkOWh4L0J2cms4cVZEODlOaklkMnU0d2NGSnBxbUVkc1lka2ZXZz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "10", + "200" + ] + }, + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "intValue": { + "N": "123" + }, + "doubleValue": { + "N": "15" + }, + "version": { + "N": "1" + }, + "*amzn-ddb-map-sig*": { + "B": "5NHNzCBtZcVAUlz1ymLB7Ta+1n3VjffLj5WniFA9afo=" + } + }, + { + "rangeKey": { + "N": "3" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOE55eTdqK3FkNEJMNzV2MTlnRHdHVHdtTGgrbmlMaER0cjdaL3ZZMVFmQTFEQmE5Y0JGdzIxdz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "cSTe0npOBBtsxSN4F9mLF2WTyCN1+1owsVoGkYumiZQ=" + } + }, + { + "rangeKey": { + "N": "9" + }, + "hashKey": { + "N": "7" + }, + "stringValue": { + "S": "Hello world!" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "10", + "200" + ] + }, + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "intValue": { + "N": "123" + }, + "doubleValue": { + "N": "15" + }, + "version": { + "N": "1" + } + }, + { + "rangeKey": { + "N": "1" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOGcrY1NpV2I3eWZYZ2pQS2gzOVM0anBZZWFNeEhHRG90c2JCOG5sQkp3ei9vclBRQzhOZFNxdz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "lBLoUXuc8TgsJJlItgBh6PJ1YVk52nvQE9aErEB8jK8=" + } + }, + { + "rangeKey": { + "N": "2" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOHlKa2M4OW9HNEpoajhyazlEQnpVeEQ1cForN1Q4Z2pQUEU1TE9uVDhvd2tJWDJ6bGFpdUJKQT09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "cjd91WBBFWPnrJxIJ2p2hnXFVCemgYw0HqRWcnoQcq4=" + } + }, + { + "rangeKey": { + "N": "3" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOG9kQ2hPVmtiYkN3S3V3VHYrVjYvelNwcnZIUWVhWlpqaDZvU3JzMHV4T255bFQzSUZ0TjVVZz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "uXZKvYmUgZEOunUJctXpkvqhrgUoK1eLi8JpvlRozTI=" + } + }, + { + "rangeKey": { + "N": "2" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOEJLV0Z2T0hRVUxCMTcxTW56dkQrVVYyMVpmTUxhSXl4QjB3ekdZbStzY2VFd2pNekgxTFhVQT09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "*amzn-ddb-map-sig*": { + "B": "66Vz0G8nOQzlvIpImXSkl+nmCpTYeRy8mAF4qgGgMw0=" + } + }, + { + "rangeKey": { + "N": "8" + }, + "stringValue": { + "S": "Hello world!" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAA0FFUwAAABBhbXpuLWRkYi1lbnYta2V5AAAAOEdncWp2Q3JaYzhZL2RrMGxmQlk5K09tbWNXUWIvbjVYMW01YTNBcElZb3JLVzU0RVhRYTgrZz09AAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1tZXRhLWlkAAAAEm15IG1hdGVyaWFsIG5hbWUjNQAAABFhbXpuLWRkYi13cmFwLWFsZwAAAAdBRVNXcmFw" + }, + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "intSet": { + "B": "eBhcgBr8TocxVsTw8tJtcAK2VKFOkoZlWBUusFNtKbTulghzdpT3iTMqIJB86ViXXguO43XqMZWs1U3G/IaF+g==" + }, + "byteArrayValue": { + "B": "Y3ciZfN54gf86a4mxRfon9CgzQkNIxrtWV8s6tg/6G0=" + }, + "intValue": { + "N": "123" + }, + "stringSet": { + "B": "RhykbS8bqGEd2LEGtLV0S6Pj+4KjuVc15ExkUmlCKlClAgNpukA5Tp0FjU/XL0Qli4v6apZaraKgBC1l4YlRDg==" + }, + "doubleValue": { + "N": "15" + }, + "version": { + "N": "1" + }, + "*amzn-ddb-map-sig*": { + "B": "mC10Qiw1c/P8Bab4SaP3kmsPMBVfOZKjZ3SgvXyd3Vg=" + } + } + ] +} diff --git a/test/vectors/encrypted_item/ciphertext/metastore-rsa-rsa-1.json b/test/vectors/encrypted_item/ciphertext/metastore-rsa-rsa-1.json new file mode 100644 index 00000000..f2a84c34 --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/metastore-rsa-rsa-1.json @@ -0,0 +1,33 @@ +{ + "MetaTable": [ + { + "N": { + "S": "my material name" + }, + "V": { + "N": "5" + }, + "t": { + "B": "WWJ3Vz3c9h3jh9mgm2SeCG50AKNjck17jOqh+Ck3YYk=" + }, + "encAlg": { + "B": "+IzZN4S+D8IWe5owaeFdcIdg5/OLjcSlVXdMpPzKwKc=" + }, + "intAlg": { + "B": "NN6CBzgosLeAualiFggqd1XMzwI9jBMFaLP/b/tytooUeTL3M0eHELRp57mQazDe" + }, + "enc": { + "B": "MRP1Q6ptICi0stBWyjTpM5stvMw14it5ohM/zPgx9XbeDYVRoPgmTDkBVVA1dQDZ" + }, + "int": { + "B": "q/PhRAvCb/0XXC7Z4XYy72fYrLIWdEjqtAQm6dZK0fg=" + }, + "*amzn-ddb-map-sig*": { + "B": "oRo6E5S0vTY1ZjSPHBqB6JowBIjDKt7BTJt6xEjxgYykbpx+oq87YfDVy4P8sV/n09t8MqMHpKSxxJ8AhzKbkAe7QVLx7nGYrsBKPC3Q9y6MdWz2kpvTAFC2Q+fxj6PGyZFDeq2cip1bWmiP5Q+JkkgiS5lG6WpWgmuQTie1lklubcPGJqrnCsaG7SSPNMUc4KrhA54sv3TZXvJdPjgrjhMIiqDfYzRMle1IZcAJNyvSmJ03T9iSxFTPeqhtmFldn4DhWC3qwdlqaQQofiowiCpgcknNGBqlKwoL2ZDP6MuUKiW+Tll9nGzB2W00cmPyN/ZoX69fHLXxG2Gd/h3hgQ==" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABBhbXpuLWRkYi1lbnYtYWxnAAAAB0FFUy8yNTYAAAAQYW16bi1kZGItZW52LWtleQAAAVhEN2h3R1F3ckwxV1NVNmtTZTlvdXA3MEM3Sk0zNFZPaTlmMnlWVTJ4VE9kYWdrQ3Avczk2VFdxSmEvOWtVejEwRGcxdWxpVGtnY0llTEJNQ1JuK1hHRTg4SFovVVRCMzVMWkx5YklwY1JrL0hhelQ1U1ZQV1JaK2hWSjlhUWxUaHBocThoYWxPOTJ2WkN2RXA5YVcwRzRncXZObVhUenJoZm9MYU1LdCtPYXNCb0o1aWpaLzFYRDdGaVlMLzBCZ2hDdDlyL2pzU2NpdysweXdYNjZXZ3pVZDVGZmNIaVpYYUtENmVQd1pxOFlrc1lOS3V4NU1MNUZPcWxDd2ZjMUZiQUE0dXFZNDdrTWJzYjZ5SkVoMThzU3lTY3ZGOE9nZjNDQURGbzFRY1g2WjFxQ2Q0REl0SEhRRzZDcExiU0liSkFGaDJyOEJ4NVRKRklJR2ZXMmNxN1E9PQAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABFhbXpuLWRkYi13cmFwLWFsZwAAACVSU0EvRUNCL09BRVBXaXRoU0hBLTI1NkFuZE1HRjFQYWRkaW5n" + } + } + ] +} diff --git a/test/vectors/encrypted_item/scenarios.json b/test/vectors/encrypted_item/scenarios.json index 083f3ebe..f9b3f45c 100644 --- a/test/vectors/encrypted_item/scenarios.json +++ b/test/vectors/encrypted_item/scenarios.json @@ -10,7 +10,8 @@ "verify": "hmacKey" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/static-aes-hmac-1.json" + "ciphertext": "file://ciphertext/static-aes-hmac-1.json", + "network": true }, { "version": "v0", @@ -20,7 +21,8 @@ "verify": "hmacKey" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/static-aes-hmac-2.json" + "ciphertext": "file://ciphertext/static-aes-hmac-2.json", + "network": true }, { "version": "v1", @@ -30,7 +32,8 @@ "verify": "hmacKey" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/static-aes-hmac-3.json" + "ciphertext": "file://ciphertext/static-aes-hmac-3.json", + "network": true }, { "version": "v0", @@ -40,7 +43,8 @@ "verify": "rsaEncPub" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/wrapped-rsa-rsa-1.json" + "ciphertext": "file://ciphertext/wrapped-rsa-rsa-1.json", + "network": true }, { "version": "v1", @@ -50,7 +54,8 @@ "verify": "rsaEncPub" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/wrapped-rsa-rsa-2.json" + "ciphertext": "file://ciphertext/wrapped-rsa-rsa-2.json", + "network": true }, { "version": "v0", @@ -60,7 +65,8 @@ "verify": "rsaEncPub" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/wrapped-rsa-rsa-3.json" + "ciphertext": "file://ciphertext/wrapped-rsa-rsa-3.json", + "network": true }, { "version": "v1", @@ -70,7 +76,8 @@ "verify": "hmacKey" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/wrapped-aes-hmac-1.json" + "ciphertext": "file://ciphertext/wrapped-aes-hmac-1.json", + "network": true }, { "version": "v1", @@ -80,7 +87,25 @@ "verify": "awsKmsUsWest2" }, "plaintext": "file://plaintext.json", - "ciphertext": "file://ciphertext/aws-kms-direct-1.json" + "ciphertext": "file://ciphertext/aws-kms-direct-1.json", + "network": true + }, + { + "version": "v1", + "provider": "most_recent", + "material_name": "my material name", + "metastore": { + "table_name": "MetaTable", + "provider": "wrapped", + "keys": { + "decrypt": "rsaEncPriv", + "verify": "rsaEncPub" + }, + "ciphertext": "file://ciphertext/metastore-rsa-rsa-1.json" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/metastore-data-tables-1.json", + "network": false } ] }