working on fixes

This commit is contained in:
Brian Warner 2017-02-24 18:30:00 -08:00
parent b7df5e21eb
commit 41b7bcfed5
10 changed files with 110 additions and 29 deletions

View File

@ -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"
]

View File

@ -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"]

View File

@ -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=[])

View File

@ -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])

View File

@ -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])

View File

@ -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":

View File

@ -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:

View File

@ -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"])

View File

@ -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)

View File

@ -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,