83e55f1f3e
Previously, w.when_verified() was documented to fire only after a valid encrypted message was received, but in fact it fired as soon as the shared key was derived (before any encrypted messages are seen, so no actual "verification" could occur yet). This fixes that, and also adds a new w.when_key() API call which fires at the earlier point. Having something which fires early is useful for the CLI commands that want to print a pacifier message when the peer is responding slowly. In particular it helps detect the case where 'wormhole send' has quit early (after depositing the PAKE message on the server, but before the receiver has started). In this case, the receiver will compute the shared key, but then wait forever hoping for a VERSION that will never come. By starting a timer when w.when_key() fires, and cancelling it when w.when_verified() fires, we have a good place to tell the user that something is taking longer than it should have. This shifts responsibility for notifying Boss.got_verifier, out of Key and into Receive, since Receive is what notices the first valid encrypted message. It also shifts the Boss's ordering expectations: it now receives B.happy() before B.got_verifier(), and consequently got_verifier ought to arrive in the S2_happy state rather than S1_lonely.
344 lines
14 KiB
Python
344 lines
14 KiB
Python
from __future__ import print_function, absolute_import, unicode_literals
|
|
import re
|
|
import six
|
|
from zope.interface import implementer
|
|
from attr import attrs, attrib
|
|
from attr.validators import provides, instance_of
|
|
from twisted.python import log
|
|
from automat import MethodicalMachine
|
|
from . import _interfaces
|
|
from ._nameplate import Nameplate
|
|
from ._mailbox import Mailbox
|
|
from ._send import Send
|
|
from ._order import Order
|
|
from ._key import Key
|
|
from ._receive import Receive
|
|
from ._rendezvous import RendezvousConnector
|
|
from ._lister import Lister
|
|
from ._allocator import Allocator
|
|
from ._input import Input
|
|
from ._code import Code
|
|
from ._terminator import Terminator
|
|
from ._wordlist import PGPWordList
|
|
from .errors import (ServerError, LonelyError, WrongPasswordError,
|
|
KeyFormatError, OnlyOneCodeError, _UnknownPhaseError,
|
|
WelcomeError)
|
|
from .util import bytes_to_dict
|
|
|
|
@attrs
|
|
@implementer(_interfaces.IBoss)
|
|
class Boss(object):
|
|
_W = attrib()
|
|
_side = attrib(validator=instance_of(type(u"")))
|
|
_url = attrib(validator=instance_of(type(u"")))
|
|
_appid = attrib(validator=instance_of(type(u"")))
|
|
_versions = attrib(validator=instance_of(dict))
|
|
_welcome_handler = attrib() # TODO: validator: callable
|
|
_reactor = attrib()
|
|
_journal = attrib(validator=provides(_interfaces.IJournal))
|
|
_tor_manager = attrib() # TODO: ITorManager or None
|
|
_timing = attrib(validator=provides(_interfaces.ITiming))
|
|
m = MethodicalMachine()
|
|
set_trace = getattr(m, "setTrace", lambda self, f: None)
|
|
|
|
def __attrs_post_init__(self):
|
|
self._build_workers()
|
|
self._init_other_state()
|
|
|
|
def _build_workers(self):
|
|
self._N = Nameplate()
|
|
self._M = Mailbox(self._side)
|
|
self._S = Send(self._side, self._timing)
|
|
self._O = Order(self._side, self._timing)
|
|
self._K = Key(self._appid, self._versions, self._side, self._timing)
|
|
self._R = Receive(self._side, self._timing)
|
|
self._RC = RendezvousConnector(self._url, self._appid, self._side,
|
|
self._reactor, self._journal,
|
|
self._tor_manager, self._timing)
|
|
self._L = Lister(self._timing)
|
|
self._A = Allocator(self._timing)
|
|
self._I = Input(self._timing)
|
|
self._C = Code(self._timing)
|
|
self._T = Terminator()
|
|
|
|
self._N.wire(self._M, self._I, self._RC, self._T)
|
|
self._M.wire(self._N, self._RC, self._O, self._T)
|
|
self._S.wire(self._M)
|
|
self._O.wire(self._K, self._R)
|
|
self._K.wire(self, self._M, self._R)
|
|
self._R.wire(self, self._S)
|
|
self._RC.wire(self, self._N, self._M, self._A, self._L, self._T)
|
|
self._L.wire(self._RC, self._I)
|
|
self._A.wire(self._RC, self._C)
|
|
self._I.wire(self._C, self._L)
|
|
self._C.wire(self, self._A, self._N, self._K, self._I)
|
|
self._T.wire(self, self._RC, self._N, self._M)
|
|
|
|
def _init_other_state(self):
|
|
self._did_start_code = False
|
|
self._next_tx_phase = 0
|
|
self._next_rx_phase = 0
|
|
self._rx_phases = {} # phase -> plaintext
|
|
|
|
self._result = "empty"
|
|
|
|
# these methods are called from outside
|
|
def start(self):
|
|
self._RC.start()
|
|
|
|
def _set_trace(self, client_name, which, file):
|
|
names = {"B": self, "N": self._N, "M": self._M, "S": self._S,
|
|
"O": self._O, "K": self._K, "SK": self._K._SK, "R": self._R,
|
|
"RC": self._RC, "L": self._L, "C": self._C,
|
|
"T": self._T}
|
|
for machine in which.split():
|
|
def tracer(old_state, input, new_state, output, machine=machine):
|
|
if output is None:
|
|
if new_state:
|
|
print("%s.%s[%s].%s -> [%s]" %
|
|
(client_name, machine, old_state, input,
|
|
new_state), file=file)
|
|
else:
|
|
# the RendezvousConnector emits message events as if
|
|
# they were state transitions, except that old_state
|
|
# and new_state are empty strings. "input" is one of
|
|
# R.connected, R.rx(type phase+side), R.tx(type
|
|
# phase), R.lost .
|
|
print("%s.%s.%s" % (client_name, machine, input),
|
|
file=file)
|
|
else:
|
|
if new_state:
|
|
print(" %s.%s.%s()" % (client_name, machine, output),
|
|
file=file)
|
|
file.flush()
|
|
names[machine].set_trace(tracer)
|
|
|
|
## def serialize(self):
|
|
## raise NotImplemented
|
|
|
|
# and these are the state-machine transition functions, which don't take
|
|
# args
|
|
@m.state(initial=True)
|
|
def S0_empty(self): pass # pragma: no cover
|
|
@m.state()
|
|
def S1_lonely(self): pass # pragma: no cover
|
|
@m.state()
|
|
def S2_happy(self): pass # pragma: no cover
|
|
@m.state()
|
|
def S3_closing(self): pass # pragma: no cover
|
|
@m.state(terminal=True)
|
|
def S4_closed(self): pass # pragma: no cover
|
|
|
|
# from the Wormhole
|
|
|
|
# input/allocate/set_code are regular methods, not state-transition
|
|
# inputs. We expect them to be called just after initialization, while
|
|
# we're in the S0_empty state. You must call exactly one of them, and the
|
|
# call must happen while we're in S0_empty, which makes them good
|
|
# candiates for being a proper @m.input, but set_code() will immediately
|
|
# (reentrantly) cause self.got_code() to be fired, which is messy. These
|
|
# are all passthroughs to the Code machine, so one alternative would be
|
|
# to have Wormhole call Code.{input,allocate,set_code} instead, but that
|
|
# would require the Wormhole to be aware of Code (whereas right now
|
|
# Wormhole only knows about this Boss instance, and everything else is
|
|
# hidden away).
|
|
def input_code(self):
|
|
if self._did_start_code:
|
|
raise OnlyOneCodeError()
|
|
self._did_start_code = True
|
|
return self._C.input_code()
|
|
def allocate_code(self, code_length):
|
|
if self._did_start_code:
|
|
raise OnlyOneCodeError()
|
|
self._did_start_code = True
|
|
wl = PGPWordList()
|
|
self._C.allocate_code(code_length, wl)
|
|
def set_code(self, code):
|
|
if ' ' in code:
|
|
raise KeyFormatError("code (%s) contains spaces." % code)
|
|
if self._did_start_code:
|
|
raise OnlyOneCodeError()
|
|
self._did_start_code = True
|
|
self._C.set_code(code)
|
|
|
|
@m.input()
|
|
def send(self, plaintext): pass
|
|
@m.input()
|
|
def close(self): pass
|
|
|
|
# from RendezvousConnector:
|
|
# * "rx_welcome" is the Welcome message, which might signal an error, or
|
|
# our welcome_handler might signal one
|
|
# * "rx_error" is error message from the server (probably because of
|
|
# something we said badly, or due to CrowdedError)
|
|
# * "error" is when an exception happened while it tried to deliver
|
|
# something else
|
|
def rx_welcome(self, welcome):
|
|
try:
|
|
if "error" in welcome:
|
|
raise WelcomeError(welcome["error"])
|
|
# TODO: it'd be nice to not call the handler when we're in
|
|
# S3_closing or S4_closed states. I tried to implement this with
|
|
# rx_Welcome as an @input, but in the error case I'd be
|
|
# delivering a new input (rx_error or something) while in the
|
|
# middle of processing the rx_welcome input, and I wasn't sure
|
|
# Automat would handle that correctly.
|
|
self._welcome_handler(welcome) # can raise WelcomeError too
|
|
except WelcomeError as welcome_error:
|
|
self.rx_unwelcome(welcome_error)
|
|
@m.input()
|
|
def rx_unwelcome(self, welcome_error): pass
|
|
@m.input()
|
|
def rx_error(self, errmsg, orig): pass
|
|
@m.input()
|
|
def error(self, err): pass
|
|
|
|
# from Code (provoked by input/allocate/set_code)
|
|
@m.input()
|
|
def got_code(self, code): pass
|
|
|
|
# Key sends (got_key, scared)
|
|
# Receive sends (got_message, happy, got_verifier, scared)
|
|
@m.input()
|
|
def happy(self): pass
|
|
@m.input()
|
|
def scared(self): pass
|
|
|
|
def got_message(self, phase, plaintext):
|
|
assert isinstance(phase, type("")), type(phase)
|
|
assert isinstance(plaintext, type(b"")), type(plaintext)
|
|
if phase == "version":
|
|
self._got_version(plaintext)
|
|
elif re.search(r'^\d+$', phase):
|
|
self._got_phase(int(phase), plaintext)
|
|
else:
|
|
# Ignore unrecognized phases, for forwards-compatibility. Use
|
|
# log.err so tests will catch surprises.
|
|
log.err(_UnknownPhaseError("received unknown phase '%s'" % phase))
|
|
@m.input()
|
|
def _got_version(self, plaintext): pass
|
|
@m.input()
|
|
def _got_phase(self, phase, plaintext): pass
|
|
@m.input()
|
|
def got_key(self, key): pass
|
|
@m.input()
|
|
def got_verifier(self, verifier): pass
|
|
|
|
# Terminator sends closed
|
|
@m.input()
|
|
def closed(self): pass
|
|
|
|
@m.output()
|
|
def do_got_code(self, code):
|
|
self._W.got_code(code)
|
|
@m.output()
|
|
def process_version(self, plaintext):
|
|
# most of this is wormhole-to-wormhole, ignored for now
|
|
# in the future, this is how Dilation is signalled
|
|
self._their_versions = bytes_to_dict(plaintext)
|
|
# but this part is app-to-app
|
|
app_versions = self._their_versions.get("app_versions", {})
|
|
self._W.got_version(app_versions)
|
|
|
|
@m.output()
|
|
def S_send(self, plaintext):
|
|
assert isinstance(plaintext, type(b"")), type(plaintext)
|
|
phase = self._next_tx_phase
|
|
self._next_tx_phase += 1
|
|
self._S.send("%d" % phase, plaintext)
|
|
|
|
@m.output()
|
|
def close_unwelcome(self, welcome_error):
|
|
#assert isinstance(err, WelcomeError)
|
|
self._result = welcome_error
|
|
self._T.close("unwelcome")
|
|
@m.output()
|
|
def close_error(self, errmsg, orig):
|
|
self._result = ServerError(errmsg)
|
|
self._T.close("errory")
|
|
@m.output()
|
|
def close_scared(self):
|
|
self._result = WrongPasswordError()
|
|
self._T.close("scary")
|
|
@m.output()
|
|
def close_lonely(self):
|
|
self._result = LonelyError()
|
|
self._T.close("lonely")
|
|
@m.output()
|
|
def close_happy(self):
|
|
self._result = "happy"
|
|
self._T.close("happy")
|
|
|
|
@m.output()
|
|
def W_got_key(self, key):
|
|
self._W.got_key(key)
|
|
@m.output()
|
|
def W_got_verifier(self, verifier):
|
|
self._W.got_verifier(verifier)
|
|
@m.output()
|
|
def W_received(self, phase, plaintext):
|
|
assert isinstance(phase, six.integer_types), type(phase)
|
|
# we call Wormhole.received() in strict phase order, with no gaps
|
|
self._rx_phases[phase] = plaintext
|
|
while self._next_rx_phase in self._rx_phases:
|
|
self._W.received(self._rx_phases.pop(self._next_rx_phase))
|
|
self._next_rx_phase += 1
|
|
|
|
@m.output()
|
|
def W_close_with_error(self, err):
|
|
self._result = err # exception
|
|
self._W.closed(self._result)
|
|
|
|
@m.output()
|
|
def W_closed(self):
|
|
# result is either "happy" or a WormholeError of some sort
|
|
self._W.closed(self._result)
|
|
|
|
S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely])
|
|
S0_empty.upon(send, enter=S0_empty, outputs=[S_send])
|
|
S0_empty.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
|
|
S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code])
|
|
S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
|
S0_empty.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
|
|
|
S1_lonely.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
|
|
S1_lonely.upon(happy, enter=S2_happy, outputs=[])
|
|
S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared])
|
|
S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely])
|
|
S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send])
|
|
S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key])
|
|
S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
|
S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
|
|
|
S2_happy.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
|
|
S2_happy.upon(got_verifier, enter=S2_happy, outputs=[W_got_verifier])
|
|
S2_happy.upon(_got_phase, enter=S2_happy, outputs=[W_received])
|
|
S2_happy.upon(_got_version, enter=S2_happy, outputs=[process_version])
|
|
S2_happy.upon(scared, enter=S3_closing, outputs=[close_scared])
|
|
S2_happy.upon(close, enter=S3_closing, outputs=[close_happy])
|
|
S2_happy.upon(send, enter=S2_happy, outputs=[S_send])
|
|
S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
|
S2_happy.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
|
|
|
S3_closing.upon(rx_unwelcome, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(rx_error, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(got_verifier, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(_got_phase, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(_got_version, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(happy, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(scared, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(close, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(send, enter=S3_closing, outputs=[])
|
|
S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed])
|
|
S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
|
|
|
S4_closed.upon(rx_unwelcome, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(got_verifier, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(_got_phase, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(_got_version, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(happy, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(scared, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(close, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(send, enter=S4_closed, outputs=[])
|
|
S4_closed.upon(error, enter=S4_closed, outputs=[])
|