From 41b7bcfed598691bbdfd57775e120120e3b5c664 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 24 Feb 2017 18:30:00 -0800 Subject: [PATCH] working on fixes --- docs/boss.dot | 11 ++++++++--- docs/machines.dot | 2 +- src/wormhole/_boss.py | 38 ++++++++++++++++++++++++++++++++----- src/wormhole/_key.py | 16 +++++++++++----- src/wormhole/_mailbox.py | 14 ++++++++------ src/wormhole/_order.py | 1 + src/wormhole/_receive.py | 2 +- src/wormhole/_rendezvous.py | 28 ++++++++++++++++++++++----- src/wormhole/_send.py | 6 +++++- src/wormhole/wormhole.py | 21 ++++++++++++++++++-- 10 files changed, 110 insertions(+), 29 deletions(-) diff --git a/docs/boss.dot b/docs/boss.dot index bff04d3..053d06a 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -17,6 +17,9 @@ digraph { S0 [label="S0: empty"] S0 -> P0_build [label="set_code"] + S0 -> P_close_error [label="rx_error"] + P_close_error [shape="box" label="M.close(errory)"] + P_close_error -> S_closing S0 -> P_close_lonely [label="close"] P0_build [shape="box" label="W.got_code\nM.set_nameplate\nK.got_code"] @@ -25,6 +28,7 @@ digraph { S1 -> S2 [label="happy"] + S1 -> P_close_error [label="rx_error"] S1 -> P_close_scary [label="scared" color="red"] S1 -> P_close_lonely [label="close"] P_close_lonely [shape="box" label="M.close(lonely)"] @@ -39,22 +43,23 @@ digraph { P2_close -> S_closing S2 -> P2_got_message [label="got_message"] - P2_got_message [shape="box" label="A.received"] + P2_got_message [shape="box" label="W.received"] P2_got_message -> S2 + S2 -> P_close_error [label="rx_error"] S2 -> P_close_scary [label="scared" color="red"] S_closing [label="closing"] S_closing -> P_closed [label="closed"] S_closing -> S_closing [label="got_message\nhappy\nscared\nclose"] - P_closed [shape="box" label="A.closed(reason)"] + P_closed [shape="box" label="W.closed(reason)"] P_closed -> S_closed S_closed [label="closed"] {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> A.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/docs/machines.dot b/docs/machines.dot index 4c75515..68a6a5e 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -24,7 +24,7 @@ digraph { #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] - Connection -> Boss [style="dashed" label="rx_welcome"] + Connection -> Boss [style="dashed" label="rx_welcome\nrx_error"] Boss -> Send [style="dashed" label="send"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index e6b9238..1b5dd9a 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -1,7 +1,10 @@ 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 ._mailbox import Mailbox @@ -12,8 +15,12 @@ from ._receive import Receive from ._rendezvous import RendezvousConnector from ._nameplate import NameplateListing from ._code import Code +from .errors import WrongPasswordError from .util import bytes_to_dict +class WormholeError(Exception): + pass + @attrs @implementer(_interfaces.IBoss) class Boss(object): @@ -30,7 +37,7 @@ class Boss(object): 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._timing) + self._K = Key(self._appid, self._side, self._timing) self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, @@ -51,6 +58,8 @@ class Boss(object): 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() @@ -107,13 +116,20 @@ class Boss(object): def happy(self): pass @m.input() def scared(self): pass + @m.input() + def rx_error(self, err, orig): 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: - self.got_phase(phase, plaintext) + # Ignore unrecognized phases, for forwards-compatibility. Use + # log.err so tests will catch surprises. + log.err("received unknown phase '%s'" % phase) @m.input() def got_version(self, plaintext): pass @m.input() @@ -143,18 +159,26 @@ class Boss(object): @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(phase, plaintext) + self._S.send("%d" % phase, plaintext) @m.output() + def close_error(self, err, orig): + self._result = WormholeError(err) + self._M.close("errory") + @m.output() def close_scared(self): + self._result = WrongPasswordError() self._M.close("scary") @m.output() def close_lonely(self): + self._result = WormholeError("lonely") self._M.close("lonely") @m.output() def close_happy(self): + self._result = "happy" self._M.close("happy") @m.output() @@ -162,6 +186,7 @@ class Boss(object): 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: @@ -170,27 +195,30 @@ class Boss(object): @m.output() def W_closed(self): - result = "???" - self._W.closed(result) + 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_welcome, enter=S0_empty, outputs=[process_welcome]) S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code]) + S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error]) S1_lonely.upon(rx_welcome, enter=S1_lonely, outputs=[process_welcome]) 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_verifier, enter=S1_lonely, outputs=[W_got_verifier]) + S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error]) S2_happy.upon(rx_welcome, enter=S2_happy, outputs=[process_welcome]) 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]) S3_closing.upon(rx_welcome, enter=S3_closing, outputs=[]) + S3_closing.upon(rx_error, 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=[]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 3472871..016bc86 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -1,5 +1,6 @@ from __future__ import print_function, absolute_import, unicode_literals from hashlib import sha256 +import six from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides, instance_of @@ -22,10 +23,10 @@ def HKDF(skm, outlen, salt=None, CTXinfo=b""): def derive_key(key, purpose, length=SecretBox.KEY_SIZE): if not isinstance(key, type(b"")): raise TypeError(type(key)) if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) - if not isinstance(length, int): raise TypeError(type(length)) + if not isinstance(length, six.integer_types): raise TypeError(type(length)) return HKDF(key, length, CTXinfo=purpose) -def derive_phase_key(side, phase): +def derive_phase_key(key, side, phase): assert isinstance(side, type("")), type(side) assert isinstance(phase, type("")), type(phase) side_bytes = side.encode("ascii") @@ -33,7 +34,7 @@ def derive_phase_key(side, phase): purpose = (b"wormhole:phase:" + sha256(side_bytes).digest() + sha256(phase_bytes).digest()) - return derive_key(purpose) + return derive_key(key, purpose) def decrypt_data(key, encrypted): assert isinstance(key, type(b"")), type(key) @@ -55,6 +56,7 @@ def encrypt_data(key, plaintext): @implementer(_interfaces.IKey) class Key(object): _appid = attrib(validator=instance_of(type(u""))) + _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @@ -106,9 +108,13 @@ class Key(object): assert isinstance(msg2, type(b"")) with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) - self._my_versions = {} - self._M.add_message("version", self._my_versions) self._B.got_verifier(derive_key(key, b"wormhole:verifier")) + phase = "version" + data_key = derive_phase_key(key, self._side, phase) + my_versions = {} # TODO: get from Wormhole? + plaintext = dict_to_bytes(my_versions) + encrypted = encrypt_data(data_key, plaintext) + self._M.add_message(phase, encrypted) self._R.got_key(key) S0_know_nothing.upon(got_code, enter=S1_know_code, outputs=[build_pake]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 5d9642f..16dd882 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -121,7 +121,10 @@ class Mailbox(object): # from Send or Key @m.input() - def add_message(self, phase, body): pass + def add_message(self, phase, body): + assert isinstance(body, type(b"")), type(body) + #print("ADD_MESSAGE", phase, len(body)) + pass @m.output() @@ -142,7 +145,7 @@ class Mailbox(object): @m.output() def queue(self, phase, body): assert isinstance(phase, type("")), type(phase) - assert isinstance(body, type(b"")), type(body) + assert isinstance(body, type(b"")), (type(body), phase, body) self._pending_outbound[phase] = body @m.output() def store_mailbox_and_RC_tx_open_and_drain(self, mailbox): @@ -189,11 +192,11 @@ class Mailbox(object): self._accept(phase, body) def _accept(self, phase, body): if phase not in self._processed: - self._O.got_message(phase, body) self._processed.add(phase) + self._O.got_message(phase, body) @m.output() def dequeue(self, phase, body): - self._pending_outbound.pop(phase) + self._pending_outbound.pop(phase, None) @m.output() def record_mood(self, mood): self._mood = mood @@ -235,8 +238,7 @@ class Mailbox(object): S3B.upon(rx_claimed, enter=S3B, outputs=[]) S3B.upon(add_message, enter=S3B, outputs=[queue, RC_tx_add]) - S4A.upon(connected, enter=S4B, - outputs=[RC_tx_open, drain, RC_tx_release]) + S4A.upon(connected, enter=S4B, outputs=[RC_tx_open, drain, RC_tx_release]) S4A.upon(add_message, enter=S4A, outputs=[queue]) S4B.upon(lost, enter=S4A, outputs=[]) S4B.upon(add_message, enter=S4B, outputs=[queue, RC_tx_add]) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index e16f2b6..b21829d 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -25,6 +25,7 @@ class Order(object): def S1_yes_pake(self): pass def got_message(self, phase, body): + #print("ORDER[%s].got_message(%s)" % (self._side, phase)) assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) if phase == "pake": diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 47a3c12..ba2bbc3 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -35,7 +35,7 @@ class Receive(object): assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) assert self._key - data_key = derive_phase_key(self._side, phase) + data_key = derive_phase_key(self._key, self._side, phase) try: plaintext = decrypt_data(data_key, body) except CryptoError: diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index c65dc68..f6a34fa 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -26,12 +26,12 @@ class WSClient(websocket.WebSocketClientProtocol): self._RC.ws_open(self) def onMessage(self, payload, isBinary): - #print("onMessage") assert not isBinary try: self._RC.ws_message(payload) except: - print("LOGGING") + from twisted.python.failure import Failure + print("LOGGING", Failure()) log.err() raise @@ -57,6 +57,10 @@ class WSFactory(websocket.WebSocketClientFactory): #proto.wormhole_open = False return proto +def dmsg(side, text): + offset = int(side, 16) % 20 + print(" "*offset, text) + @attrs @implementer(_interfaces.IRendezvousConnector) class RendezvousConnector(object): @@ -66,7 +70,7 @@ class RendezvousConnector(object): _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) - DEBUG = False + DEBUG = True def __attrs_post_init__(self): self._ws = None @@ -124,6 +128,8 @@ class RendezvousConnector(object): # from our WSClient (the WebSocket protocol) def ws_open(self, proto): + if self.DEBUG: + dmsg(self._side, "R.connected") self._ws = proto self._tx("bind", appid=self._appid, side=self._side) self._C.connected() @@ -132,7 +138,11 @@ class RendezvousConnector(object): def ws_message(self, payload): msg = bytes_to_dict(payload) - if self.DEBUG and msg["type"]!="ack": print("DIS", msg["type"], msg) + if self.DEBUG and msg["type"]!="ack": + dmsg(self._side, "R.rx(%s %s%s)" % + (msg["type"], msg.get("phase",""), + "[mine]" if msg.get("side","") == self._side else "", + )) self._timing.add("ws_receive", _side=self._side, message=msg) mtype = msg["type"] meth = getattr(self, "_response_handle_"+mtype, None) @@ -143,6 +153,8 @@ class RendezvousConnector(object): return meth(msg) def ws_close(self, wasClean, code, reason): + if self.DEBUG: + dmsg(self._side, "R.lost") self._ws = None self._C.lost() self._M.lost() @@ -158,9 +170,10 @@ class RendezvousConnector(object): # their receives, and vice versa. They are also correlated with the # ACKs we get back from the server (which we otherwise ignore). There # are so few messages, 16 bits is enough to be mostly-unique. - if self.DEBUG: print("SEND", mtype) kwargs["id"] = bytes_to_hexstr(os.urandom(2)) kwargs["type"] = mtype + if self.DEBUG: + dmsg(self._side, "R.tx(%s %s)" % (mtype.upper(), kwargs.get("phase", ""))) payload = dict_to_bytes(kwargs) self._timing.add("ws_send", _side=self._side, **kwargs) self._ws.sendMessage(payload, False) @@ -184,6 +197,11 @@ class RendezvousConnector(object): def _response_handle_ack(self, msg): pass + def _response_handle_error(self, msg): + err = msg["error"] + orig = msg["orig"] + self._B.rx_error(err, orig) + def _response_handle_welcome(self, msg): self._B.rx_welcome(msg["welcome"]) diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 355a267..fd3e590 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -13,6 +13,9 @@ class Send(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + def __attrs_post_init__(self): + self._queue = [] + def wire(self, mailbox): self._M = _interfaces.IMailbox(mailbox) @@ -49,7 +52,8 @@ class Send(object): self._encrypt_and_send(phase, plaintext) def _encrypt_and_send(self, phase, plaintext): - data_key = derive_phase_key(self._side, phase) + assert self._key + data_key = derive_phase_key(self._key, self._side, phase) encrypted = encrypt_data(data_key, plaintext) self._M.add_message(phase, encrypted) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index d965d72..1977d16 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -7,7 +7,7 @@ from ._interfaces import IWormhole from .util import bytes_to_hexstr from .timing import DebugTiming from .journal import ImmediateJournal -from ._boss import Boss +from ._boss import Boss, WormholeError # We can provide different APIs to different apps: # * Deferreds @@ -61,6 +61,9 @@ class _DelegatedWormhole(object): def closed(self, result): self._delegate.wormhole_closed(result) +class WormholeClosed(Exception): + pass + @implementer(IWormhole) class _DeferredWormhole(object): def __init__(self): @@ -70,6 +73,7 @@ class _DeferredWormhole(object): self._verifier_observers = [] self._received_data = [] self._received_observers = [] + self._closed_observers = [] def _set_boss(self, boss): self._boss = boss @@ -107,6 +111,9 @@ class _DeferredWormhole(object): self._boss.send(plaintext) def close(self): self._boss.close() + d = defer.Deferred() + self._closed_observers.append(d) + return d # from below def got_code(self, code): @@ -127,7 +134,17 @@ class _DeferredWormhole(object): self._received_data.append(plaintext) def closed(self, result): - print("closed", result) + print("closed", result, type(result)) + if isinstance(result, WormholeError): + e = result + else: + e = WormholeClosed(result) + for d in self._verifier_observers: + d.errback(e) + for d in self._received_observers: + d.errback(e) + for d in self._closed_observers: + d.callback(result) def _wormhole(appid, relay_url, reactor, delegate=None, tor_manager=None, timing=None,