class AwsKmsKeySpec(KeySpec): """AWS KMS key specification. :param bool encrypt: Key can be used to encrypt :param bool decrypt: Key can be used to decrypt :param str type_name: Master key type name (must be "aws-kms") :param str key_id: Master key ID """ # pylint: disable=too-few-public-methods type_name = attr.ib(validator=membership_validator(("aws-kms", ))) def __init__(self, encrypt, decrypt, type_name, key_id): # noqa=D107 # type: (bool, bool, str, str) -> None # Workaround pending resolution of attrs/mypy interaction. # https://github.com/python/mypy/issues/2088 # https://github.com/python-attrs/attrs/issues/215 self.type_name = type_name super(AwsKmsKeySpec, self).__init__(encrypt, decrypt, key_id) @property def manifest_spec(self): # type: () -> AWS_KMS_KEY_SPEC """Build a key specification describing this key specification. :return: Key specification JSON :rtype: dict """ return { "encrypt": self.encrypt, "decrypt": self.decrypt, "type": self.type_name, "key-id": arn_from_key_id(self.key_id), }
class KeysManifest(object): """Keys Manifest handler. Described in AWS Crypto Tools Test Vector Framework feature #0002 Keys Manifest. :param int version: Version of this manifest :param dict keys: Mapping of key names to :class:`KeySpec`s """ version = attr.ib(validator=membership_validator(SUPPORTED_VERSIONS)) keys = attr.ib(validator=dictionary_validator(six.string_types, KeySpec)) type_name = "keys" @classmethod def from_manifest_spec(cls, raw_manifest): # type: (KEYS_MANIFEST) -> KeysManifest """Load from a JSON keys manifest.""" manifest_version = raw_manifest["manifest"] # type: MANIFEST_VERSION validate_manifest_type(type_name=cls.type_name, manifest_version=manifest_version, supported_versions=SUPPORTED_VERSIONS) raw_key_specs = raw_manifest["keys"] # type: Dict[str, KEY_SPEC] keys = { name: key_from_manifest_spec(key_spec) for name, key_spec in raw_key_specs.items() } return cls(version=raw_manifest["manifest"]["version"], keys=keys) def key(self, name): # type: (str) -> KeySpec """Provide the key with the specified name. :param str name: Key name :return: Specified key :rtype: KeySpec :raises ValueError: if key name is unknown """ try: return self.keys[name] except KeyError: raise ValueError('Unknown key name: "{}"'.format(name)) @property def manifest_spec(self): # type: () -> KEYS_MANIFEST """Build a keys manifest describing this manifest. :return: Manifest JSON :rtype: dict """ return { "manifest": { "type": self.type_name, "version": self.version }, "keys": {name: key.manifest_spec for name, key in self.keys.items()}, }
class MessageEncryptionManifest(object): """AWS Encryption SDK Encrypt Message manifest handler. Described in AWS Crypto Tools Test Vector Framework feature #0003 AWS Encryption SDK Encrypt Message. :param int version: Version of this manifest :param KeysManifest keys: Loaded keys :param dict plaintexts: Mapping of plaintext names to plaintext values :param dict tests: Mapping of test scenario names to :class:`EncryptTextScenario`s """ version = attr.ib(validator=membership_validator(SUPPORTED_VERSIONS)) keys = attr.ib(validator=attr.validators.instance_of(KeysManifest)) plaintexts = attr.ib( validator=dictionary_validator(six.string_types, six.binary_type)) tests = attr.ib(validator=dictionary_validator( six.string_types, MessageEncryptionTestScenario)) type_name = "awses-encrypt" @staticmethod def _generate_plaintexts(plaintexts_specs): # type: (PLAINTEXTS_SPEC) -> Dict[str, bytes] """Generate required plaintext values. :param dict plaintexts_specs: Mapping of plaintext name to size in bytes :return: Mapping of plaintext name to randomly generated bytes :rtype: dict """ return { name: os.urandom(size) for name, size in plaintexts_specs.items() } @classmethod def from_file(cls, input_file): # type: (IO) -> MessageEncryptionManifest """Load frome a file containing a full message encrypt manifest. :param file input_file: File object for file containing JSON manifest :return: Loaded manifest :rtype: MessageEncryptionManifest """ raw_manifest = json.load(input_file) validate_manifest_type(type_name=cls.type_name, manifest_version=raw_manifest["manifest"], supported_versions=SUPPORTED_VERSIONS) parent_dir = os.path.abspath(os.path.dirname(input_file.name)) reader = file_reader(parent_dir) raw_keys_manifest = json.loads( reader(raw_manifest["keys"]).decode(ENCODING)) keys = KeysManifest.from_manifest_spec(raw_keys_manifest) plaintexts = cls._generate_plaintexts(raw_manifest["plaintexts"]) tests = {} for name, scenario in raw_manifest["tests"].items(): try: tests[name] = MessageEncryptionTestScenario.from_scenario( scenario=scenario, keys=keys, plaintexts=plaintexts) except NotImplementedError: continue return cls(version=raw_manifest["manifest"]["version"], keys=keys, plaintexts=plaintexts, tests=tests) def run_and_write_to_dir(self, target_directory, json_indent=None): # type: (str, Optional[int]) -> None """Process all known encrypt test scenarios and write the resulting data and manifests to disk. :param str target_directory: Directory in which to write all output :param int json_indent: Number of spaces to indent JSON files (optional: default is to write minified) """ root_dir = os.path.abspath(target_directory) root_writer = file_writer(root_dir) root_writer( "keys.json", json.dumps(self.keys.manifest_spec, indent=json_indent).encode(ENCODING)) plaintext_writer = file_writer(os.path.join(root_dir, "plaintexts")) plaintext_uris = { name: plaintext_writer(name, plaintext) for name, plaintext in self.plaintexts.items() } ciphertext_writer = file_writer(os.path.join(root_dir, "ciphertexts")) test_scenarios = { name: scenario.run(ciphertext_writer, plaintext_uris[scenario.plaintext_name]) for name, scenario in self.tests.items() } decrypt_manifest = MessageDecryptionManifest( keys_uri="file://keys.json", keys=self.keys, test_scenarios=test_scenarios) root_writer( "manifest.json", json.dumps(decrypt_manifest.manifest_spec, indent=json_indent).encode(ENCODING))
class ManualKeySpec(KeySpec): # pylint: disable=too-many-arguments """Manual key specification. Allowed values described in AWS Crypto Tools Test Vector Framework feature #0002 Keys Manifest. :param str key_id: Master key ID :param bool encrypt: Key can be used to encrypt :param bool decrypt: Key can be used to decrypt :param str algorithm: Algorithm to use with key :param str type_name: Key type :param int bits: Key length in bits :param str encoding: Encoding used to encode key material :param str material: Raw material encoded """ algorithm = attr.ib(validator=membership_validator(KEY_ALGORITHMS)) type_name = attr.ib(validator=membership_validator(KEY_TYPES)) bits = attr.ib(validator=attr.validators.instance_of(int)) encoding = attr.ib(validator=membership_validator(KEY_ENCODINGS)) material = attr.ib(validator=attr.validators.instance_of(six.string_types)) def __init__( self, key_id, # type: str encrypt, # type: bool decrypt, # type: bool algorithm, # type: str type_name, # type: str bits, # type: int encoding, # type: str material, # type: Iterable[str] ): # 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 self.algorithm = algorithm self.type_name = type_name self.bits = bits self.encoding = encoding self.material = material super(ManualKeySpec, self).__init__(encrypt, decrypt, key_id) @property def raw_material(self): # type: () -> bytes """Provide the raw binary material for this key. :return: Binary key material :rtype: bytes """ raw_material = self.material.encode(ENCODING) if self.encoding == "base64": return base64.b64decode(raw_material) return raw_material @property def manifest_spec(self): # type: () -> MANUAL_KEY_SPEC """Build a key specification describing this key specification. :return: Key specification JSON :rtype: dict """ return { "encrypt": self.encrypt, "decrypt": self.decrypt, "algorithm": self.algorithm, "type": self.type_name, "bits": self.bits, "encoding": self.encoding, "material": self.material, "key-id": self.key_id, }
class MasterKeySpec(object): """AWS Encryption SDK master key specification utilities. Described in AWS Crypto Tools Test Vector Framework features #0003 and #0004. :param str type_name: Master key type name :param str key_name: Name of key in keys spec :param str provider_id: Master key provider ID :param str encryption_algorithm: Wrapping key encryption algorithm (required for raw master keys) :param str padding_algorithm: Wrapping key padding algorithm (required for raw master keys) :param str padding_hash: Wrapping key padding hash (required for raw master keys) """ type_name = attr.ib(validator=membership_validator(KNOWN_TYPES)) key_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) provider_id = attr.ib(validator=attr.validators.optional( attr.validators.instance_of(six.string_types))) encryption_algorithm = attr.ib(validator=attr.validators.optional( membership_validator(KNOWN_ALGORITHMS))) padding_algorithm = attr.ib(validator=attr.validators.optional( membership_validator(KNOWN_PADDING))) padding_hash = attr.ib(validator=attr.validators.optional( membership_validator(KNOWN_PADDING_HASH))) def __attrs_post_init__(self): # type: () -> None """Verify that known types all have loaders and that all required parameters are provided.""" if set(KNOWN_TYPES) != set(self._MASTER_KEY_LOADERS.keys()): raise NotImplementedError( "Gap found between known master key types and available master key loaders." ) if self.type_name == "raw": if None in (self.provider_id, self.encryption_algorithm): raise ValueError( "Provider ID and encryption algorithm are both required for raw keys" ) if self.encryption_algorithm == "rsa" and self.padding_algorithm is None: raise ValueError( "Padding algorithm is required for raw RSA keys") if self.padding_algorithm == "oaep-mgf1" and self.padding_hash is None: raise ValueError( 'Padding hash must be specified if padding algorithm is "oaep-mgf1"' ) @classmethod def from_scenario(cls, spec): # type: (MASTER_KEY_SPEC) -> MasterKeySpec """Load from a master key specification. :param dict spec: Master key specification JSON :return: Loaded master key specification :rtype: MasterKeySpec """ return cls( type_name=spec["type"], key_name=spec["key"], provider_id=spec.get("provider-id"), encryption_algorithm=spec.get("encryption-algorithm"), padding_algorithm=spec.get("padding-algorithm"), padding_hash=spec.get("padding-hash"), ) def _wrapping_algorithm(self, key_bits): # type: (int) -> WrappingAlgorithm """Determine the correct wrapping algorithm if this is a raw master key. :param key_bits: Key size in bits :return: Correct wrapping algorithm :rtype: WrappingAlgorithm :raises TypeError: if this is not a raw master key specification """ if not self.type_name == "raw": raise TypeError("This is not a raw master key") key_spec_values = [self.encryption_algorithm] if self.encryption_algorithm == "aes": key_spec_values.append(str(key_bits)) elif self.encryption_algorithm == "rsa": key_spec_values.append(self.padding_algorithm) if self.padding_hash is not None: key_spec_values.append(self.padding_hash) key_spec_name = "/".join(key_spec_values) if key_spec_name in _NOT_YET_IMPLEMENTED: raise NotImplementedError('Key spec "{}" is not yet available.') return _RAW_WRAPPING_KEY_ALGORITHMS[key_spec_name] def _wrapping_key(self, key_spec): # type: (KeySpec) -> WrappingKey """Build the correct wrapping key if this is a raw master key. :param KeySpec key_spec: Key specification to use with this master key :return: Wrapping key to use :rtype: WrappingKey :raises TypeError: if this is not a raw master key specification """ if not self.type_name == "raw": raise TypeError("This is not a raw master key") algorithm = self._wrapping_algorithm(key_spec.bits) material = key_spec.raw_material key_type = _RAW_ENCRYPTION_KEY_TYPE[key_spec.type_name] return WrappingKey(wrapping_algorithm=algorithm, wrapping_key=material, wrapping_key_type=key_type) def _raw_master_key_from_spec(self, key_spec): # type: (KeySpec) -> RawMasterKey """Build a raw master key using this specification. :param KeySpec key_spec: Key specification to use with this master key :return: Raw master key based on this specification :rtype: RawMasterKey :raises TypeError: if this is not a raw master key specification """ if not self.type_name == "raw": raise TypeError("This is not a raw master key") wrapping_key = self._wrapping_key(key_spec) return RawMasterKey(provider_id=self.provider_id, key_id=key_spec.key_id, wrapping_key=wrapping_key) def _kms_master_key_from_spec(self, key_spec): # type: (KeySpec) -> KMSMasterKey """Build an AWS KMS master key using this specification. :param KeySpec key_spec: Key specification to use with this master key :return: AWS KMS master key based on this specification :rtype: KMSMasterKey :raises TypeError: if this is not an AWS KMS master key specification """ if not self.type_name == "aws-kms": raise TypeError("This is not an AWS KMS master key") return KMS_MASTER_KEY_PROVIDER.master_key(key_id=key_spec.key_id) _MASTER_KEY_LOADERS = { "aws-kms": _kms_master_key_from_spec, "raw": _raw_master_key_from_spec } def master_key(self, keys): # type: (KeysManifest) -> MasterKeyProvider """Build a master key using this specification. :param KeysManifest keys: Loaded key materials """ key_spec = keys.key(self.key_name) key_loader = self._MASTER_KEY_LOADERS[self.type_name] return key_loader(self, key_spec) @property def scenario_spec(self): # type: () -> MASTER_KEY_SPEC """Build a master key specification describing this master key. :return: Master key specification JSON :rtype: dict """ spec = {"type": self.type_name, "key": self.key_name} if self.type_name != "aws-kms": spec.update({ "provider-id": self.provider_id, "encryption-algorithm": self.encryption_algorithm, "padding-algorithm": self.padding_algorithm, }) if self.padding_hash is not None: spec["padding-hash"] = self.padding_hash return spec
class ManualKeySpec(KeySpec): """Manual key specification. Allowed values described in AWS Crypto Tools Test Vector Framework feature #0002 Keys Manifest. :param bool encrypt: Key can be used to encrypt :param bool decrypt: Key can be used to decrypt :param str algorithm: Algorithm to use with key :param str type_name: Key type :param int bits: Key length in bits :param str encoding: Encoding used to encode key material :param material: Raw material encoded, then split into lines separated by ``line_separator`` :type material: list of str :param str line_separator: Character with which to separate members of ``material`` before decoding (optional: default is empty string) """ algorithm = attr.ib(validator=membership_validator(KEY_ALGORITHMS)) type_name = attr.ib(validator=membership_validator(KEY_TYPES)) bits = attr.ib(validator=attr.validators.instance_of(int)) encoding = attr.ib(validator=membership_validator(KEY_ENCODINGS)) material = attr.ib(validator=iterable_validator(list, six.string_types)) line_separator = attr.ib(default="", validator=attr.validators.instance_of( six.string_types)) def __init__( self, encrypt, # type: bool decrypt, # type: bool algorithm, # type: str type_name, # type: str bits, # type: int encoding, # type: str material, # type: Iterable[str] line_separator="", # type: Optional[str] ): # 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 self.algorithm = algorithm self.type_name = type_name self.bits = bits self.encoding = encoding self.material = material self.line_separator = line_separator super(ManualKeySpec, self).__init__(encrypt, decrypt) @property def raw_material(self): # type: () -> bytes """Provide the raw binary material for this key. :return: Binary key material :rtype: bytes """ raw_material = self.line_separator.join(self.material).encode(ENCODING) if self.encoding == "base64": return base64.b64decode(raw_material) return raw_material @property def manifest_spec(self): # type: () -> MANUAL_KEY_SPEC """Build a key specification describing this key specification. :return: Key specification JSON :rtype: dict """ return { "encrypt": self.encrypt, "decrypt": self.decrypt, "algorithm": self.algorithm, "type": self.type_name, "bits": self.bits, "encoding": self.encoding, "line-separator": self.line_separator, "material": self.material, }
class MessageEncryptionManifest(object): """AWS Encryption SDK Encrypt Message manifest handler. Described in AWS Crypto Tools Test Vector Framework feature #0003 AWS Encryption SDK Encrypt Message. :param int version: Version of this manifest :param KeysManifest keys: Loaded keys :param dict plaintexts: Mapping of plaintext names to plaintext values :param dict tests: Mapping of test scenario names to :class:`EncryptTextScenario`s """ version = attr.ib(validator=membership_validator(SUPPORTED_VERSIONS)) keys = attr.ib(validator=attr.validators.instance_of(KeysManifest)) plaintexts = attr.ib( validator=dictionary_validator(six.string_types, six.binary_type)) tests = attr.ib(validator=dictionary_validator( six.string_types, MessageEncryptionTestScenario)) type_name = "awses-encrypt" @staticmethod def _generate_plaintexts(plaintexts_specs): # type: (PLAINTEXTS_SPEC) -> Dict[str, bytes] """Generate required plaintext values. :param dict plaintexts_specs: Mapping of plaintext name to size in bytes :return: Mapping of plaintext name to randomly generated bytes :rtype: dict """ return { name: os.urandom(size) for name, size in plaintexts_specs.items() } @classmethod def from_file(cls, input_file): # type: (IO) -> MessageEncryptionManifest """Load frome a file containing a full message encrypt manifest. :param file input_file: File object for file containing JSON manifest :return: Loaded manifest :rtype: MessageEncryptionManifest """ raw_manifest = json.load(input_file) validate_manifest_type(type_name=cls.type_name, manifest_version=raw_manifest["manifest"], supported_versions=SUPPORTED_VERSIONS) parent_dir = os.path.abspath(os.path.dirname(input_file.name)) reader = file_reader(parent_dir) raw_keys_manifest = json.loads( reader(raw_manifest["keys"]).decode(ENCODING)) keys = KeysManifest.from_manifest_spec(raw_keys_manifest) plaintexts = cls._generate_plaintexts(raw_manifest["plaintexts"]) tests = {} for name, scenario in raw_manifest["tests"].items(): try: tests[name] = MessageEncryptionTestScenario.from_scenario( scenario=scenario, keys=keys, plaintexts=plaintexts) except NotImplementedError: continue return cls(version=raw_manifest["manifest"]["version"], keys=keys, plaintexts=plaintexts, tests=tests) def run(self): # () -> None """Process all scenarios in this manifest.""" for _, scenario in self.tests.items(): scenario.run()