From bf43dae2ad1221ad8a56b4dae4aeecc2c38cc200 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 6 Oct 2015 20:39:20 -0700 Subject: [PATCH] add multiple phases, change key-derivation strings Because of the key-derivation change, clients will not be compatible across this commit. --- docs/api.md | 36 +++++++++++++----- src/wormhole/blocking/transcribe.py | 26 +++++++------ src/wormhole/test/test_blocking.py | 57 +++++++++++++++++++++++++++++ src/wormhole/test/test_twisted.py | 56 ++++++++++++++++++++++++++++ src/wormhole/twisted/transcribe.py | 26 +++++++------ 5 files changed, 170 insertions(+), 31 deletions(-) diff --git a/docs/api.md b/docs/api.md index 5d9826f..e443c41 100644 --- a/docs/api.md +++ b/docs/api.md @@ -45,15 +45,16 @@ Transit class currently distinguishes "Sender" from "Receiver", so the programs on each side must have some way to decide (ahead of time) which is which. -Each side gets to do one `send_data()` call and one `get_data()` call. -`get_data` will wait until the other side has done `send_data`, so the -application developer must be careful to avoid deadlocks (don't get before -you send on both sides in the same protocol). When both sides are done, they -must call `close()`, to let the library know that the connection is complete -and it can deallocate the channel. If you forget to call `close()`, the -server will not free the channel, and other users will suffer longer -invitation codes as a result. To encourage `close()`, the library will log an -error if a Wormhole object is destroyed before being closed. +Each side gets to do one `send_data()` call and one `get_data()` call per +phase (see below). `get_data` will wait until the other side has done +`send_data`, so the application developer must be careful to avoid deadlocks +(don't get before you send on both sides in the same protocol). When both +sides are done, they must call `close()`, to let the library know that the +connection is complete and it can deallocate the channel. If you forget to +call `close()`, the server will not free the channel, and other users will +suffer longer invitation codes as a result. To encourage `close()`, the +library will log an error if a Wormhole object is destroyed before being +closed. ## Examples @@ -123,6 +124,23 @@ Note that the Twisted-form `close()` accepts (and returns) an optional argument, so you can use `d.addCallback(w.close)` instead of `d.addCallback(lambda _: w.close())`. +## Phases + +If necessary, more than one message can be exchanged through the relay +server. It is not meant as a long-term communication channel, but some +protocols work better if they can exchange an initial pair of messages +(perhaps offering some set of negotiable capabilities), and then follow up +with a second pair (to reveal the results of the negotiation). + +To support this, `send_data()/get_data()` accept a "phase" argument: an +arbitrary (unicode) string. It must match the other side: calling +`send_data(data, phase=u"offer")` on one side will deliver that data to +`get_data(phase=u"offer")` on the other. + +It is a UsageError to call `send_data()` or `get_data()` twice with the same +phase name. The relay server may limit the number of phases that may be +exchanged, however it will always allow at least two. + ## Verifier You can call `w.get_verifier()` before `send_data()/get_data()`: this will diff --git a/src/wormhole/blocking/transcribe.py b/src/wormhole/blocking/transcribe.py index 75f8302..bcfaea6 100644 --- a/src/wormhole/blocking/transcribe.py +++ b/src/wormhole/blocking/transcribe.py @@ -157,8 +157,8 @@ class Wormhole: self.code = None self.key = None self.verifier = None - self._sent_data = False - self._got_data = False + self._sent_data = set() # phases + self._got_data = set() def handle_welcome(self, welcome): if ("motd" in welcome and @@ -245,7 +245,7 @@ class Wormhole: self.channel.send(u"pake", self.msg1) pake_msg = self.channel.get(u"pake") self.key = self.sp.finish(pake_msg) - self.verifier = self.derive_key(self._appid+u":Verifier") + self.verifier = self.derive_key(u"wormhole:verifier") def get_verifier(self): if self.code is None: raise UsageError @@ -253,27 +253,31 @@ class Wormhole: self._get_key() return self.verifier - def send_data(self, outbound_data): - if self._sent_data: raise UsageError # only call this once + def send_data(self, outbound_data, phase=u"data"): if not isinstance(outbound_data, type(b"")): raise UsageError + if not isinstance(phase, type(u"")): raise UsageError + if phase in self._sent_data: raise UsageError # only call this once if self.code is None: raise UsageError if self.channel is None: raise UsageError # Without predefined roles, we can't derive predictably unique keys # for each side, so we use the same key for both. We use random # nonces to keep the messages distinct, and the Channel automatically # ignores reflections. + self._sent_data.add(phase) self._get_key() - data_key = self.derive_key(u"data-key") + data_key = self.derive_key(u"wormhole:phase:%s" % phase) outbound_encrypted = self._encrypt_data(data_key, outbound_data) - self.channel.send(u"data", outbound_encrypted) + self.channel.send(phase, outbound_encrypted) - def get_data(self): - if self._got_data: raise UsageError # only call this once + def get_data(self, phase=u"data"): + if not isinstance(phase, type(u"")): raise UsageError + if phase in self._got_data: raise UsageError # only call this once if self.code is None: raise UsageError if self.channel is None: raise UsageError + self._got_data.add(phase) self._get_key() - data_key = self.derive_key(u"data-key") - inbound_encrypted = self.channel.get(u"data") + data_key = self.derive_key(u"wormhole:phase:%s" % phase) + inbound_encrypted = self.channel.get(phase) try: inbound_data = self._decrypt_data(data_key, inbound_encrypted) return inbound_data diff --git a/src/wormhole/test/test_blocking.py b/src/wormhole/test/test_blocking.py index 6463d1a..1e7f079 100644 --- a/src/wormhole/test/test_blocking.py +++ b/src/wormhole/test/test_blocking.py @@ -154,6 +154,34 @@ class Blocking(ServerBase, unittest.TestCase): d.addCallback(_done) return d + def test_phases(self): + w1 = BlockingWormhole(APPID, self.relayurl) + w2 = BlockingWormhole(APPID, self.relayurl) + w1.set_code(u"123-purple-elephant") + w2.set_code(u"123-purple-elephant") + d = self.doBoth([w1.send_data, b"data1", u"p1"], + [w2.send_data, b"data2", u"p1"]) + d.addCallback(lambda _: + self.doBoth([w1.send_data, b"data3", u"p2"], + [w2.send_data, b"data4", u"p2"])) + d.addCallback(lambda _: + self.doBoth([w1.get_data, u"p2"], + [w2.get_data, u"p1"])) + def _got_1(dl): + (dataX, dataY) = dl + self.assertEqual(dataX, b"data4") + self.assertEqual(dataY, b"data1") + return self.doBoth([w1.get_data, u"p1"], + [w2.get_data, u"p2"]) + d.addCallback(_got_1) + def _got_2(dl): + (dataX, dataY) = dl + self.assertEqual(dataX, b"data2") + self.assertEqual(dataY, b"data3") + return self.doBoth([w1.close], [w2.close]) + d.addCallback(_got_2) + return d + def test_verifier(self): w1 = BlockingWormhole(APPID, self.relayurl) w2 = BlockingWormhole(APPID, self.relayurl) @@ -212,6 +240,35 @@ class Blocking(ServerBase, unittest.TestCase): d.addCallback(_done) return d + def test_repeat_phases(self): + w1 = BlockingWormhole(APPID, self.relayurl) + w1.set_code(u"123-purple-elephant") + w2 = BlockingWormhole(APPID, self.relayurl) + w2.set_code(u"123-purple-elephant") + # we must let them establish a key before we can send data + d = self.doBoth([w1.get_verifier], [w2.get_verifier]) + d.addCallback(lambda _: + deferToThread(w1.send_data, b"data1", phase=u"1")) + def _sent(res): + # you can't send twice to the same phase + self.assertRaises(UsageError, w1.send_data, b"data1", phase=u"1") + # but you can send to a different one + return deferToThread(w1.send_data, b"data2", phase=u"2") + d.addCallback(_sent) + d.addCallback(lambda _: deferToThread(w2.get_data, phase=u"1")) + def _got1(res): + self.failUnlessEqual(res, b"data1") + # and you can't read twice from the same phase + self.assertRaises(UsageError, w2.get_data, phase=u"1") + # but you can read from a different one + return deferToThread(w2.get_data, phase=u"2") + d.addCallback(_got1) + def _got2(res): + self.failUnlessEqual(res, b"data2") + return self.doBoth([w1.close], [w2.close]) + d.addCallback(_got2) + return d + def test_serialize(self): w1 = BlockingWormhole(APPID, self.relayurl) self.assertRaises(UsageError, w1.serialize) # too early diff --git a/src/wormhole/test/test_twisted.py b/src/wormhole/test/test_twisted.py index 70ac2ae..144cd1e 100644 --- a/src/wormhole/test/test_twisted.py +++ b/src/wormhole/test/test_twisted.py @@ -142,6 +142,34 @@ class Basic(ServerBase, unittest.TestCase): d.addCallback(_done) return d + def test_phases(self): + w1 = Wormhole(APPID, self.relayurl) + w2 = Wormhole(APPID, self.relayurl) + w1.set_code(u"123-purple-elephant") + w2.set_code(u"123-purple-elephant") + d = self.doBoth(w1.send_data(b"data1", u"p1"), + w2.send_data(b"data2", u"p1")) + d.addCallback(lambda _: + self.doBoth(w1.send_data(b"data3", u"p2"), + w2.send_data(b"data4", u"p2"))) + d.addCallback(lambda _: + self.doBoth(w1.get_data(u"p2"), + w2.get_data(u"p1"))) + def _got_1(dl): + (dataX, dataY) = dl + self.assertEqual(dataX, b"data4") + self.assertEqual(dataY, b"data1") + return self.doBoth(w1.get_data(u"p1"), + w2.get_data(u"p2")) + d.addCallback(_got_1) + def _got_2(dl): + (dataX, dataY) = dl + self.assertEqual(dataX, b"data2") + self.assertEqual(dataY, b"data3") + return self.doBoth(w1.close(), w2.close()) + d.addCallback(_got_2) + return d + def test_verifier(self): w1 = Wormhole(APPID, self.relayurl) w2 = Wormhole(APPID, self.relayurl) @@ -199,6 +227,34 @@ class Basic(ServerBase, unittest.TestCase): d.addCallback(_got_code) return d + def test_repeat_phases(self): + w1 = Wormhole(APPID, self.relayurl) + w1.set_code(u"123-purple-elephant") + w2 = Wormhole(APPID, self.relayurl) + w2.set_code(u"123-purple-elephant") + # we must let them establish a key before we can send data + d = self.doBoth(w1.get_verifier(), w2.get_verifier()) + d.addCallback(lambda _: w1.send_data(b"data1", phase=u"1")) + def _sent(res): + # you can't send twice to the same phase + self.assertRaises(UsageError, w1.send_data, b"data1", phase=u"1") + # but you can send to a different one + return w1.send_data(b"data2", phase=u"2") + d.addCallback(_sent) + d.addCallback(lambda _: w2.get_data(phase=u"1")) + def _got1(res): + self.failUnlessEqual(res, b"data1") + # and you can't read twice from the same phase + self.assertRaises(UsageError, w2.get_data, phase=u"1") + # but you can read from a different one + return w2.get_data(phase=u"2") + d.addCallback(_got1) + def _got2(res): + self.failUnlessEqual(res, b"data2") + return self.doBoth(w1.close(), w2.close()) + d.addCallback(_got2) + return d + def test_serialize(self): w1 = Wormhole(APPID, self.relayurl) self.assertRaises(UsageError, w1.serialize) # too early diff --git a/src/wormhole/twisted/transcribe.py b/src/wormhole/twisted/transcribe.py index 675ad5e..a039c52 100644 --- a/src/wormhole/twisted/transcribe.py +++ b/src/wormhole/twisted/transcribe.py @@ -186,8 +186,8 @@ class Wormhole: self.code = None self.key = None self._started_get_code = False - self._sent_data = False - self._got_data = False + self._sent_data = set() # phases + self._got_data = set() def _set_side(self, side): self._side = side @@ -312,7 +312,7 @@ class Wormhole: def _got_pake(pake_msg): key = self.sp.finish(pake_msg) self.key = key - self.verifier = self.derive_key(self._appid+u":Verifier") + self.verifier = self.derive_key(u"wormhole:verifier") return key d.addCallback(_got_pake) return d @@ -323,31 +323,35 @@ class Wormhole: d.addCallback(lambda _: self.verifier) return d - def send_data(self, outbound_data): - if self._sent_data: raise UsageError # only call this once + def send_data(self, outbound_data, phase=u"data"): if not isinstance(outbound_data, type(b"")): raise UsageError + if not isinstance(phase, type(u"")): raise UsageError + if phase in self._sent_data: raise UsageError # only call this once if self.code is None: raise UsageError if self.channel is None: raise UsageError # Without predefined roles, we can't derive predictably unique keys # for each side, so we use the same key for both. We use random # nonces to keep the messages distinct, and the Channel automatically # ignores reflections. + self._sent_data.add(phase) d = self._get_key() def _send(key): - data_key = self.derive_key(u"data-key") + data_key = self.derive_key(u"wormhole:phase:%s" % phase) outbound_encrypted = self._encrypt_data(data_key, outbound_data) - return self.channel.send(u"data", outbound_encrypted) + return self.channel.send(phase, outbound_encrypted) d.addCallback(_send) return d - def get_data(self): - if self._got_data: raise UsageError # only call this once + def get_data(self, phase=u"data"): + if not isinstance(phase, type(u"")): raise UsageError + if phase in self._got_data: raise UsageError # only call this once if self.code is None: raise UsageError if self.channel is None: raise UsageError + self._got_data.add(phase) d = self._get_key() def _get(key): - data_key = self.derive_key(u"data-key") - d1 = self.channel.get(u"data") + data_key = self.derive_key(u"wormhole:phase:%s" % phase) + d1 = self.channel.get(phase) def _decrypt(inbound_encrypted): try: inbound_data = self._decrypt_data(data_key,