Merge pull request #939 from vepkenez/937-alice-decrypt

decryption endpoint for alice
pull/993/head
K Prasch 2019-05-08 08:19:00 -04:00 committed by GitHub
commit c3ce32f393
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 240 additions and 34 deletions

View File

@ -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):
"""

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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():
"""

View File

@ -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}")

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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