mirror of https://github.com/nucypher/nucypher.git
commit
c3ce32f393
|
@ -81,6 +81,12 @@ class AliceJSONController(AliceInterface, CharacterController):
|
|||
response_data = result
|
||||
return response_data
|
||||
|
||||
@character_control_interface
|
||||
def decrypt(self, request: dict):
|
||||
result = super().decrypt(**self.serializer.load_decrypt_input(request=request))
|
||||
response_data = self.serializer.dump_decrypt_output(response=result)
|
||||
return response_data
|
||||
|
||||
@character_control_interface
|
||||
def public_keys(self, request):
|
||||
"""
|
||||
|
|
|
@ -125,6 +125,30 @@ class AliceInterface(CharacterPublicInterface, AliceSpecification):
|
|||
response_data = {'failed_revocations': len(failed_revocations)}
|
||||
return response_data
|
||||
|
||||
def decrypt(self, label: bytes, message_kit: bytes):
|
||||
"""
|
||||
Character control endpoint to allow Alice to decrypt her own data.
|
||||
"""
|
||||
|
||||
from nucypher.characters.lawful import Enrico
|
||||
|
||||
policy_encrypting_key = self.character.get_policy_pubkey_from_label(label)
|
||||
message_kit = UmbralMessageKit.from_bytes(message_kit) # TODO #846: May raise UnknownOpenSSLError and InvalidTag.
|
||||
|
||||
data_source = Enrico.from_public_keys(
|
||||
{SigningPower: message_kit.sender_pubkey_sig},
|
||||
policy_encrypting_key=policy_encrypting_key,
|
||||
label=label
|
||||
)
|
||||
|
||||
plaintexts = self.alice.decrypt_message_kit(
|
||||
message_kit=message_kit,
|
||||
data_source=data_source,
|
||||
label=label
|
||||
)
|
||||
|
||||
return {'cleartexts': plaintexts}
|
||||
|
||||
def public_keys(self):
|
||||
"""
|
||||
Character control endpoint for getting Alice's public keys.
|
||||
|
|
|
@ -16,6 +16,24 @@ class CharacterControlSerializer(ABC):
|
|||
"""Invalid data for I/O serialization or deserialization"""
|
||||
|
||||
|
||||
class MessageHandlerMixin:
|
||||
|
||||
__message_kit_transport_encoder = b64encode
|
||||
__message_kit_transport_decoder = b64decode
|
||||
|
||||
def set_message_encoder(self, encoder: Callable):
|
||||
self.__message_kit_transport_encoder = encoder
|
||||
|
||||
def set_message_decoder(self, decoder: Callable):
|
||||
self.__message_kit_transport_decoder = decoder
|
||||
|
||||
def encode(self, plaintext: bytes) -> str:
|
||||
return MessageHandlerMixin.__message_kit_transport_encoder(plaintext).encode()
|
||||
|
||||
def decode(self, cleartext: bytes) -> bytes:
|
||||
return MessageHandlerMixin.__message_kit_transport_decoder(cleartext)
|
||||
|
||||
|
||||
class CharacterControlJSONSerializer(CharacterControlSerializer):
|
||||
|
||||
def __call__(self, data, specification: tuple, *args, **kwargs):
|
||||
|
@ -60,7 +78,7 @@ class CharacterControlJSONSerializer(CharacterControlSerializer):
|
|||
return response_data
|
||||
|
||||
|
||||
class AliceControlJSONSerializer(CharacterControlJSONSerializer):
|
||||
class AliceControlJSONSerializer(CharacterControlJSONSerializer, MessageHandlerMixin):
|
||||
|
||||
@staticmethod
|
||||
def load_create_policy_input(request: dict):
|
||||
|
@ -85,6 +103,16 @@ class AliceControlJSONSerializer(CharacterControlJSONSerializer):
|
|||
response_data = {'policy_encrypting_key': policy_encrypting_key_hex, 'label': unicode_label}
|
||||
return response_data
|
||||
|
||||
def load_decrypt_input(self, request: dict) -> dict:
|
||||
parsed_input = dict(label=request['label'].encode(),
|
||||
message_kit=self.decode(request['message_kit']))
|
||||
return parsed_input
|
||||
|
||||
def dump_decrypt_output(self, response: dict) -> dict:
|
||||
cleartexts = [cleartext.decode() for cleartext in response['cleartexts']]
|
||||
response_data = {'cleartexts': cleartexts}
|
||||
return response_data
|
||||
|
||||
@staticmethod
|
||||
def parse_grant_input(request: dict):
|
||||
parsed_input = dict(bob_encrypting_key=bytes.fromhex(request['bob_encrypting_key']),
|
||||
|
@ -122,24 +150,6 @@ class AliceControlJSONSerializer(CharacterControlJSONSerializer):
|
|||
return response_data
|
||||
|
||||
|
||||
class MessageHandlerMixin:
|
||||
|
||||
__message_kit_transport_encoder = b64encode
|
||||
__message_kit_transport_decoder = b64decode
|
||||
|
||||
def set_message_encoder(self, encoder: Callable):
|
||||
self.__message_kit_transport_encoder = encoder
|
||||
|
||||
def set_message_decoder(self, decoder: Callable):
|
||||
self.__message_kit_transport_decoder = decoder
|
||||
|
||||
def encode(self, plaintext: bytes) -> str:
|
||||
return MessageHandlerMixin.__message_kit_transport_encoder(plaintext).encode()
|
||||
|
||||
def decode(self, cleartext: bytes) -> bytes:
|
||||
return MessageHandlerMixin.__message_kit_transport_decoder(cleartext)
|
||||
|
||||
|
||||
class BobControlJSONSerializer(CharacterControlJSONSerializer, MessageHandlerMixin):
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -37,7 +37,6 @@ class CharacterSpecification(ABC):
|
|||
|
||||
@staticmethod
|
||||
def __validate(specification: tuple, data: dict, error_class):
|
||||
|
||||
invalid_fields = set(data.keys()) - set(specification)
|
||||
if invalid_fields:
|
||||
pretty_invalid_fields = ', '.join(invalid_fields)
|
||||
|
@ -73,6 +72,11 @@ class AliceSpecification(CharacterSpecification):
|
|||
__revoke = (('label', 'bob_verifying_key', ), # In
|
||||
('failed_revocations',)) # Out
|
||||
|
||||
__decrypt = (
|
||||
('label', 'message_kit'), # In
|
||||
('cleartexts', ), # Out
|
||||
)
|
||||
|
||||
__public_keys = ((),
|
||||
('alice_verifying_key',))
|
||||
|
||||
|
@ -80,7 +84,8 @@ class AliceSpecification(CharacterSpecification):
|
|||
'derive_policy_encrypting_key': __derive_policy_encrypting_key,
|
||||
'grant': __grant,
|
||||
'revoke': __revoke,
|
||||
'public_keys': __public_keys}
|
||||
'public_keys': __public_keys,
|
||||
'decrypt': __decrypt, }
|
||||
|
||||
|
||||
class BobSpecification(CharacterSpecification):
|
||||
|
|
|
@ -70,7 +70,7 @@ from nucypher.blockchain.eth.decorators import validate_checksum_address
|
|||
|
||||
|
||||
class Alice(Character, PolicyAuthor):
|
||||
|
||||
|
||||
banner = ALICE_BANNER
|
||||
_controller_class = AliceJSONController
|
||||
_default_crypto_powerups = [SigningPower, DecryptingPower, DelegatingPower]
|
||||
|
@ -283,6 +283,31 @@ class Alice(Character, PolicyAuthor):
|
|||
failed_revocations[node_id] = (revocation, UnexpectedResponse)
|
||||
return failed_revocations
|
||||
|
||||
def decrypt_message_kit(
|
||||
self,
|
||||
message_kit: UmbralMessageKit,
|
||||
data_source: Character,
|
||||
label: bytes
|
||||
) -> List[bytes]:
|
||||
|
||||
"""
|
||||
Decrypt this Alice's own encrypted data.
|
||||
|
||||
I/O signatures match Bob's retrieve interface.
|
||||
"""
|
||||
|
||||
cleartexts = []
|
||||
cleartexts.append(
|
||||
self.verify_from(
|
||||
data_source,
|
||||
message_kit,
|
||||
signature=message_kit.signature,
|
||||
decrypt=True,
|
||||
label=label
|
||||
)
|
||||
)
|
||||
return cleartexts
|
||||
|
||||
def make_web_controller(drone_alice, crash_on_error: bool = False):
|
||||
|
||||
app_name = bytes(drone_alice.stamp).hex()[:6]
|
||||
|
@ -316,6 +341,18 @@ class Alice(Character, PolicyAuthor):
|
|||
control_request=request)
|
||||
return response
|
||||
|
||||
@alice_control.route("/decrypt", methods=['POST'])
|
||||
def decrypt():
|
||||
"""
|
||||
Character control endpoint for decryption of Alice's own policy data.
|
||||
"""
|
||||
|
||||
response = controller(
|
||||
interface=controller._internal_controller.decrypt,
|
||||
control_request=request
|
||||
)
|
||||
return response
|
||||
|
||||
@alice_control.route('/derive_policy_encrypting_key/<label>', methods=['POST'])
|
||||
def derive_policy_encrypting_key(label) -> Response:
|
||||
"""
|
||||
|
@ -624,7 +661,7 @@ class Bob(Character):
|
|||
"""
|
||||
return controller(interface=controller._internal_controller.public_keys,
|
||||
control_request=request)
|
||||
|
||||
|
||||
@bob_control.route('/join_policy', methods=['POST'])
|
||||
def join_policy():
|
||||
"""
|
||||
|
|
|
@ -35,6 +35,7 @@ from nucypher.config.constants import GLOBAL_DOMAIN
|
|||
@click.option('--dev', '-d', help="Enable development mode", is_flag=True)
|
||||
@click.option('--force', help="Don't ask for confirmation", is_flag=True)
|
||||
@click.option('--dry-run', '-x', help="Execute normally without actually starting the node", is_flag=True)
|
||||
@click.option('--message-kit', help="The message kit unicode string encoded in base64", type=click.STRING)
|
||||
@nucypher_click_config
|
||||
def alice(click_config,
|
||||
action,
|
||||
|
@ -57,12 +58,13 @@ def alice(click_config,
|
|||
bob_verifying_key,
|
||||
label,
|
||||
m,
|
||||
n):
|
||||
n,
|
||||
message_kit
|
||||
):
|
||||
|
||||
"""
|
||||
Start and manage an "Alice" character.
|
||||
"""
|
||||
|
||||
if not click_config.json_ipc and not click_config.quiet:
|
||||
click.secho(ALICE_BANNER)
|
||||
|
||||
|
@ -126,7 +128,6 @@ def alice(click_config,
|
|||
network_middleware=click_config.middleware)
|
||||
# Produce
|
||||
ALICE = alice_config(known_nodes=teacher_nodes, network_middleware=click_config.middleware)
|
||||
|
||||
# Switch to character control emitter
|
||||
if click_config.json_ipc:
|
||||
ALICE.controller.emitter = IPCStdoutEmitter(quiet=click_config.quiet)
|
||||
|
@ -192,5 +193,19 @@ def alice(click_config,
|
|||
raise click.BadOptionUsage(option_name='--dev', message=message)
|
||||
return actions.destroy_configuration(character_config=alice_config, force=force)
|
||||
|
||||
elif action == "decrypt":
|
||||
|
||||
if not all((label, message_kit)):
|
||||
input_specification, output_specification = ALICE.controller.get_specifications(interface_name='decrypt')
|
||||
required_fields = ', '.join(input_specification)
|
||||
raise click.BadArgumentUsage(f'{required_fields} are required flags to decrypt')
|
||||
|
||||
request_data = {
|
||||
'label': label,
|
||||
'message_kit': message_kit,
|
||||
}
|
||||
response = ALICE.controller.decrypt(request=request_data)
|
||||
return response
|
||||
|
||||
else:
|
||||
raise click.BadArgumentUsage(f"No such argument {action}")
|
||||
|
|
|
@ -435,7 +435,7 @@ class TreasureMap:
|
|||
|
||||
Alice and Bob have all the information they need to construct this.
|
||||
Ursula does not, so we share it with her.
|
||||
|
||||
|
||||
This way, Bob can generate it and use it to find the TreasureMap.
|
||||
"""
|
||||
self._hrac = keccak_digest(bytes(alice_stamp) + bytes(bob_verifying_key) + label)
|
||||
|
|
|
@ -24,7 +24,7 @@ import pytest
|
|||
|
||||
from umbral.kfrags import KFrag
|
||||
|
||||
from nucypher.characters.lawful import Bob
|
||||
from nucypher.characters.lawful import Bob, Enrico
|
||||
from nucypher.config.characters import AliceConfiguration
|
||||
from nucypher.crypto.api import keccak_digest
|
||||
from nucypher.crypto.powers import SigningPower, DecryptingPower
|
||||
|
@ -108,6 +108,47 @@ def test_federated_grant(federated_alice, federated_bob):
|
|||
assert kfrag == retrieved_kfrag
|
||||
|
||||
|
||||
def test_alice_can_decrypt(federated_alice, federated_bob):
|
||||
"""
|
||||
Test that alice can decrypt data encrypted by an enrico
|
||||
for her own derived policy pubkey.
|
||||
"""
|
||||
|
||||
# Setup the policy details
|
||||
m, n = 2, 3
|
||||
policy_end_datetime = maya.now() + datetime.timedelta(days=5)
|
||||
label = b"this_is_the_path_to_which_access_is_being_granted"
|
||||
|
||||
policy = federated_alice.create_policy(
|
||||
federated_bob,
|
||||
label, m, n,
|
||||
federated=True,
|
||||
expiration=policy_end_datetime,
|
||||
)
|
||||
|
||||
enrico = Enrico.from_alice(
|
||||
federated_alice,
|
||||
policy.label,
|
||||
)
|
||||
plaintext = b"this is the first thing i'm encrypting ever."
|
||||
|
||||
# use the enrico to encrypt the message
|
||||
message_kit, signature = enrico.encrypt_message(
|
||||
plaintext
|
||||
)
|
||||
|
||||
# decrypt the data
|
||||
decrypted_data = federated_alice.verify_from(
|
||||
enrico,
|
||||
message_kit,
|
||||
signature=signature,
|
||||
decrypt=True,
|
||||
label=policy.label
|
||||
)
|
||||
|
||||
assert plaintext == decrypted_data
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('federated_ursulas')
|
||||
def test_revocation(federated_alice, federated_bob):
|
||||
m, n = 2, 3
|
||||
|
|
|
@ -6,12 +6,16 @@ import maya
|
|||
import pytest
|
||||
|
||||
import nucypher
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from nucypher.config.characters import AliceConfiguration
|
||||
from nucypher.crypto.kits import UmbralMessageKit
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.policy.models import TreasureMap
|
||||
from nucypher.utilities.sandbox.policy import generate_random_label
|
||||
|
||||
from click.testing import CliRunner
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
click_runner = CliRunner()
|
||||
|
||||
|
||||
def test_alice_character_control_create_policy(alice_control_test_client, federated_bob):
|
||||
bob_pubkey_enc = federated_bob.public_keys(DecryptingPower)
|
||||
|
@ -111,6 +115,36 @@ def test_alice_character_control_revoke(alice_control_test_client, federated_bob
|
|||
assert response_data['result']['failed_revocations'] == 0
|
||||
|
||||
|
||||
def test_alice_character_control_decrypt(mocker, alice_federated_test_config, alice_control_test_client, enacted_federated_policy, capsule_side_channel):
|
||||
message_kit, data_source = capsule_side_channel
|
||||
|
||||
label = enacted_federated_policy.label.decode()
|
||||
policy_encrypting_key = bytes(enacted_federated_policy.public_key).hex()
|
||||
message_kit = b64encode(message_kit.to_bytes()).decode()
|
||||
|
||||
request_data = {
|
||||
'label': label,
|
||||
'message_kit': message_kit,
|
||||
}
|
||||
|
||||
response = alice_control_test_client.post('/decrypt', data=json.dumps(request_data))
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
assert 'cleartexts' in response_data['result']
|
||||
|
||||
for plaintext in response_data['result']['cleartexts']:
|
||||
assert bytes(plaintext, encoding='utf-8') == b'Welcome to the flippering.'
|
||||
|
||||
# Send bad data to assert error returns
|
||||
response = alice_control_test_client.post('/decrypt', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
||||
del(request_data['message_kit'])
|
||||
response = alice_control_test_client.put('/decrypt', data=json.dumps(request_data))
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_bob_character_control_join_policy(bob_control_test_client, enacted_federated_policy):
|
||||
request_data = {
|
||||
'label': enacted_federated_policy.label.decode(),
|
||||
|
@ -206,7 +240,7 @@ def test_character_control_lifecycle(alice_control_test_client,
|
|||
|
||||
bob_encrypting_key_hex = bob_keys['bob_encrypting_key']
|
||||
bob_verifying_key_hex = bob_keys['bob_verifying_key']
|
||||
|
||||
|
||||
# Create a policy via Alice control
|
||||
alice_request_data = {
|
||||
'bob_encrypting_key': bob_encrypting_key_hex,
|
||||
|
|
|
@ -187,8 +187,41 @@ def test_cli_lifecycle(click_runner,
|
|||
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 = (
|
||||
'--mock-networking',
|
||||
'--json-ipc',
|
||||
'alice', 'decrypt',
|
||||
'--federated-only',
|
||||
'--config-file', alice_configuration_file_location,
|
||||
'--message-kit', message_kit,
|
||||
'--label', policy.label,
|
||||
)
|
||||
|
||||
decrypt_response_fail = click_runner.invoke(
|
||||
nucypher_cli, decrypt_args[:-1], 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:
|
||||
Scene 5: Alice grants access to Bob:
|
||||
We catch up with Alice later on, but before she has learned about existing Ursulas...
|
||||
"""
|
||||
teacher = list(federated_ursulas)[0]
|
||||
|
@ -262,8 +295,9 @@ def test_cli_lifecycle(click_runner,
|
|||
|
||||
# Run the Callbacks
|
||||
d = threads.deferToThread(enrico_encrypts) # scene 4
|
||||
d.addCallback(_run_teacher) # scene 5 (preamble)
|
||||
d.addCallback(_grant) # scene 5
|
||||
d.addCallback(_bob_retrieves) # scene 6
|
||||
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
|
||||
|
||||
yield d
|
||||
|
|
Loading…
Reference in New Issue