appid and derive_key(purpose) are now unicode

This commit is contained in:
Brian Warner 2015-10-06 17:02:52 -07:00
parent 9ba7de6e1e
commit 9e1a00cbd9
8 changed files with 72 additions and 76 deletions

View File

@ -63,7 +63,7 @@ The synchronous+blocking flow looks like this:
from wormhole.blocking.transcribe import Wormhole
from wormhole.public_relay import RENDEZVOUS_RELAY
mydata = b"initiator's data"
i = Wormhole(b"appid", RENDEZVOUS_RELAY)
i = Wormhole(u"appid", RENDEZVOUS_RELAY)
code = i.get_code()
print("Invitation Code: %s" % code)
i.send_data(mydata)
@ -78,7 +78,7 @@ from wormhole.blocking.transcribe import Wormhole
from wormhole.public_relay import RENDEZVOUS_RELAY
mydata = b"receiver's data"
code = sys.argv[1]
r = Wormhole(b"appid", RENDEZVOUS_RELAY)
r = Wormhole(u"appid", RENDEZVOUS_RELAY)
r.set_code(code)
r.send_data(mydata)
theirdata = r.get_data()
@ -95,7 +95,7 @@ from twisted.internet import reactor
from wormhole.public_relay import RENDEZVOUS_RELAY
from wormhole.twisted.transcribe import Wormhole
outbound_message = b"outbound data"
w1 = Wormhole(b"appid", RENDEZVOUS_RELAY)
w1 = Wormhole(u"appid", RENDEZVOUS_RELAY)
d = w1.get_code()
def _got_code(code):
print "Invitation Code:", code
@ -113,7 +113,7 @@ reactor.run()
On the other side, you call `set_code()` instead of waiting for `get_code()`:
```python
w2 = Wormhole(b"appid", RENDEZVOUS_RELAY)
w2 = Wormhole(u"appid", RENDEZVOUS_RELAY)
w2.set_code(code)
d = w2.send_data(my_message)
...
@ -163,12 +163,12 @@ unicode in python3, plain bytes in python2).
## Application Identifier
Applications using this library must provide an "application identifier", a
simple bytestring that distinguishes one application from another. To ensure
uniqueness, use a domain name. To use multiple apps for a single domain, just
use a string like `example.com/app1`. This string must be the same on both
clients, otherwise they will not see each other. The invitation codes are
scoped to the app-id. Note that the app-id must be a bytestring, not unicode,
so on python3 use `b"appid"`.
simple string that distinguishes one application from another. To ensure
uniqueness, use a domain name. To use multiple apps for a single domain,
append a URL-like slash and path, like `example.com/app1`. This string must
be the same on both clients, otherwise they will not see each other. The
invitation codes are scoped to the app-id. Note that the app-id must be
unicode, not bytes, so on python2 use `u"appid"`.
Distinct app-ids reduce the size of the connection-id numbers. If fewer than
ten initiators are active for a given app-id, the connection-id will only
@ -244,10 +244,8 @@ re-send it if necessary.
All cryptographically-sensitive parameters are passed as bytes ("str" in
python2, "bytes" in python3):
* application identifier
* verifier string
* data in/out
* derived-key "purpose" string
* transit records in/out
Some human-readable parameters are passed as strings: "str" in python2, "str"
@ -260,6 +258,8 @@ Some human-readable parameters are passed as strings: "str" in python2, "str"
And some are always unicode, in both python2 and python3:
* relay URL
* application identifier
* derived-key "purpose" string: `w.derive_key(PURPOSE)`
## Detailed Example

View File

@ -1,5 +1,5 @@
from __future__ import print_function
import os, sys, time, re, requests, json
import os, sys, time, re, requests, json, unicodedata
from binascii import hexlify, unhexlify
from spake2 import SPAKE2_Symmetric
from nacl.secret import SecretBox
@ -15,6 +15,9 @@ from ..channel_monitor import monitor
SECOND = 1
MINUTE = 60*SECOND
def to_bytes(u):
return unicodedata.normalize("NFC", u).encode("utf-8")
# relay URLs are:
# GET /list -> {channelids: [INT..]}
# POST /allocate {side: SIDE} -> {channelid: INT}
@ -130,7 +133,7 @@ class Wormhole:
version_warning_displayed = False
def __init__(self, appid, relay_url):
if not isinstance(appid, type(b"")): raise UsageError
if not isinstance(appid, type(u"")): raise UsageError
if not isinstance(relay_url, type(u"")): raise UsageError
if not relay_url.endswith(u"/"): raise UsageError
self._appid = appid
@ -201,12 +204,12 @@ class Wormhole:
def _start(self):
# allocate the rest now too, so it can be serialized
self.sp = SPAKE2_Symmetric(self.code.encode("ascii"),
idSymmetric=self._appid)
idSymmetric=to_bytes(self._appid))
self.msg1 = self.sp.start()
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
if not isinstance(purpose, type(b"")): raise UsageError
return HKDF(self.key, length, CTXinfo=purpose)
if not isinstance(purpose, type(u"")): raise UsageError
return HKDF(self.key, length, CTXinfo=to_bytes(purpose))
def _encrypt_data(self, key, data):
assert isinstance(key, type(b"")), type(key)
@ -230,7 +233,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+b":Verifier")
self.verifier = self.derive_key(self._appid+u":Verifier")
def get_verifier(self):
if self.code is None: raise UsageError
@ -248,7 +251,7 @@ class Wormhole:
# nonces to keep the messages distinct, and the Channel automatically
# ignores reflections.
self._get_key()
data_key = self.derive_key(b"data-key")
data_key = self.derive_key(u"data-key")
outbound_encrypted = self._encrypt_data(data_key, outbound_data)
self.channel.send(u"data", outbound_encrypted)
@ -257,7 +260,7 @@ class Wormhole:
if self.code is None: raise UsageError
if self.channel is None: raise UsageError
self._get_key()
data_key = self.derive_key(b"data-key")
data_key = self.derive_key(u"data-key")
inbound_encrypted = self.channel.get(u"data")
try:
inbound_data = self._decrypt_data(data_key, inbound_encrypted)

View File

@ -2,7 +2,7 @@ from __future__ import print_function
import os, sys, json, binascii, six
from ..errors import handle_server_error
APPID = b"lothar.com/wormhole/text-or-file-xfer"
APPID = u"lothar.com/wormhole/text-or-file-xfer"
@handle_server_error
def receive(args):
@ -94,7 +94,7 @@ def receive(args):
# now receive the rest of the owl
tdata = them_d["transit"]
transit_key = w.derive_key(APPID+b"/transit-key")
transit_key = w.derive_key(APPID+u"/transit-key")
transit_receiver.set_transit_key(transit_key)
transit_receiver.add_their_direct_hints(tdata["direct_connection_hints"])
transit_receiver.add_their_relay_hints(tdata["relay_connection_hints"])

View File

@ -2,7 +2,7 @@ from __future__ import print_function
import os, sys, json, binascii, six
from ..errors import handle_server_error
APPID = b"lothar.com/wormhole/text-or-file-xfer"
APPID = u"lothar.com/wormhole/text-or-file-xfer"
@handle_server_error
def send(args):
@ -109,7 +109,7 @@ def send(args):
w.close()
tdata = them_phase1["transit"]
transit_key = w.derive_key(APPID+b"/transit-key")
transit_key = w.derive_key(APPID+"/transit-key")
transit_sender.set_transit_key(transit_key)
transit_sender.add_their_direct_hints(tdata["direct_connection_hints"])
transit_sender.add_their_relay_hints(tdata["relay_connection_hints"])

View File

@ -5,6 +5,8 @@ from twisted.internet.threads import deferToThread
from ..blocking.transcribe import Wormhole as BlockingWormhole, UsageError
from .common import ServerBase
APPID = u"appid"
class Blocking(ServerBase, unittest.TestCase):
# we need Twisted to run the server, but we run the sender and receiver
# with deferToThread()
@ -18,9 +20,8 @@ class Blocking(ServerBase, unittest.TestCase):
deferToThread(f2, *f2args)], True)
def test_basic(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
d = deferToThread(w1.get_code)
def _got_code(code):
w2.set_code(code)
@ -39,9 +40,8 @@ class Blocking(ServerBase, unittest.TestCase):
return d
def test_interleaved(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
d = deferToThread(w1.get_code)
def _got_code(code):
w2.set_code(code)
@ -61,9 +61,8 @@ class Blocking(ServerBase, unittest.TestCase):
return d
def test_fixed_code(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
d = self.doBoth([w1.send_data, b"data1"], [w2.send_data, b"data2"])
@ -79,9 +78,8 @@ class Blocking(ServerBase, unittest.TestCase):
return d
def test_verifier(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
d = deferToThread(w1.get_code)
def _got_code(code):
w2.set_code(code)
@ -106,9 +104,8 @@ class Blocking(ServerBase, unittest.TestCase):
return d
def test_verifier_mismatch(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
d = deferToThread(w1.get_code)
def _got_code(code):
w2.set_code(code+"not")
@ -123,15 +120,14 @@ class Blocking(ServerBase, unittest.TestCase):
return d
def test_errors(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.get_verifier)
self.assertRaises(UsageError, w1.get_data)
self.assertRaises(UsageError, w1.send_data, b"data")
w1.set_code("123-purple-elephant")
self.assertRaises(UsageError, w1.set_code, "123-nope")
self.assertRaises(UsageError, w1.get_code)
w2 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
d = deferToThread(w2.get_code)
def _done(code):
self.assertRaises(UsageError, w2.get_code)
@ -140,10 +136,9 @@ class Blocking(ServerBase, unittest.TestCase):
return d
def test_serialize(self):
appid = b"appid"
w1 = BlockingWormhole(appid, self.relayurl)
w1 = BlockingWormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.serialize) # too early
w2 = BlockingWormhole(appid, self.relayurl)
w2 = BlockingWormhole(APPID, self.relayurl)
d = deferToThread(w1.get_code)
def _got_code(code):
self.assertRaises(UsageError, w2.serialize) # too early

View File

@ -4,15 +4,16 @@ from twisted.internet.defer import gatherResults
from ..twisted.transcribe import Wormhole, UsageError
from .common import ServerBase
APPID = u"appid"
class Basic(ServerBase, unittest.TestCase):
def doBoth(self, d1, d2):
return gatherResults([d1, d2], True)
def test_basic(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w2 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
d = w1.get_code()
def _got_code(code):
w2.set_code(code)
@ -30,9 +31,8 @@ class Basic(ServerBase, unittest.TestCase):
return d
def test_interleaved(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w2 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
d = w1.get_code()
def _got_code(code):
w2.set_code(code)
@ -51,9 +51,8 @@ class Basic(ServerBase, unittest.TestCase):
return d
def test_fixed_code(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w2 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
d = self.doBoth(w1.send_data(b"data1"), w2.send_data(b"data2"))
@ -69,9 +68,8 @@ class Basic(ServerBase, unittest.TestCase):
return d
def test_verifier(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w2 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
d = w1.get_code()
def _got_code(code):
w2.set_code(code)
@ -95,9 +93,8 @@ class Basic(ServerBase, unittest.TestCase):
return d
def test_verifier_mismatch(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w2 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
d = w1.get_code()
def _got_code(code):
w2.set_code(code+"not")
@ -112,15 +109,14 @@ class Basic(ServerBase, unittest.TestCase):
return d
def test_errors(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.get_verifier)
self.assertRaises(UsageError, w1.send_data, b"data")
self.assertRaises(UsageError, w1.get_data)
w1.set_code("123-purple-elephant")
self.assertRaises(UsageError, w1.set_code, "123-nope")
self.assertRaises(UsageError, w1.get_code)
w2 = Wormhole(appid, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
d = w2.get_code()
self.assertRaises(UsageError, w2.get_code)
def _got_code(code):
@ -129,10 +125,9 @@ class Basic(ServerBase, unittest.TestCase):
return d
def test_serialize(self):
appid = b"appid"
w1 = Wormhole(appid, self.relayurl)
w1 = Wormhole(APPID, self.relayurl)
self.assertRaises(UsageError, w1.serialize) # too early
w2 = Wormhole(appid, self.relayurl)
w2 = Wormhole(APPID, self.relayurl)
d = w1.get_code()
def _got_code(code):
self.assertRaises(UsageError, w2.serialize) # too early

View File

@ -5,7 +5,7 @@ from twisted.internet import reactor
from .transcribe import Wormhole
from .. import public_relay
APPID = b"lothar.com/wormhole/text-or-file-xfer"
APPID = u"lothar.com/wormhole/text-or-file-xfer"
w = Wormhole(APPID, public_relay.RENDEZVOUS_RELAY)

View File

@ -1,5 +1,5 @@
from __future__ import print_function
import os, sys, json, re
import os, sys, json, re, unicodedata
from binascii import hexlify, unhexlify
from zope.interface import implementer
from twisted.internet import reactor, defer
@ -17,6 +17,9 @@ from ..errors import ServerError, WrongPasswordError, UsageError
from ..util.hkdf import HKDF
from ..channel_monitor import monitor
def to_bytes(u):
return unicodedata.normalize("NFC", u).encode("utf-8")
@implementer(IBodyProducer)
class DataProducer:
def __init__(self, data):
@ -146,7 +149,7 @@ class Wormhole:
version_warning_displayed = False
def __init__(self, appid, relay_url):
if not isinstance(appid, type(b"")): raise UsageError
if not isinstance(appid, type(u"")): raise UsageError
if not isinstance(relay_url, type(u"")): raise UsageError
if not relay_url.endswith(u"/"): raise UsageError
self._appid = appid
@ -219,7 +222,7 @@ class Wormhole:
def _start(self):
# allocate the rest now too, so it can be serialized
self.sp = SPAKE2_Symmetric(self.code.encode("ascii"),
idSymmetric=self._appid)
idSymmetric=to_bytes(self._appid))
self.msg1 = self.sp.start()
def serialize(self):
@ -242,7 +245,7 @@ class Wormhole:
@classmethod
def from_serialized(klass, data):
d = json.loads(data)
self = klass(d["appid"].encode("ascii"), d["relay_url"])
self = klass(d["appid"], d["relay_url"])
self._set_side(d["side"].encode("ascii"))
self._set_code_and_channelid(d["code"].encode("ascii"))
self.sp = SPAKE2_Symmetric.from_serialized(json.dumps(d["spake2"]))
@ -250,11 +253,11 @@ class Wormhole:
return self
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
if not isinstance(purpose, type(u"")): raise UsageError
if self.key is None:
# call after get_verifier() or get_data()
raise UsageError
if not isinstance(purpose, type(b"")): raise UsageError
return HKDF(self.key, length, CTXinfo=purpose)
return HKDF(self.key, length, CTXinfo=to_bytes(purpose))
def _encrypt_data(self, key, data):
assert isinstance(key, type(b"")), type(key)
@ -282,7 +285,7 @@ class Wormhole:
def _got_pake(pake_msg):
key = self.sp.finish(pake_msg)
self.key = key
self.verifier = self.derive_key(self._appid+b":Verifier")
self.verifier = self.derive_key(self._appid+u":Verifier")
return key
d.addCallback(_got_pake)
return d
@ -304,7 +307,7 @@ class Wormhole:
# ignores reflections.
d = self._get_key()
def _send(key):
data_key = self.derive_key(b"data-key")
data_key = self.derive_key(u"data-key")
outbound_encrypted = self._encrypt_data(data_key, outbound_data)
return self.channel.send(u"data", outbound_encrypted)
d.addCallback(_send)
@ -316,7 +319,7 @@ class Wormhole:
if self.channel is None: raise UsageError
d = self._get_key()
def _get(key):
data_key = self.derive_key(b"data-key")
data_key = self.derive_key(u"data-key")
d1 = self.channel.get(u"data")
def _decrypt(inbound_encrypted):
try: