2017-11-03 21:50:04 +00:00
|
|
|
import asyncio
|
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
import msgpack
|
2017-11-03 21:50:04 +00:00
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
from kademlia.network import Server
|
2017-11-03 04:54:58 +00:00
|
|
|
from kademlia.utils import digest
|
2017-10-19 20:13:41 +00:00
|
|
|
from nkms.crypto import api as API
|
2017-11-03 04:54:58 +00:00
|
|
|
from nkms.crypto.api import secure_random
|
2017-10-13 02:11:07 +00:00
|
|
|
from nkms.crypto.constants import NOT_SIGNED, NO_DECRYPTION_PERFORMED
|
2017-10-17 21:01:25 +00:00
|
|
|
from nkms.crypto.powers import CryptoPower, SigningPower, EncryptingPower
|
2017-11-06 04:30:34 +00:00
|
|
|
from nkms.crypto.utils import verify
|
2017-11-03 21:50:04 +00:00
|
|
|
from nkms.network import blockchain_client
|
2017-11-03 04:54:58 +00:00
|
|
|
from nkms.network.blockchain_client import list_all_ursulas
|
2017-09-28 00:07:36 +00:00
|
|
|
from nkms.network.server import NuCypherDHTServer, NuCypherSeedOnlyDHTServer
|
2017-10-28 04:32:32 +00:00
|
|
|
from nkms.policy.constants import NOT_FROM_ALICE
|
|
|
|
|
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
class Character(object):
|
|
|
|
"""
|
|
|
|
A base-class for any character in our cryptography protocol narrative.
|
|
|
|
"""
|
|
|
|
_server = None
|
|
|
|
_server_class = Server
|
2017-10-07 02:29:07 +00:00
|
|
|
_default_crypto_powerups = None
|
2017-11-03 04:54:58 +00:00
|
|
|
_seal = None
|
2017-09-28 00:07:36 +00:00
|
|
|
|
2017-10-09 21:03:16 +00:00
|
|
|
class NotFound(KeyError):
|
2017-10-06 00:26:41 +00:00
|
|
|
"""raised when we try to interact with an actor of whom we haven't learned yet."""
|
|
|
|
|
2017-10-07 02:29:07 +00:00
|
|
|
def __init__(self, attach_server=True, crypto_power: CryptoPower = None,
|
2017-11-07 20:51:30 +00:00
|
|
|
crypto_power_ups=[], is_me=True):
|
2017-10-07 02:29:07 +00:00
|
|
|
"""
|
|
|
|
: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.
|
|
|
|
"""
|
2017-11-07 20:51:30 +00:00
|
|
|
if is_me:
|
|
|
|
self._actor_mapping = {}
|
|
|
|
if crypto_power and crypto_power_ups:
|
|
|
|
raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.")
|
|
|
|
|
|
|
|
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)
|
2017-10-07 02:29:07 +00:00
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
self._seal = Seal(self)
|
|
|
|
else:
|
|
|
|
assert False
|
2017-11-03 04:54:58 +00:00
|
|
|
|
|
|
|
if attach_server:
|
|
|
|
self.attach_server()
|
2017-09-28 00:07:36 +00:00
|
|
|
|
|
|
|
def attach_server(self, ksize=20, alpha=3, id=None, storage=None,
|
|
|
|
*args, **kwargs) -> None:
|
|
|
|
self._server = self._server_class(ksize, alpha, id, storage, *args, **kwargs)
|
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
@property
|
|
|
|
def seal(self):
|
|
|
|
if not self._seal:
|
|
|
|
raise AttributeError("Seal has not been set up yet.")
|
|
|
|
else:
|
|
|
|
return self._seal
|
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
@property
|
|
|
|
def server(self) -> Server:
|
|
|
|
if self._server:
|
|
|
|
return self._server
|
|
|
|
else:
|
|
|
|
raise RuntimeError("Server hasn't been attached.")
|
|
|
|
|
2017-10-14 22:07:10 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
return self.__class__.__name__
|
|
|
|
|
2017-10-07 02:29:07 +00:00
|
|
|
def learn_about_actor(self, actor):
|
|
|
|
self._actor_mapping[actor.id()] = actor
|
2017-10-05 19:20:53 +00:00
|
|
|
|
|
|
|
def encrypt_for(self, recipient: str, cleartext: bytes, sign: bool = True,
|
2017-10-13 21:06:33 +00:00
|
|
|
sign_cleartext=True) -> tuple:
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
|
|
|
Looks up recipient actor, finds that actor's pubkey_enc on our keyring, and encrypts for them.
|
|
|
|
Optionally signs the message as well.
|
|
|
|
|
|
|
|
:param recipient: The character whose public key will be used to encrypt cleartext.
|
2017-10-06 00:26:41 +00:00
|
|
|
:param cleartext: The secret to be encrypted.
|
2017-10-05 19:20:53 +00:00
|
|
|
:param sign: Whether or not to sign the message.
|
|
|
|
:param sign_cleartext: 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.
|
|
|
|
"""
|
|
|
|
actor = self._lookup_actor(recipient)
|
2017-10-13 02:11:07 +00:00
|
|
|
|
2017-10-17 04:45:43 +00:00
|
|
|
ciphertext = self._crypto_power.encrypt_for(actor.public_key(EncryptingPower),
|
|
|
|
cleartext)
|
2017-10-05 19:20:53 +00:00
|
|
|
if sign:
|
|
|
|
if sign_cleartext:
|
2017-10-11 05:39:25 +00:00
|
|
|
signature = self.seal(cleartext)
|
2017-10-05 19:20:53 +00:00
|
|
|
else:
|
2017-10-11 05:39:25 +00:00
|
|
|
signature = self.seal(ciphertext)
|
2017-10-05 19:20:53 +00:00
|
|
|
else:
|
|
|
|
signature = NOT_SIGNED
|
|
|
|
|
|
|
|
return ciphertext, signature
|
|
|
|
|
2017-10-13 04:31:03 +00:00
|
|
|
def verify_from(self, actor_whom_sender_claims_to_be: "Character", signature: bytes,
|
2017-10-17 03:13:38 +00:00
|
|
|
message: bytes, decrypt=False,
|
2017-10-13 21:06:33 +00:00
|
|
|
signature_is_on_cleartext=False) -> tuple:
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
|
|
|
Inverse of encrypt_for.
|
|
|
|
|
2017-10-13 04:31:03 +00:00
|
|
|
: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)
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
2017-10-13 02:11:07 +00:00
|
|
|
cleartext = NO_DECRYPTION_PERFORMED
|
|
|
|
if signature_is_on_cleartext:
|
|
|
|
if decrypt:
|
2017-10-17 03:13:38 +00:00
|
|
|
cleartext = self._crypto_power.decrypt(message)
|
2017-11-06 04:30:34 +00:00
|
|
|
message = cleartext
|
2017-10-13 02:11:07 +00:00
|
|
|
else:
|
|
|
|
raise ValueError(
|
|
|
|
"Can't look for a signature on the cleartext if we're not decrypting.")
|
|
|
|
|
2017-10-05 19:20:53 +00:00
|
|
|
actor = self._lookup_actor(actor_whom_sender_claims_to_be)
|
2017-10-13 02:11:07 +00:00
|
|
|
|
2017-11-06 04:30:34 +00:00
|
|
|
return verify(signature, message, actor.seal), cleartext
|
2017-10-05 19:20:53 +00:00
|
|
|
|
2017-10-07 02:29:07 +00:00
|
|
|
def _lookup_actor(self, actor: "Character"):
|
2017-10-05 19:20:53 +00:00
|
|
|
try:
|
2017-10-07 02:29:07 +00:00
|
|
|
return self._actor_mapping[actor.id()]
|
2017-10-06 00:26:41 +00:00
|
|
|
except KeyError:
|
2017-10-09 21:03:16 +00:00
|
|
|
raise self.NotFound("We haven't learned of an actor with ID {}".format(actor.id()))
|
2017-10-07 02:29:07 +00:00
|
|
|
|
|
|
|
def id(self):
|
|
|
|
return "whatever actor id ends up being - {}".format(id(self))
|
2017-10-05 19:20:53 +00:00
|
|
|
|
2017-10-17 04:45:43 +00:00
|
|
|
def public_key(self, key_class):
|
2017-10-17 03:13:38 +00:00
|
|
|
try:
|
2017-10-17 04:45:43 +00:00
|
|
|
return self._crypto_power.public_keys[key_class]
|
2017-10-17 03:13:38 +00:00
|
|
|
except KeyError:
|
2017-10-17 04:45:43 +00:00
|
|
|
raise # TODO: Does it make sense to have a specialized exception here? Probably.
|
2017-10-17 03:13:38 +00:00
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
|
|
|
|
class Alice(Character):
|
|
|
|
_server_class = NuCypherSeedOnlyDHTServer
|
2017-10-17 21:01:25 +00:00
|
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
2017-09-28 00:07:36 +00:00
|
|
|
|
|
|
|
def find_best_ursula(self):
|
2017-11-03 04:54:58 +00:00
|
|
|
# TODO: This just finds *some* Ursula - let's have it find a particularly good one.
|
2017-11-03 21:50:04 +00:00
|
|
|
return list_all_ursulas()[1]
|
2017-10-06 00:26:41 +00:00
|
|
|
|
2017-10-24 00:28:10 +00:00
|
|
|
def generate_rekey_frags(self, alice_privkey, bob, m, n):
|
2017-10-19 20:13:41 +00:00
|
|
|
"""
|
|
|
|
Generates re-encryption key frags and returns the frags and encrypted
|
|
|
|
ephemeral key data.
|
|
|
|
|
|
|
|
:param alice_privkey: Alice's private key
|
|
|
|
:param bob_pubkey: Bob's public key
|
|
|
|
:param m: Minimum number of rekey shares needed to rebuild ciphertext
|
|
|
|
:param n: Total number of rekey shares to generate
|
|
|
|
|
|
|
|
:return: Tuple(kfrags, eph_key_data)
|
|
|
|
"""
|
|
|
|
kfrags, eph_key_data = API.ecies_ephemeral_split_rekey(
|
2017-11-03 21:50:04 +00:00
|
|
|
alice_privkey, bytes(bob.seal), m, n)
|
2017-10-19 20:13:41 +00:00
|
|
|
return (kfrags, eph_key_data)
|
2017-10-12 22:30:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Bob(Character):
|
2017-10-28 04:32:32 +00:00
|
|
|
_server_class = NuCypherSeedOnlyDHTServer
|
2017-10-17 21:01:25 +00:00
|
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
2017-10-12 22:30:17 +00:00
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
def __init__(self, alice=None):
|
|
|
|
super().__init__()
|
|
|
|
if alice:
|
|
|
|
self.alice = alice
|
|
|
|
|
|
|
|
@property
|
|
|
|
def alice(self):
|
|
|
|
if not self._alice:
|
|
|
|
raise Alice.NotFound
|
|
|
|
else:
|
|
|
|
return self._alice
|
2017-10-28 02:26:23 +00:00
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
@alice.setter
|
|
|
|
def alice(self, alice_object):
|
|
|
|
self.learn_about_actor(alice_object)
|
|
|
|
self._alice = alice_object
|
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
def follow_treasure_map(self, treasure_map):
|
|
|
|
# TODO: perform this part concurrently.
|
|
|
|
for ursula_interface_id in treasure_map:
|
|
|
|
getter = self.server.get(ursula_interface_id)
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
value = loop.run_until_complete(getter)
|
|
|
|
signature, ursula_pubkey_sig, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
|
|
|
port, interface = msgpack.loads(interface_info)
|
|
|
|
self._ursulas[ursula_interface_id] = Ursula.as_discovered_on_network(port=port, interface=interface,
|
|
|
|
pubkey_sig_bytes=ursula_pubkey_sig)
|
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
def get_treasure_map(self, policy_group, signature):
|
|
|
|
ursula_coro = self.server.get(policy_group.id)
|
2017-10-28 02:26:23 +00:00
|
|
|
event_loop = asyncio.get_event_loop()
|
2017-10-28 17:53:56 +00:00
|
|
|
packed_encrypted_treasure_map = event_loop.run_until_complete(ursula_coro)
|
|
|
|
encrypted_treasure_map = msgpack.loads(packed_encrypted_treasure_map)
|
2017-10-28 04:32:32 +00:00
|
|
|
verified, packed_node_list = self.verify_from(self.alice, signature, encrypted_treasure_map,
|
2017-11-03 21:50:04 +00:00
|
|
|
signature_is_on_cleartext=True, decrypt=True)
|
2017-10-28 04:32:32 +00:00
|
|
|
if not verified:
|
|
|
|
return NOT_FROM_ALICE
|
|
|
|
else:
|
|
|
|
from nkms.policy.models import TreasureMap
|
|
|
|
return TreasureMap(msgpack.loads(packed_node_list))
|
2017-10-12 22:30:17 +00:00
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
|
2017-10-12 22:30:17 +00:00
|
|
|
class Ursula(Character):
|
|
|
|
_server_class = NuCypherDHTServer
|
2017-10-17 21:01:25 +00:00
|
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
2017-10-27 01:39:13 +00:00
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
port = None
|
|
|
|
interface = None
|
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
@staticmethod
|
|
|
|
def as_discovered_on_network(port, interface, pubkey_sig_bytes):
|
|
|
|
# CryptoPower([SigningPower(keypair=Signing)])
|
|
|
|
ursula = Ursula()
|
|
|
|
ursula.port = port
|
|
|
|
ursula.interface = interface
|
|
|
|
ursula.seal = pubkey_sig_bytes # TODO: This probably needs to be a legit Seal object, with proper exceptions if used improperly.
|
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
def ip_dht_key(self):
|
2017-11-06 03:04:04 +00:00
|
|
|
return bytes(self.seal)
|
2017-11-03 04:54:58 +00:00
|
|
|
|
|
|
|
def attach_server(self, ksize=20, alpha=3, id=None, storage=None,
|
|
|
|
*args, **kwargs):
|
|
|
|
|
|
|
|
if not id:
|
|
|
|
id = digest(secure_random(32)) # TODO: Network-wide deterministic ID generation (ie, auction or whatever)
|
|
|
|
|
|
|
|
super().attach_server(ksize, alpha, id, storage)
|
|
|
|
|
|
|
|
def listen(self, port, interface):
|
|
|
|
self.port = port
|
|
|
|
self.interface = interface
|
|
|
|
return self.server.listen(port, interface)
|
|
|
|
|
|
|
|
def publish_interface_information(self):
|
|
|
|
if not self.port and self.interface:
|
|
|
|
raise RuntimeError("Must listen before publishing interface information.")
|
2017-11-03 21:50:04 +00:00
|
|
|
ip_dht_key = self.ip_dht_key()
|
2017-11-06 03:04:04 +00:00
|
|
|
|
|
|
|
interface_info = msgpack.dumps((self.port, self.interface))
|
|
|
|
signature = self.seal(interface_info)
|
|
|
|
|
|
|
|
value = b"uaddr-" + msgpack.dumps([signature, bytes(self.seal), interface_info])
|
|
|
|
setter = self.server.set(key=ip_dht_key, value=value)
|
2017-11-03 21:50:04 +00:00
|
|
|
blockchain_client._ursulas_on_blockchain.append(ip_dht_key)
|
2017-11-03 04:54:58 +00:00
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
loop.run_until_complete(setter)
|
|
|
|
|
2017-10-27 01:39:13 +00:00
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
class Seal(object):
|
|
|
|
"""
|
|
|
|
Can be called to sign something or used to express the signing public key as bytes.
|
|
|
|
"""
|
|
|
|
def __init__(self, character):
|
|
|
|
self.character = character
|
|
|
|
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
return self.character._crypto_power.sign(*args, **kwargs)
|
|
|
|
|
|
|
|
def _as_tuple(self):
|
|
|
|
return self.character._crypto_power.pubkey_sig_tuple()
|
|
|
|
|
|
|
|
def __iter__(seal):
|
|
|
|
yield from seal._as_tuple()
|
|
|
|
|
|
|
|
def __bytes__(self):
|
|
|
|
return self.character._crypto_power.pubkey_sig_bytes()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return other == self._as_tuple() or other == bytes(self)
|
|
|
|
|
|
|
|
#
|
|
|
|
# class StrangerSeal(object):
|
|
|
|
#
|
|
|
|
# """
|
|
|
|
# Seal of a stranger (ie, can only be used to glean public key, not to sign)
|
|
|
|
# """
|
|
|
|
# def __init__(self, pubkey_bytes: bytes):
|
|
|
|
# self.pubkey_bytes = pubkey_bytes
|
|
|
|
#
|
|
|
|
# def _as_tuple(self):
|
|
|
|
# return self.character._crypto_power.pubkey_sig_tuple()
|
|
|
|
#
|
|
|
|
# def __iter__(seal):
|
|
|
|
# yield from seal._as_tuple()
|
|
|
|
#
|
|
|
|
# def __bytes__(self):
|
|
|
|
# return self.character._crypto_power.pubkey_sig_bytes()
|
|
|
|
#
|
|
|
|
# def __eq__(self, other):
|
|
|
|
# return other == self._as_tuple() or other == bytes(self)
|
|
|
|
|
|
|
|
def congregate(*characters):
|
2017-10-27 01:39:13 +00:00
|
|
|
for character in characters:
|
|
|
|
for newcomer in characters:
|
2017-11-03 04:54:58 +00:00
|
|
|
character.learn_about_actor(newcomer)
|