build out all state machines

still early: automat is happy (they're syntactically valid), but the Outputs
are not implemented, and there are plenty of type mismatches
This commit is contained in:
Brian Warner 2017-02-22 11:26:11 -08:00
parent 9ae8091ec3
commit 80661392b6
14 changed files with 682 additions and 271 deletions

View File

@ -73,6 +73,8 @@ digraph {
]
Code -> Wormhole [style="dashed"
label="set_code"]
App -> Code [style="dashed"
label="allocate\ninput\nset"]

View File

@ -38,5 +38,5 @@ digraph {
{rank=same; foo foo2 legend}
legend [shape="box" style="dotted"
label="refresh: NL.refresh_nameplates()\nrx: NL.got_nameplates()"]
label="refresh: NL.refresh_nameplates()\nrx: NL.rx_nameplates()"]
}

View File

@ -40,6 +40,8 @@ digraph {
P2_got_message [shape="box" label="A.received"]
P2_got_message -> S2
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"]

112
src/wormhole/_code.py Normal file
View File

@ -0,0 +1,112 @@
import os
from zope.interface import implementer
from automat import MethodicalMachine
from . import _interfaces
from .wordlist import (byte_to_even_word, byte_to_odd_word,
#even_words_lowercase, odd_words_lowercase,
)
def make_code(nameplate, code_length):
assert isinstance(nameplate, type("")), type(nameplate)
words = []
for i in range(code_length):
# we start with an "odd word"
if i % 2 == 0:
words.append(byte_to_odd_word[os.urandom(1)].lower())
else:
words.append(byte_to_even_word[os.urandom(1)].lower())
return "%s-%s" % (nameplate, "-".join(words))
@implementer(_interfaces.ICode)
class Code(object):
m = MethodicalMachine()
def __init__(self, code_length, timing):
self._code_length = code_length
self._timing = timing
def wire(self, wormhole, rendezvous_connector, nameplate_lister):
self._W = _interfaces.IWormhole(wormhole)
self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
self._NL = _interfaces.INameplateListing(nameplate_lister)
@m.state(initial=True)
def S0_unknown(self): pass
@m.state()
def S1_allocating(self): pass
@m.state()
def S2_typing_nameplate(self): pass
@m.state()
def S3_typing_code(self): pass
@m.state()
def S4_known(self): pass
# from App
@m.input()
def allocate(self): pass
@m.input()
def input(self): pass
@m.input()
def set(self, code): pass
# from RendezvousConnector
@m.input()
def rx_allocated(self, nameplate): pass
# from NameplateLister
@m.input()
def got_nameplates(self, nameplates): pass
# from stdin/readline/???
@m.input()
def tab(self): pass
@m.input()
def hyphen(self): pass
@m.input()
def RETURN(self, code): pass
@m.output()
def NL_refresh_nameplates(self):
self._NL.refresh_nameplates()
@m.output()
def RC_tx_allocate(self):
self._RC.tx_allocate()
@m.output()
def do_completion_nameplates(self):
pass
@m.output()
def stash_nameplates(self, nameplates):
self._known_nameplates = nameplates
pass
@m.output()
def lookup_wordlist(self):
pass
@m.output()
def do_completion_code(self):
pass
@m.output()
def generate_and_set(self, nameplate):
self._code = make_code(nameplate, self._code_length)
self._W_set_code()
@m.output()
def W_set_code(self, code):
self._code = code
self._W_set_code()
def _W_set_code(self):
self._W.set_code(self._code)
S0_unknown.upon(allocate, enter=S1_allocating, outputs=[RC_tx_allocate])
S1_allocating.upon(rx_allocated, enter=S4_known, outputs=[generate_and_set])
S0_unknown.upon(set, enter=S4_known, outputs=[W_set_code])
S0_unknown.upon(input, enter=S2_typing_nameplate,
outputs=[NL_refresh_nameplates])
S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate,
outputs=[do_completion_nameplates])
S2_typing_nameplate.upon(got_nameplates, enter=S2_typing_nameplate,
outputs=[stash_nameplates])
S2_typing_nameplate.upon(hyphen, enter=S3_typing_code,
outputs=[lookup_wordlist])
S3_typing_code.upon(tab, enter=S3_typing_code, outputs=[do_completion_code])
S3_typing_code.upon(RETURN, enter=S4_known, outputs=[W_set_code])

View File

@ -1,8 +1,8 @@
from zope.interface import Interface
from six.moves.urllib_parse import urlparse
from attr import attrs, attrib
from twisted.internet import defer, endpoints #, error
from twisted.application import internet
from twisted.application import internet, service
from autobahn.twisted import websocket
from automat import MethodicalMachine

View File

@ -0,0 +1,20 @@
from zope.interface import Interface
class IWormhole(Interface):
pass
class IMailbox(Interface):
pass
class ISend(Interface):
pass
class IOrder(Interface):
pass
class IKey(Interface):
pass
class IReceive(Interface):
pass
class IRendezvousConnector(Interface):
pass
class INameplateListing(Interface):
pass
class ICode(Interface):
pass

View File

@ -1,8 +1,15 @@
from automat import MethodicalMachine
from hashlib import sha256
from zope.interface import implementer
from spake2 import SPAKE2_Symmetric
from hkdf import Hkdf
from nacl.secret import SecretBox
from nacl.exceptions import CryptoError
from automat import MethodicalMachine
from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes)
from . import _interfaces
CryptoError
__all__ = ["derive_key", "derive_phase_key", "CryptoError",
"Key"]
def HKDF(skm, outlen, salt=None, CTXinfo=b""):
return Hkdf(salt, skm).expand(CTXinfo, outlen)
@ -13,15 +20,33 @@ def derive_key(key, purpose, length=SecretBox.KEY_SIZE):
if not isinstance(length, int): raise TypeError(type(length))
return HKDF(key, length, CTXinfo=purpose)
class KeyMachine(object):
def derive_phase_key(side, phase):
assert isinstance(side, type("")), type(side)
assert isinstance(phase, type("")), type(phase)
side_bytes = side.encode("ascii")
phase_bytes = phase.encode("ascii")
purpose = (b"wormhole:phase:"
+ sha256(side_bytes).digest()
+ sha256(phase_bytes).digest())
return derive_key(purpose)
def decrypt_data(key, encrypted):
assert isinstance(key, type(b"")), type(key)
assert isinstance(encrypted, type(b"")), type(encrypted)
assert len(key) == SecretBox.KEY_SIZE, len(key)
box = SecretBox(key)
data = box.decrypt(encrypted)
return data
@implementer(_interfaces.IKey)
class Key(object):
m = MethodicalMachine()
def __init__(self, wormhole, timing):
self._wormhole = wormhole
def __init__(self, timing):
self._timing = timing
def set_mailbox(self, mailbox):
self._mailbox = mailbox
def set_receive(self, receive):
self._receive = receive
def wire(self, wormhole, mailbox, receive):
self._W = _interfaces.IWormhole(wormhole)
self._M = _interfaces.IMailbox(mailbox)
self._R = _interfaces.IReceive(receive)
@m.state(initial=True)
def S0_know_nothing(self): pass
@ -29,7 +54,7 @@ class KeyMachine(object):
def S1_know_code(self): pass
@m.state()
def S2_know_key(self): pass
@m.state()
@m.state(terminal=True)
def S3_scared(self): pass
def got_pake(self, payload):
@ -51,20 +76,20 @@ class KeyMachine(object):
self._sp = SPAKE2_Symmetric(to_bytes(code),
idSymmetric=to_bytes(self._appid))
msg1 = self._sp.start()
self._mailbox.add_message("pake", {"pake_v1": bytes_to_hexstr(msg1)})
self._M.add_message("pake", {"pake_v1": bytes_to_hexstr(msg1)})
@m.output()
def scared(self):
self._wormhole.scared()
self._W.scared()
@m.output()
def compute_key(self, msg2):
assert isinstance(msg2, type(b""))
with self._timing.add("pake2", waiting="crypto"):
key = self._sp.finish(msg2)
self._my_versions = {}
self._mailbox.add_message("version", self._my_versions)
self._wormhole.got_verifier(derive_key(key, b"wormhole:verifier"))
self._receive.got_key(key)
self._M.add_message("version", self._my_versions)
self._W.got_verifier(derive_key(key, b"wormhole:verifier"))
self._R.got_key(key)
S0_know_nothing.upon(set_code, enter=S1_know_code, outputs=[build_pake])
S1_know_code.upon(got_pake_good, enter=S2_know_key, outputs=[compute_key])

View File

@ -1,12 +1,21 @@
from attr import attrs, attrib
from zope.interface import implementer
from automat import MethodicalMachine
from . import _interfaces
@attrs
class _Mailbox_Machine(object):
_connection_machine = attrib()
_m = attrib()
@implementer(_interfaces.IMailbox)
class Mailbox(object):
m = MethodicalMachine()
def __init__(self, side):
self._side = side
self._mood = None
self._nameplate = None
def wire(self, wormhole, rendezvous_connector, ordering):
self._W = _interfaces.IWormhole(wormhole)
self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
self._O = _interfaces.IOrder(ordering)
@m.state(initial=True)
def initial(self): pass
@ -68,136 +77,201 @@ class _Mailbox_Machine(object):
# Ss: closed and released, waiting for stop
@m.state()
def SsB(self): pass
@m.state()
def Ss(self): pass # terminal
@m.state(terminal=True)
def Ss(self): pass
def connected(self, ws):
self._ws = ws
self.M_connected()
@m.input()
def M_start_unconnected(self): pass
def start_unconnected(self): pass
@m.input()
def M_start_connected(self): pass
def start_connected(self): pass
# from Wormhole
@m.input()
def M_set_nameplate(self): pass
def set_nameplate(self, nameplate): pass
@m.input()
def M_connected(self): pass
def close(self, mood): pass
# from RendezvousConnector
@m.input()
def M_lost(self): pass
def connected(self): pass
@m.input()
def M_send(self, msg): pass
def lost(self): pass
@m.input()
def M_rx_claimed(self): pass
def rx_claimed(self, mailbox): pass
def rx_message(self, side, phase, msg):
if side == self._side:
self.rx_message_ours(phase, msg)
else:
self.rx_message_theirs(phase, msg)
@m.input()
def M_rx_msg_from_me(self, msg): pass
def rx_message_ours(self, phase, msg): pass
@m.input()
def M_rx_msg_from_them(self, msg): pass
def rx_message_theirs(self, phase, msg): pass
@m.input()
def M_rx_released(self): pass
def rx_released(self): pass
@m.input()
def M_rx_closed(self): pass
def rx_closed(self): pass
@m.input()
def M_stop(self): pass
def stopped(self): pass
# from Send or Key
@m.input()
def M_stopped(self): pass
def add_message(self, phase, msg): pass
@m.output()
def tx_claim(self):
self._c.send_command("claim", nameplate=self._nameplate)
def record_nameplate(self, nameplate):
self._nameplate = nameplate
@m.output()
def tx_open(self): pass
def record_nameplate_and_RC_tx_claim(self, nameplate):
self._nameplate = nameplate
self._RX.tx_claim(self._nameplate)
@m.output()
def queue(self, msg): pass
def RC_tx_claim(self):
# when invoked via M.connected(), we must use the stored nameplate
self._RC.tx_claim(self._nameplate)
@m.output()
def store_mailbox(self): pass # trouble(mb)
def RC_tx_open(self):
assert self._mailbox
self._RC.tx_open(self._mailbox)
@m.output()
def tx_add(self, msg): pass
def queue(self, phase, msg):
self._pending_outbound[phase] = msg
@m.output()
def tx_add_queued(self): pass
def store_mailbox_and_RC_tx_open_and_drain(self, mailbox):
self._mailbox = mailbox
self._RC.tx_open(mailbox)
self._drain()
@m.output()
def tx_release(self): pass
def drain(self):
self._drain()
def _drain(self):
for phase, msg in self._pending_outbound.items():
self._RC.tx_add(phase, msg)
@m.output()
def tx_close(self): pass
def RC_tx_add(self, phase, msg):
self._RC.tx_add(phase, msg)
@m.output()
def process_first_msg_from_them(self, msg):
self.tx_release()
self.process_msg_from_them(msg)
def RC_tx_release(self):
self._RC.tx_release()
@m.output()
def process_msg_from_them(self, msg): pass
def RC_tx_release_and_accept(self, phase, msg):
self._RC.tx_release()
self._accept(phase, msg)
@m.output()
def dequeue(self, msg): pass
def record_mood_and_RC_tx_release(self, mood):
self._mood = mood
self._RC.tx_release()
@m.output()
def C_stop(self): pass
def record_mood_and_RC_tx_release_and_RC_tx_close(self, mood):
self._mood = mood
self._RC.tx_release()
self._RC.tx_close(self._mood)
@m.output()
def WM_stopped(self): pass
def RC_tx_close(self):
assert self._mood
self._RC.tx_close(self._mood)
@m.output()
def record_mood_and_RC_tx_close(self, mood):
self._mood = mood
self._RC.tx_close(self._mood)
@m.output()
def accept(self, phase, msg):
self._accept(phase, msg)
def _accept(self, phase, msg):
if phase not in self._processed:
self._O.got_message(phase, msg)
self._processed.add(phase)
@m.output()
def dequeue(self, phase, msg):
self._pending_outbound.pop(phase)
@m.output()
def record_mood(self, mood):
self._mood = mood
@m.output()
def record_mood_and_RC_stop(self, mood):
self._mood = mood
self._RC_stop()
@m.output()
def RC_stop(self):
self._RC_stop()
@m.output()
def W_closed(self):
self._W.closed()
initial.upon(M_start_unconnected, enter=S0A, outputs=[])
initial.upon(M_start_connected, enter=S0B, outputs=[])
S0A.upon(M_connected, enter=S0B, outputs=[])
S0A.upon(M_set_nameplate, enter=S1A, outputs=[])
S0A.upon(M_stop, enter=SsB, outputs=[C_stop])
S0B.upon(M_lost, enter=S0A, outputs=[])
S0B.upon(M_set_nameplate, enter=S2B, outputs=[tx_claim])
S0B.upon(M_stop, enter=SsB, outputs=[C_stop])
initial.upon(start_unconnected, enter=S0A, outputs=[])
initial.upon(start_connected, enter=S0B, outputs=[])
S0A.upon(connected, enter=S0B, outputs=[])
S0A.upon(set_nameplate, enter=S1A, outputs=[record_nameplate])
S0A.upon(add_message, enter=S0A, outputs=[queue])
S0B.upon(lost, enter=S0A, outputs=[])
S0B.upon(set_nameplate, enter=S2B, outputs=[record_nameplate_and_RC_tx_claim])
S0B.upon(add_message, enter=S0B, outputs=[queue])
S1A.upon(M_connected, enter=S2B, outputs=[tx_claim])
S1A.upon(M_send, enter=S1A, outputs=[queue])
S1A.upon(M_stop, enter=SsB, outputs=[C_stop])
S1A.upon(connected, enter=S2B, outputs=[RC_tx_claim])
S1A.upon(add_message, enter=S1A, outputs=[queue])
S2A.upon(M_connected, enter=S2B, outputs=[tx_claim])
S2A.upon(M_stop, enter=SrA, outputs=[])
S2A.upon(M_send, enter=S2A, outputs=[queue])
S2B.upon(M_lost, enter=S2A, outputs=[])
S2B.upon(M_send, enter=S2B, outputs=[queue])
S2B.upon(M_stop, enter=SrB, outputs=[tx_release])
S2B.upon(M_rx_claimed, enter=S3B, outputs=[store_mailbox, tx_open,
tx_add_queued])
S2A.upon(connected, enter=S2B, outputs=[RC_tx_claim])
S2A.upon(add_message, enter=S2A, outputs=[queue])
S2B.upon(lost, enter=S2A, outputs=[])
S2B.upon(add_message, enter=S2B, outputs=[queue])
S2B.upon(rx_claimed, enter=S3B,
outputs=[store_mailbox_and_RC_tx_open_and_drain])
S3A.upon(M_connected, enter=S3B, outputs=[tx_open, tx_add_queued])
S3A.upon(M_send, enter=S3A, outputs=[queue])
S3A.upon(M_stop, enter=SrcA, outputs=[])
S3B.upon(M_lost, enter=S3A, outputs=[])
S3B.upon(M_rx_msg_from_them, enter=S4B,
outputs=[process_first_msg_from_them])
S3B.upon(M_rx_msg_from_me, enter=S3B, outputs=[dequeue])
S3B.upon(M_rx_claimed, enter=S3B, outputs=[])
S3B.upon(M_send, enter=S3B, outputs=[queue, tx_add])
S3B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close])
S3A.upon(connected, enter=S3B, outputs=[RC_tx_open, drain])
S3A.upon(add_message, enter=S3A, outputs=[queue])
S3B.upon(lost, enter=S3A, outputs=[])
S3B.upon(rx_message_theirs, enter=S4B, outputs=[RC_tx_release_and_accept])
S3B.upon(rx_message_ours, enter=S3B, outputs=[dequeue])
S3B.upon(rx_claimed, enter=S3B, outputs=[])
S3B.upon(add_message, enter=S3B, outputs=[queue, RC_tx_add])
S4A.upon(M_connected, enter=S4B,
outputs=[tx_open, tx_add_queued, tx_release])
S4A.upon(M_send, enter=S4A, outputs=[queue])
S4A.upon(M_stop, enter=SrcA, outputs=[])
S4B.upon(M_lost, enter=S4A, outputs=[])
S4B.upon(M_send, enter=S4B, outputs=[queue, tx_add])
S4B.upon(M_rx_msg_from_them, enter=S4B, outputs=[process_msg_from_them])
S4B.upon(M_rx_msg_from_me, enter=S4B, outputs=[dequeue])
S4B.upon(M_rx_released, enter=S5B, outputs=[])
S4B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close])
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])
S4B.upon(rx_message_theirs, enter=S4B, outputs=[accept])
S4B.upon(rx_message_ours, enter=S4B, outputs=[dequeue])
S4B.upon(rx_released, enter=S5B, outputs=[])
S5A.upon(M_connected, enter=S5B, outputs=[tx_open, tx_add_queued])
S5A.upon(M_send, enter=S5A, outputs=[queue])
S5A.upon(M_stop, enter=ScA, outputs=[])
S5B.upon(M_lost, enter=S5A, outputs=[])
S5B.upon(M_send, enter=S5B, outputs=[queue, tx_add])
S5B.upon(M_rx_msg_from_them, enter=S5B, outputs=[process_msg_from_them])
S5B.upon(M_rx_msg_from_me, enter=S5B, outputs=[dequeue])
S5B.upon(M_stop, enter=ScB, outputs=[tx_close])
S5A.upon(connected, enter=S5B, outputs=[RC_tx_open, drain])
S5A.upon(add_message, enter=S5A, outputs=[queue])
S5B.upon(lost, enter=S5A, outputs=[])
S5B.upon(add_message, enter=S5B, outputs=[queue, RC_tx_add])
S5B.upon(rx_message_theirs, enter=S5B, outputs=[accept])
S5B.upon(rx_message_ours, enter=S5B, outputs=[dequeue])
SrcA.upon(M_connected, enter=SrcB, outputs=[tx_release, tx_close])
SrcB.upon(M_lost, enter=SrcA, outputs=[])
SrcB.upon(M_rx_closed, enter=SrB, outputs=[])
SrcB.upon(M_rx_released, enter=ScB, outputs=[])
if True:
S0A.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop])
S0B.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop])
S1A.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop])
S2A.upon(close, enter=SrA, outputs=[record_mood])
S2B.upon(close, enter=SrB, outputs=[record_mood_and_RC_tx_release])
S3A.upon(close, enter=SrcA, outputs=[record_mood])
S3B.upon(close, enter=SrcB,
outputs=[record_mood_and_RC_tx_release_and_RC_tx_close])
S4A.upon(close, enter=SrcA, outputs=[record_mood])
S4B.upon(close, enter=SrcB,
outputs=[record_mood_and_RC_tx_release_and_RC_tx_close])
S5A.upon(close, enter=ScA, outputs=[record_mood])
S5B.upon(close, enter=ScB, outputs=[record_mood_and_RC_tx_close])
SrB.upon(M_lost, enter=SrA, outputs=[])
SrA.upon(M_connected, enter=SrB, outputs=[tx_release])
SrB.upon(M_rx_released, enter=SsB, outputs=[C_stop])
SrcA.upon(connected, enter=SrcB, outputs=[RC_tx_release, RC_tx_close])
SrcB.upon(lost, enter=SrcA, outputs=[])
SrcB.upon(rx_closed, enter=SrB, outputs=[])
SrcB.upon(rx_released, enter=ScB, outputs=[])
ScB.upon(M_lost, enter=ScA, outputs=[])
ScB.upon(M_rx_closed, enter=SsB, outputs=[C_stop])
ScA.upon(M_connected, enter=ScB, outputs=[tx_close])
SrB.upon(lost, enter=SrA, outputs=[])
SrA.upon(connected, enter=SrB, outputs=[RC_tx_release])
SrB.upon(rx_released, enter=SsB, outputs=[RC_stop])
SsB.upon(M_stopped, enter=Ss, outputs=[WM_stopped])
ScB.upon(lost, enter=ScA, outputs=[])
ScB.upon(rx_closed, enter=SsB, outputs=[RC_stop])
ScA.upon(connected, enter=ScB, outputs=[RC_tx_close])
SsB.upon(stopped, enter=Ss, outputs=[W_closed])

View File

@ -1,8 +1,15 @@
from zope.interface import implementer
from automat import MethodicalMachine
from . import _interfaces
class NameplateListingMachine(object):
@implementer(_interfaces.INameplateListing)
class NameplateListing(object):
m = MethodicalMachine()
def __init__(self):
self._list_nameplate_waiters = []
pass
def wire(self, rendezvous_connector, code):
self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
self._C = _interfaces.ICode(code)
# Ideally, each API request would spawn a new "list_nameplates" message
# to the server, so the response would be maximally fresh, but that would
@ -14,40 +21,45 @@ class NameplateListingMachine(object):
# request arrives, both requests will be satisfied by the same response.
@m.state(initial=True)
def idle(self): pass
def S0A_idle_disconnected(self): pass
@m.state()
def requesting(self): pass
def S1A_wanting_disconnected(self): pass
@m.state()
def S0B_idle_connected(self): pass
@m.state()
def S1B_wanting_connected(self): pass
@m.input()
def list_nameplates(self): pass # returns Deferred
def connected(self): pass
@m.input()
def response(self, message): pass
def lost(self): pass
@m.input()
def refresh_nameplates(self): pass
@m.input()
def rx_nameplates(self, message): pass
@m.output()
def add_deferred(self):
d = defer.Deferred()
self._list_nameplate_waiters.append(d)
return d
def RC_tx_list(self):
self._RC.tx_list()
@m.output()
def send_request(self):
self._connection.send_command("list")
@m.output()
def distribute_response(self, message):
nameplates = parse(message)
waiters = self._list_nameplate_waiters
self._list_nameplate_waiters = []
for d in waiters:
d.callback(nameplates)
def C_got_nameplates(self, message):
self._C.got_nameplates(message["nameplates"])
idle.upon(list_nameplates, enter=requesting,
outputs=[add_deferred, send_request],
collector=lambda outs: outs[0])
idle.upon(response, enter=idle, outputs=[])
requesting.upon(list_nameplates, enter=requesting,
outputs=[add_deferred],
collector=lambda outs: outs[0])
requesting.upon(response, enter=idle, outputs=[distribute_response])
S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[])
S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[])
# nlm._connection = c = Connection(ws)
# nlm.list_nameplates().addCallback(display_completions)
# c.register_dispatch("nameplates", nlm.response)
S0A_idle_disconnected.upon(refresh_nameplates,
enter=S1A_wanting_disconnected, outputs=[])
S1A_wanting_disconnected.upon(refresh_nameplates,
enter=S1A_wanting_disconnected, outputs=[])
S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected,
outputs=[RC_tx_list])
S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected,
outputs=[RC_tx_list])
S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected,
outputs=[C_got_nameplates])
S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[])
S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected,
outputs=[RC_tx_list])
S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected,
outputs=[C_got_nameplates])

55
src/wormhole/_order.py Normal file
View File

@ -0,0 +1,55 @@
from zope.interface import implementer
from automat import MethodicalMachine
from . import _interfaces
@implementer(_interfaces.IOrder)
class Order(object):
m = MethodicalMachine()
def __init__(self, side, timing):
self._side = side
self._timing = timing
self._key = None
self._queue = []
def wire(self, key, receive):
self._K = _interfaces.IKey(key)
self._R = _interfaces.IReceive(receive)
@m.state(initial=True)
def S0_no_pake(self): pass
@m.state(terminal=True)
def S1_yes_pake(self): pass
def got_message(self, phase, payload):
if phase == "pake":
self.got_pake(phase, payload)
else:
self.got_non_pake(phase, payload)
@m.input()
def got_pake(self, phase, payload): pass
@m.input()
def got_non_pake(self, phase, payload): pass
@m.output()
def queue(self, phase, payload):
self._queue.append((phase, payload))
@m.output()
def notify_key(self, phase, payload):
self._K.got_pake(payload)
@m.output()
def drain(self, phase, payload):
del phase
del payload
for (phase, payload) in self._queue:
self._deliver(phase, payload)
self._queue[:] = []
@m.output()
def deliver(self, phase, payload):
self._deliver(phase, payload)
def _deliver(self, phase, payload):
self._R.got_message(phase, payload)
S0_no_pake.upon(got_non_pake, enter=S0_no_pake, outputs=[queue])
S0_no_pake.upon(got_pake, enter=S1_yes_pake, outputs=[notify_key, drain])
S1_yes_pake.upon(got_non_pake, enter=S1_yes_pake, outputs=[deliver])

72
src/wormhole/_receive.py Normal file
View File

@ -0,0 +1,72 @@
from zope.interface import implementer
from automat import MethodicalMachine
from . import _interfaces
from ._key import derive_phase_key, decrypt_data, CryptoError
@implementer(_interfaces.IReceive)
class Receive(object):
m = MethodicalMachine()
def __init__(self, side, timing):
self._side = side
self._timing = timing
self._key = None
def wire(self, wormhole, key, send):
self._W = _interfaces.IWormhole(wormhole)
self._K = _interfaces.IKey(key)
self._S = _interfaces.ISend(send)
@m.state(initial=True)
def S0_unknown_key(self): pass
@m.state()
def S1_unverified_key(self): pass
@m.state()
def S2_verified_key(self): pass
@m.state(terminal=True)
def S3_scared(self): pass
def got_message(self, phase, payload):
assert self._key
data_key = derive_phase_key(self._side, phase)
try:
plaintext = decrypt_data(data_key, body)
except CryptoError:
self.got_message_bad()
return
self.got_message_good(phase, plaintext)
@m.input()
def got_key(self, key): pass
@m.input()
def got_message_good(self, phase, plaintext): pass
@m.input()
def got_message_bad(self): pass
@m.output()
def record_key(self, key):
self._key = key
@m.output()
def S_got_verified_key(self, phase, plaintext):
assert self._key
self._S.got_verified_key(self._key)
@m.output()
def W_happy(self, phase, plaintext):
self._W.happy()
@m.output()
def W_got_message(self, phase, plaintext):
self._W.got_message(phase, plaintext)
@m.output()
def W_scared(self):
self._W.scared()
S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key])
S1_unverified_key.upon(got_message_good, enter=S2_verified_key,
outputs=[S_got_verified_key, W_happy, W_got_message])
S1_unverified_key.upon(got_message_bad, enter=S3_scared,
outputs=[W_scared])
S2_verified_key.upon(got_message_bad, enter=S3_scared,
outputs=[W_scared])
S2_verified_key.upon(got_message_good, enter=S2_verified_key,
outputs=[W_got_message])
S3_scared.upon(got_message_good, enter=S3_scared, outputs=[])
S3_scared.upon(got_message_bad, enter=S3_scared, outputs=[])

View File

@ -0,0 +1,39 @@
from zope.interface import implementer
from twisted.application import service
from . import _interfaces
@implementer(_interfaces.IRendezvousConnector)
class RendezvousConnector(service.MultiService, object):
def __init__(self, journal, timing):
self._journal = journal
self._timing = timing
def wire(self, mailbox, code, nameplate_lister):
self._M = _interfaces.IMailbox(mailbox)
self._C = _interfaces.ICode(code)
self._NL = _interfaces.INameplateListing(nameplate_lister)
# from Mailbox
def tx_claim(self):
pass
def tx_open(self):
pass
def tx_add(self, x):
pass
def tx_release(self):
pass
def tx_close(self, mood):
pass
def stop(self):
pass
# from NameplateLister
def tx_list(self):
pass
# from Code
def tx_allocate(self):
pass
# record, message, payload, packet, bundle, ciphertext, plaintext

View File

@ -1,28 +1,20 @@
from zope.interface import implementer
from automat import MethodicalMachine
from spake2 import SPAKE2_Symmetric
from hkdf import Hkdf
from nacl.secret import SecretBox
from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes)
from . import _interfaces
from .util import hexstr_to_bytes
def HKDF(skm, outlen, salt=None, CTXinfo=b""):
return Hkdf(salt, skm).expand(CTXinfo, outlen)
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))
return HKDF(key, length, CTXinfo=purpose)
class SendMachine(object):
@implementer(_interfaces.ISend)
class Send(object):
m = MethodicalMachine()
def __init__(self, timing):
def __init__(self, side, timing):
self._side = side
self._timing = timing
def set_mailbox(self, mailbox):
self._mailbox = mailbox
def wire(self, mailbox):
self._M = _interfaces.IMailbox(mailbox)
@m.state(initial=True)
def S0_no_key(self): pass
@m.state()
@m.state(terminal=True)
def S1_verified_key(self): pass
def got_pake(self, payload):
@ -47,6 +39,7 @@ class SendMachine(object):
del key
for (phase, payload) in self._queue:
self._encrypt_and_send(phase, payload)
self._queue[:] = []
@m.output()
def deliver(self, phase, payload):
self._encrypt_and_send(phase, payload)
@ -54,7 +47,7 @@ class SendMachine(object):
def _encrypt_and_send(self, phase, payload):
data_key = self._derive_phase_key(self._side, phase)
encrypted = self._encrypt_data(data_key, plaintext)
self._mailbox.add_message(phase, encrypted)
self._M.add_message(phase, encrypted)
S0_no_key.upon(send, enter=S0_no_key, outputs=[queue])
S0_no_key.upon(got_verified_key, enter=S1_verified_key,

View File

@ -1,14 +1,39 @@
from zope.interface import implementer
from automat import MethodicalMachine
from . import _interfaces
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 ._nameplate import NameplateListing
from ._code import Code
@implementer(_interfaces.IWormhole)
class Wormhole:
m = MethodicalMachine()
def __init__(self, ws_url, reactor):
self._relay_client = WSRelayClient(self, ws_url, reactor)
# This records all the messages we want the relay to have. Each time
# we establish a connection, we'll send them all (and the relay
# server will filter out duplicates). If we add any while a
# connection is established, we'll send the new ones.
self._outbound_messages = []
def __init__(self, side, reactor, timing):
self._reactor = reactor
self._M = Mailbox(side)
self._S = Send(side, timing)
self._O = Order(side, timing)
self._K = Key(timing)
self._R = Receive(side, timing)
self._RC = RendezvousConnector(side, timing, reactor)
self._NL = NameplateListing()
self._C = Code(timing)
self._M.wire(self, self._RC, self._O)
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._K, self._S)
self._RC.wire(self._M, self._C, self._NL)
self._NL.wire(self._RC, self._C)
self._C.wire(self, self._RC, self._NL)
# these methods are called from outside
def start(self):
@ -16,130 +41,110 @@ class Wormhole:
# and these are the state-machine transition functions, which don't take
# args
@m.state(initial=True)
def S0_empty(self): pass
@m.state()
def closed(initial=True): pass
def S1_lonely(self): pass
@m.state()
def know_code_not_mailbox(): pass
def S2_happy(self): pass
@m.state()
def know_code_and_mailbox(): pass # no longer need nameplate
@m.state()
def waiting_first_msg(): pass # key is established, want any message
@m.state()
def processing_version(): pass
@m.state()
def processing_phase(): pass
@m.state()
def open(): pass # key is verified, can post app messages
def S3_closing(self): pass
@m.state(terminal=True)
def failed(): pass
def S4_closed(self): pass
# from the Application, or some sort of top-level shim
@m.input()
def deliver_message(self, message): pass
def send(self, phase, message): pass
@m.input()
def close(self): pass
def w_set_seed(self, code, mailbox):
"""Call w_set_seed when we sprout a Wormhole Seed, which
contains both the code and the mailbox"""
self.w_set_code(code)
self.w_set_mailbox(mailbox)
# from Code (which may be provoked by the Application)
@m.input()
def set_code(self, code): pass
# Key sends (got_verifier, scared)
# Receive sends (got_message, happy, scared)
@m.input()
def w_set_code(self, code):
"""Call w_set_code when you learn the code, probably because the user
typed it in."""
def happy(self): pass
@m.input()
def w_set_mailbox(self, mailbox):
"""Call w_set_mailbox() when you learn the mailbox id, from the
response to claim_nameplate"""
pass
def scared(self): pass
def got_message(self, phase, plaintext):
if phase == "version":
self.got_version(plaintext)
else:
self.got_phase(phase, plaintext)
@m.input()
def got_version(self, version): pass
@m.input()
def got_phase(self, phase, plaintext): pass
@m.input()
def got_verifier(self, verifier): pass
# Mailbox sends closed
@m.input()
def closed(self): pass
@m.input()
def rx_pake(self, pake): pass # reponse["message"][phase=pake]
@m.input()
def rx_version(self, version): # response["message"][phase=version]
pass
@m.input()
def verify_good(self, verifier): pass
@m.input()
def verify_bad(self, f): pass
@m.input()
def rx_phase(self, message): pass
@m.input()
def phase_good(self, message): pass
@m.input()
def phase_bad(self, f): pass
@m.output()
def compute_and_post_pake(self, code):
self._code = code
self._pake = compute(code)
self._post(pake=self._pake)
self._ws_send_command("add", phase="pake", body=XXX(pake))
@m.output()
def set_mailbox(self, mailbox):
self._mailbox = mailbox
@m.output()
def set_seed(self, code, mailbox):
self._code = code
self._mailbox = mailbox
def got_code(self, code):
nameplate = code.split("-")[0]
self._M.set_nameplate(nameplate)
self._K.set_code(code)
@m.output()
def process_version(self, version): # response["message"][phase=version]
their_verifier = com
if OK:
self.verify_good(verifier)
else:
self.verify_bad(f)
pass
@m.output()
def notify_verified(self, verifier):
for d in self._verify_waiters:
d.callback(verifier)
@m.output()
def notify_failed(self, f):
for d in self._verify_waiters:
d.errback(f)
def S_send(self, phase, message):
self._S.send(phase, message)
@m.output()
def process_phase(self, message): # response["message"][phase=version]
their_verifier = com
if OK:
self.verify_good(verifier)
else:
self.verify_bad(f)
pass
def close_scared(self):
self._M.close("scary")
@m.output()
def close_lonely(self):
self._M.close("lonely")
@m.output()
def close_happy(self):
self._M.close("happy")
@m.output()
def post_inbound(self, message):
pass
def A_received(self, phase, plaintext):
self._A.received(phase, plaintext)
@m.output()
def A_got_verifier(self, verifier):
self._A.got_verifier(verifier)
@m.output()
def deliver_message(self, message):
self._qc.deliver_message(message)
def A_closed(self):
result = "???"
self._A.closed(result)
@m.output()
def compute_key_and_post_version(self, pake):
self._key = x
self._verifier = x
plaintext = dict_to_bytes(self._my_versions)
phase = "version"
data_key = self._derive_phase_key(self._side, phase)
encrypted = self._encrypt_data(data_key, plaintext)
self._msg_send(phase, encrypted)
S0_empty.upon(send, enter=S0_empty, outputs=[S_send])
S0_empty.upon(set_code, enter=S1_lonely, outputs=[got_code])
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=[A_got_verifier])
S2_happy.upon(got_phase, enter=S2_happy, outputs=[A_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])
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=[A_closed])
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=[])
closed.upon(w_set_code, enter=know_code_not_mailbox,
outputs=[compute_and_post_pake])
know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox,
outputs=[set_mailbox])
know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg,
outputs=[compute_key_and_post_version])
waiting_first_msg.upon(rx_version, enter=processing_version,
outputs=[process_version])
processing_version.upon(verify_good, enter=open, outputs=[notify_verified])
processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed])
open.upon(rx_phase, enter=processing_phase, outputs=[process_phase])
processing_phase.upon(phase_good, enter=open, outputs=[post_inbound])
processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed])