mirror of https://github.com/nucypher/nucypher.git
688 lines
29 KiB
Python
688 lines
29 KiB
Python
import asyncio
|
|
from contextlib import suppress
|
|
from logging import getLogger
|
|
import msgpack
|
|
from collections import OrderedDict
|
|
from kademlia.network import Server
|
|
from kademlia.utils import digest
|
|
from typing import Dict, ClassVar
|
|
from typing import Union, List
|
|
|
|
from bytestring_splitter import BytestringSplitter
|
|
from nkms.network.node import NetworkyStuff
|
|
from umbral.keys import UmbralPublicKey
|
|
from constant_sorrow import constants, default_constant_splitter
|
|
|
|
from nkms.blockchain.eth.actors import PolicyAuthor
|
|
from nkms.config.configs import KMSConfig
|
|
from nkms.crypto.api import secure_random, keccak_digest, encrypt_and_sign
|
|
from nkms.crypto.constants import PUBLIC_KEY_LENGTH
|
|
from nkms.crypto.kits import UmbralMessageKit
|
|
from nkms.crypto.powers import CryptoPower, SigningPower, EncryptingPower, DelegatingPower, NoSigningPower
|
|
from nkms.crypto.signature import Signature, signature_splitter, SignatureStamp, StrangerStamp
|
|
from nkms.network import blockchain_client
|
|
from nkms.network.protocols import dht_value_splitter, dht_with_hrac_splitter
|
|
from nkms.network.server import NuCypherDHTServer, NuCypherSeedOnlyDHTServer, ProxyRESTServer
|
|
|
|
|
|
class Character(object):
|
|
"""
|
|
A base-class for any character in our cryptography protocol narrative.
|
|
"""
|
|
_server = None
|
|
_server_class = Server
|
|
_default_crypto_powerups = None
|
|
_stamp = None
|
|
|
|
address = "This is a fake address." # TODO: #192
|
|
|
|
def __init__(self, attach_server=True, crypto_power: CryptoPower=None,
|
|
crypto_power_ups=None, is_me=True, network_middleware=None,
|
|
config: "KMSConfig"=None) -> None:
|
|
"""
|
|
:param attach_server: Whether to attach a Server when this Character is
|
|
born.
|
|
:param crypto_power: A CryptoPower object; if provided, this will be the
|
|
character's CryptoPower.
|
|
:param crypto_power_ups: If crypto_power is not provided, a new
|
|
CryptoPower will be made and will consume all of the CryptoPowerUps
|
|
in this list.
|
|
|
|
If neither crypto_power nor crypto_power_ups are provided, we give this
|
|
Character all CryptoPowerUps listed in their _default_crypto_powerups
|
|
attribute.
|
|
|
|
:param is_me: Set this to True when you want this Character to represent
|
|
the owner of the configuration under which the program is being run.
|
|
A Character who is_me can do things that other Characters can't,
|
|
like run servers, sign messages, and decrypt messages which are
|
|
encrypted for them. Typically this will be True for exactly one
|
|
Character, but there are scenarios in which its imaginable to be
|
|
represented by zero Characters or by more than one Character.
|
|
"""
|
|
# self.config = config if config is not None else KMSConfig.get_config()
|
|
self.known_nodes = {}
|
|
self.log = getLogger("characters")
|
|
|
|
if crypto_power and crypto_power_ups:
|
|
raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.")
|
|
|
|
if not crypto_power_ups:
|
|
crypto_power_ups = []
|
|
|
|
if crypto_power:
|
|
self._crypto_power = crypto_power
|
|
elif crypto_power_ups:
|
|
self._crypto_power = CryptoPower(power_ups=crypto_power_ups)
|
|
else:
|
|
self._crypto_power = CryptoPower(self._default_crypto_powerups)
|
|
if is_me:
|
|
self.network_middleware = network_middleware or NetworkyStuff()
|
|
try:
|
|
self._stamp = SignatureStamp(self._crypto_power.power_ups(SigningPower).keypair)
|
|
except NoSigningPower:
|
|
self._stamp = constants.NO_SIGNING_POWER
|
|
|
|
if attach_server:
|
|
self.attach_server()
|
|
else:
|
|
if network_middleware is not None:
|
|
raise TypeError("Can't attach network middleware to a Character who isn't me. What are you even trying to do?")
|
|
self._stamp = StrangerStamp(self._crypto_power.power_ups(SigningPower).keypair)
|
|
|
|
def __eq__(self, other):
|
|
return bytes(self.stamp) == bytes(other.stamp)
|
|
|
|
def __hash__(self):
|
|
return int.from_bytes(self.stamp, byteorder="big")
|
|
|
|
class NotEnoughUrsulas(RuntimeError):
|
|
"""
|
|
All Characters depend on knowing about enough Ursulas to perform their role.
|
|
This exception is raised when a piece of logic can't proceed without more Ursulas.
|
|
"""
|
|
|
|
class SuspiciousActivity(RuntimeError):
|
|
"""raised when an action appears to amount to malicious conduct."""
|
|
|
|
@classmethod
|
|
def from_public_keys(cls, powers_and_keys: Dict, *args, **kwargs):
|
|
"""
|
|
Sometimes we discover a Character and, at the same moment, learn one or
|
|
more of their public keys. Here, we take a Dict
|
|
(powers_and_key_bytes) in the following format:
|
|
{CryptoPowerUp class: public_key_bytes}
|
|
|
|
Each item in the collection will have the CryptoPowerUp instantiated
|
|
with the public_key_bytes, and the resulting CryptoPowerUp instance
|
|
consumed by the Character.
|
|
"""
|
|
crypto_power = CryptoPower()
|
|
|
|
for power_up, public_key in powers_and_keys.items():
|
|
try:
|
|
umbral_key = UmbralPublicKey(public_key)
|
|
except TypeError:
|
|
umbral_key = public_key
|
|
|
|
crypto_power.consume_power_up(power_up(pubkey=umbral_key))
|
|
|
|
return cls(is_me=False, crypto_power=crypto_power, *args, **kwargs)
|
|
|
|
def attach_server(self, ksize=20, alpha=3, id=None,
|
|
storage=None, *args, **kwargs) -> None:
|
|
if self._server:
|
|
raise RuntimeError("Attaching the server twice is almost certainly a bad idea.")
|
|
self._server = self._server_class(
|
|
ksize, alpha, id, storage, *args, **kwargs)
|
|
|
|
@property
|
|
def stamp(self):
|
|
if self._stamp is constants.NO_SIGNING_POWER:
|
|
raise NoSigningPower
|
|
elif not self._stamp:
|
|
raise AttributeError("SignatureStamp has not been set up yet.")
|
|
else:
|
|
return self._stamp
|
|
|
|
@property
|
|
def server(self) -> Server:
|
|
if self._server:
|
|
return self._server
|
|
else:
|
|
raise RuntimeError("Server hasn't been attached.")
|
|
|
|
@property
|
|
def name(self):
|
|
return self.__class__.__name__
|
|
|
|
def encrypt_for(self,
|
|
recipient: "Character",
|
|
plaintext: bytes,
|
|
sign: bool=True,
|
|
sign_plaintext=True,
|
|
) -> tuple:
|
|
"""
|
|
Encrypts plaintext for recipient actor. Optionally signs the message as well.
|
|
|
|
:param recipient: The character whose public key will be used to encrypt
|
|
cleartext.
|
|
:param plaintext: The secret to be encrypted.
|
|
:param sign: Whether or not to sign the message.
|
|
:param sign_plaintext: When signing, the cleartext is signed if this is
|
|
True, Otherwise, the resulting ciphertext is signed.
|
|
|
|
:return: A tuple, (ciphertext, signature). If sign==False,
|
|
then signature will be NOT_SIGNED.
|
|
"""
|
|
signer = self.stamp if sign else constants.DO_NOT_SIGN
|
|
|
|
message_kit, signature = encrypt_and_sign(recipient_pubkey_enc=recipient.public_key(EncryptingPower),
|
|
plaintext=plaintext,
|
|
signer=signer,
|
|
sign_plaintext=sign_plaintext
|
|
)
|
|
return message_kit, signature
|
|
|
|
def verify_from(self,
|
|
actor_whom_sender_claims_to_be: "Character",
|
|
message_kit: Union[UmbralMessageKit, bytes],
|
|
signature: Signature=None,
|
|
decrypt=False,
|
|
) -> tuple:
|
|
"""
|
|
Inverse of encrypt_for.
|
|
|
|
:param actor_that_sender_claims_to_be: A Character instance representing
|
|
the actor whom the sender claims to be. We check the public key
|
|
owned by this Character instance to verify.
|
|
:param messages: The messages to be verified.
|
|
:param decrypt: Whether or not to decrypt the messages.
|
|
:param signature_is_on_cleartext: True if we expect the signature to be
|
|
on the cleartext. Otherwise, we presume that the ciphertext is what
|
|
is signed.
|
|
:return: Whether or not the signature is valid, the decrypted plaintext
|
|
or NO_DECRYPTION_PERFORMED
|
|
"""
|
|
sender_pubkey_sig = actor_whom_sender_claims_to_be.stamp.as_umbral_pubkey()
|
|
with suppress(AttributeError):
|
|
if message_kit.sender_pubkey_sig:
|
|
if not message_kit.sender_pubkey_sig == sender_pubkey_sig:
|
|
raise ValueError(
|
|
"This MessageKit doesn't appear to have come from {}".format(actor_whom_sender_claims_to_be))
|
|
|
|
signature_from_kit = None
|
|
|
|
if decrypt:
|
|
# We are decrypting the message; let's do that first and see what the sig header says.
|
|
cleartext_with_sig_header = self.decrypt(message_kit)
|
|
sig_header, cleartext = default_constant_splitter(cleartext_with_sig_header, return_remainder=True)
|
|
if sig_header == constants.SIGNATURE_IS_ON_CIPHERTEXT:
|
|
# THe ciphertext is what is signed - note that for later.
|
|
message = message_kit.ciphertext
|
|
if not signature:
|
|
raise ValueError("Can't check a signature on the ciphertext if don't provide one.")
|
|
elif sig_header == constants.SIGNATURE_TO_FOLLOW:
|
|
# The signature follows in this cleartext - split it off.
|
|
signature_from_kit, cleartext = signature_splitter(cleartext,
|
|
return_remainder=True)
|
|
message = cleartext
|
|
else:
|
|
# Not decrypting - the message is the object passed in as a message kit. Cast it.
|
|
message = bytes(message_kit)
|
|
cleartext = constants.NO_DECRYPTION_PERFORMED
|
|
|
|
if signature and signature_from_kit:
|
|
if signature != signature_from_kit:
|
|
raise ValueError(
|
|
"The MessageKit has a Signature, but it's not the same one you provided. Something's up.")
|
|
|
|
signature_to_use = signature or signature_from_kit
|
|
|
|
if signature_to_use:
|
|
is_valid = signature_to_use.verify(message, sender_pubkey_sig)
|
|
else:
|
|
# Meh, we didn't even get a signature. Not much we can do.
|
|
is_valid = False
|
|
|
|
return is_valid, cleartext
|
|
|
|
"""
|
|
Next we have decrypt(), sign(), and generate_self_signed_certificate() - these use the private
|
|
keys of their respective powers; any character who has these powers can use these functions.
|
|
|
|
If they don't have the correct Power, the appropriate PowerUpError is raised.
|
|
"""
|
|
|
|
def decrypt(self, message_kit):
|
|
return self._crypto_power.power_ups(EncryptingPower).decrypt(message_kit)
|
|
|
|
def sign(self, message):
|
|
return self._crypto_power.power_ups(SigningPower).sign(message)
|
|
|
|
def generate_self_signed_certificate(self):
|
|
signing_power = self._crypto_power.power_ups(SigningPower)
|
|
return signing_power.generate_self_signed_cert(self.stamp.fingerprint().decode())
|
|
|
|
"""
|
|
And finally, some miscellaneous but generally-applicable abilities:
|
|
"""
|
|
|
|
def public_key(self, power_up_class: ClassVar):
|
|
"""
|
|
Pass a power_up_class, get the public key for this Character which corresponds to that
|
|
class.
|
|
|
|
If the Character doesn't have the power corresponding to that class, raises the
|
|
appropriate PowerUpError (ie, NoSigningPower or NoEncryptingPower).
|
|
"""
|
|
power_up = self._crypto_power.power_ups(power_up_class)
|
|
return power_up.public_key()
|
|
|
|
def learn_about_nodes(self, address, port):
|
|
"""
|
|
Sends a request to node_url to find out about known nodes.
|
|
"""
|
|
response = self.network_middleware.get_nodes_via_rest(address, port)
|
|
signature, nodes = signature_splitter(response.content, return_remainder=True)
|
|
# TODO: Although not treasure map-related, this has a whiff of #172.
|
|
ursula_interface_splitter = dht_value_splitter + BytestringSplitter((bytes, 17))
|
|
split_nodes = ursula_interface_splitter.repeat(nodes)
|
|
new_nodes = {}
|
|
for node_meta in split_nodes:
|
|
header, sig, pubkey, interface_info = node_meta
|
|
if not pubkey in self.known_nodes:
|
|
if sig.verify(keccak_digest(interface_info), pubkey):
|
|
address, dht_port, rest_port = msgpack.loads(interface_info)
|
|
new_nodes[pubkey] = \
|
|
Ursula.as_discovered_on_network(
|
|
rest_port=rest_port,
|
|
dht_port=dht_port,
|
|
ip_address=address.decode("utf-8"),
|
|
powers_and_keys=({SigningPower: pubkey})
|
|
)
|
|
else:
|
|
message = "Suspicious Activity: Discovered node with bad signature: {}. Propagated by: {}:{}".format(node_meta, address, port)
|
|
self.log.warn(message)
|
|
return new_nodes
|
|
|
|
def network_bootstrap(self, node_list):
|
|
for node_addr, port in node_list:
|
|
new_nodes = self.learn_about_nodes(node_addr, port)
|
|
self.known_nodes.update(new_nodes)
|
|
|
|
|
|
class FakePolicyAgent: # TODO: #192
|
|
_token = "fake token"
|
|
|
|
|
|
class Alice(Character, PolicyAuthor):
|
|
_server_class = NuCypherSeedOnlyDHTServer
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower, DelegatingPower]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
PolicyAuthor.__init__(self, self.address, policy_agent=FakePolicyAgent())
|
|
|
|
def generate_kfrags(self, bob, label, m, n) -> List:
|
|
"""
|
|
Generates re-encryption key frags ("KFrags") and returns them.
|
|
|
|
These KFrags can be used by Ursula to re-encrypt a Capsule for Bob so
|
|
that he can activate the Capsule.
|
|
:param bob: Bob instance which will be able to decrypt messages re-encrypted with these kfrags.
|
|
:param m: Minimum number of kfrags needed to activate a Capsule.
|
|
:param n: Total number of kfrags to generate
|
|
"""
|
|
bob_pubkey_enc = bob.public_key(EncryptingPower)
|
|
return self._crypto_power.power_ups(DelegatingPower).generate_kfrags(bob_pubkey_enc, label, m, n)
|
|
|
|
def create_policy(self, bob: "Bob", label: bytes, m: int, n: int):
|
|
"""
|
|
Create a Policy to share uri with bob.
|
|
Generates KFrags and attaches them.
|
|
"""
|
|
public_key, kfrags = self.generate_kfrags(bob, label, m, n)
|
|
from nkms.policy.models import Policy
|
|
policy = Policy.from_alice(
|
|
alice=self,
|
|
label=label,
|
|
bob=bob,
|
|
kfrags=kfrags,
|
|
public_key=public_key,
|
|
m=m,
|
|
)
|
|
|
|
return policy
|
|
|
|
def grant(self, bob, uri, m=None, n=None, expiration=None, deposit=None):
|
|
if not m:
|
|
# TODO: get m from config #176
|
|
raise NotImplementedError
|
|
if not n:
|
|
# TODO: get n from config #176
|
|
raise NotImplementedError
|
|
if not expiration:
|
|
# TODO: check default duration in config #176
|
|
raise NotImplementedError
|
|
if not deposit:
|
|
default_deposit = None # TODO: Check default deposit in config. #176
|
|
if not default_deposit:
|
|
deposit = self.network_middleware.get_competitive_rate()
|
|
if deposit == NotImplemented:
|
|
deposit = constants.NON_PAYMENT(b"0000000")
|
|
|
|
policy = self.create_policy(bob, uri, m, n)
|
|
|
|
# We'll find n Ursulas by default. It's possible to "play the field"
|
|
# by trying differet
|
|
# deposits and expirations on a limited number of Ursulas.
|
|
# Users may decide to inject some market strategies here.
|
|
found_ursulas = policy.find_ursulas(self.network_middleware, deposit,
|
|
expiration, num_ursulas=n)
|
|
policy.match_kfrags_to_found_ursulas(found_ursulas)
|
|
# REST call happens here, as does population of TreasureMap.
|
|
policy.enact(self.network_middleware)
|
|
policy.publish_treasure_map(self.network_middleware)
|
|
|
|
return policy # Now with TreasureMap affixed!
|
|
|
|
|
|
class Bob(Character):
|
|
_server_class = NuCypherSeedOnlyDHTServer
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.treasure_maps = {}
|
|
|
|
from nkms.policy.models import WorkOrderHistory # Need a bigger strategy to avoid circulars.
|
|
self._saved_work_orders = WorkOrderHistory()
|
|
|
|
def follow_treasure_map(self, hrac, using_dht=False):
|
|
|
|
treasure_map = self.treasure_maps[hrac]
|
|
number_of_known_treasure_ursulas = 0
|
|
if not using_dht:
|
|
for ursula_interface_id in treasure_map:
|
|
pubkey = UmbralPublicKey.from_bytes(ursula_interface_id)
|
|
if pubkey in self.known_nodes:
|
|
number_of_known_treasure_ursulas += 1
|
|
|
|
newly_discovered_nodes = {}
|
|
nodes_to_check = iter(self.known_nodes.values())
|
|
|
|
while number_of_known_treasure_ursulas < treasure_map.m:
|
|
try:
|
|
node_to_check = next(nodes_to_check)
|
|
except StopIteration:
|
|
raise self.NotEnoughUrsulas(
|
|
"Unable to follow the TreasureMap; we just don't know enough nodes to ask about this. Maybe try using the DHT instead.")
|
|
|
|
new_nodes = self.learn_about_nodes(node_to_check.ip_address,
|
|
node_to_check.rest_port)
|
|
for new_node_pubkey in new_nodes.keys():
|
|
if new_node_pubkey in treasure_map:
|
|
number_of_known_treasure_ursulas += 1
|
|
newly_discovered_nodes.update(new_nodes)
|
|
|
|
self.known_nodes.update(newly_discovered_nodes)
|
|
return newly_discovered_nodes, number_of_known_treasure_ursulas
|
|
else:
|
|
for ursula_interface_id in self.treasure_maps[hrac]:
|
|
pubkey = UmbralPublicKey.from_bytes(ursula_interface_id)
|
|
if ursula_interface_id in self.known_nodes:
|
|
# If we already know about this Ursula,
|
|
# we needn't learn about it again.
|
|
continue
|
|
|
|
if using_dht:
|
|
# TODO: perform this part concurrently.
|
|
value = self.server.get_now(ursula_interface_id)
|
|
|
|
# TODO: Make this much prettier
|
|
header, signature, ursula_pubkey_sig, _hrac, (
|
|
port, interface, ttl) = dht_value_splitter(value, msgpack_remainder=True)
|
|
|
|
if header != constants.BYTESTRING_IS_URSULA_IFACE_INFO:
|
|
raise TypeError("Unknown DHT value. How did this get on the network?")
|
|
|
|
# TODO: If we're going to implement TTL, it will be here.
|
|
self.known_nodes[ursula_interface_id] = \
|
|
Ursula.as_discovered_on_network(
|
|
dht_port=port,
|
|
dht_interface=interface,
|
|
powers_and_keys=({SigningPower: ursula_pubkey_sig})
|
|
)
|
|
|
|
def get_treasure_map(self, alice_pubkey_sig, hrac, using_dht=False, verify_sig=True):
|
|
map_id = keccak_digest(alice_pubkey_sig + hrac)
|
|
|
|
if using_dht:
|
|
ursula_coro = self.server.get(map_id)
|
|
event_loop = asyncio.get_event_loop()
|
|
packed_encrypted_treasure_map = event_loop.run_until_complete(ursula_coro)
|
|
else:
|
|
if not self.known_nodes:
|
|
# TODO: Try to find more Ursulas on the blockchain.
|
|
raise self.NotEnoughUrsulas
|
|
tmap_message_kit = self.get_treasure_map_from_known_ursulas(self.network_middleware,
|
|
map_id)
|
|
|
|
if verify_sig:
|
|
alice = Alice.from_public_keys({SigningPower: alice_pubkey_sig})
|
|
verified, packed_node_list = self.verify_from(
|
|
alice, tmap_message_kit,
|
|
decrypt=True
|
|
)
|
|
else:
|
|
assert False
|
|
|
|
if not verified:
|
|
return constants.NOT_FROM_ALICE
|
|
else:
|
|
from nkms.policy.models import TreasureMap
|
|
node_list = msgpack.loads(packed_node_list)
|
|
m = node_list.pop()
|
|
treasure_map = TreasureMap(m=m, ursula_interface_ids=node_list)
|
|
self.treasure_maps[hrac] = treasure_map
|
|
return treasure_map
|
|
|
|
def get_treasure_map_from_known_ursulas(self, networky_stuff, map_id):
|
|
"""
|
|
Iterate through swarm, asking for the TreasureMap.
|
|
Return the first one who has it.
|
|
TODO: What if a node gives a bunk TreasureMap?
|
|
"""
|
|
from nkms.network.protocols import dht_value_splitter
|
|
for node in self.known_nodes.values():
|
|
response = networky_stuff.get_treasure_map_from_node(node, map_id)
|
|
|
|
if response.status_code == 200 and response.content:
|
|
# TODO: Make this prettier
|
|
header, _signature_for_ursula, pubkey_sig_alice, hrac, encrypted_treasure_map = \
|
|
dht_with_hrac_splitter(response.content, return_remainder=True)
|
|
tmap_messaage_kit = UmbralMessageKit.from_bytes(encrypted_treasure_map)
|
|
return tmap_messaage_kit
|
|
else:
|
|
assert False
|
|
|
|
def generate_work_orders(self, kfrag_hrac, *capsules, num_ursulas=None):
|
|
from nkms.policy.models import WorkOrder # Prevent circular import
|
|
|
|
try:
|
|
treasure_map_to_use = self.treasure_maps[kfrag_hrac]
|
|
except KeyError:
|
|
raise KeyError(
|
|
"Bob doesn't have a TreasureMap matching the hrac {}".format(kfrag_hrac))
|
|
|
|
generated_work_orders = OrderedDict()
|
|
|
|
if not treasure_map_to_use:
|
|
raise ValueError(
|
|
"Bob doesn't have a TreasureMap to match any of these capsules: {}".format(
|
|
capsules))
|
|
|
|
for ursula_dht_key in treasure_map_to_use:
|
|
ursula = self.known_nodes[UmbralPublicKey.from_bytes(ursula_dht_key)]
|
|
|
|
capsules_to_include = []
|
|
for capsule in capsules:
|
|
if not capsule in self._saved_work_orders[ursula_dht_key]:
|
|
capsules_to_include.append(capsule)
|
|
|
|
if capsules_to_include:
|
|
work_order = WorkOrder.construct_by_bob(
|
|
kfrag_hrac, capsules_to_include, ursula, self)
|
|
generated_work_orders[ursula_dht_key] = work_order
|
|
self._saved_work_orders[ursula_dht_key][capsule] = work_order
|
|
|
|
if num_ursulas is not None:
|
|
if num_ursulas == len(generated_work_orders):
|
|
break
|
|
|
|
return generated_work_orders
|
|
|
|
def get_reencrypted_c_frags(self, work_order):
|
|
cfrags = self.network_middleware.reencrypt(work_order)
|
|
if not len(work_order) == len(cfrags):
|
|
raise ValueError("Ursula gave back the wrong number of cfrags. She's up to something.")
|
|
for counter, capsule in enumerate(work_order.capsules):
|
|
# TODO: Ursula is actually supposed to sign this. See #141.
|
|
# TODO: Maybe just update the work order here instead of setting it anew.
|
|
work_orders_by_ursula = self._saved_work_orders[bytes(work_order.ursula.stamp)]
|
|
work_orders_by_ursula[capsule] = work_order
|
|
return cfrags
|
|
|
|
def get_ursula(self, ursula_id):
|
|
return self._ursulas[ursula_id]
|
|
|
|
def join_policy(self, label, alice_pubkey_sig,
|
|
using_dht=False, node_list=None, verify_sig=True):
|
|
hrac = keccak_digest(bytes(alice_pubkey_sig) + bytes(self.stamp) + label)
|
|
if node_list:
|
|
self.network_bootstrap(node_list)
|
|
self.get_treasure_map(alice_pubkey_sig, hrac, using_dht=using_dht, verify_sig=verify_sig)
|
|
self.follow_treasure_map(hrac, using_dht=using_dht)
|
|
|
|
def retrieve(self, message_kit, data_source, alice_pubkey_sig):
|
|
hrac = keccak_digest(bytes(alice_pubkey_sig) + self.stamp + data_source.label)
|
|
treasure_map = self.treasure_maps[hrac]
|
|
|
|
# First, a quick sanity check to make sure we know about at least m nodes.
|
|
known_nodes_as_bytes = set([bytes(n) for n in self.known_nodes.keys()])
|
|
intersection = treasure_map.ids.intersection(known_nodes_as_bytes)
|
|
|
|
if len(intersection) < treasure_map.m:
|
|
raise RuntimeError("Not enough known nodes. Try following the TreasureMap again.")
|
|
|
|
work_orders = self.generate_work_orders(hrac, message_kit.capsule)
|
|
for node_id in self.treasure_maps[hrac]:
|
|
node = self.known_nodes[UmbralPublicKey.from_bytes(node_id)]
|
|
cfrags = self.get_reencrypted_c_frags(work_orders[bytes(node.stamp)])
|
|
message_kit.capsule.attach_cfrag(cfrags[0])
|
|
verified, delivered_cleartext = self.verify_from(data_source, message_kit, decrypt=True)
|
|
|
|
if verified:
|
|
return delivered_cleartext
|
|
else:
|
|
raise RuntimeError("Not verified - replace this with real message.")
|
|
|
|
|
|
class Ursula(Character, ProxyRESTServer):
|
|
_server_class = NuCypherDHTServer
|
|
_alice_class = Alice
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
|
|
|
def __init__(self, dht_port=None, ip_address=None, dht_ttl=0,
|
|
rest_port=None, db_name=None,
|
|
*args, **kwargs):
|
|
self.dht_port = dht_port
|
|
self.ip_address = ip_address
|
|
self._work_orders = []
|
|
ProxyRESTServer.__init__(self, rest_port, db_name)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
@property
|
|
def rest_app(self):
|
|
if not self._rest_app:
|
|
raise AttributeError(
|
|
"This Ursula doesn't have a REST app attached. If you want one, init with is_me and attach_server.")
|
|
else:
|
|
return self._rest_app
|
|
|
|
@classmethod
|
|
def as_discovered_on_network(cls, dht_port=None, ip_address=None,
|
|
rest_port=None, powers_and_keys=()):
|
|
# TODO: We also need the encrypting public key here.
|
|
ursula = cls.from_public_keys(powers_and_keys)
|
|
ursula.dht_port = dht_port
|
|
ursula.ip_address = ip_address
|
|
ursula.rest_port = rest_port
|
|
return ursula
|
|
|
|
@classmethod
|
|
def from_rest_url(cls, networky_stuff, address, port):
|
|
response = networky_stuff.ursula_from_rest_interface(address, port)
|
|
if not response.status_code == 200:
|
|
raise RuntimeError("Got a bad response: {}".format(response))
|
|
|
|
key_splitter = BytestringSplitter(
|
|
(UmbralPublicKey, PUBLIC_KEY_LENGTH))
|
|
signing_key, encrypting_key = key_splitter.repeat(response.content)
|
|
|
|
stranger_ursula_from_public_keys = cls.from_public_keys(
|
|
{SigningPower: signing_key, EncryptingPower: encrypting_key},
|
|
ip_address=address,
|
|
rest_port=port
|
|
)
|
|
|
|
return stranger_ursula_from_public_keys
|
|
|
|
def attach_server(self, ksize=20, alpha=3, id=None,
|
|
storage=None, *args, **kwargs):
|
|
# TODO: Network-wide deterministic ID generation (ie, auction or
|
|
# whatever) See #136.
|
|
if not id:
|
|
id = digest(secure_random(32))
|
|
|
|
super().attach_server(ksize, alpha, id, storage)
|
|
self.attach_rest_server(db_name=self.db_name)
|
|
|
|
def dht_listen(self):
|
|
return self.server.listen(self.dht_port, self.ip_address)
|
|
|
|
def interface_information(self):
|
|
return msgpack.dumps((self.ip_address,
|
|
self.dht_port,
|
|
self.rest_port))
|
|
|
|
def interface_info_with_metadata(self):
|
|
interface_info = self.interface_information()
|
|
signature = self.stamp(keccak_digest(interface_info))
|
|
return constants.BYTESTRING_IS_URSULA_IFACE_INFO + signature + self.stamp + interface_info
|
|
|
|
def publish_dht_information(self):
|
|
if not self.dht_port and self.dht_interface:
|
|
raise RuntimeError("Must listen before publishing interface information.")
|
|
|
|
dht_key = bytes(self.stamp)
|
|
value = self.interface_info_with_metadata()
|
|
setter = self.server.set(key=dht_key, value=value)
|
|
blockchain_client._ursulas_on_blockchain.append(dht_key)
|
|
loop = asyncio.get_event_loop()
|
|
loop.run_until_complete(setter)
|
|
|
|
def work_orders(self, bob=None):
|
|
"""
|
|
TODO: This is better written as a model method for Ursula's datastore.
|
|
"""
|
|
if not bob:
|
|
return self._work_orders
|
|
else:
|
|
work_orders_from_bob = []
|
|
for work_order in self._work_orders:
|
|
if work_order.bob == bob:
|
|
work_orders_from_bob.append(work_order)
|
|
return work_orders_from_bob
|