Merge pull request #2987 from KPrasch/purge

Assorted deprecations (RPC and Web servers, Enrico CLI, Literature, Specifications, Ursula Interactivity)
pull/2991/head
KPrasch 2022-10-27 11:09:05 +02:00 committed by GitHub
commit a895b0d72e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 170 additions and 4146 deletions

View File

@ -0,0 +1,8 @@
Removals:
- RPC servers
- character WebControllers
- unused literature
- unused CLI option definitions
- CLI helper functions for Alice, Bob, Contacts interactivity
- interactive Ursula mode
- enrico CLI commands

View File

@ -17,16 +17,20 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import math
import os
import pprint
from pathlib import Path
from eth.typing import TransactionDict
from typing import Callable, NamedTuple, Tuple, Union, Optional
from typing import List
from urllib.parse import urlparse
import requests
from constant_sorrow.constants import (
INSUFFICIENT_ETH,
NO_BLOCKCHAIN_CONNECTION,
NO_COMPILATION_PERFORMED,
UNKNOWN_TX_STATUS
)
from eth.typing import TransactionDict
from eth_tester import EthereumTester
from eth_tester.exceptions import TransactionFailed as TestTransactionFailed
from eth_typing import ChecksumAddress
@ -39,15 +43,6 @@ from web3.middleware import geth_poa_middleware
from web3.providers import BaseProvider
from web3.types import TxReceipt
from constant_sorrow.constants import (
INSUFFICIENT_ETH,
NO_BLOCKCHAIN_CONNECTION,
NO_COMPILATION_PERFORMED,
READ_ONLY_INTERFACE,
UNKNOWN_TX_STATUS
)
from nucypher.crypto.powers import TransactingPower
from nucypher.blockchain.eth.clients import EthereumClient, POA_CHAINS, InfuraClient
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.providers import (
@ -63,7 +58,8 @@ from nucypher.blockchain.eth.sol.compile.compile import multiversion_compile
from nucypher.blockchain.eth.sol.compile.constants import SOLIDITY_SOURCE_ROOT
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
from nucypher.blockchain.eth.utils import get_transaction_name, prettify_eth_amount
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter
from nucypher.control.emitters import StdoutEmitter
from nucypher.crypto.powers import TransactingPower
from nucypher.utilities.ethereum import encode_constructor_arguments
from nucypher.utilities.gas_strategies import (
construct_datafeed_median_strategy,

View File

@ -93,16 +93,3 @@ Yb, `88 88 IP'`Yb
the Untrusted Re-Encryption Proxy.
{}
'''
STAKEHOLDER_BANNER = r"""
____ __ __
/\ _`\ /\ \__ /\ \
\ \,\L\_\ \ ,_\ __ \ \ \/'\ __ _ __
\/_\__ \\ \ \/ /'__`\\ \ , < /'__`\/\`'__\
/\ \L\ \ \ \_/\ \L\.\\ \ \\`\ /\ __/\ \ \/
\ `\____\ \__\ \__/.\_\ \_\ \_\ \____\\ \_\
\/_____/\/__/\/__/\/_/\/_/\/_/\/____/ \/_/
The Holder of Stakes.
"""

View File

@ -18,7 +18,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
from contextlib import suppress
from pathlib import Path
from typing import ClassVar, Dict, List, Optional, Union
from typing import ClassVar, Dict, List, Optional
from constant_sorrow.constants import (
NO_BLOCKCHAIN_CONNECTION,
@ -29,15 +29,12 @@ from constant_sorrow.constants import (
)
from eth_keys import KeyAPI as EthKeyAPI
from eth_utils import to_canonical_address
from nucypher_core import MessageKit
from nucypher_core.umbral import PublicKey
from nucypher.acumen.nicknames import Nickname
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
from nucypher.blockchain.eth.signers.base import Signer
from nucypher.characters.control.controllers import CharacterCLIController
from nucypher.control.controllers import JSONRPCController
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.powers import (
CryptoPower,
@ -380,24 +377,6 @@ class Character(Learner):
raise RuntimeError('Federated address can only be derived for federated characters.')
return federated_address
def make_rpc_controller(self, crash_on_error: bool = False):
app_name = bytes(self.stamp).hex()[:6]
controller = JSONRPCController(app_name=app_name,
crash_on_error=crash_on_error,
interface=self.interface)
self.controller = controller
return controller
def make_cli_controller(self, crash_on_error: bool = False):
app_name = bytes(self.stamp).hex()[:6]
controller = CharacterCLIController(app_name=app_name,
crash_on_error=crash_on_error,
interface=self.interface)
self.controller = controller
return controller
def disenchant(self):
self.log.debug(f"Disenchanting {self}")
Learner.stop_learning_loop(self)

View File

@ -1,16 +0,0 @@
"""
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/>.
"""

View File

@ -1,38 +0,0 @@
"""
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 nucypher.control.controllers import CLIController
from nucypher.control.emitters import StdoutEmitter
class CharacterCLIController(CLIController):
_emitter_class = StdoutEmitter
def __init__(self,
interface: 'CharacterPublicInterface',
*args,
**kwargs):
super().__init__(interface=interface, *args, **kwargs)
def _perform_action(self, *args, **kwargs) -> dict:
try:
response_data = super()._perform_action(*args, **kwargs)
finally:
self.log.debug(f"Finished action '{kwargs['action']}', stopping {self.interface.implementer}")
self.interface.implementer.disenchant()
return response_data

View File

@ -1,184 +0,0 @@
"""
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 typing import Union, List
import maya
from nucypher_core import MessageKit, HRAC, EncryptedTreasureMap
from nucypher_core.umbral import PublicKey
from nucypher.characters.base import Character
from nucypher.characters.control.specifications import alice, bob, enrico
from nucypher.control.interfaces import attach_schema, ControlInterface
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.network.middleware import RestMiddleware
class CharacterPublicInterface(ControlInterface):
def __init__(self, character: Character = None, *args, **kwargs):
super().__init__(implementer=character, *args, **kwargs)
class AliceInterface(CharacterPublicInterface):
@attach_schema(alice.CreatePolicy)
def create_policy(self,
bob_encrypting_key: PublicKey,
bob_verifying_key: PublicKey,
label: bytes,
threshold: int,
shares: int,
expiration: maya.MayaDT,
value: int = None
) -> dict:
from nucypher.characters.lawful import Bob
bob = Bob.from_public_keys(encrypting_key=bob_encrypting_key,
verifying_key=bob_verifying_key)
new_policy = self.implementer.create_policy(
bob=bob,
label=label,
threshold=threshold,
shares=shares,
expiration=expiration,
value=value
)
response_data = {'label': new_policy.label, 'policy_encrypting_key': new_policy.public_key}
return response_data
@attach_schema(alice.DerivePolicyEncryptionKey)
def derive_policy_encrypting_key(self, label: bytes) -> dict:
policy_encrypting_key = self.implementer.get_policy_encrypting_key_from_label(label)
response_data = {'policy_encrypting_key': policy_encrypting_key, 'label': label}
return response_data
@attach_schema(alice.GrantPolicy)
def grant(self,
bob_encrypting_key: PublicKey,
bob_verifying_key: PublicKey,
label: bytes,
threshold: int,
shares: int,
expiration: maya.MayaDT,
value: int = None,
rate: int = None,
) -> dict:
from nucypher.characters.lawful import Bob
bob = Bob.from_public_keys(encrypting_key=bob_encrypting_key,
verifying_key=bob_verifying_key)
new_policy = self.implementer.grant(bob=bob,
label=label,
threshold=threshold,
shares=shares,
value=value,
rate=rate,
expiration=expiration)
response_data = {'treasure_map': new_policy.treasure_map,
'policy_encrypting_key': new_policy.public_key,
# For the users of this interface, Publisher is always the same as Alice,
# so we are only returning the Alice's key.
'alice_verifying_key': self.implementer.stamp.as_umbral_pubkey()}
return response_data
@attach_schema(alice.Revoke)
def revoke(self, label: bytes, bob_verifying_key: PublicKey) -> dict:
# TODO: Move deeper into characters
policy_hrac = HRAC(self.implementer.stamp.as_umbral_pubkey(), bob_verifying_key, label)
policy = self.implementer.active_policies[policy_hrac]
receipt, failed_revocations = self.implementer.revoke(policy)
if len(failed_revocations) > 0:
for node_id, attempt in failed_revocations.items():
revocation, fail_reason = attempt
if fail_reason == RestMiddleware.NotFound:
del (failed_revocations[node_id])
if len(failed_revocations) <= (policy.shares - policy.threshold + 1):
del (self.implementer.active_policies[policy_hrac])
response_data = {'failed_revocations': len(failed_revocations)}
return response_data
@attach_schema(alice.Decrypt)
def decrypt(self, label: bytes, message_kit: MessageKit) -> dict:
"""
Character control endpoint to allow Alice to decrypt her own data.
"""
plaintexts = self.implementer.decrypt_message_kit(
message_kit=message_kit,
label=label
)
response = {'cleartexts': plaintexts}
return response
@attach_schema(alice.PublicKeys)
def public_keys(self) -> dict:
"""
Character control endpoint for getting Alice's public keys.
"""
verifying_key = self.implementer.public_keys(SigningPower)
response_data = {'alice_verifying_key': verifying_key}
return response_data
class BobInterface(CharacterPublicInterface):
@attach_schema(bob.RetrieveAndDecrypt)
def retrieve_and_decrypt(self,
alice_verifying_key: PublicKey,
message_kits: List[MessageKit],
encrypted_treasure_map: EncryptedTreasureMap) -> dict:
"""
Character control endpoint for re-encrypting and decrypting policy data.
"""
plaintexts = self.implementer.retrieve_and_decrypt(message_kits,
alice_verifying_key=alice_verifying_key,
encrypted_treasure_map=encrypted_treasure_map)
response_data = {'cleartexts': plaintexts}
return response_data
@attach_schema(bob.PublicKeys)
def public_keys(self) -> dict:
"""
Character control endpoint for getting Bob's encrypting and signing public keys
"""
verifying_key = self.implementer.public_keys(SigningPower)
encrypting_key = self.implementer.public_keys(DecryptingPower)
response_data = {'bob_encrypting_key': encrypting_key, 'bob_verifying_key': verifying_key}
return response_data
class EnricoInterface(CharacterPublicInterface):
@attach_schema(enrico.EncryptMessage)
def encrypt_message(self, plaintext: Union[str, bytes]) -> dict:
"""
Character control endpoint for encrypting data for a policy and
receiving the messagekit (and signature) to give to Bob.
"""
message_kit = self.implementer.encrypt_message(plaintext=plaintext)
response_data = {'message_kit': message_kit}
return response_data

View File

@ -1,16 +0,0 @@
"""
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/>.
"""

View File

@ -1,151 +0,0 @@
"""
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 click
from marshmallow import validates_schema
from nucypher.characters.control.specifications import fields as character_fields
from nucypher.control.specifications import fields as base_fields
from nucypher.control.specifications.base import BaseSchema
from nucypher.control.specifications.exceptions import InvalidArgumentCombo
from nucypher.cli import options, types
class PolicyBaseSchema(BaseSchema):
bob_encrypting_key = character_fields.Key(
required=True, load_only=True,
click=click.option(
'--bob-encrypting-key',
'-bek',
help="Bob's encrypting key as a hexadecimal string",
type=click.STRING, required=False))
bob_verifying_key = character_fields.Key(
required=True, load_only=True,
click=click.option(
'--bob-verifying-key',
'-bvk',
help="Bob's verifying key as a hexadecimal string",
type=click.STRING, required=False))
threshold = base_fields.PositiveInteger(
required=True, load_only=True,
click=options.option_threshold)
shares = base_fields.PositiveInteger(
required=True, load_only=True,
click=options.option_shares)
expiration = character_fields.DateTime(
required=True, load_only=True,
click=click.option(
'--expiration',
help="Expiration Datetime of a policy",
type=click.DateTime())
)
# optional input
value = character_fields.Wei(
load_only=True,
click=click.option('--value', help="Total policy value (in Wei)", type=types.WEI))
rate = character_fields.Wei(
load_only=True,
required=False,
click=options.option_rate
)
# output
policy_encrypting_key = character_fields.Key(dump_only=True)
@validates_schema
def check_valid_n_and_m(self, data, **kwargs):
# ensure that n is greater than or equal to m
if not (0 < data['threshold'] <= data['shares']):
raise InvalidArgumentCombo(f"`shares` and `threshold` must satisfy 0 < threshold ≤ shares")
@validates_schema
def check_rate_or_value_not_both(self, data, **kwargs):
if (data.get('rate') is not None) and (data.get('value') is not None):
raise InvalidArgumentCombo("Choose either rate (per period in duration) OR value (total for duration)")
# TODO: decide if we should inject config defaults before this validation
# if not (data.get('rate', 0) ^ data.get('value', 0)):
# raise InvalidArgumentCombo("Either rate or value must be greater than zero.")
class CreatePolicy(PolicyBaseSchema):
label = character_fields.Label(
required=True,
click=options.option_label(required=True))
class GrantPolicy(PolicyBaseSchema):
label = character_fields.Label(
load_only=True, required=True,
click=options.option_label(required=False))
# output fields
# treasure map only used for serialization so no need to provide federated/non-federated context
treasure_map = character_fields.EncryptedTreasureMap(dump_only=True)
alice_verifying_key = character_fields.Key(dump_only=True)
class DerivePolicyEncryptionKey(BaseSchema):
label = character_fields.Label(
required=True,
click=options.option_label(required=True))
# output
policy_encrypting_key = character_fields.Key(dump_only=True)
class Revoke(BaseSchema):
label = character_fields.Label(
required=True, load_only=True,
click=options.option_label(required=True))
bob_verifying_key = character_fields.Key(
required=True, load_only=True,
click=click.option(
'--bob-verifying-key',
'-bvk',
help="Bob's verifying key as a hexadecimal string", type=click.STRING,
required=True))
# output
failed_revocations = base_fields.Integer(dump_only=True)
class Decrypt(BaseSchema):
label = character_fields.Label(
required=True, load_only=True,
click=options.option_label(required=True))
message_kit = character_fields.MessageKit(
load_only=True,
click=options.option_message_kit(required=True))
# output
cleartexts = base_fields.List(character_fields.Cleartext(), dump_only=True)
class PublicKeys(BaseSchema):
alice_verifying_key = character_fields.Key(dump_only=True)

View File

@ -1,48 +0,0 @@
"""
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 nucypher.control.specifications.fields as base_fields
from nucypher.characters.control.specifications import fields as character_fields
from nucypher.characters.control.specifications.fields.treasuremap import EncryptedTreasureMap
from nucypher.cli import options
from nucypher.control.specifications.base import BaseSchema
class RetrieveAndDecrypt(BaseSchema):
alice_verifying_key = character_fields.Key(
required=True,
load_only=True,
click=options.option_alice_verifying_key(required=True)
)
message_kits = base_fields.StringList(
character_fields.MessageKit(),
required=True,
load_only=True,
click=options.option_message_kit(required=True, multiple=True)
)
encrypted_treasure_map = EncryptedTreasureMap(required=True,
load_only=True,
click=options.option_treasure_map)
# output
cleartexts = base_fields.List(character_fields.Cleartext(), dump_only=True)
class PublicKeys(BaseSchema):
bob_encrypting_key = character_fields.Key(dump_only=True)
bob_verifying_key = character_fields.Key(dump_only=True)

View File

@ -1,68 +0,0 @@
"""
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 click
from marshmallow import post_load
import nucypher.control.specifications.exceptions
from nucypher.characters.control.specifications import fields
from nucypher.cli import options
from nucypher.cli.types import EXISTING_READABLE_FILE
from nucypher.control.specifications.base import BaseSchema
class EncryptMessage(BaseSchema):
# input
message = fields.Cleartext(
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()
)
@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 nucypher.control.specifications.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.MessageKit(dump_only=True)

View File

@ -1,25 +0,0 @@
"""
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 nucypher.characters.control.specifications.fields.key import *
from nucypher.characters.control.specifications.fields.treasuremap import *
from nucypher.characters.control.specifications.fields.messagekit import *
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 *

View File

@ -1,31 +0,0 @@
"""
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 b64encode
from marshmallow import fields
from nucypher.control.specifications.fields.base import BaseField
class Cleartext(BaseField, fields.String):
def _serialize(self, value, attr, obj, **kwargs):
return value.decode()
def _deserialize(self, value, attr, data, **kwargs):
return b64encode(bytes(value, encoding='utf-8')).decode()

View File

@ -1,34 +0,0 @@
"""
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 maya
from marshmallow import fields
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields.base import BaseField
class DateTime(BaseField, fields.Field):
def _serialize(self, value, attr, obj, **kwargs):
return value.iso8601()
def _deserialize(self, value, attr, data, **kwargs):
try:
return maya.MayaDT.from_iso8601(iso8601_string=value)
except maya.pendulum.parsing.ParserError as e:
raise InvalidInputData(f"Could not convert input for {self.name} to a valid date time: {e}")

View File

@ -1,36 +0,0 @@
"""
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 pathlib import Path
from marshmallow import fields
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields.base import BaseField
class FileField(BaseField, fields.String):
def _deserialize(self, value, attr, data, **kwargs):
p = Path(value)
if not p.exists():
raise InvalidInputData(f"Filepath {value} does not exist")
if not p.is_file():
raise InvalidInputData(f"Filepath {value} does not map to a file")
with p.open(mode='rb') as plaintext_file:
plaintext = plaintext_file.read() # TODO: #2106 Handle large files
return plaintext

View File

@ -1,31 +0,0 @@
"""
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 marshmallow import fields
from nucypher.control.specifications.fields.base import BaseField
class Label(BaseField, fields.Field):
def _serialize(self, value, attr, obj, **kwargs):
return value.decode('utf-8')
def _deserialize(self, value, attr, data, **kwargs):
if isinstance(value, bytes):
return value
return value.encode()

View File

@ -1,23 +0,0 @@
"""
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 nucypher.control.specifications.fields.base import Integer
from nucypher.cli import types
class Wei(Integer):
click_type = types.WEI

View File

@ -1,45 +0,0 @@
"""
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 nucypher_core import EncryptedTreasureMap as EncryptedTreasureMapClass, TreasureMap as TreasureMapClass
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields.base import Base64BytesRepresentation
class EncryptedTreasureMap(Base64BytesRepresentation):
"""
JSON Parameter representation of EncryptedTreasureMap.
"""
def _deserialize(self, value, attr, data, **kwargs):
try:
encrypted_treasure_map_bytes = super()._deserialize(value, attr, data, **kwargs)
return EncryptedTreasureMapClass.from_bytes(encrypted_treasure_map_bytes)
except Exception as e:
raise InvalidInputData(f"Could not convert input for {self.name} to an EncryptedTreasureMap: {e}") from e
class TreasureMap(Base64BytesRepresentation):
"""
JSON Parameter representation of (unencrypted) TreasureMap.
"""
def _deserialize(self, value, attr, data, **kwargs):
try:
treasure_map_bytes = super()._deserialize(value, attr, data, **kwargs)
return TreasureMapClass.from_bytes(treasure_map_bytes)
except Exception as e:
raise InvalidInputData(f"Could not convert input for {self.name} to a TreasureMap: {e}") from e

View File

@ -19,9 +19,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import contextlib
import json
import time
from base64 import b64encode
from http import HTTPStatus
from json.decoder import JSONDecodeError
from pathlib import Path
from queue import Queue
from typing import (
@ -50,7 +47,6 @@ from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID
from eth_typing.evm import ChecksumAddress
from eth_utils import to_checksum_address
from flask import Response, request
from nucypher_core import (
Address,
HRAC,
@ -83,14 +79,7 @@ from nucypher.characters.banners import (
URSULA_BANNER,
)
from nucypher.characters.base import Character, Learner
from nucypher.characters.control.interfaces import (
AliceInterface,
BobInterface,
EnricoInterface,
)
from nucypher.cli.processes import UrsulaCommandProtocol
from nucypher.config.storages import NodeStorage
from nucypher.control.controllers import WebController
from nucypher.control.emitters import StdoutEmitter
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.powers import (
@ -117,7 +106,6 @@ from nucypher.utilities.networking import validate_operator_ip
class Alice(Character, BlockchainPolicyAuthor):
banner = ALICE_BANNER
_interface_class = AliceInterface
_default_crypto_powerups = [SigningPower, DecryptingPower, DelegatingPower]
def __init__(self,
@ -142,12 +130,10 @@ class Alice(Character, BlockchainPolicyAuthor):
# Policy Storage
store_policy_credentials: bool = None,
store_character_cards: bool = None,
# Middleware
timeout: int = 10, # seconds # TODO: configure NRN
network_middleware: RestMiddleware = None,
controller: bool = True,
*args, **kwargs) -> None:
@ -192,8 +178,6 @@ class Alice(Character, BlockchainPolicyAuthor):
self.log = Logger(self.__class__.__name__)
if is_me:
if controller:
self.make_cli_controller()
# Policy Payment
if federated_only and not payment_method:
@ -209,15 +193,9 @@ class Alice(Character, BlockchainPolicyAuthor):
self.active_policies = dict()
self.revocation_kits = dict()
self.store_policy_credentials = store_policy_credentials
self.store_character_cards = store_character_cards
self.log.info(self.banner)
def get_card(self) -> 'Card':
from nucypher.policy.identity import Card
card = Card.from_character(self)
return card
def add_active_policy(self, active_policy):
"""
Adds a Policy object that is active on the NuCypher network to Alice's
@ -453,74 +431,9 @@ class Alice(Character, BlockchainPolicyAuthor):
# Shouldn't it be able to take a list of them too?
return [cleartext]
def make_web_controller(drone_alice, crash_on_error: bool = False):
app_name = bytes(drone_alice.stamp).hex()[:6]
controller = WebController(app_name=app_name,
crash_on_error=crash_on_error,
interface=drone_alice._interface_class(character=drone_alice))
drone_alice.controller = controller
# Register Flask Decorator
alice_flask_control = controller.make_control_transport()
#
# Character Control HTTP Endpoints
#
@alice_flask_control.route('/public_keys', methods=['GET'])
def public_keys():
"""
Character control endpoint for getting Alice's encrypting and signing public keys
"""
return controller(method_name='public_keys', control_request=request)
@alice_flask_control.route("/create_policy", methods=['PUT'])
def create_policy() -> Response:
"""
Character control endpoint for creating an enacted network policy
"""
response = controller(method_name='create_policy', control_request=request)
return response
@alice_flask_control.route("/decrypt", methods=['POST'])
def decrypt():
"""
Character control endpoint for decryption of Alice's own policy data.
"""
response = controller(method_name='decrypt', control_request=request)
return response
@alice_flask_control.route('/derive_policy_encrypting_key/<label>', methods=['POST'])
def derive_policy_encrypting_key(label) -> Response:
"""
Character control endpoint for deriving a policy encrypting given a unicode label.
"""
response = controller(method_name='derive_policy_encrypting_key', control_request=request, label=label)
return response
@alice_flask_control.route("/grant", methods=['PUT'])
def grant() -> Response:
"""
Character control endpoint for policy granting.
"""
response = controller(method_name='grant', control_request=request)
return response
@alice_flask_control.route("/revoke", methods=['DELETE'])
def revoke():
"""
Character control endpoint for policy revocation.
"""
response = controller(method_name='revoke', control_request=request)
return response
return controller
class Bob(Character):
banner = BOB_BANNER
_interface_class = BobInterface
_default_crypto_powerups = [SigningPower, DecryptingPower]
class IncorrectCFragsReceived(Exception):
@ -533,7 +446,6 @@ class Bob(Character):
def __init__(self,
is_me: bool = True,
controller: bool = True,
verify_node_bonding: bool = False,
eth_provider_uri: str = None,
*args, **kwargs) -> None:
@ -545,9 +457,6 @@ class Bob(Character):
eth_provider_uri=eth_provider_uri,
*args, **kwargs)
if controller:
self.make_cli_controller()
# Cache of decrypted treasure maps
self._treasure_maps: Dict[int, TreasureMap] = {}
@ -555,11 +464,6 @@ class Bob(Character):
if is_me:
self.log.info(self.banner)
def get_card(self) -> 'Card':
from nucypher.policy.identity import Card
card = Card.from_character(self)
return card
def _decrypt_treasure_map(self,
encrypted_treasure_map: EncryptedTreasureMap,
publisher_verifying_key: PublicKey
@ -654,38 +558,6 @@ class Bob(Character):
return cleartexts
def make_web_controller(drone_bob, crash_on_error: bool = False):
app_name = bytes(drone_bob.stamp).hex()[:6]
controller = WebController(app_name=app_name,
crash_on_error=crash_on_error,
interface=drone_bob._interface_class(character=drone_bob))
drone_bob.controller = controller.make_control_transport()
# Register Flask Decorator
bob_control = controller.make_control_transport()
#
# Character Control HTTP Endpoints
#
@bob_control.route('/public_keys', methods=['GET'])
def public_keys():
"""
Character control endpoint for getting Bob's encrypting and signing public keys
"""
return controller(method_name='public_keys', control_request=request)
@bob_control.route('/retrieve_and_decrypt', methods=['POST'])
def retrieve_and_decrypt():
"""
Character control endpoint for re-encrypting and decrypting policy
data.
"""
return controller(method_name='retrieve_and_decrypt', control_request=request)
return controller
class Ursula(Teacher, Character, Operator):
@ -910,7 +782,6 @@ class Ursula(Teacher, Character, Operator):
discovery: bool = True, # TODO: see below
availability: bool = False,
worker: bool = True,
interactive: bool = False,
hendrix: bool = True,
start_reactor: bool = True,
prometheus_config: 'PrometheusMetricsConfig' = None,
@ -978,9 +849,6 @@ class Ursula(Teacher, Character, Operator):
if emitter:
emitter.message(f"✓ Prometheus Exporter", color='green')
if interactive and emitter:
stdio.StandardIO(UrsulaCommandProtocol(ursula=self, emitter=emitter))
if hendrix:
if emitter:
emitter.message(f"✓ Rest Server https://{self.rest_interface}", color='green')
@ -1328,13 +1196,11 @@ class Enrico(Character):
"""A Character that represents a Data Source that encrypts data for some policy's public key"""
banner = ENRICO_BANNER
_interface_class = EnricoInterface
_default_crypto_powerups = [SigningPower]
def __init__(self,
is_me: bool = True,
policy_encrypting_key: Optional[PublicKey] = None,
controller: bool = True,
*args, **kwargs):
self._policy_pubkey = policy_encrypting_key
@ -1344,9 +1210,6 @@ class Enrico(Character):
kwargs['known_node_class'] = None
super().__init__(is_me=is_me, *args, **kwargs)
if controller:
self.make_cli_controller()
self.log = Logger(f'{self.__class__.__name__}-{bytes(self.public_keys(SigningPower)).hex()[:6]}')
if is_me:
self.log.info(self.banner.format(policy_encrypting_key))
@ -1376,48 +1239,4 @@ class Enrico(Character):
return self._policy_pubkey
def _set_known_node_class(self, *args, **kwargs):
"""
Enrico doesn't init nodes, so it doesn't care what class they are.
"""
def make_web_controller(drone_enrico, crash_on_error: bool = False):
app_name = bytes(drone_enrico.stamp).hex()[:6]
controller = WebController(app_name=app_name,
crash_on_error=crash_on_error,
interface=drone_enrico._interface_class(character=drone_enrico))
drone_enrico.controller = controller
# Register Flask Decorator
enrico_control = controller.make_control_transport()
#
# Character Control HTTP Endpoints
#
@enrico_control.route('/encrypt_message', methods=['POST'])
def encrypt_message():
"""
Character control endpoint for encrypting data for a policy and
receiving the messagekit (and signature) to give to Bob.
"""
try:
request_data = json.loads(request.data)
message = request_data['message']
except (KeyError, JSONDecodeError) as e:
return Response(str(e), status=HTTPStatus.BAD_REQUEST)
# Encrypt
message_kit = drone_enrico.encrypt_message(bytes(message, encoding='utf-8'))
response_data = {
'result': {
'message_kit': b64encode(bytes(message_kit)).decode(), # FIXME, but NRN
},
'version': str(nucypher.__version__)
}
return Response(json.dumps(response_data), status=HTTPStatus.OK)
return controller
"""Enrico doesn't init nodes, so it doesn't care what class they are."""

View File

@ -23,7 +23,6 @@ from constant_sorrow.constants import NO_PASSWORD
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.control.emitters import StdoutEmitter
from nucypher.cli.literature import (
COLLECT_ETH_PASSWORD,
COLLECT_NUCYPHER_PASSWORD,
@ -33,6 +32,7 @@ from nucypher.cli.literature import (
)
from nucypher.config.base import CharacterConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD
from nucypher.control.emitters import StdoutEmitter
from nucypher.crypto.keystore import Keystore, _WORD_COUNT

View File

@ -1,180 +0,0 @@
"""
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 collections import namedtuple
import click
import maya
from constant_sorrow.constants import FEDERATED
from datetime import timedelta
from typing import Tuple
from web3.main import Web3
from nucypher.control.emitters import StdoutEmitter
from nucypher.characters.lawful import Bob, Alice
from nucypher.cli.painting.help import enforce_probationary_period, paint_probationary_period_disclaimer
from nucypher.cli.painting.policies import paint_single_card
from nucypher.cli.types import GWEI
from nucypher.policy.identity import Card
PublicKeys = namedtuple('PublicKeys', 'encrypting_key verifying_key')
PolicyParameters = namedtuple('PolicyParameters', 'label threshold shares expiration value rate')
def collect_keys_from_card(emitter: StdoutEmitter, card_identifier: str, force: bool):
emitter.message(f"Searching contacts for {card_identifier}\n", color='yellow')
card = Card.load(identifier=card_identifier)
if card.character is not Bob:
emitter.error('Grantee card is not a Bob.')
raise click.Abort
paint_single_card(emitter=emitter, card=card)
if not force:
click.confirm('Is this the correct grantee (Bob)?', abort=True)
bob_encrypting_key = bytes(card.encrypting_key).hex()
bob_verifying_key = bytes(card.verifying_key).hex()
public_keys = PublicKeys(encrypting_key=bob_encrypting_key, verifying_key=bob_verifying_key)
return public_keys
def collect_bob_public_keys(
emitter: StdoutEmitter,
force: bool,
card_identifier: str,
bob_encrypting_key: str,
bob_verifying_key: str
) -> PublicKeys:
"""helper function for collecting Bob's public keys interactively in the Alice CLI."""
if card_identifier:
public_keys = collect_keys_from_card(
emitter=emitter,
card_identifier=card_identifier,
force=force)
return public_keys
if not bob_encrypting_key:
bob_encrypting_key = click.prompt("Enter Bob's encrypting key")
if not bob_verifying_key:
bob_verifying_key = click.prompt("Enter Bob's verifying key")
public_keys = PublicKeys(encrypting_key=bob_encrypting_key, verifying_key=bob_verifying_key)
return public_keys
def collect_label(label: str, bob_identifier: str):
if not label:
label = click.prompt(f'Enter label to grant Bob {bob_identifier}', type=click.STRING)
return label
def collect_expiration(alice: Alice, expiration: maya.MayaDT, force: bool) -> maya.MayaDT:
# TODO: Support interactive expiration periods?
if not force and not expiration:
default_expiration = None
expiration_prompt = 'Enter policy expiration (Y-M-D H:M:S)'
if alice.duration:
default_expiration = maya.now() + timedelta(hours=alice.duration * alice.economics.hours_per_period)
expiration = click.prompt(expiration_prompt, type=click.DateTime(), default=default_expiration)
return expiration
def collect_redundancy_ratio(alice: Alice, threshold: int, shares: int, force: bool) -> Tuple[int, int]:
# Policy Threshold and Shares
if not shares:
shares = alice.shares
if not force and not click.confirm(f'Use default value for N ({shares})?', default=True):
shares = click.prompt('Enter total number of shares (N)', type=click.INT)
if not threshold:
threshold = alice.threshold
if not force and not click.confirm(f'Use default value for M ({threshold})?', default=True):
threshold = click.prompt('Enter threshold (M)', type=click.IntRange(1, shares))
return threshold, shares
def collect_policy_rate_and_value(alice: Alice, rate: int, value: int, shares: int, force: bool) -> Tuple[int, int]:
policy_value_provided = bool(value) or bool(rate)
if not policy_value_provided:
# TODO #1709 - Fine tuning and selection of default rates
rate = alice.payment_method.rate # wei
if not force:
default_gwei = Web3.from_wei(rate, 'gwei') # wei -> gwei
prompt = "Confirm rate of {node_rate} gwei * {shares} nodes ({period_rate} gwei per period)?"
if not click.confirm(prompt.format(node_rate=default_gwei, period_rate=default_gwei * shares, shares=shares), default=True):
interactive_rate = click.prompt('Enter rate per period in gwei', type=GWEI)
# TODO: Interactive rate sampling & validation (#1709)
interactive_prompt = prompt.format(node_rate=interactive_rate, period_rate=interactive_rate * shares, shares=shares)
click.confirm(interactive_prompt, default=True, abort=True)
rate = Web3.to_wei(interactive_rate, 'gwei') # gwei -> wei
return rate, value
def collect_policy_parameters(
emitter: StdoutEmitter,
alice: Alice,
force: bool,
bob_identifier: str,
label: str,
threshold: int,
shares: int,
value: int,
rate: int,
expiration: maya.MayaDT
) -> PolicyParameters:
# Interactive collection follows:
# - Disclaimer
# - Label
# - Expiration Date & Time
# - M of N
# - Policy Value (ETH)
label = collect_label(label=label, bob_identifier=bob_identifier)
# TODO: Remove this line when the time is right.
paint_probationary_period_disclaimer(emitter)
expiration = collect_expiration(alice=alice, expiration=expiration, force=force)
enforce_probationary_period(emitter=emitter, expiration=expiration)
threshold, shares = collect_redundancy_ratio(alice=alice, threshold=threshold, shares=shares, force=force)
if alice.federated_only:
rate, value = FEDERATED, FEDERATED
else:
rate, value = collect_policy_rate_and_value(
alice=alice,
rate=rate,
value=value,
shares=shares,
force=force)
policy_parameters = PolicyParameters(
label=label,
threshold=threshold,
shares=shares,
expiration=expiration,
rate=rate,
value=value
)
return policy_parameters

View File

@ -92,40 +92,3 @@ def verify_upgrade_details(blockchain: Union[BlockchainDeployerInterface, Blockc
click.confirm(CONFIRM_VERSIONED_UPGRADE.format(contract_name=deployer.contract_name,
old_version=old_contract.version,
new_version=new_version), abort=True)
def confirm_staged_grant(emitter, grant_request: Dict, federated: bool, seconds_per_period=None) -> None:
pretty_request = grant_request.copy() # WARNING: Do not mutate
if federated: # Boring
table = [[field.capitalize(), value] for field, value in pretty_request.items()]
emitter.echo(tabulate(table, tablefmt="simple"))
return
period_rate = Web3.from_wei(pretty_request['shares'] * pretty_request['rate'], 'gwei')
pretty_request['rate'] = f"{pretty_request['rate']} wei/period * {pretty_request['shares']} nodes"
expiration = pretty_request['expiration']
periods = calculate_period_duration(future_time=MayaDT.from_datetime(expiration),
seconds_per_period=seconds_per_period)
periods += 1 # current period is always included
pretty_request['expiration'] = f"{pretty_request['expiration']} ({periods} periods)"
# M of N
pretty_request['Threshold Shares'] = f"{pretty_request['threshold']} of {pretty_request['shares']}"
del pretty_request['threshold']
del pretty_request['shares']
def prettify_field(field):
field_words = [word.capitalize() for word in field.split('_')]
field = ' '.join(field_words)
return field
table = [[prettify_field(field), value] for field, value in pretty_request.items()]
table.append(['Period Rate', f'{period_rate} gwei'])
table.append(['Policy Value', f'{period_rate * periods} gwei'])
emitter.echo("\nSuccessfully staged grant, Please review the details:\n", color='green')
emitter.echo(tabulate(table, tablefmt="simple"))
click.confirm('\nGrant access and sign transaction?', abort=True)

View File

@ -1,82 +0,0 @@
"""
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 collections import namedtuple
import click
import maya
from nucypher.control.emitters import StdoutEmitter
from nucypher.characters.lawful import Alice
Precondition = namedtuple('Precondition', 'options condition')
def validate_grant_command(
emitter: StdoutEmitter,
alice: Alice,
force: bool,
bob: str,
bob_verifying_key: str,
bob_encrypting_key: str,
label: str,
expiration: maya.MayaDT,
rate: int,
value: int
):
# Force mode validation
if force:
required = (
Precondition(
options='--bob or --bob-encrypting-key and --bob-verifying-key.',
condition=bob or all((bob_verifying_key, bob_encrypting_key))
),
Precondition(options='--label', condition=bool(label)),
Precondition(options='--expiration', condition=bool(expiration))
)
triggered = False
for condition in required:
# see what condition my condition was in.
if not condition.condition:
triggered = True
emitter.error(f'Missing options in force mode: {condition.options}', color="red")
if triggered:
raise click.Abort()
# Handle federated
if alice.federated_only:
if any((value, rate)):
message = "Can't use --value or --rate with a federated Alice."
raise click.BadOptionUsage(option_name="--value, --rate", message=click.style(message, fg="red"))
elif bool(value) and bool(rate):
raise click.BadOptionUsage(option_name="--rate", message=click.style("Can't use --value if using --rate", fg="red"))
# From Bob card
if bob:
if any((bob_encrypting_key, bob_verifying_key)):
message = '--bob cannot be used with --bob-encrypting-key or --bob-verifying key'
raise click.BadOptionUsage(option_name='--bob', message=click.style(message, fg="red"))
# From hex public keys
else:
if not all((bob_encrypting_key, bob_verifying_key)):
if force:
emitter.message('Missing options in force mode: --bob or --bob-encrypting-key and --bob-verifying-key.', color="red")
click.Abort()
emitter.message("*Caution: Only enter public keys*")

View File

@ -1,106 +0,0 @@
"""
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 click
from nucypher_core.umbral import PublicKey
from nucypher.characters.control.interfaces import EnricoInterface
from nucypher.characters.lawful import Enrico
from nucypher.cli.utils import setup_emitter
from nucypher.cli.config import group_general_config
from nucypher.cli.options import option_dry_run, option_policy_encrypting_key
from nucypher.cli.types import NETWORK_PORT
@click.group()
def enrico():
""""Enrico the Encryptor" management commands."""
@enrico.command()
@option_policy_encrypting_key(required=True)
@option_dry_run
@click.option('--http-port', help="The host port to run Enrico HTTP services on", type=NETWORK_PORT)
@group_general_config
def run(general_config, policy_encrypting_key, dry_run, http_port):
"""Start Enrico's controller."""
# Setup
emitter = setup_emitter(general_config, policy_encrypting_key)
ENRICO = _create_enrico(emitter, policy_encrypting_key)
# RPC
if general_config.json_ipc:
rpc_controller = ENRICO.make_rpc_controller()
_transport = rpc_controller.make_control_transport()
rpc_controller.start()
return
ENRICO.log.info('Starting HTTP Character Web Controller')
controller = ENRICO.make_web_controller()
return controller.start(port=http_port, dry_run=dry_run)
@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, file, ipfs):
"""Encrypt a message under a given policy public key."""
emitter = setup_emitter(general_config=general_config, banner=policy_encrypting_key)
if ipfs:
try:
import ipfshttpclient
except ImportError:
raise ImportError("IPFS HTTP client not installed. Run 'pip install ipfshttpclient' then try again.")
# Connect to IPFS before proceeding
ipfs_client = ipfshttpclient.connect(ipfs)
emitter.message(f"Connected to IPFS Gateway {ipfs}")
if not policy_encrypting_key:
policy_encrypting_key = click.prompt("Enter policy encrypting key", type=click.STRING)
ENRICO = _create_enrico(emitter, policy_encrypting_key)
if message and file:
emitter.error(f'Pass either --message or --file, not both.')
raise click.Abort
if not message and not file:
message = click.prompt('Enter plaintext to encrypt', type=click.STRING)
# Encryption Request
encryption_request = {'policy_encrypting_key': policy_encrypting_key, 'message': message, 'file': file}
response = ENRICO.controller.encrypt_message(request=encryption_request)
# Handle ciphertext upload to sidechannel
if ipfs:
cid = ipfs_client.add_str(response['message_kit'])
emitter.message(f"Uploaded message kit to IPFS (CID {cid})", color='green')
return response
def _create_enrico(emitter, policy_encrypting_key) -> Enrico:
policy_encrypting_key = PublicKey.from_bytes(bytes.fromhex(policy_encrypting_key))
ENRICO = Enrico(policy_encrypting_key=policy_encrypting_key)
ENRICO.controller.emitter = emitter
return ENRICO

View File

@ -398,14 +398,13 @@ def forget(general_config, config_options, config_file):
@option_dry_run
@option_force
@group_general_config
@click.option('--interactive', '-I', help="Run interactively", is_flag=True, default=False)
@click.option('--prometheus', help="Run the ursula prometheus exporter", is_flag=True, default=False)
@click.option('--metrics-port', help="Run a Prometheus metrics exporter on specified HTTP port", type=NETWORK_PORT)
@click.option("--metrics-listen-address", help="Run a prometheus metrics exporter on specified IP address", default='')
@click.option("--metrics-prefix", help="Create metrics params with specified prefix", default="ursula")
@click.option("--metrics-interval", help="The frequency of metrics collection", type=click.INT, default=90)
@click.option("--ip-checkup/--no-ip-checkup", help="Verify external IP matches configuration", default=True)
def run(general_config, character_options, config_file, interactive, dry_run, prometheus, metrics_port,
def run(general_config, character_options, config_file, dry_run, prometheus, metrics_port,
metrics_listen_address, metrics_prefix, metrics_interval, force, ip_checkup):
"""Run an "Ursula" node."""
@ -440,7 +439,6 @@ def run(general_config, character_options, config_file, interactive, dry_run, pr
try:
URSULA.run(emitter=emitter,
start_reactor=not dry_run,
interactive=interactive,
prometheus_config=prometheus_config,
preflight=not dev_mode)
finally:

View File

@ -15,14 +15,16 @@
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
import click
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter
from nucypher.cli.options import group_options
from nucypher.cli.utils import get_env_bool
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, NUCYPHER_SENTRY_ENDPOINT
from nucypher.control.emitters import StdoutEmitter
from nucypher.utilities.logging import GlobalLoggerSettings, Logger

View File

@ -22,8 +22,6 @@
# Common
#
IS_THIS_CORRECT = "Is this correct?"
FORCE_MODE_WARNING = "WARNING: Force is enabled"
DEVELOPMENT_MODE_WARNING = "WARNING: Running in Development mode"
@ -138,24 +136,6 @@ CONFIRM_FORGET_NODES = "Permanently delete all known node data?"
SUCCESSFUL_FORGET_NODES = "Removed all stored known nodes metadata and certificates"
CONFIRM_OVERWRITE_DATABASE = "Overwrite existing database?"
SUCCESSFUL_DATABASE_DESTRUCTION = "Destroyed existing database {path}"
SUCCESSFUL_DATABASE_CREATION = "\nCreated new database at {path}"
SUCCESSFUL_NEW_STAKEHOLDER_CONFIG = """
Configured new stakeholder!
Wrote JSON configuration to {filepath}
* Review configuration -> nucypher stake config
* View connected accounts -> nucypher stake accounts
* Create a new stake -> nucypher stake create
* Bond a worker -> nucypher stake bond-worker
* List active stakes -> nucypher stake list
"""
IGNORE_OLD_CONFIGURATION = "Ignoring configuration file '{config_file}' - version is too old"
DEFAULT_TO_LONE_CONFIG_FILE = "Defaulting to {config_class} configuration file: '{config_file}'"
@ -188,31 +168,12 @@ CONFIRM_URSULA_IPV4_ADDRESS = "Detected IPv4 address ({rest_host}) - Is this the
COLLECT_URSULA_IPV4_ADDRESS = "Enter Ursula's public-facing IPv4 address"
#
# Seednodes
#
START_LOADING_SEEDNODES = "Connecting to preferred teacher nodes..."
UNREADABLE_SEEDNODE_ADVISORY = "Failed to connect to teacher: {uri}"
NO_DOMAIN_PEERS = "WARNING: No Peers Available for domain: {domain}"
SEEDNODE_NOT_STAKING_WARNING = "Teacher ({uri}) is not actively staking, skipping"
#
# Deployment
#
PROMPT_NEW_MIN_RANGE_VALUE = "Enter new minimum value for range"
PROMPT_NEW_MAXIMUM_RANGE_VALUE = "Enter new maximum value for range"
PROMPT_NEW_OWNER_ADDRESS = "Enter new owner's checksum address"
PROMPT_NEW_DEFAULT_VALUE_FOR_RANGE = "Enter new default value for range"
CONFIRM_MANUAL_REGISTRY_DOWNLOAD = "Fetch and download latest contract registry from {source}?"
MINIMUM_POLICY_RATE_EXCEEDED_WARNING = """
@ -221,29 +182,8 @@ The staker's fee rate was set to the default value {default} such that it falls
CONTRACT_IS_NOT_OWNABLE = "Contract {contract_name} is not ownable."
CONFIRM_TOKEN_ALLOWANCE = "Approve allowance of {value} from {deployer_address} to {spender_address}?"
CONFIRM_TOKEN_TRANSFER = "Transfer {value} from {deployer_address} to {target_address}?"
PROMPT_TOKEN_VALUE = "Enter value in NU"
PROMPT_RECIPIENT_CHECKSUM_ADDRESS = "Enter recipient's checksum address"
DISPLAY_SENDER_TOKEN_BALANCE_BEFORE_TRANSFER = "Deployer NU balance: {token_balance}"
PROMPT_FOR_ALLOCATION_DATA_FILEPATH = "Enter allocations data filepath"
SUCCESSFUL_SAVE_DEPLOY_RECEIPTS = "Saved deployment receipts to {receipts_filepath}"
SUCCESSFUL_REGISTRY_CREATION = 'Wrote to registry {registry_outfile}'
CONFIRM_LOCAL_REGISTRY_DESTRUCTION = "*DESTROY* existing local registry and continue?"
EXISTING_REGISTRY_FOR_DOMAIN = """
There is an existing contract registry at {registry_filepath}.
Did you mean 'nucypher-deploy upgrade'?
"""
CONTRACT_DEPLOYMENT_SERIES_BEGIN_ADVISORY = "Deploying {contract_name}"
@ -293,8 +233,6 @@ WARNING: --etherscan is disabled. If you want to see deployed contracts and TXs
# Upgrade
#
IDENTICAL_REGISTRY_WARNING = "Local registry ({local_registry.id}) is identical to the one on GitHub ({github_registry.id})."
DEPLOYER_IS_NOT_OWNER = "Address {deployer_address} is not the owner of {contract_name}'s Dispatcher ({agent.contract_address}). Aborting."
CONFIRM_VERSIONED_UPGRADE = "Confirm upgrade {contract_name} from version {old_version} to version {new_version}?"
@ -319,10 +257,6 @@ Compiled with solc version {solc_version}
# Ursula
#
CONFIRMING_ACTIVITY_NOW = "Making a commitment to period {committed_period}"
SUCCESSFUL_CONFIRM_ACTIVITY = '\nCommitment was made to period #{committed_period} (starting at {date})'
SUCCESSFUL_MANUALLY_SAVE_METADATA = "Successfully saved node metadata to {metadata_path}."

View File

@ -18,7 +18,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click
from nucypher.cli.commands import (
enrico,
status,
ursula,
porter,
@ -46,7 +45,6 @@ ENTRY_POINTS = (
# Characters & Actors
ursula.ursula, # Untrusted Re-Encryption Proxy
enrico.enrico, # Encryptor of Data
# PRE Application
bond.bond,

View File

@ -38,7 +38,6 @@ from nucypher.utilities.logging import Logger
# Alphabetical
option_checksum_address = click.option('--checksum-address', help="Run with a specified account", type=EIP55_CHECKSUM_ADDRESS)
option_config_file = click.option('--config-file', help="Path to configuration file", type=EXISTING_READABLE_FILE)
option_config_root = click.option('--config-root', help="Custom configuration directory", type=click.Path(path_type=Path))
option_dev = click.option('--dev', '-d', help="Enable development mode", is_flag=True)
@ -48,7 +47,6 @@ option_etherscan = click.option('--etherscan/--no-etherscan', help="Enable/disab
option_event_name = click.option('--event-name', help="Specify an event by name", type=click.STRING)
option_federated_only = click.option('--federated-only/--decentralized', '-F', help="Connect only to federated nodes", is_flag=True, default=None)
option_force = click.option('--force', help="Don't ask for confirmation", is_flag=True)
option_gas_price = click.option('--gas-price', help="Set a static gas price (in GWEI)", type=GWEI)
option_gas_strategy = click.option('--gas-strategy', help="Operate with a specified gas price strategy", type=click.STRING) # TODO: GAS_STRATEGY_CHOICES
option_key_material = click.option('--key-material', help="A pre-secured hex-encoded secret to use for private key derivations", type=click.STRING)
option_max_gas_price = click.option('--max-gas-price', help="Maximum acceptable gas price (in GWEI)", type=GWEI)
@ -58,19 +56,15 @@ option_lonely = click.option('--lonely', help="Do not connect to seednodes", is_
option_min_stake = click.option('--min-stake', help="The minimum stake the teacher must have to be locally accepted.", type=STAKED_TOKENS_RANGE, default=MIN_AUTHORIZATION)
option_operator_address = click.option('--operator-address', help="Address to bond as an operator", type=EIP55_CHECKSUM_ADDRESS, required=True)
option_parameters = click.option('--parameters', help="Filepath to a JSON file containing additional parameters", type=EXISTING_READABLE_FILE)
option_participant_address = click.option('--participant-address', help="Participant's checksum address.", type=EIP55_CHECKSUM_ADDRESS)
option_payment_provider = click.option('--payment-provider', 'payment_provider', help="Connection URL for payment method", type=click.STRING, required=False)
option_payment_network = click.option('--payment-network', help="Payment network name", type=click.STRING, required=False) # TODO: Choices
option_payment_method = click.option('--payment-method', help="Payment method name", type=PAYMENT_METHOD_CHOICES, required=False)
option_poa = click.option('--poa/--disable-poa', help="Inject POA middleware", is_flag=True, default=None)
option_registry_filepath = click.option('--registry-filepath', help="Custom contract registry filepath", type=EXISTING_READABLE_FILE)
option_policy_registry_filepath = click.option('--policy-registry-filepath', help="Custom contract registry filepath for policies", type=EXISTING_READABLE_FILE)
option_shares = click.option('--shares', '-n', help="N-Total shares", type=click.INT)
option_signer_uri = click.option('--signer', 'signer_uri', '-S', default=None, type=str)
option_staking_provider = click.option('--staking-provider', help="Staking provider ethereum address", type=EIP55_CHECKSUM_ADDRESS, required=True)
option_teacher_uri = click.option('--teacher', 'teacher_uri', help="An Ursula URI to start learning from (seednode)", type=click.STRING)
option_threshold = click.option('--threshold', '-m', help="M-Threshold KFrags", type=click.INT)
option_treasure_map = click.option('--treasure-map', 'treasure_map', help="Encrypted treasure map as base64 for retrieval", type=click.STRING, required=True)
_option_middleware = click.option('-Z', '--mock-networking', help="Use in-memory transport instead of networking", count=True)
# Avoid circular input

View File

@ -17,15 +17,13 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click
import maya
from constant_sorrow.constants import NO_KEYSTORE_ATTACHED
from nucypher.blockchain.eth.sol.__conf__ import SOLIDITY_COMPILER_VERSION
from nucypher.characters.banners import NUCYPHER_BANNER
from nucypher.config.constants import (
DEFAULT_CONFIG_ROOT,
USER_LOG_DIR,
END_OF_POLICIES_PROBATIONARY_PERIOD
USER_LOG_DIR
)
@ -99,34 +97,3 @@ Path to Keystore: {new_configuration.keystore_dir}
raise ValueError(f'Unknown character type "{character_name}"')
emitter.echo(hint, color='green')
def paint_probationary_period_disclaimer(emitter):
width = 60
import textwrap
disclaimer_title = " DISCLAIMER ".center(width, "=")
paragraph = f"""
Some areas of the NuCypher network are still under active development;
as a consequence, we have established a probationary period for policies in the network.
Currently the creation of sharing policies with durations beyond {END_OF_POLICIES_PROBATIONARY_PERIOD} are prevented.
After this date the probationary period will be over, and you will be able to create policies with any duration
as supported by nodes on the network.
"""
text = (
"\n",
disclaimer_title,
*[line.center(width) for line in textwrap.wrap(paragraph, width - 2)],
"=" * len(disclaimer_title),
"\n"
)
for sentence in text:
emitter.echo(sentence, color='yellow')
def enforce_probationary_period(emitter, expiration):
"""Used during CLI grant to prevent publication of a policy outside the probationary period."""
if maya.MayaDT.from_datetime(expiration) > END_OF_POLICIES_PROBATIONARY_PERIOD:
emitter.echo(f"The requested duration for this policy (until {expiration}) exceeds the probationary period"
f" ({END_OF_POLICIES_PROBATIONARY_PERIOD}).", color="red")
raise click.Abort()

View File

@ -15,21 +15,11 @@ 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 nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.utils import etherscan_url
def paint_decoded_transaction(emitter, proposal, contract, registry):
emitter.echo("Decoded transaction:\n")
contract_function, params = proposal.decode_transaction_data(contract, registry)
emitter.echo(str(contract_function), color='yellow', bold=True)
for param, value in params.items():
emitter.echo(f" {param}", color='green', nl=False)
emitter.echo(" = ", nl=False)
emitter.echo(str(value), color='green')
emitter.echo()
def paint_receipt_summary(emitter, receipt, chain_name: str = None, transaction_type=None, eth_provider_uri: str = None):
tx_hash = receipt['transactionHash'].hex()
emitter.echo("OK", color='green', nl=False, bold=True)

View File

@ -1,215 +0,0 @@
"""
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 collections import deque
from pathlib import Path
import maya
import os
from twisted.internet import reactor
from twisted.internet.protocol import connectionDone
from twisted.internet.stdio import StandardIO
from twisted.protocols.basic import LineReceiver
from nucypher.utilities.logging import Logger
class UrsulaCommandProtocol(LineReceiver):
encoding = 'utf-8'
delimiter = os.linesep.encode(encoding=encoding)
def __init__(self, ursula, emitter):
super().__init__()
self.ursula = ursula
self.emitter = emitter
self.start_time = maya.now()
self.__history = deque(maxlen=10)
self.prompt = bytes('Ursula({}) >>> '.format(self.ursula.checksum_address[:9]), encoding='utf-8')
# Expose Ursula functional entry points
self.__commands = {
# Help
'?': self.paintHelp,
'help': self.paintHelp,
# Status
'status': self.paintStatus,
'known_nodes': self.paintKnownNodes,
'fleet_state': self.paintFleetState,
# Learning Control
'cycle_teacher': self.cycle_teacher,
'start_learning': self.start_learning,
'stop_learning': self.stop_learning,
# Process Control
'stop': self.stop,
}
self._hidden_commands = ('?',)
@property
def commands(self):
return self.__commands.keys()
def paintHelp(self):
"""
Display this help message.
"""
self.emitter.echo("\nUrsula Command Help\n===================\n")
for command, func in self.__commands.items():
if command not in self._hidden_commands:
try:
self.emitter.echo(f'{command}\n{"-"*len(command)}\n{func.__doc__.lstrip()}')
except AttributeError:
raise AttributeError("Ursula Command method is missing a docstring,"
" which is required for generating help text.")
def paintKnownNodes(self):
"""
Display a list of all known nucypher peers.
"""
from nucypher.cli.painting.nodes import paint_known_nodes
paint_known_nodes(emitter=self.emitter, ursula=self.ursula)
def paintStatus(self):
"""
Display the current status of the attached Ursula node.
"""
from nucypher.cli.painting.nodes import paint_node_status
paint_node_status(emitter=self.emitter, ursula=self.ursula, start_time=self.start_time)
def paintFleetState(self):
"""
Display information about the network-wide fleet state as the attached Ursula node sees it.
"""
from nucypher.cli.painting.nodes import build_fleet_state_status
self.emitter.echo(build_fleet_state_status(ursula=self.ursula))
def connectionMade(self):
self.emitter.echo("\nType 'help' or '?' for help")
self.transport.write(self.prompt)
def connectionLost(self, reason=connectionDone) -> None:
self.ursula.stop_learning_loop(reason=reason)
def lineReceived(self, line):
"""Ursula Console REPL"""
# Read
raw_line = line.decode(encoding=self.encoding)
line = raw_line.strip().lower()
# Evaluate
try:
self.__commands[line]()
# Print
except KeyError:
if line: # allow for empty string
self.emitter.echo("Invalid input")
self.__commands["?"]()
else:
self.__history.append(raw_line)
# Loop
self.transport.write(self.prompt)
def cycle_teacher(self):
"""
Manually direct the attached Ursula node to start learning from a different teacher.
"""
return self.ursula.cycle_teacher_node()
def start_learning(self):
"""
Manually start the attached Ursula's node learning protocol.
"""
return self.ursula.start_learning_loop()
def stop_learning(self):
"""
Manually stop the attached Ursula's node learning protocol.
"""
return self.ursula.stop_learning_loop()
def stop(self):
"""
Shutdown the attached running Ursula node.
"""
return reactor.stop()
class JSONRPCLineReceiver(LineReceiver):
encoding = 'utf-8'
delimiter = os.linesep.encode(encoding=encoding)
__ipc_endpoint = Path("/tmp/nucypher.ipc")
class IPCWriter(StandardIO):
pass
def __init__(self, rpc_controller, capture_output: bool = False):
super().__init__()
self.rpc_controller = rpc_controller
self.start_time = maya.now()
self.__captured_output = list()
self.capture_output = capture_output
self.__ipc_fd = None
self.__ipc_writer = None
self.log = Logger(f"JSON-RPC-{rpc_controller.app_name}") # TODO needs ID
@property
def captured_output(self):
return self.__captured_output
def connectionMade(self):
self.__ipc_fd = open(self.__ipc_endpoint, 'ab+')
self.__ipc_writer = self.__ipc_fd.write
# Hookup the IPC endpoint file
self.transport.write = self.__ipc_writer
self.log.info(f"JSON RPC-IPC endpoint opened at {self.__ipc_endpoint.absolute()}."
f" Listening for messages.") # TODO
def connectionLost(self, reason=connectionDone) -> None:
self.__ipc_fd.close()
self.__ipc_endpoint.unlink()
self.log.info("JSON RPC-IPC Endpoint Closed.") # TODO
def rawDataReceived(self, data):
pass
def lineReceived(self, line):
line = line.strip(self.delimiter)
if line:
self.rpc_controller.handle_request(control_request=line)

View File

@ -26,7 +26,6 @@ from eth_utils import to_checksum_address
from nucypher_core.umbral import PublicKey
from nucypher.blockchain.economics import Economics
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.token import TToken
from nucypher.policy.payment import PAYMENT_METHODS
@ -157,6 +156,4 @@ NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False)
IPV4_ADDRESS = IPv4Address()
OPERATOR_IP = OperatorIPAddress()
GAS_STRATEGY_CHOICES = click.Choice(list(BlockchainInterface.GAS_STRATEGIES.keys()))
PAYMENT_METHOD_CHOICES = click.Choice(list(PAYMENT_METHODS))
UMBRAL_PUBLIC_KEY_HEX = UmbralPublicKeyHex()

View File

@ -27,16 +27,13 @@ import maya
from flask import Flask, Response
from hendrix.deploy.base import HendrixDeploy
from hendrix.deploy.tls import HendrixDeployTLS
from twisted.internet import reactor, stdio
from nucypher.cli.processes import JSONRPCLineReceiver
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
from nucypher.control.emitters import StdoutEmitter, JSONRPCStdoutEmitter, WebEmitter
from nucypher.control.emitters import StdoutEmitter, WebEmitter
from nucypher.control.interfaces import ControlInterface
from nucypher.control.specifications.exceptions import SpecificationError
from nucypher.exceptions import DevelopmentInstallationRequired
from nucypher.network.resources import get_static_resources
from nucypher.utilities.concurrency import WorkerPool, WorkerPoolException
from nucypher.utilities.concurrency import WorkerPoolException
from nucypher.utilities.logging import Logger, GlobalLoggerSettings
@ -140,106 +137,6 @@ class CLIController(InterfaceControlServer):
return response
class JSONRPCController(InterfaceControlServer):
_emitter_class = JSONRPCStdoutEmitter
def start(self):
_transport = self.make_control_transport()
reactor.run() # < ------ Blocking Call (Reactor)
def test_client(self):
try:
from tests.utils.controllers import JSONRPCTestClient
except ImportError:
raise DevelopmentInstallationRequired(importable_name='tests.utils.controllers.JSONRPCTestClient')
test_client = JSONRPCTestClient(rpc_controller=self)
return test_client
def make_control_transport(self):
transport = stdio.StandardIO(JSONRPCLineReceiver(rpc_controller=self))
return transport
def handle_procedure_call(self, control_request) -> int:
# Validate request and read request metadata
jsonrpc2 = control_request['jsonrpc']
if jsonrpc2 != '2.0':
raise self.emitter.InvalidRequest
request_id = control_request['id']
# Read the interface's signature metadata
method_name = control_request['method']
method_params = control_request.get('params', dict()) # optional
if method_name not in self._get_interfaces():
raise self.emitter.MethodNotFound(f'No method called {method_name}')
return self.call_interface(method_name=method_name,
request=method_params,
request_id=request_id)
def handle_message(self, message: dict, *args, **kwargs) -> int:
"""Handle single JSON RPC message"""
try:
_request_id = message['id']
except KeyError: # Notification
raise self.emitter.InvalidRequest('No request id')
except TypeError:
raise self.emitter.InvalidRequest(f'Request object not valid: {type(message)}')
else: # RPC
return self.handle_procedure_call(control_request=message)
def handle_batch(self, control_requests: list) -> int:
if not control_requests:
e = self.emitter.InvalidRequest()
return self.emitter.error(e)
batch_size = 0
for request in control_requests: # TODO: parallelism
response_size = self.handle_message(message=request)
batch_size += response_size
return batch_size
def handle_request(self, control_request: bytes, *args, **kwargs) -> int:
try:
control_request = json.loads(control_request)
except JSONDecodeError:
e = self.emitter.ParseError()
return self.emitter.error(e)
# Handle batch of messages
if isinstance(control_request, list):
return self.handle_batch(control_requests=control_request)
# Handle single message
try:
return self.handle_message(message=control_request, *args, **kwargs)
except self.emitter.JSONRPCError as e:
return self.emitter.error(e)
except Exception as e:
if self.crash_on_error:
raise
return self.emitter.error(e)
def call_interface(self, method_name, request, request_id: int = None):
received = maya.now()
internal_request_id = received.epoch
if request_id is None:
request_id = internal_request_id
response = self._perform_action(action=method_name, request=request)
responded = maya.now()
duration = responded - received
return self.emitter.ipc(response=response, request_id=request_id, duration=duration)
class WebController(InterfaceControlServer):
"""
A wrapper around a JSON control interface that

View File

@ -16,11 +16,11 @@
"""
from http import HTTPStatus
import json
import os
from functools import partial
from typing import Callable, Union
from http import HTTPStatus
from typing import Callable
import click
from flask import Response
@ -105,124 +105,6 @@ class StdoutEmitter:
return null_stream()
class JSONRPCStdoutEmitter(StdoutEmitter):
transport_serializer = json.dumps
delimiter = '\n'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.log = Logger("JSON-RPC-Emitter")
class JSONRPCError(RuntimeError):
code = None
message = "Unknown JSON-RPC Error"
class ParseError(JSONRPCError):
code = -32700
message = "Invalid JSON was received by the server."
class InvalidRequest(JSONRPCError):
code = -32600
message = "The JSON sent is not a valid Request object."
class MethodNotFound(JSONRPCError):
code = -32601
message = "The method does not exist / is not available."
class InvalidParams(JSONRPCError):
code = -32602
message = "Invalid method parameter(s)."
class InternalError(JSONRPCError):
code = -32603
message = "Internal JSON-RPC error."
@staticmethod
def assemble_response(response: dict, message_id: int) -> dict:
response_data = {'jsonrpc': '2.0',
'id': str(message_id),
'result': response}
return response_data
@staticmethod
def assemble_error(message, code, data=None) -> dict:
response_data = {'jsonrpc': '2.0',
'error': {'code': str(code),
'message': str(message),
'data': data},
'id': None} # error has no ID
return response_data
def __serialize(self, data: dict, delimiter=delimiter, as_bytes: bool = False) -> Union[str, bytes]:
# Serialize
serialized_response = JSONRPCStdoutEmitter.transport_serializer(data) # type: str
if as_bytes:
serialized_response = bytes(serialized_response, encoding='utf-8') # type: bytes
# Add delimiter
if delimiter:
if as_bytes:
delimiter = bytes(delimiter, encoding='utf-8')
serialized_response = delimiter + serialized_response
return serialized_response
def __write(self, data: dict):
"""Outlet"""
serialized_response = self.__serialize(data=data)
# Write to stdout file descriptor
number_of_written_bytes = self.sink(serialized_response) # < ------ OUTLET
return number_of_written_bytes
def clear(self):
pass
def message(self, message: str, **kwds):
pass
def echo(self, *args, **kwds):
pass
def banner(self, banner):
pass
def ipc(self, response: dict, request_id: int, duration) -> int:
"""
Write RPC response object to stdout and return the number of bytes written.
"""
# Serialize JSON RPC Message
assembled_response = self.assemble_response(response=response, message_id=request_id)
size = self.__write(data=assembled_response)
self.log.info(f"OK | Responded to IPC request #{request_id} with {size} bytes, took {duration}")
return size
def error(self, e):
"""
Write RPC error object to stdout and return the number of bytes written.
"""
try:
assembled_error = self.assemble_error(message=e.message, code=e.code)
except AttributeError:
if not isinstance(e, self.JSONRPCError):
self.log.info(str(e))
raise e # a different error was raised
else:
raise self.JSONRPCError
size = self.__write(data=assembled_error)
# self.log.info(f"Error {e.code} | {e.message}") # TODO: Restore this log message
return size
def get_stream(self, *args, **kwargs):
return null_stream()
class WebEmitter:
class MethodNotFound(BaseException):

View File

@ -17,7 +17,6 @@
import functools
from typing import Optional, Set
def attach_schema(schema):
@ -38,27 +37,3 @@ class ControlInterface:
def __init__(self, implementer=None, *args, **kwargs):
self.implementer = implementer
super().__init__(*args, **kwargs)
@classmethod
def connect_cli(cls, action, exclude: Optional[Set[str]] = None):
"""
Provides click CLI options based on the defined schema for the action.
"exclude" can be used to allow CLI to exclude a subset of click options from the schema from being defined,
and allow the CLI to define them differently. For example, it can be used to exclude a required schema click
option and allow the CLI to make it not required.
"""
schema = getattr(cls, action)._schema
def callable(func):
c = func
for f in [f for f in schema.load_fields.values() if f.click and (not exclude or f.name not in exclude)]:
c = f.click(c)
@functools.wraps(func)
def wrapped(*args, **kwargs):
return c(*args, **kwargs)
return wrapped
return callable

View File

@ -22,18 +22,10 @@ class SpecificationError(ValueError):
"""The protocol request is completely unusable"""
class MissingField(SpecificationError):
"""The protocol request cannot be deserialized because it is missing required fields"""
class InvalidInputData(SpecificationError):
"""Input data does not match the input specification"""
class InvalidOutputData(SpecificationError):
"""Response data does not match the output specification"""
class InvalidArgumentCombo(SpecificationError):
"""Arguments specified are incompatible"""

View File

@ -1,22 +1,18 @@
"""
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 marshmallow import fields
from nucypher_core.umbral import PublicKey
from nucypher.control.specifications.exceptions import InvalidInputData, InvalidNativeDataTypes
@ -32,4 +28,4 @@ class Key(BaseField, fields.Field):
try:
return PublicKey.from_bytes(bytes.fromhex(value))
except InvalidNativeDataTypes as e:
raise InvalidInputData(f"Could not convert input for {self.name} to an Umbral Key: {e}")
raise InvalidInputData(f"Could not convert input for {self.name} to an Umbral Key: {e}")

View File

@ -1,31 +1,31 @@
"""
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 nucypher_core import MessageKit as MessageKitClass
from nucypher_core import TreasureMap as TreasureMapClass
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields.base import Base64BytesRepresentation
class MessageKit(Base64BytesRepresentation):
class TreasureMap(Base64BytesRepresentation):
"""
JSON Parameter representation of (unencrypted) TreasureMap.
"""
def _deserialize(self, value, attr, data, **kwargs):
try:
message_kit_bytes = super()._deserialize(value, attr, data, **kwargs)
return MessageKitClass.from_bytes(message_kit_bytes)
treasure_map_bytes = super()._deserialize(value, attr, data, **kwargs)
return TreasureMapClass.from_bytes(treasure_map_bytes)
except Exception as e:
raise InvalidInputData(f"Could not parse {self.name} as MessageKit: {e}")
raise InvalidInputData(f"Could not convert input for {self.name} to a TreasureMap: {e}") from e

View File

@ -18,11 +18,11 @@
from eth_utils import to_checksum_address
from marshmallow import fields
from nucypher.characters.control.specifications.fields import Key
from nucypher.cli import types
from nucypher.control.specifications.base import BaseSchema
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.control.specifications.fields import String
from nucypher.utilities.porter.control.specifications.fields.key import Key
class UrsulaChecksumAddress(String):

View File

@ -20,12 +20,13 @@ import click
from marshmallow import fields as marshmallow_fields
from marshmallow import validates_schema
from nucypher.characters.control.specifications import fields as character_fields
from nucypher.cli import types
from nucypher.control.specifications import fields as base_fields
from nucypher.control.specifications.base import BaseSchema
from nucypher.control.specifications.exceptions import InvalidArgumentCombo
from nucypher.utilities.porter.control.specifications import fields
from nucypher.utilities.porter.control.specifications.fields.key import Key
from nucypher.utilities.porter.control.specifications.fields.treasuremap import TreasureMap
def option_ursula():
@ -49,6 +50,8 @@ def option_bob_encrypting_key():
#
# Alice Endpoints
#
class AliceGetUrsulas(BaseSchema):
quantity = base_fields.PositiveInteger(
required=True,
@ -114,8 +117,9 @@ class AliceRevoke(BaseSchema):
#
# Bob Endpoints
#
class BobRetrieveCFrags(BaseSchema):
treasure_map = character_fields.TreasureMap(
treasure_map = TreasureMap(
required=True,
load_only=True,
click=click.option(
@ -136,7 +140,7 @@ class BobRetrieveCFrags(BaseSchema):
default=[]),
required=True,
load_only=True)
alice_verifying_key = character_fields.Key(
alice_verifying_key = Key(
required=True,
load_only=True,
click=click.option(
@ -145,11 +149,11 @@ class BobRetrieveCFrags(BaseSchema):
help="Alice's verifying key as a hexadecimal string",
type=click.STRING,
required=True))
bob_encrypting_key = character_fields.Key(
bob_encrypting_key = Key(
required=True,
load_only=True,
click=option_bob_encrypting_key())
bob_verifying_key = character_fields.Key(
bob_verifying_key = Key(
required=True,
load_only=True,
click=click.option(

View File

@ -33,7 +33,7 @@ from nucypher.blockchain.eth.registry import (
InMemoryContractRegistry,
)
from nucypher.characters.lawful import Ursula
from nucypher.control.controllers import JSONRPCController, WebController
from nucypher.control.controllers import WebController
from nucypher.crypto.powers import DecryptingPower
from nucypher.network.nodes import Learner
from nucypher.network.retrieval import RetrievalClient
@ -219,14 +219,6 @@ the Pipe for PRE Application network operations
self.controller = controller
return controller
def make_rpc_controller(self, crash_on_error: bool = False):
controller = JSONRPCController(app_name=self.APP_NAME,
crash_on_error=crash_on_error,
interface=self.interface)
self.controller = controller
return controller
def make_web_controller(self,
crash_on_error: bool = False,
htpasswd_filepath: Path = None,

View File

@ -1,140 +0,0 @@
"""
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 b64encode
import datetime
import maya
import pytest
from nucypher.characters.lawful import Enrico
from nucypher.crypto.powers import DecryptingPower
@pytest.fixture(scope='module')
def alice_web_controller_test_client(blockchain_alice):
web_controller = blockchain_alice.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
@pytest.fixture(scope='module')
def bob_web_controller_test_client(blockchain_bob):
web_controller = blockchain_bob.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
@pytest.fixture(scope='module')
def enrico_web_controller_test_client(capsule_side_channel_blockchain):
_message_kit = capsule_side_channel_blockchain()
web_controller = capsule_side_channel_blockchain.enrico.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
@pytest.fixture(scope='module')
def enrico_web_controller_from_alice(blockchain_alice, random_policy_label):
enrico = Enrico.from_alice(blockchain_alice, random_policy_label)
web_controller = enrico.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
#
# RPC
#
@pytest.fixture(scope='module')
def alice_rpc_test_client(blockchain_alice):
rpc_controller = blockchain_alice.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def bob_rpc_controller(blockchain_bob):
rpc_controller = blockchain_bob.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def enrico_rpc_controller_test_client(capsule_side_channel_blockchain):
# Side Channel
_message_kit = capsule_side_channel_blockchain()
# RPC Controler
rpc_controller = capsule_side_channel_blockchain.enrico.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def enrico_rpc_controller_from_alice(blockchain_alice, random_policy_label):
enrico = Enrico.from_alice(blockchain_alice, random_policy_label)
rpc_controller = enrico.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def create_policy_control_request(blockchain_bob):
method_name = 'create_policy'
bob_pubkey_enc = blockchain_bob.public_keys(DecryptingPower)
params = {
'bob_encrypting_key': bytes(bob_pubkey_enc).hex(),
'bob_verifying_key': bytes(blockchain_bob.stamp).hex(),
'label': b64encode(bytes(b'test')).decode(),
'threshold': 2,
'shares': 3,
'expiration': (maya.now() + datetime.timedelta(days=35)).iso8601(),
'value': 3 * 3 * 10 ** 16
}
return method_name, params
@pytest.fixture(scope='module')
def grant_control_request(blockchain_bob):
method_name = 'grant'
bob_pubkey_enc = blockchain_bob.public_keys(DecryptingPower)
params = {
'bob_encrypting_key': bytes(bob_pubkey_enc).hex(),
'bob_verifying_key': bytes(blockchain_bob.stamp).hex(),
'label': 'test',
'threshold': 2,
'shares': 3,
'expiration': (maya.now() + datetime.timedelta(days=35)).iso8601(),
'value': 3 * 3 * 10 ** 16
}
return method_name, params
@pytest.fixture(scope='function')
def retrieve_control_request(blockchain_alice, blockchain_bob, enacted_blockchain_policy, capsule_side_channel_blockchain):
capsule_side_channel_blockchain.reset()
method_name = 'retrieve_and_decrypt'
message_kit = capsule_side_channel_blockchain()
params = {
'alice_verifying_key': bytes(enacted_blockchain_policy.publisher_verifying_key).hex(),
'message_kits': [b64encode(bytes(message_kit)).decode()],
'encrypted_treasure_map': b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode()
}
return method_name, params
@pytest.fixture(scope='module')
def encrypt_control_request():
method_name = 'encrypt_message'
params = {
'message': b64encode(b"The admiration I had for your work has completely evaporated!").decode(),
}
return method_name, params

View File

@ -1,123 +0,0 @@
"""
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 b64encode
import pytest
from nucypher.characters.control.interfaces import AliceInterface
from nucypher.characters.control.interfaces import EnricoInterface
from tests.utils.controllers import get_fields, validate_json_rpc_response_data
def test_enrico_rpc_character_control_encrypt_message(enrico_rpc_controller_test_client, encrypt_control_request):
method_name, params = encrypt_control_request
request_data = {'method': method_name, 'params': params}
response = enrico_rpc_controller_test_client.send(request_data)
assert validate_json_rpc_response_data(response=response,
method_name=method_name,
interface=EnricoInterface)
def test_bob_rpc_character_control_retrieve_with_tmap(enacted_blockchain_policy,
blockchain_bob,
bob_rpc_controller,
retrieve_control_request):
# So that this test can run even independently.
if not blockchain_bob.done_seeding:
blockchain_bob.learn_from_teacher_node()
tmap_64 = b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode()
method_name, params = retrieve_control_request
params['encrypted_treasure_map'] = tmap_64
request_data = {'method': method_name, 'params': params}
response = bob_rpc_controller.send(request_data)
assert response.data['result']['cleartexts'][0] == 'Welcome to flippering number 1.'
# Make a wrong treasure map
enc_wrong_tmap = bytes(enacted_blockchain_policy.treasure_map)[1:-1]
tmap_bytes = bytes(enc_wrong_tmap)
tmap_64 = b64encode(tmap_bytes).decode()
request_data['params']['encrypted_treasure_map'] = tmap_64
with pytest.raises(ValueError):
bob_rpc_controller.send(request_data)
def test_alice_rpc_character_control_create_policy(alice_rpc_test_client, create_policy_control_request):
alice_rpc_test_client.__class__.MESSAGE_ID = 0
method_name, params = create_policy_control_request
request_data = {'method': method_name, 'params': params}
rpc_response = alice_rpc_test_client.send(request=request_data)
assert rpc_response.success is True
assert rpc_response.id == 1
_input_fields, _optional_fields, required_output_fileds = get_fields(AliceInterface, method_name)
assert 'jsonrpc' in rpc_response.data
for output_field in required_output_fileds:
assert output_field in rpc_response.content
try:
bytes.fromhex(rpc_response.content['policy_encrypting_key'])
except (KeyError, ValueError):
pytest.fail("Invalid Policy Encrypting Key")
# Confirm the same message send works again, with a unique ID
request_data = {'method': method_name, 'params': params}
rpc_response = alice_rpc_test_client.send(request=request_data)
assert rpc_response.success is True
assert rpc_response.id == 2
# Send a bulk create policy request
bulk_request = list()
for i in range(50):
request_data = {'method': method_name, 'params': params}
bulk_request.append(request_data)
rpc_responses = alice_rpc_test_client.send(request=bulk_request)
for response_id, rpc_response in enumerate(rpc_responses, start=3):
assert rpc_response.success is True
assert rpc_response.id == response_id
def test_alice_rpc_character_control_bad_input(alice_rpc_test_client, create_policy_control_request):
alice_rpc_test_client.__class__.MESSAGE_ID = 0
# Send bad data to assert error returns (Request #3)
alice_rpc_test_client.crash_on_error = False
response = alice_rpc_test_client.send(request={'bogus': 'input'}, malformed=True)
assert response.error_code == -32600
def test_alice_rpc_character_control_derive_policy_encrypting_key(alice_rpc_test_client):
method_name = 'derive_policy_encrypting_key'
request_data = {'method': method_name, 'params': {'label': 'test'}}
response = alice_rpc_test_client.send(request_data)
assert response.success is True
assert validate_json_rpc_response_data(response=response,
method_name=method_name,
interface=AliceInterface)
def test_alice_rpc_character_control_grant(alice_rpc_test_client, grant_control_request):
method_name, params = grant_control_request
request_data = {'method': method_name, 'params': params}
response = alice_rpc_test_client.send(request_data)
assert validate_json_rpc_response_data(response=response,
method_name=method_name,
interface=AliceInterface)

View File

@ -1,36 +0,0 @@
"""
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 b64encode
import pytest
from nucypher.characters.control.specifications.fields import EncryptedTreasureMap
from nucypher.control.specifications.exceptions import InvalidInputData
def test_treasure_map(enacted_blockchain_policy):
treasure_map = enacted_blockchain_policy.treasure_map
field = EncryptedTreasureMap()
serialized = field._serialize(value=treasure_map, attr=None, obj=None)
assert serialized == b64encode(bytes(treasure_map)).decode()
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert bytes(deserialized) == bytes(treasure_map)
with pytest.raises(InvalidInputData):
field._deserialize(value=b64encode(b"TreasureMap").decode(), attr=None, data=None)

View File

@ -1,346 +0,0 @@
"""
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 datetime
import json
from base64 import b64decode, b64encode
import maya
import pytest
from click.testing import CliRunner
from nucypher_core import MessageKit, EncryptedTreasureMap
import nucypher
from nucypher.crypto.powers import DecryptingPower
click_runner = CliRunner()
def test_label_whose_b64_representation_is_invalid_utf8(alice_web_controller_test_client,
create_policy_control_request):
# In our Discord, user robin#2324 (github username @robin-thomas) reported certain labels
# break Bob's retrieve endpoint.
# convo starts here: https://ptb.discordapp.com/channels/411401661714792449/411401661714792451/564353305887637517
bad_label = '516d593559505355376d454b61374751577146467a47754658396d516a685674716b7663744b376b4b666a35336d'
method_name, params = create_policy_control_request
params['label'] = bad_label
# This previously caused an unhandled UnicodeDecodeError. #920
response = alice_web_controller_test_client.put(f'/{method_name}', data=json.dumps(params))
assert response.status_code == 200
def test_alice_web_character_control_create_policy(alice_web_controller_test_client, create_policy_control_request):
method_name, params = create_policy_control_request
response = alice_web_controller_test_client.put(f'/{method_name}', data=json.dumps(params))
assert response.status_code == 200
create_policy_response = json.loads(response.data)
assert 'version' in create_policy_response
assert 'label' in create_policy_response['result']
try:
bytes.fromhex(create_policy_response['result']['policy_encrypting_key'])
except (KeyError, ValueError):
pytest.fail("Invalid Policy Encrypting Key")
# Send bad data to assert error returns
response = alice_web_controller_test_client.put('/create_policy', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
def test_alice_web_character_control_derive_policy_encrypting_key(alice_web_controller_test_client):
label = 'test'
response = alice_web_controller_test_client.post(f'/derive_policy_encrypting_key/{label}')
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'policy_encrypting_key' in response_data['result']
def test_alice_web_character_control_grant(alice_web_controller_test_client, grant_control_request):
method_name, params = grant_control_request
endpoint = f'/{method_name}'
response = alice_web_controller_test_client.put(endpoint, data=json.dumps(params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'treasure_map' in response_data['result']
assert 'policy_encrypting_key' in response_data['result']
assert 'alice_verifying_key' in response_data['result']
map_bytes = b64decode(response_data['result']['treasure_map'])
encrypted_map = EncryptedTreasureMap.from_bytes(map_bytes)
# Send bad data to assert error returns
response = alice_web_controller_test_client.put(endpoint, data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
bad_params = params.copy()
# Malform the request
del(bad_params['bob_encrypting_key'])
response = alice_web_controller_test_client.put(endpoint, data=json.dumps(bad_params))
assert response.status_code == 400
def test_alice_web_character_control_grant_error_messages(alice_web_controller_test_client, grant_control_request):
method_name, params = grant_control_request
endpoint = f'/{method_name}'
params['threshold'] = params['shares'] + 1
response = alice_web_controller_test_client.put(endpoint, data=json.dumps(params))
assert response.status_code == 400
@pytest.mark.skip(reason="Current implementation requires Alice to directly depend on SubscriptionManager")
def test_alice_character_control_revoke(alice_web_controller_test_client, blockchain_bob):
bob_pubkey_enc = blockchain_bob.public_keys(DecryptingPower)
grant_request_data = {
'bob_encrypting_key': bytes(bob_pubkey_enc).hex(),
'bob_verifying_key': bytes(blockchain_bob.stamp).hex(),
'label': 'test-revoke',
'threshold': 2,
'shares': 3,
'expiration': (maya.now() + datetime.timedelta(days=35)).iso8601(),
'value': 100500 * 3 * 3,
}
response = alice_web_controller_test_client.put('/grant', data=json.dumps(grant_request_data))
assert response.status_code == 200
revoke_request_data = {
'label': 'test-revoke',
'bob_verifying_key': bytes(blockchain_bob.stamp).hex()
}
response = alice_web_controller_test_client.delete(f'/revoke', data=json.dumps(revoke_request_data))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'result' in response_data
assert 'failed_revocations' in response_data['result']
assert response_data['result']['failed_revocations'] == 0
def test_alice_character_control_decrypt(alice_web_controller_test_client,
enacted_blockchain_policy,
capsule_side_channel_blockchain):
message_kit = capsule_side_channel_blockchain()
label = enacted_blockchain_policy.label.decode()
# policy_encrypting_key = bytes(enacted_blockchain_policy.public_key).hex()
message_kit = b64encode(bytes(message_kit)).decode()
request_data = {
'label': label,
'message_kit': message_kit,
}
response = alice_web_controller_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']
response_message = response_data['result']['cleartexts'][0]
assert response_message == 'Welcome to flippering number 1.'
# Send bad data to assert error returns
response = alice_web_controller_test_client.post('/decrypt', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
del (request_data['message_kit'])
response = alice_web_controller_test_client.put('/decrypt', data=json.dumps(request_data))
assert response.status_code == 405
def test_bob_web_character_control_retrieve(bob_web_controller_test_client, retrieve_control_request):
method_name, params = retrieve_control_request
endpoint = f'/{method_name}'
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'cleartexts' in response_data['result']
response_message = response_data['result']['cleartexts'][0]
assert response_message == 'Welcome to flippering number 1.'
# Send bad data to assert error returns
response = bob_web_controller_test_client.post(endpoint, data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
del (params['alice_verifying_key'])
response = bob_web_controller_test_client.put(endpoint, data=json.dumps(params))
def test_bob_web_character_control_retrieve_multiple_kits(bob_web_controller_test_client,
retrieve_control_request,
capsule_side_channel_blockchain):
method_name, params = retrieve_control_request
message_kits = []
# resetting produces a message kit...ok(?)
reset_message_kit, _ = capsule_side_channel_blockchain.reset(plaintext_passthrough=True)
message_kits.append(b64encode(bytes(reset_message_kit)).decode()) # add initial message kit
# add some more
for index in range(1, 5):
message_kit = capsule_side_channel_blockchain()
message_kits.append(b64encode(bytes(message_kit)).decode())
endpoint = f'/{method_name}'
params['message_kits'] = message_kits # replace message kits entry
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'cleartexts' in response_data['result']
cleartexts = response_data['result']['cleartexts']
assert len(cleartexts) == len(message_kits)
for index, cleartext in enumerate(cleartexts):
assert cleartext.encode() == capsule_side_channel_blockchain.plaintexts[index]
def test_bob_web_character_control_retrieve_with_tmap(
enacted_blockchain_policy, bob_web_controller_test_client, retrieve_control_request):
tmap_64 = b64encode(bytes(enacted_blockchain_policy.treasure_map)).decode()
method_name, params = retrieve_control_request
params['encrypted_treasure_map'] = tmap_64
endpoint = f'/{method_name}'
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(params))
assert response.status_code == 200
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
response_data = json.loads(response.data)
assert 'message_kit' in response_data['result']
# Check that it serializes correctly.
message_kit = MessageKit.from_bytes(b64decode(response_data['result']['message_kit']))
# Send bad data to assert error return
response = enrico_web_controller_test_client.post('/encrypt_message', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
del (params['message'])
response = enrico_web_controller_test_client.post('/encrypt_message', data=params)
assert response.status_code == 400
def test_web_character_control_lifecycle(alice_web_controller_test_client,
bob_web_controller_test_client,
enrico_web_controller_from_alice,
blockchain_alice,
blockchain_bob,
blockchain_ursulas,
random_policy_label):
random_label = random_policy_label.decode() # Unicode string
bob_keys_response = bob_web_controller_test_client.get('/public_keys')
assert bob_keys_response.status_code == 200
response_data = json.loads(bob_keys_response.data)
assert str(nucypher.__version__) == response_data['version']
bob_keys = response_data['result']
assert 'bob_encrypting_key' in bob_keys
assert 'bob_verifying_key' in bob_keys
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,
'bob_verifying_key': bob_verifying_key_hex,
'threshold': 1,
'shares': 1,
'label': random_label,
'expiration': (maya.now() + datetime.timedelta(days=35)).iso8601(),
'value': 3 * 10 ** 10
}
response = alice_web_controller_test_client.put('/grant', data=json.dumps(alice_request_data))
assert response.status_code == 200
# Check Response Keys
alice_response_data = json.loads(response.data)
assert 'treasure_map' in alice_response_data['result']
assert 'policy_encrypting_key' in alice_response_data['result']
assert 'alice_verifying_key' in alice_response_data['result']
assert 'version' in alice_response_data
assert str(nucypher.__version__) == alice_response_data['version']
# This is sidechannel policy metadata. It should be given to Bob by the
# application developer at some point.
alice_verifying_key_hex = alice_response_data['result']['alice_verifying_key']
# Encrypt some data via Enrico control
# Alice will also be Enrico via Enrico.from_alice
# (see enrico_control_from_alice fixture)
plaintext = "I'm bereaved, not a sap!" # type: str
enrico_request_data = {
'message': b64encode(bytes(plaintext, encoding='utf-8')).decode(),
}
response = enrico_web_controller_from_alice.post('/encrypt_message', data=json.dumps(enrico_request_data))
assert response.status_code == 200
enrico_response_data = json.loads(response.data)
assert 'message_kit' in enrico_response_data['result']
kit_bytes = b64decode(enrico_response_data['result']['message_kit'].encode())
bob_message_kit = MessageKit.from_bytes(kit_bytes)
# Retrieve data via Bob control
encoded_message_kit = b64encode(bytes(bob_message_kit)).decode()
bob_request_data = {
'alice_verifying_key': alice_verifying_key_hex,
'message_kits': [encoded_message_kit],
'encrypted_treasure_map': alice_response_data['result']['treasure_map']
}
# Give bob a node to remember
teacher = list(blockchain_ursulas)[1]
blockchain_bob.remember_node(teacher)
response = bob_web_controller_test_client.post('/retrieve_and_decrypt', data=json.dumps(bob_request_data))
assert response.status_code == 200
bob_response_data = json.loads(response.data)
assert 'cleartexts' in bob_response_data['result']
for cleartext in bob_response_data['result']['cleartexts']:
assert b64decode(cleartext.encode()).decode() == plaintext

View File

@ -1,43 +0,0 @@
"""
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 nucypher_core.umbral import SecretKey
from nucypher.cli.main import nucypher_cli
def test_enrico_encrypt(click_runner):
policy_encrypting_key = bytes(SecretKey.random().public_key()).hex()
encrypt_args = ('enrico', 'encrypt',
'--message', 'to be or not to be',
'--policy-encrypting-key', policy_encrypting_key)
result = click_runner.invoke(nucypher_cli, encrypt_args, catch_exceptions=False)
assert result.exit_code == 0
assert policy_encrypting_key in result.output
assert "message_kit" in result.output
def test_enrico_control_starts(click_runner):
policy_encrypting_key = bytes(SecretKey.random().public_key()).hex()
run_args = ('enrico', 'run',
'--policy-encrypting-key', policy_encrypting_key,
'--dry-run')
result = click_runner.invoke(nucypher_cli, run_args, catch_exceptions=False)
assert result.exit_code == 0
assert policy_encrypting_key in result.output

View File

@ -1,117 +0,0 @@
"""
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 json
from collections import deque
import pytest
import sys
from nucypher.cli.processes import JSONRPCLineReceiver
class TransportTrap:
"""Temporarily diverts system standard output"""
def __init__(self):
self.___stdout = sys.stdout
self.buffer = deque()
def __enter__(self):
"""Diversion"""
sys.stdout = self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Return to normal"""
sys.stdout = self.___stdout
def read(self, lines: int = 1):
# Read from faked buffer
results = list()
for readop in range(lines):
results.append(self.buffer.popleft())
# return the popped values
if not lines > 1:
results = results[0]
return results
def write(self, data) -> int:
if data != '\n':
self.buffer.append(data)
size = len(data)
return size
def flush(self) -> None:
pass
@pytest.fixture(scope='module')
def rpc_protocol(federated_alice):
rpc_controller = federated_alice.make_rpc_controller()
protocol = JSONRPCLineReceiver(rpc_controller=rpc_controller, capture_output=True)
yield protocol
def test_alice_rpc_controller_creation(federated_alice):
rpc_controller = federated_alice.make_rpc_controller()
protocol = JSONRPCLineReceiver(rpc_controller=rpc_controller)
assert protocol.rpc_controller == federated_alice.controller
def test_rpc_invalid_input(rpc_protocol, federated_alice):
"""
Example test data fround here: https://www.jsonrpc.org/specification
"""
semi_valid_collection = dict(
# description = (input, error code)
# Semi-valid
number_only=(42, -32600),
empty_batch_request=([], -32600),
empty_request=({}, -32600),
bogus_input=({'bogus': 'input'}, -32600),
non_existent_method=({"jsonrpc": "2.0", "method": "llamas", "id": "9"}, -32601),
invalid_request=({"jsonrpc": "2.0", "method": 1, "params": "bar"}, -32600),
# Malformed
invalid_json=(b'{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]', -32700),
invalid_batch=(b'[{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, '
b'{"jsonrpc": "2.0", "method"]', -32700)
)
with TransportTrap():
for description, payload in semi_valid_collection.items():
request, expected_error_code = payload
# Allow malformed input to passthrough
if not isinstance(request, bytes):
request = bytes(json.dumps(request), encoding='utf-8')
rpc_protocol.lineReceived(line=request)
stdout = sys.stdout.read(lines=1)
deserialized_response = json.loads(stdout)
assert 'jsonrpc' in deserialized_response
actual_error_code = int(deserialized_response['error']['code'])
assert (actual_error_code == expected_error_code), str(request)

View File

@ -152,7 +152,6 @@ def test_run_federated_ursula_from_config_file(custom_filepath: Path, click_runn
# Run Ursula
run_args = ('ursula', 'run',
'--dry-run',
'--interactive',
'--lonely',
'--config-file', str(custom_config_filepath.absolute()))
@ -164,7 +163,6 @@ def test_run_federated_ursula_from_config_file(custom_filepath: Path, click_runn
assert result.exit_code == 0, result.output
assert 'Federated' in result.output, 'WARNING: Federated ursula is not running in federated mode'
assert 'Running' in result.output
assert "'help' or '?'" in result.output
def test_ursula_save_metadata(click_runner, custom_filepath):

View File

@ -30,7 +30,6 @@ from nucypher.config.constants import (
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
TEMPORARY_DOMAIN,
)
from nucypher.network.nodes import Teacher
from nucypher.utilities.networking import LOOPBACK_ADDRESS, UnknownIPAddress
from tests.constants import (
FAKE_PASSWORD_CONFIRMED,
@ -204,7 +203,6 @@ def test_persistent_node_storage_integration(click_runner,
run_args = ('ursula', 'run',
'--dry-run',
'--debug',
'--interactive',
'--config-file', str(another_ursula_configuration_file_location.absolute()),
'--teacher', teacher_uri)
@ -222,7 +220,6 @@ def test_persistent_node_storage_integration(click_runner,
run_args = ('ursula', 'run',
'--dry-run',
'--debug',
'--interactive',
'--config-file', str(another_ursula_configuration_file_location.absolute()))
with pytest.raises(Operator.ActorError):

View File

@ -1,121 +0,0 @@
"""
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 sys
from contextlib import contextmanager
from io import StringIO
import pytest
from nucypher.cli.processes import UrsulaCommandProtocol
from nucypher.control.emitters import StdoutEmitter
@contextmanager
def capture_output():
new_out, new_err = StringIO(), StringIO()
old_out, old_err = sys.stdout, sys.stderr
try:
sys.stdout, sys.stderr = new_out, new_err
yield sys.stdout, sys.stderr
finally:
sys.stdout, sys.stderr = old_out, old_err
@pytest.fixture(scope='module')
def ursula(federated_ursulas):
ursula = federated_ursulas.pop()
return ursula
@pytest.fixture(scope='module')
def protocol(ursula):
emitter = StdoutEmitter()
protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter)
return protocol
def test_ursula_command_protocol_creation(ursula):
emitter = StdoutEmitter()
protocol = UrsulaCommandProtocol(ursula=ursula, emitter=emitter)
assert protocol.ursula == ursula
assert b'Ursula' in protocol.prompt
def test_ursula_command_help(protocol, ursula):
class FakeTransport:
"""This is a transport"""
mock_output = b''
@staticmethod
def write(data: bytes):
FakeTransport.mock_output += data
protocol.transport = FakeTransport
with capture_output() as (out, err):
protocol.lineReceived(line=b'bananas')
commands = protocol.commands
commands = list(set(commands) - set(protocol._hidden_commands))
# Ensure all commands are in the help text
result = out.getvalue()
assert "Invalid input" in result
for command in commands:
assert command in result, '{} is missing from help text'.format(command)
for command in protocol._hidden_commands:
assert command not in result, f'Hidden command {command} in help text'
# Try again with valid 'help' command
with capture_output() as (out, err):
protocol.lineReceived(line=b'help')
result = out.getvalue()
assert "Invalid input" not in result
for command in commands:
assert command in result, '{} is missing from help text'.format(command)
for command in protocol._hidden_commands:
assert command not in result, f'Hidden command {command} in help text'
# Blank lines are OK!
with capture_output() as (out, err):
protocol.lineReceived(line=b'')
assert protocol.prompt in FakeTransport.mock_output
def test_ursula_command_status(protocol, ursula):
with capture_output() as (out, err):
protocol.paintStatus()
result = out.getvalue()
assert ursula.checksum_address in result
assert '...' in result
assert 'Known Nodes' in result
def test_ursula_command_known_nodes(protocol, ursula):
with capture_output() as (out, err):
protocol.paintKnownNodes()
result = out.getvalue()
assert 'Known Nodes' in result
assert ursula.checksum_address not in result

View File

@ -18,9 +18,6 @@
import pytest
#
# Web
#
@pytest.fixture(scope='module')
def blockchain_porter_web_controller(blockchain_porter):
web_controller = blockchain_porter.make_web_controller(crash_on_error=False)
@ -31,12 +28,3 @@ def blockchain_porter_web_controller(blockchain_porter):
def blockchain_porter_basic_auth_web_controller(blockchain_porter, basic_auth_file):
web_controller = blockchain_porter.make_web_controller(crash_on_error=False, htpasswd_filepath=basic_auth_file)
yield web_controller.test_client()
#
# RPC
#
@pytest.fixture(scope='module')
def blockchain_porter_rpc_controller(blockchain_porter):
rpc_controller = blockchain_porter.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()

View File

@ -1,124 +0,0 @@
"""
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 base64 import b64encode
import pytest
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.network.nodes import Learner
from tests.utils.middleware import MockRestMiddleware
from tests.utils.policy import retrieval_request_setup, retrieval_params_decode_from_rest
def test_get_ursulas(blockchain_porter_rpc_controller, blockchain_ursulas):
method = 'get_ursulas'
expected_response_id = 0
quantity = 4
blockchain_ursulas_list = list(blockchain_ursulas)
include_ursulas = [blockchain_ursulas_list[0].checksum_address, blockchain_ursulas_list[1].checksum_address]
exclude_ursulas = [blockchain_ursulas_list[2].checksum_address, blockchain_ursulas_list[3].checksum_address]
get_ursulas_params = {
'quantity': quantity,
'include_ursulas': include_ursulas,
'exclude_ursulas': exclude_ursulas
}
#
# Success
#
request_data = {'method': method, 'params': get_ursulas_params}
response = blockchain_porter_rpc_controller.send(request_data)
expected_response_id += 1
assert response.success
# assert response.id == expected_response_id # FIXME
ursulas_info = response.data['result']['ursulas']
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
assert len(returned_ursula_addresses) == quantity
for address in include_ursulas:
assert address in returned_ursula_addresses
for address in exclude_ursulas:
assert address not in returned_ursula_addresses
# Confirm the same message send works again, with a unique ID
request_data = {'method': method, 'params': get_ursulas_params}
rpc_response = blockchain_porter_rpc_controller.send(request=request_data)
expected_response_id += 1
assert rpc_response.success
# assert rpc_response.id == expected_response_id # FIXME
#
# Failure case
#
failed_ursula_params = dict(get_ursulas_params)
failed_ursula_params['quantity'] = len(blockchain_ursulas_list) + 1 # too many to get
request_data = {'method': method, 'params': failed_ursula_params}
with pytest.raises(Learner.NotEnoughNodes):
blockchain_porter_rpc_controller.send(request_data)
def test_retrieve_cfrags(blockchain_porter,
blockchain_porter_rpc_controller,
random_blockchain_policy,
blockchain_bob,
blockchain_alice,
random_context):
method = 'retrieve_cfrags'
# Setup
network_middleware = MockRestMiddleware()
# enact new random policy since idle_blockchain_policy/enacted_blockchain_policy already modified in previous tests
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware)
retrieve_cfrags_params, _ = retrieval_request_setup(enacted_policy,
blockchain_bob,
blockchain_alice,
encode_for_rest=True)
# Success
request_data = {'method': method, 'params': retrieve_cfrags_params}
response = blockchain_porter_rpc_controller.send(request_data)
assert response.success
retrieval_results = response.data['result']['retrieval_results']
assert retrieval_results
# expected results - can only compare length of results, ursulas are randomized to obtain cfrags
retrieve_args = retrieval_params_decode_from_rest(retrieve_cfrags_params)
expected_results = blockchain_porter.retrieve_cfrags(**retrieve_args)
assert len(retrieval_results) == len(expected_results)
# Use context
retrieve_cfrags_params_with_context, _ = retrieval_request_setup(enacted_policy,
blockchain_bob,
blockchain_alice,
context=random_context,
encode_for_rest=True)
request_data = {'method': method, 'params': retrieve_cfrags_params_with_context}
response = blockchain_porter_rpc_controller.send(request_data)
assert response.success
retrieval_results = response.data['result']['retrieval_results']
assert retrieval_results
# Failure - use encrypted treasure map
failure_retrieve_cfrags_params = dict(retrieve_cfrags_params)
failure_retrieve_cfrags_params['treasure_map'] = b64encode(os.urandom(32)).decode()
request_data = {'method': method, 'params': failure_retrieve_cfrags_params}
with pytest.raises(InvalidInputData):
blockchain_porter_rpc_controller.send(request_data)

View File

@ -1,137 +0,0 @@
"""
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 b64encode
import datetime
import maya
import pytest
from nucypher.characters.lawful import Enrico
from nucypher.crypto.powers import DecryptingPower
@pytest.fixture(scope='module')
def alice_web_controller_test_client(federated_alice):
web_controller = federated_alice.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
@pytest.fixture(scope='module')
def bob_web_controller_test_client(federated_bob):
web_controller = federated_bob.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
@pytest.fixture(scope='module')
def enrico_web_controller_test_client(capsule_side_channel):
_message_kit = capsule_side_channel()
web_controller = capsule_side_channel.enrico.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
@pytest.fixture(scope='module')
def enrico_web_controller_from_alice(federated_alice, random_policy_label):
enrico = Enrico.from_alice(federated_alice, random_policy_label)
web_controller = enrico.make_web_controller(crash_on_error=True)
yield web_controller.test_client()
#
# RPC
#
@pytest.fixture(scope='module')
def alice_rpc_test_client(federated_alice):
rpc_controller = federated_alice.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def bob_rpc_controller(federated_bob):
rpc_controller = federated_bob.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def enrico_rpc_controller_test_client(capsule_side_channel):
# Side Channel
_message_kit = capsule_side_channel()
# RPC Controler
rpc_controller = capsule_side_channel.enrico.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def enrico_rpc_controller_from_alice(federated_alice, random_policy_label):
enrico = Enrico.from_alice(federated_alice, random_policy_label)
rpc_controller = enrico.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()
@pytest.fixture(scope='module')
def create_policy_control_request(federated_bob):
method_name = 'create_policy'
bob_pubkey_enc = federated_bob.public_keys(DecryptingPower)
params = {
'bob_encrypting_key': bytes(bob_pubkey_enc).hex(),
'bob_verifying_key': bytes(federated_bob.stamp).hex(),
'label': b64encode(bytes(b'test')).decode(),
'threshold': 2,
'shares': 3,
'expiration': (maya.now() + datetime.timedelta(days=3)).iso8601(),
}
return method_name, params
@pytest.fixture(scope='module')
def grant_control_request(federated_bob):
method_name = 'grant'
bob_pubkey_enc = federated_bob.public_keys(DecryptingPower)
params = {
'bob_encrypting_key': bytes(bob_pubkey_enc).hex(),
'bob_verifying_key': bytes(federated_bob.stamp).hex(),
'label': 'test',
'threshold': 2,
'shares': 3,
'expiration': (maya.now() + datetime.timedelta(days=3)).iso8601(),
}
return method_name, params
@pytest.fixture(scope='module')
def retrieve_control_request(federated_bob, enacted_federated_policy, capsule_side_channel):
method_name = 'retrieve_and_decrypt'
message_kit = capsule_side_channel()
params = {
'alice_verifying_key': bytes(enacted_federated_policy.publisher_verifying_key).hex(),
'message_kits': [b64encode(bytes(message_kit)).decode()],
'encrypted_treasure_map': b64encode(bytes(enacted_federated_policy.treasure_map)).decode()
}
return method_name, params
@pytest.fixture(scope='module')
def encrypt_control_request():
method_name = 'encrypt_message'
params = {
'message': b64encode(b"The admiration I had for your work has completely evaporated!").decode(),
}
return method_name, params

View File

@ -1,89 +0,0 @@
"""
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 pytest
from nucypher.characters.control.interfaces import BobInterface
from tests.utils.controllers import validate_json_rpc_response_data
def test_alice_rpc_character_control_create_policy(alice_rpc_test_client, create_policy_control_request):
alice_rpc_test_client.__class__.MESSAGE_ID = 0
method_name, params = create_policy_control_request
request_data = {'method': method_name, 'params': params}
rpc_response = alice_rpc_test_client.send(request=request_data)
assert rpc_response.success is True
assert rpc_response.id == 1
try:
bytes.fromhex(rpc_response.content['policy_encrypting_key'])
except (KeyError, ValueError):
pytest.fail("Invalid Policy Encrypting Key")
# Confirm the same message send works again, with a unique ID
request_data = {'method': method_name, 'params': params}
rpc_response = alice_rpc_test_client.send(request=request_data)
assert rpc_response.success is True
assert rpc_response.id == 2
# Send bad data to assert error returns (Request #3)
alice_rpc_test_client.crash_on_error = False
response = alice_rpc_test_client.send(request={'bogus': 'input'}, malformed=True)
assert response.error_code == -32600
# Send a bulk create policy request
bulk_request = list()
for i in range(50):
request_data = {'method': method_name, 'params': params}
bulk_request.append(request_data)
rpc_responses = alice_rpc_test_client.send(request=bulk_request)
for response_id, rpc_response in enumerate(rpc_responses, start=3):
assert rpc_response.success is True
assert rpc_response.id == response_id
def test_alice_rpc_character_control_derive_policy_encrypting_key(alice_rpc_test_client):
method_name = 'derive_policy_encrypting_key'
request_data = {'method': method_name, 'params': {'label': 'test'}}
response = alice_rpc_test_client.send(request_data)
assert response.success is True
assert 'jsonrpc' in response.data
def test_alice_rpc_character_control_grant(alice_rpc_test_client, grant_control_request):
method_name, params = grant_control_request
request_data = {'method': method_name, 'params': params}
response = alice_rpc_test_client.send(request_data)
assert 'jsonrpc' in response.data
def test_enrico_rpc_character_control_encrypt_message(enrico_rpc_controller_test_client, encrypt_control_request):
method_name, params = encrypt_control_request
request_data = {'method': method_name, 'params': params}
response = enrico_rpc_controller_test_client.send(request_data)
assert 'jsonrpc' in response.data
def test_bob_rpc_character_control_retrieve(bob_rpc_controller, retrieve_control_request):
method_name, params = retrieve_control_request
request_data = {'method': method_name, 'params': params}
response = bob_rpc_controller.send(request_data)
assert validate_json_rpc_response_data(response=response,
method_name=method_name,
interface=BobInterface)

View File

@ -1,346 +0,0 @@
"""
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 datetime
import json
from base64 import b64decode, b64encode
import maya
import pytest
from click.testing import CliRunner
from nucypher_core import MessageKit, EncryptedTreasureMap
import nucypher
from nucypher.crypto.powers import DecryptingPower
click_runner = CliRunner()
def test_label_whose_b64_representation_is_invalid_utf8(alice_web_controller_test_client, create_policy_control_request):
# In our Discord, user robin#2324 (github username @robin-thomas) reported certain labels
# break Bob's retrieve endpoint.
# convo starts here: https://ptb.discordapp.com/channels/411401661714792449/411401661714792451/564353305887637517
bad_label = '516d593559505355376d454b61374751577146467a47754658396d516a685674716b7663744b376b4b666a35336d'
method_name, params = create_policy_control_request
params['label'] = bad_label
# This previously caused an unhandled UnicodeDecodeError. #920
response = alice_web_controller_test_client.put(f'/{method_name}', data=json.dumps(params))
assert response.status_code == 200
def test_alice_web_character_control_create_policy(alice_web_controller_test_client, create_policy_control_request):
method_name, params = create_policy_control_request
response = alice_web_controller_test_client.put(f'/{method_name}', data=json.dumps(params))
assert response.status_code == 200
create_policy_response = json.loads(response.data)
assert 'version' in create_policy_response
assert 'label' in create_policy_response['result']
try:
bytes.fromhex(create_policy_response['result']['policy_encrypting_key'])
except (KeyError, ValueError):
pytest.fail("Invalid Policy Encrypting Key")
# Send bad data to assert error returns
response = alice_web_controller_test_client.put('/create_policy', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
def test_alice_web_character_control_derive_policy_encrypting_key(alice_web_controller_test_client):
label = 'test'
response = alice_web_controller_test_client.post(f'/derive_policy_encrypting_key/{label}')
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'policy_encrypting_key' in response_data['result']
def test_alice_web_character_control_grant(alice_web_controller_test_client, grant_control_request):
method_name, params = grant_control_request
endpoint = f'/{method_name}'
response = alice_web_controller_test_client.put(endpoint, data=json.dumps(params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'treasure_map' in response_data['result']
assert 'policy_encrypting_key' in response_data['result']
assert 'alice_verifying_key' in response_data['result']
map_bytes = b64decode(response_data['result']['treasure_map'])
encrypted_map = EncryptedTreasureMap.from_bytes(map_bytes)
# Send bad data to assert error returns
response = alice_web_controller_test_client.put(endpoint, data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
# Malform the request
bad_params = dict(params)
del(bad_params['bob_encrypting_key'])
response = alice_web_controller_test_client.put(endpoint, data=json.dumps(bad_params))
assert response.status_code == 400
# test key validation with a bad key
bad_params = dict(params)
bad_params['bob_encrypting_key'] = '12345'
response = alice_web_controller_test_client.put(endpoint, data=json.dumps(bad_params))
assert response.status_code == 400
assert b'non-hexadecimal number found in fromhex' in response.data
def test_alice_character_control_revoke(alice_web_controller_test_client, federated_bob):
bob_pubkey_enc = federated_bob.public_keys(DecryptingPower)
grant_request_data = {
'bob_encrypting_key': bytes(bob_pubkey_enc).hex(),
'bob_verifying_key': bytes(federated_bob.stamp).hex(),
'label': 'test-revoke',
'threshold': 2,
'shares': 3,
'expiration': (maya.now() + datetime.timedelta(days=3)).iso8601(),
}
response = alice_web_controller_test_client.put('/grant', data=json.dumps(grant_request_data))
assert response.status_code == 200
revoke_request_data = {
'label': 'test',
'bob_verifying_key': bytes(federated_bob.stamp).hex()
}
response = alice_web_controller_test_client.delete(f'/revoke', data=json.dumps(revoke_request_data))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'result' in response_data
assert 'failed_revocations' in response_data['result']
assert response_data['result']['failed_revocations'] == 0
def test_alice_character_control_decrypt(alice_web_controller_test_client,
enacted_federated_policy,
capsule_side_channel):
message_kit = capsule_side_channel()
label = enacted_federated_policy.label.decode()
policy_encrypting_key = bytes(enacted_federated_policy.public_key).hex()
message_kit = b64encode(bytes(message_kit)).decode()
request_data = {
'label': label,
'message_kit': message_kit,
}
response = alice_web_controller_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']
response_message = response_data['result']['cleartexts'][0]
assert response_message == 'Welcome to flippering number 1.' # This is the first message - in a test below, we'll show retrieving a second one.
# Send bad data to assert error returns
response = alice_web_controller_test_client.post('/decrypt', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
del(request_data['message_kit'])
response = alice_web_controller_test_client.put('/decrypt', data=json.dumps(request_data))
assert response.status_code == 405
def test_bob_web_character_control_retrieve(bob_web_controller_test_client, retrieve_control_request):
method_name, params = retrieve_control_request
endpoint = f'/{method_name}'
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'cleartexts' in response_data['result']
response_message = response_data['result']['cleartexts'][0]
assert response_message == 'Welcome to flippering number 2.' # This is the second message - the first is in the test above.
# Send bad data to assert error returns
response = bob_web_controller_test_client.post(endpoint, data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
def test_bob_web_character_control_retrieve_again(bob_web_controller_test_client, retrieve_control_request):
method_name, params = retrieve_control_request
endpoint = f'/{method_name}'
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'cleartexts' in response_data['result']
response_message = response_data['result']['cleartexts'][0]
assert response_message == 'Welcome to flippering number 2.' # We have received exactly the same message again.
bad_params = dict(params)
del(bad_params['alice_verifying_key'])
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(bad_params))
assert response.status_code == 400
def test_bob_web_character_control_retrieve_multiple_kits(bob_web_controller_test_client,
retrieve_control_request,
capsule_side_channel):
method_name, params = retrieve_control_request
multiple_kits_params = dict(params)
message_kits = []
# capsule_side_channel has module scope so resetting - weird: resetting produces a message kit...ok(?)
reset_message_kit, _ = capsule_side_channel.reset(plaintext_passthrough=True)
message_kits.append(b64encode(bytes(reset_message_kit)).decode()) # add initial message kit
# add some more
for index in range(1, 5):
message_kit = capsule_side_channel()
message_kits.append(b64encode(bytes(message_kit)).decode())
endpoint = f'/{method_name}'
multiple_kits_params['message_kits'] = message_kits # replace message kits entry
response = bob_web_controller_test_client.post(endpoint, data=json.dumps(multiple_kits_params))
assert response.status_code == 200
response_data = json.loads(response.data)
assert 'cleartexts' in response_data['result']
cleartexts = response_data['result']['cleartexts']
assert len(cleartexts) == len(message_kits)
for index, cleartext in enumerate(cleartexts):
assert cleartext.encode() == capsule_side_channel.plaintexts[index]
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
response_data = json.loads(response.data)
assert 'message_kit' in response_data['result']
# Check that it serializes correctly.
MessageKit.from_bytes(b64decode(response_data['result']['message_kit']))
# Send bad data to assert error return
response = enrico_web_controller_test_client.post('/encrypt_message', data=json.dumps({'bad': 'input'}))
assert response.status_code == 400
bad_params = dict(params)
del(bad_params['message'])
response = enrico_web_controller_test_client.post('/encrypt_message', data=bad_params)
assert response.status_code == 400
def test_web_character_control_lifecycle(alice_web_controller_test_client,
bob_web_controller_test_client,
enrico_web_controller_from_alice,
federated_alice,
federated_bob,
federated_ursulas,
random_policy_label):
random_label = random_policy_label.decode() # Unicode string
bob_keys_response = bob_web_controller_test_client.get('/public_keys')
assert bob_keys_response.status_code == 200
response_data = json.loads(bob_keys_response.data)
assert str(nucypher.__version__) == response_data['version']
bob_keys = response_data['result']
assert 'bob_encrypting_key' in bob_keys
assert 'bob_verifying_key' in bob_keys
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,
'bob_verifying_key': bob_verifying_key_hex,
'threshold': 1,
'shares': 1,
'label': random_label,
'expiration': (maya.now() + datetime.timedelta(days=3)).iso8601(), # TODO
}
response = alice_web_controller_test_client.put('/grant', data=json.dumps(alice_request_data))
assert response.status_code == 200
# Check Response Keys
alice_response_data = json.loads(response.data)
assert 'treasure_map' in alice_response_data['result']
assert 'policy_encrypting_key' in alice_response_data['result']
assert 'alice_verifying_key' in alice_response_data['result']
assert 'version' in alice_response_data
assert str(nucypher.__version__) == alice_response_data['version']
# This is sidechannel policy metadata. It should be given to Bob by the
# application developer at some point.
alice_verifying_key_hex = alice_response_data['result']['alice_verifying_key']
# Encrypt some data via Enrico control
# Alice will also be Enrico via Enrico.from_alice
# (see enrico_control_from_alice fixture)
plaintext = "I'm bereaved, not a sap!" # type: str
enrico_request_data = {
'message': b64encode(bytes(plaintext, encoding='utf-8')).decode(),
}
response = enrico_web_controller_from_alice.post('/encrypt_message', data=json.dumps(enrico_request_data))
assert response.status_code == 200
enrico_response_data = json.loads(response.data)
assert 'message_kit' in enrico_response_data['result']
kit_bytes = b64decode(enrico_response_data['result']['message_kit'].encode())
bob_message_kit = MessageKit.from_bytes(kit_bytes)
# Retrieve data via Bob control
encoded_message_kit = b64encode(bytes(bob_message_kit)).decode()
bob_request_data = {
'alice_verifying_key': alice_verifying_key_hex,
'message_kits': [encoded_message_kit],
'encrypted_treasure_map': alice_response_data['result']['treasure_map']
}
# Give bob a node to remember
teacher = list(federated_ursulas)[1]
federated_bob.remember_node(teacher)
response = bob_web_controller_test_client.post('/retrieve_and_decrypt', data=json.dumps(bob_request_data))
assert response.status_code == 200
bob_response_data = json.loads(response.data)
assert 'cleartexts' in bob_response_data['result']
for cleartext in bob_response_data['result']['cleartexts']:
assert b64decode(cleartext.encode()).decode() == plaintext

View File

@ -1,209 +0,0 @@
"""
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 base64
import datetime
import maya
import pytest
from nucypher_core import (
MessageKit,
EncryptedTreasureMap as EncryptedTreasureMapClass,
TreasureMap as TreasureMapClass,
)
from nucypher_core.umbral import PublicKey
from nucypher.characters.control.specifications import fields
from nucypher.characters.control.specifications.alice import GrantPolicy
from nucypher.characters.control.specifications.fields.treasuremap import EncryptedTreasureMap, TreasureMap
from nucypher.control.specifications.base import BaseSchema
from nucypher.control.specifications.exceptions import SpecificationError, InvalidInputData, InvalidArgumentCombo
from nucypher.crypto.powers import DecryptingPower
def make_header(brand: bytes, major: int, minor: int) -> bytes:
# Hardcoding this since it's too much trouble to expose it all the way from Rust
assert len(brand) == 4
major_bytes = major.to_bytes(2, 'big')
minor_bytes = minor.to_bytes(2, 'big')
header = brand + major_bytes + minor_bytes
return header
def test_various_field_validations_by_way_of_alice_grant(federated_bob):
""" test some semi-complex validation situations """
with pytest.raises(InvalidInputData):
GrantPolicy().load(dict())
bob_encrypting_key = federated_bob.public_keys(DecryptingPower)
data = {
'bob_encrypting_key': bytes(bob_encrypting_key).hex(),
'bob_verifying_key': bytes(federated_bob.stamp).hex(),
'threshold': 5,
'shares': 6,
'expiration': (maya.now() + datetime.timedelta(days=3)).iso8601(),
'label': 'cats the animal',
'rate': 1000,
'value': 3000,
}
# validate data with both rate and value fails validation
with pytest.raises(InvalidArgumentCombo):
GrantPolicy().load(data)
# remove value and now it works
del data['value']
result = GrantPolicy().load(data)
assert result['label'] == b'cats the animal'
# validate that negative "m" value fails
data['threshold'] = -5
with pytest.raises(SpecificationError):
GrantPolicy().load(data)
# validate that m > n fails validation
data['threshold'] = data['shares'] + 19
with pytest.raises(SpecificationError):
GrantPolicy().load(data)
def test_treasure_map_validation(enacted_federated_policy,
federated_bob):
"""Tell people exactly what's wrong with their treasuremaps"""
#
# encrypted treasure map
#
class EncryptedTreasureMapsOnly(BaseSchema):
tmap = EncryptedTreasureMap()
# this will raise a base64 error
with pytest.raises(SpecificationError) as e:
EncryptedTreasureMapsOnly().load({'tmap': "your face looks like a treasure map"})
# assert that field name is in the error message
assert "Could not parse tmap" in str(e)
assert "Invalid base64-encoded string" in str(e)
# valid base64 but invalid treasuremap
bad_map = make_header(b'EMap', 1, 0) + b"your face looks like a treasure map"
bad_map_b64 = base64.b64encode(bad_map).decode()
with pytest.raises(InvalidInputData) as e:
EncryptedTreasureMapsOnly().load({'tmap': bad_map_b64})
assert "Could not convert input for tmap to an EncryptedTreasureMap" in str(e)
assert "Failed to deserialize" in str(e)
# a valid treasuremap for once...
tmap_bytes = bytes(enacted_federated_policy.treasure_map)
tmap_b64 = base64.b64encode(tmap_bytes)
result = EncryptedTreasureMapsOnly().load({'tmap': tmap_b64.decode()})
assert isinstance(result['tmap'], EncryptedTreasureMapClass)
#
# unencrypted treasure map
#
class UnenncryptedTreasureMapsOnly(BaseSchema):
tmap = TreasureMap()
# this will raise a base64 error
with pytest.raises(SpecificationError) as e:
UnenncryptedTreasureMapsOnly().load({'tmap': "your face looks like a treasure map"})
# assert that field name is in the error message
assert "Could not parse tmap" in str(e)
assert "Invalid base64-encoded string" in str(e)
# valid base64 but invalid treasuremap
bad_map = make_header(b'TMap', 1, 0) + b"your face looks like a treasure map"
bad_map_b64 = base64.b64encode(bad_map).decode()
with pytest.raises(InvalidInputData) as e:
UnenncryptedTreasureMapsOnly().load({'tmap': bad_map_b64})
assert "Could not convert input for tmap to a TreasureMap" in str(e)
assert "Failed to deserialize" in str(e)
# a valid treasuremap
decrypted_treasure_map = federated_bob._decrypt_treasure_map(enacted_federated_policy.treasure_map,
enacted_federated_policy.publisher_verifying_key)
tmap_bytes = bytes(decrypted_treasure_map)
tmap_b64 = base64.b64encode(tmap_bytes).decode()
result = UnenncryptedTreasureMapsOnly().load({'tmap': tmap_b64})
assert isinstance(result['tmap'], TreasureMapClass)
def test_messagekit_validation(capsule_side_channel):
"""Ensure that our users know exactly what's wrong with their message kit input"""
class MessageKitsOnly(BaseSchema):
mkit = fields.MessageKit()
# this will raise a base64 error
with pytest.raises(SpecificationError) as e:
MessageKitsOnly().load({'mkit': "I got a message for you"})
# assert that field name is in the error message
assert "Could not parse mkit" in str(e)
assert "Incorrect padding" in str(e)
# valid base64 but invalid messagekit
bad_kit = make_header(b'MKit', 1, 0) + b"I got a message for you"
bad_kit_b64 = base64.b64encode(bad_kit).decode()
with pytest.raises(SpecificationError) as e:
MessageKitsOnly().load({'mkit': bad_kit_b64})
assert "Could not parse mkit" in str(e)
assert "Failed to deserialize" in str(e)
# test a valid messagekit
valid_kit = capsule_side_channel.messages[0][0]
kit_bytes = bytes(valid_kit)
kit_b64 = base64.b64encode(kit_bytes)
result = MessageKitsOnly().load({'mkit': kit_b64.decode()})
assert isinstance(result['mkit'], MessageKit)
def test_key_validation(federated_bob):
class BobKeyInputRequirer(BaseSchema):
bobkey = fields.Key()
with pytest.raises(InvalidInputData) as e:
BobKeyInputRequirer().load({'bobkey': "I am the key to nothing"})
assert "non-hexadecimal number found in fromhex()" in str(e)
assert "bobkey" in str(e)
with pytest.raises(InvalidInputData) as e:
BobKeyInputRequirer().load({'bobkey': "I am the key to nothing"})
assert "non-hexadecimal number found in fromhex()" in str(e)
assert "bobkey" in str(e)
with pytest.raises(InvalidInputData) as e:
# lets just take a couple bytes off
BobKeyInputRequirer().load({'bobkey': "02f0cb3f3a33f16255d9b2586e6c56570aa07bbeb1157e169f1fb114ffb40037"})
assert "Could not convert input for bobkey to an Umbral Key" in str(e)
assert "xpected 33 bytes, got 32" in str(e)
result = BobKeyInputRequirer().load(dict(bobkey=bytes(federated_bob.public_keys(DecryptingPower)).hex()))
assert isinstance(result['bobkey'], PublicKey)

View File

@ -17,9 +17,6 @@
import pytest
#
# Web
#
@pytest.fixture(scope='module')
def federated_porter_web_controller(federated_porter):
web_controller = federated_porter.make_web_controller(crash_on_error=False)
@ -30,12 +27,3 @@ def federated_porter_web_controller(federated_porter):
def federated_porter_basic_auth_web_controller(federated_porter, basic_auth_file):
web_controller = federated_porter.make_web_controller(crash_on_error=False, htpasswd_filepath=basic_auth_file)
yield web_controller.test_client()
#
# RPC
#
@pytest.fixture(scope='module')
def federated_porter_rpc_controller(federated_porter):
rpc_controller = federated_porter.make_rpc_controller(crash_on_error=True)
yield rpc_controller.test_client()

View File

@ -1,120 +0,0 @@
"""
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 b64encode
import pytest
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.network.nodes import Learner
from tests.utils.policy import retrieval_request_setup, retrieval_params_decode_from_rest
def test_get_ursulas(federated_porter_rpc_controller, federated_ursulas):
method = 'get_ursulas'
quantity = 4
federated_ursulas_list = list(federated_ursulas)
include_ursulas = [federated_ursulas_list[0].checksum_address, federated_ursulas_list[1].checksum_address]
exclude_ursulas = [federated_ursulas_list[2].checksum_address, federated_ursulas_list[3].checksum_address]
get_ursulas_params = {
'quantity': quantity,
'include_ursulas': include_ursulas,
'exclude_ursulas': exclude_ursulas
}
#
# Success
#
request_data = {'method': method, 'params': get_ursulas_params}
response = federated_porter_rpc_controller.send(request_data)
expected_response_id = response.id
assert response.success
ursulas_info = response.data['result']['ursulas']
returned_ursula_addresses = {ursula_info['checksum_address'] for ursula_info in ursulas_info} # ensure no repeats
assert len(returned_ursula_addresses) == quantity
for address in include_ursulas:
assert address in returned_ursula_addresses
for address in exclude_ursulas:
assert address not in returned_ursula_addresses
# Confirm the same message send works again, with a unique ID
request_data = {'method': method, 'params': get_ursulas_params}
rpc_response = federated_porter_rpc_controller.send(request=request_data)
expected_response_id += 1
assert rpc_response.success
assert rpc_response.id == expected_response_id
#
# Failure case
#
failed_ursula_params = dict(get_ursulas_params)
failed_ursula_params['quantity'] = len(federated_ursulas_list) + 1 # too many to get
request_data = {'method': method, 'params': failed_ursula_params}
with pytest.raises(Learner.NotEnoughNodes):
federated_porter_rpc_controller.send(request_data)
def test_retrieve_cfrags(federated_porter,
federated_porter_rpc_controller,
enacted_federated_policy,
federated_bob,
federated_alice,
random_federated_treasure_map_data,
random_context):
method = 'retrieve_cfrags'
# Setup
retrieve_cfrags_params, _ = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
encode_for_rest=True)
# Success
request_data = {'method': method, 'params': retrieve_cfrags_params}
response = federated_porter_rpc_controller.send(request_data)
assert response.success
retrieval_results = response.data['result']['retrieval_results']
assert retrieval_results
# expected results - can only compare length of results, ursulas are randomized to obtain cfrags
retrieve_args = retrieval_params_decode_from_rest(retrieve_cfrags_params)
expected_results = federated_porter.retrieve_cfrags(**retrieve_args)
assert len(retrieval_results) == len(expected_results)
# Use context
retrieve_cfrags_params_with_context, _ = retrieval_request_setup(enacted_federated_policy,
federated_bob,
federated_alice,
context=random_context,
encode_for_rest=True)
request_data = {'method': method, 'params': retrieve_cfrags_params_with_context}
response = federated_porter_rpc_controller.send(request_data)
assert response.success
retrieval_results = response.data['result']['retrieval_results']
assert retrieval_results
# Failure - use encrypted treasure map
failure_retrieve_cfrags_params = dict(retrieve_cfrags_params)
_, random_treasure_map = random_federated_treasure_map_data
failure_retrieve_cfrags_params['treasure_map'] = b64encode(bytes(random_treasure_map)).decode()
request_data = {'method': method, 'params': failure_retrieve_cfrags_params}
with pytest.raises(InvalidInputData):
federated_porter_rpc_controller.send(request_data)

View File

@ -14,19 +14,25 @@
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 random
import pytest
from nucypher_core import (
MessageKit,
TreasureMap as TreasureMapClass,
)
from nucypher_core.umbral import PublicKey
from nucypher_core.umbral import SecretKey
from nucypher.control.specifications.exceptions import (
InvalidArgumentCombo,
InvalidInputData,
)
from nucypher.control.specifications.base import BaseSchema
from nucypher.control.specifications.exceptions import SpecificationError, InvalidInputData, InvalidArgumentCombo
from nucypher.crypto.powers import DecryptingPower
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalOutcomeSchema,
UrsulaInfoSchema,
UrsulaInfoSchema, Key,
)
from nucypher.utilities.porter.control.specifications.fields.treasuremap import TreasureMap
from nucypher.utilities.porter.control.specifications.porter_schema import (
AliceGetUrsulas,
BobRetrieveCFrags,
@ -326,3 +332,69 @@ def test_bob_retrieve_cfrags(federated_porter,
values = kit_errors.values() # ordered?
for j in range(i):
assert error_message_template.format(i, j) in values
def make_header(brand: bytes, major: int, minor: int) -> bytes:
# Hardcoding this since it's too much trouble to expose it all the way from Rust
assert len(brand) == 4
major_bytes = major.to_bytes(2, 'big')
minor_bytes = minor.to_bytes(2, 'big')
header = brand + major_bytes + minor_bytes
return header
def test_treasure_map_validation(enacted_federated_policy,
federated_bob):
class UnenncryptedTreasureMapsOnly(BaseSchema):
tmap = TreasureMap()
# this will raise a base64 error
with pytest.raises(SpecificationError) as e:
UnenncryptedTreasureMapsOnly().load({'tmap': "your face looks like a treasure map"})
# assert that field name is in the error message
assert "Could not parse tmap" in str(e)
assert "Invalid base64-encoded string" in str(e)
# valid base64 but invalid treasuremap
bad_map = make_header(b'TMap', 1, 0) + b"your face looks like a treasure map"
bad_map_b64 = base64.b64encode(bad_map).decode()
with pytest.raises(InvalidInputData) as e:
UnenncryptedTreasureMapsOnly().load({'tmap': bad_map_b64})
assert "Could not convert input for tmap to a TreasureMap" in str(e)
assert "Failed to deserialize" in str(e)
# a valid treasuremap
decrypted_treasure_map = federated_bob._decrypt_treasure_map(enacted_federated_policy.treasure_map,
enacted_federated_policy.publisher_verifying_key)
tmap_bytes = bytes(decrypted_treasure_map)
tmap_b64 = base64.b64encode(tmap_bytes).decode()
result = UnenncryptedTreasureMapsOnly().load({'tmap': tmap_b64})
assert isinstance(result['tmap'], TreasureMapClass)
def test_key_validation(federated_bob):
class BobKeyInputRequirer(BaseSchema):
bobkey = Key()
with pytest.raises(InvalidInputData) as e:
BobKeyInputRequirer().load({'bobkey': "I am the key to nothing"})
assert "non-hexadecimal number found in fromhex()" in str(e)
assert "bobkey" in str(e)
with pytest.raises(InvalidInputData) as e:
BobKeyInputRequirer().load({'bobkey': "I am the key to nothing"})
assert "non-hexadecimal number found in fromhex()" in str(e)
assert "bobkey" in str(e)
with pytest.raises(InvalidInputData) as e:
# lets just take a couple bytes off
BobKeyInputRequirer().load({'bobkey': "02f0cb3f3a33f16255d9b2586e6c56570aa07bbeb1157e169f1fb114ffb40037"})
assert "Could not convert input for bobkey to an Umbral Key" in str(e)
assert "xpected 33 bytes, got 32" in str(e)
result = BobKeyInputRequirer().load(dict(bobkey=bytes(federated_bob.public_keys(DecryptingPower)).hex()))
assert isinstance(result['bobkey'], PublicKey)

View File

@ -1,149 +0,0 @@
"""
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 datetime
from base64 import b64encode
import maya
import pytest
from nucypher_core import (
MessageKit as MessageKitClass,
EncryptedTreasureMap as EncryptedTreasureMapClass)
from nucypher_core.umbral import SecretKey
from nucypher.characters.control.specifications.fields import (
DateTime,
FileField,
Key,
MessageKit,
EncryptedTreasureMap
)
from nucypher.characters.lawful import Enrico
from nucypher.control.specifications.exceptions import InvalidInputData
#
# FIXME currently fails, 'Label' also has inconsistency - see #2714
# def test_cleartext():
# field = Cleartext()
#
# data = b"sdasdadsdad"
# serialized = field._serialize(value=data, attr=None, obj=None)
#
# deserialized = field._deserialize(value=serialized, attr=None, data=None)
# assert deserialized == data
def test_file(tmpdir):
text = b"I never saw a wild thing sorry for itself. A small bird will drop frozen dead from a bough without " \
b"ever having felt sorry for itself." # -- D.H. Lawrence
filepath = tmpdir / "dh_lawrence.txt"
with open(filepath, 'wb') as f:
f.write(text)
file_field = FileField()
deserialized = file_field._deserialize(value=filepath, attr=None, data=None)
assert deserialized == text
file_field._validate(value=filepath)
non_existent_file = tmpdir / "non_existent_file.txt"
file_field._validate(value=non_existent_file)
def test_date_time():
field = DateTime()
# test data
now = maya.now()
serialized = field._serialize(value=now, attr=None, obj=None)
assert serialized == now.iso8601()
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert deserialized == now
# modified time
new_time = now + datetime.timedelta(hours=5)
serialized_new_time = field._serialize(value=new_time, attr=None, obj=None)
assert serialized_new_time != now.iso8601()
assert serialized_new_time == new_time.iso8601()
deserialized_new_time = field._deserialize(value=serialized_new_time, attr=None, data=None)
assert deserialized_new_time != now
assert deserialized_new_time == new_time
# invalid date
with pytest.raises(InvalidInputData):
field._deserialize(value="test", attr=None, data=None)
def test_key():
field = Key()
umbral_pub_key = SecretKey.random().public_key()
other_umbral_pub_key = SecretKey.random().public_key()
serialized = field._serialize(value=umbral_pub_key, attr=None, obj=None)
assert serialized == bytes(umbral_pub_key).hex()
assert serialized != bytes(other_umbral_pub_key).hex()
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert deserialized == umbral_pub_key
assert deserialized != other_umbral_pub_key
with pytest.raises(InvalidInputData):
field._deserialize(value=b"PublicKey".hex(), attr=None, data=None)
def test_message_kit(enacted_federated_policy, federated_alice):
# Setup
enrico = Enrico.from_alice(federated_alice, label=enacted_federated_policy.label)
message = 'this is a message'
plaintext_bytes = bytes(message, encoding='utf-8')
message_kit = enrico.encrypt_message(plaintext=plaintext_bytes)
message_kit_bytes = bytes(message_kit)
message_kit = MessageKitClass.from_bytes(message_kit_bytes)
# Test
field = MessageKit()
serialized = field._serialize(value=message_kit, attr=None, obj=None)
assert serialized == b64encode(bytes(message_kit)).decode()
deserialized = field._deserialize(value=serialized, attr=None, data=None)
deserialized_plaintext = federated_alice.decrypt_message_kit(enacted_federated_policy.label, deserialized)[0]
assert deserialized_plaintext == plaintext_bytes
with pytest.raises(InvalidInputData):
field._deserialize(value=b"MessageKit", attr=None, data=None)
def test_treasure_map(enacted_federated_policy):
treasure_map = enacted_federated_policy.treasure_map
field = EncryptedTreasureMap()
serialized = field._serialize(value=treasure_map, attr=None, obj=None)
assert serialized == b64encode(bytes(treasure_map)).decode()
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert isinstance(deserialized, EncryptedTreasureMapClass)
assert bytes(deserialized) == bytes(treasure_map)
with pytest.raises(InvalidInputData):
field._deserialize(value=b64encode(b"TreasureMap").decode(), attr=None, data=None)

View File

@ -0,0 +1,40 @@
"""
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 pytest
from nucypher_core.umbral import SecretKey
from nucypher.control.specifications.exceptions import InvalidInputData
from nucypher.utilities.porter.control.specifications.fields import Key
def test_key():
field = Key()
umbral_pub_key = SecretKey.random().public_key()
other_umbral_pub_key = SecretKey.random().public_key()
serialized = field._serialize(value=umbral_pub_key, attr=None, obj=None)
assert serialized == bytes(umbral_pub_key).hex()
assert serialized != bytes(other_umbral_pub_key).hex()
deserialized = field._deserialize(value=serialized, attr=None, data=None)
assert deserialized == umbral_pub_key
assert deserialized != other_umbral_pub_key
with pytest.raises(InvalidInputData):
field._deserialize(value=b"PublicKey".hex(), attr=None, data=None)

View File

@ -22,13 +22,13 @@ from typing import Dict, List, Optional, Tuple
from nucypher_core import MessageKit, RetrievalKit
from nucypher.characters.control.specifications.fields import Key, TreasureMap
from nucypher.characters.lawful import Enrico
from nucypher.control.specifications.fields import JSON
from nucypher.crypto.powers import DecryptingPower
from nucypher.utilities.porter.control.specifications.fields import (
RetrievalKit as RetrievalKitField,
RetrievalKit as RetrievalKitField, Key,
)
from nucypher.utilities.porter.control.specifications.fields.treasuremap import TreasureMap
def generate_random_label() -> bytes: