add multiple phases, change key-derivation strings

Because of the key-derivation change, clients will not be compatible
across this commit.
This commit is contained in:
Brian Warner 2015-10-06 20:39:20 -07:00
parent d0a7da3a63
commit bf43dae2ad
5 changed files with 170 additions and 31 deletions

View File

@ -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 programs on each side must have some way to decide (ahead of time) which is
which. which.
Each side gets to do one `send_data()` call and one `get_data()` call. Each side gets to do one `send_data()` call and one `get_data()` call per
`get_data` will wait until the other side has done `send_data`, so the phase (see below). `get_data` will wait until the other side has done
application developer must be careful to avoid deadlocks (don't get before `send_data`, so the application developer must be careful to avoid deadlocks
you send on both sides in the same protocol). When both sides are done, they (don't get before you send on both sides in the same protocol). When both
must call `close()`, to let the library know that the connection is complete sides are done, they must call `close()`, to let the library know that the
and it can deallocate the channel. If you forget to call `close()`, the connection is complete and it can deallocate the channel. If you forget to
server will not free the channel, and other users will suffer longer call `close()`, the server will not free the channel, and other users will
invitation codes as a result. To encourage `close()`, the library will log an suffer longer invitation codes as a result. To encourage `close()`, the
error if a Wormhole object is destroyed before being closed. library will log an error if a Wormhole object is destroyed before being
closed.
## Examples ## 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 argument, so you can use `d.addCallback(w.close)` instead of
`d.addCallback(lambda _: w.close())`. `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 ## Verifier
You can call `w.get_verifier()` before `send_data()/get_data()`: this will You can call `w.get_verifier()` before `send_data()/get_data()`: this will

View File

@ -157,8 +157,8 @@ class Wormhole:
self.code = None self.code = None
self.key = None self.key = None
self.verifier = None self.verifier = None
self._sent_data = False self._sent_data = set() # phases
self._got_data = False self._got_data = set()
def handle_welcome(self, welcome): def handle_welcome(self, welcome):
if ("motd" in welcome and if ("motd" in welcome and
@ -245,7 +245,7 @@ class Wormhole:
self.channel.send(u"pake", self.msg1) self.channel.send(u"pake", self.msg1)
pake_msg = self.channel.get(u"pake") pake_msg = self.channel.get(u"pake")
self.key = self.sp.finish(pake_msg) 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): def get_verifier(self):
if self.code is None: raise UsageError if self.code is None: raise UsageError
@ -253,27 +253,31 @@ class Wormhole:
self._get_key() self._get_key()
return self.verifier return self.verifier
def send_data(self, outbound_data): def send_data(self, outbound_data, phase=u"data"):
if self._sent_data: raise UsageError # only call this once
if not isinstance(outbound_data, type(b"")): raise UsageError 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.code is None: raise UsageError
if self.channel is None: raise UsageError if self.channel is None: raise UsageError
# Without predefined roles, we can't derive predictably unique keys # Without predefined roles, we can't derive predictably unique keys
# for each side, so we use the same key for both. We use random # for each side, so we use the same key for both. We use random
# nonces to keep the messages distinct, and the Channel automatically # nonces to keep the messages distinct, and the Channel automatically
# ignores reflections. # ignores reflections.
self._sent_data.add(phase)
self._get_key() 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) 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): def get_data(self, phase=u"data"):
if self._got_data: raise UsageError # only call this once 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.code is None: raise UsageError
if self.channel is None: raise UsageError if self.channel is None: raise UsageError
self._got_data.add(phase)
self._get_key() self._get_key()
data_key = self.derive_key(u"data-key") data_key = self.derive_key(u"wormhole:phase:%s" % phase)
inbound_encrypted = self.channel.get(u"data") inbound_encrypted = self.channel.get(phase)
try: try:
inbound_data = self._decrypt_data(data_key, inbound_encrypted) inbound_data = self._decrypt_data(data_key, inbound_encrypted)
return inbound_data return inbound_data

View File

@ -154,6 +154,34 @@ class Blocking(ServerBase, unittest.TestCase):
d.addCallback(_done) d.addCallback(_done)
return d 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): def test_verifier(self):
w1 = BlockingWormhole(APPID, self.relayurl) w1 = BlockingWormhole(APPID, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl) w2 = BlockingWormhole(APPID, self.relayurl)
@ -212,6 +240,35 @@ class Blocking(ServerBase, unittest.TestCase):
d.addCallback(_done) d.addCallback(_done)
return d 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): def test_serialize(self):
w1 = BlockingWormhole(APPID, self.relayurl) w1 = BlockingWormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.serialize) # too early self.assertRaises(UsageError, w1.serialize) # too early

View File

@ -142,6 +142,34 @@ class Basic(ServerBase, unittest.TestCase):
d.addCallback(_done) d.addCallback(_done)
return d 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): def test_verifier(self):
w1 = Wormhole(APPID, self.relayurl) w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl) w2 = Wormhole(APPID, self.relayurl)
@ -199,6 +227,34 @@ class Basic(ServerBase, unittest.TestCase):
d.addCallback(_got_code) d.addCallback(_got_code)
return d 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): def test_serialize(self):
w1 = Wormhole(APPID, self.relayurl) w1 = Wormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.serialize) # too early self.assertRaises(UsageError, w1.serialize) # too early

View File

@ -186,8 +186,8 @@ class Wormhole:
self.code = None self.code = None
self.key = None self.key = None
self._started_get_code = False self._started_get_code = False
self._sent_data = False self._sent_data = set() # phases
self._got_data = False self._got_data = set()
def _set_side(self, side): def _set_side(self, side):
self._side = side self._side = side
@ -312,7 +312,7 @@ class Wormhole:
def _got_pake(pake_msg): def _got_pake(pake_msg):
key = self.sp.finish(pake_msg) key = self.sp.finish(pake_msg)
self.key = key self.key = key
self.verifier = self.derive_key(self._appid+u":Verifier") self.verifier = self.derive_key(u"wormhole:verifier")
return key return key
d.addCallback(_got_pake) d.addCallback(_got_pake)
return d return d
@ -323,31 +323,35 @@ class Wormhole:
d.addCallback(lambda _: self.verifier) d.addCallback(lambda _: self.verifier)
return d return d
def send_data(self, outbound_data): def send_data(self, outbound_data, phase=u"data"):
if self._sent_data: raise UsageError # only call this once
if not isinstance(outbound_data, type(b"")): raise UsageError 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.code is None: raise UsageError
if self.channel is None: raise UsageError if self.channel is None: raise UsageError
# Without predefined roles, we can't derive predictably unique keys # Without predefined roles, we can't derive predictably unique keys
# for each side, so we use the same key for both. We use random # for each side, so we use the same key for both. We use random
# nonces to keep the messages distinct, and the Channel automatically # nonces to keep the messages distinct, and the Channel automatically
# ignores reflections. # ignores reflections.
self._sent_data.add(phase)
d = self._get_key() d = self._get_key()
def _send(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) 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) d.addCallback(_send)
return d return d
def get_data(self): def get_data(self, phase=u"data"):
if self._got_data: raise UsageError # only call this once 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.code is None: raise UsageError
if self.channel is None: raise UsageError if self.channel is None: raise UsageError
self._got_data.add(phase)
d = self._get_key() d = self._get_key()
def _get(key): def _get(key):
data_key = self.derive_key(u"data-key") data_key = self.derive_key(u"wormhole:phase:%s" % phase)
d1 = self.channel.get(u"data") d1 = self.channel.get(phase)
def _decrypt(inbound_encrypted): def _decrypt(inbound_encrypted):
try: try:
inbound_data = self._decrypt_data(data_key, inbound_data = self._decrypt_data(data_key,