mirror of https://github.com/nucypher/nucypher.git
Merge pull request #2987 from KPrasch/purge
Assorted deprecations (RPC and Web servers, Enrico CLI, Literature, Specifications, Ursula Interactivity)pull/2991/head
commit
a895b0d72e
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -93,16 +93,3 @@ Yb, `88 88 IP'`Yb
|
|||
the Untrusted Re-Encryption Proxy.
|
||||
{}
|
||||
'''
|
||||
|
||||
|
||||
STAKEHOLDER_BANNER = r"""
|
||||
____ __ __
|
||||
/\ _`\ /\ \__ /\ \
|
||||
\ \,\L\_\ \ ,_\ __ \ \ \/'\ __ _ __
|
||||
\/_\__ \\ \ \/ /'__`\\ \ , < /'__`\/\`'__\
|
||||
/\ \L\ \ \ \_/\ \L\.\\ \ \\`\ /\ __/\ \ \/
|
||||
\ `\____\ \__\ \__/.\_\ \_\ \_\ \____\\ \_\
|
||||
\/_____/\/__/\/__/\/_/\/_/\/_/\/____/ \/_/
|
||||
|
||||
The Holder of Stakes.
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/>.
|
||||
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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/>.
|
||||
"""
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 *
|
|
@ -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()
|
|
@ -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}")
|
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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*")
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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}."
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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}")
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue