Exemple #1
0
class EncryptionContext(object):
    # pylint: disable=too-few-public-methods
    """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 as a DynamoDB JSON dictionary
    :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)
    attributes = attr.ib(
        repr=False,
        validator=(dictionary_validator(six.string_types, dict),
                   _validate_attribute_values_are_ddb_items),
        default=attr.Factory(dict),
    )
    material_description = attr.ib(
        validator=dictionary_validator(six.string_types, six.string_types),
        converter=copy.deepcopy,
        default=attr.Factory(dict),
    )

    def __init__(
            self,
            table_name=None,  # type: Optional[Text]
            partition_key_name=None,  # type: Optional[Text]
            sort_key_name=None,  # type: Optional[Text]
            attributes=None,  # type: Optional[Dict[Text, Dict]]
            material_description=None,  # type: Optional[Dict[Text, Text]]
    ):  # 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 attributes is None:
            attributes = {}
        if material_description is None:
            material_description = {}

        self.table_name = table_name
        self.partition_key_name = partition_key_name
        self.sort_key_name = sort_key_name
        self.attributes = attributes
        self.material_description = material_description
        attr.validate(self)
Exemple #2
0
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(validator=iterable_validator(
        tuple, six.string_types),
                            default=attr.Factory(tuple))
    _material_description = attr.ib(validator=dictionary_validator(
        six.string_types, six.string_types),
                                    default=attr.Factory(dict))
    _regional_clients = attr.ib(validator=dictionary_validator(
        six.string_types, botocore.client.BaseClient),
                                default=attr.Factory(dict))

    def __init__(
            self,
            key_id,  # type: Text
            botocore_session=None,  # type: Optional[botocore.session.Session]
            grant_tokens=None,  # type: Optional[Tuple[Text]]
            material_description=None,  # type: Optional[Dict[Text, Text]]
            regional_clients=None,  # type: Optional[Dict[Text, botocore.client.BaseClient]]
    ):  # 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 botocore_session is None:
            botocore_session = botocore.session.Session()
        if grant_tokens is None:
            # reassignment confuses mypy
            grant_tokens = ()  # type: ignore
        if material_description is None:
            material_description = {}
        if regional_clients is None:
            regional_clients = {}

        self._key_id = key_id
        self._botocore_session = botocore_session
        self._grant_tokens = grant_tokens
        self._material_description = material_description
        self._regional_clients = regional_clients
        attr.validate(self)
        self.__attrs_post_init__()

    def __attrs_post_init__(self):
        # type: () -> None
        """Load the content and signing key info."""
        self._user_agent_adding_config = botocore.config.Config(  # pylint: disable=attribute-defined-outside-init
            user_agent_extra=USER_AGENT_SUFFIX)
        self._content_key_info = KeyInfo.from_material_description(  # pylint: disable=attribute-defined-outside-init
            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(  # pylint: disable=attribute-defined-outside-init
            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 = ({})  # type: Dict[Text, botocore.client.BaseClient]  # noqa pylint: disable=attribute-defined-outside-init

    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", config=self._user_agent_adding_config)
        return self._regional_clients[region_name]

    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
        """
        try:
            key_region = key_id.split(":", 4)[3]
            region = key_region
        except IndexError:
            session_region = self._botocore_session.get_config_variable(
                "region")
            if session_region is None:
                raise UnknownRegionError(
                    "No region determinable from key id: {} and no default region found in session"
                    .format(key_id))
            region = session_region
        return self._add_regional_client(region)

    def _select_key_id(self, encryption_context):
        # type: (EncryptionContext) -> Text
        # pylint: disable=unused-argument
        """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 EncryptionContext encryption_context: Encryption context providing information about request
        :returns: Key id to use
        :rtype: str
        """
        return self._key_id

    def _validate_key_id(self, key_id, encryption_context):
        # type: (Text, EncryptionContext) -> None
        # pylint: disable=unused-argument,no-self-use
        """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 EncryptionContext encryption_context: Encryption context providing information about request
        """

    def _attribute_to_value(self, attribute):
        # type: (dynamodb_types.ITEM) -> Text
        """Convert a DynamoDB attribute to a value that can be added to the KMS encryption context.

        :param dict attribute: Attribute to convert
        :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).decode(TEXT_ENCODING)
        if attribute_type in ("S", "N"):
            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[Text, Text]
        """Build the KMS encryption context from the encryption context and key descriptions.

        :param EncryptionContext encryption_context: Encryption context providing information about request
        :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:
            try:
                partition_key_attribute = encryption_context.attributes[
                    encryption_context.partition_key_name]
            except KeyError:
                pass
            else:
                kms_encryption_context[
                    encryption_context.
                    partition_key_name] = self._attribute_to_value(
                        partition_key_attribute)

        if encryption_context.sort_key_name is not None:
            try:
                sort_key_attribute = encryption_context.attributes[
                    encryption_context.sort_key_name]
            except KeyError:
                pass
            else:
                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: (EncryptionContext) -> Tuple[bytes, bytes]
        """Generate the initial cryptographic material for use with HKDF.

        :param EncryptionContext encryption_context: Encryption context providing information about request
        :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):
            message = "Failed to generate materials using AWS KMS"
            _LOGGER.exception(message)
            raise WrappingError(message)

    def _decrypt_initial_material(self, encryption_context):
        # type: (EncryptionContext) -> bytes
        """Decrypt an encrypted initial cryptographic material value.

        :param encryption_context: Encryption context providing information about request
        :type encryption_context: 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(
            to_bytes(
                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):
            message = "Failed to unwrap AWS KMS protected materials"
            _LOGGER.exception(message)
            raise UnwrappingError(message)

    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 KeyInfo key_info: Key information to use to calculate encryption key
        :param HkdfInfo hkdf_info: Info to use in HKDF calculation
        :returns: Delegated key to use for encryption and decryption
        :rtype: 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=EncryptionKeyType.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 KeyInfo key_info: Key information to use to calculate encryption key
        :returns: Delegated key to use for encryption and decryption
        :rtype: 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 KeyInfo key_info: Key information to use to calculate HMAC key
        :returns: Delegated key to use for signature calculation and verification
        :rtype: 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 EncryptionContext encryption_context: Encryption context for request
        :returns: Encryption materials
        :rtype: 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 EncryptionContext encryption_context: Encryption context for request
        :returns: Encryption materials
        :rtype: 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:
            to_str(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,
        )
Exemple #3
0
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
Exemple #4
0
class WrappedCryptographicMaterialsProvider(CryptographicMaterialsProvider):
    """Cryptographic materials provider to use ephemeral content encryption keys wrapped by delegated keys.

    :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 providing encryption materials

    :param DelegatedKey unwrapping_key: Delegated key used to unwrap content key

    .. 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)
    _material_description = attr.ib(validator=attr.validators.optional(
        dictionary_validator(six.string_types, six.string_types)),
                                    default=attr.Factory(dict))

    def __init__(
        self,
        signing_key,  # type: DelegatedKey
        wrapping_key=None,  # type: Optional[DelegatedKey]
        unwrapping_key=None,  # type: Optional[DelegatedKey]
        material_description=None  # type: Optional[Dict[Text, Text]]
    ):  # 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)

    def _build_materials(self, encryption_context):
        # type: (EncryptionContext) -> WrappedCryptographicMaterials
        """Construct

        :param EncryptionContext encryption_context: Encryption context for request
        :returns: Wrapped cryptographic materials
        :rtype: WrappedCryptographicMaterials
        """
        material_description = self._material_description.copy()
        material_description.update(encryption_context.material_description)
        return WrappedCryptographicMaterials(
            wrapping_key=self._wrapping_key,
            unwrapping_key=self._unwrapping_key,
            signing_key=self._signing_key,
            material_description=material_description)

    def encryption_materials(self, encryption_context):
        # type: (EncryptionContext) -> WrappedCryptographicMaterials
        """Provide encryption materials.

        :param EncryptionContext encryption_context: Encryption context for request
        :returns: Encryption materials
        :rtype: 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 EncryptionContext encryption_context: Encryption context for request
        :returns: Decryption materials
        :rtype: 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)
Exemple #5
0
class AttributeActions(object):
    """Configuration resource used to determine what action should be taken for a specific attribute.

    :param CryptoAction default_action: Action to take if no specific action is defined in
        ``attribute_actions``
    :param dict attribute_actions: Dictionary mapping attribute names to specific actions
    """

    default_action = attr.ib(validator=attr.validators.instance_of(CryptoAction), default=CryptoAction.ENCRYPT_AND_SIGN)
    attribute_actions = attr.ib(
        validator=dictionary_validator(six.string_types, CryptoAction), default=attr.Factory(dict)
    )

    def __init__(
        self,
        default_action=CryptoAction.ENCRYPT_AND_SIGN,  # type: Optional[CryptoAction]
        attribute_actions=None,  # type: Optional[Dict[Text, CryptoAction]]
    ):  # 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 attribute_actions is None:
            attribute_actions = {}

        self.default_action = default_action
        self.attribute_actions = attribute_actions
        attr.validate(self)
        self.__attrs_post_init__()

    def __attrs_post_init__(self):
        # () -> None
        """Determine if any actions should ever be taken with this configuration and record that for reference."""
        for attribute in ReservedAttributes:
            if attribute.value in self.attribute_actions:
                raise ValueError('No override behavior can be set for reserved attribute "{}"'.format(attribute.value))

        # 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()]))
        no_actions = _unique_actions == set([CryptoAction.DO_NOTHING.name])
        self.take_no_actions = no_actions  # attrs confuses pylint: disable=attribute-defined-outside-init

    def action(self, attribute_name):
        # (text) -> CryptoAction
        """Determine the correct :class:`CryptoAction` to apply to a supplied attribute based
        on this config.

        :param str attribute_name: Attribute for which to determine action
        """
        return self.attribute_actions.get(attribute_name, self.default_action)

    def copy(self):
        # () -> AttributeActions
        """Return 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):
        """Set the appropriate action for the specified indexed attribute names.

        .. warning::

            If you have already set a custom action for any of these attributes, this will
            raise an error.

        .. code::

            Default Action   -> Index Key Action
            DO_NOTHING       -> DO_NOTHING
            SIGN_ONLY        -> SIGN_ONLY
            ENCRYPT_AND_SIGN -> SIGN_ONLY

        :param str *keys: Attribute names to treat as indexed
        :raises InvalidArgumentError: if a custom action was previously set for any specified
            attributes
        """
        for key in keys:
            index_action = min(self.action(key), CryptoAction.SIGN_ONLY)
            try:
                if self.attribute_actions[key] is not index_action:
                    raise InvalidArgumentError(
                        'Cannot overwrite a previously requested action on indexed attribute: "{}"'.format(key)
                    )
            except KeyError:
                self.attribute_actions[key] = index_action

    def contains_action(self, action):
        # (CryptoAction) -> bool
        """Determine if the specified action is a possible action from this configuration.

        :param CryptoAction action: Action to look for
        """
        return action is self.default_action or action in self.attribute_actions.values()

    def __add__(self, other):
        # (AttributeActions) -> AttributeActions
        """Merge 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)
Exemple #6
0
class RawEncryptionMaterials(EncryptionMaterials):
    # inheritance confuses pylint: disable=abstract-method
    """Encryption materials for use directly with delegated keys.

    .. note::

        Not all delegated keys allow use with raw cryptographic materials.

    :param DelegatedKey signing_key: Delegated key used as signing key
    :param DelegatedKey encryption_key: Delegated key used as encryption key
    :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.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
        encryption_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._encryption_key = encryption_key
        self._material_description = material_description
        attr.validate(self)
        self.__attrs_post_init__()

    def __attrs_post_init__(self):
        """Verify that the encryption key is allowed be used for raw materials."""
        if self._encryption_key is not None and not self._encryption_key.allowed_for_raw_materials:
            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: () -> DelegatedKey
        """Delegated key used for calculating digital signatures.

        :returns: Signing key
        :rtype: DelegatedKey
        """
        return self._signing_key

    @property
    def encryption_key(self):
        # type: () -> DelegatedKey
        """Delegated key used for encrypting attributes.

        :returns: Encryption key
        :rtype: DelegatedKey
        """
        if self._encryption_key is None:
            raise AttributeError("No encryption key available")

        return self._encryption_key