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:
parent
d0a7da3a63
commit
bf43dae2ad
36
docs/api.md
36
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue
Block a user