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)
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, )
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
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)
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)
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