class ChangeEDKProviderInfoTamperingMethod(TamperingMethod): """Tampering method that changes the provider info on all EDKs.""" new_provider_infos = attr.ib(validator=iterable_validator(list, six.string_types)) def __init__(self, new_provider_infos): """Create a new instance for a given new provider info value.""" self.new_provider_infos = new_provider_infos @classmethod def from_values_spec(cls, values_spec): """Load from a tampering parameters specification""" return ChangeEDKProviderInfoTamperingMethod(values_spec) # pylint: disable=R0201 def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, _plaintext_uri): """ Run a given scenario, tampering with the input or the result. return: a list of (ciphertext, result) pairs. """ master_key_provider = generation_scenario.encryption_scenario.master_key_provider_fn() # Use a caching CMM to avoid generating a new data key every time. cache = LocalCryptoMaterialsCache(10) caching_cmm = CachingCryptoMaterialsManager( master_key_provider=master_key_provider, cache=cache, max_age=60.0, max_messages_encrypted=100, ) return [ self.run_scenario_with_new_provider_info( ciphertext_writer, generation_scenario, caching_cmm, new_provider_info ) for new_provider_info in self.new_provider_infos ] def run_scenario_with_new_provider_info( self, ciphertext_writer, generation_scenario, materials_manager, new_provider_info ): """Run with tampering for a specific new provider info value""" tampering_materials_manager = ProviderInfoChangingCryptoMaterialsManager(materials_manager, new_provider_info) ciphertext_to_decrypt = generation_scenario.encryption_scenario.run(tampering_materials_manager) expected_result = MessageDecryptionTestResult.expect_error( "Incorrect encrypted data key provider info: " + new_provider_info ) return generation_scenario.decryption_test_scenario_pair( ciphertext_writer, ciphertext_to_decrypt, expected_result )
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 MessageDecryptionTestScenario(object): # pylint: disable=too-many-arguments """Data class for a single full message decrypt test scenario. Handles serialization and deserialization to and from manifest specs. :param str plaintext_uri: URI locating plaintext data :param bytes plaintext: Binary plaintext data :param str ciphertext_uri: URI locating ciphertext data :param bytes ciphertext: Binary ciphertext data :param master_key_specs: Iterable of master key specifications :type master_key_specs: iterable of :class:`MasterKeySpec` :param MasterKeyProvider master_key_provider: :param str description: Description of test scenario (optional) """ # pylint: disable=too-few-public-methods plaintext_uri = attr.ib( validator=attr.validators.instance_of(six.string_types)) plaintext = attr.ib(validator=attr.validators.instance_of(six.binary_type)) ciphertext_uri = attr.ib( validator=attr.validators.instance_of(six.string_types)) ciphertext = attr.ib( validator=attr.validators.instance_of(six.binary_type)) master_key_specs = attr.ib( validator=iterable_validator(list, MasterKeySpec)) master_key_provider = attr.ib( validator=attr.validators.instance_of(MasterKeyProvider)) description = attr.ib(default=None, validator=attr.validators.optional( attr.validators.instance_of(six.string_types))) def __init__( self, plaintext_uri, # type: str plaintext, # type: bytes ciphertext_uri, # type: str ciphertext, # type: bytes master_key_specs, # type: Iterable[MasterKeySpec] master_key_provider, # type: MasterKeyProvider description=None, # type: Optional[str] ): # noqa=D107 # type: (...) -> None """Set initial values for the test scenario.""" # Workaround pending resolution of attrs/mypy interaction. # https://github.com/python/mypy/issues/2088 # https://github.com/python-attrs/attrs/issues/215 self.plaintext_uri = plaintext_uri self.plaintext = plaintext self.ciphertext_uri = ciphertext_uri self.ciphertext = ciphertext self.master_key_specs = master_key_specs self.master_key_provider = master_key_provider self.description = description attr.validate(self) @classmethod def from_scenario( cls, scenario, # type: DECRYPT_SCENARIO_SPEC plaintext_reader, # type: Callable[[str], bytes] ciphertext_reader, # type: Callable[[str], bytes] keys, # type: KeysManifest ): # type: (...) -> MessageDecryptionTestScenario """Load from a scenario specification. :param dict scenario: Scenario specification JSON :param plaintext_reader: URI-handling data reader for reading plaintext :param ciphertext_reader: URI-handling data reader for reading ciphertext :param KeysManifest keys: Loaded keys :return: Loaded test scenario :rtype: MessageDecryptionTestScenario """ raw_master_key_specs = scenario[ "master-keys"] # type: Iterable[MASTER_KEY_SPEC] master_key_specs = [ MasterKeySpec.from_scenario(spec) for spec in raw_master_key_specs ] master_key_provider = master_key_provider_from_master_key_specs( keys, master_key_specs) return cls( plaintext_uri=scenario["plaintext"], plaintext=plaintext_reader(scenario["plaintext"]), ciphertext_uri=scenario["ciphertext"], ciphertext=ciphertext_reader(scenario["ciphertext"]), master_key_specs=master_key_specs, master_key_provider=master_key_provider, description=scenario.get("description"), ) @property def scenario_spec(self): # type: () -> DECRYPT_SCENARIO_SPEC """Build a scenario specification describing this test scenario. :return: Scenario specification JSON :rtype: dict """ spec = { "plaintext": self.plaintext_uri, "ciphertext": self.ciphertext_uri, "master-keys": [spec.scenario_spec for spec in self.master_key_specs], } if self.description is not None: spec["description"] = self.description return spec def run(self, name): """Run this test scenario :param str name: Descriptive name for this scenario to use in any logging or errors """ client = aws_encryption_sdk.EncryptionSDKClient( commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) plaintext, _header = client.decrypt( source=self.ciphertext, key_provider=self.master_key_provider) if plaintext != self.plaintext: raise ValueError( "Decrypted plaintext does not match expected value for scenario '{}'" .format(name))
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 MessageDecryptionTestScenarioGenerator(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 MessageEncryptionTestScenario encryption_scenario: Encryption parameters :param tampering_method: Optional method used to tamper with the ciphertext :type tampering_method: :class:`TamperingMethod` :param decryption_method: :param decryption_master_key_specs: Iterable of master key specifications :type decryption_master_key_specs: iterable of :class:`MasterKeySpec` :param Callable decryption_master_key_provider_fn: :param result: """ encryption_scenario = attr.ib(validator=attr.validators.instance_of(MessageEncryptionTestScenario)) tampering_method = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(TamperingMethod))) decryption_method = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(DecryptionMethod))) decryption_master_key_specs = attr.ib(validator=iterable_validator(list, MasterKeySpec)) decryption_master_key_provider_fn = attr.ib(validator=attr.validators.is_callable()) result = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(MessageDecryptionTestResult))) @classmethod def from_scenario(cls, scenario, keys, plaintexts): """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: MessageDecryptionTestScenarioGenerator """ encryption_scenario_spec = scenario["encryption-scenario"] encryption_scenario = MessageEncryptionTestScenario.from_scenario(encryption_scenario_spec, keys, plaintexts) tampering = scenario.get("tampering") tampering_method = TamperingMethod.from_tampering_spec(tampering) decryption_method_spec = scenario.get("decryption-method") decryption_method = DecryptionMethod(decryption_method_spec) if decryption_method_spec else None if "decryption-master-keys" in scenario: decryption_master_key_specs = [ MasterKeySpec.from_scenario(spec) for spec in scenario["decryption-master-keys"] ] def decryption_master_key_provider_fn(): return master_key_provider_from_master_key_specs(keys, decryption_master_key_specs) else: decryption_master_key_specs = encryption_scenario.master_key_specs decryption_master_key_provider_fn = encryption_scenario.master_key_provider_fn result_spec = scenario.get("result") result = MessageDecryptionTestResult.from_result_spec(result_spec, None) if result_spec else None return cls( encryption_scenario=encryption_scenario, tampering_method=tampering_method, decryption_method=decryption_method, decryption_master_key_specs=decryption_master_key_specs, decryption_master_key_provider_fn=decryption_master_key_provider_fn, result=result, ) 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 """ return dict(self.tampering_method.run_scenario_with_tampering(ciphertext_writer, self, plaintext_uri)) def decryption_test_scenario_pair(self, ciphertext_writer, ciphertext_to_decrypt, expected_result): """Create a new (name, decryption scenario) pair""" ciphertext_name = str(uuid.uuid4()) ciphertext_uri = ciphertext_writer(ciphertext_name, ciphertext_to_decrypt) return ( ciphertext_name, MessageDecryptionTestScenario( ciphertext_uri=ciphertext_uri, ciphertext=ciphertext_to_decrypt, master_key_specs=self.decryption_master_key_specs, master_key_provider_fn=self.decryption_master_key_provider_fn, decryption_method=self.decryption_method, result=expected_result, ), )
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