diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..993f84e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.egg-info +*.pyc +*.pyo +*~ +.DS_Store +.tox +/.cache* +/.coverage* +/build +/doc/generated/* +/runpy +__pycache__ +build +dist +docs/build +.python-version +.mypy_cache +.hypothesis +.pytest_cache diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 00000000..e69de29b diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md deleted file mode 100644 index 2824cedf..00000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# aws-dynamodb-crypto-client-python -The Amazon DynamoDB Client-side Encryption for Python supports encryption and signing of your data when stored in Amazon DynamoDB, and is compatible with the Amazon DynamoDB Client-side Encryption for Java. https://github.com/awslabs/aws-dynamodb-encryption-java diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..e69de29b diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 00000000..8ec66c31 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,70 @@ +# pylint: disable=invalid-name +"""Sphinx configuration.""" +from datetime import datetime +import io +import os +import re + +VERSION_RE = re.compile(r'''__version__ = ['"]([0-9.]+)['"]''') +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read(*args): + """Reads complete file contents.""" + return io.open(os.path.join(HERE, *args), encoding='utf-8').read() + + +def get_release(): + """Reads the release (full three-part version number) from this module.""" + init = read('..', 'src', 'dynamodb_encryption_sdk', 'identifiers.py') + return VERSION_RE.search(init).group(1) + + +def get_version(): + """Reads the version (MAJOR.MINOR) from this module.""" + _release = get_release() + split_version = _release.split('.') + if len(split_version) == 3: + return '.'.join(split_version[:2]) + return _release + + +project = u'dynamodb-encryption-sdk-python' +version = get_version() +release = get_release() + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', 'sphinx.ext.todo', + 'sphinx.ext.coverage', 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon'] +napoleon_include_special_with_doc = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +source_suffix = '.rst' # The suffix of source filenames. +master_doc = 'index' # The master toctree document. + +copyright = u'%s, Amazon' % datetime.now().year # pylint: disable=redefined-builtin + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['_build'] + +pygments_style = 'sphinx' + +autoclass_content = "both" +autodoc_default_flags = ['show-inheritance', 'members'] +autodoc_member_order = 'bysource' + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +htmlhelp_basename = '%sdoc' % project + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} + +# autosummary +autosummary_generate = True diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 00000000..30794af9 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,49 @@ +.. include:: ../README.rst + +******* +Modules +******* + +.. autosummary:: + :toctree: generated + + .. Add/replace module names you want documented here + dynamodb_encryption_sdk + dynamodb_encryption_sdk.exceptions + dynamodb_encryption_sdk.identifiers + dynamodb_encryption_sdk.structures + dynamodb_encryption_sdk.encrypted + dynamodb_encryption_sdk.encrypted.client + dynamodb_encryption_sdk.encrypted.item + dynamodb_encryption_sdk.encrypted.resource + dynamodb_encryption_sdk.encrypted.table + dynamodb_encryption_sdk.material_providers + dynamodb_encryption_sdk.material_providers.aws_kms + dynamodb_encryption_sdk.material_providers.static + dynamodb_encryption_sdk.material_providers.wrapped + dynamodb_encryption_sdk.material_providers.store + dynamodb_encryption_sdk.materials + dynamodb_encryption_sdk.materials.raw + dynamodb_encryption_sdk.materials.wrapped + dynamodb_encryption_sdk.internal + dynamodb_encryption_sdk.internal.defaults + dynamodb_encryption_sdk.internal.dynamodb_types + dynamodb_encryption_sdk.internal.identifiers + dynamodb_encryption_sdk.internal.str_ops + dynamodb_encryption_sdk.internal.utils + dynamodb_encryption_sdk.internal.crypto + dynamodb_encryption_sdk.internal.crypto.jce_bridge + dynamodb_encryption_sdk.internal.crypto.jce_bridge.authentication + dynamodb_encryption_sdk.internal.crypto.jce_bridge.encryption + dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives + dynamodb_encryption_sdk.internal.crypto.authentication + dynamodb_encryption_sdk.internal.crypto.encryption + dynamodb_encryption_sdk.internal.formatting + dynamodb_encryption_sdk.internal.formatting.deserialize + dynamodb_encryption_sdk.internal.formatting.deserialize.attribute + dynamodb_encryption_sdk.internal.formatting.serialize + dynamodb_encryption_sdk.internal.formatting.serialize.attribute + dynamodb_encryption_sdk.internal.formatting.material_description + dynamodb_encryption_sdk.internal.formatting.transform + +.. include:: ../CHANGELOG.rst diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..29e31945 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,2 @@ +sphinx>=1.3.0 +sphinx_rtd_theme \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..f401d4dd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +boto3>=1.4.4 +cryptography>=1.8.1 +attrs>=17.4.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..0a1d775e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +[wheel] +universal = 1 + +[metadata] +license_file = LICENSE + +[coverage:run] +branch = True + +[coverage:report] +show_missing = True + +[mypy] +ignore_missing_imports = True + +[tool:pytest] +markers = + local: superset of unit and functional (does not require network access) + unit: mark test as a unit test (does not require network access) + functional: mark test as a functional test (does not require network access) + integ: mark a test as an integration test (requires network access) + slow: mark a test as being known to take a long time to complete (order 5s < t < 60s) + veryslow: mark a test as being known to take a very long time to complete (order t > 60s) + nope: mark a test as being so slow that it should only be very infrequently (order t > 30m) +log_level=NOTSET + +# Flake8 Configuration +[flake8] +max_complexity = 10 +max_line_length = 120 +import_order_style = google +application_import_names = dynamodb_encryption_sdk +builtins = raw_input +ignore = + # Ignoring D205 and D400 because of false positives + D205, D400, + # Ignoring D401 pending discussion of imperative mood + D401, + # Ignoring D202 (no blank lines after function docstring) because mypy confuses flake8 + D202 + + +# Doc8 Configuration +[doc8] +max-line-length = 120 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..73205f9e --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +"""DynamoDB Encryption SDK.""" +import io +import os +import re + +from setuptools import find_packages, setup + +VERSION_RE = re.compile(r'''__version__ = ['"]([0-9.]+)['"]''') +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read(*args): + """Reads complete file contents.""" + return io.open(os.path.join(HERE, *args), encoding='utf-8').read() + + +def get_version(): + """Reads the version from this module.""" + init = read('src', 'dynamodb_encryption_sdk', 'identifiers.py') + return VERSION_RE.search(init).group(1) + + +def get_requirements(): + """Reads the requirements file.""" + requirements = read('requirements.txt') + return [r for r in requirements.strip().splitlines()] + + +setup( + name='dynamodb-encryption-sdk', + version=get_version(), + packages=find_packages('src'), + package_dir={'': 'src'}, + url='http://dynamodb-encryption-sdk.readthedocs.io/en/latest/', + author='Amazon Web Services', + author_email='aws-cryptools@amazon.com', + maintainer='Amazon Web Services', + long_description=read('README.rst'), + keywords='aws-encryption-sdk aws kms encryption dynamodb', + data_files=[ + 'README.rst', + 'CHANGELOG.rst', + 'LICENSE', + 'requirements.txt' + ], + license='Apache License 2.0', + install_requires=get_requirements(), + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: Implementation :: CPython', + 'Topic :: Security', + 'Topic :: Security :: Cryptography' + ] +) diff --git a/src/dynamodb_encryption_sdk/__init__.py b/src/dynamodb_encryption_sdk/__init__.py new file mode 100644 index 00000000..a7c66779 --- /dev/null +++ b/src/dynamodb_encryption_sdk/__init__.py @@ -0,0 +1,34 @@ +# 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. +"""""" +from dynamodb_encryption_sdk.encrypted.item import ( + decrypt_dynamodb_item, decrypt_python_item, + encrypt_dynamodb_item, encrypt_python_item +) + +# encrypt_item +# encrypt_raw_item +# decrypt_item +# decrypt_raw_item +# EncryptedTable +# EncryptedResource +# EncryptedClient + +# TableConfiguration +# MaterialDescription +# ItemConfiguration + +__all__ = ( + 'decrypt_dynamodb_item', 'decrypt_python_item', + 'encrypt_dynamodb_item', 'encrypt_python_item' +) diff --git a/src/dynamodb_encryption_sdk/delegated_keys/__init__.py b/src/dynamodb_encryption_sdk/delegated_keys/__init__.py new file mode 100644 index 00000000..89bf3d69 --- /dev/null +++ b/src/dynamodb_encryption_sdk/delegated_keys/__init__.py @@ -0,0 +1,149 @@ +# 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. +"""Delegated keys.""" +import abc +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Dict, Text # pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +import six + +from dynamodb_encryption_sdk.identifiers import EncryptionKeyTypes + + +@six.add_metaclass(abc.ABCMeta) +class DelegatedKey(object): + """Delegated keys are black boxes that encrypt, decrypt, sign, and verify data and wrap + and unwrap keys. Not all delegated keys implement all methods. + + Unless overridden by a subclass, any method that a delegated key does not implement raises + 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.""" + + def _raise_not_implemented(self, method_name): + """Raises a standardized ``NotImplementedError`` to report that the specified method + is not supported. + + :raises NotImplementedError: when called + """ + raise NotImplementedError('"{}" is not supported by this DelegatedKey'.format(method_name)) + + @classmethod + def generate(cls, algorithm, key_length): + """Generate an instance of this DelegatedKey using the specified algorithm and key length. + + :param str algorithm: Text description of algorithm to be used + :param int key_length: Size of key to generate + :returns: Generated delegated key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + cls._raise_not_implemented('generate') + + def encrypt(self, algorithm, name, plaintext, additional_associated_data=None): + # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + """Encrypt data. + + :param str algorithm: Text description of algorithm to use to encrypt data + :param str name: Name associated with plaintext data + :param bytes plaintext: Plaintext data to encrypt + :param dict additional_associated_data: Not used by all delegated keys, but if it + is, then if it is provided on encrypt it must be required on decrypt. + :returns: Encrypted ciphertext + :rtype: bytes + """ + self._raise_not_implemented('encrypt') + + def decrypt(self, algorithm, name, ciphertext, additional_associated_data=None): + # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + """Encrypt data. + + :param str algorithm: Text description of algorithm to use to decrypt data + :param str name: Name associated with ciphertext data + :param bytes ciphertext: Ciphertext data to decrypt + :param dict additional_associated_data: Not used by all delegated keys, but if it + is, then if it is provided on encrypt it must be required on decrypt. + :returns: Decrypted plaintext + :rtype: bytes + """ + self._raise_not_implemented('decrypt') + + def wrap(self, algorithm, content_key, additional_associated_data=None): + # type: (Text, bytes, Dict[Text, Text]) -> bytes + """Wrap content key. + + :param str algorithm: Text description of algorithm to use to wrap key + :param bytes content_key: Raw content key to wrap + :param dict additional_associated_data: Not used by all delegated keys, but if it + is, then if it is provided on wrap it must be required on unwrap. + :returns: Wrapped key + :rtype: bytes + """ + self._raise_not_implemented('wrap') + + def unwrap(self, algorithm, wrapped_key, wrapped_key_algorithm, wrapped_key_type, additional_associated_data=None): + # type: (Text, bytes, Text, EncryptionKeyTypes, Dict[Text, Text]) -> DelegatedKey + """Wrap content key. + + :param str algorithm: Text description of algorithm to use to unwrap key + :param bytes content_key: Raw content key to wrap + :param str wrapped_key_algorithm: Text description of algorithm for unwrapped key to use + :param wrapped_key_type: Type of key to treat key as once unwrapped + :type wrapped_key_type: dynamodb_encryption_sdk.identifiers.EncryptionKeyTypes + :param dict additional_associated_data: Not used by all delegated keys, but if it + is, then if it is provided on wrap it must be required on unwrap. + :returns: Delegated key using unwrapped key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + self._raise_not_implemented('unwrap') + + def sign(self, algorithm, data): + # type: (Text, bytes) -> bytes + """Sign data. + + :param str algorithm: Text description of algorithm to use to sign data + :param bytes data: Data to sign + :returns: Signature value + :rtype: bytes + """ + self._raise_not_implemented('sign') + + def verify(self, algorithm, signature, data): + # type: (Text, bytes, bytes) -> None + """Sign data. + + :param str algorithm: Text description of algorithm to use to verify signature + :param bytes signature: Signature to verify + :param bytes data: Data over which to verify signature + """ + self._raise_not_implemented('verify') + + def signing_algorithm(self): + # type: () -> Text + """Provides 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. + + :returns: Signing algorithm identifier + :rtype: str + """ + self._raise_not_implemented('signing_algorithm') diff --git a/src/dynamodb_encryption_sdk/delegated_keys/jce.py b/src/dynamodb_encryption_sdk/delegated_keys/jce.py new file mode 100644 index 00000000..f57027b0 --- /dev/null +++ b/src/dynamodb_encryption_sdk/delegated_keys/jce.py @@ -0,0 +1,281 @@ +# 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. +"""Delegated key that JCE StandardName algorithm values to determine behavior.""" +import logging +import os + +import attr +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +import six + +from . import DelegatedKey +from dynamodb_encryption_sdk.exceptions import JceTransformationError, UnwrappingError +from dynamodb_encryption_sdk.identifiers import EncryptionKeyTypes, KeyEncodingType, LOGGER_NAME +from dynamodb_encryption_sdk.internal.crypto.jce_bridge import authentication, encryption, primitives + +_LOGGER = logging.getLogger(LOGGER_NAME) + + +def _generate_symmetric_key(key_length): + """Generate a new AES key. + + :param int key_length: Required key length in bytes + :returns: raw key, symmetric key identifier, and RAW encoding identifier + :rtype: tuple of bytes, EncryptionKeyTypes, and KeyEncodingType + """ + return os.urandom(key_length), EncryptionKeyTypes.SYMMETRIC, KeyEncodingType.RAW + + +def _generate_rsa_key(key_length): + """Generate a new RSA private key. + + :param int key_length: Required key length in bytes + :returns: DER-encoded private key, private key identifier, and DER encoding identifier + :rtype: tuple of bytes, EncryptionKeyTypes, and KeyEncodingType + """ + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=key_length, + backend=default_backend() + ) + key_bytes = private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + return key_bytes, EncryptionKeyTypes.PRIVATE, KeyEncodingType.DER + + +_ALGORITHM_GENERATE_MAP = { + 'SYMMETRIC': _generate_symmetric_key, + 'RSA': _generate_rsa_key +} + + +@attr.s(hash=False) +class JceNameLocalDelegatedKey(DelegatedKey): + """Delegated key that uses JCE StandardName algorithm values to determine behavior. + + :param bytes key: Raw key bytes + :param str algorithm: JCE Standard Algorithm Name + :param key_type: Identifies what type of key is being provided + :type key_type: dynamodb_encryption_sdk.identifiers.EncryptionKeyTypes + :param key_encoding: Identifies how the provided key is encoded + :type key_encoding: dynamodb_encryption_sdk.identifiers.KeyEncodingTypes + """ + key = attr.ib(validator=attr.validators.instance_of(bytes), repr=False) + _algorithm = attr.ib(validator=attr.validators.instance_of(six.string_types)) + _key_type = attr.ib(validator=attr.validators.instance_of(EncryptionKeyTypes)) + _key_encoding = attr.ib(validator=attr.validators.instance_of(KeyEncodingType)) + + @property + def algorithm(self): + # type: () -> Text + """Text description of algorithm used by this delegated key.""" + return self._algorithm + + def _enable_authentication(self): + # () -> None + """Enable authentication methods for keys that support them.""" + self.sign = self._sign + self.verify = self._verify + self.signing_algorithm = self._signing_algorithm + + def _enable_encryption(self): + # () -> None + """Enable encryption methods for keys that support them.""" + self.encrypt = self._encrypt + self.decrypt = self._decrypt + + def _enable_wrap(self): + # () -> None + """Enable key wrapping methods for keys that support them.""" + self.wrap = self._wrap + self.unwrap = self._unwrap + + def __attrs_post_init__(self): + # () -> None + """Identify the correct key handler class for the requested algorithm and load the provided key.""" + # First try for encryption ciphers + # https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html + try: + key_transformer = primitives.JAVA_ENCRYPTION_ALGORITHM[self.algorithm] + except KeyError: + pass + else: + self.__key = key_transformer.load_key(self.key, self._key_type, self._key_encoding) + self._enable_encryption() + self._enable_wrap() + return + + # Now try for authenticators + # https://docs.oracle.com/javase/8/docs/api/javax/crypto/Mac.html + # https://docs.oracle.com/javase/8/docs/api/java/security/Signature.html + try: + key_transformer = authentication.JAVA_AUTHENTICATOR[self.algorithm] + except KeyError: + pass + else: + self.__key = key_transformer.load_key(self.key, self._key_type, self._key_encoding) + self._enable_authentication() + return + + raise JceTransformationError('Unknown algorithm: "{}"'.format(self.algorithm)) + + @classmethod + def generate(cls, algorithm, key_length=None): + # type: (Text, Optional[int]) -> JceNameLocalDelegatedKey + """Generate an instance of this DelegatedKey using the specified algorithm and key length. + + :param str algorithm: Text description of algorithm to be used + :param int key_length: Size of key to generate + :returns: Generated delegated key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + # Normalize to allow generating both encryption and signing keys + algorithm_lookup = algorithm.upper() + if 'HMAC' in algorithm_lookup or algorithm_lookup in ('AES', 'AESWRAP'): + algorithm_lookup = 'SYMMETRIC' + elif 'RSA' in algorithm_lookup: + algorithm_lookup = 'RSA' + + try: + key_generator = _ALGORITHM_GENERATE_MAP[algorithm_lookup] + except KeyError: + raise ValueError('Unknown algorithm: {}'.format(algorithm)) + + key, key_type, key_encoding = key_generator(key_length) + return cls(key=key, algorithm=algorithm, key_type=key_type, key_encoding=key_encoding) + + @property + def allowed_for_raw_materials(self): + """Only ``JceNameLocalDelegatedKey`` backed by AES keys are allowed to be used with + ``RawCryptographicMaterials``. + + :returns: decision + :rtype: bool + """ + return self.algorithm == 'AES' + + def _encrypt(self, algorithm, name, plaintext, additional_associated_data=None): + # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + """ + Encrypt data. + + :param str algorithm: Java StandardName transformation string of algorithm to use to encrypt data + https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html + :param str name: Name associated with plaintext data + :param bytes plaintext: Plaintext data to encrypt + :param dict additional_associated_data: Not used by all delegated keys, but if it + is, then if it is provided on encrypt it must be required on decrypt. + :returns: Encrypted ciphertext + :rtype: bytes + """ + encryptor = encryption.JavaCipher.from_transformation(algorithm) + return encryptor.encrypt(self.__key, plaintext) + + def _decrypt(self, algorithm, name, ciphertext, additional_associated_data=None): + # type: (Text, Text, bytes, Dict[Text, Text]) -> bytes + """Encrypt data. + + :param str algorithm: Java StandardName transformation string of algorithm to use to decrypt data + https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html + :param str name: Name associated with ciphertext data + :param bytes ciphertext: Ciphertext data to decrypt + :param dict additional_associated_data: Not used by ``JceNameLocalDelegatedKey`` + :returns: Decrypted plaintext + :rtype: bytes + """ + decryptor = encryption.JavaCipher.from_transformation(algorithm) + return decryptor.decrypt(self.__key, ciphertext) + + def _wrap(self, algorithm, content_key, additional_associated_data=None): + # type: (Text, bytes, Dict[Text, Text]) -> bytes + """Wrap content key. + + :param str algorithm: Text description of algorithm to use to wrap key + :param bytes content_key: Raw content key to wrap + :param dict additional_associated_data: Not used by ``JceNameLocalDelegatedKey`` + :returns: Wrapped key + :rtype: bytes + """ + wrapper = encryption.JavaCipher.from_transformation(algorithm) + return wrapper.wrap( + wrapping_key=self.__key, + key_to_wrap=content_key + ) + + def _unwrap(self, algorithm, wrapped_key, wrapped_key_algorithm, wrapped_key_type, additional_associated_data=None): + # type: (Text, bytes, Text, EncryptionKeyTypes, Dict[Text, Text]) -> DelegatedKey + """Wrap content key. + + :param str algorithm: Text description of algorithm to use to unwrap key + :param bytes content_key: Raw content key to wrap + :param str wrapped_key_algorithm: Text description of algorithm for unwrapped key to use + :param wrapped_key_type: Type of key to treat key as once unwrapped + :type wrapped_key_type: dynamodb_encryption_sdk.identifiers.EncryptionKeyTypes + :param dict additional_associated_data: Not used by ``JceNameLocalDelegatedKey`` + :returns: Delegated key using unwrapped key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + if wrapped_key_type is not EncryptionKeyTypes.SYMMETRIC: + raise UnwrappingError('Unsupported wrapped key type: "{}"'.format(wrapped_key_type)) + + unwrapper = encryption.JavaCipher.from_transformation(algorithm) + unwrapped_key = unwrapper.unwrap( + wrapping_key=self.__key, + wrapped_key=wrapped_key + ) + return JceNameLocalDelegatedKey( + key=unwrapped_key, + algorithm=wrapped_key_algorithm, + key_type=wrapped_key_type, + key_encoding=KeyEncodingType.RAW + ) + + def _sign(self, algorithm, data): + # type: (Text, bytes) -> bytes + """Sign data. + + :param str algorithm: Text description of algorithm to use to sign data + :param bytes data: Data to sign + :returns: Signature value + :rtype: bytes + """ + signer = authentication.JAVA_AUTHENTICATOR[algorithm] + return signer.sign(self.__key, data) + + def _verify(self, algorithm, signature, data): + # type: (Text, bytes, bytes) -> None + """Sign data. + + :param str algorithm: Text description of algorithm to use to verify signature + :param bytes signature: Signature to verify + :param bytes data: Data over which to verify signature + """ + verifier = authentication.JAVA_AUTHENTICATOR[algorithm] + verifier.verify(self.__key, signature, data) + + def _signing_algorithm(self): + # type: () -> Text + """Provides 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. + + :returns: Signing algorithm identifier + :rtype: str + """ + return self.algorithm diff --git a/src/dynamodb_encryption_sdk/encrypted/__init__.py b/src/dynamodb_encryption_sdk/encrypted/__init__.py new file mode 100644 index 00000000..1f717dd4 --- /dev/null +++ b/src/dynamodb_encryption_sdk/encrypted/__init__.py @@ -0,0 +1,77 @@ +# 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. +import attr +import copy +import six + +from dynamodb_encryption_sdk.identifiers import ItemAction +from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider +from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials +from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext + + +@attr.s(hash=False) +class CryptoConfig(object): + """Container for all configuration needed to encrypt or decrypt an item. + + :param materials_provider: Cryptographic materials provider to use + :type materials_provider: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider + :param encryption_context: Context data describing what is being encrypted or decrypted. + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :param attribute_actions: Description of what action should be taken for each attribute + :type attribute_actions: dynamodb_encryption_sdk.structures.AttributeActions + """ + materials_provider = attr.ib(validator=attr.validators.instance_of(CryptographicMaterialsProvider)) + encryption_context = attr.ib(validator=attr.validators.instance_of(EncryptionContext)) + attribute_actions = attr.ib(validator=attr.validators.instance_of(AttributeActions)) + + def __attrs_post_init__(self): + """Make sure that restricted, indexed, attributes are not being encrypted.""" + if self.encryption_context.partition_key_name is not None: + if self.attribute_actions.action(self.encryption_context.partition_key_name) is ItemAction.ENCRYPT_AND_SIGN: + raise Exception('TODO:Cannot encrypt partition key') + + if self.encryption_context.sort_key_name is not None: + if self.attribute_actions.action(self.encryption_context.sort_key_name) is ItemAction.ENCRYPT_AND_SIGN: + raise Exception('TODO:Cannot encrypt sort key') + + # TODO: secondary indexes? + # TODO: our own restricted attributes? + + def decryption_materials(self): + """Load decryption materials from instance resources. + + :returns: Decryption materials + :rtype: dynamodb_encryption_sdk.materials.DecryptionMaterials + """ + return self.materials_provider.decryption_materials(self.encryption_context) + + def encryption_materials(self): + """Load encryption materials from instance resources. + + :returns: Encryption materials + :rtype: dynamodb_encryption_sdk.materials.EncryptionMaterials + """ + return self.materials_provider.encryption_materials(self.encryption_context) + + def copy(self): + """Return a copy of this instance with a copied instance of its encryption context. + + :returns: New CryptoConfig identical to this one + :rtype: CryptoConfig + """ + return CryptoConfig( + materials_provider=self.materials_provider, + encryption_context=copy.copy(self.encryption_context), + attribute_actions=self.attribute_actions + ) diff --git a/src/dynamodb_encryption_sdk/encrypted/item.py b/src/dynamodb_encryption_sdk/encrypted/item.py new file mode 100644 index 00000000..ba8dbb9f --- /dev/null +++ b/src/dynamodb_encryption_sdk/encrypted/item.py @@ -0,0 +1,199 @@ +# 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. +"""Top-level functions for encrypting and decrypting DynamoDB items.""" +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Callable, Dict # pylint: disable=unused-import + from dynamodb_encryption_sdk.internal import dynamodb_types # pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from . import CryptoConfig +from dynamodb_encryption_sdk.exceptions import DecryptionError +from dynamodb_encryption_sdk.identifiers import ItemAction +from dynamodb_encryption_sdk.internal.crypto.authentication import sign_item, verify_item_signature +from dynamodb_encryption_sdk.internal.crypto.encryption import decrypt_attribute, encrypt_attribute +from dynamodb_encryption_sdk.internal.formatting.material_description import ( + deserialize as deserialize_material_description, serialize as serialize_material_description +) +from dynamodb_encryption_sdk.internal.identifiers import MaterialDescriptionKeys, MaterialDescriptionValues +from dynamodb_encryption_sdk.internal.formatting.transform import ddb_to_dict, dict_to_ddb +from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes + +__all__ = ('encrypt_dynamodb_item', 'encrypt_python_item', 'decrypt_dynamodb_item', 'decrypt_python_item') + + +def encrypt_dynamodb_item(item, crypto_config): + # type: (dynamodb_types.ITEM, CryptoConfig) -> dynamodb_types.ITEM + """Encrypt a DynamoDB item. + + .. note:: + + This handles DynamoDB-formatted items and is for use with the boto3 DynamoDB client. + + :param dict item: Plaintext DynamoDB item + :param crypto_config: Cryptographic configuration + :type crypto_config: dynamodb_encryption_sdk.encrypted.CryptoConfig + :returns: Encrypted and signed DynamoDB item + :rtype: dict + """ + if crypto_config.attribute_actions.take_no_actions: + # If we explicitly have been told not to do anything to this item, just copy it. + return item.copy() + + # TODO: Check for attributes that we write + crypto_config.materials_provider.refresh() + encryption_materials = crypto_config.encryption_materials() + + # Add the attribute encryption mode to the inner material description + # TODO: This is awkward...see if we can break this out any + encryption_mode = MaterialDescriptionValues.CBC_PKCS5_ATTRIBUTE_ENCRYPTION.value + inner_material_description = encryption_materials.material_description.copy() + inner_material_description[ + MaterialDescriptionKeys.ATTRIBUTE_ENCRYPTION_MODE.value + ] = encryption_mode + + algorithm_descriptor = encryption_materials.encryption_key.algorithm + encryption_mode + + encrypted_item = {} + for name, attribute in item.items(): + if crypto_config.attribute_actions.action(name) is not ItemAction.ENCRYPT_AND_SIGN: + encrypted_item[name] = attribute.copy() + continue + + encrypted_item[name] = encrypt_attribute( + attribute_name=name, + attribute=attribute, + encryption_key=encryption_materials.encryption_key, + algorithm=algorithm_descriptor + ) + + signature_attribute = sign_item(encrypted_item, encryption_materials.signing_key, crypto_config) + encrypted_item[ReservedAttributes.SIGNATURE.value] = signature_attribute + + try: + # Add the signing key algorithm identifier to the inner material description if provided + inner_material_description[ + MaterialDescriptionKeys.SIGNING_KEY_ALGORITHM.value + ] = encryption_materials.signing_key.signing_algorithm() + except NotImplementedError: + # Not all signing keys will provide this value + pass + + material_description_attribute = serialize_material_description(inner_material_description) + encrypted_item[ReservedAttributes.MATERIAL_DESCRIPTION.value] = material_description_attribute + + return encrypted_item + + +def encrypt_python_item(item, crypto_config): + # type: (dynamodb_types.ITEM, CryptoConfig) -> dynamodb_types.ITEM + """Encrypt a dictionary for DynamoDB. + + .. note:: + + This handles human-friendly dictionaries and is for use with the boto3 DynamoDB service or table resource. + + :param dict item: Plaintext dictionary + :param crypto_config: Cryptographic configuration + :type crypto_config: dynamodb_encryption_sdk.encrypted.CryptoConfig + :returns: Encrypted and signed dictionary + :rtype: dict + """ + ddb_item = dict_to_ddb(item) + encrypted_ddb_item = encrypt_dynamodb_item(ddb_item, crypto_config) + return ddb_to_dict(encrypted_ddb_item) + + +def decrypt_dynamodb_item(item, crypto_config): + # type: (dynamodb_types.ITEM, CryptoConfig) -> dynamodb_types.ITEM + """Decrypt a DynamoDB item. + + .. note:: + + This handles DynamoDB-formatted items and is for use with the boto3 DynamoDB client. + + :param dict item: Encrypted and signed DynamoDB item + :param crypto_config: Cryptographic configuration + :type crypto_config: dynamodb_encryption_sdk.encrypted.CryptoConfig + :returns: Plaintext DynamoDB item + :rtype: dict + """ + unique_actions = set([crypto_config.attribute_actions.default_action.name]) + unique_actions.update(set([action.name for action in crypto_config.attribute_actions.attribute_actions.values()])) + + if crypto_config.attribute_actions.take_no_actions: + # If we explicitly have been told not to do anything to this item, just copy it. + return item.copy() + + try: + signature_attribute = item.pop(ReservedAttributes.SIGNATURE.value) + except KeyError: + # The signature is always written, so if no signature is found then the item was not + # encrypted or signed. + raise DecryptionError('No signature attribute found in item') + + inner_crypto_config = crypto_config.copy() + # Retrieve the material description from the item if found. + try: + material_description_attribute = item.pop(ReservedAttributes.MATERIAL_DESCRIPTION.value) + except KeyError: + # If no material description is found, we use inner_crypto_config as-is. + pass + else: + # If material description is found, override the material description in inner_crypto_config. + material_description = deserialize_material_description(material_description_attribute) + inner_crypto_config.encryption_context.material_description = material_description + + decryption_materials = inner_crypto_config.decryption_materials() + + decryption_mode = inner_crypto_config.encryption_context.material_description.get( + MaterialDescriptionKeys.ATTRIBUTE_ENCRYPTION_MODE.value + ) + algorithm_descriptor = decryption_materials.decryption_key.algorithm + decryption_mode + + verify_item_signature(signature_attribute, item, decryption_materials.verification_key, inner_crypto_config) + + # Once the signature has been verified, actually decrypt the item attributes. + decrypted_item = {} + for name, attribute in item.items(): + if inner_crypto_config.attribute_actions.action(name) is not ItemAction.ENCRYPT_AND_SIGN: + decrypted_item[name] = attribute.copy() + continue + + decrypted_item[name] = decrypt_attribute( + attribute_name=name, + attribute=attribute, + decryption_key=decryption_materials.decryption_key, + algorithm=algorithm_descriptor + ) + return decrypted_item + + +def decrypt_python_item(item, crypto_config): + # type: (dynamodb_types.ITEM, CryptoConfig) -> dynamodb_types.ITEM + """Decrypt a dictionary for DynamoDB. + + .. note:: + + This handles human-friendly dictionaries and is for use with the boto3 DynamoDB service or table resource. + + :param dict item: Encrypted and signed dictionary + :param crypto_config: Cryptographic configuration + :type crypto_config: dynamodb_encryption_sdk.encrypted.CryptoConfig + :returns: Plaintext dictionary + :rtype: dict + """ + ddb_item = dict_to_ddb(item) + decrypted_ddb_item = decrypt_dynamodb_item(ddb_item, crypto_config) + return ddb_to_dict(decrypted_ddb_item) diff --git a/src/dynamodb_encryption_sdk/exceptions.py b/src/dynamodb_encryption_sdk/exceptions.py new file mode 100644 index 00000000..5cb1fa83 --- /dev/null +++ b/src/dynamodb_encryption_sdk/exceptions.py @@ -0,0 +1,86 @@ +# 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. + + +class DynamodbEncryptionSdkError(Exception): + """Base class for all custom exceptions.""" + + +class SerializationError(DynamodbEncryptionSdkError): + """Otherwise undifferentiated errors encountered while serializing data.""" + + +class DeserializationError(DynamodbEncryptionSdkError): + """Otherwise undifferentiated errors encountered while deserializing data.""" + + +class InvalidMaterialsetError(DeserializationError): + """Raised when errors are encountered processing a material description.""" + # TODO: MaterialDescription, not Materialset... + + +class InvalidMaterialsetVersionError(DeserializationError): + """Raised when a material description is encountered with an invalid version.""" + # TODO: MaterialDescription, not Materialset... + + +class InvalidAlgorithmError(DynamodbEncryptionSdkError): + """Raised when an invalid algorithm identifier is encountered.""" + + +class JceTransformationError(DynamodbEncryptionSdkError): + """""" + + +class DelegatedKeyError(DynamodbEncryptionSdkError): + """""" + + +class DelegatedKeyEncryptionError(DelegatedKeyError): + """""" + + +class DelegatedKeyDecryptionError(DelegatedKeyError): + """""" + + +class AwsKmsMaterialsProviderError(DynamodbEncryptionSdkError): + """""" + + +class UnknownRegionError(AwsKmsMaterialsProviderError): + """""" + + +class DecryptionError(DynamodbEncryptionSdkError): + """""" + + +class UnwrappingError(DynamodbEncryptionSdkError): + """""" + + +class EncryptionError(DynamodbEncryptionSdkError): + """""" + + +class WrappingError(DynamodbEncryptionSdkError): + """""" + + +class SigningError(DynamodbEncryptionSdkError): + """""" + + +class SignatureVerificationError(DynamodbEncryptionSdkError): + """""" diff --git a/src/dynamodb_encryption_sdk/identifiers.py b/src/dynamodb_encryption_sdk/identifiers.py new file mode 100644 index 00000000..e5ecb165 --- /dev/null +++ b/src/dynamodb_encryption_sdk/identifiers.py @@ -0,0 +1,47 @@ +# 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. +from enum import Enum + +__version__ = '0.0.0' + +LOGGER_NAME = 'dynamodb_encryption_sdk' + + +class ItemAction(Enum): + """Possible actions to take on an item attribute.""" + DO_NOTHING = 0 + SIGN_ONLY = 1 + ENCRYPT_AND_SIGN = 2 + + def __gt__(self, other): + return not self.__lt__(other) and not self.__eq__(other) + + def __lt__(self, other): + return self.value < other.value + + def __eq__(self, other): + return self.value == other.value + + +class EncryptionKeyTypes(Enum): + """Supported types of encryption keys.""" + SYMMETRIC = 0 + PRIVATE = 1 + PUBLIC = 2 + + +class KeyEncodingType(Enum): + """Supported key encoding schemes.""" + RAW = 0 + DER = 1 + PEM = 2 diff --git a/src/dynamodb_encryption_sdk/internal/__init__.py b/src/dynamodb_encryption_sdk/internal/__init__.py new file mode 100644 index 00000000..34bfd8a1 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/__init__.py @@ -0,0 +1,18 @@ +# 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. +"""Internal implementation details. + +.. warning:: + No guarantee is provided on the modules and APIs within this + namespace staying consistent. Directly reference at your own risk. +""" diff --git a/src/dynamodb_encryption_sdk/internal/crypto/__init__.py b/src/dynamodb_encryption_sdk/internal/crypto/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/crypto/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/src/dynamodb_encryption_sdk/internal/crypto/authentication.py b/src/dynamodb_encryption_sdk/internal/crypto/authentication.py new file mode 100644 index 00000000..e0e3031b --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/crypto/authentication.py @@ -0,0 +1,123 @@ +# 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. +"""Functions to handle calculating and verifying signatures of encrypted items.""" +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes + +from dynamodb_encryption_sdk.delegated_keys import DelegatedKey +from dynamodb_encryption_sdk.encrypted import CryptoConfig +from dynamodb_encryption_sdk.identifiers import ItemAction +from dynamodb_encryption_sdk.internal.formatting.serialize.attribute import serialize_attribute +from dynamodb_encryption_sdk.internal.identifiers import SignatureValues, Tag +from dynamodb_encryption_sdk.structures import AttributeActions + +__all__ = ('sign_item', 'verify_item_signature') + + +def sign_item(encrypted_item, signing_key, crypto_config): + # type: (dynamodb_types.ITEM, DelegatedKey, CryptoConfig) -> dynamodb_types.BINARY_ATTRIBUTE + """Generate the signature DynamoDB atttribute. + + :param dict encrypted_item: Encrypted DynamoDB item + :param signing_key: DelegatedKey to use to calculate the signature + :type signing_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param crypto_config: Cryptographic configuration + :type crypto_config: dynamodb_encryption_sdk.encrypted.CryptoConfig + :returns: Item signature DynamoDB attribute value + :rtype: dict + """ + signature = signing_key.sign( + algorithm=signing_key.algorithm, + data=_string_to_sign( + item=encrypted_item, + table_name=crypto_config.encryption_context.table_name, + attribute_actions=crypto_config.attribute_actions + ) + ) + return {Tag.BINARY.dynamodb_tag: signature} + + +def verify_item_signature(signature_attribute, encrypted_item, verification_key, crypto_config): + # type: (dynamodb_types.BINARY_ATTRIBUTE, dynamodb_types.ITEM, DelegatedKey, CryptoConfig) -> None + """Verify the item signature. + + :param dict signature_attribute: Item signature DynamoDB attribute value + :param dict encrypted_item: Encrypted DynamoDB item + :param verification_key: DelegatedKey to use to calculate the signature + :type verification_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param crypto_config: Cryptographic configuration + :type crypto_config: dynamodb_encryption_sdk.encrypted.CryptoConfig + """ + signature = signature_attribute[Tag.BINARY.dynamodb_tag] + verification_key.verify( + algorithm=verification_key.algorithm, + signature=signature, + data=_string_to_sign( + item=encrypted_item, + table_name=crypto_config.encryption_context.table_name, + attribute_actions=crypto_config.attribute_actions + ) + ) + + +def _string_to_sign(item, table_name, attribute_actions): + # type: (dynamodb_type.ITEM, Text, AttributeActions) -> bytes + """Generate the string to sign from an encrypted item and configuration. + + :param dict item: Encrypted DynamoDB item + :param str table_name: Table name to use when generating the string to sign + :type attribute_actions: dynamodb_encryption_sdk.structures.AttributeActions + """ + hasher = hashes.Hash( + hashes.SHA256(), + backend=default_backend() + ) + data_to_sign = bytearray() + data_to_sign.extend(_hash_data( + hasher=hasher, + data='TABLE>{} dynamodb_types.BINARY_ATTRIBUTE + """Encrypt a single DynamoDB attribute. + + :param str attribute_name: DynamoDB attribute name + :param dict attribute: Plaintext DynamoDB attribute + :param encryption_key: DelegatedKey to use to encrypt the attribute + :type encryption_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param str algorithm: Encryption algorithm descriptor (passed to encryption_key as algorithm) + :returns: Encrypted DynamoDB binary attribute + :rtype: dict + """ + serialized_attribute = serialize_attribute(attribute) + encrypted_attribute = encryption_key.encrypt( + algorithm=algorithm, + name=attribute_name, + plaintext=serialized_attribute + ) + return {Tag.BINARY.dynamodb_tag: encrypted_attribute} + + +def decrypt_attribute(attribute_name, attribute, decryption_key, algorithm): + # type: (Text, dynamodb_types.RAW_ATTRIBUTE, DelegatedKey, Text) -> dynamodb_types.RAW_ATTRIBUTE + """Decrypt a single DynamoDB attribute. + + :param str attribute_name: DynamoDB attribute name + :param dict attribute: Encrypted DynamoDB attribute + :param encryption_key: DelegatedKey to use to encrypt the attribute + :type encryption_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param str algorithm: Decryption algorithm descriptor (passed to encryption_key as algorithm) + :returns: Plaintext DynamoDB attribute + :rtype: dict + """ + encrypted_attribute = attribute[Tag.BINARY.dynamodb_tag] + decrypted_attribute = decryption_key.decrypt( + algorithm=algorithm, + name=attribute_name, + ciphertext=encrypted_attribute + ) + return deserialize_attribute(decrypted_attribute) diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/__init__.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/__init__.py new file mode 100644 index 00000000..22070dbc --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/__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. +"""""" diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py new file mode 100644 index 00000000..04a3469d --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/authentication.py @@ -0,0 +1,181 @@ +# 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 authentication resources for JCE bridge.""" +import abc + +import attr +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, hmac +from cryptography.hazmat.primitives.asymmetric import padding, rsa +import six + +from .primitives import load_rsa_key +from dynamodb_encryption_sdk.exceptions import InvalidAlgorithmError, SignatureVerificationError, SigningError + +__all__ = ('JavaAuthenticator', 'JavaMac', 'JavaSignature', 'JAVA_AUTHENTICATOR') + + +@six.add_metaclass(abc.ABCMeta) +class JavaAuthenticator(object): + """Parent class for all Java bridges that provide authentication characteristics.""" + + @abc.abstractmethod + def load_key(self, key, key_type, key_encoding): + """""" + + @abc.abstractmethod + def validate_algorithm(self, algorithm): + """""" + + @abc.abstractmethod + def sign(self, key, data): + """""" + + @abc.abstractmethod + def verify(self, key, signature, data): + """""" + + +@attr.s(hash=False) +class JavaMac(JavaAuthenticator): + """Symmetric MAC authenticators. + + https://docs.oracle.com/javase/8/docs/api/javax/crypto/Mac.html + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Mac + """ + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + algorithm_type = attr.ib() + hash_type = attr.ib() + + def _build_hmac_signer(self, key): + """""" + return self.algorithm_type( + key, + self.hash_type(), + backend=default_backend() + ) + + def load_key(self, key, key_type, key_encoding): + """""" + return key + + def validate_algorithm(self, algorithm): + # type: (Text) -> None + """Determine whether the requested algorithm name is compatible with this signature. + + :raises InvalidAlgorithmError: if specified algorithm name is not compatible with this authenticator + """ + if not algorithm.startswith(self.java_name): + raise InvalidAlgorithmError( + 'Requested algorithm "{requested}" is not compatible with signature "{actual}"'.format( + requested=algorithm, + actual=self.java_name + ) + ) + + def sign(self, key, data): + # type: (bytes, bytes) -> bytes + """Sign ``data`` using loaded ``key``. + + :param bytes key: Raw HMAC key + :param bytes data: Data to sign + :returns: Calculated signature + :rtype: bytes + """ + signer = self._build_hmac_signer(key) + signer.update(data) + return signer.finalize() + + def verify(self, key, signature, data): + """ + + :param bytes key: Raw HMAC key + :param bytes signature: Signature to verify + :param bytes data: Data over which to verify signature + """ + verifier = self._build_hmac_signer(key) + verifier.update(data) + verifier.verify(signature) + + +@attr.s(hash=False) +class JavaSignature(JavaAuthenticator): + """Asymmetric signature authenticators. + + https://docs.oracle.com/javase/8/docs/api/java/security/Signature.html + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Signature + """ + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + algorithm_type = attr.ib() + hash_type = attr.ib() + padding_type = attr.ib() + + def validate_algorithm(self, algorithm): + # type: (Text) -> None + """Determine whether the requested algorithm name is compatible with this signature. + + :raises InvalidAlgorithmError: if specified algorithm name is not compatible with this authenticator + """ + if not algorithm.endswith(self.java_name): + raise InvalidAlgorithmError( + 'Requested algorithm "{requested}" is not compatible with signature "{actual}"'.format( + requested=algorithm, + actual=self.java_name + ) + ) + + def load_key(self, key, key_type, key_encoding): + """""" + return load_rsa_key(key, key_type, key_encoding) + + def sign(self, key, data): + """""" + if hasattr(key, 'public_bytes'): + raise SigningError('"sign" is not supported by public keys') + # TODO: normalize to SigningError + return key.sign( + data, + self.padding_type(), + self.hash_type() + ) + + def verify(self, key, signature, data): + """""" + if hasattr(key, 'private_bytes'): + _key = key.public_key() + else: + _key = key + # TODO: normalize to SignatureVerificationError + _key.verify( + signature, + data, + self.padding_type(), + self.hash_type() + ) + + +JAVA_AUTHENTICATOR = { + 'HmacSHA224': JavaMac('HmacSHA224', hmac.HMAC, hashes.SHA224), + 'HmacSHA256': JavaMac('HmacSHA256', hmac.HMAC, hashes.SHA256), + 'HmacSHA384': JavaMac('HmacSHA384', hmac.HMAC, hashes.SHA384), + 'HmacSHA512': JavaMac('HmacSHA512', hmac.HMAC, hashes.SHA512), + 'SHA224withRSA': JavaSignature('SHA224withRSA', rsa, hashes.SHA224, padding.PKCS1v15), + 'SHA256withRSA': JavaSignature('SHA256withRSA', rsa, hashes.SHA256, padding.PKCS1v15), + 'SHA384withRSA': JavaSignature('SHA384withRSA', rsa, hashes.SHA384, padding.PKCS1v15), + 'SHA512withRSA': JavaSignature('SHA512withRSA', rsa, hashes.SHA512, padding.PKCS1v15) + # TODO: should we support these? + # HmacMD5 + # HmacSHA1 + # (NONE|SHA(1|224|256|384|512))with(|EC)DSA + # (NONE|SHA1)withRSA +} diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py new file mode 100644 index 00000000..cd7bace7 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/encryption.py @@ -0,0 +1,154 @@ +# 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. +"""Cipher resource for JCE bridge.""" +import attr + +from .primitives import ( + JAVA_ENCRYPTION_ALGORITHM, JavaEncryptionAlgorithm, JAVA_MODE, JavaMode, JAVA_PADDING, JavaPadding +) +from dynamodb_encryption_sdk.exceptions import JceTransformationError + +__all__ = ('JavaCipher',) + + +@attr.s(hash=False) +class JavaCipher(object): + """Defines the encryption cipher, mode, and padding type to use for encryption. + + https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html + + :param cipher: TODO: + :param mode: TODO: + :param padding: TODO: + """ + cipher = attr.ib(validator=attr.validators.instance_of(JavaEncryptionAlgorithm)) + mode = attr.ib(validator=attr.validators.instance_of(JavaMode)) + padding = attr.ib(validator=attr.validators.instance_of(JavaPadding)) + + def encrypt(self, key, data): + """Encrypt data using loaded key. + + :param key: Key loaded by ``cipher`` + :param bytes data: Data to encrypt + :returns: Encrypted data + :rtype: bytes + """ + return self.cipher.encrypt(key, data, self.mode, self.padding) + + def decrypt(self, key, data): + """Decrypt data using loaded key. + + :param key: Key loaded by ``cipher`` + :param bytes data: Data to decrypt + :returns: Decrypted data + :rtype: bytes + """ + return self.cipher.decrypt(key, data, self.mode, self.padding) + + def wrap(self, wrapping_key, key_to_wrap): + """Wrap key using loaded key. + + :param wrapping_key: Key loaded by ``cipher`` + :param bytes key_to_wrap: Key to wrap + :returns: Wrapped key + :rtype: bytes + """ + if hasattr(self.cipher, 'wrap'): + return self.cipher.wrap(wrapping_key, key_to_wrap) + return self.cipher.encrypt( + key=wrapping_key, + data=key_to_wrap, + mode=self.mode, + padding=self.padding + ) + + def unwrap(self, wrapping_key, wrapped_key): + """Wrap key using loaded key. + + :param wrapping_key: Key loaded by ``cipher`` + :param bytes wrapped_key: Wrapped key + :returns: Unwrapped key + :rtype: bytes + """ + if hasattr(self.cipher, 'unwrap'): + return self.cipher.unwrap(wrapping_key, wrapped_key) + return self.cipher.decrypt( + key=wrapping_key, + data=wrapped_key, + mode=self.mode, + padding=self.padding + ) + + @property + def transformation(self): + """Returns the Java transformation describing this JavaCipher. + https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + + :returns: Formatted transformation + :rtype: str + """ + return '{cipher}/{mode}/{padding}'.format( + cipher=self.cipher.java_name, + mode=self.mode.java_name, + padding=self.padding.java_name + ) + + @staticmethod + def _map_load_or_error(name_type, name, mappings): + """Load the requested name from mapping or raise an appropriate error. + + :param str name_type: Type of thing to load. This is used in the error message if name is not found in mappings. + :param str name: Name to locate in mappings + :param dict mappings: Dict in which to look for name + """ + try: + return mappings[name] + except KeyError: + raise JceTransformationError('Invalid {type} name: "{name}"'.format( + type=name_type, + name=name + )) + + @classmethod + def from_transformation(cls, cipher_transformation): + """Generates an JavaCipher object from the Java transformation. + https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + + :param str cipher_transformation: Formatted transformation + :returns: JavaCipher instance + :rtype: dynamodb_encryption_sdk.internal.structures.EncryptionClient + """ + if cipher_transformation == 'AESWrap': + # AESWrap does not support encrypt or decrypt, so mode and padding are never + # used, but we use ECB and NoPadding as placeholders to simplify handling. + return cls.from_transformation('AESWrap/ECB/NoPadding') + + if cipher_transformation == 'RSA': + # RSA does not use mode, but as with JCE, we use ECB as a placeholder to simplify handling. + return cls.from_transformation('RSA/ECB/PKCS1Padding') + + cipher_transformation_parts = cipher_transformation.split('/') + if len(cipher_transformation_parts) != 3: + raise JceTransformationError( + 'Invalid transformation: "{}": must be three parts ALGORITHM/MODE/PADDING, "RSA", or "AESWrap"'.format( + cipher_transformation + ) + ) + + cipher = cls._map_load_or_error('algorithm', cipher_transformation_parts[0], JAVA_ENCRYPTION_ALGORITHM) + mode = cls._map_load_or_error('mode', cipher_transformation_parts[1], JAVA_MODE) + padding = cls._map_load_or_error('padding', cipher_transformation_parts[2], JAVA_PADDING) + + return cls(cipher, mode, padding) diff --git a/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py new file mode 100644 index 00000000..69a12ce2 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/crypto/jce_bridge/primitives.py @@ -0,0 +1,488 @@ +# 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 primitive resources for JCE bridge.""" +import abc +import attr +import logging +import os + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import padding as symmetric_padding, hashes, serialization, keywrap +from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding, rsa +from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher +import six + +from dynamodb_encryption_sdk.exceptions import ( + DecryptionError, EncryptionError, InvalidAlgorithmError, UnwrappingError, WrappingError +) +from dynamodb_encryption_sdk.identifiers import EncryptionKeyTypes, KeyEncodingType, LOGGER_NAME + +_LOGGER = logging.getLogger(LOGGER_NAME) + + +class _NoPadding(object): + """Provide NoPadding padding object.""" + + class _NoPadder(symmetric_padding.PaddingContext): + """Provide padder/unpadder functionality for NoPadding.""" + + def update(self, data): + """Directly return the input data cast to bytes. + + :param bytes data: Data to (not) pad/unpad + :returns: (Not) padded/unpadded data + :rtype: bytes + """ + return data + + def finalize(self): + """Provide the finalize interface but returns an empty bytestring. + + :returns: Empty bytestring + :rtype: bytes + """ + return b'' + + def padder(self): + """Return NoPadder object. + + :returns: NoPadder object. + :rtype: _NoPadder + """ + return self._NoPadder() + + def unpadder(self): + """Return NoPadder object. + + :returns: NoPadder object. + :rtype: _NoPadder + """ + return self._NoPadder() + + +@six.add_metaclass(abc.ABCMeta) +class JavaPadding(object): + """Bridge the gap from the Java padding names and Python resources. + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + """ + + @abc.abstractmethod + def build(self, block_size): + """Build an instance of this padding type.""" + + +@attr.s(hash=False) +class SimplePadding(JavaPadding): + """Padding types that do not require any preparation.""" + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + padding = attr.ib() + + def build(self, block_size=None): + # type: (int) -> ANY + """Build an instance of this padding type. + + :param int block_size: Not used by SimplePadding. Ignored and not required. + :returns: Padding instance + """ + return self.padding() + + +@attr.s(hash=False) +class BlockSizePadding(JavaPadding): + """Padding types that require a block size input.""" + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + padding = attr.ib() + + def build(self, block_size): + # type: (int) -> ANY + """Build an instance of this padding type. + + :param int block_size: Block size of algorithm for which to build padder. + :returns: Padding instance + """ + return self.padding(block_size) + + +@attr.s(hash=False) +class OaepPadding(JavaPadding): + """OAEP padding types. These require more complex setup. + + .. warning:: + + By default, Java incorrectly implements RSA OAEP for all hash functions besides SHA1. + The same hashing algorithm should be used by both OAEP and the MGF, but by default + Java always uses SHA1 for the MGF. + """ + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + padding = attr.ib() + digest = attr.ib() + mgf = attr.ib() + mgf_digest = attr.ib() + + def build(self, block_size=None): + # type: (int) -> ANY + """Build an instance of this padding type. + + :param int block_size: Not used by OaepPadding. Ignored and not required. + :returns: Padding instance + """ + return self.padding( + mgf=self.mgf(algorithm=self.mgf_digest()), + algorithm=self.digest(), + label=None + ) + + +@attr.s(hash=False) +class JavaMode(object): + """Bridge the gap from the Java encryption mode names and Python resources. + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + """ + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + mode = attr.ib() + + def build(self, iv): + # type: (int) -> ANY + """Build an instance of this mode type. + + :param bytes iv: Initialization vector bytes + :returns: Mode instance + """ + return self.mode(iv) + + +@attr.s(hash=False) +class JavaEncryptionAlgorithm(object): + """Bridge the gap from the Java encryption algorithm names and Python resources. + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + """ + java_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + cipher = attr.ib() + + def validate_algorithm(self, algorithm): + # type: (Text) -> None + """Determine whether the requested algorithm name is compatible with this cipher""" + if not algorithm == self.java_name: + raise InvalidAlgorithmError( + 'Requested algorithm "{requested}" is not compatible with cipher "{actual}"'.format( + requested=algorithm, + actual=self.java_name + ) + ) + + +class JavaSymmetricEncryptionAlgorithm(JavaEncryptionAlgorithm): + """JavaEncryptionAlgorithm for symmetric algorithms. + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + """ + + def _disabled_encrypt(self, *args, **kwargs): + """Catcher for algorithms that do not support encryption.""" + raise NotImplementedError('"encrypt" is not supported by the "{}" algorithm'.format(self.java_name)) + + def _disabled_decrypt(self, *args, **kwargs): + """Catcher for algorithms that do not support decryption.""" + raise NotImplementedError('"decrypt" is not supported by the "{}" algorithm'.format(self.java_name)) + + def _disable_encryption(self): + # () -> None + """Enable encryption methods for ciphers that support them.""" + self.encrypt = self._disabled_encrypt + self.decrypt = self._disabled_decrypt + + def __attrs_post_init__(self): + # () -> None + """Disable encryption if algorithm is AESWrap.""" + if self.java_name == 'AESWrap': + self._disable_encryption() + + def load_key(self, key, key_type, key_encoding): + """Load a key from bytes. + + :param bytes key: Key bytes + :param key_type: Type of key + :type key_type: dynamodb_encryption_sdk.identifiers.EncryptionKeyTypes + :param key_encoding: Encoding used to serialize key + :type key_encoding: dynamodb_encryption_sdk.identifiers.KeyEncodingType + :returns: Loaded key + """ + if key_type is not EncryptionKeyTypes.SYMMETRIC: + raise ValueError('Invalid key type "{key_type}" for cipher "{cipher}"'.format( + key_type=key_type, + cipher=self.java_name + )) + + if key_encoding is not KeyEncodingType.RAW: + raise ValueError('Invalid key encoding "{key_encoding}" for cipher "{cipher}"'.format( + key_encoding=key_encoding, + cipher=self.java_name + )) + + return key + + def wrap(self, wrapping_key, key_to_wrap): + # type: (bytes, bytes) -> bytes + """Wrap key using AES keywrap. + + :param bytes wrapping_key: Loaded key with which to wrap + :param bytes key_to_wrap: Raw key to wrap + :returns: Wrapped key + :rtype: bytes + """ + if self.java_name not in ('AES', 'AESWrap'): + raise NotImplementedError('"wrap" is not supported by the "{}" cipher'.format(self.java_name)) + + try: + return keywrap.aes_key_wrap( + wrapping_key=wrapping_key, + key_to_wrap=key_to_wrap, + backend=default_backend() + ) + except Exception: + error_message = 'Key wrap failed' + _LOGGER.exception(error_message) + raise WrappingError(error_message) + + def unwrap(self, wrapping_key, wrapped_key): + # type: (bytes, bytes) -> bytes + """Unwrap key using AES keywrap. + + :param bytes wrapping_key: Loaded key with which to unwrap + :param bytes wrapped_key: Wrapped key to unwrap + :returns: Unwrapped key + :rtype: bytes + """ + if self.java_name not in ('AES', 'AESWrap'): + raise NotImplementedError('"unwrap" is not supported by this cipher') + + try: + return keywrap.aes_key_unwrap( + wrapping_key=wrapping_key, + wrapped_key=wrapped_key, + backend=default_backend() + ) + except Exception: + error_message = 'Key unwrap failed' + _LOGGER.exception(error_message) + raise UnwrappingError(error_message) + + def encrypt(self, key, data, mode, padding): + """Encrypt data using the supplied values. + + :param bytes key: Loaded encryption key + :param bytes data: Data to encrypt + :param mode: Encryption mode to use + :type mode: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaMode + :param padding: Padding mode to use + :type padding: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaPadding + :returns: IV prepended to encrypted data + :rtype: bytes + """ + try: + block_size = self.cipher.block_size + iv_len = block_size // 8 + iv = os.urandom(iv_len) + + encryptor = Cipher( + self.cipher(key), + mode.build(iv), + backend=default_backend() + ).encryptor() + padder = padding.build(block_size).padder() + + padded_data = padder.update(data) + padder.finalize() + return iv + encryptor.update(padded_data) + encryptor.finalize() + except Exception: + error_message = 'Encryption failed' + _LOGGER.exception(error_message) + raise EncryptionError(error_message) + + def decrypt(self, key, data, mode, padding): + """Decrypt data using the supplied values. + + :param bytes key: Loaded decryption key + :param bytes data: IV prepended to encrypted data + :param mode: Decryption mode to use + :type mode: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaMode + :param padding: Padding mode to use + :type padding: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaPadding + :returns: Decrypted data + :rtype: bytes + """ + try: + block_size = self.cipher.block_size + iv_len = block_size // 8 + iv = data[:iv_len] + data = data[iv_len:] + + decryptor = Cipher( + self.cipher(key), + mode.build(iv), + backend=default_backend() + ).decryptor() + decrypted_data = decryptor.update(data) + decryptor.finalize() + + unpadder = padding.build(block_size).unpadder() + return unpadder.update(decrypted_data) + unpadder.finalize() + except Exception: + error_message = 'Decryption failed' + _LOGGER.exception(error_message) + raise DecryptionError(error_message) + + +_RSA_KEY_LOADING = { + EncryptionKeyTypes.PRIVATE: { + KeyEncodingType.DER: serialization.load_der_private_key, + KeyEncodingType.PEM: serialization.load_pem_private_key + }, + EncryptionKeyTypes.PUBLIC: { + KeyEncodingType.DER: serialization.load_der_public_key, + KeyEncodingType.PEM: serialization.load_pem_public_key + } +} + + +def load_rsa_key(key, key_type, key_encoding): + """""" + try: + loader = _RSA_KEY_LOADING[key_type][key_encoding] + except KeyError: + raise Exception('Invalid key type: {}'.format(key_type)) + + kwargs = dict(data=key, backend=default_backend()) + if key_type is EncryptionKeyTypes.PRIVATE: + kwargs['password'] = None + + return loader(**kwargs) + + +_KEY_LOADERS = { + rsa: load_rsa_key +} + + +class JavaAsymmetricEncryptionAlgorithm(JavaEncryptionAlgorithm): + """JavaEncryptionAlgorithm for asymmetric algorithms. + + https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher + """ + + def load_key(self, key, key_type, key_encoding): + """Load a key from bytes. + + :param bytes key: Key bytes + :param key_type: Type of key + :type key_type: dynamodb_encryption_sdk.identifiers.EncryptionKeyTypes + :param key_encoding: Encoding used to serialize key + :type key_encoding: dynamodb_encryption_sdk.identifiers.KeyEncodingType + :returns: Loaded key + """ + if key_type not in (EncryptionKeyTypes.PRIVATE, EncryptionKeyTypes.PUBLIC): + raise ValueError('Invalid key type "{key_type}" for cipher "{cipher}"'.format( + key_type=key_type, + cipher=self.java_name + )) + + if key_encoding not in (KeyEncodingType.DER, KeyEncodingType.PEM): + raise ValueError('Invalid key encoding "{key_encoding}" for cipher "{cipher}"'.format( + key_encoding=key_encoding, + cipher=self.java_name + )) + + return _KEY_LOADERS[self.cipher](key, key_type, key_encoding) + + def encrypt(self, key, data, mode, padding): + """Encrypt data using the supplied values. + + :param bytes key: Loaded encryption key + :param bytes data: Data to encrypt + :param mode: Encryption mode to use (not used by ``JavaAsymmetricEncryptionAlgorithm``) + :type mode: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaMode + :param padding: Padding mode to use + :type padding: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaPadding + :returns: Encrypted data + :rtype: bytes + """ + if hasattr(key, 'private_bytes'): + _key = key.public_key() + else: + _key = key + try: + return _key.encrypt(data, padding.build()) + except Exception: + error_message = 'Encryption failed' + _LOGGER.exception(error_message) + raise EncryptionError(error_message) + + def decrypt(self, key, data, mode, padding): + """Decrypt data using the supplied values. + + :param bytes key: Loaded decryption key + :param bytes data: IV prepended to encrypted data + :param mode: Decryption mode to use (not used by ``JavaAsymmetricEncryptionAlgorithm``) + :type mode: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaMode + :param padding: Padding mode to use + :type padding: dynamodb_encryption_sdk.internal.crypto.jce_bridge.primitives.JavaPadding + :returns: Decrypted data + :rtype: bytes + """ + if hasattr(key, 'public_bytes'): + raise NotImplementedError('TODO:"decrypt" is not supported by public keys') + try: + return key.decrypt(data, padding.build()) + except Exception: + error_message = 'Decryption failed' + _LOGGER.exception(error_message) + raise DecryptionError(error_message) + + +JAVA_ENCRYPTION_ALGORITHM = { + 'RSA': JavaAsymmetricEncryptionAlgorithm('RSA', rsa), + 'AES': JavaSymmetricEncryptionAlgorithm('AES', algorithms.AES), + 'AESWrap': JavaSymmetricEncryptionAlgorithm('AESWrap', algorithms.AES) + # TODO: Should we support these? + # DES : pretty sure we don't want to support this + # DESede : pretty sure we don't want to support this + # 'BLOWFISH': JavaSymmetricEncryptionAlgorithm('Blowfish', algorithms.Blowfish) +} +JAVA_MODE = { + 'ECB': JavaMode('ECB', modes.ECB), + 'CBC': JavaMode('CBC', modes.CBC), + 'CTR': JavaMode('CTR', modes.CTR), + 'GCM': JavaMode('GCM', modes.GCM) + # TODO: Should we support these? + # 'OFB': JavaMode('OFB', modes.OFB) + # 'CFB': JavaMode('CFB', modes.CFB) + # 'CFB8': JavaMode('CFB8', modes.CFB8) +} +JAVA_PADDING = { + 'NoPadding': SimplePadding('NoPadding', _NoPadding), + 'PKCS1Padding': SimplePadding('PKCS1Padding', asymmetric_padding.PKCS1v15), + # PKCS7 padding is a generalization of PKCS5 padding. + 'PKCS5Padding': BlockSizePadding('PKCS5Padding', symmetric_padding.PKCS7), + # By default, Java incorrectly implements RSA OAEP for all hash functions besides SHA1. + # The same hashing algorithm should be used by both OAEP and the MGF, but by default + # Java always uses SHA1 for the MGF. + 'OAEPWithSHA-1AndMGF1Padding': OaepPadding( + 'OAEPWithSHA-1AndMGF1Padding', asymmetric_padding.OAEP, hashes.SHA1, asymmetric_padding.MGF1, hashes.SHA1 + ), + 'OAEPWithSHA-256AndMGF1Padding': OaepPadding( + 'OAEPWithSHA-256AndMGF1Padding', asymmetric_padding.OAEP, hashes.SHA256, asymmetric_padding.MGF1, hashes.SHA1 + ), + 'OAEPWithSHA-384AndMGF1Padding': OaepPadding( + 'OAEPWithSHA-384AndMGF1Padding', asymmetric_padding.OAEP, hashes.SHA384, asymmetric_padding.MGF1, hashes.SHA1 + ), + 'OAEPWithSHA-512AndMGF1Padding': OaepPadding( + 'OAEPWithSHA-512AndMGF1Padding', asymmetric_padding.OAEP, hashes.SHA512, asymmetric_padding.MGF1, hashes.SHA1 + ) +} diff --git a/src/dynamodb_encryption_sdk/internal/defaults.py b/src/dynamodb_encryption_sdk/internal/defaults.py new file mode 100644 index 00000000..d262409a --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/defaults.py @@ -0,0 +1,17 @@ +# 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. +"""""" + +ENCODING = 'utf-8' +LOGGING_NAME = 'dynamodb_encryption_sdk' +MATERIAL_DESCRIPTION_VERSION = b'\00' * 4 diff --git a/src/dynamodb_encryption_sdk/internal/dynamodb_types.py b/src/dynamodb_encryption_sdk/internal/dynamodb_types.py new file mode 100644 index 00000000..abac1d3b --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/dynamodb_types.py @@ -0,0 +1,19 @@ +"""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 + + ATTRIBUTE = 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 + BINARY = ByteString + BINARY_ATTRIBUTE = Dict[Text, BINARY] + SET = List # DynamoDB TypeSerializer converts sets into lists + MAP = RAW_ATTRIBUTE + LIST = List[RAW_ATTRIBUTE] +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/formatting/__init__.py b/src/dynamodb_encryption_sdk/internal/formatting/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/src/dynamodb_encryption_sdk/internal/formatting/deserialize/__init__.py b/src/dynamodb_encryption_sdk/internal/formatting/deserialize/__init__.py new file mode 100644 index 00000000..23196684 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/deserialize/__init__.py @@ -0,0 +1,84 @@ +# 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. +"""Helper functions for deserializing values.""" +import struct + +from dynamodb_encryption_sdk.exceptions import DeserializationError + +__all__ = ('unpack_value', 'decode_length', 'decode_value', 'decode_tag') + + +def unpack_value(format_string, stream): + """Helper function to unpack struct data from a stream and update the signature verifier. + + :param str format_string: Struct format string + :param stream: Source data stream + :type stream: io.BytesIO + :returns: Unpacked values + :rtype: tuple + """ + message_bytes = stream.read(struct.calcsize(format_string)) + return struct.unpack(format_string, message_bytes) + + +def decode_length(stream): + """Decode the length of a value from a serialized stream. + + :param stream: Source data stream + :type stream: io.BytesIO + :returns: Decoded length + :rtype: int + """ + (value,) = unpack_value('>I', stream) + return value + + +def decode_value(stream): + """Decode the contents of a value from a serialized stream. + + :param stream: Source data stream + :type stream: io.BytesIO + :returns: Decoded value + :rtype: bytes + """ + length = decode_length(stream) + (value,) = unpack_value('>{:d}s'.format(length), stream) + return value + + +def decode_byte(stream): + """Decode a single raw byte from a serialized stream (used for deserialize bool). + + :param stream: Source data stream + :type stream: io.BytesIO + :returns: Decoded value + :rtype: bytes + """ + (value,) = unpack_value('>1s', stream) + return value + + +def decode_tag(stream): + """Decode a tag value from a serialized stream. + + :param stream: Source data stream + :type stream: io.BytesIO + :returns: Decoded tag + :rtype: bytes + """ + (reserved, tag) = unpack_value('>cc', stream) + + if reserved != b'\x00': + raise DeserializationError('Invalid tag: reserved byte is not null') + + return tag diff --git a/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py b/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py new file mode 100644 index 00000000..32bd10a6 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/deserialize/attribute.py @@ -0,0 +1,256 @@ +# 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. +"""Tooling for deserializing attributes.""" +import codecs +from decimal import Decimal +import io +import logging +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 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 boto3.dynamodb.types import Binary + +from dynamodb_encryption_sdk.exceptions import DeserializationError +from dynamodb_encryption_sdk.internal.defaults import ENCODING, LOGGING_NAME +from dynamodb_encryption_sdk.internal.formatting.deserialize import decode_byte, decode_length, decode_tag, decode_value +from dynamodb_encryption_sdk.internal.identifiers import Tag, TagValues +from dynamodb_encryption_sdk.internal.str_ops import to_str + +__all__ = ('deserialize_attribute',) +_LOGGER = logging.getLogger(LOGGING_NAME) + + +def deserialize_attribute(serialized_attribute): # noqa: C901 pylint: disable=too-many-locals + # type: (bytes) -> dynamodb_types.RAW_ATTRIBUTE + """Deserializes serialized attributes for decryption.""" + + def _transform_binary_value(value): + # (bytes) -> bytes + """Transforms a serialized binary value. + + :param bytes value: Raw deserialized value + :rtype: bytes + """ + if isinstance(value, Binary): + return value.value + return value + + def _deserialize_binary(stream): + # type: (io.BytesIO) -> Dict[str, bytes] + """Deserializes a binary object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + value = decode_value(stream) + return {Tag.BINARY.dynamodb_tag: _transform_binary_value(value)} + + def _transform_string_value(value): + # (bytes) -> dynamodb_types.STRING + """Transforms a serialized string value. + + :param bytes value: Raw deserialized value + :rtype: dynamodb_encryption_sdk.internal.dynamodb_types.STRING + """ + return codecs.decode(value, ENCODING) + + def _deserialize_string(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.STRING] + """Deserializes a string object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + value = decode_value(stream) + return {Tag.STRING.dynamodb_tag: _transform_string_value(value)} + + def _transform_number_value(value): + # (bytes) -> dynamodb_types.STRING + """Transforms a serialized number value. + + :param bytes value: Raw deserialized value + :rtype: dynamodb_encryption_sdk.internal.dynamodb_types.STRING + """ + raw_value = codecs.decode(value, ENCODING) + decimal_value = Decimal(to_str(raw_value)) + return str(decimal_value.normalize()) + + def _deserialize_number(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.STRING] + """Deserializes a number object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + value = decode_value(stream) + return {Tag.NUMBER.dynamodb_tag: _transform_number_value(value)} + + _boolean_map = { + TagValues.FALSE.value: False, + TagValues.TRUE.value: True + } + + def _deserialize_boolean(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.BOOLEAN] + """Deserializes a boolean object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + value = decode_byte(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] + """Deserializes a null object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + return {Tag.NULL.dynamodb_tag: True} + + def _deserialize_set(stream, member_transform): + # type: (io.BytesIO, Callable) -> List[Union[dynamodb_types.BINARY, dynamodb_types.STRING]] + """Deserializes contents of serialized set. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: list + """ + member_count = decode_length(stream) + return sorted([ + member_transform(decode_value(stream)) + for _ in range(member_count) + ]) + + def _deserialize_binary_set(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.SET[dynamodb_types.BINARY]] + """Deserializes a binary set object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + 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]] + """Deserializes a string set object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + 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]] + """Deserializes a number set object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + return {Tag.NUMBER_SET.dynamodb_tag: _deserialize_set(stream, _transform_number_value)} + + def _deserialize_list(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.LIST] + """Deserializes a list object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + member_count = decode_length(stream) + return {Tag.LIST.dynamodb_tag: [ + _deserialize(stream) + for _ in range(member_count) + ]} + + def _deserialize_map(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.MAP] + """Deserializes a map object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + member_count = decode_length(stream) + members = {} + for _ in range(member_count): + key = _deserialize(stream) + if Tag.STRING.dynamodb_tag not in key: + raise DeserializationError( + 'Malformed serialized map: found "{}" as map key.'.format(list(key.keys())[0]) + ) + + value = _deserialize(stream) + members[key[Tag.STRING.dynamodb_tag]] = value + + return {Tag.MAP.dynamodb_tag: members} + + def _deserialize_function(tag): + # type: (bytes) -> Callable + """Identifies the correct deserialization function based on the provided tag. + + :param tag: Identifying tag, read from start of serialized object + :type tag: dynamodb_encryption_sdk.internal.identifiers.Tag + :rtype: callable + """ + deserialize_functions = { + Tag.BINARY.tag: _deserialize_binary, + Tag.BINARY_SET.tag: _deserialize_binary_set, + Tag.NUMBER.tag: _deserialize_number, + Tag.NUMBER_SET.tag: _deserialize_number_set, + Tag.STRING.tag: _deserialize_string, + Tag.STRING_SET.tag: _deserialize_string_set, + Tag.BOOLEAN.tag: _deserialize_boolean, + Tag.NULL.tag: _deserialize_null, + Tag.LIST.tag: _deserialize_list, + Tag.MAP.tag: _deserialize_map + } + try: + return deserialize_functions[tag] + except KeyError: + raise DeserializationError('Unsupported tag: "{}"'.format(tag)) + + def _deserialize(stream): + # type: (io.BytesIO) -> Dict[str, dynamodb_types.RAW_ATTRIBUTE] + """Deserializes a serialized object. + + :param stream: Stream containing serialized object + :type stream: io.BytesIO + :rtype: dict + """ + try: + tag = decode_tag(stream) + return _deserialize_function(tag)(stream) + except struct.error: + raise DeserializationError('Malformed serialized data') + + if not serialized_attribute: + raise DeserializationError('Empty serialized attribute data') + + stream = io.BytesIO(serialized_attribute) + return _deserialize(stream) diff --git a/src/dynamodb_encryption_sdk/internal/formatting/material_description.py b/src/dynamodb_encryption_sdk/internal/formatting/material_description.py new file mode 100644 index 00000000..ac00426a --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/material_description.py @@ -0,0 +1,103 @@ +# 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. +"""Tools for serializing and deserializing material descriptions.""" +import io +import logging +import struct + +from .deserialize import decode_value, unpack_value +from .serialize import encode_value +from dynamodb_encryption_sdk.exceptions import InvalidMaterialsetError, InvalidMaterialsetVersionError +from dynamodb_encryption_sdk.internal.defaults import LOGGING_NAME, MATERIAL_DESCRIPTION_VERSION +from dynamodb_encryption_sdk.internal.identifiers import Tag +from dynamodb_encryption_sdk.internal.str_ops import to_bytes, to_str + +__all__ = ('serialize', 'deserialize') +_LOGGER = logging.getLogger(LOGGING_NAME) + + +def serialize(material_description): + # type: (Dict[Text, Text]) -> dynamodb_types.BINARY_ATTRIBUTE + """Serialize a material description dictionary into a DynamodDB attribute. + + :param dict material_description: Material description dictionary + :returns: Serialized material description as a DynamoDB binary attribute value + :rtype: dict + """ + material_description_bytes = bytearray(MATERIAL_DESCRIPTION_VERSION) + + # TODO: verify Java sorting order + for name, value in sorted(material_description.items(), key=lambda x: x[0]): + try: + material_description_bytes.extend(encode_value(to_bytes(name))) + material_description_bytes.extend(encode_value(to_bytes(value))) + except (TypeError, struct.error): + raise InvalidMaterialsetError('Invalid name or value in material description: "{name}"="{value}"'.format( + name=name, + value=value + )) + + return {Tag.BINARY.dynamodb_tag: bytes(material_description_bytes)} + + +def deserialize(serialized_material_description): + # type: (dynamodb_types.BINARY_ATTRIBUTE) -> Dict[Text, Text] + """Deserialize a serialized material description attribute into a material description dictionary. + + :param dict serialized_material_description: DynamoDB attribute value containing serialized material description. + :returns: Material description dictionary + :rtype: dict + :raises InvalidMaterialsetError: if material description is invalid or malformed + """ + try: + _raw_material_description = serialized_material_description[Tag.BINARY.dynamodb_tag] + + material_description_bytes = io.BytesIO(_raw_material_description) + total_bytes = len(_raw_material_description) + except (TypeError, KeyError): + message = 'Invalid material description' + _LOGGER.exception(message) + raise InvalidMaterialsetError(message) + # We don't currently do anything with the version, but do check to make sure it is the one we know about. + _read_version(material_description_bytes) + + material_description = {} + try: + while material_description_bytes.tell() < total_bytes: + name = to_str(decode_value(material_description_bytes)) + value = to_str(decode_value(material_description_bytes)) + material_description[name] = value + except struct.error: + message = 'Invalid material description' + _LOGGER.exception(message) + raise InvalidMaterialsetError(message) + return material_description + + +def _read_version(material_description_bytes): + # type: (io.BytesIO) -> None + """Read the version from the serialized material description and raise an error if it is unknown. + + :param material_description_bytes: serializezd material description + :type material_description_bytes: io.BytesIO + :raises InvalidMaterialsetError: if malformed version + :raises InvalidMaterialsetVersionError: if unknown version is found + """ + try: + (version,) = unpack_value('>4s', material_description_bytes) + except struct.error: + message = 'Malformed material description version' + _LOGGER.exception(message) + raise InvalidMaterialsetError(message) + if version != MATERIAL_DESCRIPTION_VERSION: + raise InvalidMaterialsetVersionError('Invalid material description version: {}'.format(repr(version))) diff --git a/src/dynamodb_encryption_sdk/internal/formatting/serialize/__init__.py b/src/dynamodb_encryption_sdk/internal/formatting/serialize/__init__.py new file mode 100644 index 00000000..de34b905 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/serialize/__init__.py @@ -0,0 +1,49 @@ +# 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. +"""Helper functions for serializing values.""" +import struct + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Sized # pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +__all__ = ('encode_length', 'encode_value') + + +def encode_length(attribute): + # type: (Sized) -> bytes + """Encodes the length of the attribute as an unsigned int. + + :param attribute: Attribute with length value + :returns: Encoded value + :rtype: bytes + """ + return struct.pack('>I', len(attribute)) + + +def encode_value(value): + # type: (bytes) -> bytes + """Encodes the value in Length-Value format. + + :param value: Value to encode + :type value: six.string_types or :class:`boto3.dynamodb_encryption_sdk.types.Binary` + :returns: Length-Value encoded value + :rtype: bytes + """ + return struct.pack( + '>I{attr_len:d}s'.format(attr_len=len(value)), + len(value), + value + ) diff --git a/src/dynamodb_encryption_sdk/internal/formatting/serialize/attribute.py b/src/dynamodb_encryption_sdk/internal/formatting/serialize/attribute.py new file mode 100644 index 00000000..9ab0afd6 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/serialize/attribute.py @@ -0,0 +1,251 @@ +# 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. +"""Tooling for serializing attributes.""" +import io +import logging + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Callable # 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 boto3.dynamodb.types import Binary, DYNAMODB_CONTEXT + +from dynamodb_encryption_sdk.exceptions import SerializationError +from dynamodb_encryption_sdk.internal.defaults import LOGGING_NAME +from dynamodb_encryption_sdk.internal.formatting.serialize import encode_length, encode_value +from dynamodb_encryption_sdk.internal.identifiers import Tag, TagValues +from dynamodb_encryption_sdk.internal.str_ops import to_bytes +from dynamodb_encryption_sdk.internal.utils import sorted_key_map + +__all__ = ('serialize_attribute',) +_LOGGER = logging.getLogger(LOGGING_NAME) +_RESERVED = b'\x00' + + +def serialize_attribute(attribute): # noqa: C901 pylint: disable=too-many-locals + # type: (dynamodb_types.RAW_ATTRIBUTE) -> bytes + """Serializes a raw attribute to a byte string as defined for the DynamoDB Client-Side Encryption Standard. + + :param attribute: Item attribute value + :returns: Serialized attribute + :rtype: bytes + """ + + def _transform_binary_value(value): + # type: (dynamodb_types.BINARY) -> bytes + """ + :param value: Input value + :type value: boto3.dynamodb.types.Binary + :returns: bytes value + :rtype: bytes + """ + if isinstance(value, Binary): + return bytes(value.value) + return bytes(value) + + def _serialize_binary(_attribute): + # type: (dynamodb_types.BINARY) -> bytes + """ + :param _attribute: Attribute to serialize + :type _attribute: boto3.dynamodb.types.Binary + :returns: Serialized _attribute + :rtype: bytes + """ + return _RESERVED + Tag.BINARY.tag + encode_value(_transform_binary_value(_attribute)) + + def _transform_number_value(value): + # type: (str) -> bytes + """ + :param value: Input value + :type value: numbers.Number + :returns: bytes value + :rtype: bytes + """ + # At this point we are receiving values which have already been transformed + # by dynamodb.TypeSerializer, so all numbers are str. However, TypeSerializer + # leaves trailing zeros if they are defined in the Decimal call, but we need to + # strip all trailing zeros. + decimal_value = DYNAMODB_CONTEXT.create_decimal(value) + raw_value = '{:f}'.format(decimal_value.normalize()) + return to_bytes(raw_value) + + def _serialize_number(_attribute): + # type: (str) -> bytes + """ + :param _attribute: Attribute to serialize + :type _attribute: numbers.Number + :returns: Serialized _attribute + :rtype: bytes + """ + return _RESERVED + Tag.NUMBER.tag + encode_value(_transform_number_value(_attribute)) + + def _transform_string_value(value): + # type: (dynamodb_types.STRING) -> bytes + """ + :param value: Input value + :type value: bytes or str + :returns: bytes value + :rtype: bytes + """ + return to_bytes(value) + + def _serialize_string(_attribute): + # type: (dynamodb_types.STRING) -> bytes + """ + :param _attribute: Attribute to serialize + :type _attribute: six.string_types + :returns: Serialized _attribute + :rtype: bytes + """ + return _RESERVED + Tag.STRING.tag + encode_value(_transform_string_value(_attribute)) + + def _serialize_boolean(_attribute): + # type: (dynamodb_types.BOOLEAN) -> bytes + """ + :param bool _attribute: Attribute to serialize + :returns: Serialized _attribute + :rtype: bytes + """ + _attribute_value = TagValues.TRUE.value if _attribute else TagValues.FALSE.value + return _RESERVED + Tag.BOOLEAN.tag + _attribute_value + + def _serialize_null(_attribute): + # type: (dynamodb_types.NULL) -> bytes + """ + :param _attribute: Attribute to serialize + :type _attribute: types.NoneType + :returns: Serialized _attribute + :rtype: bytes + """ + return _RESERVED + Tag.NULL.tag + + def _serialize_set(tag, _attribute, member_function): + # type: (Tag, dynamodb_types.SET[dynamodb_types.ATTRIBUTE], Callable) -> bytes + """ + :param bytes tag: Tag to identify this set + :param set _attribute: Attribute to serialize + :param member_function: Serialization function for members + :returns: Serialized _attribute + :rtype: bytes + """ + serialized_attribute = io.BytesIO() + serialized_attribute.write(_RESERVED) + serialized_attribute.write(tag.tag) + serialized_attribute.write(encode_length(_attribute)) + + encoded_members = [] + for member in _attribute: + encoded_members.append(member_function(member)) + for member in sorted(encoded_members): + serialized_attribute.write(encode_value(member)) + + return serialized_attribute.getvalue() + + def _serialize_binary_set(_attribute): + # type: (dynamodb_types.SET[dynamodb_types.ATTRIBUTE]) -> bytes + """ + :param set _attribute: Attribute to serialize + :returns: Serialized _attribute + :rtype: bytes + """ + return _serialize_set(Tag.BINARY_SET, _attribute, _transform_binary_value) + + def _serialize_number_set(_attribute): + # type: (dynamodb_types.SET[dynamodb_types.ATTRIBUTE]) -> bytes + """ + :param set _attribute: Attribute to serialize + :returns: Serialized _attribute + :rtype: bytes + """ + return _serialize_set(Tag.NUMBER_SET, _attribute, _transform_number_value) + + def _serialize_string_set(_attribute): + # type: (dynamodb_types.SET[dynamodb_types.ATTRIBUTE]) -> bytes + """ + :param set _attribute: Attribute to serialize + :returns: Serialized _attribute + :rtype: bytes + """ + return _serialize_set(Tag.STRING_SET, _attribute, _transform_string_value) + + def _serialize_list(_attribute): + # type: (dynamodb_types.LIST) -> bytes + """ + :param list _attribute: Attribute to serialize + :returns: Serialized _attribute + :rtype: bytes + """ + serialized_attribute = io.BytesIO() + serialized_attribute.write(_RESERVED) + serialized_attribute.write(Tag.LIST.tag) + serialized_attribute.write(encode_length(_attribute)) + for member in _attribute: + serialized_attribute.write(serialize_attribute(member)) + + return serialized_attribute.getvalue() + + def _serialize_map(_attribute): + # type: (dynamodb_types.MAP) -> bytes + """ + :param list _attribute: Attribute to serialize + :returns: Serialized _attribute + :rtype: bytes + """ + serialized_attribute = io.BytesIO() + serialized_attribute.write(_RESERVED) + serialized_attribute.write(Tag.MAP.tag) + serialized_attribute.write(encode_length(_attribute)) + + sorted_items = sorted_key_map( + item=_attribute, + transform=_transform_string_value + ) + + for key, value, _original_key in sorted_items: + serialized_attribute.write(_serialize_string(key)) + serialized_attribute.write(serialize_attribute(value)) + + return serialized_attribute.getvalue() + + def _serialize_function(dynamodb_tag): + # type: (str) -> Callable[[dynamodb_types.ATTRIBUTE], bytes] + """Locates the appropriate serialization function for the specified DynamoDB attribute tag.""" + serialize_functions = { + Tag.BINARY.dynamodb_tag: _serialize_binary, + Tag.BINARY_SET.dynamodb_tag: _serialize_binary_set, + Tag.NUMBER.dynamodb_tag: _serialize_number, + Tag.NUMBER_SET.dynamodb_tag: _serialize_number_set, + Tag.STRING.dynamodb_tag: _serialize_string, + Tag.STRING_SET.dynamodb_tag: _serialize_string_set, + Tag.BOOLEAN.dynamodb_tag: _serialize_boolean, + Tag.NULL.dynamodb_tag: _serialize_null, + Tag.LIST.dynamodb_tag: _serialize_list, + Tag.MAP.dynamodb_tag: _serialize_map + } + try: + return serialize_functions[dynamodb_tag] + except KeyError: + raise SerializationError('Unsupported DynamoDB data type: "{}"'.format(dynamodb_tag)) + + if not isinstance(attribute, dict): + raise TypeError('Invalid attribute type "{}": must be dict'.format(type(attribute))) + + if len(attribute) != 1: + raise SerializationError('cannot serialize attribute: incorrect number of members {} != 1'.format( + len(attribute) + )) + key, value = list(attribute.items())[0] + return _serialize_function(key)(value) diff --git a/src/dynamodb_encryption_sdk/internal/formatting/transform.py b/src/dynamodb_encryption_sdk/internal/formatting/transform.py new file mode 100644 index 00000000..43c441e7 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/formatting/transform.py @@ -0,0 +1,54 @@ +# 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. +"""Helper tools for translating between native and DynamoDB items.""" +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, Dict # pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from boto3.dynamodb.types import TypeSerializer, TypeDeserializer + + +def dict_to_ddb(item): + # type: (Dict[str, Any]) -> Dict[str, Any] + # TODO: narrow these types down + """Converts a native Python dictionary to a raw DynamoDB item. + + :param dict item: Native item + :returns: DynamoDB item + :rtype: dict + """ + serializer = TypeSerializer() + return { + key: serializer.serialize(value) + for key, value + in item.items() + } + + +def ddb_to_dict(item): + # type: (Dict[str, Any]) -> Dict[str, Any] + # TODO: narrow these types down + """Converts a raw DynamoDB item to a native Python dictionary. + + :param dict item: DynamoDB item + :returns: Native item + :rtype: dict + """ + deserializer = TypeDeserializer() + return { + key: deserializer.deserialize(value) + for key, value + in item.items() + } diff --git a/src/dynamodb_encryption_sdk/internal/identifiers.py b/src/dynamodb_encryption_sdk/internal/identifiers.py new file mode 100644 index 00000000..f2b32a66 --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/identifiers.py @@ -0,0 +1,102 @@ +# 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. +"""""" +from enum import Enum + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Any, ByteString, Dict, List, Text, Union # pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + + +class ReservedAttributes(Enum): + """Item attributes reserved for use by DynamoDBEncryptionClient""" + MATERIAL_DESCRIPTION = '*amzn-ddb-map-desc*' + SIGNATURE = '*amzn-ddb-map-sig*' + + +class Tag(Enum): + """Attribute data type identifiers used for serialization and deserialization of attributes.""" + + BINARY = (b'b', 'B') + BINARY_SET = (b'B', 'BS', b'b') + NUMBER = (b'n', 'N') + NUMBER_SET = (b'N', 'NS', b'n') + STRING = (b's', 'S') + STRING_SET = (b'S', 'SS', b's') + BOOLEAN = (b'?', 'BOOL') + NULL = (b'\x00', 'NULL') + LIST = (b'L', 'L') + MAP = (b'M', 'M') + + def __init__(self, tag, dynamodb_tag, element_tag=None): + # type: (bytes, Text, Optional[bytes]) -> None + """Sets up new Tag object. + + :param bytes tag: DynamoDB Encryption SDK tag + :param bytes dynamodb_tag: DynamoDB tag + :param bytes element_tag: The type of tag contained within attributes of this type + """ + self.tag = tag + self.dynamodb_tag = dynamodb_tag + self.element_tag = element_tag + + +class TagValues(Enum): + """Static values to use when serializing attribute values.""" + FALSE = b'\x00' + TRUE = b'\x01' + + +class SignatureValues(Enum): + """Values used when building the string to sign. + + .. note:: + + The only time we actually use these values, we use the SHA256 hash of the value, so + we pre-compute these hashes here. + """ + ENCRYPTED = ( + b'ENCRYPTED', + b"9A\x15\xacN\xb0\x9a\xa4\x94)4\x88\x16\xb2\x03\x81'\xb0\xf9\xe3\xa5 7*\xe1\x00\xca\x19\xfb\x08\xfdP" + ) + PLAINTEXT = ( + b'PLAINTEXT', + b'\xcb@\xe7\xda\xdc\x86\x16\x1b\x97\x98\xdeHQ/3-!\xc1A\xfc\xc1\xe2\x8a\x08o\xdeJ3u\xaa\xb1\xb5' + ) + + def __init__(self, raw, sha256): + # type: (bytes, bytes) -> None + """Set up a new SignatureValues object. + + :param bytes raw: Raw value + :param bytes sha256: SHA256 hash of raw value + """ + self.raw = raw + self.sha256 = sha256 + + +class MaterialDescriptionKeys(Enum): + """Static keys for use when building and reading material descriptions.""" + ATTRIBUTE_ENCRYPTION_MODE = 'amzn-ddb-map-sym-mode' + SIGNING_KEY_ALGORITHM = 'amzn-ddb-map-signingAlg' + WRAPPED_DATA_KEY = 'amzn-ddb-env-key' + CONTENT_ENCRYPTION_ALGORITHM = 'amzn-ddb-env-alg' + CONTENT_KEY_WRAPPING_ALGORITHM = 'amzn-ddb-wrap-alg' + ITEM_SIGNATURE_ALGORITHM = 'amzn-ddb-sig-alg' + + +class MaterialDescriptionValues(Enum): + """Static default values for use when building material descriptions.""" + CBC_PKCS5_ATTRIBUTE_ENCRYPTION = '/CBC/PKCS5Padding' diff --git a/src/dynamodb_encryption_sdk/internal/str_ops.py b/src/dynamodb_encryption_sdk/internal/str_ops.py new file mode 100644 index 00000000..2a04719b --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/str_ops.py @@ -0,0 +1,42 @@ +# 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. +"""Helper functions for consistently obtaining str and bytes objects in both Python2 and Python3.""" +import codecs + +import six + + +def to_str(data): + """Takes an input str or bytes object and returns an equivalent str object. + + :param data: Input data + :type data: str or bytes + :returns: Data normalized to str + :rtype: str + """ + if isinstance(data, bytes): + return codecs.decode(data, 'utf-8') + return data + + +def to_bytes(data): + """Takes an input str or bytes object and returns an equivalent bytes object. + + :param data: Input data + :type data: str or bytes + :returns: Data normalized to bytes + :rtype: bytes + """ + if isinstance(data, six.string_types) and not isinstance(data, bytes): + return codecs.encode(data, 'utf-8') + return data diff --git a/src/dynamodb_encryption_sdk/internal/utils.py b/src/dynamodb_encryption_sdk/internal/utils.py new file mode 100644 index 00000000..738bba5d --- /dev/null +++ b/src/dynamodb_encryption_sdk/internal/utils.py @@ -0,0 +1,30 @@ +# 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. +"""""" +from dynamodb_encryption_sdk.internal.str_ops import to_bytes + + +def sorted_key_map(item, transform=to_bytes): + """Creates a list of the item's key/value pairs as tuples, sorted by the keys transformed by transform. + + :param dict item: Source dictionary + :param function transform: Transform function + :returns: List of tuples containing transformed key, original value, and original key for each entry + :rtype: list of tuples + """ + sorted_items = [] + for key, value in item.items(): + _key = transform(key) + sorted_items.append((_key, value, key)) + sorted_items = sorted(sorted_items, key=lambda x: x[0]) + return sorted_items diff --git a/src/dynamodb_encryption_sdk/material_providers/__init__.py b/src/dynamodb_encryption_sdk/material_providers/__init__.py new file mode 100644 index 00000000..e0981e18 --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/__init__.py @@ -0,0 +1,47 @@ +# 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 providers.""" +from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials +from dynamodb_encryption_sdk.structures import EncryptionContext + + +class CryptographicMaterialsProvider(object): + """Base class for all cryptographic materials providers.""" + + def decryption_materials(self, encryption_context): + # type: (EncryptionContext) -> DecryptionMaterials + """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 + """ + raise AttributeError('No decryption materials available') + + def encryption_materials(self, encryption_context): + # type: (EncryptionContext) -> EncryptionMaterials + """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 + """ + raise AttributeError('No encryption materials available') + + def refresh(self): + """Ask this instance to refresh the cryptographic materials. + + .. note:: + + Default behavior is to do nothing. + """ diff --git a/src/dynamodb_encryption_sdk/material_providers/aws_kms.py b/src/dynamodb_encryption_sdk/material_providers/aws_kms.py new file mode 100644 index 00000000..72b3cdeb --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/aws_kms.py @@ -0,0 +1,463 @@ +# 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 for use with the AWS Key Management Service (KMS).""" +from __future__ import division +import base64 +from enum import Enum + +import attr +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +import boto3 +import botocore.client +import botocore.session +import six + +from . import CryptographicMaterialsProvider +from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey +from dynamodb_encryption_sdk.exceptions import UnknownRegionError, UnwrappingError, WrappingError +from dynamodb_encryption_sdk.identifiers import EncryptionKeyTypes, KeyEncodingType +from dynamodb_encryption_sdk.internal import dynamodb_types +from dynamodb_encryption_sdk.internal.identifiers import MaterialDescriptionKeys +from dynamodb_encryption_sdk.materials.raw import RawDecryptionMaterials, RawEncryptionMaterials +from dynamodb_encryption_sdk.structures import EncryptionContext + +__all__ = ('AwsKmsCryptographicMaterialsProvider',) + +_COVERED_ATTR_CTX_KEY = 'aws-kms-ec-attr' +_TABLE_NAME_EC_KEY = '*aws-kms-table*' +_DEFAULT_CONTENT_ENCRYPTION_ALGORITHM = 'AES/256' +_DEFAULT_CONTENT_KEY_LENGTH = 256 +_DEFAULT_SIGNING_ALGORITHM = 'HmacSHA256/256' +_DEFAULT_SIGNING_KEY_LENGTH = 256 +_KEY_COVERAGE = '*keys*' +_KDF_ALG = 'HmacSHA256' + + +class HkdfInfo(Enum): + """Info strings used for HKDF calculations.""" + ENCRYPTION = b'Encryption' + SIGNING = b'Signing' + + +class EncryptionContextKeys(Enum): + """Special keys for use in the AWS KMS encryption context.""" + CONTENT_ENCRYPTION_ALGORITHM = '*' + MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value + '*' + SIGNATURE_ALGORITHM = '*' + MaterialDescriptionKeys.ITEM_SIGNATURE_ALGORITHM.value + '*' + TABLE_NAME = '*aws-kms-table*' + + +@attr.s(hash=False) +class KeyInfo(object): + """Identifying information for a specific key and how it should be used. + + :param str description: algorithm identifier joined with key length in bits + :param str algorithm: algorithm identifier + :param int length: Key length in bits + """ + description = attr.ib(validator=attr.validators.instance_of(six.string_types)) + algorithm = attr.ib(validator=attr.validators.instance_of(six.string_types)) + length = attr.ib(validator=attr.validators.instance_of(six.integer_types)) + + @classmethod + def from_material_description(cls, material_description, description_key, default_algorithm, default_key_length): + # type: (Dict[Text, Text], Text, Text, int) -> KeyInfo + """Load key info from material description. + + :param dict material_description: Material description to read + :param str description_key: Material description key containing desired key info description + :param str default_algorithm: Algorithm name to use if not found in material description + :param int default_key_length: Key length to use if not found in material description + :returns: Key info loaded from material description, with defaults applied if necessary + :rtype: dynamodb_encryption_sdk.material_providers.aws_kms.KeyInfo + """ + description = material_description.get(description_key, default_algorithm) + description_parts = description.split('/', 1) + algorithm = description_parts[0] + try: + key_length = int(description_parts[1]) + except IndexError: + key_length = default_key_length + return cls(description, algorithm, key_length) + + +@attr.s(hash=False) +class AwsKmsCryptographicMaterialsProvider(CryptographicMaterialsProvider): + """Cryptographic materials provider for use with the AWS Key Management Service (KMS). + + .. note:: + + This cryptographic materials provider makes one AWS KMS API call each time encryption + or decryption materials are requested. This means that one request will be made for + each item that you read or write. + + :param str key_id: ID of AWS KMS CMK to use + :param botocore_session: botocore session object (optional) + :type botocore_session: botocore.session.Session + :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations (optional) + :param dict material_description: Material description to use as default state for this CMP (optional) + :param dict regional_clients: Dictionary mapping AWS region names to pre-configured boto3 + KMS clients (optional) + """ + _key_id = attr.ib(validator=attr.validators.instance_of(six.string_types)) + _botocore_session = attr.ib( + validator=attr.validators.instance_of(botocore.session.Session), + default=attr.Factory(botocore.session.Session) + ) + _grant_tokens = attr.ib(default=attr.Factory(tuple)) + + @_grant_tokens.validator + def _grant_tokens_validator(self, attribute, value): + """Validate grant token values.""" + if not isinstance(value, tuple): + raise TypeError('"grant_tokens" must be a tuple') + + for token in value: + if not isinstance(token, six.string_types): + raise TypeError('"grant_tokens" must contain strings') + + _material_description = attr.ib(default=attr.Factory(dict)) + + @_material_description.validator + def _material_description_validator(self, attribute, value): + """Validate material description values.""" + if not isinstance(value, dict): + raise TypeError('"material_description" must be a dictionary') + + for key, data in value.items(): + if not (isinstance(key, six.string_types) and isinstance(data, six.string_types)): + raise TypeError('"material_description" must be a string-string dictionary') + + _regional_clients = attr.ib(default=attr.Factory(dict)) + + @_regional_clients.validator + def regional_clients_validator(self, attribute, value): + """Validate regional clients values.""" + if not isinstance(value, dict): + raise TypeError('"regional_clients" must be a dictionary') + + for key, client in value.items(): + if not isinstance(key, six.string_types): + raise TypeError('"regional_clients" region name must be a string') + + if not isinstance(client, botocore.client.BaseClient): + raise TypeError('"regional_clients" client must be a botocore client') + + def __attrs_post_init__(self): + # type: () -> None + """Load the content and signing key info.""" + self._content_key_info = KeyInfo.from_material_description( + material_description=self._material_description, + description_key=MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value, + default_algorithm=_DEFAULT_CONTENT_ENCRYPTION_ALGORITHM, + default_key_length=_DEFAULT_CONTENT_KEY_LENGTH + ) + self._signing_key_info = KeyInfo.from_material_description( + material_description=self._material_description, + description_key=MaterialDescriptionKeys.ITEM_SIGNATURE_ALGORITHM.value, + default_algorithm=_DEFAULT_SIGNING_ALGORITHM, + default_key_length=_DEFAULT_SIGNING_KEY_LENGTH + ) + self._regional_clients = {} + + def _add_regional_client(self, region_name): + # type: (Text) -> None + """Adds a regional client for the specified region if it does not already exist. + + :param str region_name: AWS Region ID (ex: us-east-1) + """ + if region_name not in self._regional_clients: + self._regional_clients[region_name] = boto3.session.Session( + region_name=region_name, + botocore_session=self._botocore_session + ).client('kms') + + def _client(self, key_id): + """Returns a boto3 KMS client for the appropriate region. + + :param str key_id: KMS CMK ID + :returns: Boto3 KMS client for requested key id + :rtype: botocore.client.KMS + """ + region = self._botocore_session.get_config_variable('region') + if region is None: + try: + region_name = key_id.split(':', 4)[3] + region = region_name + except IndexError: + if region is None: + raise UnknownRegionError( + 'No default region found and no region determinable from key id: {}'.format(key_id) + ) + self._add_regional_client(region) + return self._regional_clients[region] + + def _select_key_id(self, encryption_context): + # type: (EncryptionContext) -> Text + """Select the desired key id. + + .. note:: + + Default behavior is to use the key id provided on creation, but this method provides + an extension point for a CMP that might select a different key id based on the + encryption context. + + :param encryption_context: Encryption context providing information about request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Key id to use + :rtype: str + """ + return self._key_id + + def _validate_key_id(self, key_id, encryption_context): + """Validate the selected key id. + + .. note:: + + Default behavior is to do nothing, but this method provides an extension point + for a CMP that overrides ``_select_key_id`` or otherwise wants to validate a + key id before it is used. + + :param encryption_context: Encryption context providing information about request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + """ + + def _attribute_to_value(self, attribute): + # type: (dynamodb_types.ITEM) -> str + """Convert a DynamoDB attribute to a value that can be added to the KMS encryption context. + + :param dict attribute: Attribute to convert + :returns: value from attribute, ready to be addd to the KMS encryption context + :rtype: str + """ + attribute_type, attribute_value = list(attribute.items())[0] + if attribute_type == 'B': + return base64.b64encode(attribute_value.value).decode('utf-8') + if attribute_type == 'S': + return attribute_value + 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] + """Build the KMS encryption context from the encryption context and key descriptions. + + :param encryption_context: Encryption context providing information about request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :param str encryption_description: Description value from encryption KeyInfo + :param str signing_description: Description value from signing KeyInfo + :returns: KMS encryption context for use in request + :rtype: dict + """ + kms_encryption_context = { + EncryptionContextKeys.CONTENT_ENCRYPTION_ALGORITHM.value: encryption_description, + EncryptionContextKeys.SIGNATURE_ALGORITHM.value: signing_description + } + + if encryption_context.partition_key_name is not None: + partition_key_attribute = encryption_context.attributes.get(encryption_context.partition_key_name) + kms_encryption_context[encryption_context.partition_key_name] = self._attribute_to_value( + partition_key_attribute + ) + + if encryption_context.sort_key_name is not None: + sort_key_attribute = encryption_context.attributes.get(encryption_context.sort_key_name) + kms_encryption_context[encryption_context.sort_key_name] = self._attribute_to_value(sort_key_attribute) + + if encryption_context.table_name is not None: + kms_encryption_context[_TABLE_NAME_EC_KEY] = encryption_context.table_name + + return kms_encryption_context + + def _generate_initial_material(self, encryption_context): + # type: () -> (bytes, bytes) + """Generate the initial cryptographic material for use with HKDF. + + :param encryption_context: Encryption context providing information about request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Plaintext and ciphertext of initial cryptographic material + :rtype: bytes and bytes + """ + key_id = self._select_key_id(encryption_context) + self._validate_key_id(key_id, encryption_context) + key_length = 256 // 8 + kms_encryption_context = self._kms_encryption_context( + encryption_context=encryption_context, + encryption_description=self._content_key_info.description, + signing_description=self._signing_key_info.description + ) + kms_params = dict( + KeyId=key_id, + NumberOfBytes=key_length, + EncryptionContext=kms_encryption_context + ) + if self._grant_tokens: + kms_params['GrantTokens'] = self._grant_tokens + # Catch any boto3 errors and normalize to expected WrappingError + try: + response = self._client(key_id).generate_data_key(**kms_params) + return response['Plaintext'], response['CiphertextBlob'] + except (botocore.exceptions.ClientError, KeyError): + raise WrappingError('TODO:SOMETHING') + + def _decrypt_initial_material(self, encryption_context): + # type: () -> bytes + """Decrypt an encrypted initial cryptographic material value. + + :param encryption_context: Encryption context providing information about request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Plaintext of initial cryptographic material + :rtype: bytes + """ + key_id = self._select_key_id(encryption_context) + self._validate_key_id(key_id, encryption_context) + kms_encryption_context = self._kms_encryption_context( + encryption_context=encryption_context, + encryption_description=encryption_context.material_description.get( + MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value + ), + signing_description=encryption_context.material_description.get( + MaterialDescriptionKeys.ITEM_SIGNATURE_ALGORITHM.value + ) + ) + encrypted_initial_material = base64.b64decode(encryption_context.material_description.get( + MaterialDescriptionKeys.WRAPPED_DATA_KEY.value + )) + kms_params = dict( + CiphertextBlob=encrypted_initial_material, + EncryptionContext=kms_encryption_context + ) + if self._grant_tokens: + kms_params['GrantTokens'] = self._grant_tokens + # Catch any boto3 errors and normalize to expected UnwrappingError + try: + response = self._client(key_id).decrypt(**kms_params) + return response['Plaintext'] + except (botocore.exceptions.ClientError, KeyError): + raise UnwrappingError('TODO:SOMETHING') + + def _hkdf(self, initial_material, key_length, info): + # type: (bytes, int, Text) -> bytes + """Use HKDF to derive a key. + + :param bytes initial_material: Initial material to use with HKDF + :param int key_length: Length of key to derive + :param str info: Info value to use in HKDF calculate + :returns: Derived key material + :rtype: bytes + """ + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=key_length, + salt=None, + info=info, + backend=default_backend() + ) + return hkdf.derive(initial_material) + + def _derive_delegated_key(self, initial_material, key_info, hkdf_info): + # type: (bytes, KeyInfo, HkdfInfo) -> JceNameLocalDelegatedKey + """Derive the raw key and use it to build a JceNameLocalDelegatedKey. + + :param bytes initial_material: Initial material to use with KDF + :param key_info: Key information to use to calculate encryption key + :type key_info: dynamodb_encryption_sdk.material_providers.aws_kms.KeyInfo + :param hkdf_info: Info to use in HKDF calculation + :type hkdf_info: dynamodb_encryption_sdk.material_providers.aws_kms.HkdfInfo + :returns: Delegated key to use for encryption and decryption + :rtype: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey + """ + raw_key = self._hkdf(initial_material, key_info.length // 8, hkdf_info.value) + return JceNameLocalDelegatedKey( + key=raw_key, + algorithm=key_info.algorithm, + key_type=EncryptionKeyTypes.SYMMETRIC, + key_encoding=KeyEncodingType.RAW + ) + + def _encryption_key(self, initial_material, key_info): + # type: (bytes, KeyInfo) -> JceNameLocalDelegatedKey + """Calculate an encryption key from ``initial_material`` using the requested key info. + + :param bytes initial_material: Initial material to use with KDF + :param key_info: Key information to use to calculate encryption key + :type key_info: dynamodb_encryption_sdk.material_providers.aws_kms.KeyInfo + :returns: Delegated key to use for encryption and decryption + :rtype: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey + """ + return self._derive_delegated_key(initial_material, key_info, HkdfInfo.ENCRYPTION) + + def _mac_key(self, initial_material, key_info): + # type: (bytes, KeyInfo) -> JceNameLocalDelegatedKey + """Calculate an HMAC key from ``initial_material`` using the requested key info. + + :param bytes initial_material: Initial material to use with KDF + :param key_info: Key information to use to calculate HMAC key + :type key_info: dynamodb_encryption_sdk.material_providers.aws_kms.KeyInfo + :returns: Delegated key to use for signature calculation and verification + :rtype: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey + """ + return self._derive_delegated_key(initial_material, key_info, HkdfInfo.SIGNING) + + def decryption_materials(self, encryption_context): + # type: (EncryptionContext) -> RawDecryptionMaterials + """Provide decryption materials. + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Encryption materials + :rtype: dynamodb_encryption_sdk.materials.wrapped.RawDecryptionMaterials + """ + decryption_material_description = encryption_context.material_description.copy() + initial_material = self._decrypt_initial_material(encryption_context) + signing_key_info = KeyInfo.from_material_description( + material_description=encryption_context.material_description, + description_key=MaterialDescriptionKeys.ITEM_SIGNATURE_ALGORITHM.value, + default_algorithm=_DEFAULT_SIGNING_ALGORITHM, + default_key_length=_DEFAULT_SIGNING_KEY_LENGTH + ) + decryption_key_info = KeyInfo.from_material_description( + material_description=encryption_context.material_description, + description_key=MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value, + default_algorithm=_DEFAULT_CONTENT_ENCRYPTION_ALGORITHM, + default_key_length=_DEFAULT_CONTENT_KEY_LENGTH + ) + return RawDecryptionMaterials( + verification_key=self._mac_key(initial_material, signing_key_info), + decryption_key=self._encryption_key(initial_material, decryption_key_info), + material_description=decryption_material_description + ) + + def encryption_materials(self, encryption_context): + # type: (EncryptionContext) -> RawEncryptionMaterials + """Provide encryption materials. + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Encryption materials + :rtype: dynamodb_encryption_sdk.materials.wrapped.RawEncryptionMaterials + """ + initial_material, encrypted_initial_material = self._generate_initial_material(encryption_context) + encryption_material_description = encryption_context.material_description.copy() + encryption_material_description.update({ + _COVERED_ATTR_CTX_KEY: _KEY_COVERAGE, + MaterialDescriptionKeys.CONTENT_KEY_WRAPPING_ALGORITHM.value: 'kms', + MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value: self._content_key_info.description, + MaterialDescriptionKeys.ITEM_SIGNATURE_ALGORITHM.value: self._signing_key_info.description, + MaterialDescriptionKeys.WRAPPED_DATA_KEY.value: base64.b64encode(encrypted_initial_material) + }) + return RawEncryptionMaterials( + signing_key=self._mac_key(initial_material, self._signing_key_info), + encryption_key=self._encryption_key(initial_material, self._content_key_info), + material_description=encryption_material_description + ) diff --git a/src/dynamodb_encryption_sdk/material_providers/static.py b/src/dynamodb_encryption_sdk/material_providers/static.py new file mode 100644 index 00000000..6bdd403b --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/static.py @@ -0,0 +1,63 @@ +# 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 for use with pre-configured encryption and decryption materials.""" +import attr + +from . import CryptographicMaterialsProvider +from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials +from dynamodb_encryption_sdk.structures import EncryptionContext + + +@attr.s(hash=False) +class StaticCryptographicMaterialsProvider(CryptographicMaterialsProvider): + """Manually combine encryption and decryption materials for use as a cryptographic materials provider. + + :param decryption_materials: Decryption materials to provide (optional) + :type decryption_materials: dynamodb_encryption_sdk.materials.DecryptionMaterials + :param encryption_materials: Encryption materials to provide (optional) + :type encryption_materials: dynamodb_encryption_sdk.materials.EncryptionMaterials + """ + _decryption_materials = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(DecryptionMaterials)), + default=None + ) + _encryption_materials = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(EncryptionMaterials)), + default=None + ) + + def decryption_materials(self, encryption_context): + # type: (EncryptionContext) -> DecryptionMaterials + """Return the static decryption materials. + + :param encryption_context: Encryption context for request (not used by ``StaticCryptographicMaterialsProvider``) + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :raises AttributeError: if no decryption materials are available + """ + if self._decryption_materials is None: + super(StaticCryptographicMaterialsProvider, self).decryption_materials(encryption_context) + + return self._decryption_materials + + def encryption_materials(self, encryption_context): + # type: (EncryptionContext) -> EncryptionMaterials + """Return the static encryption materials. + + :param encryption_context: Encryption context for request (not used by ``StaticCryptographicMaterialsProvider``) + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :raises AttributeError: if no encryption materials are available + """ + if self._encryption_materials is None: + super(StaticCryptographicMaterialsProvider, self).encryption_materials(encryption_context) + + return self._encryption_materials diff --git a/src/dynamodb_encryption_sdk/material_providers/wrapped.py b/src/dynamodb_encryption_sdk/material_providers/wrapped.py new file mode 100644 index 00000000..6e074df9 --- /dev/null +++ b/src/dynamodb_encryption_sdk/material_providers/wrapped.py @@ -0,0 +1,97 @@ +# 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 to use ephemeral content encryption keys wrapped by delegated keys.""" +import attr + +from . import CryptographicMaterialsProvider +from dynamodb_encryption_sdk.delegated_keys import DelegatedKey +from dynamodb_encryption_sdk.exceptions import UnwrappingError, WrappingError +from dynamodb_encryption_sdk.materials.wrapped import WrappedCryptographicMaterials +from dynamodb_encryption_sdk.structures import EncryptionContext + + +@attr.s(hash=False) +class WrappedCryptographicMaterialsProvider(CryptographicMaterialsProvider): + """Cryptographic materials provider to use ephemeral content encryption keys wrapped by delegated keys. + + :param signing_key: Delegated key used as signing and verification key + :type signing_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param wrapping_key: Delegated key used to wrap content key + :type wrapping_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + + .. note:: + + ``wrapping_key`` must be provided if providing encryption materials + + :param unwrapping_key: Delegated key used to unwrap content key + :type unwrapping_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + + .. note:: + + ``unwrapping_key`` must be provided if providing decryption materials or loading + materials from material description + """ + _signing_key = attr.ib(validator=attr.validators.instance_of(DelegatedKey)) + _wrapping_key = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(DelegatedKey)), + default=None + ) + _unwrapping_key = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(DelegatedKey)), + default=None + ) + + def _build_materials(self, encryption_context): + """Construct + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Wrapped cryptographic materials + :rtype: dynamodb_encryption_sdk.materials.wrapped.WrappedCryptographicMaterials + """ + return WrappedCryptographicMaterials( + wrapping_key=self._wrapping_key, + unwrapping_key=self._unwrapping_key, + signing_key=self._signing_key, + material_description=encryption_context.material_description.copy() + ) + + def encryption_materials(self, encryption_context): + # type: (EncryptionContext) -> WrappedCryptographicMaterials + """Provide encryption materials. + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Encryption materials + :rtype: dynamodb_encryption_sdk.materials.wrapped.WrappedCryptographicMaterials + :raises WrappingError: if no wrapping key is available + """ + if self._wrapping_key is None: + raise WrappingError('Encryption materials cannot be provided: no wrapping key') + + return self._build_materials(encryption_context) + + def decryption_materials(self, encryption_context): + # type: (EncryptionContext) -> WrappedCryptographicMaterials + """Provide decryption materials. + + :param encryption_context: Encryption context for request + :type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext + :returns: Decryption materials + :rtype: dynamodb_encryption_sdk.materials.wrapped.WrappedCryptographicMaterials + :raises UnwrappingError: if no unwrapping key is available + """ + if self._unwrapping_key is None: + raise UnwrappingError('Decryption materials cannot be provided: no unwrapping key') + + return self._build_materials(encryption_context) diff --git a/src/dynamodb_encryption_sdk/materials/__init__.py b/src/dynamodb_encryption_sdk/materials/__init__.py new file mode 100644 index 00000000..f61c3553 --- /dev/null +++ b/src/dynamodb_encryption_sdk/materials/__init__.py @@ -0,0 +1,114 @@ +# 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. +"""""" +import abc + +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from mypy_extensions import NoReturn +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +import six + +from dynamodb_encryption_sdk.delegated_keys import DelegatedKey + + +@six.add_metaclass(abc.ABCMeta) +class CryptographicMaterials(object): + """Base class for all cryptographic materials.""" + + @abc.abstractproperty + def material_description(self): + # type: () -> Dict[Text, Text] + """Material description to use with these cryptographic materials. + + :returns: Material description + :rtype: dict + """ + + @abc.abstractproperty + def encryption_key(self): + # type: () -> DelegatedKey + """Delegated key used for encrypting attributes. + + :returns: Encryption key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + + @abc.abstractproperty + def decryption_key(self): + # type: () -> DelegatedKey + """Delegated key used for decrypting attributes. + + :returns: Decryption key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + + @abc.abstractproperty + def signing_key(self): + # type: () -> DelegatedKey + """Delegated key used for calculating digital signatures. + + :returns: Signing key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + + @abc.abstractproperty + def verification_key(self): + # type: () -> DelegatedKey + """Delegated key used for verifying digital signatures. + + :returns: Verification key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + + +class EncryptionMaterials(CryptographicMaterials): + """Base class for all encryption materials.""" + + @property + def decryption_key(self): + # type: () -> NoReturn + """ + :raises NotImplementedError: because encryption materials do not contain decryption keys + """ + raise NotImplementedError('EncryptionMaterials do not provide decryption keys.') + + @property + def verification_key(self): + # type: () -> NoReturn + """ + :raises NotImplementedError: because encryption materials do not contain verification keys + """ + raise NotImplementedError('EncryptionMaterials do not provide verification keys.') + + +class DecryptionMaterials(CryptographicMaterials): + """Base class for all decryption materials.""" + + @property + def encryption_key(self): + # type: () -> NoReturn + """ + :raises NotImplementedError: because decryption materials do not contain encryption keys + """ + raise NotImplementedError('EncryptionMaterials do not provide encryption keys.') + + @property + def signing_key(self): + # type: () -> NoReturn + """ + :raises NotImplementedError: because decryption materials do not contain signing keys + """ + raise NotImplementedError('EncryptionMaterials do not provide signing keys.') diff --git a/src/dynamodb_encryption_sdk/materials/raw.py b/src/dynamodb_encryption_sdk/materials/raw.py new file mode 100644 index 00000000..15fe188b --- /dev/null +++ b/src/dynamodb_encryption_sdk/materials/raw.py @@ -0,0 +1,150 @@ +# 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 classes for use directly with delegated keys. + +.. warning:: + + Using raw cryptographic materials can be very dangerous because you are likely to be + encrypting many items using the same encryption key material. This can have some unexpected + and difficult to detect side effects that weaken the security of your encrypted data. + + Unless you have specific reasons for using raw cryptographic materials, we highly recommend + that you use wrapped cryptographic materials instead. +""" +import copy + +import attr + +from dynamodb_encryption_sdk.delegated_keys import DelegatedKey +from dynamodb_encryption_sdk.materials import DecryptionMaterials, EncryptionMaterials + + +@attr.s(hash=False) +class RawEncryptionMaterials(EncryptionMaterials): + """Encryption materials for use directly with delegated keys. + + .. note:: + + Not all delegated keys allow use with raw cryptographic materials. + + :param signing_key: Delegated key used as signing key + :type signing_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param encryption_key: Delegated key used as encryption key + :type encryption_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param dict material_description: Material description to use with these cryptographic materials + """ + _signing_key = attr.ib(validator=attr.validators.instance_of(DelegatedKey)) + _encryption_key = attr.ib(validator=attr.validators.instance_of(DelegatedKey)) + _material_description = attr.ib( + validator=attr.validators.instance_of(dict), + converter=copy.deepcopy, + default=attr.Factory(dict) + ) + + def __attrs_post_init__(self): + """Verify that the encryption key is allowed be used for raw materials.""" + if not self._encryption_key.allowed_for_raw_materials: + raise ValueError('Encryption key type "{}" does not allow use with RawEncryptionMaterials'.format( + type(self._encryption_key) + )) + + @property + def material_description(self): + # type: () -> Dict[Text, Text] + """Material description to use with these cryptographic materials. + + :returns: Material description + :rtype: dict + """ + return self._material_description + + @property + def signing_key(self): + # type: () -> Dict[Text, Text] + """Delegated key used for calculating digital signatures. + + :returns: Signing key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._signing_key + + @property + def encryption_key(self): + # type: () -> Dict[Text, Text] + """Delegated key used for encrypting attributes. + + :returns: Encryption key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._encryption_key + + +@attr.s(hash=False) +class RawDecryptionMaterials(DecryptionMaterials): + """Encryption materials for use directly with delegated keys. + + .. note:: + + Not all delegated keys allow use with raw cryptographic materials. + + :param verification_key: Delegated key used as verification key + :type verification_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param decryption_key: Delegated key used as decryption key + :type decryption_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param dict material_description: Material description to use with these cryptographic materials + """ + _verification_key = attr.ib(validator=attr.validators.instance_of(DelegatedKey)) + _decryption_key = attr.ib(validator=attr.validators.instance_of(DelegatedKey)) + _material_description = attr.ib( + validator=attr.validators.instance_of(dict), + converter=copy.deepcopy, + default=attr.Factory(dict) + ) + + def __attrs_post_init__(self): + """Verify that the encryption key is allowed be used for raw materials.""" + if not self._decryption_key.allowed_for_raw_materials: + raise ValueError('Decryption key type "{}" does not allow use with RawDecryptionMaterials'.format( + type(self._decryption_key) + )) + + @property + def material_description(self): + # type: () -> Dict[Text, Text] + """Material description to use with these cryptographic materials. + + :returns: Material description + :rtype: dict + """ + return self._material_description + + @property + def verification_key(self): + # type: () -> Dict[Text, Text] + """Delegated key used for verifying digital signatures. + + :returns: Verification key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._verification_key + + @property + def decryption_key(self): + # type: () -> Dict[Text, Text] + """Delegated key used for decrypting attributes. + + :returns: Decryption key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._decryption_key diff --git a/src/dynamodb_encryption_sdk/materials/wrapped.py b/src/dynamodb_encryption_sdk/materials/wrapped.py new file mode 100644 index 00000000..e8baa440 --- /dev/null +++ b/src/dynamodb_encryption_sdk/materials/wrapped.py @@ -0,0 +1,202 @@ +# 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 to use ephemeral content encryption keys wrapped by delegated keys.""" +from __future__ import division +import base64 +import copy + +import attr + +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 +from dynamodb_encryption_sdk.identifiers import EncryptionKeyTypes +from dynamodb_encryption_sdk.internal.identifiers import MaterialDescriptionKeys +from dynamodb_encryption_sdk.materials import CryptographicMaterials + +__all__ = ('WrappedRawCryptographicMaterials',) +_DEFAULT_CONTENT_ENCRYPTION_ALGORITHM = 'AES/256' +_WRAPPING_TRANSFORMATION = { + 'AES': 'AESWrap', + 'RSA': 'RSA/ECB/OAEPWithSHA-256AndMGF1Padding' +} + + +@attr.s(hash=False) +class WrappedCryptographicMaterials(CryptographicMaterials): + """Encryption/decryption key is a content key stored in the material description, wrapped + by the wrapping key. + + :param signing_key: Delegated key used as signing and verification key + :type signing_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + :param wrapping_key: Delegated key used to wrap content key + :type wrapping_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + + .. note:: + + ``wrapping_key`` must be provided if material description contains a wrapped content key + + :param unwrapping_key: Delegated key used to unwrap content key + :type unwrapping_key: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + + .. note:: + + ``unwrapping_key`` must be provided if material description does not contain a wrapped content key + + :param dict material_description: Material description to use with these cryptographic materials + """ + _signing_key = attr.ib(validator=attr.validators.instance_of(DelegatedKey)) + _wrapping_key = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(DelegatedKey)), + default=None + ) + _unwrapping_key = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(DelegatedKey)), + default=None + ) + _material_description = attr.ib( + validator=attr.validators.instance_of(dict), + converter=copy.deepcopy, + default=attr.Factory(dict) + ) + + def __attrs_post_init__(self): + """Prepare the content key.""" + self._content_key_algorithm = self.material_description.get( + MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value, + _DEFAULT_CONTENT_ENCRYPTION_ALGORITHM + ) + + if MaterialDescriptionKeys.WRAPPED_DATA_KEY.value in self.material_description: + self._content_key = self._content_key_from_material_description() + else: + self._content_key, self._material_description = self._generate_content_key() + + def _wrapping_transformation(self, algorithm): + """Convert the specified algorithm name to the desired wrapping algorithm transformation. + + :param str algorithm: Algorithm name + :returns: Algorithm transformation for wrapping with algorithm + :rtype: str + """ + return _WRAPPING_TRANSFORMATION.get(algorithm, algorithm) + + def _content_key_from_material_description(self): + """Load the content key from material description and unwrap it for use. + + :returns: Unwrapped content key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + if self._unwrapping_key is None: + raise UnwrappingError( + 'Cryptographic materials cannot be loaded from material description: no unwrapping key' + ) + + wrapping_algorithm = self.material_description.get( + MaterialDescriptionKeys.CONTENT_KEY_WRAPPING_ALGORITHM.value, + self._unwrapping_key.algorithm + ) + wrapped_key = base64.b64decode( + self.material_description[MaterialDescriptionKeys.WRAPPED_DATA_KEY.value] + ) + content_key_algorithm = self._content_key_algorithm.split('/', 1)[0] + return self._unwrapping_key.unwrap( + algorithm=wrapping_algorithm, + wrapped_key=wrapped_key, + wrapped_key_algorithm=content_key_algorithm, + wrapped_key_type=EncryptionKeyTypes.SYMMETRIC, + additional_associated_data=None + ) + + def _generate_content_key(self): + """Generate the content encryption key and create a new material description containing + necessary information about the content and wrapping keys. + + :returns content key and new material description + :rtype: tuple containing dynamodb_encryption_sdk.delegated_keys.DelegatedKey and dict + """ + if self._wrapping_key is None: + raise WrappingError('Cryptographic materials cannot be generated: no wrapping key') + + wrapping_algorithm = self.material_description.get( + MaterialDescriptionKeys.CONTENT_KEY_WRAPPING_ALGORITHM.value, + self._wrapping_transformation(self._wrapping_key.algorithm) + ) + args = self._content_key_algorithm.split('/', 1) + content_algorithm = args[0] + try: + content_key_length = int(args[1]) // 8 + except IndexError: + content_key_length = None + content_key = JceNameLocalDelegatedKey.generate( + algorithm=content_algorithm, + key_length=content_key_length + ) + wrapped_key = self._wrapping_key.wrap( + algorithm=wrapping_algorithm, + content_key=content_key.key, + additional_associated_data=None + ) + new_material_description = self.material_description.copy() + new_material_description.update({ + MaterialDescriptionKeys.WRAPPED_DATA_KEY.value: base64.b64encode(wrapped_key), + MaterialDescriptionKeys.CONTENT_ENCRYPTION_ALGORITHM.value: self._content_key_algorithm, + MaterialDescriptionKeys.CONTENT_KEY_WRAPPING_ALGORITHM.value: wrapping_algorithm + }) + return content_key, new_material_description + + @property + def material_description(self): + # type: () -> Dict[Text, Text] + """Material description to use with these cryptographic materials. + + :returns: Material description + :rtype: dict + """ + return self._material_description + + @property + def encryption_key(self): + """Content key used for encrypting attributes. + + :returns: Encryption key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._content_key + + @property + def decryption_key(self): + """Content key used for decrypting attributes. + + :returns: Decryption key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._content_key + + @property + def signing_key(self): + """Delegated key used for calculating digital signatures. + + :returns: Signing key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._signing_key + + @property + def verification_key(self): + """Delegated key used for verifying digital signatures. + + :returns: Verification key + :rtype: dynamodb_encryption_sdk.delegated_keys.DelegatedKey + """ + return self._signing_key diff --git a/src/dynamodb_encryption_sdk/structures.py b/src/dynamodb_encryption_sdk/structures.py new file mode 100644 index 00000000..d50b4f8e --- /dev/null +++ b/src/dynamodb_encryption_sdk/structures.py @@ -0,0 +1,218 @@ +# 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. +"""""" +import attr +import copy + +import six + +from .identifiers import ItemAction + + +@attr.s(hash=False) +class EncryptionContext(object): + """Additional information about an encryption request. + + :param str table_name: Table name + :param str partition_key_name: Name of primary index partition attribute + :param str sort_key_name: Name of primary index sort attribute + :param dict attributes: Plaintext item attributes + :param dict material_description: Material description to use with this request + """ + table_name = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(six.string_types)), + default=None + ) + partition_key_name = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(six.string_types)), + default=None + ) + sort_key_name = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(six.string_types)), + default=None + ) + # TODO: converter to make sure that attributes are in DDB form + attributes = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(dict)), + default=attr.Factory(dict) + ) + material_description = attr.ib( + validator=attr.validators.instance_of(dict), + converter=copy.deepcopy, + default=attr.Factory(dict) + ) + + +@attr.s(hash=False) +class AttributeActions(object): + """Configuration resource used to determine what action should be taken for a specific attribute. + + :param default_action: Action to take if no specific action is defined in ``attribute_actions`` + :type default_action: dynamodb_encryption_sdk.identifiers.ItemAction + :param dict attribute_actions: Dictionary mapping attribute names to specific actions + """ + default_action = attr.ib( + validator=attr.validators.instance_of(ItemAction), + default=ItemAction.ENCRYPT_AND_SIGN + ) + attribute_actions = attr.ib( + validator=attr.validators.instance_of(dict), + default=attr.Factory(dict) + ) + + def __attrs_post_init__(self): + # () -> None + """Determine if any actions should ever be taken with this configuration and record that for reference.""" + # Enums are not hashable, but their names are unique + _unique_actions = set([self.default_action.name]) + _unique_actions.update(set([action.name for action in self.attribute_actions.values()])) + self.take_no_actions = _unique_actions == set([ItemAction.DO_NOTHING.name]) + + def action(self, attribute_name): + # (text) -> ItemAction + """Determines the correct ItemAction to apply to a supplied attribute based on this config.""" + return self.attribute_actions.get(attribute_name, self.default_action) + + def copy(self): + # () -> AttributeActions + """Returns a new copy of this object.""" + return AttributeActions( + default_action=self.default_action, + attribute_actions=self.attribute_actions.copy() + ) + + def set_index_keys(self, *keys): + """Sets the appropriate action for the specified indexed attribute names. + + DO_NOTHING -> DO_NOTHING + SIGN_ONLY -> SIGN_ONLY + ENCRYPT_AND_SIGN -> SIGN_ONLY + """ + for key in keys: + current_action = self.action(key) + self.attribute_actions[key] = min(current_action, ItemAction.SIGN_ONLY) + + def __add__(self, other): + # (AttributeActions) -> AttributeActions + """Merges two AttributeActions objects into a new instance, applying the dominant + action in each discovered case. + """ + default_action = self.default_action + other.default_action + all_attributes = set(self.attribute_actions.keys()).union(set(other.attribute_actions.keys())) + attribute_actions = {} + for attribute in all_attributes: + attribute_actions[attribute] = max(self.action(attribute), other.action(attribute)) + return AttributeActions( + default_action=default_action, + attribute_actions=attribute_actions + ) + + +@attr.s(hash=False) +class TableIndex(object): + """Describes a table index. + + :param str partition: Name of the partition attribute + :param str sort: Name of the sort attribute (optional) + """ + partition = attr.ib(validator=attr.validators.instance_of(six.string_types)) + sort = attr.ib( + default=None, + validator=attr.validators.optional(attr.validators.instance_of(six.string_types)) + ) + + def __attrs_post_init__(self): + """Set the ``attributes`` attribute for ease of access later.""" + self.attributes = set([self.partition]) + if self.sort is None: + self.attributes.add(self.sort) + + +@attr.s(hash=False) +class TableInfo(object): + """Description of a DynamoDB table. + + :param str name: Table name + :param bool all_encrypting_secondary_indexes: Should we allow secondary index attributes to be encrypted? + :param primary_index: Description of primary index + :type primary_index: dynamodb_encryption_sdk.structures.TableIndex + :param indexed_attributes: Listing of all indexes attribute names + :type indexed_attributes: set of str + """ + name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + allow_encrypting_secondary_indexes = attr.ib( + validator=attr.validators.instance_of(bool), + default=False + ) + _primary_index = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(TableIndex)), + default=None + ) + _indexed_attributes = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(set)), + default=None + ) + + @property + def primary_index(self): + # type: () -> TableIndex + """""" + if self._primary_index is None: + raise Exception('TODO:Indexes unknown. Run refresh_indexed_attributes') + return self._primary_index + + @property + def indexed_attributes(self): + # type: () -> TableIndex + # TODO: Think about merging this and all_index_keys + """""" + if self._indexed_attributes is None: + raise Exception('TODO:Indexes unknown. Run refresh_indexed_attributes') + return self._indexed_attributes + + def all_index_keys(self): + # type: () -> Set[str] + """Provide a set containing the names of all indexed attributes that must not be encrypted.""" + if self._primary_index is None: + return set() + + if self.allow_encrypting_secondary_indexes: + return self.primary_index.attributes + + return self.indexed_attributes + + def refresh_indexed_attributes(self, client): + """Use the provided boto3 DynamoDB client to determine all indexes for this table. + + :param client: Pre-configured boto3 DynamoDB client + :type client: TODO: + """ + table = client.describe_table(TableName=self.name)['Table'] + primary_index = { + key['KeyType']: key['AttributeName'] + for key in table['KeySchema'] + } + indexed_attributes = set(primary_index.values()) + self._primary_index = TableIndex( + partition=primary_index['HASH'], + sort=primary_index.get('RANGE', None) + ) + for group in ('LocalSecondaryIndexes', 'GlobalSecondaryIndexes'): + try: + for index in table[group]: + indexed_attributes.update(set([ + key['AttributeName'] for key in index['KeySchema'] + ])) + except KeyError: + pass # Not all tables will have secondary indexes. + self._indexed_attributes = indexed_attributes diff --git a/src/pylintrc b/src/pylintrc new file mode 100644 index 00000000..de56ef0f --- /dev/null +++ b/src/pylintrc @@ -0,0 +1,12 @@ +[BASIC] +# Allow function names up to 50 characters +function-rgx = [a-z_][a-z0-9_]{2,50}$ + +[DESIGN] +max-args = 10 + +[FORMAT] +max-line-length = 120 + +[REPORTS] +msg-template = {path}:{line}: [{msg_id}({symbol}), {obj}] {msg} diff --git a/test/acceptance/__init__.py b/test/acceptance/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/test/acceptance/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/test/acceptance/acceptance_test_utils.py b/test/acceptance/acceptance_test_utils.py new file mode 100644 index 00000000..dae4cd9c --- /dev/null +++ b/test/acceptance/acceptance_test_utils.py @@ -0,0 +1,243 @@ +# 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. +import base64 +from collections import defaultdict +import json +import os +import sys +sys.path.append(os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '..', + 'functional' +)) + +import pytest +from six.moves.urllib.parse import urlparse + + +from dynamodb_encryption_sdk.material_providers.static import StaticCryptographicMaterialsProvider +from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider +from dynamodb_encryption_sdk.materials.raw import RawDecryptionMaterials, RawEncryptionMaterials +from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey +from dynamodb_encryption_sdk.identifiers import EncryptionKeyTypes, ItemAction, KeyEncodingType +from dynamodb_encryption_sdk.structures import AttributeActions + +import functional_test_vector_generators + +_ENCRYPTED_ITEM_VECTORS_DIR = os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '..', + 'vectors', + 'encrypted_item' +) +_SCENARIO_FILE = os.path.join(_ENCRYPTED_ITEM_VECTORS_DIR, 'scenarios.json') + + +def _filename_from_uri(uri): + parsed = urlparse(uri) + if parsed.scheme != 'file': + raise ValueError('Unsupported URI scheme: "{}"'.format(parsed.scheme)) + relative_path = [parsed.netloc] + for part in parsed.path.split('/'): + if part: + relative_path.append(part) + return os.path.join(_ENCRYPTED_ITEM_VECTORS_DIR, *relative_path) + + +def _action(name): + return functional_test_vector_generators.ACTION_MAP[name.lower()] + + +def _decode_item(item): + for name, attribute in item.items(): + item[name] = functional_test_vector_generators.decode_value(attribute) + + +def _build_plaintext_items(plaintext_file, version): + """""" + with open(plaintext_file) as f: + plaintext_data = json.load(f) + + actions = {} + for name, description in plaintext_data['actions'].items(): + default_action = _action(description['default']) + attribute_actions = { + attribute_name: _action(attribute_action) + for attribute_name, attribute_action + in description.get('override', {}).items() + } + actions[name.lower()] = AttributeActions( + default_action=default_action, + attribute_actions=attribute_actions + ) + + tables = defaultdict(list) + for table_name, table_data in plaintext_data['items'].items(): + table_items = [] + for item in table_data['items']: + item_actions = actions[item['action']].copy() + item_actions.set_index_keys(*table_data['index'].values()) + attributes = item['attributes'].copy() + if not item.get('exact', False): + for group in plaintext_data['versions'].get(table_name, {}).get(version, []): + attributes.update(plaintext_data['attributes'][group]) + _decode_item(attributes) + table_items.append(dict( + item=attributes, + action=item_actions + )) + + tables[table_name] = dict( + index=table_data['index'], + items=table_items + ) + + return tables + + +def _load_ciphertext_items(ciphertext_file): + with open(ciphertext_file) as f: + ciphertexts = json.load(f) + + for _table, items in ciphertexts.items(): + for item in items: + _decode_item(item) + + return ciphertexts + + +def _load_keys(keys_file): + with open(keys_file) as f: + return json.load(f) + + +_KEY_TYPE = { + 'SYMMETRIC': EncryptionKeyTypes.SYMMETRIC, + 'PUBLIC': EncryptionKeyTypes.PUBLIC, + 'PRIVATE': EncryptionKeyTypes.PRIVATE +} +_KEY_ENCODING = { + 'RAW': KeyEncodingType.RAW, + 'DER': KeyEncodingType.DER +} + + +def _load_key(key): + key_material = base64.b64decode(key['material']) + key_type = _KEY_TYPE[key['type'].upper()] + key_encoding = _KEY_ENCODING[key['encoding'].upper()] + return JceNameLocalDelegatedKey( + key=key_material, + algorithm=key['algorithm'], + key_type=key_type, + key_encoding=key_encoding + ) + + +def _load_signing_key(key): + if key['type'].upper() == 'RSA': + key['type'] = 'RSA' + return _load_key(key) + + +def _build_static_cmp(decrypt_key, verify_key): + decryption_key = _load_key(decrypt_key) + verification_key = _load_signing_key(verify_key) + decryption_materials = RawDecryptionMaterials( + decryption_key=decryption_key, + verification_key=verification_key + ) + return StaticCryptographicMaterialsProvider(decryption_materials=decryption_materials) + + +def _build_wrapped_cmp(decrypt_key, verify_key): + unwrapping_key = _load_key(decrypt_key) + signing_key = _load_signing_key(verify_key) + return WrappedCryptographicMaterialsProvider( + signing_key=signing_key, + unwrapping_key=unwrapping_key + ) + + +_CMP_TYPE_MAP = { + 'STATIC': _build_static_cmp, + 'WRAPPED': _build_wrapped_cmp +} + + +def _build_cmp(provider_type, decrypt_key, verify_key): + try: + cmp_builder = _CMP_TYPE_MAP[provider_type.upper()] + except KeyError: + raise ValueError('Unsupported cryptographic materials provider type: "{}"'.format(provider_type)) + return cmp_builder(decrypt_key, verify_key) + + +def _index(item, keys): + return {key: item[key] for key in keys} + + +def _expand_items(ciphertext_items, plaintext_items): + for table_name, table_items in ciphertext_items.items(): + table_index = plaintext_items[table_name]['index'] + for ciphertext_item in table_items: + ct_index = _index(ciphertext_item, plaintext_items[table_name]['index'].values()) + pt_items = [ + item for item + in plaintext_items[table_name]['items'] + if ct_index == _index(item['item'], plaintext_items[table_name]['index'].values()) + ] + if not pt_items: + continue + + if len(pt_items) > 1: + raise Exception('TODO: Ciphertext matches multiple plaintext items: "{}"'.format(ct_index)) + + pt_item = pt_items[0] + yield table_name, table_index, ciphertext_item, pt_item['item'], pt_item['action'] + + +def load_scenarios(): + 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']: + plaintext_file = _filename_from_uri(scenario['plaintext']) + ciphertext_file = _filename_from_uri(scenario['ciphertext']) + plaintext_items = _build_plaintext_items(plaintext_file, scenario['version']) + 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']] + ) + items = _expand_items(ciphertext_items, plaintext_items) + for table_name, table_index, ciphertext_item, plaintext_item, attribute_actions in items: + item_index = _index(ciphertext_item, table_index.values()) + yield pytest.param( + materials_provider, + table_name, + table_index, + ciphertext_item, + plaintext_item, + attribute_actions, + 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'], + table=table_name, + index=str(item_index) + ) + ) diff --git a/test/acceptance/test_a_encrypted_item.py b/test/acceptance/test_a_encrypted_item.py new file mode 100644 index 00000000..273b91db --- /dev/null +++ b/test/acceptance/test_a_encrypted_item.py @@ -0,0 +1,51 @@ +# 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. +import pytest + +from dynamodb_encryption_sdk.encrypted import CryptoConfig +from dynamodb_encryption_sdk.encrypted.item import decrypt_dynamodb_item +from dynamodb_encryption_sdk.structures import EncryptionContext + +from .acceptance_test_utils import load_scenarios + +pytestmark = [pytest.mark.acceptance, pytest.mark.local] + + +@pytest.mark.parametrize( + 'materials_provider, table_name, table_index, ciphertext_item, plaintext_item, attribute_actions', + load_scenarios() +) +def test_item_encryptor( + materials_provider, + table_name, + table_index, + ciphertext_item, + plaintext_item, + attribute_actions +): + encryption_context = EncryptionContext( + table_name=table_name, + partition_key_name=table_index['partition'], + sort_key_name=table_index.get('sort', None) + ) + crypto_config = CryptoConfig( + materials_provider=materials_provider, + encryption_context=encryption_context, + attribute_actions=attribute_actions + ) + decrypted_item = decrypt_dynamodb_item(ciphertext_item.copy(), crypto_config) + assert set(decrypted_item.keys()) == set(plaintext_item.keys()) + for key in decrypted_item: + if key == 'version': + continue + assert decrypted_item[key] == plaintext_item[key] diff --git a/test/functional/__init__.py b/test/functional/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/test/functional/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/test/functional/functional_test_utils.py b/test/functional/functional_test_utils.py new file mode 100644 index 00000000..4c4279e6 --- /dev/null +++ b/test/functional/functional_test_utils.py @@ -0,0 +1,267 @@ +# 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. +from __future__ import division +import copy +from collections import defaultdict +from decimal import Decimal +import itertools + +from boto3.dynamodb.types import Binary +import pytest + +from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey +from dynamodb_encryption_sdk.encrypted.item import decrypt_python_item, encrypt_python_item +from dynamodb_encryption_sdk.identifiers import ItemAction +from dynamodb_encryption_sdk.internal.identifiers import ReservedAttributes +from dynamodb_encryption_sdk.material_providers.static import StaticCryptographicMaterialsProvider +from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider +from dynamodb_encryption_sdk.materials.raw import RawDecryptionMaterials, RawEncryptionMaterials +from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext + +_DELEGATED_KEY_CACHE = defaultdict(lambda: defaultdict(dict)) + + +def _get_from_cache(dk_class, algorithm, key_length): + """Don't generate new keys every time. All we care about is that they are valid keys, not that they are unique.""" + try: + return _DELEGATED_KEY_CACHE[dk_class][algorithm][key_length] + except KeyError: + key = dk_class.generate(algorithm, key_length) + _DELEGATED_KEY_CACHE[dk_class][algorithm][key_length] = key + return key + + +def build_static_jce_cmp(encryption_algorithm, encryption_key_length, signing_algorithm, signing_key_length): + """Build a StaticCryptographicMaterialsProvider using ephemeral JceNameLocalDelegatedKeys as specified.""" + encryption_key = _get_from_cache(JceNameLocalDelegatedKey, encryption_algorithm, encryption_key_length) + authentication_key = _get_from_cache(JceNameLocalDelegatedKey, signing_algorithm, signing_key_length) + encryption_materials = RawEncryptionMaterials( + signing_key=authentication_key, + encryption_key=encryption_key + ) + decryption_materials = RawDecryptionMaterials( + verification_key=authentication_key, + decryption_key=encryption_key + ) + return StaticCryptographicMaterialsProvider( + encryption_materials=encryption_materials, + decryption_materials=decryption_materials + ) + + +def _build_wrapped_jce_cmp(wrapping_algorithm, wrapping_key_length, signing_algorithm, signing_key_length): + """Build a WrappedCryptographicMaterialsProvider using ephemeral JceNameLocalDelegatedKeys as specified.""" + wrapping_key = _get_from_cache(JceNameLocalDelegatedKey, wrapping_algorithm, wrapping_key_length) + signing_key = _get_from_cache(JceNameLocalDelegatedKey, signing_algorithm, signing_key_length) + return WrappedCryptographicMaterialsProvider( + wrapping_key=wrapping_key, + unwrapping_key=wrapping_key, + signing_key=signing_key + ) + + +def _all_encryption(): + """All encryption configurations to test in slow tests.""" + return itertools.chain( + itertools.product(('AES',), (128, 256)), + itertools.product(('RSA',), (1024, 2048, 4096)) + ) + + +def _all_authentication(): + """All authentication configurations to test in slow tests.""" + return itertools.chain( + itertools.product( + ('HmacSHA224', 'HmacSHA256', 'HmacSHA384', 'HmacSHA512'), + (128, 256) + ), + itertools.product( + ('SHA224withRSA', 'SHA256withRSA', 'SHA384withRSA', 'SHA512withRSA'), + (1024, 2048, 4096) + ) + ) + + +def _all_algorithm_pairs(): + """All algorithm pairs (encryption + authentication) to test in slow tests.""" + for encryption_pair, signing_pair in itertools.product(_all_encryption(), _all_authentication()): + yield encryption_pair + signing_pair + + +def _some_algorithm_pairs(): + """Cherry-picked set of algorithm pairs (encryption + authentication) to test in fast tests.""" + return ( + ('AES', 256, 'HmacSHA256', 256), + ('AES', 256, 'SHA256withRSA', 4096), + ('RSA', 4096, 'SHA256withRSA', 4096) + ) + + +_cmp_builders = { + 'static': build_static_jce_cmp, + 'wrapped': _build_wrapped_jce_cmp +} + + +def _all_possible_cmps(algorithm_generator): + """Generate all possible cryptographic materials providers based on the supplied generator.""" + # The AES combinations do the same thing, but this makes sure that the AESWrap name works as expected. + yield _build_wrapped_jce_cmp('AESWrap', 32, 'HmacSHA256', 32) + + for builder_info, args in itertools.product(_cmp_builders.items(), algorithm_generator()): + builder_type, builder_func = builder_info + encryption_algorithm, encryption_key_length, signing_algorithm, signing_key_length = args + + if builder_type == 'static' and encryption_algorithm != 'AES': + # Only AES keys are allowed to be used with static materials + continue + + id_string = '{enc_algorithm}/{enc_key_length} {builder_type} {sig_algorithm}/{sig_key_length}'.format( + enc_algorithm=encryption_algorithm, + enc_key_length=encryption_key_length, + builder_type=builder_type, + sig_algorithm=signing_algorithm, + sig_key_length=signing_key_length + ) + + if encryption_algorithm == 'AES': + encryption_key_length //= 8 + + yield pytest.param( + builder_func( + encryption_algorithm, + encryption_key_length, + signing_algorithm, + signing_key_length + ), + id=id_string + ) + + +def set_parametrized_cmp(metafunc): + """Set paramatrized values for cryptographic materials providers.""" + for name, algorithm_generator in (('all_the_cmps', _all_algorithm_pairs), ('some_cmps', _some_algorithm_pairs)): + if name in metafunc.fixturenames: + metafunc.parametrize(name, _all_possible_cmps(algorithm_generator), scope='module') + + +def set_parametrized_actions(metafunc): + """Set parametrized values for attribute actions""" + if 'parametrized_actions' in metafunc.fixturenames: + metafunc.parametrize( + 'parametrized_actions', + ( + pytest.param(AttributeActions(default_action=ItemAction.ENCRYPT_AND_SIGN), id='encrypt all'), + pytest.param(AttributeActions(default_action=ItemAction.SIGN_ONLY), id='sign only all'), + pytest.param(AttributeActions(default_action=ItemAction.DO_NOTHING), id='do nothing'), + pytest.param( + AttributeActions( + default_action=ItemAction.ENCRYPT_AND_SIGN, + attribute_actions={ + 'number_set': ItemAction.SIGN_ONLY, + 'string_set': ItemAction.SIGN_ONLY, + 'binary_set': ItemAction.SIGN_ONLY + } + ), + id='sign sets, encrypt everything else' + ), + pytest.param( + AttributeActions( + default_action=ItemAction.ENCRYPT_AND_SIGN, + attribute_actions={ + 'number_set': ItemAction.DO_NOTHING, + 'string_set': ItemAction.DO_NOTHING, + 'binary_set': ItemAction.DO_NOTHING + } + ), + id='ignore sets, encrypt everything else' + ), + pytest.param( + AttributeActions( + default_action=ItemAction.DO_NOTHING, + attribute_actions={'map': ItemAction.ENCRYPT_AND_SIGN} + ), + id='encrypt map, ignore everything else' + ), + pytest.param( + AttributeActions( + default_action=ItemAction.SIGN_ONLY, + attribute_actions={ + 'number_set': ItemAction.DO_NOTHING, + 'string_set': ItemAction.DO_NOTHING, + 'binary_set': ItemAction.DO_NOTHING, + 'map': ItemAction.ENCRYPT_AND_SIGN + } + ), + id='ignore sets, encrypt map, sign everything else' + ) + ) + ) + + +def set_parametrized_item(metafunc): + """Set parametrized values for items to cycle.""" + if 'parametrized_item' in metafunc.fixturenames: + metafunc.parametrize( + 'parametrized_item', + ( + pytest.param(diverse_item(), id='diverse item'), + ) + ) + + +def diverse_item(): + base_item = { + 'int': 5, + 'decimal': Decimal('123.456'), + 'string': 'this is a string', + 'binary': b'this is a bytestring! \x01', + 'number_set': set([5, 4, 3]), + 'string_set': set(['abc', 'def', 'geh']), + 'binary_set': set([b'\x00\x00\x00', b'\x00\x01\x00', b'\x00\x00\x02']) + } + base_item['list'] = [copy.copy(i) for i in base_item.values()] + base_item['map'] = copy.deepcopy(base_item) + return copy.deepcopy(base_item) + + +_reserved_attributes = set([attr.value for attr in ReservedAttributes]) + + +def cycle_item_check(plaintext_item, crypto_config): + """Common logic for cycled item (plaintext->encrypted->decrypted) tests: used by many test suites.""" + ciphertext_item = encrypt_python_item(plaintext_item, crypto_config) + + # Verify that all expected attributes are present + ciphertext_attributes = set(ciphertext_item.keys()) + plaintext_attributes = set(plaintext_item.keys()) + if crypto_config.attribute_actions.take_no_actions: + assert ciphertext_attributes == plaintext_attributes + else: + assert ciphertext_attributes == plaintext_attributes.union(_reserved_attributes) + + for name, value in ciphertext_item.items(): + # Skip the attributes we add + if name in _reserved_attributes: + continue + + # If the attribute should have been encrypted, verify that it is Binary and different from the original + if crypto_config.attribute_actions.action(name) is ItemAction.ENCRYPT_AND_SIGN: + assert isinstance(value, Binary) + assert value != plaintext_item[name] + # Otherwise, verify that it is the same as the original + else: + assert value == plaintext_item[name] + + cycled_item = decrypt_python_item(ciphertext_item, crypto_config) + assert cycled_item == plaintext_item diff --git a/test/functional/functional_test_vector_generators.py b/test/functional/functional_test_vector_generators.py new file mode 100644 index 00000000..a425d2b2 --- /dev/null +++ b/test/functional/functional_test_vector_generators.py @@ -0,0 +1,159 @@ +# 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. +"""Helper tools for attribute de/serialization functional tests.""" +import base64 +from decimal import Decimal +import codecs +import json +import os + +from boto3.dynamodb.types import Binary +from dynamodb_encryption_sdk.identifiers import ItemAction +from dynamodb_encryption_sdk.structures import AttributeActions + +_ATTRIBUTE_TEST_VECTOR_FILE_TEMPLATE = os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '..', + 'vectors', + '{mode}_attribute.json' +) +_MATERIAL_DESCRIPTION_TEST_VECTORS_FILE = os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '..', + 'vectors', + 'material_description.json' +) +_STRING_TO_SIGN_TEST_VECTORS_FILE = os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '..', + 'vectors', + 'string_to_sign.json' +) + + +def decode_value(value, transform_binary=False): + def _decode_string(_value): + return _value + + def _decode_number(_value): + return str(Decimal(_value).normalize()) + + def _decode_binary(_value): + raw = base64.b64decode(_value) + if transform_binary: + return Binary(raw) + return raw + + def _binary_sort_key(x): + if transform_binary: + return x.value + return x + + def _passthrough_sort_key(x): + return x + + def _decode_set(_value, member_decode, key_func=_passthrough_sort_key): + decoded_members = [] + for member in _value: + decoded_members.append(member_decode(member)) + return sorted(decoded_members, key=key_func) + + def _decode_binary_set(_value): + return _decode_set(_value, _decode_binary, _binary_sort_key) + + def _decode_string_set(_value): + return _decode_set(_value, _decode_string) + + def _decode_number_set(_value): + return _decode_set(_value, _decode_number) + + def _decode_list(_value): + decoded_members = [] + for member in _value: + decoded_members.append(_decode_complex_value(member)) + return decoded_members + + def _decode_map(_value): + decoded_value = {} + for member_key, member_value in _value.items(): + decoded_value[member_key] = _decode_complex_value(member_value) + return decoded_value + + _decode_mapping = { + 'S': _decode_string, + 'B': _decode_binary, + 'SS': _decode_string_set, + 'BS': _decode_binary_set, + 'L': _decode_list, + 'M': _decode_map, + 'N': _decode_number, + 'NS': _decode_number_set + } + + def _decode_complex_value(_value): + key, item = list(_value.items())[0] + transform = _decode_mapping.get(key, None) + if transform is None: + return {key: item} + return {key: transform(item)} + + return _decode_complex_value(value) + + +def attribute_test_vectors(mode): + filepath = _ATTRIBUTE_TEST_VECTOR_FILE_TEMPLATE.format(mode=mode) + with open(filepath) as f: + vectors = json.load(f) + for vector in vectors: + yield ( + decode_value(vector['attribute']), + base64.b64decode(codecs.encode(vector['serialized'], 'utf-8')) + ) + + +def material_description_test_vectors(): + with open(_MATERIAL_DESCRIPTION_TEST_VECTORS_FILE) as f: + vectors = json.load(f) + for vector in vectors: + yield ( + vector['material_description'], + decode_value({'B': codecs.encode(vector['serialized'], 'utf-8')}) + ) + + +ACTION_MAP = { + 'encrypt': ItemAction.ENCRYPT_AND_SIGN, + 'sign': ItemAction.SIGN_ONLY, + 'nothing': ItemAction.DO_NOTHING +} + + +def string_to_sign_test_vectors(): + with open(_STRING_TO_SIGN_TEST_VECTORS_FILE) as f: + vectors = json.load(f) + for vector in vectors: + item = { + key: decode_value(value['value']) + for key, value in vector['item'].items() + } + bare_actions = {key: ACTION_MAP[value['action']] for key, value in vector['item'].items()} + attribute_actions = AttributeActions( + default_action=ItemAction.DO_NOTHING, + attribute_actions=bare_actions + ) + yield ( + item, + vector['table'], + attribute_actions, + base64.b64decode(codecs.encode(vector['string_to_sign'], 'utf-8')) + ) diff --git a/test/functional/hypothesis_strategies.py b/test/functional/hypothesis_strategies.py new file mode 100644 index 00000000..e4f9038d --- /dev/null +++ b/test/functional/hypothesis_strategies.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# 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. +"""Helper resources for functional tests.""" +from collections import namedtuple +from decimal import Decimal + +from boto3.dynamodb.types import Binary, DYNAMODB_CONTEXT +import hypothesis +from hypothesis.strategies import binary, booleans, dictionaries, deferred, fractions, just, lists, none, sets, text + +SLOW_SETTINGS = hypothesis.settings( + suppress_health_check=( + hypothesis.HealthCheck.too_slow, + hypothesis.HealthCheck.data_too_large, + hypothesis.HealthCheck.hung_test, + hypothesis.HealthCheck.large_base_example + ), + timeout=hypothesis.unlimited, + deadline=None +) +VERY_SLOW_SETTINGS = hypothesis.settings( + SLOW_SETTINGS, + max_examples=1000, + max_iterations=1500 +) +MAX_ITEM_BYTES = 400 * 1024 * 1024 + +NumberRange = namedtuple('NumberRange', ('min', 'max')) +_MIN_NUMBER = '1E-128' # The DDB min is 1E-130, but DYNAMODB_CONTEXT Emin is -128 +_MAX_NUMBER = '9.9999999999999999999999999999999999999E+125' +POSITIVE_NUMBER_RANGE = NumberRange( + min=Decimal(_MIN_NUMBER), + max=Decimal(_MAX_NUMBER) +) +NEGATIVE_NUMBER_RANGE = NumberRange( + min=Decimal('-' + _MAX_NUMBER), + max=Decimal('-' + _MIN_NUMBER) +) + + +ddb_string = text(min_size=1, max_size=MAX_ITEM_BYTES) +ddb_string_set = sets(ddb_string, min_size=1) + + +def _ddb_fraction_to_decimal(val): + """hypothesis does not support providing a custom Context, so working around that""" + return DYNAMODB_CONTEXT.create_decimal(Decimal(val.numerator) / Decimal(val.denominator)) + + +_ddb_positive_numbers = fractions( + min_value=POSITIVE_NUMBER_RANGE.min, + max_value=POSITIVE_NUMBER_RANGE.max +).map(_ddb_fraction_to_decimal) +_ddb_negative_numbers = fractions( + min_value=NEGATIVE_NUMBER_RANGE.min, + max_value=NEGATIVE_NUMBER_RANGE.max +).map(_ddb_fraction_to_decimal) + +ddb_number = _ddb_negative_numbers | just(Decimal('0')) | _ddb_positive_numbers +ddb_number_set = sets(ddb_number, min_size=1) + +ddb_binary = binary(min_size=1, max_size=MAX_ITEM_BYTES).map(Binary) +ddb_binary_set = sets(ddb_binary, min_size=1) + +ddb_boolean = booleans() +ddb_null = none() + +ddb_scalar_types = ( + ddb_string + | ddb_number + | ddb_binary + | ddb_boolean + | ddb_null +) + +ddb_set_types = ( + ddb_string_set + | ddb_number_set + | ddb_binary_set +) +# TODO: List and Map types have a max depth of 32 +ddb_map_type = deferred(lambda: dictionaries( + keys=text(), + values=( + ddb_scalar_types + | ddb_set_types + | ddb_list_type + | ddb_map_type + ), + min_size=1 +)) +ddb_list_type = deferred(lambda: lists( + ddb_scalar_types + | ddb_set_types + | ddb_list_type + | ddb_map_type, + min_size=1 +)) +ddb_document_types = ddb_map_type | ddb_list_type + +ddb_attribute_values = ddb_scalar_types | ddb_set_types | ddb_list_type + +ddb_items = dictionaries( + keys=text(min_size=1, max_size=255), + values=ddb_scalar_types | ddb_set_types | ddb_list_type +) + + +material_descriptions = deferred(lambda: dictionaries( + keys=text(), + values=text(), + min_size=1 +)) diff --git a/test/functional/test_f_authentication_string_to_sign.py b/test/functional/test_f_authentication_string_to_sign.py new file mode 100644 index 00000000..545919a5 --- /dev/null +++ b/test/functional/test_f_authentication_string_to_sign.py @@ -0,0 +1,25 @@ +# 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. +"""Functional tests for material description de/serialization.""" +import pytest + +from .functional_test_vector_generators import string_to_sign_test_vectors +from dynamodb_encryption_sdk.internal.crypto.authentication import _string_to_sign + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +@pytest.mark.parametrize('item, table_name, attribute_actions, expected_result', string_to_sign_test_vectors()) +def test_string_to_sign(item, table_name, attribute_actions, expected_result): + generated_string = _string_to_sign(item, table_name, attribute_actions) + assert generated_string == expected_result diff --git a/test/functional/test_f_encrypted_item.py b/test/functional/test_f_encrypted_item.py new file mode 100644 index 00000000..0a39302a --- /dev/null +++ b/test/functional/test_f_encrypted_item.py @@ -0,0 +1,105 @@ +# 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. +import hypothesis +import pytest + +from .functional_test_utils import ( + build_static_jce_cmp, cycle_item_check, set_parametrized_actions, set_parametrized_cmp, set_parametrized_item +) +from .hypothesis_strategies import ddb_items, SLOW_SETTINGS, VERY_SLOW_SETTINGS +from dynamodb_encryption_sdk.encrypted import CryptoConfig +from dynamodb_encryption_sdk.encrypted.item import decrypt_python_item +from dynamodb_encryption_sdk.exceptions import DecryptionError +from dynamodb_encryption_sdk.structures import AttributeActions, EncryptionContext + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +def pytest_generate_tests(metafunc): + set_parametrized_actions(metafunc) + set_parametrized_cmp(metafunc) + set_parametrized_item(metafunc) + + +def test_unsigned_item(): + crypto_config = CryptoConfig( + materials_provider=build_static_jce_cmp('AES', 256, 'HmacSHA256', 256), + encryption_context=EncryptionContext(), + attribute_actions=AttributeActions() + ) + item = {'test': 'no signature'} + + with pytest.raises(DecryptionError) as exc_info: + decrypt_python_item(item, crypto_config) + + exc_info.match(r'No signature attribute found in item') + + +def test_ephemeral_item_cycle(some_cmps, parametrized_actions, parametrized_item): + """Test a small number of curated CMPs against a small number of curated items.""" + crypto_config = CryptoConfig( + materials_provider=some_cmps, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + cycle_item_check(parametrized_item, crypto_config) + + +@pytest.mark.slow +def test_ephemeral_item_cycle_slow(all_the_cmps, parametrized_actions, parametrized_item): + """Test ALL THE CMPS against a small number of curated items.""" + crypto_config = CryptoConfig( + materials_provider=all_the_cmps, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + cycle_item_check(parametrized_item, crypto_config) + + +@pytest.mark.slow +@SLOW_SETTINGS +@hypothesis.given(item=ddb_items) +def test_ephemeral_item_cycle_hypothesis_slow(some_cmps, parametrized_actions, item): + """Test a small number of curated CMPs against a large number of items.""" + crypto_config = CryptoConfig( + materials_provider=some_cmps, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + cycle_item_check(item, crypto_config) + + +@pytest.mark.veryslow +@VERY_SLOW_SETTINGS +@hypothesis.given(item=ddb_items) +def test_ephemeral_item_cycle_hypothesis_veryslow(some_cmps, parametrized_actions, item): + """Test a small number of curated CMPs against ALL THE ITEMS.""" + crypto_config = CryptoConfig( + materials_provider=some_cmps, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + cycle_item_check(item, crypto_config) + + +@pytest.mark.nope +@VERY_SLOW_SETTINGS +@hypothesis.given(item=ddb_items) +def test_ephemeral_item_cycle_hypothesis_nope(all_the_cmps, parametrized_actions, item): + """Test ALL THE CMPs against ALL THE ITEMS.""" + crypto_config = CryptoConfig( + materials_provider=all_the_cmps, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + cycle_item_check(item, crypto_config) diff --git a/test/functional/test_f_formatting_attribute_serialization.py b/test/functional/test_f_formatting_attribute_serialization.py new file mode 100644 index 00000000..d128758b --- /dev/null +++ b/test/functional/test_f_formatting_attribute_serialization.py @@ -0,0 +1,106 @@ +# 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. +"""Functional tests for attribute de/serialization.""" +from boto3.dynamodb.types import TypeDeserializer, TypeSerializer +import hypothesis +import pytest + +from dynamodb_encryption_sdk.exceptions import DeserializationError, SerializationError +from dynamodb_encryption_sdk.internal.formatting.deserialize.attribute import deserialize_attribute +from dynamodb_encryption_sdk.internal.formatting.serialize.attribute import serialize_attribute +from dynamodb_encryption_sdk.internal.formatting.transform import ddb_to_dict, dict_to_ddb +from .functional_test_vector_generators import attribute_test_vectors +from .hypothesis_strategies import ddb_attribute_values, ddb_items, SLOW_SETTINGS, VERY_SLOW_SETTINGS + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +@pytest.mark.parametrize('attribute, serialized', attribute_test_vectors('serialize')) +def test_serialize_attribute(attribute, serialized): + serialized_attribute = serialize_attribute(attribute) + assert serialized_attribute == serialized + + +@pytest.mark.parametrize('attribute, expected_type, expected_message', ( + ({'_': None}, SerializationError, r'Unsupported DynamoDB data type: *'), + ({}, SerializationError, r'cannot serialize attribute: incorrect number of members *'), + ({'a': None, 'b': None}, SerializationError, r'cannot serialize attribute: incorrect number of members *'), + (None, TypeError, r'Invalid attribute type *') +)) +def test_serialize_attribute_errors(attribute, expected_type, expected_message): + with pytest.raises(expected_type) as excinfo: + serialize_attribute(attribute) + + excinfo.match(expected_message) + + +@pytest.mark.parametrize('attribute, serialized', attribute_test_vectors('deserialize')) +def test_deserialize_attribute(attribute, serialized): + deserialized_attribute = deserialize_attribute(serialized) + assert deserialized_attribute == attribute + + +@pytest.mark.parametrize('data, expected_type, expected_message', ( + (b'', DeserializationError, r'Empty serialized attribute data'), + (b'_', DeserializationError, r'Malformed serialized data'), + (b'\x00_', DeserializationError, r'Unsupported tag: *'), + (b'__', DeserializationError, r'Invalid tag: reserved byte is not null'), + (b'\x00M\x00\x00\x00\x01\x00\x00', DeserializationError, r'Malformed serialized map: *') +)) +def test_deserialize_attribute_errors(data, expected_type, expected_message): + with pytest.raises(expected_type) as exc_info: + deserialize_attribute(data) + + exc_info.match(expected_message) + + +def _serialize_deserialize_cycle(attribute): + raw_attribute = TypeSerializer().serialize(attribute) + serialized_attribute = serialize_attribute(raw_attribute) + cycled_attribute = deserialize_attribute(serialized_attribute) + deserialized_attribute = TypeDeserializer().deserialize(cycled_attribute) + assert deserialized_attribute == attribute + + +@pytest.mark.slow +@SLOW_SETTINGS +@hypothesis.given(ddb_attribute_values) +def test_serialize_deserialize_attribute_slow(attribute): + _serialize_deserialize_cycle(attribute) + + +@pytest.mark.veryslow +@VERY_SLOW_SETTINGS +@hypothesis.given(ddb_attribute_values) +def test_serialize_deserialize_attribute_vslow(attribute): + _serialize_deserialize_cycle(attribute) + + +def _ddb_dict_ddb_transform_cycle(item): + ddb_item = dict_to_ddb(item) + cycled_item = ddb_to_dict(ddb_item) + assert cycled_item == item + + +@pytest.mark.slow +@SLOW_SETTINGS +@hypothesis.given(ddb_items) +def test_dict_to_ddb_and_back_slow(item): + _ddb_dict_ddb_transform_cycle(item) + + +@pytest.mark.veryslow +@VERY_SLOW_SETTINGS +@hypothesis.given(ddb_items) +def test_dict_to_ddb_and_back_vslow(item): + _ddb_dict_ddb_transform_cycle(item) diff --git a/test/functional/test_f_formatting_material_description_serialization.py b/test/functional/test_f_formatting_material_description_serialization.py new file mode 100644 index 00000000..37d66b28 --- /dev/null +++ b/test/functional/test_f_formatting_material_description_serialization.py @@ -0,0 +1,90 @@ +# 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. +"""Functional tests for material description de/serialization.""" +import hypothesis +import pytest + +from .functional_test_vector_generators import material_description_test_vectors +from .hypothesis_strategies import material_descriptions, SLOW_SETTINGS, VERY_SLOW_SETTINGS +from dynamodb_encryption_sdk.exceptions import InvalidMaterialsetError, InvalidMaterialsetVersionError +from dynamodb_encryption_sdk.internal.formatting.material_description import ( + deserialize as deserialize_material_description, serialize as serialize_material_description +) + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +@pytest.mark.parametrize('material_description, serialized', material_description_test_vectors()) +def test_serialize_material_description(material_description, serialized): + serialized_material_description = serialize_material_description(material_description) + assert serialized_material_description == serialized + + +@pytest.mark.parametrize('data, expected_type, expected_message', ( + ({'test': 5}, InvalidMaterialsetError, 'Invalid name or value in material description: *'), + ({5: 'test'}, InvalidMaterialsetError, 'Invalid name or value in material description: *'), +)) +def test_serialize_material_description_errors(data, expected_type, expected_message): + with pytest.raises(expected_type) as exc_info: + serialize_material_description(data) + + exc_info.match(expected_message) + + +@pytest.mark.parametrize('material_description, serialized', material_description_test_vectors()) +def test_deserialize_material_description(material_description, serialized): + deserialized_material_description = deserialize_material_description(serialized) + assert deserialized_material_description == material_description + + +@pytest.mark.parametrize('data, expected_type, expected_message', ( + # Invalid version + ({'B': b'\x00\x00\x00\x01'}, InvalidMaterialsetVersionError, r'Invalid material description version: *'), + # Malformed version + ({'B': b'\x00\x00\x00'}, InvalidMaterialsetError, r'Malformed material description version'), + # Invalid attribute type + ({'S': 'not bytes'}, InvalidMaterialsetError, r'Invalid material description'), + # Invalid data: not a DDB attribute + (b'bare bytes', InvalidMaterialsetError, r'Invalid material description'), + # Partial entry + ( + {'B': b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01A\x00\x00\x00\x01'}, + InvalidMaterialsetError, + r'Invalid material description' + ) +)) +def test_deserialize_material_description_errors(data, expected_type, expected_message): + with pytest.raises(expected_type) as exc_info: + deserialize_material_description(data) + + exc_info.match(expected_message) + + +def _serialize_deserialize_cycle(material_description): + serialized_material_description = serialize_material_description(material_description) + deserialized_material_description = deserialize_material_description(serialized_material_description) + assert deserialized_material_description == material_description + + +@pytest.mark.slow +@SLOW_SETTINGS +@hypothesis.given(material_descriptions) +def test_serialize_deserialize_material_description_slow(material_description): + _serialize_deserialize_cycle(material_description) + + +@pytest.mark.veryslow +@VERY_SLOW_SETTINGS +@hypothesis.given(material_descriptions) +def test_serialize_deserialize_material_description_vslow(material_description): + _serialize_deserialize_cycle(material_description) diff --git a/test/functional/test_f_identifiers.py b/test/functional/test_f_identifiers.py new file mode 100644 index 00000000..61986780 --- /dev/null +++ b/test/functional/test_f_identifiers.py @@ -0,0 +1,67 @@ +# 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. +import operator + +import pytest + +from dynamodb_encryption_sdk.identifiers import ItemAction + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +@pytest.mark.parametrize('left, right, expected', ( + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.SIGN_ONLY, ItemAction.ENCRYPT_AND_SIGN), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.DO_NOTHING, ItemAction.ENCRYPT_AND_SIGN), + (ItemAction.SIGN_ONLY, ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN), + (ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY), + (ItemAction.SIGN_ONLY, ItemAction.DO_NOTHING, ItemAction.SIGN_ONLY), + (ItemAction.DO_NOTHING, ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN), + (ItemAction.DO_NOTHING, ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY), + (ItemAction.DO_NOTHING, ItemAction.DO_NOTHING, ItemAction.DO_NOTHING), +)) +def test_item_action_max(left, right, expected): + assert max(left, right) == expected + + +@pytest.mark.parametrize('left, right, expected', ( + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.DO_NOTHING, ItemAction.DO_NOTHING), + (ItemAction.SIGN_ONLY, ItemAction.ENCRYPT_AND_SIGN, ItemAction.SIGN_ONLY), + (ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY), + (ItemAction.SIGN_ONLY, ItemAction.DO_NOTHING, ItemAction.DO_NOTHING), + (ItemAction.DO_NOTHING, ItemAction.ENCRYPT_AND_SIGN, ItemAction.DO_NOTHING), + (ItemAction.DO_NOTHING, ItemAction.SIGN_ONLY, ItemAction.DO_NOTHING), + (ItemAction.DO_NOTHING, ItemAction.DO_NOTHING, ItemAction.DO_NOTHING), +)) +def test_item_action_min(left, right, expected): + assert min(left, right) == expected + + +@pytest.mark.parametrize('left, right, expected_comparison', ( + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.ENCRYPT_AND_SIGN, operator.eq), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.SIGN_ONLY, operator.ne), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.SIGN_ONLY, operator.gt), + (ItemAction.ENCRYPT_AND_SIGN, ItemAction.DO_NOTHING, operator.gt), + (ItemAction.SIGN_ONLY, ItemAction.ENCRYPT_AND_SIGN, operator.lt), + (ItemAction.SIGN_ONLY, ItemAction.SIGN_ONLY, operator.eq), + (ItemAction.SIGN_ONLY, ItemAction.DO_NOTHING, operator.ne), + (ItemAction.SIGN_ONLY, ItemAction.DO_NOTHING, operator.gt), + (ItemAction.DO_NOTHING, ItemAction.ENCRYPT_AND_SIGN, operator.lt), + (ItemAction.DO_NOTHING, ItemAction.SIGN_ONLY, operator.lt), + (ItemAction.DO_NOTHING, ItemAction.DO_NOTHING, operator.eq), + (ItemAction.DO_NOTHING, ItemAction.ENCRYPT_AND_SIGN, operator.ne) +)) +def test_item_action_comp(left, right, expected_comparison): + assert expected_comparison(left, right) diff --git a/test/functional/test_f_str_ops.py b/test/functional/test_f_str_ops.py new file mode 100644 index 00000000..08782367 --- /dev/null +++ b/test/functional/test_f_str_ops.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# 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. +"""Test suite for dynamodb_encryption_sdk.internal.str_ops""" +import codecs + +import pytest + +from dynamodb_encryption_sdk.internal.str_ops import to_bytes, to_str + +pytestmark = [pytest.mark.functional, pytest.mark.local] + + +@pytest.mark.parametrize('data, expected_output', ( + ('asdf', 'asdf'), + (b'asdf', 'asdf'), + (codecs.encode(u'Предисловие', 'utf-8'), u'Предисловие'), + (u'Предисловие', u'Предисловие') +)) +def test_to_str(data, expected_output): + test = to_str(data) + assert test == expected_output + + +@pytest.mark.parametrize('data, expected_output', ( + ('asdf', b'asdf'), + (b'asdf', b'asdf'), + (b'\x3a\x00\x99', b'\x3a\x00\x99'), + (u'Предисловие', codecs.encode(u'Предисловие', 'utf-8')), + (codecs.encode(u'Предисловие', 'utf-8'), codecs.encode(u'Предисловие', 'utf-8')) +)) +def test_to_bytes(data, expected_output): + test = to_bytes(data) + assert test == expected_output diff --git a/test/integration/__init__.py b/test/integration/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/test/integration/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py new file mode 100644 index 00000000..f2673cd0 --- /dev/null +++ b/test/integration/integration_test_utils.py @@ -0,0 +1,47 @@ +# 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. +import os +import sys +sys.path.append(os.path.join( + os.path.abspath(os.path.dirname(__file__)), + '..', + 'functional' +)) + +import pytest + +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider + +import functional_test_utils, hypothesis_strategies + +AWS_KMS_KEY_ID = 'AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID' + + +@pytest.fixture +def cmk_arn(): + """Retrieves the target CMK ARN from environment variable.""" + arn = os.environ.get(AWS_KMS_KEY_ID, None) + if arn is None: + raise ValueError( + 'Environment variable "{}" must be set to a valid KMS CMK ARN for integration tests to run'.format( + AWS_KMS_KEY_ID + ) + ) + if arn.startswith('arn:') and ':alias/' not in arn: + return arn + raise ValueError('KMS CMK ARN provided for integration tests must be a key not an alias') + + +@pytest.fixture +def aws_kms_cmp(): + return AwsKmsCryptographicMaterialsProvider(key_id=cmk_arn()) diff --git a/test/integration/test_i_materials_provider_aws_kms.py b/test/integration/test_i_materials_provider_aws_kms.py new file mode 100644 index 00000000..e22c4810 --- /dev/null +++ b/test/integration/test_i_materials_provider_aws_kms.py @@ -0,0 +1,58 @@ +# 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. +import hypothesis +import pytest + +from .integration_test_utils import aws_kms_cmp, functional_test_utils, hypothesis_strategies +from dynamodb_encryption_sdk.encrypted import CryptoConfig +from dynamodb_encryption_sdk.structures import EncryptionContext + +pytestmark = pytest.mark.integ + + +def pytest_generate_tests(metafunc): + functional_test_utils.set_parametrized_actions(metafunc) + functional_test_utils.set_parametrized_item(metafunc) + + +def test_aws_kms_item_cycle(aws_kms_cmp, parametrized_actions, parametrized_item): + crypto_config = CryptoConfig( + materials_provider=aws_kms_cmp, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + functional_test_utils.cycle_item_check(parametrized_item, crypto_config) + + +@pytest.mark.slow +@hypothesis_strategies.SLOW_SETTINGS +@hypothesis.given(item=hypothesis_strategies.ddb_items) +def test_aws_kms_item_cycle_hypothesis_slow(aws_kms_cmp, parametrized_actions, item): + crypto_config = CryptoConfig( + materials_provider=aws_kms_cmp, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + functional_test_utils.cycle_item_check(item, crypto_config) + + +@pytest.mark.veryslow +@hypothesis_strategies.VERY_SLOW_SETTINGS +@hypothesis.given(item=hypothesis_strategies.ddb_items) +def test_aws_kms_item_cycle_hypothesis_veryslow(aws_kms_cmp, parametrized_actions, item): + crypto_config = CryptoConfig( + materials_provider=aws_kms_cmp, + encryption_context=EncryptionContext(), + attribute_actions=parametrized_actions + ) + functional_test_utils.cycle_item_check(item, crypto_config) diff --git a/test/pylintrc b/test/pylintrc new file mode 100644 index 00000000..bc06e8de --- /dev/null +++ b/test/pylintrc @@ -0,0 +1,21 @@ +[MESSAGES CONTROL] +# Disabling messages that we either don't care about +# for tests or are necessary to break for tests. +# +# C0103 : invalid-name (we prefer long, descriptive, names for tests) +# C0111 : missing-docstring (we don't write docstrings for tests) +# E1101 : no-member (raised on patched objects with mock checks) +# R0801 : duplicate-code (unit tests for similar things tend to be similar) +# W0212 : protected-access (raised when calling _ methods) +# W0621 : redefined-outer-name (raised when using pytest-mock) +# W0613 : unused-argument (raised when patches are needed but not called) +disable = C0103, C0111, E1101, R0801, W0212, W0621, W0613 + +[DESIGN] +max-args = 10 + +[FORMAT] +max-line-length = 120 + +[REPORTS] +msg-template = {path}:{line}: [{msg_id}({symbol}), {obj}] {msg} diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/test/unit/material_providers/__init__.py b/test/unit/material_providers/__init__.py new file mode 100644 index 00000000..1ccc7fa1 --- /dev/null +++ b/test/unit/material_providers/__init__.py @@ -0,0 +1,12 @@ +# 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. diff --git a/test/unit/material_providers/test_aws_kms.py b/test/unit/material_providers/test_aws_kms.py new file mode 100644 index 00000000..701143ce --- /dev/null +++ b/test/unit/material_providers/test_aws_kms.py @@ -0,0 +1,51 @@ +# 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. +import boto3 +import botocore.session +from moto import mock_kms +import pytest + +from dynamodb_encryption_sdk.material_providers.aws_kms import AwsKmsCryptographicMaterialsProvider + +pytestmark = [pytest.mark.unit, pytest.mark.local] + + +def build_cmp(**custom_kwargs): + kwargs = dict( + key_id='test_key_id', + botocore_session=botocore.session.Session() + ) + kwargs.update(custom_kwargs) + if isinstance(kwargs.get('regional_clients', None), dict): + for region, client in kwargs['regional_clients'].items(): + if client == 'generate client': + kwargs['regional_clients'][region] = boto3.client('kms', region='us-west-2') + return AwsKmsCryptographicMaterialsProvider(**kwargs) + + +@mock_kms +@pytest.mark.parametrize('invalid_kwargs', ( + dict(key_id=9), + dict(botocore_session='not a botocore session'), + dict(grant_tokens='not a tuple'), + dict(grant_tokens=(1, 5)), + dict(material_description='not a dict'), + dict(material_description={2: 'value'}), + dict(material_description={'key': 9}), + dict(regional_clients='not a dict'), + dict(regional_clients={3: 'generate client'}), + dict(regional_clients={'region': 'not a client'}) +)) +def test_attrs_fail(invalid_kwargs): + with pytest.raises(TypeError): + build_cmp(**invalid_kwargs) diff --git a/test/vectors/deserialize_attribute.json b/test/vectors/deserialize_attribute.json new file mode 100644 index 00000000..5be0ee32 --- /dev/null +++ b/test/vectors/deserialize_attribute.json @@ -0,0 +1,152 @@ +[ + { + "attribute": {"NULL": true}, + "serialized": "AAA=" + }, + { + "attribute": {"BOOL": true}, + "serialized": "AD8B" + }, + { + "attribute": {"BOOL": false}, + "serialized": "AD8A" + }, + { + "attribute": {"N": "55"}, + "serialized": "AG4AAAACNTU=" + }, + { + "attribute": {"N": "55.34"}, + "serialized": "AG4AAAAFNTUuMzQ=" + }, + { + "attribute": { + "NS": [ + "1.23", + "34", + "35", + "55.2" + ] + }, + "serialized": "AE4AAAAEAAAABDEuMjMAAAACMzQAAAACMzUAAAAENTUuMg==" + }, + { + "attribute": {"S": "test ascii string"}, + "serialized": "AHMAAAARdGVzdCBhc2NpaSBzdHJpbmc=" + }, + { + "attribute": { + "SS": [ + "another ascii string", + "test ascii string" + ] + }, + "serialized": "AFMAAAACAAAAFGFub3RoZXIgYXNjaWkgc3RyaW5nAAAAEXRlc3QgYXNjaWkgc3RyaW5n" + }, + { + "attribute": {"B": "AAECAw=="}, + "serialized": "AGIAAAAEAAECAw==" + }, + { + "attribute": {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + "serialized": "AGIAAAAUYW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + }, + { + "attribute": { + "BS": [ + "AAECAw==", + "YW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + ] + }, + "serialized": "AEIAAAACAAAABAABAgMAAAAUYW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + }, + { + "attribute": { + "L": [ + {"N": "55.34"}, + {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + {"S": "test ascii string"} + ] + }, + "serialized": "AEwAAAADAG4AAAAFNTUuMzQAYgAAABRhbiBhc2NpaSBieXRlIHN0cmluZwBzAAAAEXRlc3QgYXNjaWkgc3RyaW5n" + }, + { + "attribute": { + "M": { + "one thing": {"NULL": true}, + "maybe a bool?": {"BOOL": false}, + "and a list too": { + "L": [ + {"N": "55.34"}, + {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + {"S": "test ascii string"} + ] + } + } + }, + "serialized": "AE0AAAADAHMAAAAOYW5kIGEgbGlzdCB0b28ATAAAAAMAbgAAAAU1NS4zNABiAAAAFGFuIGFzY2lpIGJ5dGUgc3RyaW5nAHMAAAARdGVzdCBhc2NpaSBzdHJpbmcAcwAAAA1tYXliZSBhIGJvb2w/AD8AAHMAAAAJb25lIHRoaW5nAAA=" + }, + { + "attribute": { + "M": { + "complex_map": {"M": { + "a": {"L": [ + {"S": "asdf"}, + {"N": "99"}, + {"M": { + "c": {"BOOL": true}, + "b": {"NULL": true} + }} + ]} + }}, + "another_key": {"BOOL": false} + } + }, + "serialized": "AE0AAAACAHMAAAALYW5vdGhlcl9rZXkAPwAAcwAAAAtjb21wbGV4X21hcABNAAAAAQBzAAAAAWEATAAAAAMAcwAAAARhc2RmAG4AAAACOTkATQAAAAIAcwAAAAFiAAAAcwAAAAFjAD8B" + }, + { + "attribute": {"M": { + "SingleMap": {"M": { + "FOO": {"S": "BAR"} + }}, + "InnerList": {"L": [ + {"S": "ComplexList"}, + {"N": "5"}, + {"B": "AAECAwQF"}, + {"L": [ + {"BOOL": true}, + {"NULL": true}, + {"NULL": true}, + {"L": [ + {"BOOL": false} + ]}, + {"M": { + "Pink": {"S": "Floyd"}, + "Version": {"N": "1"}, + "Test": {"BOOL": true} + }} + ]}, + {"NULL": true}, + {"M": { + "True": {"BOOL": true}, + "List": {"L": [ + {"N": "5"}, + {"N": "4"}, + {"N": "3"}, + {"N": "2"}, + {"N": "1"} + ]}, + "Map": {"M": { + "Nested": {"BOOL": true} + }} + }} + ]}, + "StringSet": {"SS": [ + "bar", + "baz", + "foo" + ]} + }}, + "serialized": "AE0AAAADAHMAAAAJSW5uZXJMaXN0AEwAAAAGAHMAAAALQ29tcGxleExpc3QAbgAAAAE1AGIAAAAGAAECAwQFAEwAAAAFAD8BAAAAAABMAAAAAQA/AABNAAAAAwBzAAAABFBpbmsAcwAAAAVGbG95ZABzAAAABFRlc3QAPwEAcwAAAAdWZXJzaW9uAG4AAAABMQAAAE0AAAADAHMAAAAETGlzdABMAAAABQBuAAAAATUAbgAAAAE0AG4AAAABMwBuAAAAATIAbgAAAAExAHMAAAADTWFwAE0AAAABAHMAAAAGTmVzdGVkAD8BAHMAAAAEVHJ1ZQA/AQBzAAAACVNpbmdsZU1hcABNAAAAAQBzAAAAA0ZPTwBzAAAAA0JBUgBzAAAACVN0cmluZ1NldABTAAAAAwAAAANiYXIAAAADYmF6AAAAA2Zvbw==" + } +] \ No newline at end of file diff --git a/test/vectors/encrypted_item/ciphertext/static-aes-hmac-1.json b/test/vectors/encrypted_item/ciphertext/static-aes-hmac-1.json new file mode 100644 index 00000000..e35a7c1e --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/static-aes-hmac-1.json @@ -0,0 +1,309 @@ +{ + "TableName": [ + { + "*amzn-ddb-map-sig*": { + "B": "lBLoUXuc8TgsJJlItgBh6PJ1YVk52nvQE9aErEB8jK8=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cjd91WBBFWPnrJxIJ2p2hnXFVCemgYw0HqRWcnoQcq4=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "uXZKvYmUgZEOunUJctXpkvqhrgUoK1eLi8JpvlRozTI=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "yT2ehLcx/a609Ez6laLkTAqCtp0IYzzKV8Amv8jdQMw=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "YAai32/7MVrGjSzgcVxkFDqU+G9HcmuiNSWZHcnvfjg=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "0iwjbBLCdtSosmDTDYzKxu3Q5qda0Ok9q3VbIJczBV0=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "Gl1jMNLZl/B70Hz2B4K4K46kir+hE6AeX8azZfFi8GA=" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "66Vz0G8nOQzlvIpImXSkl+nmCpTYeRy8mAF4qgGgMw0=" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cSTe0npOBBtsxSN4F9mLF2WTyCN1+1owsVoGkYumiZQ=" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "SAl9L6mP5YRNF8II0NsFXI9boH3t3lIKiF79HRTI/S4=" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "iev8e8T8ah3qIYPZ1n1KIxfRSzYIQuSnQt3bCSyuDHMf0iWGuHCe+n78jHZfaYwp5I1gB/6hZxtvN9eX64C+8A==" + }, + "stringValue": { + "B": "4kfr8MUHJOhcnCX8KwlBWMXckr09wIg+o4DsYPZCdAL5HIQDaeVpd+RFmWdM3eDa" + }, + "stringSet": { + "B": "72pIpNYQv5fnqNV7hcxwtFM13JtmisBIRfW29VZVVgb7HQSV9ypTaDMwjqV0TyQOnEN/tDsHTfj0v4TvKYXYtw==" + }, + "rangeKey": { + "N": "7" + }, + "byteArrayValue": { + "B": "5ZYktI5VjhPx0mN97APhxdi8u6vzDB/8O4XIDHVeJ2A=" + }, + "intValue": { + "B": "rfALMD+0hs7L1YzVVqLOraA4IOWnaOOTad7r7VErGm8=" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "+rzZO2IBAmjcybCXzbPtI3sF+u8f9GzLMGJGEPXofAI=" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "NLdRexuTujebNfVSeiYZ5RD6IZcmE1UDcvJ4PbiLP3Dng+MjwXWUt2+Eolw0HDm1Gd2rfITxs4Oor0ImZGlJBw==" + }, + "stringValue": { + "S": "Hello world!" + }, + "stringSet": { + "B": "CZrDLG0nOoo9SDT0ib0zz7d0x5rN9UK8q7vhthuJxNJxo/3Qs+rjhYQYLI8DcLom35aTzsgyIIjyzFagyqtnBA==" + }, + "rangeKey": { + "N": "8" + }, + "byteArrayValue": { + "B": "VVFzWfSD4PO/bD9g8RQOgCpZ+KlRH5+vdN2i1Wn9bDA=" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "iWWvGpcrzkUu241+NNtykoiWoeaSR3QHQMhHTmf0XAU=" + }, + "hashKey": { + "N": "8" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "rangeKey": { + "N": "1E+1" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "hashKey": { + "N": "7" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "rangeKey": { + "N": "9" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + } + ], + "HashKeyOnly": [ + { + "*amzn-ddb-map-sig*": { + "B": "iZXCp3s7VEMYdf01YEWqMlXOBHv3+e8gKbECrPUW47I=" + }, + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "zh74eH/yJQFzkm5mq52iFAlSDpXAFe3ZP2nv7X/xY1w=" + }, + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "HR5P6kozMSqqs+rnDMaCiymH8++OwEVzx2Y13ZMp5P8=" + }, + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + } + ] +} diff --git a/test/vectors/encrypted_item/ciphertext/static-aes-hmac-2.json b/test/vectors/encrypted_item/ciphertext/static-aes-hmac-2.json new file mode 100644 index 00000000..c01e93b9 --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/static-aes-hmac-2.json @@ -0,0 +1,309 @@ +{ + "HashKeyOnly": [ + { + "*amzn-ddb-map-sig*": { + "B": "iZXCp3s7VEMYdf01YEWqMlXOBHv3+e8gKbECrPUW47I=" + }, + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "zh74eH/yJQFzkm5mq52iFAlSDpXAFe3ZP2nv7X/xY1w=" + }, + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "HR5P6kozMSqqs+rnDMaCiymH8++OwEVzx2Y13ZMp5P8=" + }, + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + } + ], + "TableName": [ + { + "*amzn-ddb-map-sig*": { + "B": "lBLoUXuc8TgsJJlItgBh6PJ1YVk52nvQE9aErEB8jK8=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cjd91WBBFWPnrJxIJ2p2hnXFVCemgYw0HqRWcnoQcq4=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "uXZKvYmUgZEOunUJctXpkvqhrgUoK1eLi8JpvlRozTI=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "yT2ehLcx/a609Ez6laLkTAqCtp0IYzzKV8Amv8jdQMw=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "YAai32/7MVrGjSzgcVxkFDqU+G9HcmuiNSWZHcnvfjg=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "0iwjbBLCdtSosmDTDYzKxu3Q5qda0Ok9q3VbIJczBV0=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "Gl1jMNLZl/B70Hz2B4K4K46kir+hE6AeX8azZfFi8GA=" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "OVOhmBYFqn8JmCr3U53n0gUHm9sOFlCzfslQTndM2d4=" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "OZ4q47AhicK3RzfypoCMvUy3qZNXNejFTSYygP00tL8ox0Zcr6xxxyAHNEf+L/gXv/D2/0fZ1ZXRkUx6I4Q/ag==" + }, + "stringValue": { + "B": "Wr8LK3dNif8LCWIEVTk4LsShW/T0/KZqxRFOADzHbI0ou1IFHF+Oy3BwqIP+/zK3" + }, + "stringSet": { + "B": "Wyqt6ciL7p3eIoT5dnONVBoFLK6nUxnIcC6NylJfdrUWh7/ckBnGMl7c4CCq1ifPD601xrh4+TO99kMSHSaLNw==" + }, + "rangeKey": { + "N": "7" + }, + "byteArrayValue": { + "B": "j47NBhEawqzHQb6prTGB6RvYyDuh+A4TIrTSwgZoxDA=" + }, + "intValue": { + "B": "rwlX4rD1gcqVpnTT4DfX79JPLAtOsw2CYssZ4VS7fnA=" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "66Vz0G8nOQzlvIpImXSkl+nmCpTYeRy8mAF4qgGgMw0=" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "xZvHJ377MCQ4hf1BZJGRgTF+l7YiaydAkILG+7CaQ8M=" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "optMhPYvzeL68/ubOiJu32JBETi7ss0o9nqCSDAN22RaR17CGXge0r6OgJlfWfVFBhUebM/uN42OJpyB3VvuwQ==" + }, + "stringValue": { + "S": "Hello world!" + }, + "stringSet": { + "B": "p3shcn0B9/lVCp9UjP2mRcARZQ8PQC4hR5L0fAsC154j+2kUPu6iRhazVKxkJ8Fr25jtc61X2M9Q32kPwyRmwg==" + }, + "rangeKey": { + "N": "8" + }, + "byteArrayValue": { + "B": "E1p3OH249idr68bawV56P5lo+nvBvJwbqVPTHMM40/c=" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cSTe0npOBBtsxSN4F9mLF2WTyCN1+1owsVoGkYumiZQ=" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "hashKey": { + "N": "7" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "rangeKey": { + "N": "9" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "iWWvGpcrzkUu241+NNtykoiWoeaSR3QHQMhHTmf0XAU=" + }, + "hashKey": { + "N": "8" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "rangeKey": { + "N": "1E+1" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + } + ] +} diff --git a/test/vectors/encrypted_item/ciphertext/static-aes-hmac-3.json b/test/vectors/encrypted_item/ciphertext/static-aes-hmac-3.json new file mode 100644 index 00000000..58c8835a --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/static-aes-hmac-3.json @@ -0,0 +1,427 @@ +{ + "TableName": [ + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "m1KU7lGZlO6bNSxx1ZMr6pVmY1PuYw8uDIcFDisFjSw=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "vTohJVr6lrUAhSuZT7nPaxgL6iW+IC0TZA1/ht30GWig2OO7JQFIS4O6Kk2ANI6w" + }, + "stringValue": { + "S": "Blargh!" + }, + "doubleValue": { + "N": "15" + }, + "rangeKey": { + "N": "15" + }, + "intValue": { + "N": "0" + }, + "version": { + "N": "1" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "m1KU7lGZlO6bNSxx1ZMr6pVmY1PuYw8uDIcFDisFjSw=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "vTohJVr6lrUAhSuZT7nPaxgL6iW+IC0TZA1/ht30GWig2OO7JQFIS4O6Kk2ANI6w" + }, + "stringValue": { + "S": "Blargh!" + }, + "doubleValue": { + "N": "15" + }, + "rangeKey": { + "N": "15" + }, + "intValue": { + "N": "0" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "lBLoUXuc8TgsJJlItgBh6PJ1YVk52nvQE9aErEB8jK8=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cjd91WBBFWPnrJxIJ2p2hnXFVCemgYw0HqRWcnoQcq4=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "uXZKvYmUgZEOunUJctXpkvqhrgUoK1eLi8JpvlRozTI=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "yT2ehLcx/a609Ez6laLkTAqCtp0IYzzKV8Amv8jdQMw=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "YAai32/7MVrGjSzgcVxkFDqU+G9HcmuiNSWZHcnvfjg=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "0iwjbBLCdtSosmDTDYzKxu3Q5qda0Ok9q3VbIJczBV0=" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "Gl1jMNLZl/B70Hz2B4K4K46kir+hE6AeX8azZfFi8GA=" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "doubleSet": { + "B": "bu/qi2UnCw6Saur96Xjc+1sQQzo6ZUdeu9W0/uX958B9utw+rDlclexaDcf6VGnz7OYM18eeEXrpjIgLtH4iaQ==" + }, + "*amzn-ddb-map-sig*": { + "B": "fqIf7vj1G3qbcEv1nbyTqNoKSAFfj9fLMb3S8YEFjfM=" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "j0vcD4vvFrL/7JHHgnyBCIHb5u/WvT/Uc/kk4lBMFKV2NXshqqEmBu8UK96OLbYAK3+vW4mwm4rIZ7MqgV95LQ==" + }, + "stringValue": { + "B": "yV633bs6t+yjSw6vHtUgrpDNB5YyMgXue0prPMXVm6SmGiUxS5l93cJx4vPWF/bi" + }, + "doubleValue": { + "B": "BhZIjFx+b3DExrUfnOkJjYNw0/Bw+KoDxG4LUyzQoRA=" + }, + "stringSet": { + "B": "WsWFF2IDOEl0f4PlW73arTFdMCyS6lMbvnrH9sPnCCMCQzEaSmdZmz1Kcb3ZDxRiaeLLWV2om/J9b260y2igRg==" + }, + "rangeKey": { + "N": "7" + }, + "byteArrayValue": { + "B": "LB7p2Ewobv+WsSeh1KxHx0Gkw0e1sKTbZBfjkvoEZBs=" + }, + "intValue": { + "B": "O8EY3/1vRX0odEkXQejXrUP24ToyD+4EHJ6TmKZVPkk=" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "66Vz0G8nOQzlvIpImXSkl+nmCpTYeRy8mAF4qgGgMw0=" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "QWOgS/Ba8ZZa9Y2l8DolewfyZosDKcLysahlumr0MVk=" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "AtJqJj1aokidlC6qr8L3xZQNo7Yl2z8DsEXgJLRKnK73Oyg7jRDF0zjgp02qNae7mYNDkK2QeafeAexk8s7qdw==" + }, + "stringValue": { + "S": "Hello world!" + }, + "doubleValue": { + "N": "15" + }, + "stringSet": { + "B": "nJNYeqOA5x3J3k3zO7CWUcbD1gU2xifPxQ4sraRhsnKyd+mE+ouhX2LpMwQ45nRXxV1nSeaN7MW+4vYn/sA/oQ==" + }, + "rangeKey": { + "N": "8" + }, + "byteArrayValue": { + "B": "/icc0cvbG45rqCNdeMFJaklPx69nXo3/8XTE+vQafmI=" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cSTe0npOBBtsxSN4F9mLF2WTyCN1+1owsVoGkYumiZQ=" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "hashKey": { + "N": "7" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "doubleValue": { + "N": "15" + }, + "rangeKey": { + "N": "9" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "5NHNzCBtZcVAUlz1ymLB7Ta+1n3VjffLj5WniFA9afo=" + }, + "hashKey": { + "N": "8" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "doubleValue": { + "N": "15" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "rangeKey": { + "N": "1E+1" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + } + ], + "HashKeyOnly": [ + { + "*amzn-ddb-map-sig*": { + "B": "iZXCp3s7VEMYdf01YEWqMlXOBHv3+e8gKbECrPUW47I=" + }, + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "zh74eH/yJQFzkm5mq52iFAlSDpXAFe3ZP2nv7X/xY1w=" + }, + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "HR5P6kozMSqqs+rnDMaCiymH8++OwEVzx2Y13ZMp5P8=" + }, + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + } + } + ] +} diff --git a/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-1.json b/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-1.json new file mode 100644 index 00000000..04f57d66 --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-1.json @@ -0,0 +1,309 @@ +{ + "TableName": [ + { + "*amzn-ddb-map-sig*": { + "B": "VRRX8l/eqIeMo7TvQbHI+0Zfh6tbwT5rFJ2zTLYoloudkb8WcBjcHuHEGUhFia6lSKOXwU1cEi/dT4YbQUXf2vzVTxS7jDstYHwHxscVPYNKp7FKzrG/Rym2lF1D78cTn46Zu2/XPw/JgTUhL0Ar7nmmDjUONzzd41QZGr45PFtgBZzGSHyyIpWU2+TRA87quKL71YnrzfbfWoIutJLQ8lAuGlx/gm++09c8PCL60CwUGl6moaVzSYpu/zR+1lxFZ67sWnNrxlsezsQcWUbPJKgeaHfeKDxSevaALTS9dCAjSlE0Sv7XbsdjxW2huNPcPTQCOcqUtetDJ1W2GLa1mg==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYkRuNjVvVFdnenJYQmV5bGVwb2x4bm93TTBxa1AzYTRHdjFBS0pHUkhOK2hSMW8wL3ZBM1pEVUowRTczWk1HTGZ0UUtQSjQrbkRqd3kNCkQyTmkwekNlTjVaMnJIOW54cU1TeGI4VUlDNGNWMjRJYVhCa3hUU0IrRUN5b3VYVzBINnhBbFlGdzhZNTZvTEhYYjNqYkdWRFZyUmoNCjBoby9ZZ3FDTjRVZmhFUmxKN2hhTnZMTmVWNXBMa0FLRWlPa2cyMWZEZFBXUWZaYllMMGhYL0RtaVpLWVlSU0N1aS9KcWFudFhkREUNCmppMkZqUVlCRnp6cmxkTkFYckRKcnBFSk9STkRhRDJ3UXpJYVpFY2ZBY1RlR3oyUzM5a1Q2azN2MlBIWndaRFdKUVdKMWcxc2pvdWcNClNVUnZHVkZ3UThpUW5XdmNPRDBRQkF0aFhqZVhEOEd6eGZxSFZnPT0NCg==" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MG6vTV+uPAaPmZIGR4I4DbUwIUmivEZQ5sqpK83hue0SArv2a9TtlOTIighJa3b+u/LR/0kxm2Jbx5nqrI7oT0eKSjqJYk1S3w2W/JDPzyk4wwwSoOKH4TLq0KxwXE7QEM4aS5hs92ja6jKPIj7nEJKYOOwHdCdu3Qu2SBmY0VWyj+pUohZv5fzDD81nMeCWU7KmtFsXfKAFFHM2ufCWywXRBXKfYTDPYR87+bfNvbw5W/FmDeu9pdpCIbV66yR3pl4d9+FLoDqbS5yQjKzDI+X5Z90FBaW1xaPCKLcp2l9tRq8q8hfvyXZXrJVisu+/igjqpZ3Tszj9XBmmqLFo/A==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYmovWUFVaTV5N0tRUWQrVFl3a296ZURERXFuY2RpTWFWc3dKYmI2dERuM0VTeHVaRXpZempOMlN3dEJxSENia1NrZmpGMGZBMVNvdUQNCi9Yc21iRTJGaXlOSWRTZE5uSFQzak5xN0FHOTMrVG9rUCtrREx3Y1Y5VFJYMUNwK1FIbk5TVzN1ZmxqbGlKTDFPQTZCS294bHNmNEoNCng0elNvck8yUHhac1ZDY2g2LzFSNU8zNHJqcU1QZUxlTHBrYmpUcS9xK1RkNDNxODJoeUVQejFTdHo2NU9xUVkrellTcjlZOHp5WTANCmkxNWlwcGVyRzQ3aUIxc002aXV3YWhxNFBVVEdkdTZiQkRiQmRxb04wTFpwUldVMTdueDRHNDRhTUJlZFNHODk0NXpiVkUrRE1hNEINCitORUpYeDBzdkxmZXM1aEJIMUNBQzFyZml2ckFCaDRXQndLYTZ3PT0NCg==" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "ed4gAI82hqUpvoUH/glIJXIbasq7CDMbcfm2u/fojO+3FsujnsCRCcIJZIe6ny3ExNC/o272WzUL+Tw1tFnM0VYcS1aAgpdJiTyX4LFPp4uJRlutcxDWCOBpAVh+Ma/oIQDAgxlm1EOcKiWyxhyXm3Bjm8c//rV/YyMkm7NpqK99zCfbgnwI/ezGvEaJe5L3N4eLZBAV9BG7B6if9uvSvCWh3NABr9XNeaXLCHC300ENCk8iUNJJASi1sGQnlTR186Ix8s4DPCfZJbNwWlHrbupgmBq+AZRffbU059QrLfvzdxpaRtHIlDxQwmvk8C7EU2kUuLGyEA8XSdiT5y2fRw==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYlEzNXRBaXlVYWc2VjZpTGFPS3kxS2pJZ1AyekdydEI3QVpvS1UxNmVpbHF3WnN0emJIK3hwYkRYZTQxRVlIRjIwL1VrdVAxeWdwVG0NCmJTOEM2VjVVMHl3dnRaOE5CSmV3ZGFIeXNMemVZTEhiMmNaL3VrTWg4dUFXRjhIRjJ1NGF5eXVKZkZtcldIbGRMTGVRdENUUVA4eEUNCnFpMEtrcU5lcURGTitEeDVNeFpxaVhUd0MycmptT2N5MVBiOTBCQWJLRFNLVERsLzRNRzhWcVBCUjFhMENFalExL3dwOWRSdS9FRUoNCmN1eUhQdjFHVmp6YmFNUWRpem0yWmhOTnZvRWpCUlJkRFgyRGVDU1hNcjFIVXFnbVRuSVoza3l6SWtIVG5NNjl4MGErSDltNGZadEgNCnM1ckxmQjd2R3ZlQjgrQTRLdURTVDBueksrWHA3Q2o5YzIrZ1hnPT0NCg==" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MaO/4MFm20VFjw2ER/jpwi6iR2VBYKp+uwdJH+/CZv1NlwMDp+9t7MHu9DArLIzQlHjUQ905a8FV9LeNHcDD29CNDXz3u0I6u7Rznhoa78N6fO08aDdHn+MtLzoZaKi7dpJ1M2xNzAM/3x2dTkLiCGKuAOnpmk4SSG2vKu1OssM4e9VTwWgdWgUBHyMef38fEoT55XRy67phr4e77kVesV+X/lM+JudGuzxZgbrFsFVgy98DQ2SJF4gpNKkNOeWKFIomT8bEukxECfi0Vyk/m7PSMKgvF5JBBNQYEt7HXRUo1lVmUc7WvBHYU4dVkz2oQZn06F//IAZo+qsmqOM12Q==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYlZ5OTRVdHkweHVxUlFtNWl5S3ZkM2NNZHhPalBWSlBsWWZHdDVJaStseFNwb2pnKy9XRjQyNmkyWmVZb1Y4a1BBN284cnRXMlk2UHUNCnRJZzFtc2gzdDROYzZGTnJPOFNkR2JxZFE4TVdmYWVHSWdiaElmWmM3NVFQaE9UWWdSNytEOG4rbnpwZ2FqUDNiTWMyYkRDK1lac1INCnFmbnhTVDhYa2tGRGZqWFVlZGlld2VrcXloU2NUc0QrTkJZSUxEWU1vWCtoNUZZYTgyQWRMSWZNTHlmS3Y0Y2FIeTE4YklzcFM2TVMNCmJjcFJkcnBUNGI0M2c1Q295UTdLZTNwZHhQT3IyNEpKbHlEeWhDeFpLY1FUNi9GWjNMeDl3blBONW9IRXZaMEVqc0R5VUREeEdwOTUNCkF1TmVCbmN5UTRUTEhQTW9RSE9kSklnS3VvTUsvZ0xwUzQvamJ3PT0NCg==" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "fq5jMK7LBRwa63vh+Unxjxxuj8ugx/l0jqRalmWNql+k/RTz3lxsNCTFh1svGTP4QZTLL/GghdZGmGH2Pb82M45ExGsvZoVzkdQ6Gc/y8NNCMkD98pZyYeWchDazrqC1EnB+IoYbuG5vQF5vCwR2jEfd42bu+YnPMy3ackMEF9fDamQdHsAwfDDFsshmePA0Q4RMOaBUu48YhrDhSYPXH2DAv8lwPqh4lWGOrtalV5MFCvVzFO5ss47XDeI5zjafkwoJQPU5b44cvvLXeq56p0cWn9uFt2XMZ3HBHxDOOOAUkqNKShlaQ3m39SdU58fN50MLrc3G3mUjbttFBBE5AA==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYkl0Qkp1TnJxeFdTVWk0OGVWR2RFUS9RdVpzL0JSWnlpTVYreVBkaFdIemNpZk9QbVROWndFckdybGhuS1ovZ1BWT2NzSlRGWHMyelcNCmJ1Rm12cVdZdlVYN1lnaDdacjJSQm0zbGRHQ25WRTFjZEluak4yU042clh3S1dzbzF2Z0RpUmJRcUF5TVpMS2dwbTZZTnlFZnhjSzYNCkVpZGNpKzhWRVlHdkYzYm5mVGtoMWVmK0RXbHYxTWxYSnYwcGRSaXFmU3dnZThjNmxGTUs2M2t5Y2JFWTFpNXV1NWZubEIydURJTTgNCnRrNUhDV2gzQlVoZ1B2Z01zQWM4SHN3QVNyazcxWFZlQzNXWVR1dTlTQVVOUS9Va25IdzNvcitvT09POW5YTXVEWFFWbjF3WGhpUUQNCkFxNWgzN0hULy8vYmZDUDlWQ3cxQXRwY0ZKRXNHNzViNjg3N0JRPT0NCg==" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "raKHapJyc7wtw9Qzbr4c4AbRlLAT8p0rkrN+gm3JFSJwFLHtf6dHBQv9tveVRNo4VMeV+PJDbWDcPDEivK4Vq5N9BAlveRSx+d9Mj/ueK323VUIGynQwdI2PO0J4pncTvFIH/VMauMcCItOlmaOV/pKogUIYLqEGdgqPd5M6TuL0Gxki9i9lzZOg10yJZjTIg33I4L1C04xQVZ7c9gcyQB715y0TwF+0oXs1EG2KtUdF2oS2yqCb67v226gdj5aoFNUzfijy7v3s3cRMVA0fQKwpda+d9Rj5NzkvwBo43oKFFh58tl6FbRa3nN9Jj9cxWGtTSIlVd9RQ+vttzObdIg==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYmdvZVVuaFU5YTBoR3hiWVl4MXZnckE0MFcvYmRLTjEraHBvUi9wazZsOVpXM05kNlV6cmhlR3VWN3NCSG1sSUVqcXVCemIyd3FnTncNCnFCRy9hZ3FvZ2lsdGQ3ZUZpNmdVdXQ4N3MvMm5kMjZFL1JiRERPd1QvUUxCYzRGZWtxSlEzTmZzaitPL25VeDVWeVJKTU82ZStHOVENCjRYWWRXcG5OWmlmMTRVTWMzZlR3YlBmY3BzYWFUQm1VTzJ6WS84d0lhbzlNb1hPYXd3ZVlydWZJSXdZZEp5bWhTTnIyZFBqTUVVWUQNCnJjOW1oOXZZQklTRWIyZnNOTFBObWFRL2FKK25NK0VBVGN2SDJVWTBCMjU3a2dVOENqOEhhbG82VGswVEZ3ckIxVXhoVkRvZ0dUZHUNCmdiTHpFMHFjbDZNbHkwNS9wT1lLeU1rcGV4MzM5M1Rzc2xRR0pnPT0NCg==" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MlADNyM2Rd+jSXzd/NgK53qnNIWrjOswmITkLKy6wmuP7tyYZZfdz/yN9rv/AeaDF0SKxQiTkIuWxtibyATiEFLc2DdulIx8Kl2ZydWSgvEI8ZCrKDNjhX8auceL2XZwqUQEWgNIoSRj+TpXZNwxygg0ZyT9d+PP8RT3yM64/9A2nW9WHMWK/ASwGJVHo1dlDzdspvcUCEtkO7U4ey9q25HX7YDx5p+yMxUH360fDuDYnXIdMyOSwPFO6LkcBpkxWSHsgB1jSZ9bVVceXi+mM3sUL+aLkUd/sP9Yl5/mOKASpJezNKcetAdSaC7VSKJ1PMbcEDSmK6XqblnNGF1L/Q==" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYnBSTWdtTmwvdjl0QUtOUXdhVjNCTVRaU2pVSjIxd3d4UHVWcU9wLzBtRzl1c2ZXeHU1WEJMQ1hqOFNtM2RaalBEbzJwdldYeEFhNVANCkxBZnBxZG1sVUlOVFJzVG9PaWdrTWFVaEx3bEwyTEZtTVpDUXJPZElPdWV1aFhqN1NvblUvaXRvRlZTYTBxU2hNZjVsbld3OHVGWlkNCnNmUjhXM3B2NDNuRjc3ZHF4SU1KWnkwR1kvdENjeFNtcUZBSkl3YW5PQVVZVXVPQWo4Qmpld3Y4U2lkTVFTWkExY3krV0NkdHkrVFQNClg1cUJubDFvNHI1c1NhSENjNHU0OFBkcGpmczF3ZDY5SVRQMnFCZVZBM3hxVFMyejN4bkJ2Z0VEZWtlWEJlNzE1Q0JvM1ZsM0RVSW0NCndkbVppejdKTEVUYVQvVXpmVGZ1NlVGYUZzR2Q2ODN4dzgvS0hBPT0NCg==" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "m2vH2mT8PqjrYI2ljgT6HlMeXhNylQvKZQmz8JkoLr50ks+SZ7WAQ/u+l6OMowXQ9pIWadzSsDiwX47UJTcE2gibTVfbBj8XTnvxOerQeYKm1wJ6rSpFDCt1I75xmbxr3GVbD+eCFS/kPPLR8U0uOVW1RY3vhg2qlrOFVYeOEEWQK1Ds7UF12EQm51ClL+UwH1RoPo/SCABqkiU998a4hvWV57TIefrOtrQBs//ZOGm2BswAtnVjmOd9OZmmnwyVQC6/i50YJOcaON1qiW5+Yl9o8gfE7kRXJ+iCuoOzT4iR2i7Z0xOsMuKme1M1ZNBirBNSpHKvJnTpRJ7K2fzXQA==" + }, + "hashKey": { + "N": "5" + }, + "intSet": { + "B": "w1+zjeZeeNkjUAgxg6HrlPi++MwbD8DSvM0jSvQQ+lQyzmVEB5IT5/6CoEtPVMasQUVTE/iODio0Yjkdek6vcA==" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYnJ5ZGJGRzVFbWkxVURXS1htTEdQZ05zaXh0SURPYXd4Ty9yVXlsWVFTTFduYjB1NTlyM0xWdzVlcmx1Rk5YdC9mS1RYQWdOVFhNeDANClhiSm9Oa3BvcFRDWFlQRnFPSkVINWwxNnBDWXpyU1N3ZXJWdWlRMk5ZcHJ0KytwNlRZOWFKTzZvOVhEUW4wVzNEQmgrSWVUMDEzcDYNCmVCSnhyTjNHRzM0ejc3RHhUZnJRWENpVFp1c2E4WFp5akptVkh2eTdNMmxUdzlMUG00SjBNZ1lHNjBKMUFvd2N3ajdnSGpmaXRuNnkNCnhjL3RqUUg4VVBjcnBvME9HSFpjU0htZGhadlNtSzJnRlh4WkFuVFpLZ2hUL2VxRHFCODhISWJJbzl1QTBzQWdMN0tscjY0c3RvbEUNCmc3TkVhNzdqMGFpd0RKVmhzMk5ZTkdWcjg4UWgzVVNrZmk4akpnPT0NCg==" + }, + "stringValue": { + "B": "Wm3eBDw275auRay6J0l2m3KEn9sDTPC75ESTTKaE0mJXasDHiYEWWMt2ubWIMrYv" + }, + "rangeKey": { + "N": "7" + }, + "stringSet": { + "B": "U6yqvtM7vMRXHD9uugPaJp68Ro9jNhoUyKwoItVSvkZDPNypGFXX1L42AuBQbeq4km4kXBwPbnLRfqoPVUG/tw==" + }, + "byteArrayValue": { + "B": "5d/ldqZexHMg6B/GfymHSqxoSFNG3hJKVsof8P6fIgg=" + }, + "intValue": { + "B": "4YeimDgZV76L9rTz+0Me0rXVvSvlPt3W0+1ah1roEqE=" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "jKVKU8uHbhAg8vlU8WqK3qIss6XKPJQXATVwFlkqw5N7RMj0yjQWQ5pJC81sdkXp3NmIgF9Wnavzl5TEVB6R4v/cwxT85ih/kMN7NDOXU5OEkQUlzCRCZ3U6wVvWgFbbI68r42LNPav+uuWBB2/cp9Uu/4VbsOQC7IjEdWIPkir+5BP7HBFg78cs9YgpkDuw2J8+4KLj4z5CsSW6dPjhmbPolKmhn8DinezJ6bHpRFmP0ry75HxMUTu2wInwHD0mCpK1TXWJ3t8V1+UJkNHHpD6j78UhNH9Ky2h9pgj+7Gml0pnZ9t0skUCXNcBLf0Pj3RsqvQuYrU6f2tV8DDxm8g==" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYm5TZFVpMlQzWjRNRjJQUjJVUHRweWZIK25CZU1wUEdKbzg0TUJneFVWek4rOXFvNStvaGRXSEp2NEsrTDRwRUJJVGVZTEllV3lGVmsNCkdEZ3dNTk5ZanNmYjgxVy9iekdpM3pzclFjUGFRTi9JZ2RaWTFEdFFKbkFvRUZSbmJ0SGkxaG5pSkdiUlljc3BOanY0NXpaOWd3K2cNCitMN2dnM1Q0WEY1dUg1QUsxY1lLUkVWZUJKb2RFdU43ZGVGZkRWZ3A1NjR6cDA0QXVaTldqcENmYmNOb0dsenJaSGtIZkhtTTVtQ0cNCmFPaUpzc2crZmM0Nm5tLy9BdUc1eUs2MnV5cyt1QWJ1d2RYZlNhUDZSbjY3ZXkrL1BBUVA3SUpYRWVKYndEb1U3djVTcngrWWEyaDYNCmtYbUZFLzhMbVMzM3Vad0lqeW1nNDRWTjE5cERYZUd4NWRLU3Z3PT0NCg==" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "hALCfRcpQ0FpzL1HJQQk5WbSRKN7pD9C1vkkzVlA74pYCsBmHm9GAKTrO7Thyw58BpZtFX8wiRO1aTkPpL5L1oh9rQ9+TlvMv7+MbBB/WwMnx57FbV9I4Cu5mMFiDXpTt7k8I+QEFRJwMdzs5HSv0bjz3FyOBbmXFqkMQaak61nz2KoM3kwUd38jb7sU+calk2Chlh5Rh6Y2JFgJ3L38h6DPrbsB6Hxqx1q8+vod80XWw2IyYmqZ2EKQte6Ot21AcDv3ECm2+XQZsFHAQTHJUSlFAilkhGv1FEtt0NwEUCsgR7Z6YE4gi0JCQ0bdQuVY7XrlY0A6ywT2+wkHBtYI/Q==" + }, + "hashKey": { + "N": "6" + }, + "intSet": { + "B": "mMIiJONTWUfeTaBy+FgY9TkJadGmsLe4X3qaJ8H1pnebXSz+GiZKhz9P9UfTgkpmYdxEyIqK9Pyq3zMtPDoOXw==" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYk8ycmIwcEZMWlV5cUQyT1JDM0xYMkdDZWxjNWtiTjJPaU1wYWxTSVNzVUhnV2ZuVjJ0ZmVoUlo3VGZPZEhrYXpDUVBzNDlQWDN6b1MNCldlV25NS0NjQ1YzS0g3NFN4V3VQTkVhZlVBRVl2RTMvRUtBdmhWc0t2K01tUWdYd2pJckJNUGN0THBKcUtOVU9hUWxkR3EzVnZ0eXYNCkFqam5CUktzTU13RVFJY2VMbUxWbTRkSjJhSTd3STBDL0pZZmtJU090MWdqY2gycTI2c3hiYzYzYXhwOEtnUWg3dms5WTQzQVpMeTkNCnhVL3NJNHVkck1DN2hOLzc0WEZkemdWbU1vbHk4S3lUUklCK2JTN2ZwV1F4RE9ESk0xK1RhNEZPd1Z2VHQrTVUrdUtCQnJ4aVkrT2QNCnkxYVVRSlVXakVHM3VSNW1raFdtVHVtemNYZm8yakNUNlJIYWdBPT0NCg==" + }, + "stringValue": { + "S": "Hello world!" + }, + "rangeKey": { + "N": "8" + }, + "stringSet": { + "B": "AbnuuD5NoWcpbMV05yWaeXoq/UDb6VcAxqC6JMaFFktVEYp3BWjmqtyFRrt6Gc0t03nzLvPOWs6Uj6k33J87bQ==" + }, + "byteArrayValue": { + "B": "VkfWaVfJ9aQxJWHKPpPFyrAvQ3Eogu4H04hNNcG+bno=" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "UKESqnTKdCqAtM6aDkJGg068ssNWFv811njBVuRK7mzVtmIG5OxLQKr8ycBf/Zm3j2fDnkeLnZwc/Fya9XCTygte4yy1QZSywrSb83uhGFlmLsjGOKcE5ZTMPEMb75+I+8I8OQ3ggfM3EnyaTFQCIfeY+3antQ3augrWioBaoJ3VpoUU+RSA6FOrlVtd01qNO2ZOXCfcX5soh2r60FXZ3fdJZJKvO61xkf4nlZJQkc175bsV8KRHh+125a/KETb+3Gc8uL2aRFBO03fuSCHS97YN7nbevtzM/WdqfXh83N0sBIibHhY73xd5n1sDwKhn9D3madRlzlj6GgwiY6wOqQ==" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYkRjTHUzdlByS0xiN0g4eVZOTkJQMFZ3dXpGWU1ONkpDb2hCT3NscUpYN09wSGdORFgwNUhEZlRMNGFBRGFmamVvMUMrQ2pxZ0JLZVgNCkU1UTE0aTYxN0JyMDQwOHczRlNhdnRObkI4eTZEVXY3cmhSSElBMThGbTJqWVhvMkFZRUlXbzZFRkR6TGh4RUtIV0pmVnppYzZXSzcNCkc2L24xUFUwc2pEUFRtNC9DdE1STWRnbkk1SnA3c1BBSVFOZ0M1M3JPY0FaS0p0ZEU1UmJ1TllCTzJZaTE4eGVPMUNNUVVnOFlWWmkNCllDM25NQlVjZitSTGk0NE1IZHUzMnNCMHFtMGZoZnZ3WjlMaE8xL1VaWlRmK1ptejRFT3h0cDMyaTI5K09LQmxnUlFmNzZRaExGaEYNClVlZVhsRjdZRytKSG9iUTV4dlBzbmxXQUY0T3FaOXJwWGVIQUxnPT0NCg==" + }, + "rangeKey": { + "N": "3" + } + }, + { + "hashKey": { + "N": "7" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "rangeKey": { + "N": "9" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "fuTnJmdj2YTv+7PSKT/hVA/HaYJZCuXquvdCFafntAtjNqcQI371menVgbKHLKYZsqaTrCEHskNESd8qzXjJup2uOYdJOl01OKc0qasI+a0XwQspILlhIBo+TJ91/XyUTbqvRExXv+yJ3S1AY7vQmqIIumzy6kcVk0IR0pJjyqCKLbWumJdR+NgITuaowVMGi4BrjE8W3/ucJnB1yh1MZ5kQlINCgW+80MdEmCtxkZ9Lq40CMlhtXoXXXKNtZ5vh/TK0IDEoDMBQKMv3/MoSDxmRjkwCVRjJaD4ofpbmOaubInuVhMMQ2gPkQ8oxNRaNxoqhMv44rgry/sThLLOt7A==" + }, + "hashKey": { + "N": "8" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYlZnejZZeFFlNEFBQTNEVzRZZUN5K1Z1OWJPWGs2NzNYbVREV1VRcnZZb00xRTUwTm1KRkNQNlhrWktnMDVNRmFKODRicVBkOHZwelQNCnlxS2pKOWdOQnJ6Sk02MVhvZitPeGErRm02MkIrSzRlVHlOa0U2aFV1Z2U2Q2pqa1pkaW54bHBIZlVBbVJXTXhxcW9JanluR3FWTUsNCm5qS0RYRWkzWHJFSDY3VkhIQnZrcFFVbGg5QVJLNlg1bXFFTHFRb2twamw0b1Z4d1RqakZ6L0c1b2VVSnFkR0R6eXdTSW12NjNESGkNCmhZeWRkSlEvQ1d4VUsxVG5UcVByWnVKMkNneVhQb1pkcEVrMHV5TllGb3dMWm9iNi9kb2VTWjJBdmdHaXBhQS9yZGFlbXZxdVdBbDMNClAxOHRZQ2FOaVhieHM3enRRMExSanp1U3R3Ung0OXRuL09vQUlnPT0NCg==" + }, + "stringValue": { + "S": "Hello world!" + }, + "rangeKey": { + "N": "1E+1" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + } + ], + "HashKeyOnly": [ + { + "*amzn-ddb-map-sig*": { + "B": "SNpX+4QUwYC+yMsNiQQcYTXiYWWqnkR02KLn1VRH0YLx1wEuFJiOhhqD4a4AhiorExenoP2HHkZdZMJpGGGU9NbupQIr2SeKvV/dkEXrCADvVaaB5O6xIhsN638f9ibknZLEhUt+XAgGDzhPedKwPBr4ZC0UnQCasedHqb9CGXYMCB8P8URbllcJRayM5mf/bv4vfBW7t9uUTd2p6wsiDNG542pw9unP5+/74mZewfgbbp6bp+8KECVLjwTny24LHdSS7XGRb1uJcZsapnhDDamjctjc1jsaaWk2WWUf2YSp/mGNWgk9+m/St/cRwwVr9wjcGpcMld7QDHEEJQmNxg==" + }, + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYnJQOXA1YnN4eitlbW41dHdWY2dCSHZ0MHJBVW9xa2tzaktTeHJYdTZZTElsSkR3dWJ0MzdrbG90M2IxZGJvRU9mZVFBQkRtcHJFb28NCnVnbTk5Q2paQTBJN0ZRNzJIMElzTEkySUZwV3JzZzRHUTJOM0x4S084Zk5TYW41SGRIWUkwUVlIMjcvMExaQWprbGltckpVeGh2ZUoNCjJYYlFidzhlNWRWTmdLdG1sNkxsTEVWOGlXSG13Z0gwQmtoYzJBUFpIMzlLMHUzTzNBNjd2Yk9pTGlsUWxRaVBJRGhGdlRsWlhtSlgNCmtYYjUyU2R1UXhIMjJubWhFOUc0U0pRMG5BYVhOWkh6S2NZZFJSalBNZmV0VzExZlR2LzhucHRKeGs5YXpQOGl6YnhZR1R5cEh2K2gNCjd6T1NZc2hDUzdWZ0l0MGtsUmJtcDFFV0RtdW5vT2xNZStZaXRRPT0NCg==" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "V0k/mB2e8DSl4lIriyqbYBQbWZDKbiwcfc4ZQB2R3PA+S+hnjiYgwr4zgOXKNk2Dq72M1aIEXzbrej8jVoCSTSiC8pBXxekTqSnUsIYy7ilo8uvoSAN4a8zyfLXxvFPn+ZMwTs48uz7fVe+4MTTIkdd9+sJDTx/ZPEf88mAg3yiQ27cnnqG1N909cvljgjO1ADCcNqfvIMAys3xW5ML4GzdF/G/c/MlRRBMy1rq8HcRC0E09L9BAChfSV3OAwYyns90X5QuTcmpgr5PnY4NFm5WBWYhLwA/nyZDb+Y8e/XAd45i5gLpEpBBxFUiU3X949byFTr/naYFoatBoiWuyKw==" + }, + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYkhQZ3dZSWZzcjlhaVJ6bUIwdVg3WWxkbklUeVRkU3lHTnJIdHVqbytmd1RVUjJabFY3RmxpbjdPS0k3SzNaNE55bFM3RWY1cXk2ZHgNCjRhNkpOT0NCbzIwTTBjbnhEbWFsSG5iZ2p1aUs2ZUhvUzhqbFVvMFl3RDhFcnM1NFFaWExFZVNCQWxucDNUYk55dTlRNkNkTExoRXcNCmtxNkJ0akZNeGtmRk9SaUk3TmdwL21mVmVMSHhqajNleXFCbkpJSmM5RXZVSTVVWlZRK0wvRnAwa1pKdDFuMHdVMFU1UVNqVUVBeGgNCnNQUm5PWHpiZXhGSWdHc05jVE9wdy8vbGoySkZoWGJyWXowNFVqQkpLUEJibTErbllicmZPWi9yU0w2bDRCR3VUTUkwU0pta3ZxbmUNCjkyRmhpUlhJNjJZN0xIZWRzbFAwVnBQMWxXSEhaM1dkbTlVR2hRPT0NCg==" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "QZii10yqicfBPRRi31KpeTnpe5Dp1oSJAqB7L3uyTWUXz+sXeTqsEFqaIebiTtTCixgK3ZCs9mlM4X1V2iEgFWYuCs8mNoO8oY30vXw17E9EpW79kMn8Tuqr6XQqt+lMorFxKjiYcIkhVbNF6greXbSZ1HQdUGIPLQkACQfzX5I6YWjOCcGm60hXb2dp2uZy9kFceKCTIb0OtryI+7bVXX5YH4Ks9IOKNULWNGbjXEr3J2QdkeLcWZgZQVHtaikXiOlaz+WWyU4h9LaL5DxrojDCu68GXDmOzHYUvHbGCfk3y3hhfkwt9vwucEnA+Y3uDGH3vxUerA8iQ6qUH3m8wg==" + }, + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABYmI1L0Y2NE5MRlk5eTdVZ05EeUxiYTEwTnpBck1hV2dIZ0hjNndGZncvK0RESVppRUUyRTZSY3NvYnM0U3F3QVROc0JlSHptZjQ4UWQNCjBIWXdyYlVsc3ZOWnl6WHZOMkNqdFVqa1dDdHJOTTJQRlRuVWZHUldPaFU5WDdaR1BPM3FHdzZ1cTVxc1d5eGU5SVhRZTUvbkUvNlUNCnZ1eHlZUG8yR1dwM3BxOS9GSEpIMG1oTCtIL3ROZzBIazRtQi9MV1BxZGphc0F3Zk5ldzQ1Wjh2T3V1aDFGc3hZaExFMmR6Y2VpcVUNCnI0M0dMZGNPVTlsWnpxajF0eW1HTjBubUY0cTBYeVBnVU5pdkJ6anlla1hRTEJBc1RYY0lqNVRrc1lKNHRPR0ZtN3pHdVlVanUrZ04NCm90SGRROThuaGlLMzhnbHFkdDRoOW4zc2FxdGVhMDQwcFA1SVFnPT0NCg==" + } + } + ] +} diff --git a/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-2.json b/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-2.json new file mode 100644 index 00000000..212e7d5f --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-2.json @@ -0,0 +1,427 @@ +{ + "TableName": [ + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "ABNOeG5nexpOr+MWQa4B48/NZFBV/UTkeSCMbe5j8oM=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "U2/wlRLHVZxqV4/1FiC8CWSdn7f+wZco9kdFttMyLrhkYBeS7d0dROTlsFK8BY9J" + }, + "stringValue": { + "S": "Blargh!" + }, + "doubleValue": { + "N": "15" + }, + "rangeKey": { + "N": "15" + }, + "intValue": { + "N": "0" + }, + "version": { + "N": "1" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "ABNOeG5nexpOr+MWQa4B48/NZFBV/UTkeSCMbe5j8oM=" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABVhbXpuLWRkYi1tYXAtc3ltLW1vZGUAAAARL0NCQy9QS0NTNVBhZGRpbmc=" + }, + "intSet": { + "B": "U2/wlRLHVZxqV4/1FiC8CWSdn7f+wZco9kdFttMyLrhkYBeS7d0dROTlsFK8BY9J" + }, + "stringValue": { + "S": "Blargh!" + }, + "doubleValue": { + "N": "15" + }, + "rangeKey": { + "N": "15" + }, + "intValue": { + "N": "0" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "VRRX8l/eqIeMo7TvQbHI+0Zfh6tbwT5rFJ2zTLYoloudkb8WcBjcHuHEGUhFia6lSKOXwU1cEi/dT4YbQUXf2vzVTxS7jDstYHwHxscVPYNKp7FKzrG/Rym2lF1D78cTn46Zu2/XPw/JgTUhL0Ar7nmmDjUONzzd41QZGr45PFtgBZzGSHyyIpWU2+TRA87quKL71YnrzfbfWoIutJLQ8lAuGlx/gm++09c8PCL60CwUGl6moaVzSYpu/zR+1lxFZ67sWnNrxlsezsQcWUbPJKgeaHfeKDxSevaALTS9dCAjSlE0Sv7XbsdjxW2huNPcPTQCOcqUtetDJ1W2GLa1mg==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWE5ZbFNGc3FDcDg3Q09ia0FhcnllaUhCNkxEVGROMzZ2NUJBNnc4MEFnOTc0Um9IRkNibE1qcDJLZm80MW1NWjBhUFpXTllRL0RaWVNQRUtnelBsN3FvTktKS1ZweXIweU0rQXZpOXVoNUo2RGpxZTlka2pJMDE2WlFMbkFuQkdGK2ZRdm4wNUV6MWQ5TU82Q2hoQkcyckVxVUJvUWY5RUtjRG84VkJHU1FKaE1RVDROVGZKRmFHN254Z2p4Zjd6VnE0K3QzejNMMDlISHhjQ1A4VUZiUGlFUUpFNWVIOENJeFk0emljVHpIZ05mcTl0OFFXcjNEY3ptck9RSGVDQmNwcThVR3d1ejJ6WTFJd1g0ZkJrdlltTHA5ZnVPSEF2OVEzUXF5dWxpVXNpNlJUREFCMy9GeEtFbXpmQmZQMmlGVU9ycFdKTGRUNWJ1akxpa21mMjlWUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MG6vTV+uPAaPmZIGR4I4DbUwIUmivEZQ5sqpK83hue0SArv2a9TtlOTIighJa3b+u/LR/0kxm2Jbx5nqrI7oT0eKSjqJYk1S3w2W/JDPzyk4wwwSoOKH4TLq0KxwXE7QEM4aS5hs92ja6jKPIj7nEJKYOOwHdCdu3Qu2SBmY0VWyj+pUohZv5fzDD81nMeCWU7KmtFsXfKAFFHM2ufCWywXRBXKfYTDPYR87+bfNvbw5W/FmDeu9pdpCIbV66yR3pl4d9+FLoDqbS5yQjKzDI+X5Z90FBaW1xaPCKLcp2l9tRq8q8hfvyXZXrJVisu+/igjqpZ3Tszj9XBmmqLFo/A==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFpRSGxBa3NnNVRLeU1NMW1pUENnSFVnc2NoR2VwSWEyMVBsRVJPYk44N0dYWFN1SVJwK1QvZG9qVkxzaGlKdnQrWGZQT0ZGNE5uZTBZNWp5cEVYNXZCSTk0OVFwaEorSnM4U2FQMWNWTlNqV1pKdVUya0k0V0NCZGsxNXN1Z2hGdVFEN254eEVGa1lSQXNsZWl1d2x3TnlpN3FCOTVSMG44eUVWdmFHNmgxc0RXc0c5QlpxVUVCUXZrb0NKTDhFeWU4RmxUMkRZbkN4UDVmL05FYlJkTGZKYTFZbzI5Q0VWMDF3YlZ0ZFpYemhxZXRBc2ZkYmRnTW1KNXFySTRlSkxBeERwQm5jeVY5Z0dKT0xvV21BZTN5YWdveU9MalRaYkVWUkV3dDN2QnhHaGR3K0M5QXVqVitsblNYWjM2czR0Tm12dWxDeVlaTEYxWHByUEtGNlJYZz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "ed4gAI82hqUpvoUH/glIJXIbasq7CDMbcfm2u/fojO+3FsujnsCRCcIJZIe6ny3ExNC/o272WzUL+Tw1tFnM0VYcS1aAgpdJiTyX4LFPp4uJRlutcxDWCOBpAVh+Ma/oIQDAgxlm1EOcKiWyxhyXm3Bjm8c//rV/YyMkm7NpqK99zCfbgnwI/ezGvEaJe5L3N4eLZBAV9BG7B6if9uvSvCWh3NABr9XNeaXLCHC300ENCk8iUNJJASi1sGQnlTR186Ix8s4DPCfZJbNwWlHrbupgmBq+AZRffbU059QrLfvzdxpaRtHIlDxQwmvk8C7EU2kUuLGyEA8XSdiT5y2fRw==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGZpVVJXYnJtWXMrNllPUnYvUlk5NXM0RjhOaXBtS0V2MFVBV2FBVUhvQTlJSlJ1WnhGakNkUk1PWE50S0k5RW8rUjc5SXNVTXF4dm1wVjJLK2M1SzBKUjJDclZ5Vmw0ZVd3STRJekM0d3I0d0xPV0k0djU2S0tGeTN5TXIzSkpvTE9BMVdUUVBaRGl4Z0x5SlNoYlJsbjNaODJIOVFYT0hYUFdXR3VaN3ZDTm5HbVhnZVhSSVZlTkYrREFnZ2sxdDFydEU5ajJ3ZDdxZDdOeDRCb2pjM1JKa29STXJkRHFycXpMWjBsWUNPZFdvbVl6YXh5dGZnNzhkVlh1bVFCMHRvM3pnaHRDNUhTb3BRRzgwNk5vWG1rdXgyNTdHU0dWNGhERkMweGgrczB4SVYyTTZhWURYS2VnQWVJbjY0R0ZJYi84NFBvcWxoeEhQOWRKcUQ4NVBFQT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MaO/4MFm20VFjw2ER/jpwi6iR2VBYKp+uwdJH+/CZv1NlwMDp+9t7MHu9DArLIzQlHjUQ905a8FV9LeNHcDD29CNDXz3u0I6u7Rznhoa78N6fO08aDdHn+MtLzoZaKi7dpJ1M2xNzAM/3x2dTkLiCGKuAOnpmk4SSG2vKu1OssM4e9VTwWgdWgUBHyMef38fEoT55XRy67phr4e77kVesV+X/lM+JudGuzxZgbrFsFVgy98DQ2SJF4gpNKkNOeWKFIomT8bEukxECfi0Vyk/m7PSMKgvF5JBBNQYEt7HXRUo1lVmUc7WvBHYU4dVkz2oQZn06F//IAZo+qsmqOM12Q==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFJNM1NvSmRpdUp0ZkpsUUNFeWtRUy9NS3MvbFZmTlFYK3U1OXhMVkRZTC9PcmFBeFR2em1HS3hCcjFLc2ZCZU4wQW82YlNEb21MSDdsRHdNSGpRZjVCZnQyRlliRUc4bm5rSnBhaVpHYnBCQlBUVHh6SURGbkZJZ3JMRnpTT21UanRTK2ZBTVpjYXJuNGVvTFJvbEd6OE5FbnNrQVBUNWV0QkFIMXg2UUJjQ3h3WUxtOENWeDZ2T3JyZDJUUThONTZ5WEpMOWpZZzFNczdoN3dYSitDV1lVRTI4ME40a3lId1ZCSk56aHVNNkFFdVkxcDdNNEtqSFpycHBXWm9QM0FCYmk0RW9GaCszdjdXYnNCbzFpSFltLzVSQnBqTFZyRW9RNkpuMDlHSllVMDNvM0dRcjIyaVozK3YrUVVnLzNmUWZGUy96UGsxS3NVQlpPOHRWNnhyQT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "fq5jMK7LBRwa63vh+Unxjxxuj8ugx/l0jqRalmWNql+k/RTz3lxsNCTFh1svGTP4QZTLL/GghdZGmGH2Pb82M45ExGsvZoVzkdQ6Gc/y8NNCMkD98pZyYeWchDazrqC1EnB+IoYbuG5vQF5vCwR2jEfd42bu+YnPMy3ackMEF9fDamQdHsAwfDDFsshmePA0Q4RMOaBUu48YhrDhSYPXH2DAv8lwPqh4lWGOrtalV5MFCvVzFO5ss47XDeI5zjafkwoJQPU5b44cvvLXeq56p0cWn9uFt2XMZ3HBHxDOOOAUkqNKShlaQ3m39SdU58fN50MLrc3G3mUjbttFBBE5AA==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWG1CWUs5Z05nRTBMYU9jL2NvaTZjdkJCbzlLVW45ZVpzbmdyZVE4UnR4R25OTVQvbkN6R3REcWlXajEreVd1REpHU1dlb1RBQysrYTR3KzJpNUx5WkV2bFg2K01uVzE2L0NyZ1VaWjBlcnp2eFVseVFDYU9ZbmJhcHM0UFR0NjdZYWxBaTRXaEE3Mjc1a29LQVltYzVBSFg1cFlnUWh3eFVzcys4ZDlJRkg1bGlhUldWY3hVTVQrMzBZcWhERkQ3bnE4TXVyZDNPY3h3eUljd1EwZDZacDdvRCtHMDRNeG5tWjM0cjUvRTRHYU5JYlpNVGE5VDBWUW1qYmJEM2piallKMWlrQkduRnlwemd5czJJVU9lREJ1SjRxVTJBek1nM3NqbkpIcmNWcGRzU2NQcnpISnJkZEtNblE5V2Y2NVkxSTFWNUpOOU9RbUtyQ1pxZ1VuVUtuUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "raKHapJyc7wtw9Qzbr4c4AbRlLAT8p0rkrN+gm3JFSJwFLHtf6dHBQv9tveVRNo4VMeV+PJDbWDcPDEivK4Vq5N9BAlveRSx+d9Mj/ueK323VUIGynQwdI2PO0J4pncTvFIH/VMauMcCItOlmaOV/pKogUIYLqEGdgqPd5M6TuL0Gxki9i9lzZOg10yJZjTIg33I4L1C04xQVZ7c9gcyQB715y0TwF+0oXs1EG2KtUdF2oS2yqCb67v226gdj5aoFNUzfijy7v3s3cRMVA0fQKwpda+d9Rj5NzkvwBo43oKFFh58tl6FbRa3nN9Jj9cxWGtTSIlVd9RQ+vttzObdIg==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWEt3NUpWUEZBVitPUkdYWmJBSFQ1eVhFMFhGdGo2NzZkOUVtaEo5dDJJVmF5VjIyZG9PN3JJUlZ6d2ZJOFdtbWxGeUZ5aHpwNlpaNk8wT1dhejhybUc1Vmdwc0o0cHRLVWU1Y2Q4d0VxeDQ3eEJEeXgraTNrUCs5bFhQQTdnc2VORk83OU9URkJCbG1qUjJnYU1OaTNjOXp0U3VVZjNpaGY5cTZ4TzhLcDRYL3F4dHVpNnhwaXBCK05xQVFjNlpYSHR0TVNDVXIvNTIvTjFBc0p4Nm1TcGsxTjFXQjdlY2VKVk1KUDYvSUh2Vm8wSUF2aWxicUVPVHY1OURYM2JzVEpTZmRUSlkrUHF3dzVRVXFLTXYxSEgxb2xMQnpPejVTZ3lsSlBlSzlFeGM1L1hnZnpXTWd1VzNlbzVpYlREUXdrM0FzSXk1R1ZZZnRQZmc2Yjg4NTArQT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MlADNyM2Rd+jSXzd/NgK53qnNIWrjOswmITkLKy6wmuP7tyYZZfdz/yN9rv/AeaDF0SKxQiTkIuWxtibyATiEFLc2DdulIx8Kl2ZydWSgvEI8ZCrKDNjhX8auceL2XZwqUQEWgNIoSRj+TpXZNwxygg0ZyT9d+PP8RT3yM64/9A2nW9WHMWK/ASwGJVHo1dlDzdspvcUCEtkO7U4ey9q25HX7YDx5p+yMxUH360fDuDYnXIdMyOSwPFO6LkcBpkxWSHsgB1jSZ9bVVceXi+mM3sUL+aLkUd/sP9Yl5/mOKASpJezNKcetAdSaC7VSKJ1PMbcEDSmK6XqblnNGF1L/Q==" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWG9SMjRkU1FWK0lNTXZIY0c0emlxWmpNLzgxQ0Y3VENJQk5HMFJEdjJlRDlLaU93MGNRdEJ2ZVgxamlPS0J4eGR5cThVeGQ4WnRXYnZUTzNWNXkwRno1aUY3TTBMK25TeHluei8yZ1NRdGg2UGZKZ1MyYzVGQTZBeFBRNkFTNmNsT3FmQTlGa1d4ZkdNVHkrRExSVm9DaDBFbGlyY0JIcGNZQUNqc3lWMkd3am1nZEpaVCtKeDAzZWZNV0RpdlNmWlI3cW01dkwzemhhTUZJU1gyK2dBdFZKaXpmQ2wxbllBYklnMjZzbHoxMkdjcjY4Z1VGaGM2ZlhvWVRMZTlNQ1dzRHF0UTh1eVJDMFBQNUZwWERqdVFEdzR2ZzJGZ3NFQy9scjRKY3FhYUJHTldMNTlHNWRpc091NXNTS0FFZkUyY0NIV2Y1TFpEU2xWamVPSnBSdzF2UT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "doubleSet": { + "B": "R/EUfopsFa4thzJjPi5wDKrD0xkQTxsqNBPQThG+FzXVtVojpI8hd82BLjWYjjTH6t20+rvutk9tXnfkywoygQ==" + }, + "*amzn-ddb-map-sig*": { + "B": "p8q1hUKl9fSZWKI8tvL2kxU8rPnTRACedH4snUgOB5u6ZQ/sTOI9fbRdbStCWnybYiAfGAcxrpDKED/t3tWGcWqUVWzinfDOi3qPrGi51JKE02j9Gl0wzgrVe65wvCDfrvaTGggWPkqOKyqgdzT8HPFVoGXGYdAFSo+v5XCXs8/PxWxWkYxJvFXprzQ56FKV0IFG+HZmpoltqI4cv+46NdcIerEd6W8J1LhjZU4PKID/6QwPLa0iY8LVC9pWyqR9GVZDQ/bRko52KrUp1BYwXyrmu7CyN/jWT+nacxMMHJuIkCsXBGFV3+CEbbxYXvjTg89d/rD++OBWoM6Celm3iQ==" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGlzOUlLakQ0dEZWcWxvVVc2QW5ESUhxbVRNa3djNnQvVkhoUnQrbVk3SGpTUU5UZ0hIbkJKOGZsWDF2QlpGbmJYN1FsMVBjMEViN0ZWSGJKbXV3SWp6L2xnVzF4K0FZT1RVYStvRzRNZEJJK2ZVUVkrMU1VNFh5RG9EVmN2TW8rUDFkL2hGNGxPcng0T2xOV3ovVm8zUzY0M1VVNitrWU43SVVCOC85RGp6Z1NTZkRIS3E3bEFsMEVCZm1MY3NtRkpmUFFLbVJ0WjIrbXRRNUhsV1Fubi9DVnQ3bmZGTGhEV1NEbEtYMm5uSVlCVmdrMEJHQWFOaGJFbVNqREFaaEJ6V1M4N2pOVTJnMGJkaHlyOHRNY2c1TmlCM1F5TDdPWERiS0orTXlrUmJvNlA0Vm9NY1lkTHk2OUdCYWM4WjZFQjFkNGdmTUtxNklnazRhelp0aDhVdz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "intSet": { + "B": "0Sj/sNRaozw9XKYKDVNLW4vFoEB0e76L6awEUvtOCBA8pL77j2CUtHIiBDB1HIf2pBb/i+oNF85lwptguFE6rA==" + }, + "stringValue": { + "B": "w9aZNvR3yzxbGhub/qkSk+AK8+ltsl15eH9e37CudODt8OjztQo0YVwrP0o+JS3y" + }, + "doubleValue": { + "B": "isjjsah8rGZ78Af2gnP2yhWZ8Wq6PDLb5aP312l5zl0=" + }, + "stringSet": { + "B": "gW/RgnOMZM9nZk7PRQ4qQwakhReiS2oaQC3OFTQkigx8nO+KAGlpdqSKZGV6vtVyDcgEtmA8zcphXizTCZGQiQ==" + }, + "rangeKey": { + "N": "7" + }, + "byteArrayValue": { + "B": "GjoUlvWLhyxuSzUKtatZd4r/rUudD7hsajyQ0oOzjZ8=" + }, + "intValue": { + "B": "165MtGUmgndEpx90SVAKf7dSTkmwS0wrVmubkpMBxl8=" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "jKVKU8uHbhAg8vlU8WqK3qIss6XKPJQXATVwFlkqw5N7RMj0yjQWQ5pJC81sdkXp3NmIgF9Wnavzl5TEVB6R4v/cwxT85ih/kMN7NDOXU5OEkQUlzCRCZ3U6wVvWgFbbI68r42LNPav+uuWBB2/cp9Uu/4VbsOQC7IjEdWIPkir+5BP7HBFg78cs9YgpkDuw2J8+4KLj4z5CsSW6dPjhmbPolKmhn8DinezJ6bHpRFmP0ry75HxMUTu2wInwHD0mCpK1TXWJ3t8V1+UJkNHHpD6j78UhNH9Ky2h9pgj+7Gml0pnZ9t0skUCXNcBLf0Pj3RsqvQuYrU6f2tV8DDxm8g==" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWEJxTHVkd2Y1bFhPWUNTN3hJUnkyclJBMzBVZUk4WGYvd01yTFNMNG9zNHFvNjgyY29tNGx6M3IvOTE4bFFiMDhmazRZMktqUk1EdFpydkZsRVhrN3lDam85QmQ5OTNWMm1zeXBTUGIzYkt2R2d3NXFzeExJbWdDOUtrL1kvNkJRd0NUM3FxVWZ4aWlLZmdqNEs3ZHBsTXZJVFlPZzRlY29nMXVKQkl3cENhWE5YYTlWZkI5ZDlWRHg3bUhHSnpROG5vekdJUDdzU1dUaWRzakI0NW1NVXU3dTkwLzRoYTR2VGZ6czQ4QmhVZnZMczNvKzlIRmZxVks2cW1va3JlRWFvVjhPVFdIYThxMGxSekYwZ3J0M2hDamUrREdvNVpFcnUyclFmdmgvK1prTlpZbkw1V2p3Y2VhcEJnT1h3UDBPYjA0TWsrWldzZkdLRG5uYlF6V25iUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "RJpKAoXXSPxKUlYA3zxTAMIbaXJoIjo6Mq+Sy4CbGHQzsf4UkWEz157mdT+OCwMNGBUXUnvJhX+9GhTB/dU0pCkGrC9p7BwazNaAhGO4fcDPEsVP5LTSAAs5ZEw1CdopWQsK+mVMAw12XwO9NeOW/cUG7wDZ/u4Y01ejnO3nLaMIi24riIQRiMduk8AJTg41lU4rcSxDKWUn1pBweolLTX6W8zo50BcmAn/qeThVVQBoqDgJYPyUZ6UIDDl3OSg1Ujsn2c0JgzlAtxddWQ22uHSRKUbv7tURIO5N7WmK3RhUnumACekG4acXt9kAn8PBWj2Yvwr4Z3+w908RET7+vQ==" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFdZOCswY2ZZaFkyT1Fjek1Ya25YZDQ3UXZxZUxsQk82dTJvTS9Id1phZVVFb3FjNlhtNVd1VjJ5V1pZNW5qREorS3BrdGhaQ1RsTnU2QXdNcmpBbm5WVVZZdHkxWmtlRUVMWU9UZmhzN2ZGTkdqQ0d0a1FRWEpQb2lLOVB4OEt3N1VRdDAvTXJTY2k4Ylk5VHgwVmFVQ0I1MDV5ZG1IN21WeDN4bkZlVXRZekMzNWNlMEt3cDdqaG1iRTFTZVlocWxPTVRCRE1sQmZvcTJRSG83bmJUNHo0V0Z1RlhZbHdSMVJnRDNsK2cydjl4dkFIekF6SDZmeW5qTFM5c2RXYUc1Q0FESElqYXRJMmlqeGh4NXFmeEtIRmFBN1JraC92UEdFdkdLdEhsdkhBMmlwOGtRTTRmcVpVaC9lU2RhT3EzZ2RSR1NRUTE2aFZMT2JlZkNJZHlmdz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "intSet": { + "B": "1y82dQGX/4gbSZ9okCbwkWYt35CHvGRlHSwKR1K/NV/+AQ04tEWx3+HNze/t78qIa5ttNWw6gzFl6lop6iaVmQ==" + }, + "stringValue": { + "S": "Hello world!" + }, + "doubleValue": { + "N": "15" + }, + "stringSet": { + "B": "CNJWkRL3Rjm5akaVNBUizbKtQ13INgSUwMNQR+KYYKJPJQJpAQIrk1u8PALl7V7JvDEOAdwcv+gNjFT+WQGniw==" + }, + "rangeKey": { + "N": "8" + }, + "byteArrayValue": { + "B": "fqCji3Cm6TKPeE6Huejc22X/1746VPMPVwrkqvNtGHw=" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "UKESqnTKdCqAtM6aDkJGg068ssNWFv811njBVuRK7mzVtmIG5OxLQKr8ycBf/Zm3j2fDnkeLnZwc/Fya9XCTygte4yy1QZSywrSb83uhGFlmLsjGOKcE5ZTMPEMb75+I+8I8OQ3ggfM3EnyaTFQCIfeY+3antQ3augrWioBaoJ3VpoUU+RSA6FOrlVtd01qNO2ZOXCfcX5soh2r60FXZ3fdJZJKvO61xkf4nlZJQkc175bsV8KRHh+125a/KETb+3Gc8uL2aRFBO03fuSCHS97YN7nbevtzM/WdqfXh83N0sBIibHhY73xd5n1sDwKhn9D3madRlzlj6GgwiY6wOqQ==" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGxjNFJBeDhFbllhaEk2ZFVaOUpQeDR4TVhpcEVpUzVxK25jMS9EQ2gxQXhwMXJIdjdPTUREWVV0cnQ0Z2djTjRwS2wvZi82WlU3aUE0VFZRZnFFMnNaTVJWSEVrUmR3ODFSKzNRMHJPVGthK09kN3BKSWNVOUprOTczM1orK2t2ZlhXNXlKYlZwQ2diRmV2WjhGS2NDdXZEMFE4WGltR1NTa1JXRXgwY2lHbG1mRm03MEs2aEd0ZW5RU3prZ3NBSnBCc2pMVCtYbXUwWjBiOU43b0hrWFRJZHVmd3pJa1lKSitBMW5ORG1nbEVhM2U3Mm1tMFBCRHJpVVlMUFhSaHdXQVo5TnRaSUF3ZkwzeFlMNGI1WUFqaGdJMk5KYm03YnRYb3F4WkdlWmdONkRyWkZSNGEyTVBBMm56OXpYNXFsQWYycVdMc3Q2SUlHbUlDeTVEWk12dz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "hashKey": { + "N": "7" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "doubleValue": { + "N": "15" + }, + "rangeKey": { + "N": "9" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "doubleSet": { + "NS": [ + "-3", + "-34.2", + "0", + "15", + "7.6" + ] + }, + "*amzn-ddb-map-sig*": { + "B": "POVybkDUXTky3BszXwOiehdzQnXrOoFsVz9l6o9hxXSBQ30LzwwNSNe2UxGZsZGfnHW1BWhg+T4ycxdcXwImovTRRUNUAn1RFU1nJLZaVvAw9FSvDbRWbk4oTiyv3kr7NiCgCQfKOM0H1eUi6tDUYdnR5kPwP2aAyPVtJE0oLR5g2s+09IoOs5FSipYcPOtlN0rT5fOtMCEe2goCIMyluerqISBYmCnLrpg4fhWpQQTvCFuSCccJC4zoQjFrSQAd4hBHlS+xsCmXi1KS3ECwK2bRutntUzZJaeFjFpEn5y7CV0Y/9SuK5fc4QO/XBkubGhiHU74199/etB11rnETDw==" + }, + "hashKey": { + "N": "8" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFltNUt5NEFSaTZIZ3ZxQXVwMkcvM2puWFBXOHdoRGRpTTE5RG9OVlVTenYvU0F2Q1pScnNnMS9IM25jWVRYY2Fvc2wrU2xJc2NQSVlDTXBVYmhmd2F1aFM3ZE1TUEFqa3FaZnFpb0FLbmMwOWFGR015RHA2Qi8zU25VRm04TGs2Mit3S050R0pyclA5cTh6TFBsYWtJSzE1OGZwM1p6aFk2L0xtVjFrYVBoUms0azkyZGdFOHBMRVZzckVZZXVrYUdleXBFKzFaRnJCc2kraldoWVdIeHRNSS9kUzNqTnJYSFhXblVhV3pwNkQ5YmlONDk0YnhiRlR2b2s5bnk2R1o5ZVVIYXFPdDgxcDdSR1ovUHkwT2hLdThYQVp0TlJWRG1VZTc0T0prMkkzNjcyQzdheVVXeGRZOVB5ZThZcnRaUHY2em5RYlVvVFRybHRPNG8wbDFkdz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "doubleValue": { + "N": "15" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "rangeKey": { + "N": "1E+1" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + } + ], + "HashKeyOnly": [ + { + "*amzn-ddb-map-sig*": { + "B": "SNpX+4QUwYC+yMsNiQQcYTXiYWWqnkR02KLn1VRH0YLx1wEuFJiOhhqD4a4AhiorExenoP2HHkZdZMJpGGGU9NbupQIr2SeKvV/dkEXrCADvVaaB5O6xIhsN638f9ibknZLEhUt+XAgGDzhPedKwPBr4ZC0UnQCasedHqb9CGXYMCB8P8URbllcJRayM5mf/bv4vfBW7t9uUTd2p6wsiDNG542pw9unP5+/74mZewfgbbp6bp+8KECVLjwTny24LHdSS7XGRb1uJcZsapnhDDamjctjc1jsaaWk2WWUf2YSp/mGNWgk9+m/St/cRwwVr9wjcGpcMld7QDHEEJQmNxg==" + }, + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWEFWclNqZ2RNQTZkcEV0ajhkZU5oVEtXdVhGMlBCclFSeXBKRzRkdTRYWFZlTjRWYVBOT3NwY0dRNFcwVjQ4dVQzeWVzSXlad1JyamwwL2pIby9oOFlxb2Jya0hNOWNUMXQvN0h0VjNRamRzbWZHb0xGL1Q1QXBxaEttY2dYOTZ5V0hWZTYyMlFLRk5Sd3lHaHNTWE9MYnBLOTR4Yk1iWVh5Wkk2d1JwYVl4TTRaaStrTkJQZkpqekxycHdrTXNjN2Y4b3hxV1BZdUdxMzVzSFk4WStXRmc0bUNoMmpQZCtCUWRFeThEaFF3enFHRUtkcW1ObmY4UDFMMzZKbE9NUDVyU1RwUDUzVlVYM0ZVbmVxbG9zT1lGMStwdkdTQ3lScDlJcUZmdGE1N1BEbTkrVkRySnZXd2dXUDh0TVAzdkhDZzB2UnMyV01BN05MSkJRdStYTUdDQT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "V0k/mB2e8DSl4lIriyqbYBQbWZDKbiwcfc4ZQB2R3PA+S+hnjiYgwr4zgOXKNk2Dq72M1aIEXzbrej8jVoCSTSiC8pBXxekTqSnUsIYy7ilo8uvoSAN4a8zyfLXxvFPn+ZMwTs48uz7fVe+4MTTIkdd9+sJDTx/ZPEf88mAg3yiQ27cnnqG1N909cvljgjO1ADCcNqfvIMAys3xW5ML4GzdF/G/c/MlRRBMy1rq8HcRC0E09L9BAChfSV3OAwYyns90X5QuTcmpgr5PnY4NFm5WBWYhLwA/nyZDb+Y8e/XAd45i5gLpEpBBxFUiU3X949byFTr/naYFoatBoiWuyKw==" + }, + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWG1sTElwc2ZrSWlLdFJjUWg1anpSY0hIelU2emIyRjB0dTJPNTVESWI0OHlGaVNrTENkUTJYNHZEL0Z6OWYwR1lkYSs1eVdwd2VrNkFCRmxFRHRuVUwwU2MxU2h3c1FOMCt0eE9PaHVYTTlnZjhEbjVmaWpLUVlxaE00N1JMRVlyNzZvak12eGtiR3JIUlIrd24vQjNwb3RKZlhBM3pGYVVRb0xybkJ6VnorR2lNeE04QnJNNnRxN1U5U2k1VDczUVJlRmdUdlluc1F0SVhsWEtMZFhzTG4zaHFlTmk4bzBsMVdxY1ZCSU4rcUNwMWpkZzF1Zmo3K3Q5aFJFbCtWQjd5dktDZ21Ld2J0MTgyVFZteGFZV0RRMGtyS1F4WWdOK0N2MXBlSENmYjZmWjllRW9SdjJ2VXgwZEg4RlpTV3h6K014Q09UVFkwU21jVHJiR1dEOUZRdz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "QZii10yqicfBPRRi31KpeTnpe5Dp1oSJAqB7L3uyTWUXz+sXeTqsEFqaIebiTtTCixgK3ZCs9mlM4X1V2iEgFWYuCs8mNoO8oY30vXw17E9EpW79kMn8Tuqr6XQqt+lMorFxKjiYcIkhVbNF6greXbSZ1HQdUGIPLQkACQfzX5I6YWjOCcGm60hXb2dp2uZy9kFceKCTIb0OtryI+7bVXX5YH4Ks9IOKNULWNGbjXEr3J2QdkeLcWZgZQVHtaikXiOlaz+WWyU4h9LaL5DxrojDCu68GXDmOzHYUvHbGCfk3y3hhfkwt9vwucEnA+Y3uDGH3vxUerA8iQ6qUH3m8wg==" + }, + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWEg2Skp6ZUR1RW5zZ2V5K1k4OE1ZZHNLeUx1c09NLzRuT25qRTdxNGVVYTZrRUFXcFpIQjhSMjVFckl3VW1TSXVCbk5rOHd4bmo0cjY2Kzg2VFd2SklHcWlSSTJ3QS9xL0x4U1FxUDY5YWE4YnBiMUhhWHlBZFZpVWhnT3I4MHAvSGpCTm90bFh6VjMvU1h4WXU1cTVxUVEwU3VrR3RkU2tIN1dZeU02YmpyaTE3aUZPQ3F4UzZNNGR6NWRtSU9SbEE1U1NSc0dUcjBmNXUvNWw3dXR0YVFvdG04UzFid3RIZVA2U1R5dWVUYjhLRU5mSm13OFc4K25TVXdhNGtHb3lTUDZ6L0E2NTlrVEc4SkVES2JuWTkxY2tycSswUUozU01OZmtuWG5BRGYreVYrTE1YTDNWMGprQ0hab3VBWktZNWxxelRoK1lTcjE0WGNGWkFlUG5zUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + } + } + ] +} diff --git a/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-3.json b/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-3.json new file mode 100644 index 00000000..0b0fef37 --- /dev/null +++ b/test/vectors/encrypted_item/ciphertext/wrapped-rsa-rsa-3.json @@ -0,0 +1,309 @@ +{ + "HashKeyOnly": [ + { + "*amzn-ddb-map-sig*": { + "B": "SNpX+4QUwYC+yMsNiQQcYTXiYWWqnkR02KLn1VRH0YLx1wEuFJiOhhqD4a4AhiorExenoP2HHkZdZMJpGGGU9NbupQIr2SeKvV/dkEXrCADvVaaB5O6xIhsN638f9ibknZLEhUt+XAgGDzhPedKwPBr4ZC0UnQCasedHqb9CGXYMCB8P8URbllcJRayM5mf/bv4vfBW7t9uUTd2p6wsiDNG542pw9unP5+/74mZewfgbbp6bp+8KECVLjwTny24LHdSS7XGRb1uJcZsapnhDDamjctjc1jsaaWk2WWUf2YSp/mGNWgk9+m/St/cRwwVr9wjcGpcMld7QDHEEJQmNxg==" + }, + "hashKey": { + "S": "Bar" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWHFremNPOXl3MlpwUlh5U1pRQ2Jra1BxKzkyakJIZ2hpUmh2Q3hQTS8yNGVjdW83SFdvL2FDRHl1NTZ6eFJ5UkhiVkNnTUw5V2tua1dBTWhOL1UxMFJjOUhKQzlmd0pYM0h0cUVxVXl0T3NqZjRzUlhRWElKZmNoTjRKNFlYNHROQ3pZT3EzWE1BcUxCUng2L3ZkbFp3QUVCSURLVnJQWkZVcHEyZlVVYkNNeCtxSWV3NGJwWVRsVmhteC93MlZ1d2JldHRTT3huckxiOGZINCs4VGpGZ29MMlgrSnk5MERUV3EwNTdoVStDbUx3RjVxazJKaVRuenpSOFY4S1lVOUJTNXJydlBDcmd2N1g1S2QxYlc3akd4dlE5dk05cFI1TysrQ3dQUCtKVE9YbGNFMXluRXpRSFd3d3EvSG9RalJBaXRjaDFINDd0WUJPU3pPSHNYUUZGUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "V0k/mB2e8DSl4lIriyqbYBQbWZDKbiwcfc4ZQB2R3PA+S+hnjiYgwr4zgOXKNk2Dq72M1aIEXzbrej8jVoCSTSiC8pBXxekTqSnUsIYy7ilo8uvoSAN4a8zyfLXxvFPn+ZMwTs48uz7fVe+4MTTIkdd9+sJDTx/ZPEf88mAg3yiQ27cnnqG1N909cvljgjO1ADCcNqfvIMAys3xW5ML4GzdF/G/c/MlRRBMy1rq8HcRC0E09L9BAChfSV3OAwYyns90X5QuTcmpgr5PnY4NFm5WBWYhLwA/nyZDb+Y8e/XAd45i5gLpEpBBxFUiU3X949byFTr/naYFoatBoiWuyKw==" + }, + "hashKey": { + "S": "Baz" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFQ1WlltM0doNGZ3UnBmUE9MMWduSTdaekNSbWtXa1kzZjVqV2h0NGZ4a2QvMnF4NWNxNm5aa1IydFhoL09ZQjVvZVE3VDRBUVZBNE95TUsvVDJ6QUNSallhNlhSdE92Y05EdnVRalVGUHVBbThKRUlOZWd0cER4RjZreklYZG8zanhQenZscWNFakZxdnpBdDRBWGxCa2hZOFZGeFBCc2lRV2owUTdwYUJxak5DWGNjZ2lLdG83U0ZTaklCNmZNRFFtWkNaNUpFRHVLcXkrY1h4UytyOGJLODVaMTBwMlB0YzZUVVZZdEExVTdXUmxwOThIZExUL1lJZmhoQlVDT2hLN3ZXN3c3WWp0T3g0UTRUT2dnbndBQndxclEvUXlrZXZCaG9SVHRoQ2NkR1JqUVdrTXhSaW0yQkNaeWgwS0NtZStTUFhKcXAvbTZybVlxbEFxUUtyUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "QZii10yqicfBPRRi31KpeTnpe5Dp1oSJAqB7L3uyTWUXz+sXeTqsEFqaIebiTtTCixgK3ZCs9mlM4X1V2iEgFWYuCs8mNoO8oY30vXw17E9EpW79kMn8Tuqr6XQqt+lMorFxKjiYcIkhVbNF6greXbSZ1HQdUGIPLQkACQfzX5I6YWjOCcGm60hXb2dp2uZy9kFceKCTIb0OtryI+7bVXX5YH4Ks9IOKNULWNGbjXEr3J2QdkeLcWZgZQVHtaikXiOlaz+WWyU4h9LaL5DxrojDCu68GXDmOzHYUvHbGCfk3y3hhfkwt9vwucEnA+Y3uDGH3vxUerA8iQ6qUH3m8wg==" + }, + "hashKey": { + "S": "Foo" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWHJFbldWQmRWVHdpOERuOG9qTHQ5R20zV3dCTm9nYWZmbFBXdmRCODYzL1Buemtsc3N1QzVlaThUVGlOY09NVzlqZGZvTEtLTWRrVm5OQ3BpaDc5QWRTU0ZpalVERXNnODNZdHFlS1JYRzdsd1B5KytnYTBkOXhFWmMwZHlPN0FSS1ozekcwU25NcVBhTEFIdURaYUQvSzVIVDRPeDZpaGpmS21PQ1Y1QytFQlNRaFVRUTk0QjVOeG1EOUZObEl2MHp6QXpXUVo0R0htTTI4ZS9tMWJGZ1hITExLT0s3cVFhZjM1TWNqNHFoVUNJVzV0TjVFSW9hK0tTYWxtZkMvRE5ub29oU0JWejN4cDdpbU9wUkMwdUZXdW1HaHRpR2krYVl0czJwOUdodUpobHFvQi9RSk93TDVDKzNNelZRaHJWUG5aMWRYemhtTHBGblUrTTYrRDl5QT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + } + } + ], + "TableName": [ + { + "*amzn-ddb-map-sig*": { + "B": "VRRX8l/eqIeMo7TvQbHI+0Zfh6tbwT5rFJ2zTLYoloudkb8WcBjcHuHEGUhFia6lSKOXwU1cEi/dT4YbQUXf2vzVTxS7jDstYHwHxscVPYNKp7FKzrG/Rym2lF1D78cTn46Zu2/XPw/JgTUhL0Ar7nmmDjUONzzd41QZGr45PFtgBZzGSHyyIpWU2+TRA87quKL71YnrzfbfWoIutJLQ8lAuGlx/gm++09c8PCL60CwUGl6moaVzSYpu/zR+1lxFZ67sWnNrxlsezsQcWUbPJKgeaHfeKDxSevaALTS9dCAjSlE0Sv7XbsdjxW2huNPcPTQCOcqUtetDJ1W2GLa1mg==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWENNRDA0MG0reDd6am94YkZNb2FzRDJ5cU1MRGVzWnpwZlRGMXNNcDVHUDM0NXljTWlIbnhONDRLUEJQUlE5clh5bjE0T01wVko2bkNBVFMvMUdnRVRLdTYrQmdmeEtFZ3NYZ29wRWtVOE52QnJ5S1creHlyeFN1bHFvUG1JR2pnM0hpMEd2YnRvcnRkeGt0NTVEbXVyZkYyTjVKbnZtUmIvZmRscGRkdEJsTVNycTR2UFZtS0UrbTl2b0hwNVE3VzdoZ2RDODVxZGE5MVBkd1hsb1RRZU4xS0NhWmVlTUtnMlF2a1BSa2hWbHNZOFJwekVrZEFSRWxDLzBwT3VTQS82ZURhbU1hNTEzK3VqKzNlRmZ4ZzNPS2MzL1VSSHpGY2NGb1p2VjZJcmtkODN5TWwxVkpIaGVKbGxTU01UeGFwWitOSXp1THRJNGhUeHh6bElEYk1pUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MG6vTV+uPAaPmZIGR4I4DbUwIUmivEZQ5sqpK83hue0SArv2a9TtlOTIighJa3b+u/LR/0kxm2Jbx5nqrI7oT0eKSjqJYk1S3w2W/JDPzyk4wwwSoOKH4TLq0KxwXE7QEM4aS5hs92ja6jKPIj7nEJKYOOwHdCdu3Qu2SBmY0VWyj+pUohZv5fzDD81nMeCWU7KmtFsXfKAFFHM2ufCWywXRBXKfYTDPYR87+bfNvbw5W/FmDeu9pdpCIbV66yR3pl4d9+FLoDqbS5yQjKzDI+X5Z90FBaW1xaPCKLcp2l9tRq8q8hfvyXZXrJVisu+/igjqpZ3Tszj9XBmmqLFo/A==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWEhTY2swR0lJQXBPT1hvenpudVlpRjJNOUZQZG1FNUpsejBLTlBsaFJPcHVzaDliNzhseGxBK01QYkMySngveEQ5NnZVTmtqTW5Dc1BkbFZDcHQ5QmV4ZkdGNkYzbC92QWVOeVRMMEFvTkdYRCttMWVCL2tIbm5ZalVyWUJ0U2ZseWxlOHF1UEZZRzVxbkppZm12SGRzRXk3R2VnREZKM3M3NktqaU5RNDZGWW9KTXVUblN5OXlRekhtbm9uUDVrcXliYVB4b0E2TTE1Z0JQYjNiWlNTUHpTYmQ2M2M5cVU4UW1DMWVtY05TWUxYTFYyZGUrUmhaRFM2YnlnNTBuTkQrZXRPUEpnUmNCVjhVd3J0d0RFSGo3SUcvbXdqWWcwM0hxdUxjcUhUcG5BVFVOeVJVMHVYTGtlZmpRRStxSTZiS2xSTHdac1gvYndwOXFLdGJOOW1sQT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "ed4gAI82hqUpvoUH/glIJXIbasq7CDMbcfm2u/fojO+3FsujnsCRCcIJZIe6ny3ExNC/o272WzUL+Tw1tFnM0VYcS1aAgpdJiTyX4LFPp4uJRlutcxDWCOBpAVh+Ma/oIQDAgxlm1EOcKiWyxhyXm3Bjm8c//rV/YyMkm7NpqK99zCfbgnwI/ezGvEaJe5L3N4eLZBAV9BG7B6if9uvSvCWh3NABr9XNeaXLCHC300ENCk8iUNJJASi1sGQnlTR186Ix8s4DPCfZJbNwWlHrbupgmBq+AZRffbU059QrLfvzdxpaRtHIlDxQwmvk8C7EU2kUuLGyEA8XSdiT5y2fRw==" + }, + "hashKey": { + "N": "0" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGZLWHJVY0NjZjdSOGpTbERSVEkyOVRDMEFMbXlVQjBCQzE3WmZBLzlQTjNkYldNamhTb2FCNjhzUFU5ZkFCQld4YzliZDZRZFFndkxHM1cwWmgzZzFuWWpVYTVkbklUdU1TU0Rxdm81MENZbkI4QXp5dmU0NmVXNitJYWh5MlZJR3pWMVRkWHA5MnoycXhSUHNMOFlkVmswZ01MTUh3cERadXljZE9RYTAyQllJWVNZSHQ4NlNDSllxWjVoejBrc3hCaTFBTHM2ZzFBTGNiNHMwRU5nbWoyZ09wV2xQeGNXOCtVUVFjWVpHWEFGUDEyWkpzNk9rM0ZoM2tiMlhzR29uWFBweGZtaUpCU0paMjJ6UWNiekJ1S2VHOWxVdG5nSExLb3dqU0t1NTNKR3pjTXl4MHdCQmF2SGI2aFNicVh0YkRKcXRCaHMyRXRzMkdlZzlKZzRzdz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MaO/4MFm20VFjw2ER/jpwi6iR2VBYKp+uwdJH+/CZv1NlwMDp+9t7MHu9DArLIzQlHjUQ905a8FV9LeNHcDD29CNDXz3u0I6u7Rznhoa78N6fO08aDdHn+MtLzoZaKi7dpJ1M2xNzAM/3x2dTkLiCGKuAOnpmk4SSG2vKu1OssM4e9VTwWgdWgUBHyMef38fEoT55XRy67phr4e77kVesV+X/lM+JudGuzxZgbrFsFVgy98DQ2SJF4gpNKkNOeWKFIomT8bEukxECfi0Vyk/m7PSMKgvF5JBBNQYEt7HXRUo1lVmUc7WvBHYU4dVkz2oQZn06F//IAZo+qsmqOM12Q==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGJ2OTlUVTVNbUozRVJ1Und0ODdZNE0wVnptZXdnNXhRUFg3R0FSR2hkYXhDNGZQNFphMUlWOTQ5V3pQcjJvRXJsaVhLTjc2STY2bWJHd0piZ1Q2UzJtdmlFRWYwZXl2bDlKTTNSUmNzbFpsVmpMSzFFcDc2aGthNGpoclhVaURuaUNTZndPYXlYbm9GMDhIMml4S1YxazdPZFBvazRDcms3UHNFZlh3UmN0eGU5SmoyaTZ1Vzk5a3VielY1R215Q0FPRDNweTgzZEJtME4wVG9RMDJaMmlJYUlYb2I3cW5nTlBDZEtDakpyQ21xbE9KTUswOWJsRkFucUhvc3E4K2wyNXIrOVhtSnErajByZjZJQ09MMDlteGpFZkZnOHFYaVAxSGUrWUt6YStIbTU4MWZ4dnY1QUtpUngwaFlDc1p6VkZLODlVN0FJT1NISnlxaVUreTJuZz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "fq5jMK7LBRwa63vh+Unxjxxuj8ugx/l0jqRalmWNql+k/RTz3lxsNCTFh1svGTP4QZTLL/GghdZGmGH2Pb82M45ExGsvZoVzkdQ6Gc/y8NNCMkD98pZyYeWchDazrqC1EnB+IoYbuG5vQF5vCwR2jEfd42bu+YnPMy3ackMEF9fDamQdHsAwfDDFsshmePA0Q4RMOaBUu48YhrDhSYPXH2DAv8lwPqh4lWGOrtalV5MFCvVzFO5ss47XDeI5zjafkwoJQPU5b44cvvLXeq56p0cWn9uFt2XMZ3HBHxDOOOAUkqNKShlaQ3m39SdU58fN50MLrc3G3mUjbttFBBE5AA==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFdpcEVnbm5uTm9adGZKZ1JibktFbWlJZlh1dDluNEorR3N4YTZ3NTZJVkFZQk5Oakg0Q1RXaGZKcXlEZjc3UVlNQTNhNVdQeG5SeDUzTk9JNStraWgxcmlrS3lqaFpXTDh1ZlQxOUwwWTZsd1FjTUFaekRGckVFSS9UZW5EWFJQZUR5UnBXR01NQWZCbnlaMzl0aGUrRXNzYk5aZ1poWndtaVRYNlYyUlVmQ0tuRUFlVk40eXB2NVdzUEJIcnFmdXBtVXNBVjF4MFdVNUVSSnhNNUl4SURhL24zOFlqOEVsa09mY3pYbzlNbTBmOE8xVmJKZGhQbVVmZEYzQkxIcWdMY1o3aDhNWDUwVjdlYm83ZWY2SkdWR293MFVVVGRwUC93K3l6RFUvK1dQQnpwR0wwSmgzaFhQK010M2dxZXBLQXBXOWtGRXBKTzhQOVMrMFN3VEhVZz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "raKHapJyc7wtw9Qzbr4c4AbRlLAT8p0rkrN+gm3JFSJwFLHtf6dHBQv9tveVRNo4VMeV+PJDbWDcPDEivK4Vq5N9BAlveRSx+d9Mj/ueK323VUIGynQwdI2PO0J4pncTvFIH/VMauMcCItOlmaOV/pKogUIYLqEGdgqPd5M6TuL0Gxki9i9lzZOg10yJZjTIg33I4L1C04xQVZ7c9gcyQB715y0TwF+0oXs1EG2KtUdF2oS2yqCb67v226gdj5aoFNUzfijy7v3s3cRMVA0fQKwpda+d9Rj5NzkvwBo43oKFFh58tl6FbRa3nN9Jj9cxWGtTSIlVd9RQ+vttzObdIg==" + }, + "hashKey": { + "N": "1" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGljaldDWmg5dlNDTXhQZE1mNlVKdkpDME9jMGR5cFUycUJ3ZlZmOG9zRWxjb05ZdkVaeGtPQlFrcnpIVmtZaVI1dHBRelplOUZ6QzFnelZOSUNYSDVuWVNZN29sNTJISlN3azhtbDZaaGw5T0F5bHA1SVhISEkydnhyUFhRa3BsSzJjcDZLQjAxQWltQ3VwZTI4dCtQR3JUOGdka2Q3UG5TMlBTVmMzTnJpUXZVU1Q2Q0VpMnNaSUVnMDA1Ui84MDJUc252eGtGdnNyTVU4SVlhN2NodFEydHdEK1I3QXpzeE9OdnBjU25ud0t2NEtKd3RobjVnNlVNdVJHcXhKZHB4cUxKSGxqQXhNQTZiajFEWEVNNXgrUkF1NWExNEppbmRFSjJ4dUN3Yk1zSWVwR3BXTHFGeFA0SlZuL2F2bmUwcWVURGUxNXlMRnpWcmJPdkNFbEk3dz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "MlADNyM2Rd+jSXzd/NgK53qnNIWrjOswmITkLKy6wmuP7tyYZZfdz/yN9rv/AeaDF0SKxQiTkIuWxtibyATiEFLc2DdulIx8Kl2ZydWSgvEI8ZCrKDNjhX8auceL2XZwqUQEWgNIoSRj+TpXZNwxygg0ZyT9d+PP8RT3yM64/9A2nW9WHMWK/ASwGJVHo1dlDzdspvcUCEtkO7U4ey9q25HX7YDx5p+yMxUH360fDuDYnXIdMyOSwPFO6LkcBpkxWSHsgB1jSZ9bVVceXi+mM3sUL+aLkUd/sP9Yl5/mOKASpJezNKcetAdSaC7VSKJ1PMbcEDSmK6XqblnNGF1L/Q==" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWGRMN05wNXRiTDQ2SEVhcDBlNCtGek5KditIMmo2WkZIdTdHQmZYZUdNTWlUaE1Ibklkb0hrVmQ3WUg1OEVaZVU5dElNVEh2cHVTT1NlV2V4NFVMbUZNYXVHM2o0aEFOZ0ZOOEtKR296STE3dlJQaWhmcmZ1VW1QQlVtVzZwTi8xYVpXejhtQlJBSUVyamRRMFFuWHFxaWoyNGl2QlNwVHB6SVY2MExTYndCeFJMYU9OYVhIVXNtTUF5U1k3NjJJZWtrbWMwWDd5MEJmRDRnQXVZbTdlU013SHdyR2l4QjZJbFJjQVp4dElPSVVoRGhIWWwxa1FWVExoZ1k0Q0N6eW1TbHo3bVlBNHBGVHhYeE5KRGZsYVg0TmE4eFBmVy9yL1hJZkZOdUZuajUxSzhKTHhueHNUM0VKV25FNXN0bktFcldHVEhHMFlhUXN0MmYvZ05QbEhKUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "cXwsjmUgMBWevJTnvjoJHkcpr4Lq9EynnWDVpHTuVT+981Dwfz9Tb77Ct9PrlpH2tmLAPa7Men6fweM96FHixKZprq4fpDtdS8/uYpwR/R1A3YJ2PV/7Z5tpnbcsK7vzWv5FFfu7ExiDhiyo3BO0tUfzmgIj+n6AC4t+Av1H+ezftU5RFvrZRLyXqznb4BgGMFw2hrp492AmgRmkyn7tH0gSH2ov5511xXTLDxb+4FN1pcFMXunegJ/mrTZbAEpA5cwSmFrAG5HF9+1HMaW4xkKngG/RDM1uAqW39cFwullfdwQrfGdWcjP8S3gwZjRBYBuVmmT4I9+iReWtKyffwQ==" + }, + "hashKey": { + "N": "5" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFNMbk1xMlcyM1lMbFZnUG16dUIwSGcySGlrRTE0VUowZlU4cG45SlVVSG0yUmZkNFR4aTd1MkpkWmRUcXB6NVNvOG9XNHBPQkw0ZlJKN09EUXpLN1U4dWZzTDJaa2gwL0U4STZoSVVHa3dlYnhXdll4eEtuUWYzRzd6ZG1BTWVVU0JDWGx5amgyTzFrMXVSZVAvREkzdDBhcThkVFpZVDhGZ1UvaWpPb0tNRzlTdk9SQjJJK0NlL2RPb21KTEhLSTloT2RvSHNsZ3BEUGlFR2JRemtrR2Z5SmJVQWdMN01UQnNnM1NreUFiQWNHa2NKM1lVTk5STDJaczBYdnR0cXdYWWpqRGZjUFFURlY2UXJHNWEzK3l5TzJKUkQ4UlZGcStTcUQrVjBHdGNHZlB3OE5aV1RyNnlnNHh4ejVMSnJuWStZbmo3ZUQ2SmtXMnp1SjZlSUp6Zz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "intSet": { + "B": "8ZtcMSFyLVWShX6ppvvQSieBh7o22ZgvG2W9YnfvAl0Skl8MSs5ARJBSFjfwu1ZUkkAu4TkSVDzYHQ6OyVqY3Q==" + }, + "stringValue": { + "B": "Oai1ObEsra4j02oNBT/sjPPbs90yHVhv1sj/+JWeLADZb0BcIgjI/YZpJf16khFf" + }, + "stringSet": { + "B": "vzb/AuXZQzfsY6g9eCX1bfnIaNrP4AmLmsEsG9c2vhV0DsRUBuJ7A5eRJCUkS6M3V41+kL1wl0kPQ2KE2ldVMg==" + }, + "rangeKey": { + "N": "7" + }, + "byteArrayValue": { + "B": "KAywNi4WcQfbA327kGuM/gvIezcA4/jBlnZDXhl8M+U=" + }, + "intValue": { + "B": "ZmWF+am2vPEV2IkuCb/6ULBiGYSvjR6OwCcQxfQ4uS8=" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "jKVKU8uHbhAg8vlU8WqK3qIss6XKPJQXATVwFlkqw5N7RMj0yjQWQ5pJC81sdkXp3NmIgF9Wnavzl5TEVB6R4v/cwxT85ih/kMN7NDOXU5OEkQUlzCRCZ3U6wVvWgFbbI68r42LNPav+uuWBB2/cp9Uu/4VbsOQC7IjEdWIPkir+5BP7HBFg78cs9YgpkDuw2J8+4KLj4z5CsSW6dPjhmbPolKmhn8DinezJ6bHpRFmP0ry75HxMUTu2wInwHD0mCpK1TXWJ3t8V1+UJkNHHpD6j78UhNH9Ky2h9pgj+7Gml0pnZ9t0skUCXNcBLf0Pj3RsqvQuYrU6f2tV8DDxm8g==" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFEzcXNQenFUZ1hJUStDYTVTakxJaXNuWXZEbEx0cHRId2Y5WFpNV1AyVzhFVEk3WG9tOGlaN3o2eE1KSjBrM1JYVFJ4ZEZWcHByVXlRc1owTnBYdElYTTdSSUtxZHkra0U2UUtqVk1FampHKzhxa0pHUmZTY0NraUZrQW1VRmRIWHFoVWRtTFFiUDVSYTc0RjBzSU9GU1JJd1Qrcy92cS9kNG5ialpkUEg0OGp1TE1lS2ovUEtrUkx0QzA4Qzl3bTdBMmxDTXc1SndMK1paY2JZc0RnY1c0UmFPcE1QeUtTWTZmc05QMHBlc1NuUDZjTkY1ODZ4Q0lYYkhpQ2dqbjN6dC9GbGxIbWR6UDhocUVwWlRibWFyOThWM0ZOOWczWXpaVm4rSXJTUWZ0V0VxYlpDWUxoZDBWRjdXaGNNZU85YXNIenJhVU5tUS92cmVLelI3QWJNUT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "2" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "jyMvaawr9SVz2o2aZu9DTapsym/AbELIdz8JPKoWFbxmEQqmUmf1jaMDC91xLfBw5b9qjeCEgb0FmsSXtnT8QhrGrzsCYGwiAq8DVg4rxnSRGdILKzWeYpxJO1Laf1vzow5DIz2FWxMSaRzVYh8nGPoPdO8XiPWb9tp7uLF+s0vjt6UuGcwR+4aOw2Dr+1xL2jO18uKrCxGPtXUP5D0ThZctwiQaamP2Nz1RSHGGatKU2zPcMzp6zoIPIjdnMqX1g28La2IR2mxR0Miq7Mit5EkqYk+s7XdYlTPNhVXifJu9iHvFmsLtc/hJryvLG7hrzI3Bzg2b6mkdL7oVuH2wGQ==" + }, + "hashKey": { + "N": "6" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWFVMWk1LekJhWnV4WHgxSDIvSUtiSHFMMnIwU3lJc1NTTnBWSkxuTlpXM2s0VzYvZkZ1SW5CaHdJdkxLUXFmY2tzcWtTSUFNbzdnd29KUVpneXZzUTZpeFRpS0ZuT1VFMS9aM3E0YnVjSGhKa3VOdmhFcExrZlYxNzg0QW93WFQzYVNEcGtuNUllc2p0ODRZVGdOVEEzUFBGSGZYMktaclBOT0tJWFdTUWtaQk8rK0Zvb0laQW5xQjkxZGs2NFZCMFRyTnRVbXYxcTlkbHgyWklqcFlIUU96aWZha1QzcWV4WVNLRStHSUs2cGdGSzd6dTBMTHRTREpVdnZUcHArSy9xNzMyVjdGY0dhK3VEY2NsUEM4SFdEaWEwN0RNa0hQN3o4WGkwdHcrM3VWU0NmVlA4R2YvUTIrODhyM0x0djRWRms1YUM4OUtlcEVNditZTlBhZXBXZz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "intSet": { + "B": "S+mU+2GCOyieXTCnXN6EW0aJ2q1u0lzYR+0Klp3ie7JKzT50zgGIfTTF7mL4UglTVEBsUGuTDB1InROrNkpU+A==" + }, + "stringValue": { + "S": "Hello world!" + }, + "stringSet": { + "B": "ROVxXPL8ZIBOjE+SqmxNuWymmlubdUnMnlVzVAcZvWYQFtkQf7Fw7tgHYILDcn9x1MEsqq60wupRhyRLCJD7hQ==" + }, + "rangeKey": { + "N": "8" + }, + "byteArrayValue": { + "B": "cEXPxkOed6M21vOTq5vi0KArgXdhtumzZkTxLH39N3w=" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "UKESqnTKdCqAtM6aDkJGg068ssNWFv811njBVuRK7mzVtmIG5OxLQKr8ycBf/Zm3j2fDnkeLnZwc/Fya9XCTygte4yy1QZSywrSb83uhGFlmLsjGOKcE5ZTMPEMb75+I+8I8OQ3ggfM3EnyaTFQCIfeY+3antQ3augrWioBaoJ3VpoUU+RSA6FOrlVtd01qNO2ZOXCfcX5soh2r60FXZ3fdJZJKvO61xkf4nlZJQkc175bsV8KRHh+125a/KETb+3Gc8uL2aRFBO03fuSCHS97YN7nbevtzM/WdqfXh83N0sBIibHhY73xd5n1sDwKhn9D3madRlzlj6GgwiY6wOqQ==" + }, + "hashKey": { + "N": "7" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWHRoUXFMR29QbkJydUNBa3FpeEROY3g5VEYycEIzZnRobmdZZ21NT3RUQ3pHSmtodVZxbzR6bnRoYzEyZkdjR05DY3UyRit1WC9NRTZ4bFY4UU8zblRjQVlXLzlScWw0UXo1OXB3dXBFV1NhR05aTDhVSlQ2UHhaa2w3WElTMzJvbnhuRVd4ZEJsR2U1a3RadFdSaUN6b0t5S3FZMXRCaHJKcVdWU2xsL0tYY3l0ci9FMk14WjRGT2NZRjlTVmdEeDIvSGtJL2VXaEpkbWFMWU16d3J3RWR0S295aXNwVVZLbUN3T2QxNFYvRGNSQkczM0VpK3hQbWtGOE8xTXJIMG5zeFBCQW0wYXQ3azJnQ0ZBMTBTUmJ3ejhFZFpMeTVlcS9HVTUzaFNuaHlnUmUvT2toWHdWMG5qMlU2YTYyd0lzVWhQdllVR2JIU0VzTkMzNVJmbDkyQT09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "rangeKey": { + "N": "3" + } + }, + { + "hashKey": { + "N": "7" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "rangeKey": { + "N": "9" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + }, + { + "*amzn-ddb-map-sig*": { + "B": "fuTnJmdj2YTv+7PSKT/hVA/HaYJZCuXquvdCFafntAtjNqcQI371menVgbKHLKYZsqaTrCEHskNESd8qzXjJup2uOYdJOl01OKc0qasI+a0XwQspILlhIBo+TJ91/XyUTbqvRExXv+yJ3S1AY7vQmqIIumzy6kcVk0IR0pJjyqCKLbWumJdR+NgITuaowVMGi4BrjE8W3/ucJnB1yh1MZ5kQlINCgW+80MdEmCtxkZ9Lq40CMlhtXoXXXKNtZ5vh/TK0IDEoDMBQKMv3/MoSDxmRjkwCVRjJaD4ofpbmOaubInuVhMMQ2gPkQ8oxNRaNxoqhMv44rgry/sThLLOt7A==" + }, + "hashKey": { + "N": "8" + }, + "*amzn-ddb-map-desc*": { + "B": "AAAAAAAAABdhbXpuLWRkYi1tYXAtc2lnbmluZ0FsZwAAAA1TSEEyNTZ3aXRoUlNBAAAAEGFtem4tZGRiLWVudi1hbGcAAAADQUVTAAAAFWFtem4tZGRiLW1hcC1zeW0tbW9kZQAAABEvQ0JDL1BLQ1M1UGFkZGluZwAAABBhbXpuLWRkYi1lbnYta2V5AAABWG0xKzA3d2o4dUJZYTJyeEs4TkVBUUdnRmVDcWtqYlp5bndMdk92K1ZkdmVsWjZ2bkVqMHI0cnNFbG9SeVdocU5YZWJuMDBReXI0QVhsQW9aektFQWRPTkhxL3A3RWwxdnZ3VHB5NnpmYzF3dkhvc1RVdlRTLy9wRlRvaHptclNsTkFtNFVCb0JLNjJsbUZKTWlYUy9EakNoVzgxQmRvbFZHOHZUb25tTUhJS2Q5RFlJVmtxYUJBTUdYaENuc1NIQWpCUmxQWk5XU3pTZnQyRGc4dGx0b3poY3FwTitsZHNYdTVJSjZoUk44RjlXcW9hUUJLYVY1QXRDdG92dm1BWjU4c0l1SDZnVjVWSllSL0ZzUmZBUlo2UzNJWlErQ2ZsNzRXRXpueElJa2IwWElDMmc1enhTYlhFL0NTYW1pVTIrNm92NjQrSFlvR2tkWEw2ZmpDTUtoZz09AAAAEWFtem4tZGRiLXdyYXAtYWxnAAAAJVJTQS9FQ0IvT0FFUFdpdGhTSEEtMjU2QW5kTUdGMVBhZGRpbmc=" + }, + "intSet": { + "NS": [ + "0", + "1", + "15", + "1E+1", + "2E+2" + ] + }, + "stringValue": { + "S": "Hello world!" + }, + "stringSet": { + "SS": [ + "?", + "Cruel", + "Goodbye", + "World" + ] + }, + "rangeKey": { + "N": "1E+1" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "intValue": { + "N": "123" + }, + "version": { + "N": "1" + } + } + ] +} diff --git a/test/vectors/encrypted_item/keys.json b/test/vectors/encrypted_item/keys.json new file mode 100644 index 00000000..8dbb365d --- /dev/null +++ b/test/vectors/encrypted_item/keys.json @@ -0,0 +1,26 @@ +{ + "aesKey": { + "material": "AAECAwQFBgcICQoLDA0ODw==", + "algorithm": "AES", + "encoding": "RAW", + "type": "symmetric" + }, + "hmacKey": { + "material": "AAECAwQFBgc=", + "algorithm": "HmacSHA256", + "encoding": "RAW", + "type": "symmetric" + }, + "rsaEncPub": { + "material": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtiNSLSvT9cExXOcD0dGZ9DFEMHw8895gAZcCdSppDrxbD7XgZiQYTlgt058i5fS+l11guAUJtKt5sZ2u8Fx0K9pxMdlczGtvQJdx/LQETEnLnfzAijvHisJ8h6dQOVczM7t01KIkS24QZElyO+kYqMWLytUV4RSHnrnIuUtPHCe6LieDWT2+1UBguxgtFt1xdXlquACLVv/Em3wp40XcbIwzhqLitb98rTY/wqSiGTz1uvvBX46n+f2j3geZKCEDGkWcXYw3dH4lRtDWTbqweRcaNDT/MJswQlBk/Up9KCyN7gjX67gttiCO6jMoTNDejGeJhG4Dd2o0vmn8WJlr5wIDAQAB", + "algorithm": "SHA256withRSA", + "encoding": "DER", + "type": "public" + }, + "rsaEncPriv": { + "material": "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2I1ItK9P1wTFc5wPR0Zn0MUQwfDzz3mABlwJ1KmkOvFsPteBmJBhOWC3TnyLl9L6XXWC4BQm0q3mxna7wXHQr2nEx2VzMa29Al3H8tARMScud/MCKO8eKwnyHp1A5VzMzu3TUoiRLbhBkSXI76RioxYvK1RXhFIeeuci5S08cJ7ouJ4NZPb7VQGC7GC0W3XF1eWq4AItW/8SbfCnjRdxsjDOGouK1v3ytNj/CpKIZPPW6+8Ffjqf5/aPeB5koIQMaRZxdjDd0fiVG0NZNurB5Fxo0NP8wmzBCUGT9Sn0oLI3uCNfruC22II7qMyhM0N6MZ4mEbgN3ajS+afxYmWvnAgMBAAECggEBAIIU293zDWDZZ73oJ+w0fHXQsdjHAmlRitPX3CN99KZXk9m2ldudL9bUV3Zqk2wUzgIg6LDEuFfWmAVojsaP4VBopKtriEFfAYfqIbjPgLpTgh8FoyWW6D6MBJCFyGALjUAHQ7uRScathvt5ESMEqV3wKJTmdsfX97w/B8J+rLN33fT3ZJUck5duZ8XKD+UtX1Y3UE1hTWo3Ae2MFND964XyUqy+HaYXjH0x6dhZzqyJ/OJ/MPGeMJgxp+nUbMWerwxrLQceNFVgnQgHj8e8k4fd04rkowkkPua912gNtmz7DuIEvcMnY64z585cn+cnXUPJwtu3JbAmn/AyLsV9FLECgYEA798Ut/r+vORB16JDKFu38pQCgIbdCPkXeI0DC6u1cW8JFhgRqi+AqSrEy5SzY3IY7NVMSRsBI9Y026BlR9OQwTrOzLRAw26NPSDvbTkeYXlY9+hX7IovHjGkho/OxyTJ7bKRDYLoNCz56BC1khIWvECpcf/fZU0nqOFVFqF3H/UCgYEAwmJ4rjl5fksTNtNRL6ivkqkHIPKXzk5wC+L90HKNicic9bqyX8K4JRkGKSNYN3mkjrguAzUlEld390qNBw5Lu7PwATv0e2i+6hdwJsjTKNpj7Nh4Mieq6d7lWe1L8FLyHEhxgIeQ4BgqrVtPPOH8IBGpuzVZdWwIdgOvEvAi/usCgYBdfk3NB/+SEEW5jn0uldE0s4vmHKq6fJwxWIT/X4XxGJ4qBmecNbeoOAtMbkEdWbNtXBXHyMbA+RTRJctUG5ooNou0Le2wPr6+PMAVilXVGD8dIWpjv9htpFvENvkZlbU++IKhCY0ICR++3ARpUrOZ3Hou/NRN36y9nlZT48tSoQKBgES2Bi6fxmBsLUiN/f64xAc1lH2DA0I728N343xRYdK4hTMfYXoUHH+QjurvwXkqmI6ScEFWAdqv7IoPYjaCSSb6ffYRuWP+LK4WxuAO0QV53SSViDdCalntHmlhRhyXVVnGCckDIqT0JfHNev7savDzDWpNe2fUXlFJEBPDqrstAoGBAOpd5+QBHF/tP5oPILH4aD/zmqMH7VtB+b/fOPwtIM+B/WnU7hHLO5t2lJYu18Be3amPkfoQIB7bpkM3Cer2G7Jw+TcHrY+EtIziDB5vwau1fl4VcbA9SfWpBojJ5Ifo9ELVxGiK95WxeQNSmLUy7AJzhK1Gwey8a/v+xfqiu9sE", + "algorithm": "RSA", + "encoding": "DER", + "type": "private" + } +} diff --git a/test/vectors/encrypted_item/plaintext.json b/test/vectors/encrypted_item/plaintext.json new file mode 100644 index 00000000..9791ed93 --- /dev/null +++ b/test/vectors/encrypted_item/plaintext.json @@ -0,0 +1,277 @@ +{ + "actions": { + "mixed": { + "default": "encrypt", + "override": { + "version": "sign", + "stringValue": "sign", + "doubleValue": "sign", + "doubleSet": "sign", + "intValue": "nothing" + } + }, + "encrypt": { + "default": "encrypt", + "override": { + "version": "sign" + } + }, + "sign": { + "default": "sign" + }, + "nothing": { + "default": "nothing" + } + }, + "items": { + "TableName": { + "index": { + "partition": "hashKey", + "sort": "rangeKey" + }, + "items": [ + { + "attributes": { + "hashKey": { + "N": "0" + }, + "rangeKey": { + "N": "1" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "0" + }, + "rangeKey": { + "N": "2" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "0" + }, + "rangeKey": { + "N": "3" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "1" + }, + "rangeKey": { + "N": "1" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "1" + }, + "rangeKey": { + "N": "2" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "1" + }, + "rangeKey": { + "N": "3" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "5" + }, + "rangeKey": { + "N": "1" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "6" + }, + "rangeKey": { + "N": "2" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "7" + }, + "rangeKey": { + "N": "3" + } + }, + "action": "encrypt", + "exact": true + }, + { + "attributes": { + "hashKey": { + "N": "5" + }, + "rangeKey": { + "N": "7" + } + }, + "action": "encrypt" + }, + { + "attributes": { + "hashKey": { + "N": "6" + }, + "rangeKey": { + "N": "8" + } + }, + "action": "mixed" + }, + { + "attributes": { + "hashKey": { + "N": "8" + }, + "rangeKey": { + "N": "10" + } + }, + "action": "sign" + }, + { + "attributes": { + "hashKey": { + "N": "7" + }, + "rangeKey": { + "N": "9" + } + }, + "action": "nothing" + } + ] + }, + "HashKeyOnly": { + "index": { + "partition": "hashKey" + }, + "items": [ + { + "attributes": { + "hashKey": { + "S": "Foo" + } + }, + "action": "encrypt" + }, + { + "attributes": { + "hashKey": { + "S": "Bar" + } + }, + "action": "encrypt" + }, + { + "attributes": { + "hashKey": { + "S": "Baz" + } + }, + "action": "encrypt" + } + ] + } + }, + "versions": { + "TableName": { + "v0": [ + "base" + ], + "v1": [ + "base", + "doubles" + ] + } + }, + "attributes": { + "base": { + "stringValue": { + "S": "Hello world!" + }, + "intValue": { + "N": "123" + }, + "byteArrayValue": { + "B": "AAECAwQF" + }, + "stringSet": { + "SS": [ + "Goodbye", + "Cruel", + "World", + "?" + ] + }, + "intSet": { + "NS": [ + "1", + "200", + "10", + "15", + "0" + ] + }, + "version": { + "N": "0" + } + }, + "doubles": { + "doubleValue": { + "N": "15" + }, + "doubleSet": { + "NS": [ + "15", + "7.6", + "-3", + "-34.2", + "0" + ] + } + } + } +} diff --git a/test/vectors/encrypted_item/scenarios.json b/test/vectors/encrypted_item/scenarios.json new file mode 100644 index 00000000..3bfe1ec7 --- /dev/null +++ b/test/vectors/encrypted_item/scenarios.json @@ -0,0 +1,66 @@ +{ + "plaintext": "file://plaintext.json", + "keys": "file://keys.json", + "scenarios": [ + { + "version": "v0", + "provider": "static", + "keys": { + "decrypt": "aesKey", + "verify": "hmacKey" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/static-aes-hmac-1.json" + }, + { + "version": "v0", + "provider": "static", + "keys": { + "decrypt": "aesKey", + "verify": "hmacKey" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/static-aes-hmac-2.json" + }, + { + "version": "v1", + "provider": "static", + "keys": { + "decrypt": "aesKey", + "verify": "hmacKey" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/static-aes-hmac-3.json" + }, + { + "version": "v0", + "provider": "wrapped", + "keys": { + "decrypt": "rsaEncPriv", + "verify": "rsaEncPub" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/wrapped-rsa-rsa-1.json" + }, + { + "version": "v1", + "provider": "wrapped", + "keys": { + "decrypt": "rsaEncPriv", + "verify": "rsaEncPub" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/wrapped-rsa-rsa-2.json" + }, + { + "version": "v0", + "provider": "wrapped", + "keys": { + "decrypt": "rsaEncPriv", + "verify": "rsaEncPub" + }, + "plaintext": "file://plaintext.json", + "ciphertext": "file://ciphertext/wrapped-rsa-rsa-3.json" + } + ] +} diff --git a/test/vectors/material_description.json b/test/vectors/material_description.json new file mode 100644 index 00000000..e0dc199b --- /dev/null +++ b/test/vectors/material_description.json @@ -0,0 +1,22 @@ +[ + { + "material_description": { + "A": "Field A", + "B": "Field B", + "C": "123" + }, + "serialized": "AAAAAAAAAAFBAAAAB0ZpZWxkIEEAAAABQgAAAAdGaWVsZCBCAAAAAUMAAAADMTIz" + }, + { + "material_description": { + "B": "Field B", + "C": "123", + "A": "Field A" + }, + "serialized": "AAAAAAAAAAFBAAAAB0ZpZWxkIEEAAAABQgAAAAdGaWVsZCBCAAAAAUMAAAADMTIz" + }, + { + "material_description": {}, + "serialized": "AAAAAA==" + } +] \ No newline at end of file diff --git a/test/vectors/serialize_attribute.json b/test/vectors/serialize_attribute.json new file mode 100644 index 00000000..aa71a5d2 --- /dev/null +++ b/test/vectors/serialize_attribute.json @@ -0,0 +1,220 @@ +[ + { + "attribute": {"NULL": true}, + "serialized": "AAA=" + }, + { + "attribute": {"BOOL": true}, + "serialized": "AD8B" + }, + { + "attribute": {"BOOL": false}, + "serialized": "AD8A" + }, + { + "attribute": {"N": "55"}, + "serialized": "AG4AAAACNTU=" + }, + { + "attribute": {"N": "55.00"}, + "serialized": "AG4AAAACNTU=" + }, + { + "attribute": {"N": "5.5E+1"}, + "serialized": "AG4AAAACNTU=" + }, + { + "attribute": {"N": "55.34"}, + "serialized": "AG4AAAAFNTUuMzQ=" + }, + { + "attribute": {"N": "55.34000"}, + "serialized": "AG4AAAAFNTUuMzQ=" + }, + { + "attribute": { + "NS": [ + "55.2", + "34", + "35.0", + "1.230" + ] + }, + "serialized": "AE4AAAAEAAAABDEuMjMAAAACMzQAAAACMzUAAAAENTUuMg==" + }, + { + "attribute": { + "NS": [ + "1.230", + "34", + "35.0", + "55.2" + ] + }, + "serialized": "AE4AAAAEAAAABDEuMjMAAAACMzQAAAACMzUAAAAENTUuMg==" + }, + { + "attribute": {"S": "test ascii string"}, + "serialized": "AHMAAAARdGVzdCBhc2NpaSBzdHJpbmc=" + }, + { + "attribute": { + "SS": [ + "test ascii string", + "another ascii string" + ] + }, + "serialized": "AFMAAAACAAAAFGFub3RoZXIgYXNjaWkgc3RyaW5nAAAAEXRlc3QgYXNjaWkgc3RyaW5n" + }, + { + "attribute": { + "SS": [ + "another ascii string", + "test ascii string" + ] + }, + "serialized": "AFMAAAACAAAAFGFub3RoZXIgYXNjaWkgc3RyaW5nAAAAEXRlc3QgYXNjaWkgc3RyaW5n" + }, + { + "attribute": {"B": "AAECAw=="}, + "serialized": "AGIAAAAEAAECAw==" + }, + { + "attribute": {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + "serialized": "AGIAAAAUYW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + }, + { + "attribute": { + "BS": [ + "YW4gYXNjaWkgYnl0ZSBzdHJpbmc=", + "AAECAw==" + ] + }, + "serialized": "AEIAAAACAAAABAABAgMAAAAUYW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + }, + { + "attribute": { + "BS": [ + "AAECAw==", + "YW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + ] + }, + "serialized": "AEIAAAACAAAABAABAgMAAAAUYW4gYXNjaWkgYnl0ZSBzdHJpbmc=" + }, + { + "attribute": { + "L": [ + {"N": "55.34"}, + {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + {"S": "test ascii string"}, + {"BOOL": false} + ] + }, + "serialized": "AEwAAAAEAG4AAAAFNTUuMzQAYgAAABRhbiBhc2NpaSBieXRlIHN0cmluZwBzAAAAEXRlc3QgYXNjaWkgc3RyaW5nAD8A" + }, + { + "attribute": { + "L": [ + {"S": "test ascii string"}, + {"S": "test ascii string"}, + {"S": "test ascii string"} + ] + }, + "serialized": "AEwAAAADAHMAAAARdGVzdCBhc2NpaSBzdHJpbmcAcwAAABF0ZXN0IGFzY2lpIHN0cmluZwBzAAAAEXRlc3QgYXNjaWkgc3RyaW5n" + }, + { + "attribute": { + "M": { + "one thing": {"NULL": true}, + "maybe a bool?": {"BOOL": false}, + "and a list too": { + "L": [ + {"N": "55.34"}, + {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + {"S": "test ascii string"} + ] + } + } + }, + "serialized": "AE0AAAADAHMAAAAOYW5kIGEgbGlzdCB0b28ATAAAAAMAbgAAAAU1NS4zNABiAAAAFGFuIGFzY2lpIGJ5dGUgc3RyaW5nAHMAAAARdGVzdCBhc2NpaSBzdHJpbmcAcwAAAA1tYXliZSBhIGJvb2w/AD8AAHMAAAAJb25lIHRoaW5nAAA=" + }, + { + "attribute": { + "M": { + "and a list too": { + "L": [ + {"N": "55.34"}, + {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + {"S": "test ascii string"} + ] + }, + "maybe a bool?": {"BOOL": false}, + "one thing": {"NULL": true} + } + }, + "serialized": "AE0AAAADAHMAAAAOYW5kIGEgbGlzdCB0b28ATAAAAAMAbgAAAAU1NS4zNABiAAAAFGFuIGFzY2lpIGJ5dGUgc3RyaW5nAHMAAAARdGVzdCBhc2NpaSBzdHJpbmcAcwAAAA1tYXliZSBhIGJvb2w/AD8AAHMAAAAJb25lIHRoaW5nAAA=" + }, + { + "attribute": { + "M": { + "complex_map": {"M": { + "a": {"L": [ + {"S": "asdf"}, + {"N": "99"}, + {"M": { + "c": {"BOOL": true}, + "b": {"NULL": true} + }} + ]} + }}, + "another_key": {"BOOL": false} + } + }, + "serialized": "AE0AAAACAHMAAAALYW5vdGhlcl9rZXkAPwAAcwAAAAtjb21wbGV4X21hcABNAAAAAQBzAAAAAWEATAAAAAMAcwAAAARhc2RmAG4AAAACOTkATQAAAAIAcwAAAAFiAAAAcwAAAAFjAD8B" + }, + { + "attribute": {"M": { + "SingleMap": {"M": { + "FOO": {"S": "BAR"} + }}, + "InnerList": {"L": [ + {"S": "ComplexList"}, + {"N": "5"}, + {"B": "AAECAwQF"}, + {"L": [ + {"BOOL": true}, + {"NULL": true}, + {"NULL": true}, + {"L": [ + {"BOOL": false} + ]}, + {"M": { + "Pink": {"S": "Floyd"}, + "Version": {"N": "1"}, + "Test": {"BOOL": true} + }} + ]}, + {"NULL": true}, + {"M": { + "True": {"BOOL": true}, + "List": {"L": [ + {"N": "5"}, + {"N": "4"}, + {"N": "3"}, + {"N": "2"}, + {"N": "1"} + ]}, + "Map": {"M": { + "Nested": {"BOOL": true} + }} + }} + ]}, + "StringSet": {"SS": [ + "bar", + "baz", + "foo" + ]} + }}, + "serialized": "AE0AAAADAHMAAAAJSW5uZXJMaXN0AEwAAAAGAHMAAAALQ29tcGxleExpc3QAbgAAAAE1AGIAAAAGAAECAwQFAEwAAAAFAD8BAAAAAABMAAAAAQA/AABNAAAAAwBzAAAABFBpbmsAcwAAAAVGbG95ZABzAAAABFRlc3QAPwEAcwAAAAdWZXJzaW9uAG4AAAABMQAAAE0AAAADAHMAAAAETGlzdABMAAAABQBuAAAAATUAbgAAAAE0AG4AAAABMwBuAAAAATIAbgAAAAExAHMAAAADTWFwAE0AAAABAHMAAAAGTmVzdGVkAD8BAHMAAAAEVHJ1ZQA/AQBzAAAACVNpbmdsZU1hcABNAAAAAQBzAAAAA0ZPTwBzAAAAA0JBUgBzAAAACVN0cmluZ1NldABTAAAAAwAAAANiYXIAAAADYmF6AAAAA2Zvbw==" + } +] \ No newline at end of file diff --git a/test/vectors/string_to_sign.json b/test/vectors/string_to_sign.json new file mode 100644 index 00000000..9d34790f --- /dev/null +++ b/test/vectors/string_to_sign.json @@ -0,0 +1,60 @@ +[ + { + "table": "ExampleTableName", + "item": { + "a_number": { + "value": {"N": "55"}, + "action": "encrypt" + } + }, + "string_to_sign": "9sgaD2yFEAmIplAUwJS+KesIRv1Xw0Q/DVnzVNZqp/U4+5C1DIfNg97Pu2hKprvFT1nzJNuMvsdX9zwUYqSJqjlBFaxOsJqklCk0iBayA4EnsPnjpSA3KuEAyhn7CP1QYD1mapH53y+ljTvr5TAcGBTbydPe4VTANZOIHuHviQ8=" + }, + { + "table": "ExampleTableName", + "item": { + "a_number": { + "value": {"N": "55"}, + "action": "sign" + } + }, + "string_to_sign": "9sgaD2yFEAmIplAUwJS+KesIRv1Xw0Q/DVnzVNZqp/U4+5C1DIfNg97Pu2hKprvFT1nzJNuMvsdX9zwUYqSJqstA59rchhYbl5jeSFEvMy0hwUH8weKKCG/eSjN1qrG1YD1mapH53y+ljTvr5TAcGBTbydPe4VTANZOIHuHviQ8=" + }, + { + "table": "ExampleTableName", + "item": { + "a_number": { + "value": {"N": "55"}, + "action": "nothing" + } + }, + "string_to_sign": "9sgaD2yFEAmIplAUwJS+KesIRv1Xw0Q/DVnzVNZqp/U=" + }, + { + "table": "ExampleTableName", + "item": { + "a_number": { + "value": {"N": "55"}, + "action": "nothing" + }, + "some_string": { + "value": {"S": "test ascii string"}, + "action": "sign" + }, + "bytes": { + "value": {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + "action": "encrypt" + }, + "list_of_stuff": { + "value": { + "L": [ + {"N": "55.34"}, + {"B": "YW4gYXNjaWkgYnl0ZSBzdHJpbmc="}, + {"S": "test ascii string"} + ] + }, + "action": "encrypt" + } + }, + "string_to_sign": "9sgaD2yFEAmIplAUwJS+KesIRv1Xw0Q/DVnzVNZqp/UncInZHAvfTy5oYrp+SgdgURlDH10T9ybdNSsG8bIGqTlBFaxOsJqklCk0iBayA4EnsPnjpSA3KuEAyhn7CP1QvuXxYe5x5afhZUv3xkXlofC611Cw5eAlbNdEaMf1xKGE96hSpUnIMULGeHZTXUI2ydB2gfFa9+W0FInbJtdwEjlBFaxOsJqklCk0iBayA4EnsPnjpSA3KuEAyhn7CP1QyIKWBxib2u7/xzb6bZmy+BHoUuSg5pgwcqIfCofBkyJTmjdP9D3OLolP1AYapUXm9/WXLUDumhZ2kB+5ISX/7stA59rchhYbl5jeSFEvMy0hwUH8weKKCG/eSjN1qrG1bSWEI5OScw844jVePhRaqST4raDsPSjQVLzgN1rEAl8=" + } +] \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..a2daa457 --- /dev/null +++ b/tox.ini @@ -0,0 +1,223 @@ +[tox] +envlist = + py{27,34,35,36}-{local,integ}-full, + bandit, doc8, readme, + flake8, pylint, + flake8-tests, pylint-tests, + vulture + +# Additional environments: +# vulture :: Runs vulture. Prone to false-positives. +# linters :: Runs all linters over all source code. +# linters-tests :: Runs all linters over all tests. + +[testenv:base-command] +commands = pytest --basetemp={envtmpdir} -l --cov dynamodb_encryption_sdk {posargs} + +[testenv] +passenv = + # Identifies AWS KMS key id to use in integration tests + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID \ + # Pass through AWS credentials + AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN \ + # Pass through AWS profile name (useful for local testing) + AWS_PROFILE +sitepackages = False +deps = + hypothesis + mock + moto + pytest>=3.3.1 + pytest-cov + pytest-mock + pytest-xdist +commands = + local-fast: {[testenv:base-command]commands} -m "local and not slow and not veryslow and not nope" + integ-fast: {[testenv:base-command]commands} -m "integ and not slow and not veryslow and not nope" + all-fast: {[testenv:base-command]commands} -m "not slow and not veryslow and not nope" + local-slow: {[testenv:base-command]commands} -m "local and not veryslow and not nope" + integ-slow: {[testenv:base-command]commands} -m "integ and not veryslow and not nope" + all-slow: {[testenv:base-command]commands} -m "not veryslow and not nope" + local-full: {[testenv:base-command]commands} -m "local and not nope" + integ-full: {[testenv:base-command]commands} -m "integ and not nope" + all-full: {[testenv:base-command]commands} -m "not nope" + local-nope: {[testenv:base-command]commands} -m "local and nope" + integ-nope: {[testenv:base-command]commands} -m "integ and nope" + all-nope: {[testenv:base-command]commands} -m "nope" + +# mypy +[testenv:mypy-coverage] +commands = + # Make mypy linecoverage report readable by coverage + python -c \ + "t = open('.coverage', 'w');\ + c = open('build/coverage.json').read();\ + t.write('!coverage.py: This is a private format, don\'t read it directly!\n');\ + t.write(c);\ + t.close()" + coverage report -m + +[testenv:mypy-py3] +basepython = python3 +deps = + coverage + mypy + mypy_extensions + typing>=3.6.2 +commands = + python -m mypy \ + --linecoverage-report build \ + src/dynamodb_encryption_sdk/ + {[testenv:mypy-coverage]commands} + +[testenv:mypy-py2] +basepython = python3 +deps = + coverage + mypy + mypy_extensions + typing>=3.6.2 +commands = + python -m mypy \ + --py2 \ + --linecoverage-report build \ + src/dynamodb_encryption_sdk/ + {[testenv:mypy-coverage]commands} + +# Linters +[testenv:flake8] +basepython = python3 +deps = + flake8 + flake8-docstrings + flake8-import-order + # https://github.com/JBKahn/flake8-print/pull/30 + flake8-print>=3.1.0 +commands = + flake8 \ + src/dynamodb_encryption_sdk/ \ + setup.py \ + doc/conf.py + +[testenv:flake8-tests] +basepython = {[testenv:flake8]basepython} +deps = {[testenv:flake8]deps} +commands = + flake8 \ + # Ignore F811 redefinition errors in tests (breaks with pytest-mock use) + # Ignore D103 docstring requirements for tests + --ignore F811,D103 \ + test/ + +[testenv:pylint] +basepython = python3 +deps = + {[testenv]deps} + pyflakes + pylint +commands = + pylint \ + --rcfile=src/pylintrc \ + src/dynamodb_encryption_sdk/ \ + setup.py \ + doc/conf.py + +[testenv:pylint-tests] +basepython = {[testenv:pylint]basepython} +deps = {[testenv:pylint]deps} +commands = + pylint \ + --rcfile=test/pylintrc \ + test/unit/ + +[testenv:doc8] +basepython = python3 +deps = + sphinx + doc8 +commands = doc8 doc/index.rst README.rst CHANGELOG.rst + +[testenv:readme] +basepython = python3 +deps = readme_renderer +commands = python setup.py check -r -s + +[testenv:bandit] +basepython = python3 +deps = bandit +commands = bandit -r src/dynamodb_encryption_sdk/ + +# Prone to false positives: only run independently +[testenv:vulture] +basepython = python3 +deps = vulture +commands = vulture src/dynamodb_encryption_sdk/ + +[testenv:linters] +basepython = python3 +deps = + {[testenv:flake8]deps} + {[testenv:pylint]deps} + {[testenv:doc8]deps} + {[testenv:readme]deps} + {[testenv:bandit]deps} +commands = + {[testenv:flake8]commands} + {[testenv:pylint]commands} + {[testenv:doc8]commands} + {[testenv:readme]commands} + {[testenv:bandit]commands} + +[testenv:linters-tests] +basepython = python3 +deps = + {[testenv:flake8-tests]deps} + {[testenv:pylint-tests]deps} +commands = + {[testenv:flake8-tests]commands} + {[testenv:pylint-tests]commands} + +# Documentation +[testenv:docs] +basepython = python3 +deps = -rdoc/requirements.txt +commands = + sphinx-build -E -c doc/ -b html doc/ doc/build/html + +[testenv:serve-docs] +basepython = python3 +skip_install = true +changedir = doc/build/html +deps = +commands = + python -m http.server {posargs} + +# Release tooling +[testenv:build] +basepython = python3 +skip_install = true +deps = + wheel + setuptools +commands = + python setup.py sdist bdist_wheel + +[testenv:test-release] +basepython = python3 +skip_install = true +deps = + {[testenv:build]deps} + twine +commands = + {[testenv:build]commands} + twine upload --skip-existing --repository testpypi dist/* + +[testenv:release] +basepython = python3 +skip_install = true +deps = + {[testenv:build]deps} + twine +commands = + {[testenv:build]commands} + twine upload --skip-existing --repository pypi dist/*