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
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user