Merge pull request #2098 from KPrasch/enrico

Simple Enrico file encryption
pull/2104/head
K Prasch 2020-06-21 07:42:38 -07:00 committed by GitHub
commit 523ea089f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 181 additions and 41 deletions

View File

@ -17,11 +17,12 @@ COPY ./nucypher/blockchain/eth/sol/__conf__.py /install/nucypher/blockchain/eth/
COPY scripts/installation/install_solc.py /install/scripts/installation/ COPY scripts/installation/install_solc.py /install/scripts/installation/
COPY dev-requirements.txt /install COPY dev-requirements.txt /install
COPY requirements.txt /install COPY requirements.txt /install
COPY docs-requirements.txt /install
COPY dev/docker/scripts/install/entrypoint.sh /install COPY dev/docker/scripts/install/entrypoint.sh /install
# install reqs and solc # install reqs and solc
RUN pip install --upgrade pip RUN pip install --upgrade pip
RUN pip3 install -r /install/dev-requirements.txt --src /usr/local/src RUN pip3 install .[dev] --src /usr/local/src
RUN pip3 install ipdb RUN pip3 install ipdb
# puts the nucypher executable in bin path # puts the nucypher executable in bin path

View File

@ -201,7 +201,7 @@ Encrypt
from nucypher.characters.lawful import Enrico from nucypher.characters.lawful import Enrico
enrico = Enrico(policy_encrypting_key=policy_encrypting_key) enrico = Enrico(policy_encrypting_key=policy_encrypting_key)
ciphertext, signature = enrico.encrypt_message(message=b'Peace at dawn.') ciphertext, signature = enrico.encrypt_message(plaintext=b'Peace at dawn.')
The ciphertext can then be sent to Bob via the application side channel. The ciphertext can then be sent to Bob via the application side channel.

View File

@ -55,7 +55,7 @@ def mario_box_cli(plaintext_dir, alice_config, label, outfile):
encoded_plaintext = base64.b64encode(plaintext) encoded_plaintext = base64.b64encode(plaintext)
enrico = Enrico(policy_encrypting_key=policy_encrypting_key) enrico = Enrico(policy_encrypting_key=policy_encrypting_key)
message_kit, _signature = enrico.encrypt_message(message=encoded_plaintext) message_kit, _signature = enrico.encrypt_message(plaintext=encoded_plaintext)
base64_message_kit = base64.b64encode(bytes(message_kit)).decode() base64_message_kit = base64.b64encode(bytes(message_kit)).decode()
# Collect Bob Retrieve JSON Requests # Collect Bob Retrieve JSON Requests

View File

@ -60,7 +60,6 @@ class CharacterControllerBase(ABC):
method = getattr(self.interface, action, None) method = getattr(self.interface, action, None)
serializer = method._schema serializer = method._schema
params = serializer.load(request) # input validation will occur here. params = serializer.load(request) # input validation will occur here.
response = method(**params) # < ---- INLET response = method(**params) # < ---- INLET
response_data = serializer.dump(response) response_data = serializer.dump(response)
@ -139,10 +138,11 @@ class CLIController(CharacterControlServer):
def test_client(self): def test_client(self):
return return
def handle_request(self, method_name, request): def handle_request(self, method_name, request) -> dict:
start = maya.now() start = maya.now()
response = self._perform_action(action=method_name, request=request) response = self._perform_action(action=method_name, request=request)
return self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start) self.emitter.ipc(response=response, request_id=start.epoch, duration=maya.now() - start)
return response
class JSONRPCController(CharacterControlServer): class JSONRPCController(CharacterControlServer):

View File

@ -24,6 +24,7 @@ from nucypher.characters.control.specifications import alice, bob, enrico
from nucypher.crypto.kits import UmbralMessageKit from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower, SigningPower from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.crypto.utils import construct_policy_id from nucypher.crypto.utils import construct_policy_id
from nucypher.datastore.datastore import NotFound
def attach_schema(schema): def attach_schema(schema):
@ -241,11 +242,11 @@ class BobInterface(CharacterPublicInterface):
class EnricoInterface(CharacterPublicInterface): class EnricoInterface(CharacterPublicInterface):
@attach_schema(enrico.EncryptMessage) @attach_schema(enrico.EncryptMessage)
def encrypt_message(self, message: str): def encrypt_message(self, plaintext: Union[str, bytes]):
""" """
Character control endpoint for encrypting data for a policy and Character control endpoint for encrypting data for a policy and
receiving the messagekit (and signature) to give to Bob. receiving the messagekit (and signature) to give to Bob.
""" """
message_kit, signature = self.character.encrypt_message(bytes(message, encoding='utf-8')) message_kit, signature = self.character.encrypt_message(plaintext=plaintext)
response_data = {'message_kit': message_kit, 'signature': signature} response_data = {'message_kit': message_kit, 'signature': signature}
return response_data return response_data

View File

@ -16,25 +16,52 @@
""" """
import click import click
from marshmallow import post_load
from nucypher.characters.control.specifications import fields from nucypher.characters.control.specifications import fields, exceptions
from nucypher.characters.control.specifications.base import BaseSchema
from nucypher.cli import options from nucypher.cli import options
from nucypher.cli.types import EXISTING_READABLE_FILE
from nucypher.characters.control.specifications.base import BaseSchema
class EncryptMessage(BaseSchema): class EncryptMessage(BaseSchema):
# input # input
message = fields.Cleartext( message = fields.Cleartext(
required=True, load_only=True, load_only=True,
allow_none=True,
click=click.option('--message', help="A unicode message to encrypt for a policy") click=click.option('--message', help="A unicode message to encrypt for a policy")
) )
file = fields.FileField(
load_only=True,
allow_none=True,
click=click.option('--file', help="Filepath to plaintext file to encrypt", type=EXISTING_READABLE_FILE)
)
policy_encrypting_key = fields.Key( policy_encrypting_key = fields.Key(
required=False, required=False,
load_only=True, load_only=True,
click=options.option_policy_encrypting_key()) click=options.option_policy_encrypting_key()
)
@post_load()
def format_method_arguments(self, data, **kwargs):
"""
input can be through either the file input or a raw message,
we output one of them as the "plaintext" arg to enrico.encrypt_message
"""
if data.get('message') and data.get('file'):
raise exceptions.InvalidArgumentCombo("choose either a message or a filepath but not both.")
if data.get('message'):
data = bytes(data['message'], encoding='utf-8')
else:
data = data['file']
return {"plaintext": data}
# output # output
message_kit = fields.UmbralMessageKit(dump_only=True) message_kit = fields.UmbralMessageKit(dump_only=True)
signature = fields.String(dump_only=True) # maybe we need a signature field? signature = fields.UmbralSignature(dump_only=True)

View File

@ -22,3 +22,5 @@ from nucypher.characters.control.specifications.fields.datetime import *
from nucypher.characters.control.specifications.fields.label import * from nucypher.characters.control.specifications.fields.label import *
from nucypher.characters.control.specifications.fields.cleartext import * from nucypher.characters.control.specifications.fields.cleartext import *
from nucypher.characters.control.specifications.fields.misc import * from nucypher.characters.control.specifications.fields.misc import *
from nucypher.characters.control.specifications.fields.file import *
from nucypher.characters.control.specifications.fields.signature import *

View File

@ -0,0 +1,33 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
from marshmallow import fields
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
from nucypher.characters.control.specifications.fields.base import BaseField
class FileField(BaseField, fields.String):
def _deserialize(self, value, attr, data, **kwargs):
with open(value, 'rb') as plaintext_file:
plaintext = plaintext_file.read() # TODO: #2106 Handle large files
return plaintext
def _validate(self, value):
return os.path.exists(value) and os.path.isfile(value)

View File

@ -0,0 +1,44 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from base64 import b64decode, b64encode
from marshmallow import fields
from umbral.signing import Signature
from nucypher.characters.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
from nucypher.characters.control.specifications.fields.base import BaseField
class UmbralSignature(BaseField, fields.Field):
def _serialize(self, value: Signature, attr, obj, **kwargs):
return b64encode(bytes(value)).decode()
def _deserialize(self, value, attr, data, **kwargs):
if isinstance(value, bytes):
return value
try:
return Signature.from_bytes(b64decode(value))
except InvalidNativeDataTypes as e:
raise InvalidInputData(f"Could not parse {self.name}: {e}")
def _validate(self, value):
try:
Signature.from_bytes(value)
except InvalidNativeDataTypes as e:
raise InvalidInputData(f"Could not parse {self.name}: {e}")

View File

@ -791,15 +791,15 @@ class Bob(Character):
self.get_reencrypted_cfrags(work_order, retain_cfrags=retain_cfrags) self.get_reencrypted_cfrags(work_order, retain_cfrags=retain_cfrags)
except NodeSeemsToBeDown as e: except NodeSeemsToBeDown as e:
# TODO: What to do here? Ursula isn't supposed to be down. NRN # TODO: What to do here? Ursula isn't supposed to be down. NRN
self.log.info( self.log.info(f"Ursula ({work_order.ursula}) seems to be down while trying to complete WorkOrder: {work_order}")
f"Ursula ({work_order.ursula}) seems to be down while trying to complete WorkOrder: {work_order}")
continue continue
except self.network_middleware.NotFound: except self.network_middleware.NotFound:
# This Ursula claims not to have a matching KFrag. Maybe this has been revoked? # This Ursula claims not to have a matching KFrag. Maybe this has been revoked?
# TODO: What's the thing to do here? Do we want to track these Ursulas in some way in case they're lying? 567 # TODO: What's the thing to do here? Do we want to track these Ursulas in some way in case they're lying? 567
self.log.warn( self.log.warn(f"Ursula ({work_order.ursula}) claims not to have the KFrag to complete WorkOrder: {work_order}. Has accessed been revoked?")
f"Ursula ({work_order.ursula}) claims not to have the KFrag to complete WorkOrder: {work_order}. Has accessed been revoked?")
continue continue
except self.network_middleware.UnexpectedResponse:
raise # TODO: Handle this
for capsule, pre_task in work_order.tasks.items(): for capsule, pre_task in work_order.tasks.items():
try: try:
@ -1551,11 +1551,10 @@ class Enrico(Character):
self.log = Logger(f'{self.__class__.__name__}-{bytes(self.public_keys(SigningPower)).hex()[:6]}') self.log = Logger(f'{self.__class__.__name__}-{bytes(self.public_keys(SigningPower)).hex()[:6]}')
self.log.info(self.banner.format(policy_encrypting_key)) self.log.info(self.banner.format(policy_encrypting_key))
def encrypt_message(self, def encrypt_message(self, plaintext: bytes) -> Tuple[UmbralMessageKit, Signature]:
message: bytes # TODO: #2107 Rename to "encrypt"
) -> Tuple[UmbralMessageKit, Signature]:
message_kit, signature = encrypt_and_sign(self.policy_pubkey, message_kit, signature = encrypt_and_sign(self.policy_pubkey,
plaintext=message, plaintext=plaintext,
signer=self.stamp) signer=self.stamp)
message_kit.policy_pubkey = self.policy_pubkey # TODO: We can probably do better here. NRN message_kit.policy_pubkey = self.policy_pubkey # TODO: We can probably do better here. NRN
return message_kit, signature return message_kit, signature

View File

@ -14,8 +14,10 @@
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>. along with nucypher. If not, see <https://www.gnu.org/licenses/>.
""" """
import base64
import click import click
import ipfshttpclient
from nucypher.characters.control.emitters import StdoutEmitter from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import BobInterface from nucypher.characters.control.interfaces import BobInterface
@ -286,13 +288,15 @@ def public_keys(general_config, character_options, config_file):
@option_config_file @option_config_file
@BobInterface.connect_cli('retrieve') @BobInterface.connect_cli('retrieve')
@group_general_config @group_general_config
@click.option('--ipfs', help="Download an encrypted message from IPFS at the specified gateway URI")
def retrieve(general_config, def retrieve(general_config,
character_options, character_options,
config_file, config_file,
label, label,
policy_encrypting_key, policy_encrypting_key,
alice_verifying_key, alice_verifying_key,
message_kit): message_kit,
ipfs):
"""Obtain plaintext from encrypted data, if access was granted.""" """Obtain plaintext from encrypted data, if access was granted."""
# Setup # Setup
@ -305,6 +309,15 @@ def retrieve(general_config,
required_fields = ', '.join(input_specification) required_fields = ', '.join(input_specification)
raise click.BadArgumentUsage(f'{required_fields} are required flags to retrieve') raise click.BadArgumentUsage(f'{required_fields} are required flags to retrieve')
if ipfs:
# TODO: #2108
emitter.message(f"Connecting to IPFS Gateway {ipfs}")
ipfs_client = ipfshttpclient.connect(ipfs)
cid = message_kit # Understand the message kit value as an IPFS hash.
raw_message_kit = ipfs_client.cat(cid) # cat the contents at the hash reference
emitter.message(f"Downloaded message kit from IPFS (CID {cid})", color='green')
message_kit = raw_message_kit.decode() # cast to utf-8
# Request # Request
bob_request_data = { bob_request_data = {
'label': label, 'label': label,

View File

@ -17,6 +17,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click import click
import ipfshttpclient
from umbral.keys import UmbralPublicKey from umbral.keys import UmbralPublicKey
from nucypher.characters.control.interfaces import EnricoInterface from nucypher.characters.control.interfaces import EnricoInterface
@ -58,13 +59,29 @@ def run(general_config, policy_encrypting_key, dry_run, http_port):
@enrico.command() @enrico.command()
@EnricoInterface.connect_cli('encrypt_message') @EnricoInterface.connect_cli('encrypt_message')
@click.option('--ipfs', help="Upload the encrypted message to IPFS at the specified gateway URI")
@group_general_config @group_general_config
def encrypt(general_config, policy_encrypting_key, message): def encrypt(general_config, policy_encrypting_key, message, file, ipfs):
"""Encrypt a message under a given policy public key.""" """Encrypt a message under a given policy public key."""
# Setup
emitter = setup_emitter(general_config=general_config, banner=policy_encrypting_key) emitter = setup_emitter(general_config=general_config, banner=policy_encrypting_key)
ENRICO = _create_enrico(emitter, policy_encrypting_key) ENRICO = _create_enrico(emitter, policy_encrypting_key)
encryption_request = {'message': message} if not (bool(message) ^ bool(file)):
emitter.error(f'Pass either --message or --file. Got {message}, {file}.')
raise click.Abort
# Encryption Request
encryption_request = {'policy_encrypting_key': policy_encrypting_key, 'message': message, 'file': file}
response = ENRICO.controller.encrypt_message(request=encryption_request) response = ENRICO.controller.encrypt_message(request=encryption_request)
# Handle Ciphertext
# TODO: This might be crossing the bridge of being application code
if ipfs:
emitter.message(f"Connecting to IPFS Gateway {ipfs}")
ipfs_client = ipfshttpclient.connect(ipfs)
cid = ipfs_client.add_str(response['message_kit'])
emitter.message(f"Uploaded message kit to IPFS (CID {cid})", color='green')
return response return response

View File

@ -235,10 +235,13 @@ class RestMiddleware:
def send_work_order_payload_to_ursula(self, work_order): def send_work_order_payload_to_ursula(self, work_order):
payload = work_order.payload() payload = work_order.payload()
id_as_hex = work_order.arrangement_id.hex() id_as_hex = work_order.arrangement_id.hex()
return self.client.post( response = self.client.post(
node_or_sprout=work_order.ursula, node_or_sprout=work_order.ursula,
path=f"kFrag/{id_as_hex}/reencrypt", path=f"kFrag/{id_as_hex}/reencrypt",
data=payload, timeout=2) data=payload,
timeout=2
)
return response
def check_rest_availability(self, initiator, responder): def check_rest_availability(self, initiator, responder):
response = self.client.post(node_or_sprout=responder, response = self.client.post(node_or_sprout=responder,

View File

@ -36,9 +36,12 @@ from nucypher.utilities.prometheus.collector import (
import json import json
from typing import List from typing import List
from prometheus_client.core import Timestamp try:
from prometheus_client.registry import CollectorRegistry, REGISTRY from prometheus_client.core import Timestamp
from prometheus_client.utils import floatToGoString from prometheus_client.registry import CollectorRegistry, REGISTRY
from prometheus_client.utils import floatToGoString
except ImportError:
raise DevelopmentInstallationRequired(importable_name='prometheus_client')
from twisted.internet import reactor, task from twisted.internet import reactor, task
from twisted.web.resource import Resource from twisted.web.resource import Resource

View File

@ -39,11 +39,9 @@ def get_fields(interface, method_name):
def validate_json_rpc_response_data(response, method_name, interface): def validate_json_rpc_response_data(response, method_name, interface):
required_output_fields = get_fields(interface, method_name)[-1]
required_output_fileds = get_fields(interface, method_name)[-1]
assert 'jsonrpc' in response.data assert 'jsonrpc' in response.data
for output_field in required_output_fileds: for output_field in required_output_fields:
assert output_field in response.content assert output_field in response.content
return True return True

View File

@ -571,7 +571,7 @@ def test_collect_rewards_integration(click_runner,
# Encrypt # Encrypt
random_data = os.urandom(random.randrange(20, 100)) random_data = os.urandom(random.randrange(20, 100))
message_kit, signature = enrico.encrypt_message(message=random_data) message_kit, signature = enrico.encrypt_message(plaintext=random_data)
# Decrypt # Decrypt
cleartexts = blockchain_bob.retrieve(message_kit, cleartexts = blockchain_bob.retrieve(message_kit,

View File

@ -473,7 +473,7 @@ def test_collect_rewards_integration(click_runner,
# Encrypt # Encrypt
random_data = os.urandom(random.randrange(20, 100)) random_data = os.urandom(random.randrange(20, 100))
ciphertext, signature = enrico.encrypt_message(message=random_data) ciphertext, signature = enrico.encrypt_message(plaintext=random_data)
# Decrypt # Decrypt
cleartexts = blockchain_bob.retrieve(ciphertext, cleartexts = blockchain_bob.retrieve(ciphertext,

View File

@ -230,7 +230,6 @@ def test_bob_web_character_control_retrieve_again(bob_web_controller_test_client
def test_enrico_web_character_control_encrypt_message(enrico_web_controller_test_client, encrypt_control_request): def test_enrico_web_character_control_encrypt_message(enrico_web_controller_test_client, encrypt_control_request):
method_name, params = encrypt_control_request method_name, params = encrypt_control_request
endpoint = f'/{method_name}' endpoint = f'/{method_name}'
response = enrico_web_controller_test_client.post(endpoint, data=json.dumps(params)) response = enrico_web_controller_test_client.post(endpoint, data=json.dumps(params))
assert response.status_code == 200 assert response.status_code == 200

View File

@ -107,7 +107,7 @@ def test_alice_can_decrypt(federated_alice):
enrico = Enrico(policy_encrypting_key=policy_pubkey) enrico = Enrico(policy_encrypting_key=policy_pubkey)
message = b"boring test message" message = b"boring test message"
message_kit, signature = enrico.encrypt_message(message=message) message_kit, signature = enrico.encrypt_message(plaintext=message)
# Interesting thing: if Alice wants to decrypt, she needs to provide the label directly. # Interesting thing: if Alice wants to decrypt, she needs to provide the label directly.
cleartext = federated_alice.verify_from(stranger=enrico, cleartext = federated_alice.verify_from(stranger=enrico,

View File

@ -65,7 +65,7 @@ def test_message_kit_serialization_via_enrico(enacted_federated_policy, federate
plaintext_bytes = bytes(message, encoding='utf-8') plaintext_bytes = bytes(message, encoding='utf-8')
# Create # Create
message_kit, signature = enrico.encrypt_message(message=plaintext_bytes) message_kit, signature = enrico.encrypt_message(plaintext=plaintext_bytes)
# Serialize # Serialize
message_kit_bytes = message_kit.to_bytes() message_kit_bytes = message_kit.to_bytes()