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
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,