# 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."""
import base64
import copy
from typing import Dict, Optional, Text
import attr
import six
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 EncryptionKeyType
from dynamodb_encryption_sdk.internal.identifiers import MaterialDescriptionKeys
from dynamodb_encryption_sdk.internal.validators import dictionary_validator
from dynamodb_encryption_sdk.materials import CryptographicMaterials
__all__ = ("WrappedCryptographicMaterials",)
_DEFAULT_CONTENT_ENCRYPTION_ALGORITHM = "AES/256"
_WRAPPING_TRANSFORMATION = {"AES": "AESWrap", "RSA": "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"}
[docs]@attr.s(init=False)
class WrappedCryptographicMaterials(CryptographicMaterials):
"""Encryption/decryption key is a content key stored in the material description, wrapped
by the wrapping key.
:param DelegatedKey signing_key: Delegated key used as signing and verification key
:param DelegatedKey wrapping_key: Delegated key used to wrap content key
.. note::
``wrapping_key`` must be provided if material description contains a wrapped content key
:param DelegatedKey unwrapping_key: Delegated key used to unwrap content key
.. 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=dictionary_validator(six.string_types, six.string_types),
converter=copy.deepcopy,
default=attr.Factory(dict),
)
def __init__(
self,
signing_key, # type: DelegatedKey
wrapping_key=None, # type: Optional[DelegatedKey]
unwrapping_key=None, # type: Optional[DelegatedKey]
material_description=None, # type: Optional[Dict[Text, Text]]
): # noqa=D107
# type: (...) -> None
# Workaround pending resolution of attrs/mypy interaction.
# https://github.com/python/mypy/issues/2088
# https://github.com/python-attrs/attrs/issues/215
if material_description is None:
material_description = {}
self._signing_key = signing_key
self._wrapping_key = wrapping_key
self._unwrapping_key = unwrapping_key
self._material_description = material_description
attr.validate(self)
self.__attrs_post_init__()
def __attrs_post_init__(self):
"""Prepare the content key."""
self._content_key_algorithm = self.material_description.get( # pylint: disable=attribute-defined-outside-init
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()
) # noqa pylint: disable=attribute-defined-outside-init
else:
(
self._content_key,
self._material_description,
) = self._generate_content_key() # noqa pylint: disable=attribute-defined-outside-init
@staticmethod
def _wrapping_transformation(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: 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=EncryptionKeyType.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 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])
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: DelegatedKey
"""
return self._content_key
@property
def decryption_key(self):
"""Content key used for decrypting attributes.
:returns: Decryption key
:rtype: DelegatedKey
"""
return self._content_key
@property
def signing_key(self):
"""Delegated key used for calculating digital signatures.
:returns: Signing key
:rtype: DelegatedKey
"""
return self._signing_key
@property
def verification_key(self):
"""Delegated key used for verifying digital signatures.
:returns: Verification key
:rtype: DelegatedKey
"""
return self._signing_key