Merge pull request #2115 from KPrasch/card

Let me give you my card...
pull/2467/head
K Prasch 2020-12-09 17:26:03 -08:00 committed by GitHub
commit 8875075902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1184 additions and 179 deletions

View File

@ -0,0 +1 @@
Introduces "Character Cards" a serializable identity abstraction and 'nucypher contacts' CLI to support.

View File

@ -18,11 +18,11 @@
import click
from marshmallow import validates_schema
from nucypher.cli import options, types
from nucypher.characters.control.specifications import fields
from nucypher.characters.control.specifications.base import BaseSchema
from nucypher.characters.control.specifications.exceptions import InvalidArgumentCombo
from nucypher.cli import options, types
class PolicyBaseSchema(BaseSchema):
@ -33,14 +33,14 @@ class PolicyBaseSchema(BaseSchema):
'--bob-encrypting-key',
'-bek',
help="Bob's encrypting key as a hexadecimal string",
type=click.STRING, required=True,))
type=click.STRING, required=False))
bob_verifying_key = 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))
type=click.STRING, required=False))
m = fields.M(
required=True, load_only=True,
click=options.option_m)

View File

@ -33,7 +33,7 @@ class JoinPolicy(BaseSchema): #TODO: this doesn't have a cli implementation
'--alice-verifying-key',
'-avk',
help="Alice's verifying key as a hexadecimal string",
required=True, type=click.STRING,))
required=False, type=click.STRING,))
policy_encrypting_key = fields.String(dump_only=True)
# this should be a Key Field
@ -55,7 +55,7 @@ class Retrieve(BaseSchema):
'-avk',
help="Alice's verifying key as a hexadecimal string",
type=click.STRING,
required=True))
required=False))
message_kit = fields.UmbralMessageKit(
required=True, load_only=True,
click=options.option_message_kit(required=True))

View File

@ -16,10 +16,10 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import contextlib
import json
from collections import OrderedDict
import contextlib
import maya
import random
import time
@ -52,7 +52,9 @@ from queue import Queue
from random import shuffle
from twisted.internet import reactor, stdio, threads
from twisted.internet.task import LoopingCall
from typing import Dict, Iterable, List, Optional, Tuple, Union
from twisted.logger import Logger
from typing import Dict, Iterable, List, Tuple, Union
from typing import Optional
from umbral import pre
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
@ -81,7 +83,13 @@ from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.constants import HRAC_LENGTH, PUBLIC_KEY_LENGTH
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower, DelegatingPower, PowerUpError, SigningPower, TransactingPower
from nucypher.crypto.powers import (
DecryptingPower,
DelegatingPower,
PowerUpError,
SigningPower,
TransactingPower
)
from nucypher.crypto.signing import InvalidSignature
from nucypher.datastore.datastore import DatastoreTransactionError, RecordNotFound
from nucypher.datastore.models import PolicyArrangement, TreasureMap as DatastoreTreasureMap
@ -119,6 +127,10 @@ class Alice(Character, BlockchainPolicyAuthor):
rate: int = None,
duration_periods: int = None,
# Policy Storage
store_policy_credentials: bool = None,
store_character_cards: bool = None,
# Middleware
timeout: int = 10, # seconds # TODO: configure NRN
network_middleware: RestMiddleware = None,
@ -172,6 +184,13 @@ 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
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):
"""
@ -501,6 +520,11 @@ class Bob(Character):
raise ValueError("Don't pass both treasure_map and map_id - pick one or the other.")
return treasure_map
def get_card(self) -> 'Card':
from nucypher.policy.identity import Card
card = Card.from_character(self)
return card
def peek_at_treasure_map(self, treasure_map=None, map_id=None):
"""
Take a quick gander at the TreasureMap matching map_id to see which

View File

@ -17,11 +17,12 @@
import glob
import os
from typing import Callable
from typing import Optional, Tuple, Type
import click
import os
from tabulate import tabulate
from typing import Optional, Tuple, Type
from web3.main import Web3
from nucypher.blockchain.eth.actors import StakeHolder, Staker, Wallet
@ -44,10 +45,11 @@ from nucypher.cli.literature import (
SELECT_STAKING_ACCOUNT_INDEX,
SELECTED_ACCOUNT
)
from nucypher.cli.painting.policies import paint_cards
from nucypher.cli.painting.staking import paint_stakes
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, NUCYPHER_ENVVAR_WORKER_ADDRESS
from nucypher.config.node import CharacterConfiguration
from typing import Callable
from nucypher.policy.identity import Card
def select_stake(staker: Staker,
@ -289,3 +291,18 @@ def select_config_file(emitter: StdoutEmitter,
config_file = config_files[0]
return config_file
def select_card(emitter, card_identifier: str) -> Card:
if not card_identifier:
cards = []
for filename in os.listdir(Card.CARD_DIR):
filepath = Card.CARD_DIR / filename
card = Card.load(filepath=filepath)
cards.append(card)
paint_cards(emitter=emitter, cards=cards, as_table=True)
selection = click.prompt('Select card', type=click.IntRange(0, len(cards)))
card = cards[selection]
else:
card = Card.load(identifier=card_identifier)
return card

View File

@ -26,6 +26,7 @@ from web3.main import Web3
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import AliceInterface
from nucypher.characters.lawful import Bob
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.configure import (
destroy_configuration,
@ -61,11 +62,12 @@ from nucypher.cli.options import (
option_teacher_uri,
option_lonely
)
from nucypher.cli.painting.help import paint_new_installation_help
from nucypher.cli.painting.help import (
paint_new_installation_help,
paint_probationary_period_disclaimer,
enforce_probationary_period
)
from nucypher.cli.painting.policies import paint_single_card
from nucypher.cli.processes import get_geth_provider_process
from nucypher.cli.types import EIP55_CHECKSUM_ADDRESS, GWEI
from nucypher.cli.utils import make_cli_character, setup_emitter
@ -76,14 +78,7 @@ from nucypher.config.constants import (
)
from nucypher.config.keyring import NucypherKeyring
from nucypher.network.middleware import RestMiddleware
option_bob_verifying_key = click.option(
'--bob-verifying-key',
'-bvk',
help="Bob's verifying key as a hexadecimal string",
type=click.STRING,
required=True
)
from nucypher.policy.identity import Card
option_pay_with = click.option('--pay-with', help="Run with a specified account", type=EIP55_CHECKSUM_ADDRESS)
option_duration_periods = click.option('--duration-periods', help="Policy duration in periods", type=click.INT)
@ -424,6 +419,23 @@ def public_keys(general_config, character_options, config_file):
return response
@alice.command()
@group_character_options
@option_config_file
@group_general_config
@click.option('--nickname', help="Human-readable nickname / alias for a card", type=click.STRING, required=False)
def make_card(general_config, character_options, config_file, nickname):
"""Create a character card file for public key sharing"""
emitter = setup_emitter(general_config)
ALICE = character_options.create_character(emitter, config_file, general_config.json_ipc, load_seednodes=False)
card = Card.from_character(ALICE)
if nickname:
card.nickname = nickname
card.save(overwrite=True)
emitter.message(f"Saved new character card to {card.filepath}", color='green')
paint_single_card(card=card, emitter=emitter)
@alice.command('derive-policy-pubkey')
@AliceInterface.connect_cli('derive_policy_encrypting_key')
@group_character_options
@ -442,7 +454,9 @@ def derive_policy_pubkey(general_config, label, character_options, config_file):
@group_general_config
@group_character_options
@option_force
@click.option('--bob', type=click.STRING, help="The card id or nickname of a stored Bob card.")
def grant(general_config,
bob,
bob_encrypting_key,
bob_verifying_key,
label,
@ -459,7 +473,7 @@ def grant(general_config,
emitter = setup_emitter(general_config)
ALICE = character_options.create_character(emitter, config_file, general_config.json_ipc)
# Input validation
# Policy option validation
if ALICE.federated_only:
if any((value, rate)):
message = "Can't use --value or --rate with a federated Alice."
@ -467,6 +481,22 @@ def grant(general_config,
elif bool(value) and bool(rate):
raise click.BadOptionUsage(option_name="--rate", message="Can't use --value if using --rate")
# Grantee selection
if bob and any((bob_encrypting_key, bob_verifying_key)):
message = '--bob cannot be used with --bob-encrypting-key or --bob-veryfying key'
raise click.BadOptionUsage(option_name='--bob', message=message)
if bob:
card = Card.load(identifier=bob)
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 = card.encrypting_key.hex()
bob_verifying_key = card.verifying_key.hex()
# Interactive collection follows:
# TODO: Extricate to support modules
# - Disclaimer

View File

@ -14,13 +14,13 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
import click
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.characters.control.interfaces import BobInterface
from nucypher.characters.lawful import Alice
from nucypher.cli.actions.auth import get_nucypher_password
from nucypher.cli.actions.configure import (
destroy_configuration,
@ -51,11 +51,13 @@ from nucypher.cli.options import (
option_lonely
)
from nucypher.cli.painting.help import paint_new_installation_help
from nucypher.cli.painting.policies import paint_single_card
from nucypher.cli.utils import make_cli_character, setup_emitter
from nucypher.config.characters import BobConfiguration
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.powers import DecryptingPower
from nucypher.network.middleware import RestMiddleware
from nucypher.policy.identity import Card
class BobConfigOptions:
@ -297,8 +299,26 @@ def public_keys(general_config, character_options, config_file):
@bob.command()
@group_character_options
@option_config_file
@BobInterface.connect_cli('retrieve')
@group_general_config
@click.option('--nickname', help="Human-readable nickname / alias for a card", type=click.STRING, required=False)
def make_card(general_config, character_options, config_file, nickname):
emitter = setup_emitter(general_config)
BOB = character_options.create_character(emitter, config_file)
card = Card.from_character(BOB)
if nickname:
card.nickname = nickname
card.save(overwrite=True)
emitter.message(f"Saved new character card to {card.filepath}", color='green')
paint_single_card(card=card, emitter=emitter)
@bob.command()
@group_character_options
@option_config_file
@group_general_config
@option_force
@BobInterface.connect_cli('retrieve')
@click.option('--alice', type=click.STRING, help="The card id or nickname of a stored Alice card.")
@click.option('--ipfs', help="Download an encrypted message from IPFS at the specified gateway URI")
def retrieve(general_config,
character_options,
@ -307,19 +327,15 @@ def retrieve(general_config,
policy_encrypting_key,
alice_verifying_key,
message_kit,
ipfs):
ipfs,
alice,
force):
"""Obtain plaintext from encrypted data, if access was granted."""
# Setup
emitter = setup_emitter(general_config)
BOB = character_options.create_character(emitter, config_file)
# Validate
if not all((label, policy_encrypting_key, alice_verifying_key, message_kit)):
input_specification, output_specification = BOB.control.get_specifications(interface_name='retrieve')
required_fields = ', '.join(input_specification)
raise click.BadArgumentUsage(f'{required_fields} are required flags to retrieve')
if ipfs:
import ipfshttpclient
# TODO: #2108
@ -330,6 +346,21 @@ def retrieve(general_config,
emitter.message(f"Downloaded message kit from IPFS (CID {cid})", color='green')
message_kit = raw_message_kit.decode() # cast to utf-8
if not alice_verifying_key:
if alice: # from storage
card = Card.load(identifier=alice)
if card.character is not Alice:
emitter.error('Grantee card is not an Alice.')
raise click.Abort
alice_verifying_key = card.verifying_key.hex()
emitter.message(f'{card.nickname or ("Alice #"+card.id.hex())}\n'
f'Verifying Key | {card.verifying_key.hex()}',
color='green')
if not force:
click.confirm('Is this the correct Granter (Alice)?', abort=True)
else: # interactive
alice_verifying_key = click.prompt("Enter Alice's verifying key")
# Request
bob_request_data = {
'label': label,

View File

@ -0,0 +1,151 @@
"""
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
import os
import shutil
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions.select import select_card
from nucypher.cli.options import option_force
from nucypher.cli.painting.policies import paint_single_card, paint_cards
from nucypher.cli.types import EXISTING_READABLE_FILE, UMBRAL_PUBLIC_KEY_HEX
from nucypher.policy.identity import Card
id_option = click.option('--id', 'card_id', help="A single card's checksum or ID", type=click.STRING, required=False)
type_option = click.option('--type', 'character_flag', help="Type of card: (A)lice or (B)ob.", type=click.STRING, required=False)
verifying_key_option = click.option('--verifying-key', help="Alice's or Bob's verifying key as hex", type=click.STRING, required=False)
encrypting_key_option = click.option('--encrypting-key', help="An encrypting key as hex", type=click.STRING, required=False)
nickname_option = click.option('--nickname', help="Human-readable nickname / alias for a card", type=click.STRING, required=False)
@click.group()
def contacts():
"""Lightweight contacts utility to store public keys of known ("character cards") Alices and Bobs."""
@contacts.command()
@click.argument('query')
@click.option('--qrcode', help="Display the QR code representing a card to the console", is_flag=True, default=None)
def show(query, qrcode):
"""
Lookup and view existing character card
QUERY can be either the card id or nickname.
"""
emitter = StdoutEmitter()
try:
card = select_card(emitter=emitter, card_identifier=query)
except Card.UnknownCard as e:
return emitter.error(str(e))
paint_single_card(emitter=emitter, card=card, qrcode=qrcode)
@contacts.command('list')
def _list():
"""Show all character cards"""
emitter = StdoutEmitter()
card_directory = Card.CARD_DIR
try:
card_filepaths = os.listdir(card_directory)
except FileNotFoundError:
os.mkdir(Card.CARD_DIR)
card_filepaths = os.listdir(card_directory)
if not card_filepaths:
emitter.error(f'No cards found at {card_directory}. '
f"To create one run 'nucypher {contacts.name} {create.name}'.")
cards = list()
for filename in card_filepaths:
card = Card.load(filepath=Card.CARD_DIR / filename)
cards.append(card)
paint_cards(emitter=emitter, cards=cards, as_table=True)
@contacts.command()
@type_option
@encrypting_key_option
@verifying_key_option
@nickname_option
@option_force
def create(character_flag, verifying_key, encrypting_key, nickname, force):
"""Store a new character card"""
emitter = StdoutEmitter()
# Validate
if not all((character_flag, verifying_key, encrypting_key)) and force:
emitter.error(f'--verifying-key, --encrypting-key, and --type are required with --force enabled.')
# Card type
from constant_sorrow.constants import ALICE, BOB
flags = {'a': ALICE, 'b': BOB}
if not character_flag:
choice = click.prompt('Enter Card Type - (A)lice or (B)ob', type=click.Choice(['a', 'b'], case_sensitive=False))
character_flag = flags[choice]
else:
character_flag = flags[character_flag]
# Verifying Key
if not verifying_key:
verifying_key = click.prompt('Enter Verifying Key', type=UMBRAL_PUBLIC_KEY_HEX)
verifying_key = bytes.fromhex(verifying_key) # TODO: Move / Validate
# Encrypting Key
if character_flag is BOB:
if not encrypting_key:
encrypting_key = click.prompt('Enter Encrypting Key', type=UMBRAL_PUBLIC_KEY_HEX)
encrypting_key = bytes.fromhex(encrypting_key) # TODO: Move / Validate
# Init
new_card = Card(character_flag=character_flag,
verifying_key=verifying_key,
encrypting_key=encrypting_key,
nickname=nickname)
# Nickname
if not force and not nickname:
card_id_hex = new_card.id.hex()
nickname = click.prompt('Enter nickname for card', default=card_id_hex)
if nickname != card_id_hex: # not the default
nickname = nickname.strip()
new_card.nickname = nickname
# Save
new_card.save()
emitter.message(f'Saved new card {new_card}', color='green')
paint_single_card(emitter=emitter, card=new_card)
@contacts.command()
@id_option
@option_force
def delete(force, card_id):
"""Delete an existing character card."""
emitter = StdoutEmitter()
card = select_card(emitter=emitter, card_identifier=card_id)
if not force:
click.confirm(f'Are you sure you want to delete {card}?', abort=True)
card.delete()
emitter.message(f'Deleted card for {card.id.hex()}.', color='red')
@contacts.command()
@click.option('--filepath', help="System filepath of stored card to import", type=EXISTING_READABLE_FILE)
def import_card(filepath):
"""Import a character card from a card file"""
emitter = StdoutEmitter()
shutil.copy(filepath, Card.CARD_DIR)
# paint_single_card(card=card)
emitter.message(f'Successfully imported card.')

View File

@ -300,8 +300,8 @@ NO_FEE_TO_WITHDRAW = "No policy fee can be withdrawn."
# Configuration
#
MISSING_CONFIGURATION_FILE = """No {name} configuration file found. 'To create a new persistent {name} run:
nucypher {init_command}
MISSING_CONFIGURATION_FILE = """No {name} configuration file found.
To create a new persistent {name} run 'nucypher {init_command}'
"""

View File

@ -28,7 +28,8 @@ from nucypher.cli.commands import (
status,
ursula,
worklock,
cloudworkers
cloudworkers,
contacts
)
from nucypher.cli.painting.help import echo_version, echo_config_root_path, echo_logging_root_path
@ -70,20 +71,21 @@ def nucypher_cli():
ENTRY_POINTS = (
# Characters
# Characters & Actors
alice.alice, # Author of Policies
bob.bob, # Builder of Capsules
enrico.enrico, # Encryptor of Data
ursula.ursula, # Untrusted Re-Encryption Proxy
stake.stake, # Stake Management
worklock.worklock, # WorkLock
# Utility Commands
dao.dao, # NuCypher DAO
stake.stake, # Stake Management
status.status, # Network Status
felix.felix, # Faucet
dao.dao, # NuCypher DAO
multisig.multisig, # MultiSig operations
worklock.worklock, # WorkLock
cloudworkers.cloudworkers # Remote Worker node management
felix.felix, # Faucet
cloudworkers.cloudworkers, # Remote Worker node management
contacts.contacts, # Character "card" management
)
for entry_point in ENTRY_POINTS:

View File

@ -112,7 +112,7 @@ def option_message_kit(required: bool = False):
def option_network(required: bool = False,
default: str = None, # TODO: NetworksInventory.DEFAULT is not a good default for the moment -- 2214
default: str = None, # NetworksInventory.DEFAULT is not a good global default (2214)
validate: bool = False):
return click.option(
'--network',

View File

@ -0,0 +1,43 @@
"""
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 tabulate import tabulate
from typing import List
from nucypher.characters.lawful import Bob
from nucypher.policy.identity import Card
def paint_single_card(emitter, card: Card, qrcode: bool = False) -> None:
emitter.echo('*'*90)
emitter.message(f'{(card.nickname or str(card.character.__name__)).capitalize()}\'s Card ({card.id.hex()})')
emitter.echo(f'Verifying Key - {card.verifying_key.hex()}')
if card.character is Bob:
emitter.echo(f'Encrypting Key - {card.encrypting_key.hex()}')
if qrcode:
card.to_qr_code()
emitter.echo('*'*90)
def paint_cards(emitter, cards: List[Card], as_table: bool = True) -> None:
if as_table:
rows = [card.describe() for card in cards]
emitter.echo(tabulate(rows, headers='keys', showindex='always', tablefmt="presto"))
else:
for card in cards:
paint_single_card(emitter=emitter, card=card)

View File

@ -16,14 +16,16 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import click
from cryptography.exceptions import InternalError
from decimal import Decimal, DecimalException
from eth_utils import to_checksum_address
from ipaddress import ip_address
from umbral.keys import UmbralPublicKey
from nucypher.blockchain.economics import StandardTokenEconomics
from nucypher.blockchain.eth.token import NU
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.token import NU
class ChecksumAddress(click.ParamType):
@ -103,6 +105,21 @@ class NuCypherNetworkName(click.ParamType):
return value
class UmbralPublicKeyHex(click.ParamType):
name = 'nucypher_umbral_public_key'
def __init__(self, validate: bool = True):
self.validate = bool(validate)
def convert(self, value, param, ctx):
if self.validate:
try:
_key = UmbralPublicKey.from_hex(value)
except (InternalError, ValueError):
self.fail(f"'{value}' is not a valid nucypher public key.")
return value
# Ethereum
EIP55_CHECKSUM_ADDRESS = ChecksumAddress()
WEI = click.IntRange(min=1, clamp=False) # TODO: Better validation for ether and wei values?
@ -121,3 +138,4 @@ NETWORK_PORT = click.IntRange(min=0, max=65535, clamp=False)
IPV4_ADDRESS = IPv4Address()
GAS_STRATEGY_CHOICES = click.Choice(list(BlockchainInterface.GAS_STRATEGIES.keys()))
UMBRAL_PUBLIC_KEY_HEX = UmbralPublicKeyHex()

View File

@ -152,11 +152,22 @@ class AliceConfiguration(CharacterConfiguration):
DEFAULT_M = 2
DEFAULT_N = 3
DEFAULT_STORE_POLICIES = True
DEFAULT_STORE_CARDS = True
_CONFIG_FIELDS = (
*CharacterConfiguration._CONFIG_FIELDS,
'store_policies',
'store_cards'
)
def __init__(self,
m: int = None,
n: int = None,
rate: int = None,
duration_periods: int = None,
store_policies: bool = DEFAULT_STORE_POLICIES,
store_cards: bool = DEFAULT_STORE_CARDS,
*args, **kwargs):
super().__init__(*args, **kwargs)
@ -167,8 +178,16 @@ class AliceConfiguration(CharacterConfiguration):
self.rate = rate
self.duration_periods = duration_periods
self.store_policies = store_policies
self.store_cards = store_cards
def static_payload(self) -> dict:
payload = dict(m=self.m, n=self.n)
payload = dict(
m=self.m,
n=self.n,
store_policies=self.store_policies,
store_cards=self.store_cards
)
if not self.federated_only:
if self.rate:
payload['rate'] = self.rate
@ -188,8 +207,23 @@ class BobConfiguration(CharacterConfiguration):
CHARACTER_CLASS = Bob
NAME = CHARACTER_CLASS.__name__.lower()
DEFAULT_CONTROLLER_PORT = 7151
DEFFAULT_STORE_POLICIES = True
DEFAULT_STORE_CARDS = True
_CONFIG_FIELDS = (
*CharacterConfiguration._CONFIG_FIELDS,
'store_policies',
'store_cards'
)
def __init__(self,
store_policies: bool = DEFFAULT_STORE_POLICIES,
store_cards: bool = DEFAULT_STORE_CARDS,
*args, **kwargs):
super().__init__(*args, **kwargs)
self.store_policies = store_policies
self.store_cards = store_cards
def write_keyring(self, password: str, **generation_kwargs) -> NucypherKeyring:
return super().write_keyring(password=password,
@ -197,6 +231,13 @@ class BobConfiguration(CharacterConfiguration):
rest=False,
**generation_kwargs)
def static_payload(self) -> dict:
payload = dict(
store_policies=self.store_policies,
store_cards=self.store_cards
)
return {**super().static_payload(), **payload}
class FelixConfiguration(CharacterConfiguration):
from nucypher.characters.chaotic import Felix

View File

@ -69,6 +69,14 @@ class CharacterConfiguration(BaseConfiguration):
# Gas
DEFAULT_GAS_STRATEGY = 'fast'
_CONFIG_FIELDS = ('config_root',
'poa',
'light',
'provider_uri',
'registry_filepath',
'gas_strategy',
'signer_uri')
def __init__(self,
# Base
@ -334,14 +342,7 @@ class CharacterConfiguration(BaseConfiguration):
Warning: This method allows mutation and may result in an inconsistent configuration.
"""
merged_parameters = {**self.static_payload(), **self.dynamic_payload, **overrides}
non_init_params = ('config_root',
'poa',
'light',
'provider_uri',
'registry_filepath',
'gas_strategy',
'signer_uri')
character_init_params = filter(lambda t: t[0] not in non_init_params, merged_parameters.items())
character_init_params = filter(lambda t: t[0] not in self._CONFIG_FIELDS, merged_parameters.items())
return dict(character_init_params)
def produce(self, **overrides) -> CHARACTER_CLASS:

View File

@ -15,39 +15,42 @@ 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 OrderedDict
from typing import Optional, Tuple
import maya
from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.primitives import hashes
from eth_utils import to_canonical_address, to_checksum_address
from bytestring_splitter import BytestringKwargifier
from bytestring_splitter import (
BytestringKwargifier,
BytestringSplitter,
BytestringSplittingError,
VariableLengthBytestring
)
from constant_sorrow.constants import CFRAG_NOT_RETAINED, NO_DECRYPTION_PERFORMED
from constant_sorrow.constants import NOT_SIGNED
from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH, ETH_HASH_BYTE_LENGTH
from nucypher.characters.lawful import Bob, Character
from nucypher.crypto.api import encrypt_and_sign, keccak_digest, verify_eip_191
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import InvalidSignature, Signature, signature_splitter
from nucypher.crypto.splitters import capsule_splitter, cfrag_splitter, key_splitter
from nucypher.crypto.utils import (canonical_address_from_umbral_key,
get_coordinates_as_bytes,
get_signature_recovery_value)
from nucypher.network.middleware import RestMiddleware
from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.primitives import hashes
from eth_utils import to_canonical_address, to_checksum_address
from typing import Optional, Tuple
from umbral.config import default_params
from umbral.curvebn import CurveBN
from umbral.keys import UmbralPublicKey
from umbral.pre import Capsule
from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH, ETH_HASH_BYTE_LENGTH
from nucypher.characters.lawful import Bob, Character
from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.api import verify_eip_191
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import InvalidSignature, Signature, signature_splitter, SignatureStamp
from nucypher.crypto.splitters import capsule_splitter, key_splitter
from nucypher.crypto.splitters import cfrag_splitter
from nucypher.crypto.utils import (
canonical_address_from_umbral_key,
get_coordinates_as_bytes,
get_signature_recovery_value
)
from nucypher.network.middleware import RestMiddleware
class TreasureMap:
ID_LENGTH = 32
@ -274,75 +277,6 @@ class SignedTreasureMap(TreasureMap):
"Can't cast a DecentralizedTreasureMap to bytes until it has a blockchain signature (otherwise, is it really a 'DecentralizedTreasureMap'?")
return self._blockchain_signature + super().__bytes__()
class PolicyCredential:
"""
A portable structure that contains information necessary for Alice or Bob
to utilize the policy on the network that the credential describes.
"""
def __init__(self, alice_verifying_key, label, expiration, policy_pubkey,
treasure_map=None):
self.alice_verifying_key = alice_verifying_key
self.label = label
self.expiration = expiration
self.policy_pubkey = policy_pubkey
self.treasure_map = treasure_map
def to_json(self):
"""
Serializes the PolicyCredential to JSON.
"""
cred_dict = {
'alice_verifying_key': bytes(self.alice_verifying_key).hex(),
'label': self.label.hex(),
'expiration': self.expiration.iso8601(),
'policy_pubkey': bytes(self.policy_pubkey).hex()
}
if self.treasure_map is not None:
cred_dict['treasure_map'] = bytes(self.treasure_map).hex()
return json.dumps(cred_dict)
@classmethod
def from_json(cls, data: str, federated=False):
"""
Deserializes the PolicyCredential from JSON.
"""
from nucypher.characters.lawful import Ursula
cred_json = json.loads(data)
alice_verifying_key = UmbralPublicKey.from_bytes(
cred_json['alice_verifying_key'],
decoder=bytes().fromhex)
label = bytes().fromhex(cred_json['label'])
expiration = maya.MayaDT.from_iso8601(cred_json['expiration'])
policy_pubkey = UmbralPublicKey.from_bytes(
cred_json['policy_pubkey'],
decoder=bytes().fromhex)
treasure_map = None
if 'treasure_map' in cred_json:
if federated: # I know know. TODO: WTF. 466 and just... you know... whatever.
_MapClass = TreasureMap
else:
_MapClass = SignedTreasureMap
treasure_map = _MapClass.from_bytes(
bytes().fromhex(cred_json['treasure_map']))
return cls(alice_verifying_key, label, expiration, policy_pubkey,
treasure_map)
def __eq__(self, other):
return ((self.alice_verifying_key == other.alice_verifying_key) and
(self.label == other.label) and
(self.expiration == other.expiration) and
(self.policy_pubkey == other.policy_pubkey))
class WorkOrder:
class PRETask:

402
nucypher/policy/identity.py Normal file
View File

@ -0,0 +1,402 @@
"""
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 pathlib import Path
from typing import Union, Optional, Dict, Callable
import base64
import constant_sorrow
import hashlib
import maya
import os
from bytestring_splitter import VariableLengthBytestring, BytestringKwargifier
from constant_sorrow.constants import ALICE, BOB, NO_SIGNATURE
from hexbytes.main import HexBytes
from umbral.keys import UmbralPublicKey
from nucypher.characters.base import Character
from nucypher.characters.lawful import Alice, Bob
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.crypto.powers import SigningPower, DecryptingPower
from nucypher.policy.collections import SignedTreasureMap
class Card:
""""
A simple serializable representation of a character's public materials.
"""
_alice_specification = dict(
character_flag=(bytes, 8),
verifying_key=(UmbralPublicKey, 33),
nickname=(bytes, VariableLengthBytestring),
)
_bob_specification = dict(
character_flag=(bytes, 8),
verifying_key=(UmbralPublicKey, 33),
encrypting_key=(UmbralPublicKey, 33),
nickname=(bytes, VariableLengthBytestring),
)
__CARD_TYPES = {
bytes(ALICE): Alice,
bytes(BOB): Bob,
}
__ID_LENGTH = 10 # TODO: Review this size (bytes of hex len?)
__MAX_NICKNAME_SIZE = 10
__BASE_PAYLOAD_SIZE = sum(length[1] for length in _bob_specification.values() if isinstance(length[1], int))
__MAX_CARD_LENGTH = __BASE_PAYLOAD_SIZE + __MAX_NICKNAME_SIZE + 2
__FILE_EXTENSION = 'card'
__DELIMITER = ':' # delimits nickname from ID
TRUNCATE = 16
CARD_DIR = Path(DEFAULT_CONFIG_ROOT) / 'cards'
NO_SIGNATURE.bool_value(False)
class InvalidCard(Exception):
"""Raised when an invalid, corrupted, or otherwise unsable card is encountered"""
class UnknownCard(Exception):
"""Raised when a card cannot be found in storage"""
class UnsignedCard(Exception):
"""Raised when a card serialization cannot be handled due to the lack of a signature"""
def __init__(self,
character_flag: Union[ALICE, BOB],
verifying_key: Union[UmbralPublicKey, bytes],
encrypting_key: Optional[Union[UmbralPublicKey, bytes]] = None,
card_dir: Path = CARD_DIR,
nickname: Optional[Union[bytes, str]] = None):
try:
self.__character_class = self.__CARD_TYPES[bytes(character_flag)]
except KeyError:
raise ValueError(f'Unsupported card type {str(character_flag)}')
self.__character_flag = character_flag
if isinstance(verifying_key, bytes):
verifying_key = UmbralPublicKey.from_bytes(verifying_key)
self.__verifying_key = verifying_key # signing public key
if isinstance(encrypting_key, bytes):
encrypting_key = UmbralPublicKey.from_bytes(encrypting_key)
self.__encrypting_key = encrypting_key # public key
if isinstance(nickname, str):
nickname = nickname.encode()
self.__nickname = nickname
self.__validate()
def __repr__(self) -> str:
name = self.nickname or f'{self.__character_class.__name__}'
short_key = bytes(self.__verifying_key).hex()[:6]
r = f'{self.__class__.__name__}({name}:{short_key}:{self.id.hex()[:6]})'
return r
def __eq__(self, other) -> bool:
if not isinstance(other, self.__class__):
raise TypeError(f'Cannot compare {self.__class__.__name__} and {other}')
return self.id == other.id
def __validate(self) -> bool:
if self.__nickname and (len(self.__nickname) > self.__MAX_NICKNAME_SIZE):
raise self.InvalidCard(f'Nickname exceeds maximum length of {self.__MAX_NICKNAME_SIZE}')
return True
@classmethod
def __hash(cls, payload: bytes) -> HexBytes:
blake = hashlib.blake2b()
blake.update(payload)
digest = blake.digest().hex()
truncated_digest = digest[:cls.__ID_LENGTH]
return HexBytes(truncated_digest)
@property
def character(self):
return self.__CARD_TYPES[bytes(self.__character_flag)]
#
# Serializers
#
def __bytes__(self) -> bytes:
self.__validate()
payload = self.__payload
if self.nickname:
payload += VariableLengthBytestring(self.__nickname)
return payload
def __hex__(self) -> str:
return self.to_hex()
@property
def __payload(self) -> bytes:
elements = [
self.__character_flag,
self.__verifying_key,
]
if self.character is Bob:
elements.append(self.__encrypting_key)
payload = b''.join(bytes(e) for e in elements)
return payload
@classmethod
def from_bytes(cls, card_bytes: bytes) -> 'Card':
if len(card_bytes) > cls.__MAX_CARD_LENGTH:
raise cls.InvalidCard(f'Card exceeds maximum size (max is {cls.__MAX_CARD_LENGTH} bytes card is {len(card_bytes)} bytes). '
f'Verify the card filepath and contents.')
character_flag = card_bytes[:8]
if character_flag == bytes(ALICE):
specification = cls._alice_specification
elif character_flag == bytes(BOB):
specification = cls._bob_specification
else:
raise RuntimeError(f'Unknown character card header ({character_flag}).')
return BytestringKwargifier(cls, **specification)(card_bytes)
@classmethod
def from_hex(cls, hexdata: str):
return cls.from_bytes(bytes.fromhex(hexdata))
def to_hex(self) -> str:
return bytes(self).hex()
@classmethod
def from_base64(cls, b64data: str):
return cls.from_bytes(base64.urlsafe_b64decode(b64data))
def to_base64(self) -> str:
return base64.urlsafe_b64encode(bytes(self)).decode()
def to_qr_code(self):
import qrcode
from qrcode.main import QRCode
qr = QRCode(
version=1,
box_size=1,
border=4, # min spec is 4
error_correction=qrcode.constants.ERROR_CORRECT_L,
)
qr.add_data(bytes(self))
qr.print_ascii()
@classmethod
def from_dict(cls, card: Dict):
instance = cls(nickname=card.get('nickname'),
verifying_key=card['verifying_key'],
encrypting_key=card['encrypting_key'],
character_flag=card['character'])
return instance
def to_dict(self) -> Dict:
payload = dict(
nickname=self.__nickname,
verifying_key=self.verifying_key,
encrypting_key=self.encrypting_key,
character=self.__character_flag
)
return payload
def describe(self, truncate: int = TRUNCATE) -> Dict:
description = dict(
nickname=self.__nickname,
id=self.id.hex(),
verifying_key=bytes(self.verifying_key).hex()[:truncate],
character=self.character.__name__
)
if self.character is Bob:
description['encrypting_key'] = bytes(self.encrypting_key).hex()[:truncate]
return description
def to_json(self, as_string: bool = True) -> Union[dict, str]:
payload = dict(
nickname=self.__nickname.decode(),
verifying_key=bytes(self.verifying_key).hex(),
encrypting_key=bytes(self.encrypting_key).hex(),
character=self.character.__name__
)
if as_string:
payload = json.dumps(payload)
return payload
@classmethod
def from_character(cls, character: Character, nickname: Optional[str] = None) -> 'Card':
flag = getattr(constant_sorrow.constants, character.__class__.__name__.upper())
instance = cls(verifying_key=character.public_keys(power_up_class=SigningPower),
encrypting_key=character.public_keys(power_up_class=DecryptingPower),
character_flag=bytes(flag),
nickname=nickname)
return instance
#
# Card API
#
@property
def verifying_key(self) -> UmbralPublicKey:
return self.__verifying_key
@property
def encrypting_key(self) -> UmbralPublicKey:
return self.__encrypting_key
@property
def id(self) -> HexBytes:
return self.__hash(self.__payload)
@property
def nickname(self) -> str:
if self.__nickname:
return self.__nickname.decode()
def set_nickname(self, nickname: str) -> None:
if len(nickname.encode()) > self.__MAX_NICKNAME_SIZE:
raise ValueError(f'New nickname exceeds maximum size ({self.__MAX_NICKNAME_SIZE} bytes)')
self.__nickname = nickname.encode()
@nickname.setter
def nickname(self, nickname: str) -> None:
self.set_nickname(nickname)
#
# Card Storage API
#
@property
def filepath(self) -> Path:
identifier = f'{self.nickname}{self.__DELIMITER}{self.id.hex()}' if self.__nickname else self.id.hex()
filename = f'{identifier}.{self.__FILE_EXTENSION}'
filepath = self.CARD_DIR / filename
return filepath
@property
def is_saved(self) -> bool:
exists = self.filepath.exists()
return exists
def save(self, encoder: Callable = base64.b64encode, overwrite: bool = False) -> Path:
if not self.CARD_DIR.exists():
os.mkdir(str(self.CARD_DIR))
if self.is_saved and not overwrite:
raise FileExistsError('Card exists. Pass overwrite=True to allow this operation.')
with open(str(self.filepath), 'wb') as file:
file.write(encoder(bytes(self)))
return Path(self.filepath)
@classmethod
def lookup(cls, identifier: str, card_dir: Optional[Path] = CARD_DIR) -> Path:
"""Resolve a card ID or nickname into a Path object"""
try:
nickname, _id = identifier.split(cls.__DELIMITER)
except ValueError:
nickname = identifier
for filename in os.listdir(Card.CARD_DIR):
if nickname.lower() in filename.lower():
break
else:
raise cls.UnknownCard(f'Unknown card nickname or ID "{nickname}".')
filepath = card_dir / filename
return filepath
@classmethod
def load(cls,
filepath: Optional[Path] = None,
identifier: str = None,
card_dir: Path = None,
decoder: Callable = base64.b64decode
) -> 'Card':
if not card_dir:
card_dir = cls.CARD_DIR
if filepath and identifier:
raise ValueError(f'Pass either filepath or identifier, not both.')
if not filepath:
filepath = cls.lookup(identifier=identifier, card_dir=card_dir)
try:
with open(str(filepath), 'rb') as file:
card_bytes = decoder(file.read())
except FileNotFoundError:
raise cls.UnknownCard
instance = cls.from_bytes(card_bytes)
return instance
def delete(self) -> None:
os.remove(str(self.filepath))
class PolicyCredential: # TODO: Rename this. It is not a credential in any way.
"""
A portable structure that contains information necessary for Alice or Bob
to utilize the policy on the network that the credential describes.
"""
def __init__(self, alice_verifying_key, label, expiration, policy_pubkey,
treasure_map=None):
self.alice_verifying_key = alice_verifying_key
self.label = label
self.expiration = expiration
self.policy_pubkey = policy_pubkey
self.treasure_map = treasure_map
def to_json(self):
"""
Serializes the PolicyCredential to JSON.
"""
cred_dict = {
'alice_verifying_key': bytes(self.alice_verifying_key).hex(),
'label': self.label.hex(),
'expiration': self.expiration.iso8601(),
'policy_pubkey': bytes(self.policy_pubkey).hex()
}
if self.treasure_map is not None:
cred_dict['treasure_map'] = bytes(self.treasure_map).hex()
return json.dumps(cred_dict)
@classmethod
def from_json(cls, data: str):
"""Deserializes the PolicyCredential from JSON."""
cred_json = json.loads(data)
alice_verifying_key = UmbralPublicKey.from_bytes(cred_json['alice_verifying_key'], decoder=bytes.fromhex)
label = bytes.fromhex(cred_json['label'])
expiration = maya.MayaDT.from_iso8601(cred_json['expiration'])
policy_pubkey = UmbralPublicKey.from_bytes(cred_json['policy_pubkey'], decoder=bytes.fromhex)
treasure_map = None
if 'treasure_map' in cred_json:
# TODO: Support unsigned treasuremaps?
treasure_map = SignedTreasureMap.from_bytes(bytes.fromhex(cred_json['treasure_map']))
return cls(alice_verifying_key,
label,
expiration,
policy_pubkey,
treasure_map)
def __eq__(self, other):
return ((self.alice_verifying_key == other.alice_verifying_key) and
(self.label == other.label) and
(self.expiration == other.expiration) and
(self.policy_pubkey == other.policy_pubkey))

View File

@ -14,30 +14,30 @@ 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 math
import time
import random
from abc import ABC, abstractmethod
from collections import OrderedDict, deque
from queue import Queue, Empty
from typing import Callable
from typing import Generator, List, Set
import datetime
from collections import OrderedDict
from queue import Queue, Empty
from typing import Callable, Tuple
from typing import Generator, Set, Optional
import math
import maya
import random
import time
from abc import ABC, abstractmethod
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from constant_sorrow.constants import NOT_SIGNED, UNKNOWN_KFRAG
from twisted._threads import AlreadyQuit
from twisted.internet import reactor
from twisted.internet.defer import ensureDeferred, Deferred
from twisted.python.threadpool import ThreadPool
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from constant_sorrow.constants import NOT_SIGNED, UNKNOWN_KFRAG
from typing import Generator, List, Set, Optional
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor
from nucypher.blockchain.eth.agents import PolicyManagerAgent, StakingEscrowAgent
from nucypher.blockchain.eth.agents import PolicyManagerAgent
from nucypher.characters.lawful import Alice, Ursula
from nucypher.crypto.api import keccak_digest, secure_random
from nucypher.crypto.constants import HRAC_LENGTH, PUBLIC_KEY_LENGTH
@ -46,9 +46,8 @@ from nucypher.crypto.powers import DecryptingPower, SigningPower, TransactingPow
from nucypher.crypto.utils import construct_policy_id
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.middleware import RestMiddleware
from nucypher.policy.identity import PolicyCredential
from nucypher.utilities.logging import Logger
from umbral.keys import UmbralPublicKey
from umbral.kfrags import KFrag
class Arrangement:
@ -340,11 +339,11 @@ class Policy(ABC):
"""Too many Ursulas rejected"""
def __init__(self,
alice,
label,
alice: Alice,
label: bytes,
expiration: maya.MayaDT,
bob=None,
kfrags=(UNKNOWN_KFRAG,),
bob: 'Bob' = None,
kfrags: Tuple[KFrag, ...] = (UNKNOWN_KFRAG,),
public_key=None,
m: int = None,
alice_signature=NOT_SIGNED) -> None:
@ -353,10 +352,10 @@ class Policy(ABC):
:param kfrags: A list of KFrags to distribute per this Policy.
:param label: The identity of the resource to which Bob is granted access.
"""
self.alice = alice # type: Alice
self.label = label # type: bytes
self.bob = bob # type: Bob
self.kfrags = kfrags # type: List[KFrag]
self.alice = alice
self.label = label
self.bob = bob
self.kfrags = kfrags
self.public_key = public_key
self._id = construct_policy_id(self.label, bytes(self.bob.stamp))
self.treasure_map = self._treasure_map_class(m=m)
@ -440,14 +439,16 @@ class Policy(ABC):
Alice or Bob. By default, it will include the treasure_map for the
policy unless `with_treasure_map` is False.
"""
from nucypher.policy.collections import PolicyCredential
treasure_map = self.treasure_map
if not with_treasure_map:
treasure_map = None
return PolicyCredential(self.alice.stamp, self.label, self.expiration,
self.public_key, treasure_map)
credential = PolicyCredential(alice_verifying_key=self.alice.stamp,
label=self.label,
expiration=self.expiration,
policy_pubkey=self.public_key,
treasure_map=treasure_map)
return credential
def __assign_kfrags(self) -> Generator[Arrangement, None, None]:

View File

@ -18,6 +18,8 @@ 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
import subprocess
import sys
@ -27,11 +29,11 @@ from setuptools.command.develop import develop
from setuptools.command.install import install
from typing import Dict
#
# Metadata
#
PACKAGE_NAME = 'nucypher'
BASE_DIR = Path(__file__).parent
PYPI_CLASSIFIERS = [
@ -89,11 +91,11 @@ class PostDevelopCommand(develop):
develop.run(self)
subprocess.call(f"scripts/installation/install_solc.py")
#
# Requirements
#
def read_requirements(path):
with open(os.path.join(BASE_DIR, path)) as f:
_pipenv_flags, *requirements = f.read().split('\n')
@ -115,18 +117,21 @@ DEPLOY_REQUIRES = [
]
URSULA_REQUIRES = ['prometheus_client', 'sentry-sdk'] # TODO: Consider renaming to 'monitor', etc.
ALICE_REQUIRES = ['qrcode']
BOB_REQUIRES = ['qrcode']
EXTRAS = {
# Admin
'docs': DOCS_REQUIRE,
'dev': DEV_REQUIRES + DOCS_REQUIRE + URSULA_REQUIRES,
'dev': DEV_REQUIRES + DOCS_REQUIRE + URSULA_REQUIRES + ALICE_REQUIRES,
'benchmark': DEV_REQUIRES + BENCHMARK_REQUIRES,
'deploy': DOCS_REQUIRE + DEPLOY_REQUIRES,
# User
'ursula': URSULA_REQUIRES
'ursula': URSULA_REQUIRES,
'alice': ALICE_REQUIRES,
'bob': BOB_REQUIRES
}

View File

@ -18,11 +18,12 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import datetime
import maya
import pytest
from umbral.kfrags import KFrag
from nucypher.crypto.api import keccak_digest
from nucypher.datastore.models import PolicyArrangement, TreasureMap as DatastoreTreasureMap
from nucypher.policy.collections import PolicyCredential, SignedTreasureMap as DecentralizedTreasureMap
from nucypher.datastore.models import PolicyArrangement
from nucypher.datastore.models import TreasureMap as DatastoreTreasureMap
from nucypher.policy.collections import SignedTreasureMap as DecentralizedTreasureMap
from nucypher.policy.identity import PolicyCredential
from tests.utils.middleware import MockRestMiddleware

View File

@ -23,8 +23,7 @@ from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import AliceConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN
from tests.constants import (FAKE_PASSWORD_CONFIRMED, INSECURE_DEVELOPMENT_PASSWORD, MOCK_CUSTOM_INSTALLATION_PATH,
MOCK_IP_ADDRESS)
from tests.constants import (FAKE_PASSWORD_CONFIRMED, INSECURE_DEVELOPMENT_PASSWORD, MOCK_CUSTOM_INSTALLATION_PATH)
@mock.patch('nucypher.config.characters.AliceConfiguration.default_filepath', return_value='/non/existent/file')
@ -115,6 +114,14 @@ def test_alice_control_starts_with_preexisting_configuration(click_runner, custo
assert result.exit_code == 0
def test_alice_make_card(click_runner, custom_filepath):
custom_config_filepath = os.path.join(custom_filepath, AliceConfiguration.generate_filename())
command = ('alice', 'make-card', '--nickname', 'flora', '--config-file', custom_config_filepath)
result = click_runner.invoke(nucypher_cli, command, input=FAKE_PASSWORD_CONFIRMED, catch_exceptions=False)
assert result.exit_code == 0
assert "Saved new character card " in result.output
def test_alice_cannot_init_with_dev_flag(click_runner):
init_args = ('alice', 'init', '--network', TEMPORARY_DOMAIN, '--federated-only', '--dev')
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False)

View File

@ -94,6 +94,14 @@ def test_bob_control_starts_with_preexisting_configuration(click_runner, custom_
assert "Bob Encrypting Key" in result.output
def test_bob_make_card(click_runner, custom_filepath):
custom_config_filepath = os.path.join(custom_filepath, BobConfiguration.generate_filename())
command = ('bob', 'make-card', '--nickname', 'anders', '--config-file', custom_config_filepath)
result = click_runner.invoke(nucypher_cli, command, input=FAKE_PASSWORD_CONFIRMED, catch_exceptions=False)
assert result.exit_code == 0
assert "Saved new character card " in result.output
def test_bob_view_with_preexisting_configuration(click_runner, custom_filepath):
custom_config_filepath = os.path.join(custom_filepath, BobConfiguration.generate_filename())
view_args = ('bob', 'config', '--config-file', custom_config_filepath)

View File

@ -0,0 +1,183 @@
"""
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
import pytest
from umbral.keys import UmbralPrivateKey
from nucypher.cli.main import nucypher_cli
from nucypher.policy.identity import Card
@pytest.fixture(scope='module')
def alice_verifying_key():
return UmbralPrivateKey.gen_key().get_pubkey().hex()
@pytest.fixture(scope='module')
def bob_nickname():
return 'edward'.capitalize()
@pytest.fixture(scope='module')
def alice_nickname():
return 'alice'.capitalize()
@pytest.fixture(scope='module')
def bob_verifying_key():
return UmbralPrivateKey.gen_key().get_pubkey().hex()
@pytest.fixture(scope='module')
def bob_encrypting_key():
return UmbralPrivateKey.gen_key().get_pubkey().hex()
def test_card_directory_autocreation(click_runner, mocker):
mocked_mkdir = mocker.patch('os.mkdir')
mocked_listdir = mocker.patch('os.listdir', side_effect=(FileNotFoundError, []))
command = ('contacts', 'list') # form list command
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
mocked_mkdir.assert_called_once()
mocked_listdir.call_count = 2
def test_list_cards_with_none_created(click_runner, certificates_tempdir):
command = ('contacts', 'list')
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert f'No cards found at {Card.CARD_DIR}.' in result.output
def test_create_alice_card_interactive(click_runner, alice_verifying_key, alice_nickname, mocker):
command = ('contacts', 'create')
user_input = (
'a', # Alice
alice_verifying_key, # Public key
alice_nickname # Nickname
)
user_input = '\n'.join(user_input)
assert len(os.listdir(Card.CARD_DIR)) == 0
# Let's play pretend: this alice does not have the card directory (yet)
mocker.patch('pathlib.Path.exists', return_value=False)
mocked_mkdir = mocker.patch('os.mkdir')
result = click_runner.invoke(nucypher_cli, command, input=user_input, catch_exceptions=False)
# The path was created.
mocked_mkdir.assert_called_once()
assert result.exit_code == 0, result.output
assert 'Enter Verifying Key' in result.output
assert 'Saved new card' in result.output
assert len(os.listdir(Card.CARD_DIR)) == 1
def test_create_alice_card_inline(click_runner, alice_verifying_key, alice_nickname):
command = ('contacts', 'create',
'--type', 'a',
'--verifying-key', UmbralPrivateKey.gen_key().get_pubkey().hex(),
'--nickname', 'philippa')
assert len(os.listdir(Card.CARD_DIR)) == 1
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert 'Saved new card' in result.output
assert len(os.listdir(Card.CARD_DIR)) == 2
def test_create_bob_card_interactive(click_runner, bob_nickname, bob_encrypting_key, bob_verifying_key):
command = ('contacts', 'create')
user_input = (
'b', # Bob
bob_encrypting_key, # Public key 1
bob_verifying_key, # Public key 2
bob_nickname # Nickname
)
user_input = '\n'.join(user_input)
assert len(os.listdir(Card.CARD_DIR)) == 2
result = click_runner.invoke(nucypher_cli, command, input=user_input, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert 'Enter Verifying Key' in result.output
assert 'Enter Encrypting Key' in result.output
assert 'Saved new card' in result.output
assert len(os.listdir(Card.CARD_DIR)) == 3
def test_create_bob_card_inline(click_runner, alice_verifying_key, alice_nickname):
command = ('contacts', 'create',
'--type', 'b',
'--verifying-key', UmbralPrivateKey.gen_key().get_pubkey().hex(),
'--encrypting-key', UmbralPrivateKey.gen_key().get_pubkey().hex(),
'--nickname', 'hans')
assert len(os.listdir(Card.CARD_DIR)) == 3
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert 'Saved new card' in result.output
assert len(os.listdir(Card.CARD_DIR)) == 4
def test_show_unknown_card(click_runner, alice_nickname, alice_verifying_key):
command = ('contacts', 'show', 'idontknowwhothatis')
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert 'Unknown card nickname or ID' in result.output
def test_show_alice_card(click_runner, alice_nickname, alice_verifying_key):
command = ('contacts', 'show', alice_nickname)
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert alice_nickname in result.output
assert alice_verifying_key in result.output
def test_show_bob_card(click_runner, bob_nickname, bob_encrypting_key, bob_verifying_key):
command = ('contacts', 'show', bob_nickname)
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert bob_nickname in result.output
assert bob_encrypting_key in result.output
assert bob_verifying_key in result.output
def test_list_card(click_runner, bob_nickname, bob_encrypting_key,
bob_verifying_key, alice_nickname, alice_verifying_key):
command = ('contacts', 'list')
assert len(os.listdir(Card.CARD_DIR)) == 4
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert bob_nickname in result.output
assert bob_encrypting_key[:Card.TRUNCATE] in result.output
assert bob_verifying_key[:Card.TRUNCATE] in result.output
assert alice_nickname in result.output
assert alice_verifying_key[:Card.TRUNCATE] in result.output
def test_delete_card(click_runner, bob_nickname):
command = ('contacts', 'delete', '--id', bob_nickname, '--force')
assert len(os.listdir(Card.CARD_DIR)) == 4
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
assert 'Deleted card' in result.output
assert len(os.listdir(Card.CARD_DIR)) == 3

View File

@ -14,14 +14,18 @@ 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 argparse
from collections import defaultdict
import pytest
import tempfile
from pathlib import Path
from nucypher.characters.control.emitters import WebEmitter
from nucypher.crypto.powers import TransactingPower
from nucypher.network.trackers import AvailabilityTracker
from nucypher.policy.identity import Card
from nucypher.utilities.logging import GlobalLoggerSettings
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
@ -188,3 +192,14 @@ def check_character_state_after_test(request):
still_tracking = [learner for learner in test_learners if hasattr(learner, 'work_tracker') and learner.work_tracker._tracking_task.running]
for tracker in still_tracking:
tracker.work_tracker.stop()
@pytest.fixture(scope='session', autouse=True)
def patch_card_directory(session_mocker):
custom_filepath = '/tmp/nucypher-test-cards-'
tmpdir = tempfile.TemporaryDirectory(prefix=custom_filepath)
tmpdir.cleanup()
session_mocker.patch.object(Card, 'CARD_DIR', return_value=Path(tmpdir.name),
new_callable=session_mocker.PropertyMock)
yield
tmpdir.cleanup()

View File

@ -47,7 +47,9 @@ def test_bob_cannot_follow_the_treasure_map_in_isolation(enacted_federated_polic
assert len(known) == 0
def test_bob_already_knows_all_nodes_in_treasure_map(enacted_federated_policy, federated_ursulas, federated_bob,
def test_bob_already_knows_all_nodes_in_treasure_map(enacted_federated_policy,
federated_ursulas,
federated_bob,
federated_alice):
# Bob knows of no Ursulas.
assert len(federated_bob.known_nodes) == 0

View File

@ -15,11 +15,13 @@ 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
import pytest
import nucypher
from nucypher.blockchain.eth.sol.__conf__ import SOLIDITY_COMPILER_VERSION
from nucypher.cli.commands.contacts import contacts, show
from nucypher.cli.commands.deploy import deploy
from nucypher.cli.main import ENTRY_POINTS, nucypher_cli
from nucypher.config.constants import USER_LOG_DIR, DEFAULT_CONFIG_ROOT
@ -93,3 +95,20 @@ def test_echo_logging_root(click_runner):
result = click_runner.invoke(nucypher_cli, version_args, catch_exceptions=False)
assert result.exit_code == 0
assert USER_LOG_DIR in result.output, 'Log path text was not produced.'
def test_contacts_help(click_runner):
command = ('contacts', '--help')
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
normalized_help_text = ' '.join(result.output.split())
assert contacts.__doc__ in normalized_help_text
def test_contacts_show_help(click_runner):
command = ('contacts', 'show', '--help')
result = click_runner.invoke(nucypher_cli, command, catch_exceptions=False)
assert result.exit_code == 0, result.output
normalized_help_text = ' '.join(result.output.split())
normalized_docstring = ' '.join(show.__doc__.split())
assert normalized_docstring in normalized_help_text

69
tests/unit/test_card.py Normal file
View File

@ -0,0 +1,69 @@
"""
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.lawful import Bob, Alice
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.policy.identity import Card
from tests.utils.middleware import MockRestMiddleware
@pytest.mark.parametrize('character_class', (Bob, Alice))
def test_character_card(character_class):
character = character_class(federated_only=True,
start_learning_now=False,
network_middleware=MockRestMiddleware())
character_card = character.get_card()
same_card = Card.from_character(character)
assert character_card == same_card
with pytest.raises(TypeError):
# only cards can be compared to other cards
_ = character_card == same_card.verifying_key
# Bob's Keys
assert character_card.verifying_key == character.public_keys(SigningPower)
assert character_card.encrypting_key == character.public_keys(DecryptingPower)
# Card Serialization
# bytes
card_bytes = bytes(character_card)
assert Card.from_bytes(card_bytes) == character_card == same_card
# hex
hex_bob = character_card.to_hex()
assert Card.from_hex(hex_bob) == character_card == same_card
# base64
base64_bob = character_card.to_base64()
assert Card.from_base64(base64_bob) == character_card == same_card
# qr code echo
character_card.to_qr_code()
# TODO: Examine system output here?
# nicknames
original_checksum = character_card.id
nickname = 'Wilson'
character_card.set_nickname(nickname)
restored = Card.from_bytes(bytes(character_card))
restored_checksum = restored.id
assert restored.nickname == nickname
assert original_checksum == restored_checksum == same_card.id