diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index 6bf3942..2fe7bef 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -63,7 +63,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + label="rx_welcome -> process\nsend -> S.send\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/docs/state-machines/key.dot b/docs/state-machines/key.dot index 5e4e23c..b6d49f8 100644 --- a/docs/state-machines/key.dot +++ b/docs/state-machines/key.dot @@ -35,7 +35,7 @@ digraph { S1 -> P1_compute [label="got_pake\npake good"] #S1 -> P_mood_lonely [label="close"] - P1_compute [label="compute_key\nM.add_message(version)\nW.got_verifier\nR.got_key" shape="box"] + P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nB.got_verifier\nR.got_key" shape="box"] P1_compute -> S2 S2 [label="S2: know_key" color="green"] diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index e3d48d8..a4cd0da 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -21,7 +21,7 @@ digraph { Wormhole -> Boss [style="dashed" label="allocate_code\ninput_code\nset_code\nsend\nclose\n(once)"] #Wormhole -> Boss [color="blue"] - Boss -> Wormhole [style="dashed" label="got_code\ngot_verifier\nreceived (seq)\nclosed\n(once)"] + Boss -> Wormhole [style="dashed" label="got_code\ngot_key\ngot_verifier\nreceived (seq)\nclosed\n(once)"] #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] @@ -33,7 +33,7 @@ digraph { #Boss -> Mailbox [color="blue"] Mailbox -> Order [style="dashed" label="got_message (once)"] Boss -> Key [style="dashed" label="got_code"] - Key -> Boss [style="dashed" label="got_verifier\nscared"] + Key -> Boss [style="dashed" label="got_key\ngot_verifier\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] #Boss -> Key [color="blue"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 9aa73fd..2ff775c 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -135,7 +135,7 @@ class Boss(object): @m.input() def got_code(self, code): pass - # Key sends (got_verifier, scared) + # Key sends (got_key, got_verifier, scared) # Receive sends (got_message, happy, scared) @m.input() def happy(self): pass @@ -158,6 +158,8 @@ class Boss(object): @m.input() def got_phase(self, phase, plaintext): pass @m.input() + def got_key(self, key): pass + @m.input() def got_verifier(self, verifier): pass # Terminator sends closed @@ -205,6 +207,9 @@ class Boss(object): self._T.close("happy") @m.output() + def W_got_key(self, key): + self._W.got_key(key) + @m.output() def W_got_verifier(self, verifier): self._W.got_verifier(verifier) @m.output() @@ -238,6 +243,7 @@ class Boss(object): S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) + S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key]) S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error]) S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index e5f54fd..6370df2 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -110,6 +110,7 @@ class Key(object): assert isinstance(msg2, type(b"")) with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) + self._B.got_key(key) self._B.got_verifier(derive_key(key, b"wormhole:verifier")) phase = "version" data_key = derive_phase_key(key, self._side, phase) diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index d13aa87..865b5e2 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -50,3 +50,6 @@ class TransferError(WormholeError): class NoTorError(WormholeError): """--tor was requested, but 'txtorcon' is not installed.""" + +class NoKeyError(WormholeError): + """w.derive_key() was called before got_verifier() fired""" diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 661d71b..a5c3730 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -147,7 +147,7 @@ class Key(unittest.TestCase): def build(self): events = [] k = _key.Key(u"appid", u"side", timing.DebugTiming()) - b = Dummy("b", events, IBoss, "scared", "got_verifier") + b = Dummy("b", events, IBoss, "scared", "got_key", "got_verifier") m = Dummy("m", events, IMailbox, "add_message") r = Dummy("r", events, IReceive, "got_key") k.wire(b, m, r) @@ -168,10 +168,11 @@ class Key(unittest.TestCase): key2 = sp.finish(msg1_bytes) msg2 = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg2_bytes)}) k.got_pake(msg2) - self.assertEqual(len(events), 3) - self.assertEqual(events[0][0], "b.got_verifier") - self.assertEqual(events[1][:2], ("m.add_message", "version")) - self.assertEqual(events[2], ("r.got_key", key2)) + self.assertEqual(len(events), 4, events) + self.assertEqual(events[0], ("b.got_key", key2)) + self.assertEqual(events[1][0], "b.got_verifier") + self.assertEqual(events[2][:2], ("m.add_message", "version")) + self.assertEqual(events[3], ("r.got_key", key2)) def test_bad(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 15449c6..aed3931 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -9,6 +9,9 @@ from .util import bytes_to_hexstr from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss +from ._key import derive_key +from .errors import NoKeyError +from .util import to_bytes # We can provide different APIs to different apps: # * Deferreds @@ -39,6 +42,9 @@ def _log(client_name, machine_name, old_state, input, new_state): class _DelegatedWormhole(object): _delegate = attrib() + def __attrs_post_init__(self): + self._key = None + def _set_boss(self, boss): self._boss = boss @@ -59,6 +65,18 @@ class _DelegatedWormhole(object): def send(self, plaintext): self._boss.send(plaintext) + + def derive_key(self, purpose, length): + """Derive a new key from the established wormhole channel for some + other purpose. This is a deterministic randomized function of the + session key and the 'purpose' string (unicode/py3-string). This + cannot be called until when_verifier() has fired, nor after close() + was called. + """ + if not isinstance(purpose, type("")): raise TypeError(type(purpose)) + if not self._key: raise NoKeyError() + return derive_key(self._key, to_bytes(purpose), length) + def close(self): self._boss.close() @@ -69,6 +87,8 @@ class _DelegatedWormhole(object): # from below def got_code(self, code): self._delegate.wormhole_got_code(code) + def got_key(self, key): + self._key = key # for derive_key() def got_verifier(self, verifier): self._delegate.wormhole_got_verifier(verifier) def received(self, plaintext): @@ -84,6 +104,7 @@ class _DeferredWormhole(object): def __init__(self): self._code = None self._code_observers = [] + self._key = None self._verifier = None self._verifier_observers = [] self._received_data = [] @@ -125,6 +146,18 @@ class _DeferredWormhole(object): # no .serialize in Deferred-mode def send(self, plaintext): self._boss.send(plaintext) + + def derive_key(self, purpose, length): + """Derive a new key from the established wormhole channel for some + other purpose. This is a deterministic randomized function of the + session key and the 'purpose' string (unicode/py3-string). This + cannot be called until when_verifier() has fired, nor after close() + was called. + """ + if not isinstance(purpose, type("")): raise TypeError(type(purpose)) + if not self._key: raise NoKeyError() + return derive_key(self._key, to_bytes(purpose), length) + def close(self): # fails with WormholeError unless we established a connection # (state=="happy"). Fails with WrongPasswordError (a subclass of @@ -144,6 +177,8 @@ class _DeferredWormhole(object): for d in self._code_observers: d.callback(code) self._code_observers[:] = [] + def got_key(self, key): + self._key = key # for derive_key() def got_verifier(self, verifier): self._verifier = verifier for d in self._verifier_observers: