mirror of https://github.com/nucypher/nucypher.git
commit
8875075902
|
@ -0,0 +1 @@
|
|||
Introduces "Character Cards" a serializable identity abstraction and 'nucypher contacts' CLI to support.
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.')
|
|
@ -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}'
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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))
|
|
@ -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]:
|
||||
|
||||
|
|
15
setup.py
15
setup.py
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue