diff --git a/docs/machines.dot b/docs/machines.dot index a1df2e7..b87655a 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -73,6 +73,8 @@ digraph { ] Code -> Wormhole [style="dashed" label="set_code"] + App -> Code [style="dashed" + label="allocate\ninput\nset"] diff --git a/docs/nameplates.dot b/docs/nameplates.dot index 8bc165d..77b7e1e 100644 --- a/docs/nameplates.dot +++ b/docs/nameplates.dot @@ -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()"] } diff --git a/docs/wormhole.dot b/docs/wormhole.dot index 317ef59..2ce81a3 100644 --- a/docs/wormhole.dot +++ b/docs/wormhole.dot @@ -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"] diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py new file mode 100644 index 0000000..2ad6f9f --- /dev/null +++ b/src/wormhole/_code.py @@ -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]) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 735e8d3..5086571 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -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 diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py new file mode 100644 index 0000000..647dd2b --- /dev/null +++ b/src/wormhole/_interfaces.py @@ -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 diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 6a03970..7ce979e 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -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]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index f67bb5a..376393c 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -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]) diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 8014cfd..b6c1ed9 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -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]) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py new file mode 100644 index 0000000..f914130 --- /dev/null +++ b/src/wormhole/_order.py @@ -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]) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py new file mode 100644 index 0000000..a31c4f4 --- /dev/null +++ b/src/wormhole/_receive.py @@ -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=[]) + diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py new file mode 100644 index 0000000..ca8a0f7 --- /dev/null +++ b/src/wormhole/_rendezvous.py @@ -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 diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 8d2ac0f..8bfa791 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -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, diff --git a/src/wormhole/_wormhole.py b/src/wormhole/_wormhole.py index 697fab3..3c626dd 100644 --- a/src/wormhole/_wormhole.py +++ b/src/wormhole/_wormhole.py @@ -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])