2015-06-21 01:36:22 +00:00
|
|
|
from __future__ import print_function
|
2015-06-21 02:18:21 +00:00
|
|
|
import os, sys, json, re
|
2015-06-21 01:36:22 +00:00
|
|
|
from binascii import hexlify, unhexlify
|
|
|
|
from zope.interface import implementer
|
|
|
|
from twisted.internet import reactor, defer
|
|
|
|
from twisted.web import client as web_client
|
|
|
|
from twisted.web import error as web_error
|
|
|
|
from twisted.web.iweb import IBodyProducer
|
|
|
|
from nacl.secret import SecretBox
|
|
|
|
from nacl.exceptions import CryptoError
|
2015-06-21 02:18:21 +00:00
|
|
|
from nacl import utils
|
2015-06-21 01:36:22 +00:00
|
|
|
from spake2 import SPAKE2_Symmetric
|
2015-07-25 00:55:23 +00:00
|
|
|
from .eventsource_twisted import ReconnectingEventSource
|
2015-06-21 01:36:22 +00:00
|
|
|
from .. import __version__
|
|
|
|
from .. import codes
|
2015-10-03 19:36:14 +00:00
|
|
|
from ..errors import ServerError, WrongPasswordError, UsageError
|
2015-06-21 01:36:22 +00:00
|
|
|
from ..util.hkdf import HKDF
|
2015-10-06 23:31:41 +00:00
|
|
|
from ..channel_monitor import monitor
|
2015-02-11 02:34:13 +00:00
|
|
|
|
2015-06-21 01:36:22 +00:00
|
|
|
@implementer(IBodyProducer)
|
|
|
|
class DataProducer:
|
|
|
|
def __init__(self, data):
|
|
|
|
self.data = data
|
2015-06-21 02:18:21 +00:00
|
|
|
self.length = len(data)
|
2015-06-21 01:36:22 +00:00
|
|
|
def startProducing(self, consumer):
|
|
|
|
consumer.write(self.data)
|
|
|
|
return defer.succeed(None)
|
|
|
|
def stopProducing(self):
|
|
|
|
pass
|
|
|
|
def pauseProducing(self):
|
|
|
|
pass
|
|
|
|
def resumeProducing(self):
|
|
|
|
pass
|
|
|
|
|
2015-06-21 02:18:21 +00:00
|
|
|
|
2015-10-04 00:46:11 +00:00
|
|
|
def post_json(agent, url, request_body):
|
|
|
|
# POST a JSON body to a URL, parsing the response as JSON
|
|
|
|
data = json.dumps(request_body).encode("utf-8")
|
2015-10-06 23:52:33 +00:00
|
|
|
d = agent.request("POST", url.encode("utf-8"),
|
|
|
|
bodyProducer=DataProducer(data))
|
2015-10-04 00:46:11 +00:00
|
|
|
def _check_error(resp):
|
|
|
|
if resp.code != 200:
|
|
|
|
raise web_error.Error(resp.code, resp.phrase)
|
|
|
|
return resp
|
|
|
|
d.addCallback(_check_error)
|
|
|
|
d.addCallback(web_client.readBody)
|
|
|
|
d.addCallback(lambda data: json.loads(data))
|
|
|
|
return d
|
|
|
|
|
|
|
|
class Channel:
|
2015-10-06 23:36:16 +00:00
|
|
|
def __init__(self, relay_url, channelid, side, handle_welcome,
|
2015-10-04 00:46:11 +00:00
|
|
|
agent):
|
2015-10-06 23:52:33 +00:00
|
|
|
self._channel_url = u"%s%d" % (relay_url, channelid)
|
2015-10-04 00:46:11 +00:00
|
|
|
self._side = side
|
|
|
|
self._handle_welcome = handle_welcome
|
|
|
|
self._agent = agent
|
|
|
|
self._messages = set() # (phase,body) , body is bytes
|
|
|
|
self._sent_messages = set() # (phase,body)
|
|
|
|
|
|
|
|
def _add_inbound_messages(self, messages):
|
|
|
|
for msg in messages:
|
|
|
|
phase = msg["phase"]
|
|
|
|
body = unhexlify(msg["body"].encode("ascii"))
|
|
|
|
self._messages.add( (phase, body) )
|
|
|
|
|
|
|
|
def _find_inbound_message(self, phase):
|
|
|
|
for (their_phase,body) in self._messages - self._sent_messages:
|
|
|
|
if their_phase == phase:
|
|
|
|
return body
|
|
|
|
return None
|
|
|
|
|
|
|
|
def send(self, phase, msg):
|
|
|
|
# TODO: retry on failure, with exponential backoff. We're guarding
|
|
|
|
# against the rendezvous server being temporarily offline.
|
|
|
|
if not isinstance(phase, type(u"")): raise UsageError(type(phase))
|
|
|
|
if not isinstance(msg, type(b"")): raise UsageError(type(msg))
|
|
|
|
self._sent_messages.add( (phase,msg) )
|
|
|
|
payload = {"side": self._side,
|
|
|
|
"phase": phase,
|
|
|
|
"body": hexlify(msg).decode("ascii")}
|
|
|
|
d = post_json(self._agent, self._channel_url, payload)
|
|
|
|
d.addCallback(lambda resp: self._add_inbound_messages(resp["messages"]))
|
|
|
|
return d
|
|
|
|
|
|
|
|
def get(self, phase):
|
|
|
|
# fire with a bytestring of the first message for 'phase' that wasn't
|
|
|
|
# one of ours. It will either come from previously-received messages,
|
|
|
|
# or from an EventSource that we attach to the corresponding URL
|
|
|
|
body = self._find_inbound_message(phase)
|
|
|
|
if body is not None:
|
|
|
|
return defer.succeed(body)
|
|
|
|
d = defer.Deferred()
|
|
|
|
msgs = []
|
|
|
|
def _handle(name, data):
|
|
|
|
if name == "welcome":
|
|
|
|
self._handle_welcome(json.loads(data))
|
|
|
|
if name == "message":
|
|
|
|
self._add_inbound_messages([json.loads(data)])
|
|
|
|
body = self._find_inbound_message(phase)
|
|
|
|
if body is not None and not msgs:
|
|
|
|
msgs.append(body)
|
|
|
|
d.callback(None)
|
|
|
|
# TODO: use agent=self._agent
|
|
|
|
es = ReconnectingEventSource(self._channel_url, _handle)
|
|
|
|
es.startService() # TODO: .setServiceParent(self)
|
|
|
|
es.activate()
|
|
|
|
d.addCallback(lambda _: es.deactivate())
|
|
|
|
d.addCallback(lambda _: es.stopService())
|
|
|
|
d.addCallback(lambda _: msgs[0])
|
|
|
|
return d
|
|
|
|
|
2015-10-04 05:03:27 +00:00
|
|
|
def deallocate(self):
|
2015-10-04 00:46:11 +00:00
|
|
|
# only try once, no retries
|
|
|
|
d = post_json(self._agent, self._channel_url+"/deallocate",
|
|
|
|
{"side": self._side})
|
2015-10-04 05:03:27 +00:00
|
|
|
d.addBoth(lambda _: None) # ignore POST failure
|
2015-10-04 00:46:11 +00:00
|
|
|
return d
|
|
|
|
|
|
|
|
class ChannelManager:
|
2015-10-06 23:36:16 +00:00
|
|
|
def __init__(self, relay_url, side, handle_welcome):
|
2015-10-06 23:52:33 +00:00
|
|
|
assert isinstance(relay_url, type(u""))
|
2015-10-06 23:36:16 +00:00
|
|
|
self._relay_url = relay_url
|
2015-10-04 00:46:11 +00:00
|
|
|
self._side = side
|
|
|
|
self._handle_welcome = handle_welcome
|
|
|
|
self._agent = web_client.Agent(reactor)
|
|
|
|
|
|
|
|
def allocate(self):
|
2015-10-06 23:36:16 +00:00
|
|
|
url = self._relay_url + "allocate"
|
2015-10-04 00:46:11 +00:00
|
|
|
d = post_json(self._agent, url, {"side": self._side})
|
|
|
|
def _got_channel(data):
|
|
|
|
if "welcome" in data:
|
|
|
|
self._handle_welcome(data["welcome"])
|
2015-10-06 23:16:41 +00:00
|
|
|
return data["channelid"]
|
2015-10-04 00:46:11 +00:00
|
|
|
d.addCallback(_got_channel)
|
|
|
|
return d
|
|
|
|
|
|
|
|
def list_channels(self):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2015-10-06 23:16:41 +00:00
|
|
|
def connect(self, channelid):
|
2015-10-06 23:36:16 +00:00
|
|
|
return Channel(self._relay_url, channelid, self._side,
|
2015-10-04 00:46:11 +00:00
|
|
|
self._handle_welcome, self._agent)
|
|
|
|
|
2015-07-25 00:47:46 +00:00
|
|
|
class Wormhole:
|
2015-07-24 23:57:19 +00:00
|
|
|
motd_displayed = False
|
|
|
|
version_warning_displayed = False
|
|
|
|
|
2015-10-06 23:36:16 +00:00
|
|
|
def __init__(self, appid, relay_url):
|
2015-09-24 19:37:55 +00:00
|
|
|
if not isinstance(appid, type(b"")): raise UsageError
|
2015-10-06 23:52:33 +00:00
|
|
|
if not isinstance(relay_url, type(u"")): raise UsageError
|
|
|
|
if not relay_url.endswith(u"/"): raise UsageError
|
2015-10-07 00:05:05 +00:00
|
|
|
self._appid = appid
|
2015-10-06 23:36:16 +00:00
|
|
|
self._relay_url = relay_url
|
2015-10-04 00:46:11 +00:00
|
|
|
self._set_side(hexlify(os.urandom(5)).decode("ascii"))
|
2015-06-21 01:36:22 +00:00
|
|
|
self.code = None
|
2015-06-21 02:18:21 +00:00
|
|
|
self.key = None
|
|
|
|
self._started_get_code = False
|
2015-10-04 05:03:27 +00:00
|
|
|
self._sent_data = False
|
|
|
|
self._got_data = False
|
2015-10-04 00:46:11 +00:00
|
|
|
|
|
|
|
def _set_side(self, side):
|
|
|
|
self._side = side
|
2015-10-06 23:36:16 +00:00
|
|
|
self._channel_manager = ChannelManager(self._relay_url, self._side,
|
2015-10-04 00:46:11 +00:00
|
|
|
self.handle_welcome)
|
2015-07-24 23:57:19 +00:00
|
|
|
|
|
|
|
def handle_welcome(self, welcome):
|
|
|
|
if ("motd" in welcome and
|
|
|
|
not self.motd_displayed):
|
|
|
|
motd_lines = welcome["motd"].splitlines()
|
|
|
|
motd_formatted = "\n ".join(motd_lines)
|
2015-10-06 23:36:16 +00:00
|
|
|
print("Server (at %s) says:\n %s" % (self._relay_url,
|
|
|
|
motd_formatted),
|
2015-07-24 23:57:19 +00:00
|
|
|
file=sys.stderr)
|
|
|
|
self.motd_displayed = True
|
|
|
|
|
|
|
|
# Only warn if we're running a release version (e.g. 0.0.6, not
|
|
|
|
# 0.0.6-DISTANCE-gHASH). Only warn once.
|
|
|
|
if ("-" not in __version__ and
|
|
|
|
not self.version_warning_displayed and
|
|
|
|
welcome["current_version"] != __version__):
|
|
|
|
print("Warning: errors may occur unless both sides are running the same version", file=sys.stderr)
|
|
|
|
print("Server claims %s is current, but ours is %s"
|
|
|
|
% (welcome["current_version"], __version__), file=sys.stderr)
|
|
|
|
self.version_warning_displayed = True
|
|
|
|
|
|
|
|
if "error" in welcome:
|
2015-10-06 23:36:16 +00:00
|
|
|
raise ServerError(welcome["error"], self._relay_url)
|
2015-07-24 23:57:19 +00:00
|
|
|
|
2015-07-25 00:02:32 +00:00
|
|
|
def get_code(self, code_length=2):
|
|
|
|
if self.code is not None: raise UsageError
|
|
|
|
if self._started_get_code: raise UsageError
|
|
|
|
self._started_get_code = True
|
2015-10-04 00:46:11 +00:00
|
|
|
d = self._channel_manager.allocate()
|
2015-10-06 23:16:41 +00:00
|
|
|
def _got_channelid(channelid):
|
|
|
|
code = codes.make_code(channelid, code_length)
|
2015-09-28 06:09:51 +00:00
|
|
|
assert isinstance(code, str), type(code)
|
2015-10-06 23:16:41 +00:00
|
|
|
self._set_code_and_channelid(code)
|
2015-07-25 00:02:32 +00:00
|
|
|
self._start()
|
|
|
|
return code
|
2015-10-06 23:16:41 +00:00
|
|
|
d.addCallback(_got_channelid)
|
2015-07-25 00:02:32 +00:00
|
|
|
return d
|
|
|
|
|
2015-06-21 02:18:21 +00:00
|
|
|
def set_code(self, code):
|
2015-09-28 06:09:51 +00:00
|
|
|
if not isinstance(code, str): raise UsageError
|
2015-06-21 02:18:21 +00:00
|
|
|
if self.code is not None: raise UsageError
|
2015-10-06 23:16:41 +00:00
|
|
|
self._set_code_and_channelid(code)
|
2015-06-21 02:18:21 +00:00
|
|
|
self._start()
|
|
|
|
|
2015-10-06 23:16:41 +00:00
|
|
|
def _set_code_and_channelid(self, code):
|
2015-06-21 02:18:21 +00:00
|
|
|
if self.code is not None: raise UsageError
|
|
|
|
mo = re.search(r'^(\d+)-', code)
|
|
|
|
if not mo:
|
|
|
|
raise ValueError("code (%s) must start with NN-" % code)
|
|
|
|
self.code = code
|
2015-10-06 23:16:41 +00:00
|
|
|
channelid = int(mo.group(1))
|
|
|
|
self.channel = self._channel_manager.connect(channelid)
|
2015-10-06 23:31:41 +00:00
|
|
|
monitor.add(self.channel)
|
2015-06-21 01:36:22 +00:00
|
|
|
|
2015-06-21 02:18:21 +00:00
|
|
|
def _start(self):
|
|
|
|
# allocate the rest now too, so it can be serialized
|
|
|
|
self.sp = SPAKE2_Symmetric(self.code.encode("ascii"),
|
2015-10-07 00:05:05 +00:00
|
|
|
idSymmetric=self._appid)
|
2015-06-21 02:18:21 +00:00
|
|
|
self.msg1 = self.sp.start()
|
|
|
|
|
|
|
|
def serialize(self):
|
|
|
|
# I can only be serialized after get_code/set_code and before
|
|
|
|
# get_verifier/get_data
|
|
|
|
if self.code is None: raise UsageError
|
|
|
|
if self.key is not None: raise UsageError
|
2015-10-04 05:03:27 +00:00
|
|
|
if self._sent_data: raise UsageError
|
|
|
|
if self._got_data: raise UsageError
|
2015-06-21 02:18:21 +00:00
|
|
|
data = {
|
2015-10-07 00:05:05 +00:00
|
|
|
"appid": self._appid,
|
2015-10-06 23:36:16 +00:00
|
|
|
"relay_url": self._relay_url,
|
2015-06-21 02:18:21 +00:00
|
|
|
"code": self.code,
|
2015-10-04 00:46:11 +00:00
|
|
|
"side": self._side,
|
2015-06-21 02:18:21 +00:00
|
|
|
"spake2": json.loads(self.sp.serialize()),
|
|
|
|
"msg1": self.msg1.encode("hex"),
|
|
|
|
}
|
|
|
|
return json.dumps(data)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_serialized(klass, data):
|
|
|
|
d = json.loads(data)
|
2015-10-06 23:52:33 +00:00
|
|
|
self = klass(d["appid"].encode("ascii"), d["relay_url"])
|
2015-10-04 00:46:11 +00:00
|
|
|
self._set_side(d["side"].encode("ascii"))
|
2015-10-06 23:16:41 +00:00
|
|
|
self._set_code_and_channelid(d["code"].encode("ascii"))
|
2015-06-21 02:18:21 +00:00
|
|
|
self.sp = SPAKE2_Symmetric.from_serialized(json.dumps(d["spake2"]))
|
|
|
|
self.msg1 = d["msg1"].decode("hex")
|
|
|
|
return self
|
2015-06-21 01:36:22 +00:00
|
|
|
|
|
|
|
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
|
2015-07-24 22:55:42 +00:00
|
|
|
if self.key is None:
|
|
|
|
# call after get_verifier() or get_data()
|
|
|
|
raise UsageError
|
2015-07-24 23:45:20 +00:00
|
|
|
if not isinstance(purpose, type(b"")): raise UsageError
|
2015-06-21 01:36:22 +00:00
|
|
|
return HKDF(self.key, length, CTXinfo=purpose)
|
|
|
|
|
2015-06-21 02:18:21 +00:00
|
|
|
def _encrypt_data(self, key, data):
|
2015-09-28 06:09:51 +00:00
|
|
|
assert isinstance(key, type(b"")), type(key)
|
|
|
|
assert isinstance(data, type(b"")), type(data)
|
2015-07-24 22:55:42 +00:00
|
|
|
if len(key) != SecretBox.KEY_SIZE: raise UsageError
|
2015-06-21 02:18:21 +00:00
|
|
|
box = SecretBox(key)
|
|
|
|
nonce = utils.random(SecretBox.NONCE_SIZE)
|
|
|
|
return box.encrypt(data, nonce)
|
|
|
|
|
|
|
|
def _decrypt_data(self, key, encrypted):
|
2015-09-28 06:09:51 +00:00
|
|
|
assert isinstance(key, type(b"")), type(key)
|
|
|
|
assert isinstance(encrypted, type(b"")), type(encrypted)
|
2015-07-24 22:55:42 +00:00
|
|
|
if len(key) != SecretBox.KEY_SIZE: raise UsageError
|
2015-06-21 02:18:21 +00:00
|
|
|
box = SecretBox(key)
|
|
|
|
data = box.decrypt(encrypted)
|
|
|
|
return data
|
|
|
|
|
2015-06-21 01:36:22 +00:00
|
|
|
|
|
|
|
def _get_key(self):
|
|
|
|
# TODO: prevent multiple invocation
|
|
|
|
if self.key:
|
|
|
|
return defer.succeed(self.key)
|
2015-10-04 00:46:11 +00:00
|
|
|
d = self.channel.send(u"pake", self.msg1)
|
|
|
|
d.addCallback(lambda _: self.channel.get(u"pake"))
|
2015-07-24 23:33:29 +00:00
|
|
|
def _got_pake(pake_msg):
|
2015-06-21 01:36:22 +00:00
|
|
|
key = self.sp.finish(pake_msg)
|
|
|
|
self.key = key
|
2015-10-07 00:05:05 +00:00
|
|
|
self.verifier = self.derive_key(self._appid+b":Verifier")
|
2015-06-21 01:36:22 +00:00
|
|
|
return key
|
|
|
|
d.addCallback(_got_pake)
|
|
|
|
return d
|
|
|
|
|
|
|
|
def get_verifier(self):
|
2015-06-21 02:18:21 +00:00
|
|
|
if self.code is None: raise UsageError
|
2015-06-21 01:36:22 +00:00
|
|
|
d = self._get_key()
|
|
|
|
d.addCallback(lambda _: self.verifier)
|
|
|
|
return d
|
|
|
|
|
2015-10-04 05:03:27 +00:00
|
|
|
def send_data(self, outbound_data):
|
|
|
|
if self._sent_data: raise UsageError # only call this once
|
2015-09-28 06:09:51 +00:00
|
|
|
if not isinstance(outbound_data, type(b"")): raise UsageError
|
2015-06-21 02:18:21 +00:00
|
|
|
if self.code is None: raise UsageError
|
2015-10-04 05:03:27 +00:00
|
|
|
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.
|
2015-06-21 01:36:22 +00:00
|
|
|
d = self._get_key()
|
2015-10-04 05:03:27 +00:00
|
|
|
def _send(key):
|
|
|
|
data_key = self.derive_key(b"data-key")
|
|
|
|
outbound_encrypted = self._encrypt_data(data_key, outbound_data)
|
|
|
|
return self.channel.send(u"data", outbound_encrypted)
|
|
|
|
d.addCallback(_send)
|
2015-06-21 02:18:21 +00:00
|
|
|
return d
|
|
|
|
|
2015-10-04 05:03:27 +00:00
|
|
|
def get_data(self):
|
|
|
|
if self._got_data: raise UsageError # only call this once
|
|
|
|
if self.code is None: raise UsageError
|
|
|
|
if self.channel is None: raise UsageError
|
|
|
|
d = self._get_key()
|
|
|
|
def _get(key):
|
|
|
|
data_key = self.derive_key(b"data-key")
|
|
|
|
d1 = self.channel.get(u"data")
|
|
|
|
def _decrypt(inbound_encrypted):
|
|
|
|
try:
|
|
|
|
inbound_data = self._decrypt_data(data_key,
|
|
|
|
inbound_encrypted)
|
|
|
|
return inbound_data
|
|
|
|
except CryptoError:
|
|
|
|
raise WrongPasswordError
|
|
|
|
d1.addCallback(_decrypt)
|
|
|
|
return d1
|
|
|
|
d.addCallback(_get)
|
|
|
|
return d
|
|
|
|
|
|
|
|
def close(self, res=None):
|
2015-10-06 23:31:41 +00:00
|
|
|
monitor.close(self.channel)
|
2015-10-04 05:03:27 +00:00
|
|
|
d = self.channel.deallocate()
|
2015-06-21 01:36:22 +00:00
|
|
|
return d
|