diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile
index a9fa0a494..5b96e87ff 100644
--- a/dev/docker/Dockerfile
+++ b/dev/docker/Dockerfile
@@ -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
diff --git a/docs/source/guides/development/getting_started.rst b/docs/source/guides/development/getting_started.rst
index 3dfd16656..a06d01986 100644
--- a/docs/source/guides/development/getting_started.rst
+++ b/docs/source/guides/development/getting_started.rst
@@ -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.
diff --git a/examples/populate_mario_box.py b/examples/populate_mario_box.py
index 4522983b2..3d8297e3f 100644
--- a/examples/populate_mario_box.py
+++ b/examples/populate_mario_box.py
@@ -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
diff --git a/nucypher/characters/control/controllers.py b/nucypher/characters/control/controllers.py
index d92bbd121..151c1f928 100644
--- a/nucypher/characters/control/controllers.py
+++ b/nucypher/characters/control/controllers.py
@@ -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):
diff --git a/nucypher/characters/control/interfaces.py b/nucypher/characters/control/interfaces.py
index 565d41ccb..65f3cc8d1 100644
--- a/nucypher/characters/control/interfaces.py
+++ b/nucypher/characters/control/interfaces.py
@@ -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
diff --git a/nucypher/characters/control/specifications/enrico.py b/nucypher/characters/control/specifications/enrico.py
index 3992acc8c..e19a6b172 100644
--- a/nucypher/characters/control/specifications/enrico.py
+++ b/nucypher/characters/control/specifications/enrico.py
@@ -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)
diff --git a/nucypher/characters/control/specifications/fields/__init__.py b/nucypher/characters/control/specifications/fields/__init__.py
index 89ace9086..17912d924 100644
--- a/nucypher/characters/control/specifications/fields/__init__.py
+++ b/nucypher/characters/control/specifications/fields/__init__.py
@@ -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 *
diff --git a/nucypher/characters/control/specifications/fields/file.py b/nucypher/characters/control/specifications/fields/file.py
new file mode 100644
index 000000000..ed551eeed
--- /dev/null
+++ b/nucypher/characters/control/specifications/fields/file.py
@@ -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 .
+"""
+
+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)
diff --git a/nucypher/characters/control/specifications/fields/signature.py b/nucypher/characters/control/specifications/fields/signature.py
new file mode 100644
index 000000000..77adf1b3a
--- /dev/null
+++ b/nucypher/characters/control/specifications/fields/signature.py
@@ -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 .
+"""
+
+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}")
diff --git a/nucypher/characters/lawful.py b/nucypher/characters/lawful.py
index 5a6fa6372..38e199e2e 100644
--- a/nucypher/characters/lawful.py
+++ b/nucypher/characters/lawful.py
@@ -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
diff --git a/nucypher/cli/commands/bob.py b/nucypher/cli/commands/bob.py
index 0d1d87ec1..c67b1dd2a 100644
--- a/nucypher/cli/commands/bob.py
+++ b/nucypher/cli/commands/bob.py
@@ -14,8 +14,10 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see .
"""
+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,
diff --git a/nucypher/cli/commands/enrico.py b/nucypher/cli/commands/enrico.py
index a670623bf..bd47b6921 100644
--- a/nucypher/cli/commands/enrico.py
+++ b/nucypher/cli/commands/enrico.py
@@ -17,6 +17,7 @@ along with nucypher. If not, see .
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
diff --git a/nucypher/network/middleware.py b/nucypher/network/middleware.py
index b9393bf7a..e8a96c921 100644
--- a/nucypher/network/middleware.py
+++ b/nucypher/network/middleware.py
@@ -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,
diff --git a/nucypher/utilities/prometheus/metrics.py b/nucypher/utilities/prometheus/metrics.py
index b66033b54..00af4b7c4 100644
--- a/nucypher/utilities/prometheus/metrics.py
+++ b/nucypher/utilities/prometheus/metrics.py
@@ -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
diff --git a/tests/acceptance/characters/control/test_rpc_control_blockchain.py b/tests/acceptance/characters/control/test_rpc_control_blockchain.py
index ecc0df863..6cce8a0d7 100644
--- a/tests/acceptance/characters/control/test_rpc_control_blockchain.py
+++ b/tests/acceptance/characters/control/test_rpc_control_blockchain.py
@@ -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
diff --git a/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py b/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py
index 2c95e4949..6843fcb53 100644
--- a/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py
+++ b/tests/acceptance/cli/ursula/test_stake_via_allocation_contract.py
@@ -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,
diff --git a/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py
index 250aca789..51e31c775 100644
--- a/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py
+++ b/tests/acceptance/cli/ursula/test_stakeholder_and_ursula.py
@@ -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,
diff --git a/tests/integration/characters/control/test_web_control_federated.py b/tests/integration/characters/control/test_web_control_federated.py
index cd6f6e49f..d79053d09 100644
--- a/tests/integration/characters/control/test_web_control_federated.py
+++ b/tests/integration/characters/control/test_web_control_federated.py
@@ -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
diff --git a/tests/integration/characters/federated_encrypt_and_decrypt.py b/tests/integration/characters/federated_encrypt_and_decrypt.py
index 51557d550..b2149cffd 100644
--- a/tests/integration/characters/federated_encrypt_and_decrypt.py
+++ b/tests/integration/characters/federated_encrypt_and_decrypt.py
@@ -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,
diff --git a/tests/unit/test_bytestring_types.py b/tests/unit/test_bytestring_types.py
index b8f739f96..123921f23 100644
--- a/tests/unit/test_bytestring_types.py
+++ b/tests/unit/test_bytestring_types.py
@@ -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()