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 dev-requirements.txt /install
COPY requirements.txt /install
COPY docs-requirements.txt /install
COPY dev/docker/scripts/install/entrypoint.sh /install
# install reqs and solc
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
# puts the nucypher executable in bin path

View File

@ -29,7 +29,7 @@ The NuCypher network does not store or handle an application's data; instead - i
Management of encrypted secrets and public keys tends to be highly domain-specific - The surrounding architecture
will vary greatly depending on the throughput, sensitivity, and sharing cadence of application secrets.
In all cases, NuCypher must be integrated with a storage and transport layer in order to function properly.
Along with the transport of ciphertexts, a nucypher application also needs to include channels for Alice and Bob
Along with the transport of ciphertexts, a nucypher application also needs to include channels for Alice and Bob
to discover each other's public keys, and provide policy encrypting information to Bob and Enrico.
Side Channel Application Data
@ -93,7 +93,7 @@ Connecting Nucypher to an Ethereum Provider
Ursula: Untrusted Re-Encryption Proxies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When initializing an ``Alice``\ , ``Bob``\ , or ``Ursula``\ , an initial "Stranger-\ ``Ursula``\ " is needed to perform
When initializing an ``Alice``\ , ``Bob``\ , or ``Ursula``\ , an initial "Stranger-\ ``Ursula``\ " is needed to perform
the role of a ``Teacher``\ , or "seednode":
.. code-block:: python
@ -151,7 +151,7 @@ Create a NuCypher Keyring
alice.start_learning_loop(now=True)
Alice needs to know about Bob in order to grant access by acquiring Bob's public key's through
Alice needs to know about Bob in order to grant access by acquiring Bob's public key's through
the application side channel:
.. code-block:: python
@ -201,7 +201,7 @@ Encrypt
from nucypher.characters.lawful import Enrico
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.

View File

@ -55,7 +55,7 @@ def mario_box_cli(plaintext_dir, alice_config, label, outfile):
encoded_plaintext = base64.b64encode(plaintext)
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()
# Collect Bob Retrieve JSON Requests

View File

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

View File

@ -24,6 +24,7 @@ from nucypher.characters.control.specifications import alice, bob, enrico
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.crypto.utils import construct_policy_id
from nucypher.datastore.datastore import NotFound
def attach_schema(schema):
@ -241,11 +242,11 @@ class BobInterface(CharacterPublicInterface):
class EnricoInterface(CharacterPublicInterface):
@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
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}
return response_data

View File

@ -16,25 +16,52 @@
"""
import click
from marshmallow import post_load
from nucypher.characters.control.specifications import fields
from nucypher.characters.control.specifications.base import BaseSchema
from nucypher.characters.control.specifications import fields, exceptions
from nucypher.cli import options
from nucypher.cli.types import EXISTING_READABLE_FILE
from nucypher.characters.control.specifications.base import BaseSchema
class EncryptMessage(BaseSchema):
# input
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")
)
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(
required=False,
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
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.cleartext 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)
except NodeSeemsToBeDown as e:
# TODO: What to do here? Ursula isn't supposed to be down. NRN
self.log.info(
f"Ursula ({work_order.ursula}) seems to be down while trying to complete WorkOrder: {work_order}")
self.log.info(f"Ursula ({work_order.ursula}) seems to be down while trying to complete WorkOrder: {work_order}")
continue
except self.network_middleware.NotFound:
# 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
self.log.warn(
f"Ursula ({work_order.ursula}) claims not to have the KFrag to complete WorkOrder: {work_order}. Has accessed been revoked?")
self.log.warn(f"Ursula ({work_order.ursula}) claims not to have the KFrag to complete WorkOrder: {work_order}. Has accessed been revoked?")
continue
except self.network_middleware.UnexpectedResponse:
raise # TODO: Handle this
for capsule, pre_task in work_order.tasks.items():
try:
@ -1551,11 +1551,10 @@ class Enrico(Character):
self.log = Logger(f'{self.__class__.__name__}-{bytes(self.public_keys(SigningPower)).hex()[:6]}')
self.log.info(self.banner.format(policy_encrypting_key))
def encrypt_message(self,
message: bytes
) -> Tuple[UmbralMessageKit, Signature]:
def encrypt_message(self, plaintext: bytes) -> Tuple[UmbralMessageKit, Signature]:
# TODO: #2107 Rename to "encrypt"
message_kit, signature = encrypt_and_sign(self.policy_pubkey,
plaintext=message,
plaintext=plaintext,
signer=self.stamp)
message_kit.policy_pubkey = self.policy_pubkey # TODO: We can probably do better here. NRN
return message_kit, signature

View File

@ -14,8 +14,10 @@
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 base64
import click
import ipfshttpclient
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import BobInterface
@ -286,13 +288,15 @@ def public_keys(general_config, character_options, config_file):
@option_config_file
@BobInterface.connect_cli('retrieve')
@group_general_config
@click.option('--ipfs', help="Download an encrypted message from IPFS at the specified gateway URI")
def retrieve(general_config,
character_options,
config_file,
label,
policy_encrypting_key,
alice_verifying_key,
message_kit):
message_kit,
ipfs):
"""Obtain plaintext from encrypted data, if access was granted."""
# Setup
@ -305,6 +309,15 @@ def retrieve(general_config,
required_fields = ', '.join(input_specification)
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
bob_request_data = {
'label': label,

View File

@ -17,6 +17,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click
import ipfshttpclient
from umbral.keys import UmbralPublicKey
from nucypher.characters.control.interfaces import EnricoInterface
@ -58,13 +59,29 @@ def run(general_config, policy_encrypting_key, dry_run, http_port):
@enrico.command()
@EnricoInterface.connect_cli('encrypt_message')
@click.option('--ipfs', help="Upload the encrypted message to IPFS at the specified gateway URI")
@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."""
# Setup
emitter = setup_emitter(general_config=general_config, banner=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)
# 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

View File

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

View File

@ -36,9 +36,12 @@ from nucypher.utilities.prometheus.collector import (
import json
from typing import List
from prometheus_client.core import Timestamp
from prometheus_client.registry import CollectorRegistry, REGISTRY
from prometheus_client.utils import floatToGoString
try:
from prometheus_client.core import Timestamp
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.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):
required_output_fileds = get_fields(interface, method_name)[-1]
required_output_fields = get_fields(interface, method_name)[-1]
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
return True

View File

@ -571,7 +571,7 @@ def test_collect_rewards_integration(click_runner,
# Encrypt
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
cleartexts = blockchain_bob.retrieve(message_kit,

View File

@ -473,7 +473,7 @@ def test_collect_rewards_integration(click_runner,
# Encrypt
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
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):
method_name, params = encrypt_control_request
endpoint = f'/{method_name}'
response = enrico_web_controller_test_client.post(endpoint, data=json.dumps(params))
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)
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.
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')
# Create
message_kit, signature = enrico.encrypt_message(message=plaintext_bytes)
message_kit, signature = enrico.encrypt_message(plaintext=plaintext_bytes)
# Serialize
message_kit_bytes = message_kit.to_bytes()