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 MessageEncryptionTestScenario(object): """Data class for a single full message decrypt test scenario. Handles serialization and deserialization to and from manifest specs. :param str plaintext_name: Identifying name of plaintext :param bytes plaintext: Binary plaintext data :param AlgorithmSuite algorithm: Algorithm suite to use :param int frame_size: Frame size to use :param dict encryption_context: Encryption context to use :param master_key_specs: Iterable of loaded master key specifications :type master_key_specs: iterable of :class:`MasterKeySpec` :param MasterKeyProvider master_key_provider: """ plaintext_name = attr.ib( validator=attr.validators.instance_of(six.string_types)) plaintext = attr.ib(validator=attr.validators.instance_of(six.binary_type)) algorithm = attr.ib(validator=attr.validators.instance_of(AlgorithmSuite)) frame_size = attr.ib(validator=attr.validators.instance_of(int)) encryption_context = attr.ib( validator=dictionary_validator(six.string_types, six.string_types)) master_key_specs = attr.ib( validator=iterable_validator(list, MasterKeySpec)) master_key_provider = attr.ib( validator=attr.validators.instance_of(MasterKeyProvider)) @classmethod def from_scenario(cls, scenario, keys, plaintexts): # type: (ENCRYPT_SCENARIO_SPEC, KeysManifest, Dict[str, bytes]) -> MessageEncryptionTestScenario """Load from a scenario specification. :param dict scenario: Scenario specification JSON :param KeysManifest keys: Loaded keys :param dict plaintexts: Mapping of plaintext names to plaintext values :return: Loaded test scenario :rtype: MessageEncryptionTestScenario """ algorithm = algorithm_suite_from_string_id(scenario["algorithm"]) master_key_specs = [ MasterKeySpec.from_scenario(spec) for spec in scenario["master-keys"] ] master_key_provider = master_key_provider_from_master_key_specs( keys, master_key_specs) return cls( plaintext_name=scenario["plaintext"], plaintext=plaintexts[scenario["plaintext"]], algorithm=algorithm, frame_size=scenario["frame-size"], encryption_context=scenario["encryption-context"], master_key_specs=master_key_specs, master_key_provider=master_key_provider, ) @property def scenario_spec(self): # type: () -> ENCRYPT_SCENARIO_SPEC """Build a scenario specification describing this test scenario. :return: Scenario specification JSON :rtype: dict """ return { "plaintext": self.plaintext_name, "algorithm": binascii.hexlify(self.algorithm.id_as_bytes()), "frame-size": self.frame_size, "encryption-context": self.encryption_context, "master-keys": [spec.scenario_spec for spec in self.master_key_specs], } def run(self, ciphertext_writer, plaintext_uri): """Run this scenario, writing the resulting ciphertext with ``ciphertext_writer`` and returning a :class:`MessageDecryptionTestScenario` that describes the matching decrypt scenario. :param callable ciphertext_writer: Callable that will write the requested named ciphertext and return a URI locating the written data :param str plaintext_uri: URI locating the written plaintext data for this scenario :return: Decrypt test scenario that describes the generated scenario :rtype: MessageDecryptionTestScenario """ ciphertext, _header = aws_encryption_sdk.encrypt( source=self.plaintext, algorithm=self.algorithm, frame_length=self.frame_size, encryption_context=self.encryption_context, key_provider=self.master_key_provider, ) ciphertext_name = str(uuid.uuid4()) ciphertext_uri = ciphertext_writer(ciphertext_name, ciphertext) return MessageDecryptionTestScenario( plaintext_uri=plaintext_uri, plaintext=self.plaintext, ciphertext_uri=ciphertext_uri, ciphertext=ciphertext, master_key_specs=self.master_key_specs, master_key_provider=self.master_key_provider, )
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 MessageDecryptionManifest(object): # pylint: disable=too-many-arguments """AWS Encryption SDK Decrypt Message manifest handler. Described in AWS Crypto Tools Test Vector Framework feature #0003 AWS Encryption SDK Decrypt Message. :param str keys_uri: :param KeysManifest keys: :param dict test_scenarios: :param int version: :param str client_name: :param str client_version: """ keys_uri = attr.ib(validator=attr.validators.instance_of(six.string_types)) keys = attr.ib(validator=attr.validators.instance_of(KeysManifest)) test_scenarios = attr.ib(default=attr.Factory(dict), validator=dictionary_validator( six.string_types, MessageDecryptionTestScenario)) version = attr.ib(default=CURRENT_VERSION, validator=attr.validators.instance_of(int)) client_name = attr.ib(default=CLIENT_NAME, validator=attr.validators.instance_of( six.string_types)) client_version = attr.ib(default=aws_encryption_sdk.__version__, validator=attr.validators.instance_of( six.string_types)) type_name = "awses-decrypt" def __init__( self, keys_uri, # type: str keys, # type: KeysManifest test_scenarios=None, # type: Optional[Dict[str, MessageDecryptionTestScenario]] version=CURRENT_VERSION, # type: Optional[int] client_name=CLIENT_NAME, # type: Optional[str] client_version=aws_encryption_sdk. __version__, # type: Optional[str] ): # noqa=D107 # type: (...) -> None """Set initial values for the manifest.""" # Workaround pending resolution of attrs/mypy interaction. # https://github.com/python/mypy/issues/2088 # https://github.com/python-attrs/attrs/issues/215 self.keys_uri = keys_uri self.keys = keys self.test_scenarios = test_scenarios self.version = version self.client_name = client_name self.client_version = client_version attr.validate(self) @property def manifest_spec(self): # type: () -> FULL_MESSAGE_DECRYPT_MANIFEST """Build a full message decrypt manifest describing this manifest. :return: Manifest JSON :rtype: dict """ manifest_spec = {"type": self.type_name, "version": self.version} client_spec = { "name": self.client_name, "version": self.client_version } test_specs = { name: spec.scenario_spec for name, spec in self.test_scenarios.items() } return { "manifest": manifest_spec, "client": client_spec, "keys": self.keys_uri, "tests": test_specs } @classmethod def from_file(cls, input_file): # type: (IO) -> MessageDecryptionManifest """Load from a file containing a full message decrypt manifest. :param file input_file: File object for file containing JSON manifest :return: Loaded manifest :rtype: MessageDecryptionManifest """ 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)) root_reader = file_reader(parent_dir) version = raw_manifest["manifest"]["version"] # type: int keys_uri = raw_manifest["keys"] # type: str raw_keys_manifest = json.loads(root_reader(keys_uri).decode(ENCODING)) keys = KeysManifest.from_manifest_spec(raw_keys_manifest) client_name = raw_manifest["client"]["name"] # type: str client_version = raw_manifest["client"]["version"] # type: str raw_scenarios = raw_manifest[ "tests"] # type: Dict[str, DECRYPT_SCENARIO_SPEC] test_scenarios = { name: MessageDecryptionTestScenario.from_scenario( scenario=scenario, plaintext_reader=root_reader, ciphertext_reader=root_reader, keys=keys) for name, scenario in raw_scenarios.items() } return cls( keys_uri=keys_uri, keys=keys, test_scenarios=test_scenarios, version=version, client_name=client_name, client_version=client_version, ) def run(self): # () -> None """Process all scenarios in this manifest.""" for name, scenario in self.test_scenarios.items(): scenario.run(name)
class MessageEncryptionTestScenario(object): # pylint: disable=too-many-instance-attributes """Data class for a single full message decrypt test scenario. Handles serialization and deserialization to and from manifest specs. :param str plaintext_name: Identifying name of plaintext :param bytes plaintext: Binary plaintext data :param AlgorithmSuite algorithm: Algorithm suite to use :param int frame_size: Frame size to use :param dict encryption_context: Encryption context to use :param master_key_specs: Iterable of loaded master key specifications :type master_key_specs: iterable of :class:`MasterKeySpec` :param Callable master_key_provider_fn: """ plaintext_name = attr.ib( validator=attr.validators.instance_of(six.string_types)) plaintext = attr.ib(validator=attr.validators.instance_of(six.binary_type)) algorithm = attr.ib(validator=attr.validators.instance_of(AlgorithmSuite)) frame_size = attr.ib(validator=attr.validators.instance_of(int)) encryption_context = attr.ib( validator=dictionary_validator(six.string_types, six.string_types)) master_key_specs = attr.ib( validator=iterable_validator(list, MasterKeySpec)) master_key_provider_fn = attr.ib(validator=attr.validators.is_callable()) @classmethod def from_scenario(cls, scenario, keys, plaintexts): # type: (ENCRYPT_SCENARIO_SPEC, KeysManifest, Dict[str, bytes]) -> MessageEncryptionTestScenario """Load from a scenario specification. :param dict scenario: Scenario specification JSON :param KeysManifest keys: Loaded keys :param dict plaintexts: Mapping of plaintext names to plaintext values :return: Loaded test scenario :rtype: MessageEncryptionTestScenario """ algorithm = algorithm_suite_from_string_id(scenario["algorithm"]) master_key_specs = [ MasterKeySpec.from_scenario(spec) for spec in scenario["master-keys"] ] def master_key_provider_fn(): return master_key_provider_from_master_key_specs( keys, master_key_specs) return cls( plaintext_name=scenario["plaintext"], plaintext=plaintexts[scenario["plaintext"]], algorithm=algorithm, frame_size=scenario["frame-size"], encryption_context=scenario["encryption-context"], master_key_specs=master_key_specs, master_key_provider_fn=master_key_provider_fn, ) def run(self, materials_manager=None): """Run this scenario, writing the resulting ciphertext with ``ciphertext_writer`` and returning a :class:`MessageDecryptionTestScenario` that describes the matching decrypt scenario. :param callable ciphertext_writer: Callable that will write the requested named ciphertext and return a URI locating the written data :param str plaintext_uri: URI locating the written plaintext data for this scenario :return: Decrypt test scenario that describes the generated scenario :rtype: MessageDecryptionTestScenario """ commitment_policy = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT if self.algorithm.is_committing(): commitment_policy = CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT client = aws_encryption_sdk.EncryptionSDKClient( commitment_policy=commitment_policy) encrypt_kwargs = dict( source=self.plaintext, algorithm=self.algorithm, frame_length=self.frame_size, encryption_context=self.encryption_context, ) if materials_manager: encrypt_kwargs["materials_manager"] = materials_manager else: encrypt_kwargs["key_provider"] = self.master_key_provider_fn() ciphertext, _header = client.encrypt(**encrypt_kwargs) return ciphertext
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()