import datetime import json import os import shutil from base64 import b64decode from collections import namedtuple from json import JSONDecodeError import maya import pytest import pytest_twisted as pt from twisted.internet import threads from web3 import Web3 from nucypher.cli.main import nucypher_cli from nucypher.config.characters import AliceConfiguration, BobConfiguration from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD from nucypher.crypto.kits import UmbralMessageKit from nucypher.utilities.logging import GlobalLoggerSettings from nucypher.utilities.sandbox.constants import INSECURE_DEVELOPMENT_PASSWORD, TEMPORARY_DOMAIN, TEST_PROVIDER_URI from nucypher.utilities.sandbox.ursula import start_pytest_ursula_services PLAINTEXT = "I'm bereaved, not a sap!" class MockSideChannel: PolicyAndLabel = namedtuple('PolicyAndLabel', ['encrypting_key', 'label']) BobPublicKeys = namedtuple('BobPublicKeys', ['bob_encrypting_key', 'bob_verifying_key']) class NoMessageKits(Exception): pass class NoPolicies(Exception): pass def __init__(self): self.__message_kits = [] self.__policies = [] self.__alice_public_keys = [] self.__bob_public_keys = [] def save_message_kit(self, message_kit: str) -> None: self.__message_kits.append(message_kit) def fetch_message_kit(self) -> UmbralMessageKit: if self.__message_kits: message_kit = self.__message_kits.pop() return message_kit raise self.NoMessageKits def save_policy(self, policy: PolicyAndLabel): self.__policies.append(policy) def fetch_policy(self) -> PolicyAndLabel: if self.__policies: policy = self.__policies[0] return policy raise self.NoPolicies def save_alice_pubkey(self, public_key): self.__alice_public_keys.append(public_key) def fetch_alice_pubkey(self): policy = self.__alice_public_keys.pop() return policy def save_bob_public_keys(self, public_keys: BobPublicKeys): self.__bob_public_keys.append(public_keys) def fetch_bob_public_keys(self) -> BobPublicKeys: policy = self.__bob_public_keys.pop() return policy @pt.inlineCallbacks def test_federated_cli_lifecycle(click_runner, testerchain, random_policy_label, federated_ursulas, custom_filepath, custom_filepath_2): yield _cli_lifecycle(click_runner, testerchain, random_policy_label, federated_ursulas, custom_filepath, custom_filepath_2) @pt.inlineCallbacks def test_decentralized_cli_lifecycle(click_runner, testerchain, random_policy_label, blockchain_ursulas, custom_filepath, custom_filepath_2, agency_local_registry): yield _cli_lifecycle(click_runner, testerchain, random_policy_label, blockchain_ursulas, custom_filepath, custom_filepath_2, agency_local_registry.filepath) def _cli_lifecycle(click_runner, testerchain, random_policy_label, ursulas, custom_filepath, custom_filepath_2, registry_filepath=None): """ This is an end to end integration test that runs each cli call in it's own process using only CLI character control entry points, and a mock side channel that runs in the control process """ federated = list(ursulas)[0].federated_only # Boring Setup Stuff alice_config_root = custom_filepath bob_config_root = custom_filepath_2 envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD} # A side channel exists - Perhaps a dApp side_channel = MockSideChannel() shutil.rmtree(custom_filepath, ignore_errors=True) shutil.rmtree(custom_filepath_2, ignore_errors=True) """ Scene 1: Alice Installs nucypher to a custom filepath and examines her configuration """ # Alice performs an installation for the first time alice_init_args = ('alice', 'init', '--network', TEMPORARY_DOMAIN, '--config-root', alice_config_root) if federated: alice_init_args += ('--federated-only', ) else: alice_init_args += ('--provider', TEST_PROVIDER_URI, '--pay-with', testerchain.alice_account, '--registry-filepath', registry_filepath) alice_init_response = click_runner.invoke(nucypher_cli, alice_init_args, catch_exceptions=False, env=envvars) assert alice_init_response.exit_code == 0 # Prevent previous global logger settings set by aboce command from writing non-IPC messages to stdout GlobalLoggerSettings.stop_console_logging() # Alice uses her configuration file to run the character "view" command alice_configuration_file_location = os.path.join(alice_config_root, AliceConfiguration.generate_filename()) alice_view_args = ('alice', 'public-keys', '--json-ipc', '--config-file', alice_configuration_file_location) alice_view_result = click_runner.invoke(nucypher_cli, alice_view_args, input=INSECURE_DEVELOPMENT_PASSWORD, catch_exceptions=False, env=envvars) assert alice_view_result.exit_code == 0 try: alice_view_response = json.loads(alice_view_result.output) except JSONDecodeError: pytest.fail("Invalid JSON response from JSON-RPC Character process.") # Alice expresses her desire to participate in data sharing with nucypher # by saving her public key somewhere Bob and Enrico can find it. side_channel.save_alice_pubkey(alice_view_response['result']['alice_verifying_key']) """ Scene 2: Bob installs nucypher, examines his configuration and expresses his interest to participate in data retrieval by posting his public keys somewhere public (side-channel). """ bob_init_args = ('bob', 'init', '--network', TEMPORARY_DOMAIN, '--config-root', bob_config_root) if federated: bob_init_args += ('--federated-only', ) else: bob_init_args += ('--provider', TEST_PROVIDER_URI, '--registry-filepath', registry_filepath, '--checksum-address', testerchain.bob_account) bob_init_response = click_runner.invoke(nucypher_cli, bob_init_args, catch_exceptions=False, env=envvars) assert bob_init_response.exit_code == 0 # Alice uses her configuration file to run the character "view" command bob_configuration_file_location = os.path.join(bob_config_root, BobConfiguration.generate_filename()) bob_view_args = ('bob', 'public-keys', '--json-ipc', '--config-file', bob_configuration_file_location) bob_view_result = click_runner.invoke(nucypher_cli, bob_view_args, catch_exceptions=False, env=envvars) assert bob_view_result.exit_code == 0 bob_view_response = json.loads(bob_view_result.output) # Bob interacts with the sidechannel bob_public_keys = MockSideChannel.BobPublicKeys(bob_view_response['result']['bob_encrypting_key'], bob_view_response['result']['bob_verifying_key']) side_channel.save_bob_public_keys(bob_public_keys) """ Scene 3: Alice derives a policy keypair, and saves it's public key to a sidechannel. """ random_label = random_policy_label.decode() # Unicode string derive_args = ('alice', 'derive-policy-pubkey', '--mock-networking', '--json-ipc', '--config-file', alice_configuration_file_location, '--label', random_label) derive_response = click_runner.invoke(nucypher_cli, derive_args, catch_exceptions=False, env=envvars) assert derive_response.exit_code == 0 derive_response = json.loads(derive_response.output) assert derive_response['result']['label'] == random_label # Alice and the sidechannel: at Tinagre policy = MockSideChannel.PolicyAndLabel(encrypting_key=derive_response['result']['policy_encrypting_key'], label=derive_response['result']['label']) side_channel.save_policy(policy=policy) """ Scene 4: Enrico encrypts some data for some policy public key and saves it to a side channel. """ def enrico_encrypts(): # Fetch! policy = side_channel.fetch_policy() enrico_args = ('enrico', 'encrypt', '--json-ipc', '--policy-encrypting-key', policy.encrypting_key, '--message', PLAINTEXT) encrypt_result = click_runner.invoke(nucypher_cli, enrico_args, catch_exceptions=False, env=envvars) assert encrypt_result.exit_code == 0 encrypt_result = json.loads(encrypt_result.output) encrypted_message = encrypt_result['result']['message_kit'] # type: str side_channel.save_message_kit(message_kit=encrypted_message) return encrypt_result def _alice_decrypts(encrypt_result): """ alice forgot what exactly she encrypted for bob. she decrypts it just to make sure. """ policy = side_channel.fetch_policy() alice_signing_key = side_channel.fetch_alice_pubkey() message_kit = encrypt_result['result']['message_kit'] decrypt_args = ( 'alice', 'decrypt', '--mock-networking', '--json-ipc', '--config-file', alice_configuration_file_location, '--message-kit', message_kit, '--label', policy.label, ) if federated: decrypt_args += ('--federated-only',) decrypt_response_fail = click_runner.invoke(nucypher_cli, decrypt_args[0:7], catch_exceptions=False, env=envvars) assert decrypt_response_fail.exit_code == 2 decrypt_response = click_runner.invoke(nucypher_cli, decrypt_args, catch_exceptions=False, env=envvars) decrypt_result = json.loads(decrypt_response.output) for cleartext in decrypt_result['result']['cleartexts']: assert b64decode(cleartext.encode()).decode() == PLAINTEXT # replenish the side channel side_channel.save_policy(policy=policy) side_channel.save_alice_pubkey(alice_signing_key) return encrypt_result """ Scene 5: Alice grants access to Bob: We catch up with Alice later on, but before she has learned about existing Ursulas... """ if federated: teacher = list(ursulas)[0] else: teacher = list(ursulas)[1] teacher_uri = teacher.seed_node_metadata(as_teacher_uri=True) # Some Ursula is running somewhere def _run_teacher(_encrypt_result): start_pytest_ursula_services(ursula=teacher) return teacher_uri def _grant(teacher_uri): # Alice fetched Bob's public keys from the side channel bob_keys = side_channel.fetch_bob_public_keys() bob_encrypting_key = bob_keys.bob_encrypting_key bob_verifying_key = bob_keys.bob_verifying_key grant_args = ('alice', 'grant', '--mock-networking', '--json-ipc', '--network', TEMPORARY_DOMAIN, '--teacher', teacher_uri, '--config-file', alice_configuration_file_location, '--m', 2, '--n', 3, '--expiration', (maya.now() + datetime.timedelta(days=3)).iso8601(), '--label', random_label, '--bob-encrypting-key', bob_encrypting_key, '--bob-verifying-key', bob_verifying_key) if federated: grant_args += ('--federated-only',) else: grant_args += ('--provider', TEST_PROVIDER_URI, '--rate', Web3.toWei(9, 'gwei')) grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars) assert grant_result.exit_code == 0 grant_result = json.loads(grant_result.output) # TODO: Expand test to consider manual treasure map handing # # Alice puts the Treasure Map somewhere Bob can get it. # side_channel.save_treasure_map(treasure_map=grant_result['result']['treasure_map']) return grant_result def _bob_retrieves(_grant_result): """ Scene 6: Bob retrieves encrypted data from the side channel and uses nucypher to re-encrypt it """ # Bob interacts with a sidechannel ciphertext_message_kit = side_channel.fetch_message_kit() policy = side_channel.fetch_policy() policy_encrypting_key, label = policy alice_signing_key = side_channel.fetch_alice_pubkey() retrieve_args = ('bob', 'retrieve', '--mock-networking', '--json-ipc', '--teacher', teacher_uri, '--config-file', bob_configuration_file_location, '--message-kit', ciphertext_message_kit, '--label', label, '--policy-encrypting-key', policy_encrypting_key, '--alice-verifying-key', alice_signing_key) # TODO: Remove - Federated not used for retrieve any more # if federated: # retrieve_args += ('--federated-only',) retrieve_response = click_runner.invoke(nucypher_cli, retrieve_args, catch_exceptions=False, env=envvars) assert retrieve_response.exit_code == 0 retrieve_response = json.loads(retrieve_response.output) for cleartext in retrieve_response['result']['cleartexts']: assert b64decode(cleartext.encode()).decode() == PLAINTEXT return # Run the Callbacks d = threads.deferToThread(enrico_encrypts) # scene 4 d.addCallback(_alice_decrypts) # scene 5 (uncertainty) d.addCallback(_run_teacher) # scene 6 (preamble) d.addCallback(_grant) # scene 7 d.addCallback(_bob_retrieves) # scene 8 return d