diff --git a/docs/api.md b/docs/api.md index 8e998a8..9e97e15 100644 --- a/docs/api.md +++ b/docs/api.md @@ -29,44 +29,53 @@ The synchronous+blocking flow looks like this: ```python from wormhole.transcribe import Initiator -blob = b"initiator's blob" -i = Initiator("appid", blob) -print("Invitation Code: %s" % i.start() -theirblob = i.finish() -print("Their blob: %s" % theirblob.decode("ascii")) +data = b"initiator's data" +i = Initiator("appid", data) +code = i.get_code() +print("Invitation Code: %s" % code) +theirdata = i.get_data() +print("Their data: %s" % theirdata.decode("ascii")) ``` ```python import sys from wormhole.transcribe import Receiver -blob = b"receiver's blob" +data = b"receiver's data" code = sys.argv[1] -r = Receiver("appid", code, blob) -theirblob = r.finish() -print("Their blob: %s" % theirblob.decode("ascii")) +r = Receiver("appid", code, data) +theirdata = r.get_data() +print("Their data: %s" % theirdata.decode("ascii")) ``` The Twisted-friendly flow looks like this: ```python -from wormhole.transcribe import Initiator -blob = b"initiator's blob" -i = Initiator("appid", blob) -d = i.start() -d.addCallback(lambda code: print("Invitation Code: %s" % code)) -d.addCallback(lambda _: i.finish()) -d.addCallback(lambda theirblob: - print("Their blob: %s" % theirblob.decode("ascii"))) +from twisted.internet import reactor +from wormhole.transcribe import TwistedInitiator +data = b"initiator's data" +ti = TwistedInitiator("appid", data, reactor) +ti.startService() +d1 = ti.when_get_code() +d1.addCallback(lambda code: print("Invitation Code: %s" % code)) +d2 = ti.when_get_data() +d2.addCallback(lambda theirdata: + print("Their data: %s" % theirdata.decode("ascii"))) +d2.addCallback(labmda _: reactor.stop()) +reactor.run() ``` ```python -from wormhole.transcribe import Receiver -blob = b"receiver's blob" +from twisted.internet import reactor +from wormhole.transcribe import TwistedReceiver +data = b"receiver's data" code = sys.argv[1] -r = Receiver("appid", code, blob) -d = r.finish() -d.addCallback(lambda theirblob: - print("Their blob: %s" % theirblob.decode("ascii"))) +tr = TwistedReceiver("appid", code, data, reactor) +tr.startService() +d = tr.when_get_data() +d.addCallback(lambda theirdata: + print("Their data: %s" % theirdata.decode("ascii"))) +d.addCallback(lambda _: reactor.stop()) +reactor.run() ``` ## Application Identifier diff --git a/setup.py b/setup.py index 71a35e0..ee0d28c 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup(name="wormhole-sync", url="https://github.com/warner/wormhole-sync", package_dir={"": "src"}, packages=["wormhole"], - install_requires=["spake2"], + install_requires=["spake2", "requests"], test_suite="wormhole.test", cmdclass=commands, ) diff --git a/src/wormhole/codes.py b/src/wormhole/codes.py new file mode 100644 index 0000000..a0e73dc --- /dev/null +++ b/src/wormhole/codes.py @@ -0,0 +1,15 @@ +import os, random + +WORDLIST = ["able", "baker", "charlie"] # TODO: 1024 + +def make_code(channel_id): + # TODO: confirm that random.choice() uses os.urandom properly and covers + # the entire range with minimal bias. Many random.py functions do not, + # but I think this one might. If not, build our own from os.urandom, + # convert-to-int, and modulo. + word = random.choice(WORDLIST) + return "%d-%s" % (channel_id, word) + +def extract_channel_id(code): + channel_id = int(code.split("-")[0]) + return channel_id diff --git a/src/wormhole/const.py b/src/wormhole/const.py new file mode 100644 index 0000000..e92bbdf --- /dev/null +++ b/src/wormhole/const.py @@ -0,0 +1,2 @@ + +RELAY = "baked in relay URL" diff --git a/src/wormhole/receive_file.py b/src/wormhole/receive_file.py index dcda6c6..78ebe87 100644 --- a/src/wormhole/receive_file.py +++ b/src/wormhole/receive_file.py @@ -1,7 +1,7 @@ import os, sys, json from binascii import unhexlify from nacl.secret import SecretBox -from . import api +from .transcribe import Receiver APPID = "lothar.com/wormhole/file-xfer" RELAY = "example.com" @@ -9,8 +9,8 @@ RELAY = "example.com" # we're receiving code = sys.argv[1] blob = b"" -r = api.Receiver(APPID, blob, code) -them_bytes = r.finish() +r = Receiver(APPID, blob, code) +them_bytes = r.get_data() them_d = json.loads(them_bytes.decode("utf-8")) print("them: %r" % (them_d,)) xfer_key = unhexlify(them_d["xfer_key"].encode("ascii")) diff --git a/src/wormhole/receive_text.py b/src/wormhole/receive_text.py index 69db776..c6e2ac8 100644 --- a/src/wormhole/receive_text.py +++ b/src/wormhole/receive_text.py @@ -1,12 +1,12 @@ import sys, json -from . import api +from .transcribe import Receiver APPID = "lothar.com/wormhole/text-xfer" # we're receiving code = sys.argv[1] blob = b"" -r = api.Receiver(APPID, blob, code) -them_bytes = r.finish() +r = Receiver(APPID, blob, code) +them_bytes = r.get_data() them_d = json.loads(them_bytes.decode("utf-8")) print(them_d["message"]) diff --git a/src/wormhole/send_file.py b/src/wormhole/send_file.py index 87b6a61..7564e0e 100644 --- a/src/wormhole/send_file.py +++ b/src/wormhole/send_file.py @@ -2,7 +2,7 @@ import os, sys, json from binascii import hexlify from nacl.secret import SecretBox from nacl import utils -from . import api +from .transcribe import Initiator APPID = "lothar.com/wormhole/file-xfer" RELAY = "example.com" @@ -16,14 +16,14 @@ blob = json.dumps({"xfer_key": hexlify(xfer_key), "filesize": os.stat(filename).st_size, "relay": RELAY, }).encode("utf-8") -i = api.Initiator(APPID, blob) -code = i.start() +i = Initiator(APPID, blob) +code = i.get_code() print("Wormhole code is '%s'" % code) print("On the other computer, please run:") print() print(" wormhole-receive-file %s" % code) print() -them_bytes = i.finish() +them_bytes = i.get_data() them_d = json.loads(them_bytes.decode("utf-8")) print("them: %r" % (them_d,)) diff --git a/src/wormhole/send_text.py b/src/wormhole/send_text.py index 8ead0ec..a414e1b 100644 --- a/src/wormhole/send_text.py +++ b/src/wormhole/send_text.py @@ -1,5 +1,5 @@ import sys, json -from . import api +from .transcribe import Initiator APPID = "lothar.com/wormhole/text-xfer" @@ -7,13 +7,13 @@ APPID = "lothar.com/wormhole/text-xfer" message = sys.argv[1] blob = json.dumps({"message": message, }).encode("utf-8") -i = api.Initiator(APPID, blob) -code = i.start() +i = Initiator(APPID, blob) +code = i.get_code() print("Wormhole code is '%s'" % code) print("On the other computer, please run:") print() print(" wormhole-receive-text %s" % code) print() -them_bytes = i.finish() +them_bytes = i.get_data() them_d = json.loads(them_bytes.decode("utf-8")) print("them: %r" % (them_d,)) diff --git a/src/wormhole/transcribe.py b/src/wormhole/transcribe.py new file mode 100644 index 0000000..561ddcc --- /dev/null +++ b/src/wormhole/transcribe.py @@ -0,0 +1,106 @@ +import time, requests +from spake2 import SPAKE2_A, SPAKE2_B +from .const import RELAY +from .codes import make_code, extract_channel_id + +SECOND = 1 +MINUTE = 60*SECOND + +class Timeout(Exception): + pass + +# POST /allocate -> {channel-id: INT} +# POST /pake/post/CHANNEL-ID {side: STR, message: STR} -> {messages: [STR..]} +# POST /pake/poll/CHANNEL-ID {side: STR} -> {messages: [STR..]} +# POST /data/post/CHANNEL-ID {side: STR, message: STR} -> {messages: [STR..]} +# POST /data/poll/CHANNEL-ID {side: STR} -> {messages: [STR..]} +# POST /deallocate/CHANNEL-ID {side: STR} -> waiting | ok + +class Initiator: + def __init__(self, appid, data, relay=RELAY): + self.appid = appid + self.data = data + self.relay = relay + self.started = time.time() + self.wait = 2*SECOND + self.timeout = 3*MINUTE + self.side = "initiator" + + def get_code(self): + # allocate channel + r = requests.post(self.relay + "allocate", data="{}") + r.raise_for_status() + self.channel_id = r.json()["channel-id"] + self.code = codes.make_code(self.channel_id) + self.sp = SPAKE2_A(self.code.encode("ascii"), + idA=self.appid+":Initiator", + idB=self.appid+":Receiver") + msg = self.sp.start() + post_url = self.relay + "pake/post/%d" % self.channel_id + post_data = {"side": self.side, + "message": hexlify(msg).decode("ascii")} + r = requests.post(post_url, data=json.dumps(post_data)) + r.raise_for_status() + return self.code + + def get_data(self): + # poll for PAKE response + pake_url = self.relay + "pake/poll/%d" % self.channel_id + post_data = json.dumps({"side": self.side}) + while True: + r = requests.post(pake_url, data=post_data) + r.raise_for_status() + msgs = r.json()["messages"] + if msgs: + break + if time.time() > (self.started + self.timeout): + raise Timeout + time.sleep(self.wait) + pake_msg = unhexlify(msgs[0].encode("ascii")) + self.key = self.sp.finish(pake_msg) + + # post encrypted data + post_url = self.relay + "data/post/%d" % self.channel_id + post_data = json.dumps({"side": self.side, + "message": hexlify(self.data).decode("ascii")}) + r = requests.post(post_url, data=post_data) + r.raise_for_status() + + # poll for data message + data_url = self.relay + "data/poll/%d" % self.channel_id + post_data = json.dumps({"side": self.side}) + while True: + r = requests.post(data_url, data=post_data) + r.raise_for_status() + msgs = r.json()["messages"] + if msgs: + break + if time.time() > (self.started + self.timeout): + raise Timeout + time.sleep(self.wait) + data = unhexlify(msgs[0].encode("ascii")) + + # deallocate channel + deallocate_url = self.relay + "deallocate/%s" % self.channel_id + post_data = json.dumps({"side": self.side}) + r = requests.post(deallocate, data=post_data) + r.raise_for_status() + + return data + +class Receiver: + def __init__(self, appid, data, code, relay=RELAY): + self.appid = appid + self.data = data + self.code = code + self.channel_id = extract_channel_id(code) + self.relay = relay + self.sp = SPAKE2_B(code.encode("ascii"), + idA=self.appid+":Initiator", + idB=self.appid+":Receiver") + + def get_data(self): + # poll for PAKE response + # poll for data message + # deallocate channel + pass diff --git a/src/wormhole/twisted/transcribe.py b/src/wormhole/twisted/transcribe.py new file mode 100644 index 0000000..dfdf821 --- /dev/null +++ b/src/wormhole/twisted/transcribe.py @@ -0,0 +1,27 @@ +from twisted.application import service +from ..const import RELAY + +class TwistedInitiator(service.MultiService): + def __init__(self, appid, data, reactor, relay=RELAY): + self.appid = appid + self.data = data + self.reactor = reactor + self.relay = relay + + def when_get_code(self): + pass # return Deferred + + def when_get_data(self): + pass # return Deferred + +class TwistedReceiver(service.MultiService): + def __init__(self, appid, data, code, reactor, relay=RELAY): + self.appid = appid + self.data = data + self.code = code + self.reactor = reactor + self.relay = relay + + def when_get_data(self): + pass # return Deferred +