diff --git a/docs/api.md b/docs/api.md index f1421a5..26d1129 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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 diff --git a/src/wormhole/blocking/transcribe.py b/src/wormhole/blocking/transcribe.py index e84ca10..0a97053 100644 --- a/src/wormhole/blocking/transcribe.py +++ b/src/wormhole/blocking/transcribe.py @@ -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) diff --git a/src/wormhole/scripts/cmd_receive.py b/src/wormhole/scripts/cmd_receive.py index c62ea4e..3fc9331 100644 --- a/src/wormhole/scripts/cmd_receive.py +++ b/src/wormhole/scripts/cmd_receive.py @@ -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"]) diff --git a/src/wormhole/scripts/cmd_send.py b/src/wormhole/scripts/cmd_send.py index 35def33..c4a3797 100644 --- a/src/wormhole/scripts/cmd_send.py +++ b/src/wormhole/scripts/cmd_send.py @@ -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"]) diff --git a/src/wormhole/test/test_blocking.py b/src/wormhole/test/test_blocking.py index 2f2c929..977e47d 100644 --- a/src/wormhole/test/test_blocking.py +++ b/src/wormhole/test/test_blocking.py @@ -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 diff --git a/src/wormhole/test/test_twisted.py b/src/wormhole/test/test_twisted.py index 63367d2..4aa345b 100644 --- a/src/wormhole/test/test_twisted.py +++ b/src/wormhole/test/test_twisted.py @@ -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 diff --git a/src/wormhole/twisted/demo.py b/src/wormhole/twisted/demo.py index 4236cbf..75b3825 100644 --- a/src/wormhole/twisted/demo.py +++ b/src/wormhole/twisted/demo.py @@ -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) diff --git a/src/wormhole/twisted/transcribe.py b/src/wormhole/twisted/transcribe.py index 66db95a..2357867 100644 --- a/src/wormhole/twisted/transcribe.py +++ b/src/wormhole/twisted/transcribe.py @@ -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: