magic-wormhole/src/wormhole/wormhole.py

309 lines
10 KiB
Python

from __future__ import absolute_import, print_function, unicode_literals
import os
import sys
from attr import attrib, attrs
from twisted.python import failure
from twisted.internet.task import Cooperator
from zope.interface import implementer
from ._boss import Boss
from ._dilation.manager import DILATION_VERSIONS
from ._dilation.connector import Connector
from ._interfaces import IDeferredWormhole, IWormhole
from ._key import derive_key
from .errors import NoKeyError, WormholeClosed
from .eventual import EventualQueue
from .journal import ImmediateJournal
from .observer import OneShotObserver, SequenceObserver
from .timing import DebugTiming
from .util import bytes_to_hexstr, to_bytes
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
# We can provide different APIs to different apps:
# * Deferreds
# w.get_code().addCallback(print_code)
# w.send_message(data)
# w.get_message().addCallback(got_data)
# w.close().addCallback(closed)
# * delegate callbacks (better for journaled environments)
# w = wormhole(delegate=app)
# w.send_message(data)
# app.wormhole_got_code(code)
# app.wormhole_got_verifier(verifier)
# app.wormhole_got_versions(versions)
# app.wormhole_got_message(data)
# w.close()
# app.wormhole_closed()
#
# * potential delegate options
# wormhole(delegate=app, delegate_prefix="wormhole_",
# delegate_args=(args, kwargs))
@attrs
@implementer(IWormhole)
class _DelegatedWormhole(object):
_delegate = attrib()
def __attrs_post_init__(self):
self._key = None
def _set_boss(self, boss):
self._boss = boss
# from above
def allocate_code(self, code_length=2):
self._boss.allocate_code(code_length)
def input_code(self):
return self._boss.input_code()
def set_code(self, code):
self._boss.set_code(code)
# def serialize(self):
# s = {"serialized_wormhole_version": 1,
# "boss": self._boss.serialize(),
# }
# return s
def send_message(self, plaintext):
self._boss.send(plaintext)
def derive_key(self, purpose, length):
"""Derive a new key from the established wormhole channel for some
other purpose. This is a deterministic randomized function of the
session key and the 'purpose' string (unicode/py3-string). This
cannot be called until when_verifier() has fired, nor after close()
was called.
"""
if not isinstance(purpose, type("")):
raise TypeError(type(purpose))
if not self._key:
raise NoKeyError()
return derive_key(self._key, to_bytes(purpose), length)
def close(self):
self._boss.close()
def debug_set_trace(self,
client_name,
which="B N M S O K SK R RC L C T",
file=sys.stderr):
self._boss._set_trace(client_name, which, file)
# from below
def got_welcome(self, welcome):
self._delegate.wormhole_got_welcome(welcome)
def got_code(self, code):
self._delegate.wormhole_got_code(code)
def got_key(self, key):
self._delegate.wormhole_got_unverified_key(key)
self._key = key # for derive_key()
def got_verifier(self, verifier):
self._delegate.wormhole_got_verifier(verifier)
def got_versions(self, versions):
self._delegate.wormhole_got_versions(versions)
def received(self, plaintext):
self._delegate.wormhole_got_message(plaintext)
def closed(self, result):
self._delegate.wormhole_closed(result)
@implementer(IWormhole, IDeferredWormhole)
class _DeferredWormhole(object):
def __init__(self, reactor, eq, _enable_dilate=False):
self._reactor = reactor
self._welcome_observer = OneShotObserver(eq)
self._code_observer = OneShotObserver(eq)
self._key = None
self._key_observer = OneShotObserver(eq)
self._verifier_observer = OneShotObserver(eq)
self._version_observer = OneShotObserver(eq)
self._received_observer = SequenceObserver(eq)
self._closed = False
self._closed_observer = OneShotObserver(eq)
self._enable_dilate = _enable_dilate
def _set_boss(self, boss):
self._boss = boss
# from above
def get_code(self):
# TODO: consider throwing error unless one of allocate/set/input_code
# was called first. It's legit to grab the Deferred before triggering
# the process that will cause it to fire, but forbidding that
# ordering would make it easier to cause programming errors that
# forget to trigger it entirely.
return self._code_observer.when_fired()
def get_welcome(self):
return self._welcome_observer.when_fired()
def get_unverified_key(self):
return self._key_observer.when_fired()
def get_verifier(self):
return self._verifier_observer.when_fired()
def get_versions(self):
return self._version_observer.when_fired()
def get_message(self):
return self._received_observer.when_next_event()
def allocate_code(self, code_length=2):
self._boss.allocate_code(code_length)
def input_code(self):
return self._boss.input_code()
def set_code(self, code):
self._boss.set_code(code)
# no .serialize in Deferred-mode
def send_message(self, plaintext):
self._boss.send(plaintext)
def derive_key(self, purpose, length):
"""Derive a new key from the established wormhole channel for some
other purpose. This is a deterministic randomized function of the
session key and the 'purpose' string (unicode/py3-string). This
cannot be called until when_verified() has fired, nor after close()
was called.
"""
if not isinstance(purpose, type("")):
raise TypeError(type(purpose))
if not self._key:
raise NoKeyError()
return derive_key(self._key, to_bytes(purpose), length)
def dilate(self, transit_relay_location=None, no_listen=False):
if not self._enable_dilate:
raise NotImplementedError
return self._boss.dilate(transit_relay_location, no_listen) # fires with (endpoints)
def close(self):
# fails with WormholeError unless we established a connection
# (state=="happy"). Fails with WrongPasswordError (a subclass of
# WormholeError) if state=="scary".
d = self._closed_observer.when_fired() # maybe Failure
if not self._closed:
self._boss.close() # only need to close if it wasn't already
return d
def debug_set_trace(self,
client_name,
which="B N M S O K SK R RC L A I C T",
file=sys.stderr):
self._boss._set_trace(client_name, which, file)
# from below
def got_welcome(self, welcome):
self._welcome_observer.fire_if_not_fired(welcome)
def got_code(self, code):
self._code_observer.fire_if_not_fired(code)
def got_key(self, key):
self._key = key # for derive_key()
self._key_observer.fire_if_not_fired(key)
def got_verifier(self, verifier):
self._verifier_observer.fire_if_not_fired(verifier)
def got_versions(self, versions):
self._version_observer.fire_if_not_fired(versions)
def received(self, plaintext):
self._received_observer.fire(plaintext)
def closed(self, result):
self._closed = True
# print("closed", result, type(result), file=sys.stderr)
if isinstance(result, Exception):
# everything pending gets an error, including close()
f = failure.Failure(result)
self._closed_observer.error(f)
else:
# everything pending except close() gets an error:
# w.get_code()/welcome/unverified_key/verifier/versions/message
f = failure.Failure(WormholeClosed(result))
# but w.close() only gets error if we're unhappy
self._closed_observer.fire_if_not_fired(result)
self._welcome_observer.error(f)
self._code_observer.error(f)
self._key_observer.error(f)
self._verifier_observer.error(f)
self._version_observer.error(f)
self._received_observer.fire(f)
def create(
appid,
relay_url,
reactor, # use keyword args for everything else
versions={},
delegate=None,
journal=None,
tor=None,
timing=None,
stderr=sys.stderr,
_eventual_queue=None,
_enable_dilate=False):
timing = timing or DebugTiming()
side = bytes_to_hexstr(os.urandom(5))
journal = journal or ImmediateJournal()
eq = _eventual_queue or EventualQueue(reactor)
cooperator = Cooperator(scheduler=eq.eventually)
if delegate:
w = _DelegatedWormhole(delegate)
else:
w = _DeferredWormhole(reactor, eq, _enable_dilate=_enable_dilate)
# this indicates Wormhole capabilities
wormhole_versions = {
"can-dilate": DILATION_VERSIONS,
"dilation-abilities": Connector.get_connection_abilities(),
}
if not _enable_dilate:
wormhole_versions = {} # don't advertise Dilation yet: not ready
wormhole_versions["app_versions"] = versions # app-specific capabilities
v = __version__
if isinstance(v, type(b"")):
v = v.decode("utf-8", errors="replace")
client_version = ("python", v)
b = Boss(w, side, relay_url, appid, wormhole_versions, client_version,
reactor, eq, cooperator, journal, tor, timing)
w._set_boss(b)
b.start()
return w
# def from_serialized(serialized, reactor, delegate,
# journal=None, tor=None,
# timing=None, stderr=sys.stderr):
# assert serialized["serialized_wormhole_version"] == 1
# timing = timing or DebugTiming()
# w = _DelegatedWormhole(delegate)
# # now unpack state machines, including the SPAKE2 in Key
# b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing)
# w._set_boss(b)
# b.start() # ??
# raise NotImplemented
# # should the new Wormhole call got_code? only if it wasn't called before.