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/*