Merge branch 'only-symmetric'
magic-wormhole now only uses the "SPAKE2_Symmetric" protocol, which does not require the two participants to decide ahead of time which one is which. Previously, the blocking form used an asymmetric protocol, and the non-blocking/Twisted form used a symmetric protocol (however it used an earlier+slower version from SPAKE2-0.2.0, which is not compatible with the current SPAKE2-0.3.0 one). This breaks compatibility for the built-in "wormhole" script: a sender using 0.3.0 will get errors when talking to a recipient running this version or newer (including the upcoming 0.4.0 release).
This commit is contained in:
commit
262cc960cf
|
@ -113,14 +113,11 @@ All four commands accept:
|
||||||
## Library
|
## Library
|
||||||
|
|
||||||
The `wormhole` module makes it possible for other applications to use these
|
The `wormhole` module makes it possible for other applications to use these
|
||||||
code-protected channels. This includes blocking/synchronous support (for an
|
code-protected channels. This includes blocking/synchronous support and
|
||||||
asymmetric pair of "initiator" and "receiver" endpoints), and async/Twisted
|
async/Twisted support, both for a symmetric scheme. The main module is named
|
||||||
support (for a symmetric scheme). The main module is named
|
|
||||||
`wormhole.blocking.transcribe`, to reflect that it is for
|
`wormhole.blocking.transcribe`, to reflect that it is for
|
||||||
synchronous/blocking code, and uses a PAKE mode whereby one user transcribes
|
synchronous/blocking code, and uses a PAKE mode whereby one user transcribes
|
||||||
their code to the other. (internal names may change in the future). The
|
their code to the other. (internal names may change in the future).
|
||||||
synchronous support uses distinctive sides: one `Initiator`, and one
|
|
||||||
`Receiver`.
|
|
||||||
|
|
||||||
The file-transfer tools use a second module named
|
The file-transfer tools use a second module named
|
||||||
`wormhole.blocking.transit`, which provides an encrypted record-pipe. It
|
`wormhole.blocking.transit`, which provides an encrypted record-pipe. It
|
||||||
|
|
83
docs/api.md
83
docs/api.md
|
@ -12,26 +12,48 @@ server" that relays information from one machine to the other.
|
||||||
|
|
||||||
## Modes
|
## Modes
|
||||||
|
|
||||||
This library will eventually offer multiple modes.
|
This library will eventually offer multiple modes. For now, only "transcribe
|
||||||
|
mode" is available.
|
||||||
|
|
||||||
The first mode provided is "transcribe" mode. In this mode, one machine goes
|
Transcribe mode has two variants. In the "machine-generated" variant, the
|
||||||
first, and is called the "initiator". The initiator contacts the rendezvous
|
"initiator" machine creates the invitation code, displays it to the first
|
||||||
server and allocates a "channel ID", which is a small integer. The initiator
|
user, they convey it (somehow) to the second user, who transcribes it into
|
||||||
then displays the "invitation code", which is the channel-ID plus a few
|
the second ("receiver") machine. In the "human-generated" variant, the two
|
||||||
secret words. The user copies the invitation code to the second machine,
|
humans come up with the code (possibly without computers), then later
|
||||||
called the "receiver". The receiver connects to the rendezvous server, and
|
transcribe it into both machines.
|
||||||
uses the invitation code to contact the initiator. They agree upon an
|
|
||||||
encryption key, and exchange a small encrypted+authenticated data message.
|
When the initator machine generates the invitation code, the initiator
|
||||||
|
contacts the rendezvous server and allocates a "channel ID", which is a small
|
||||||
|
integer. The initiator then displays the invitation code, which is the
|
||||||
|
channel-ID plus a few secret words. The user copies the code to the second
|
||||||
|
machine. The receiver machine connects to the rendezvous server, and uses the
|
||||||
|
invitation code to contact the initiator. They agree upon an encryption key,
|
||||||
|
and exchange a small encrypted+authenticated data message.
|
||||||
|
|
||||||
|
When the humans create an invitation code out-of-band, they are responsible
|
||||||
|
for choosing an unused channel-ID (simply picking a random 3-or-more digit
|
||||||
|
number is probably enough), and some random words. The invitation code uses
|
||||||
|
the same format in either variant: channel-ID, a hyphen, and an arbitrary
|
||||||
|
string.
|
||||||
|
|
||||||
|
The two machines participating in the wormhole setup are not distinguished:
|
||||||
|
it doesn't matter which one goes first, and both use the same Wormhole class.
|
||||||
|
In the first variant, one side calls `get_code()` while the other calls
|
||||||
|
`set_code()`. In the second variant, both sides call `set_code()`. Note that
|
||||||
|
this is not true for the "Transit" protocol used for bulk data-transfer: the
|
||||||
|
Transit class currently distinguishes "Sender" from "Receiver", so the
|
||||||
|
programs on each side must have some way to decide (ahead of time) which is
|
||||||
|
which.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
The synchronous+blocking flow looks like this:
|
The synchronous+blocking flow looks like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from wormhole.transcribe import Initiator
|
from wormhole.transcribe import Wormhole
|
||||||
from wormhole.public_relay import RENDEZVOUS_RELAY
|
from wormhole.public_relay import RENDEZVOUS_RELAY
|
||||||
mydata = b"initiator's data"
|
mydata = b"initiator's data"
|
||||||
i = Initiator("appid", RENDEZVOUS_RELAY)
|
i = Wormhole("appid", RENDEZVOUS_RELAY)
|
||||||
code = i.get_code()
|
code = i.get_code()
|
||||||
print("Invitation Code: %s" % code)
|
print("Invitation Code: %s" % code)
|
||||||
theirdata = i.get_data(mydata)
|
theirdata = i.get_data(mydata)
|
||||||
|
@ -40,11 +62,11 @@ print("Their data: %s" % theirdata.decode("ascii"))
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import sys
|
import sys
|
||||||
from wormhole.transcribe import Receiver
|
from wormhole.transcribe import Wormhole
|
||||||
from wormhole.public_relay import RENDEZVOUS_RELAY
|
from wormhole.public_relay import RENDEZVOUS_RELAY
|
||||||
mydata = b"receiver's data"
|
mydata = b"receiver's data"
|
||||||
code = sys.argv[1]
|
code = sys.argv[1]
|
||||||
r = Receiver("appid", RENDEZVOUS_RELAY)
|
r = Wormhole("appid", RENDEZVOUS_RELAY)
|
||||||
r.set_code(code)
|
r.set_code(code)
|
||||||
theirdata = r.get_data(mydata)
|
theirdata = r.get_data(mydata)
|
||||||
print("Their data: %s" % theirdata.decode("ascii"))
|
print("Their data: %s" % theirdata.decode("ascii"))
|
||||||
|
@ -57,9 +79,9 @@ The Twisted-friendly flow looks like this:
|
||||||
```python
|
```python
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from wormhole.public_relay import RENDEZVOUS_RELAY
|
from wormhole.public_relay import RENDEZVOUS_RELAY
|
||||||
from wormhole.twisted.transcribe import SymmetricWormhole
|
from wormhole.twisted.transcribe import Wormhole
|
||||||
outbound_message = b"outbound data"
|
outbound_message = b"outbound data"
|
||||||
w1 = SymmetricWormhole("appid", RENDEZVOUS_RELAY)
|
w1 = Wormhole("appid", RENDEZVOUS_RELAY)
|
||||||
d = w1.get_code()
|
d = w1.get_code()
|
||||||
def _got_code(code):
|
def _got_code(code):
|
||||||
print "Invitation Code:", code
|
print "Invitation Code:", code
|
||||||
|
@ -75,9 +97,10 @@ reactor.run()
|
||||||
On the other side, you call `set_code()` instead of waiting for `get_code()`:
|
On the other side, you call `set_code()` instead of waiting for `get_code()`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
w2 = SymmetricWormhole("appid", RENDEZVOUS_RELAY)
|
w2 = Wormhole("appid", RENDEZVOUS_RELAY)
|
||||||
w2.set_code(code)
|
w2.set_code(code)
|
||||||
d = w2.get_data(my_message)
|
d = w2.get_data(my_message)
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
You can call `d=w.get_verifier()` before `get_data()`: this will perform the
|
You can call `d=w.get_verifier()` before `get_data()`: this will perform the
|
||||||
|
@ -90,14 +113,14 @@ pausing.
|
||||||
|
|
||||||
## Generating the Invitation Code
|
## Generating the Invitation Code
|
||||||
|
|
||||||
In most situations, the Initiator will call `i.get_code()` to generate the
|
In most situations, the "sending" or "initiating" side will call
|
||||||
invitation code. This returns a string in the form `NNN-code-words`. The
|
`i.get_code()` to generate the invitation code. This returns a string in the
|
||||||
numeric "NNN" prefix is the "channel id", and is a short integer allocated by
|
form `NNN-code-words`. The numeric "NNN" prefix is the "channel id", and is a
|
||||||
talking to the rendezvous server. The rest is a randomly-generated selection
|
short integer allocated by talking to the rendezvous server. The rest is a
|
||||||
from the PGP wordlist, providing a default of 16 bits of entropy. The
|
randomly-generated selection from the PGP wordlist, providing a default of 16
|
||||||
initiating program should display this code to the user, who should
|
bits of entropy. The initiating program should display this code to the user,
|
||||||
transcribe it to the receiving user, who gives it to the Receiver object by
|
who should transcribe it to the receiving user, who gives it to the Receiver
|
||||||
calling `r.set_code()`. The receiving program can also use
|
object by calling `r.set_code()`. The receiving program can also use
|
||||||
`input_code_with_completion()` to use a readline-based input function: this
|
`input_code_with_completion()` to use a readline-based input function: this
|
||||||
offers tab completion of allocated channel-ids and known codewords.
|
offers tab completion of allocated channel-ids and known codewords.
|
||||||
|
|
||||||
|
@ -168,12 +191,12 @@ Both have defaults suitable for face-to-face realtime setup environments.
|
||||||
|
|
||||||
TODO: only the Twisted form supports serialization so far
|
TODO: only the Twisted form supports serialization so far
|
||||||
|
|
||||||
You may not be able to hold the Initiator/Receiver object in memory for the
|
You may not be able to hold the Wormhole object in memory for the whole sync
|
||||||
whole sync process: maybe you allow it to wait for several days, but the
|
process: maybe you allow it to wait for several days, but the program will be
|
||||||
program will be restarted during that time. To support this, you can persist
|
restarted during that time. To support this, you can persist the state of the
|
||||||
the state of the object by calling `data = w.serialize()`, which will return
|
object by calling `data = w.serialize()`, which will return a printable
|
||||||
a printable bytestring (the JSON-encoding of a small dictionary). To restore,
|
bytestring (the JSON-encoding of a small dictionary). To restore, use the
|
||||||
use the `from_serialized(data)` classmethod (e.g. `w =
|
`from_serialized(data)` classmethod (e.g. `w =
|
||||||
SymmetricWormhole.from_serialized(data)`).
|
SymmetricWormhole.from_serialized(data)`).
|
||||||
|
|
||||||
There is exactly one point at which you can serialize the wormhole: *after*
|
There is exactly one point at which you can serialize the wormhole: *after*
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -20,7 +20,7 @@ setup(name="magic-wormhole",
|
||||||
package_data={"wormhole": ["db-schemas/*.sql"]},
|
package_data={"wormhole": ["db-schemas/*.sql"]},
|
||||||
entry_points={"console_scripts":
|
entry_points={"console_scripts":
|
||||||
["wormhole = wormhole.scripts.runner:entry"]},
|
["wormhole = wormhole.scripts.runner:entry"]},
|
||||||
install_requires=["spake2==0.2", "pynacl", "requests", "argparse"],
|
install_requires=["spake2==0.3", "pynacl", "requests", "argparse"],
|
||||||
test_suite="wormhole.test",
|
test_suite="wormhole.test",
|
||||||
cmdclass=commands,
|
cmdclass=commands,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,45 +1,20 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import sys, time, re, requests, json, textwrap
|
import os, sys, time, re, requests, json
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
from spake2 import SPAKE2_A, SPAKE2_B
|
from spake2 import SPAKE2_Symmetric
|
||||||
from nacl.secret import SecretBox
|
from nacl.secret import SecretBox
|
||||||
from nacl.exceptions import CryptoError
|
from nacl.exceptions import CryptoError
|
||||||
from nacl import utils
|
from nacl import utils
|
||||||
from .eventsource import EventSourceFollower
|
from .eventsource import EventSourceFollower
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from .. import codes
|
from .. import codes
|
||||||
from ..errors import ServerError
|
from ..errors import (ServerError, Timeout, WrongPasswordError,
|
||||||
|
ReflectionAttack, UsageError)
|
||||||
from ..util.hkdf import HKDF
|
from ..util.hkdf import HKDF
|
||||||
|
|
||||||
SECOND = 1
|
SECOND = 1
|
||||||
MINUTE = 60*SECOND
|
MINUTE = 60*SECOND
|
||||||
|
|
||||||
class Timeout(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class WrongPasswordError(Exception):
|
|
||||||
"""
|
|
||||||
Key confirmation failed.
|
|
||||||
"""
|
|
||||||
# or the data blob was corrupted, and that's why decrypt failed
|
|
||||||
def explain(self):
|
|
||||||
return textwrap.dedent(self.__doc__)
|
|
||||||
|
|
||||||
class InitiatorWrongPasswordError(WrongPasswordError):
|
|
||||||
"""
|
|
||||||
Key confirmation failed. Either your correspondent typed the code wrong,
|
|
||||||
or a would-be man-in-the-middle attacker guessed incorrectly. You could
|
|
||||||
try again, giving both your correspondent and the attacker another
|
|
||||||
chance.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ReceiverWrongPasswordError(WrongPasswordError):
|
|
||||||
"""
|
|
||||||
Key confirmation failed. Either you typed the code wrong, or a would-be
|
|
||||||
man-in-the-middle attacker guessed incorrectly. You could try again,
|
|
||||||
giving both you and the attacker another chance.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# relay URLs are:
|
# relay URLs are:
|
||||||
# GET /list -> {channel-ids: [INT..]}
|
# GET /list -> {channel-ids: [INT..]}
|
||||||
# POST /allocate/SIDE -> {channel-id: INT}
|
# POST /allocate/SIDE -> {channel-id: INT}
|
||||||
|
@ -49,16 +24,28 @@ class ReceiverWrongPasswordError(WrongPasswordError):
|
||||||
# GET /CHANNEL-ID/SIDE/poll/MSGNUM (eventsource) -> STR, STR, ..
|
# GET /CHANNEL-ID/SIDE/poll/MSGNUM (eventsource) -> STR, STR, ..
|
||||||
# POST /CHANNEL-ID/SIDE/deallocate -> waiting | deleted
|
# POST /CHANNEL-ID/SIDE/deallocate -> waiting | deleted
|
||||||
|
|
||||||
class Common:
|
class Wormhole:
|
||||||
def url(self, verb, msgnum=None):
|
motd_displayed = False
|
||||||
|
version_warning_displayed = False
|
||||||
|
|
||||||
|
def __init__(self, appid, relay):
|
||||||
|
self.appid = appid
|
||||||
|
self.relay = relay
|
||||||
|
if not self.relay.endswith("/"): raise UsageError
|
||||||
|
self.started = time.time()
|
||||||
|
self.wait = 0.5*SECOND
|
||||||
|
self.timeout = 3*MINUTE
|
||||||
|
self.side = None
|
||||||
|
self.code = None
|
||||||
|
self.key = None
|
||||||
|
self.verifier = None
|
||||||
|
|
||||||
|
def _url(self, verb, msgnum=None):
|
||||||
url = "%s%d/%s/%s" % (self.relay, self.channel_id, self.side, verb)
|
url = "%s%d/%s/%s" % (self.relay, self.channel_id, self.side, verb)
|
||||||
if msgnum is not None:
|
if msgnum is not None:
|
||||||
url += "/" + msgnum
|
url += "/" + msgnum
|
||||||
return url
|
return url
|
||||||
|
|
||||||
motd_displayed = False
|
|
||||||
version_warning_displayed = False
|
|
||||||
|
|
||||||
def handle_welcome(self, welcome):
|
def handle_welcome(self, welcome):
|
||||||
if ("motd" in welcome and
|
if ("motd" in welcome and
|
||||||
not self.motd_displayed):
|
not self.motd_displayed):
|
||||||
|
@ -81,33 +68,17 @@ class Common:
|
||||||
if "error" in welcome:
|
if "error" in welcome:
|
||||||
raise ServerError(welcome["error"], self.relay)
|
raise ServerError(welcome["error"], self.relay)
|
||||||
|
|
||||||
def get(self, old_msgs, verb, msgnum):
|
def _post_json(self, url, post_json=None):
|
||||||
# For now, server errors cause the client to fail. TODO: don't. This
|
# POST to a URL, parsing the response as JSON. Optionally include a
|
||||||
# will require changing the client to re-post messages when the
|
# JSON request body.
|
||||||
# server comes back up.
|
data = None
|
||||||
|
if post_json:
|
||||||
|
data = json.dumps(post_json).encode("utf-8")
|
||||||
|
r = requests.post(url, data=data)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
# note: while this passes around msgs (plural), our callers really
|
def _allocate_channel(self):
|
||||||
# only care about the first one. we use "WHICH" and "SIDE" so that we
|
|
||||||
# only expect to see a single message (not our own, where "SIDE" is
|
|
||||||
# our own, and not messages for earlier stages, where "WHICH" is
|
|
||||||
# different)
|
|
||||||
msgs = old_msgs
|
|
||||||
while not msgs:
|
|
||||||
remaining = self.started + self.timeout - time.time()
|
|
||||||
if remaining < 0:
|
|
||||||
raise Timeout
|
|
||||||
#time.sleep(self.wait)
|
|
||||||
f = EventSourceFollower(self.url(verb, msgnum), remaining)
|
|
||||||
for (eventtype, data) in f.iter_events():
|
|
||||||
if eventtype == "welcome":
|
|
||||||
self.handle_welcome(json.loads(data))
|
|
||||||
if eventtype == "message":
|
|
||||||
msgs = [json.loads(data)["message"]]
|
|
||||||
break
|
|
||||||
f.close()
|
|
||||||
return msgs
|
|
||||||
|
|
||||||
def _allocate(self):
|
|
||||||
r = requests.post(self.relay + "allocate/%s" % self.side)
|
r = requests.post(self.relay + "allocate/%s" % self.side)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
@ -116,124 +87,15 @@ class Common:
|
||||||
channel_id = data["channel-id"]
|
channel_id = data["channel-id"]
|
||||||
return channel_id
|
return channel_id
|
||||||
|
|
||||||
def _post_pake(self):
|
|
||||||
msg = self.sp.start()
|
|
||||||
post_data = {"message": hexlify(msg).decode("ascii")}
|
|
||||||
r = requests.post(self.url("post", "pake"), data=json.dumps(post_data))
|
|
||||||
r.raise_for_status()
|
|
||||||
other_msgs = r.json()["messages"]
|
|
||||||
return other_msgs
|
|
||||||
|
|
||||||
def _get_pake(self, other_msgs):
|
|
||||||
msgs = self.get(other_msgs, "poll", "pake")
|
|
||||||
pake_msg = unhexlify(msgs[0].encode("ascii"))
|
|
||||||
key = self.sp.finish(pake_msg)
|
|
||||||
return key
|
|
||||||
|
|
||||||
def _encrypt_data(self, key, data):
|
|
||||||
assert len(key) == SecretBox.KEY_SIZE
|
|
||||||
box = SecretBox(key)
|
|
||||||
nonce = utils.random(SecretBox.NONCE_SIZE)
|
|
||||||
return box.encrypt(data, nonce)
|
|
||||||
|
|
||||||
def _post_data(self, data):
|
|
||||||
post_data = json.dumps({"message": hexlify(data).decode("ascii")})
|
|
||||||
r = requests.post(self.url("post", "data"), data=post_data)
|
|
||||||
r.raise_for_status()
|
|
||||||
other_msgs = r.json()["messages"]
|
|
||||||
return other_msgs
|
|
||||||
|
|
||||||
def _get_data(self, other_msgs):
|
|
||||||
msgs = self.get(other_msgs, "poll", "data")
|
|
||||||
data = unhexlify(msgs[0].encode("ascii"))
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _decrypt_data(self, key, encrypted):
|
|
||||||
assert len(key) == SecretBox.KEY_SIZE
|
|
||||||
box = SecretBox(key)
|
|
||||||
data = box.decrypt(encrypted)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _deallocate(self):
|
|
||||||
r = requests.post(self.url("deallocate"))
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
|
|
||||||
assert type(purpose) == type(b"")
|
|
||||||
return HKDF(self.key, length, CTXinfo=purpose)
|
|
||||||
|
|
||||||
class Initiator(Common):
|
|
||||||
def __init__(self, appid, relay):
|
|
||||||
self.appid = appid
|
|
||||||
self.relay = relay
|
|
||||||
assert self.relay.endswith("/")
|
|
||||||
self.started = time.time()
|
|
||||||
self.wait = 0.5*SECOND
|
|
||||||
self.timeout = 3*MINUTE
|
|
||||||
self.side = "initiator"
|
|
||||||
self.key = None
|
|
||||||
self.verifier = None
|
|
||||||
|
|
||||||
def set_code(self, code): # used for human-made pre-generated codes
|
|
||||||
mo = re.search(r'^(\d+)-', code)
|
|
||||||
if not mo:
|
|
||||||
raise ValueError("code (%s) must start with NN-" % code)
|
|
||||||
self.channel_id = int(mo.group(1))
|
|
||||||
self.code = code
|
|
||||||
self.sp = SPAKE2_A(self.code.encode("ascii"),
|
|
||||||
idA=self.appid+":Initiator",
|
|
||||||
idB=self.appid+":Receiver")
|
|
||||||
self._post_pake()
|
|
||||||
|
|
||||||
def get_code(self, code_length=2):
|
def get_code(self, code_length=2):
|
||||||
channel_id = self._allocate() # allocate channel
|
if self.code is not None: raise UsageError
|
||||||
|
self.side = hexlify(os.urandom(5))
|
||||||
|
channel_id = self._allocate_channel() # allocate channel
|
||||||
code = codes.make_code(channel_id, code_length)
|
code = codes.make_code(channel_id, code_length)
|
||||||
self.set_code(code)
|
self._set_code_and_channel_id(code)
|
||||||
|
self._start()
|
||||||
return code
|
return code
|
||||||
|
|
||||||
def _wait_for_key(self):
|
|
||||||
if not self.key:
|
|
||||||
key = self._get_pake([])
|
|
||||||
self.key = key
|
|
||||||
self.verifier = self.derive_key(self.appid+b":Verifier")
|
|
||||||
|
|
||||||
def get_verifier(self):
|
|
||||||
self._wait_for_key()
|
|
||||||
return self.verifier
|
|
||||||
|
|
||||||
def get_data(self, outbound_data):
|
|
||||||
self._wait_for_key()
|
|
||||||
try:
|
|
||||||
outbound_key = self.derive_key(b"sender")
|
|
||||||
outbound_encrypted = self._encrypt_data(outbound_key, outbound_data)
|
|
||||||
other_msgs = self._post_data(outbound_encrypted)
|
|
||||||
|
|
||||||
inbound_encrypted = self._get_data(other_msgs)
|
|
||||||
inbound_key = self.derive_key(b"receiver")
|
|
||||||
try:
|
|
||||||
inbound_data = self._decrypt_data(inbound_key,
|
|
||||||
inbound_encrypted)
|
|
||||||
except CryptoError:
|
|
||||||
raise InitiatorWrongPasswordError
|
|
||||||
finally:
|
|
||||||
self._deallocate()
|
|
||||||
return inbound_data
|
|
||||||
|
|
||||||
|
|
||||||
class Receiver(Common):
|
|
||||||
def __init__(self, appid, relay):
|
|
||||||
self.appid = appid
|
|
||||||
self.relay = relay
|
|
||||||
assert self.relay.endswith("/")
|
|
||||||
self.started = time.time()
|
|
||||||
self.wait = 0.5*SECOND
|
|
||||||
self.timeout = 3*MINUTE
|
|
||||||
self.side = "receiver"
|
|
||||||
self.code = None
|
|
||||||
self.channel_id = None
|
|
||||||
self.key = None
|
|
||||||
self.verifier = None
|
|
||||||
|
|
||||||
def list_channels(self):
|
def list_channels(self):
|
||||||
r = requests.get(self.relay + "list")
|
r = requests.get(self.relay + "list")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@ -245,43 +107,117 @@ class Receiver(Common):
|
||||||
code_length)
|
code_length)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
def set_code(self, code):
|
def set_code(self, code): # used for human-made pre-generated codes
|
||||||
assert self.code is None
|
if self.code is not None: raise UsageError
|
||||||
assert self.channel_id is None
|
if self.side is not None: raise UsageError
|
||||||
self.code = code
|
self._set_code_and_channel_id(code)
|
||||||
self.channel_id = codes.extract_channel_id(code)
|
self.side = hexlify(os.urandom(5))
|
||||||
self.sp = SPAKE2_B(code.encode("ascii"),
|
self._start()
|
||||||
idA=self.appid+":Initiator",
|
|
||||||
idB=self.appid+":Receiver")
|
|
||||||
|
|
||||||
def _wait_for_key(self):
|
def _set_code_and_channel_id(self, code):
|
||||||
|
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.channel_id = int(mo.group(1))
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
def _start(self):
|
||||||
|
# allocate the rest now too, so it can be serialized
|
||||||
|
self.sp = SPAKE2_Symmetric(self.code.encode("ascii"),
|
||||||
|
idSymmetric=self.appid)
|
||||||
|
self.msg1 = self.sp.start()
|
||||||
|
|
||||||
|
def _post_message(self, url, msg):
|
||||||
|
# TODO: retry on failure, with exponential backoff. We're guarding
|
||||||
|
# against the rendezvous server being temporarily offline.
|
||||||
|
if not isinstance(msg, type(b"")): raise UsageError(type(msg))
|
||||||
|
resp = self._post_json(url, {"message": hexlify(msg).decode("ascii")})
|
||||||
|
return resp["messages"] # other_msgs
|
||||||
|
|
||||||
|
def _get_message(self, old_msgs, verb, msgnum):
|
||||||
|
# For now, server errors cause the client to fail. TODO: don't. This
|
||||||
|
# will require changing the client to re-post messages when the
|
||||||
|
# server comes back up.
|
||||||
|
|
||||||
|
# fire with a bytestring of the first message that matches
|
||||||
|
# verb/msgnum, which either came from old_msgs, or from an
|
||||||
|
# EventSource that we attached to the corresponding URL
|
||||||
|
msgs = old_msgs
|
||||||
|
while not msgs:
|
||||||
|
remaining = self.started + self.timeout - time.time()
|
||||||
|
if remaining < 0:
|
||||||
|
raise Timeout
|
||||||
|
#time.sleep(self.wait)
|
||||||
|
f = EventSourceFollower(self._url(verb, msgnum), remaining)
|
||||||
|
for (eventtype, data) in f.iter_events():
|
||||||
|
if eventtype == "welcome":
|
||||||
|
self.handle_welcome(json.loads(data))
|
||||||
|
if eventtype == "message":
|
||||||
|
msgs = [json.loads(data)["message"]]
|
||||||
|
break
|
||||||
|
f.close()
|
||||||
|
return unhexlify(msgs[0].encode("ascii"))
|
||||||
|
|
||||||
|
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
|
||||||
|
if not isinstance(purpose, type(b"")): raise UsageError
|
||||||
|
return HKDF(self.key, length, CTXinfo=purpose)
|
||||||
|
|
||||||
|
def _encrypt_data(self, key, data):
|
||||||
|
if len(key) != SecretBox.KEY_SIZE: raise UsageError
|
||||||
|
box = SecretBox(key)
|
||||||
|
nonce = utils.random(SecretBox.NONCE_SIZE)
|
||||||
|
return box.encrypt(data, nonce)
|
||||||
|
|
||||||
|
def _decrypt_data(self, key, encrypted):
|
||||||
|
if len(key) != SecretBox.KEY_SIZE: raise UsageError
|
||||||
|
box = SecretBox(key)
|
||||||
|
data = box.decrypt(encrypted)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _get_key(self):
|
||||||
if not self.key:
|
if not self.key:
|
||||||
other_msgs = self._post_pake()
|
old_msgs = self._post_message(self._url("post", "pake"), self.msg1)
|
||||||
key = self._get_pake(other_msgs)
|
pake_msg = self._get_message(old_msgs, "poll", "pake")
|
||||||
self.key = key
|
self.key = self.sp.finish(pake_msg)
|
||||||
self.verifier = self.derive_key(self.appid+b":Verifier")
|
self.verifier = self.derive_key(self.appid+b":Verifier")
|
||||||
|
|
||||||
def get_verifier(self):
|
def get_verifier(self):
|
||||||
self._wait_for_key()
|
if self.code is None: raise UsageError
|
||||||
|
if self.channel_id is None: raise UsageError
|
||||||
|
self._get_key()
|
||||||
return self.verifier
|
return self.verifier
|
||||||
|
|
||||||
def get_data(self, outbound_data):
|
def get_data(self, outbound_data):
|
||||||
assert self.code is not None
|
# only call this once
|
||||||
assert self.channel_id is not None
|
if self.code is None: raise UsageError
|
||||||
self._wait_for_key()
|
if self.channel_id is None: raise UsageError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
outbound_key = self.derive_key(b"receiver")
|
self._get_key()
|
||||||
outbound_encrypted = self._encrypt_data(outbound_key, outbound_data)
|
return self._get_data2(outbound_data)
|
||||||
other_msgs = self._post_data(outbound_encrypted)
|
|
||||||
|
|
||||||
inbound_encrypted = self._get_data(other_msgs)
|
|
||||||
inbound_key = self.derive_key(b"sender")
|
|
||||||
try:
|
|
||||||
inbound_data = self._decrypt_data(inbound_key,
|
|
||||||
inbound_encrypted)
|
|
||||||
except CryptoError:
|
|
||||||
raise ReceiverWrongPasswordError
|
|
||||||
finally:
|
finally:
|
||||||
self._deallocate()
|
self._deallocate()
|
||||||
|
|
||||||
|
def _get_data2(self, outbound_data):
|
||||||
|
# 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 check for reflection.
|
||||||
|
data_key = self.derive_key(b"data-key")
|
||||||
|
|
||||||
|
outbound_encrypted = self._encrypt_data(data_key, outbound_data)
|
||||||
|
msgs = self._post_message(self._url("post", "data"), outbound_encrypted)
|
||||||
|
|
||||||
|
inbound_encrypted = self._get_message(msgs, "poll", "data")
|
||||||
|
if inbound_encrypted == outbound_encrypted:
|
||||||
|
raise ReflectionAttack
|
||||||
|
try:
|
||||||
|
inbound_data = self._decrypt_data(data_key, inbound_encrypted)
|
||||||
return inbound_data
|
return inbound_data
|
||||||
|
except CryptoError:
|
||||||
|
raise WrongPasswordError
|
||||||
|
|
||||||
|
def _deallocate(self):
|
||||||
|
# only try once, no retries
|
||||||
|
requests.post(self._url("deallocate"))
|
||||||
|
# ignore POST failure, don't call r.raise_for_status()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import functools
|
import functools, textwrap
|
||||||
|
|
||||||
class ServerError(Exception):
|
class ServerError(Exception):
|
||||||
def __init__(self, message, relay):
|
def __init__(self, message, relay):
|
||||||
|
@ -16,3 +16,23 @@ def handle_server_error(func):
|
||||||
print("Server error (from %s):\n%s" % (e.relay, e.message))
|
print("Server error (from %s):\n%s" % (e.relay, e.message))
|
||||||
return 1
|
return 1
|
||||||
return _wrap
|
return _wrap
|
||||||
|
|
||||||
|
class Timeout(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WrongPasswordError(Exception):
|
||||||
|
"""
|
||||||
|
Key confirmation failed. Either you or your correspondent typed the code
|
||||||
|
wrong, or a would-be man-in-the-middle attacker guessed incorrectly. You
|
||||||
|
could try again, giving both your correspondent and the attacker another
|
||||||
|
chance.
|
||||||
|
"""
|
||||||
|
# or the data blob was corrupted, and that's why decrypt failed
|
||||||
|
def explain(self):
|
||||||
|
return textwrap.dedent(self.__doc__)
|
||||||
|
|
||||||
|
class ReflectionAttack(Exception):
|
||||||
|
"""An attacker (or bug) reflected our outgoing message back to us."""
|
||||||
|
|
||||||
|
class UsageError(Exception):
|
||||||
|
"""The programmer did something wrong."""
|
||||||
|
|
|
@ -7,24 +7,24 @@ APPID = "lothar.com/wormhole/file-xfer"
|
||||||
@handle_server_error
|
@handle_server_error
|
||||||
def receive_file(args):
|
def receive_file(args):
|
||||||
# we're receiving
|
# we're receiving
|
||||||
from ..blocking.transcribe import Receiver, WrongPasswordError
|
from ..blocking.transcribe import Wormhole, WrongPasswordError
|
||||||
from ..blocking.transit import TransitReceiver, TransitError
|
from ..blocking.transit import TransitReceiver, TransitError
|
||||||
from .progress import start_progress, update_progress, finish_progress
|
from .progress import start_progress, update_progress, finish_progress
|
||||||
|
|
||||||
transit_receiver = TransitReceiver(args.transit_helper)
|
transit_receiver = TransitReceiver(args.transit_helper)
|
||||||
|
|
||||||
r = Receiver(APPID, args.relay_url)
|
w = Wormhole(APPID, args.relay_url)
|
||||||
if args.zeromode:
|
if args.zeromode:
|
||||||
assert not args.code
|
assert not args.code
|
||||||
args.code = "0-"
|
args.code = "0-"
|
||||||
code = args.code
|
code = args.code
|
||||||
if not code:
|
if not code:
|
||||||
code = r.input_code("Enter receive-file wormhole code: ",
|
code = w.input_code("Enter receive-file wormhole code: ",
|
||||||
args.code_length)
|
args.code_length)
|
||||||
r.set_code(code)
|
w.set_code(code)
|
||||||
|
|
||||||
if args.verify:
|
if args.verify:
|
||||||
verifier = binascii.hexlify(r.get_verifier())
|
verifier = binascii.hexlify(w.get_verifier())
|
||||||
print("Verifier %s." % verifier)
|
print("Verifier %s." % verifier)
|
||||||
|
|
||||||
mydata = json.dumps({
|
mydata = json.dumps({
|
||||||
|
@ -34,7 +34,7 @@ def receive_file(args):
|
||||||
},
|
},
|
||||||
}).encode("utf-8")
|
}).encode("utf-8")
|
||||||
try:
|
try:
|
||||||
data = json.loads(r.get_data(mydata).decode("utf-8"))
|
data = json.loads(w.get_data(mydata).decode("utf-8"))
|
||||||
except WrongPasswordError as e:
|
except WrongPasswordError as e:
|
||||||
print("ERROR: " + e.explain(), file=sys.stderr)
|
print("ERROR: " + e.explain(), file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
@ -50,7 +50,7 @@ def receive_file(args):
|
||||||
|
|
||||||
# now receive the rest of the owl
|
# now receive the rest of the owl
|
||||||
tdata = data["transit"]
|
tdata = data["transit"]
|
||||||
transit_key = r.derive_key(APPID+"/transit-key")
|
transit_key = w.derive_key(APPID+"/transit-key")
|
||||||
transit_receiver.set_transit_key(transit_key)
|
transit_receiver.set_transit_key(transit_key)
|
||||||
transit_receiver.add_their_direct_hints(tdata["direct_connection_hints"])
|
transit_receiver.add_their_direct_hints(tdata["direct_connection_hints"])
|
||||||
transit_receiver.add_their_relay_hints(tdata["relay_connection_hints"])
|
transit_receiver.add_their_relay_hints(tdata["relay_connection_hints"])
|
||||||
|
|
|
@ -7,25 +7,25 @@ APPID = "lothar.com/wormhole/text-xfer"
|
||||||
@handle_server_error
|
@handle_server_error
|
||||||
def receive_text(args):
|
def receive_text(args):
|
||||||
# we're receiving
|
# we're receiving
|
||||||
from ..blocking.transcribe import Receiver, WrongPasswordError
|
from ..blocking.transcribe import Wormhole, WrongPasswordError
|
||||||
|
|
||||||
r = Receiver(APPID, args.relay_url)
|
w = Wormhole(APPID, args.relay_url)
|
||||||
if args.zeromode:
|
if args.zeromode:
|
||||||
assert not args.code
|
assert not args.code
|
||||||
args.code = "0-"
|
args.code = "0-"
|
||||||
code = args.code
|
code = args.code
|
||||||
if not code:
|
if not code:
|
||||||
code = r.input_code("Enter receive-text wormhole code: ",
|
code = w.input_code("Enter receive-text wormhole code: ",
|
||||||
args.code_length)
|
args.code_length)
|
||||||
r.set_code(code)
|
w.set_code(code)
|
||||||
|
|
||||||
if args.verify:
|
if args.verify:
|
||||||
verifier = binascii.hexlify(r.get_verifier())
|
verifier = binascii.hexlify(w.get_verifier())
|
||||||
print("Verifier %s." % verifier)
|
print("Verifier %s." % verifier)
|
||||||
|
|
||||||
data = json.dumps({"message": "ok"}).encode("utf-8")
|
data = json.dumps({"message": "ok"}).encode("utf-8")
|
||||||
try:
|
try:
|
||||||
them_bytes = r.get_data(data)
|
them_bytes = w.get_data(data)
|
||||||
except WrongPasswordError as e:
|
except WrongPasswordError as e:
|
||||||
print("ERROR: " + e.explain(), file=sys.stderr)
|
print("ERROR: " + e.explain(), file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -7,7 +7,7 @@ APPID = "lothar.com/wormhole/file-xfer"
|
||||||
@handle_server_error
|
@handle_server_error
|
||||||
def send_file(args):
|
def send_file(args):
|
||||||
# we're sending
|
# we're sending
|
||||||
from ..blocking.transcribe import Initiator, WrongPasswordError
|
from ..blocking.transcribe import Wormhole, WrongPasswordError
|
||||||
from ..blocking.transit import TransitSender
|
from ..blocking.transit import TransitSender
|
||||||
from .progress import start_progress, update_progress, finish_progress
|
from .progress import start_progress, update_progress, finish_progress
|
||||||
|
|
||||||
|
@ -15,15 +15,15 @@ def send_file(args):
|
||||||
assert os.path.isfile(filename)
|
assert os.path.isfile(filename)
|
||||||
transit_sender = TransitSender(args.transit_helper)
|
transit_sender = TransitSender(args.transit_helper)
|
||||||
|
|
||||||
i = Initiator(APPID, args.relay_url)
|
w = Wormhole(APPID, args.relay_url)
|
||||||
if args.zeromode:
|
if args.zeromode:
|
||||||
assert not args.code
|
assert not args.code
|
||||||
args.code = "0-"
|
args.code = "0-"
|
||||||
if args.code:
|
if args.code:
|
||||||
i.set_code(args.code)
|
w.set_code(args.code)
|
||||||
code = args.code
|
code = args.code
|
||||||
else:
|
else:
|
||||||
code = i.get_code(args.code_length)
|
code = w.get_code(args.code_length)
|
||||||
other_cmd = "wormhole receive-file"
|
other_cmd = "wormhole receive-file"
|
||||||
if args.verify:
|
if args.verify:
|
||||||
other_cmd = "wormhole --verify receive-file"
|
other_cmd = "wormhole --verify receive-file"
|
||||||
|
@ -35,7 +35,7 @@ def send_file(args):
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if args.verify:
|
if args.verify:
|
||||||
verifier = binascii.hexlify(i.get_verifier())
|
verifier = binascii.hexlify(w.get_verifier())
|
||||||
while True:
|
while True:
|
||||||
ok = raw_input("Verifier %s. ok? (yes/no): " % verifier)
|
ok = raw_input("Verifier %s. ok? (yes/no): " % verifier)
|
||||||
if ok.lower() == "yes":
|
if ok.lower() == "yes":
|
||||||
|
@ -45,7 +45,7 @@ def send_file(args):
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
reject_data = json.dumps({"error": "verification rejected",
|
reject_data = json.dumps({"error": "verification rejected",
|
||||||
}).encode("utf-8")
|
}).encode("utf-8")
|
||||||
i.get_data(reject_data)
|
w.get_data(reject_data)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
filesize = os.stat(filename).st_size
|
filesize = os.stat(filename).st_size
|
||||||
|
@ -61,7 +61,7 @@ def send_file(args):
|
||||||
}).encode("utf-8")
|
}).encode("utf-8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
them_bytes = i.get_data(data)
|
them_bytes = w.get_data(data)
|
||||||
except WrongPasswordError as e:
|
except WrongPasswordError as e:
|
||||||
print("ERROR: " + e.explain(), file=sys.stderr)
|
print("ERROR: " + e.explain(), file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
@ -70,7 +70,7 @@ def send_file(args):
|
||||||
|
|
||||||
|
|
||||||
tdata = them_d["transit"]
|
tdata = them_d["transit"]
|
||||||
transit_key = i.derive_key(APPID+"/transit-key")
|
transit_key = w.derive_key(APPID+"/transit-key")
|
||||||
transit_sender.set_transit_key(transit_key)
|
transit_sender.set_transit_key(transit_key)
|
||||||
transit_sender.add_their_direct_hints(tdata["direct_connection_hints"])
|
transit_sender.add_their_direct_hints(tdata["direct_connection_hints"])
|
||||||
transit_sender.add_their_relay_hints(tdata["relay_connection_hints"])
|
transit_sender.add_their_relay_hints(tdata["relay_connection_hints"])
|
||||||
|
|
|
@ -7,17 +7,17 @@ APPID = "lothar.com/wormhole/text-xfer"
|
||||||
@handle_server_error
|
@handle_server_error
|
||||||
def send_text(args):
|
def send_text(args):
|
||||||
# we're sending
|
# we're sending
|
||||||
from ..blocking.transcribe import Initiator, WrongPasswordError
|
from ..blocking.transcribe import Wormhole, WrongPasswordError
|
||||||
|
|
||||||
i = Initiator(APPID, args.relay_url)
|
w = Wormhole(APPID, args.relay_url)
|
||||||
if args.zeromode:
|
if args.zeromode:
|
||||||
assert not args.code
|
assert not args.code
|
||||||
args.code = "0-"
|
args.code = "0-"
|
||||||
if args.code:
|
if args.code:
|
||||||
i.set_code(args.code)
|
w.set_code(args.code)
|
||||||
code = args.code
|
code = args.code
|
||||||
else:
|
else:
|
||||||
code = i.get_code(args.code_length)
|
code = w.get_code(args.code_length)
|
||||||
other_cmd = "wormhole receive-text"
|
other_cmd = "wormhole receive-text"
|
||||||
if args.verify:
|
if args.verify:
|
||||||
other_cmd = "wormhole --verify receive-text"
|
other_cmd = "wormhole --verify receive-text"
|
||||||
|
@ -29,7 +29,7 @@ def send_text(args):
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
if args.verify:
|
if args.verify:
|
||||||
verifier = binascii.hexlify(i.get_verifier())
|
verifier = binascii.hexlify(w.get_verifier())
|
||||||
while True:
|
while True:
|
||||||
ok = raw_input("Verifier %s. ok? (yes/no): " % verifier)
|
ok = raw_input("Verifier %s. ok? (yes/no): " % verifier)
|
||||||
if ok.lower() == "yes":
|
if ok.lower() == "yes":
|
||||||
|
@ -39,14 +39,14 @@ def send_text(args):
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
reject_data = json.dumps({"error": "verification rejected",
|
reject_data = json.dumps({"error": "verification rejected",
|
||||||
}).encode("utf-8")
|
}).encode("utf-8")
|
||||||
i.get_data(reject_data)
|
w.get_data(reject_data)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
message = args.text
|
message = args.text
|
||||||
data = json.dumps({"message": message,
|
data = json.dumps({"message": message,
|
||||||
}).encode("utf-8")
|
}).encode("utf-8")
|
||||||
try:
|
try:
|
||||||
them_bytes = i.get_data(data)
|
them_bytes = w.get_data(data)
|
||||||
except WrongPasswordError as e:
|
except WrongPasswordError as e:
|
||||||
print("ERROR: " + e.explain(), file=sys.stderr)
|
print("ERROR: " + e.explain(), file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import json
|
import json
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
from twisted.internet.threads import deferToThread
|
||||||
from twisted.application import service
|
from twisted.application import service
|
||||||
from ..servers.relay import RelayServer
|
from ..servers.relay import RelayServer
|
||||||
from ..twisted.transcribe import SymmetricWormhole, UsageError
|
from ..twisted.transcribe import Wormhole, UsageError
|
||||||
from ..twisted.util import allocate_ports
|
from ..twisted.util import allocate_ports
|
||||||
|
from ..blocking.transcribe import Wormhole as BlockingWormhole
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
#from twisted.python import log
|
#from twisted.python import log
|
||||||
#import sys
|
#import sys
|
||||||
#log.startLogging(sys.stdout)
|
#log.startLogging(sys.stdout)
|
||||||
|
|
||||||
class Basic(unittest.TestCase):
|
class ServerBase:
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.sp = service.MultiService()
|
self.sp = service.MultiService()
|
||||||
self.sp.startService()
|
self.sp.startService()
|
||||||
|
@ -29,10 +31,11 @@ class Basic(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
return self.sp.stopService()
|
return self.sp.stopService()
|
||||||
|
|
||||||
|
class Basic(ServerBase, unittest.TestCase):
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
appid = "appid"
|
appid = "appid"
|
||||||
w1 = SymmetricWormhole(appid, self.relayurl)
|
w1 = Wormhole(appid, self.relayurl)
|
||||||
w2 = SymmetricWormhole(appid, self.relayurl)
|
w2 = Wormhole(appid, self.relayurl)
|
||||||
d = w1.get_code()
|
d = w1.get_code()
|
||||||
def _got_code(code):
|
def _got_code(code):
|
||||||
w2.set_code(code)
|
w2.set_code(code)
|
||||||
|
@ -43,8 +46,8 @@ class Basic(unittest.TestCase):
|
||||||
def _done(dl):
|
def _done(dl):
|
||||||
((success1, dataX), (success2, dataY)) = dl
|
((success1, dataX), (success2, dataY)) = dl
|
||||||
r1,r2 = dl
|
r1,r2 = dl
|
||||||
self.assertTrue(success1)
|
self.assertTrue(success1, dataX)
|
||||||
self.assertTrue(success2)
|
self.assertTrue(success2, dataY)
|
||||||
self.assertEqual(dataX, "data2")
|
self.assertEqual(dataX, "data2")
|
||||||
self.assertEqual(dataY, "data1")
|
self.assertEqual(dataY, "data1")
|
||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
|
@ -52,8 +55,8 @@ class Basic(unittest.TestCase):
|
||||||
|
|
||||||
def test_fixed_code(self):
|
def test_fixed_code(self):
|
||||||
appid = "appid"
|
appid = "appid"
|
||||||
w1 = SymmetricWormhole(appid, self.relayurl)
|
w1 = Wormhole(appid, self.relayurl)
|
||||||
w2 = SymmetricWormhole(appid, self.relayurl)
|
w2 = Wormhole(appid, self.relayurl)
|
||||||
w1.set_code("123-purple-elephant")
|
w1.set_code("123-purple-elephant")
|
||||||
w2.set_code("123-purple-elephant")
|
w2.set_code("123-purple-elephant")
|
||||||
d1 = w1.get_data("data1")
|
d1 = w1.get_data("data1")
|
||||||
|
@ -62,8 +65,8 @@ class Basic(unittest.TestCase):
|
||||||
def _done(dl):
|
def _done(dl):
|
||||||
((success1, dataX), (success2, dataY)) = dl
|
((success1, dataX), (success2, dataY)) = dl
|
||||||
r1,r2 = dl
|
r1,r2 = dl
|
||||||
self.assertTrue(success1)
|
self.assertTrue(success1, dataX)
|
||||||
self.assertTrue(success2)
|
self.assertTrue(success2, dataY)
|
||||||
self.assertEqual(dataX, "data2")
|
self.assertEqual(dataX, "data2")
|
||||||
self.assertEqual(dataY, "data1")
|
self.assertEqual(dataY, "data1")
|
||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
|
@ -71,22 +74,22 @@ class Basic(unittest.TestCase):
|
||||||
|
|
||||||
def test_errors(self):
|
def test_errors(self):
|
||||||
appid = "appid"
|
appid = "appid"
|
||||||
w1 = SymmetricWormhole(appid, self.relayurl)
|
w1 = Wormhole(appid, self.relayurl)
|
||||||
self.assertRaises(UsageError, w1.get_verifier)
|
self.assertRaises(UsageError, w1.get_verifier)
|
||||||
self.assertRaises(UsageError, w1.get_data, "data")
|
self.assertRaises(UsageError, w1.get_data, "data")
|
||||||
w1.set_code("123-purple-elephant")
|
w1.set_code("123-purple-elephant")
|
||||||
self.assertRaises(UsageError, w1.set_code, "123-nope")
|
self.assertRaises(UsageError, w1.set_code, "123-nope")
|
||||||
self.assertRaises(UsageError, w1.get_code)
|
self.assertRaises(UsageError, w1.get_code)
|
||||||
w2 = SymmetricWormhole(appid, self.relayurl)
|
w2 = Wormhole(appid, self.relayurl)
|
||||||
d = w2.get_code()
|
d = w2.get_code()
|
||||||
self.assertRaises(UsageError, w2.get_code)
|
self.assertRaises(UsageError, w2.get_code)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def test_serialize(self):
|
def test_serialize(self):
|
||||||
appid = "appid"
|
appid = "appid"
|
||||||
w1 = SymmetricWormhole(appid, self.relayurl)
|
w1 = Wormhole(appid, self.relayurl)
|
||||||
self.assertRaises(UsageError, w1.serialize) # too early
|
self.assertRaises(UsageError, w1.serialize) # too early
|
||||||
w2 = SymmetricWormhole(appid, self.relayurl)
|
w2 = Wormhole(appid, self.relayurl)
|
||||||
d = w1.get_code()
|
d = w1.get_code()
|
||||||
def _got_code(code):
|
def _got_code(code):
|
||||||
self.assertRaises(UsageError, w2.serialize) # too early
|
self.assertRaises(UsageError, w2.serialize) # too early
|
||||||
|
@ -96,7 +99,7 @@ class Basic(unittest.TestCase):
|
||||||
self.assertEqual(type(s), type(""))
|
self.assertEqual(type(s), type(""))
|
||||||
unpacked = json.loads(s) # this is supposed to be JSON
|
unpacked = json.loads(s) # this is supposed to be JSON
|
||||||
self.assertEqual(type(unpacked), dict)
|
self.assertEqual(type(unpacked), dict)
|
||||||
new_w1 = SymmetricWormhole.from_serialized(s)
|
new_w1 = Wormhole.from_serialized(s)
|
||||||
d1 = new_w1.get_data("data1")
|
d1 = new_w1.get_data("data1")
|
||||||
d2 = w2.get_data("data2")
|
d2 = w2.get_data("data2")
|
||||||
return defer.DeferredList([d1,d2], fireOnOneErrback=False)
|
return defer.DeferredList([d1,d2], fireOnOneErrback=False)
|
||||||
|
@ -104,10 +107,100 @@ class Basic(unittest.TestCase):
|
||||||
def _done(dl):
|
def _done(dl):
|
||||||
((success1, dataX), (success2, dataY)) = dl
|
((success1, dataX), (success2, dataY)) = dl
|
||||||
r1,r2 = dl
|
r1,r2 = dl
|
||||||
self.assertTrue(success1)
|
self.assertTrue(success1, dataX)
|
||||||
self.assertTrue(success2)
|
self.assertTrue(success2, dataY)
|
||||||
self.assertEqual(dataX, "data2")
|
self.assertEqual(dataX, "data2")
|
||||||
self.assertEqual(dataY, "data1")
|
self.assertEqual(dataY, "data1")
|
||||||
self.assertRaises(UsageError, w2.serialize) # too late
|
self.assertRaises(UsageError, w2.serialize) # too late
|
||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
class Blocking(ServerBase, unittest.TestCase):
|
||||||
|
# we need Twisted to run the server, but we run the sender and receiver
|
||||||
|
# with deferToThread()
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
appid = "appid"
|
||||||
|
w1 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
w2 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
d = deferToThread(w1.get_code)
|
||||||
|
def _got_code(code):
|
||||||
|
w2.set_code(code)
|
||||||
|
d1 = deferToThread(w1.get_data, "data1")
|
||||||
|
d2 = deferToThread(w2.get_data, "data2")
|
||||||
|
return defer.DeferredList([d1,d2], fireOnOneErrback=False)
|
||||||
|
d.addCallback(_got_code)
|
||||||
|
def _done(dl):
|
||||||
|
((success1, dataX), (success2, dataY)) = dl
|
||||||
|
r1,r2 = dl
|
||||||
|
self.assertTrue(success1, dataX)
|
||||||
|
self.assertTrue(success2, dataY)
|
||||||
|
self.assertEqual(dataX, "data2")
|
||||||
|
self.assertEqual(dataY, "data1")
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_fixed_code(self):
|
||||||
|
appid = "appid"
|
||||||
|
w1 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
w2 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
w1.set_code("123-purple-elephant")
|
||||||
|
w2.set_code("123-purple-elephant")
|
||||||
|
d1 = deferToThread(w1.get_data, "data1")
|
||||||
|
d2 = deferToThread(w2.get_data, "data2")
|
||||||
|
d = defer.DeferredList([d1,d2], fireOnOneErrback=False)
|
||||||
|
def _done(dl):
|
||||||
|
((success1, dataX), (success2, dataY)) = dl
|
||||||
|
r1,r2 = dl
|
||||||
|
self.assertTrue(success1, dataX)
|
||||||
|
self.assertTrue(success2, dataY)
|
||||||
|
self.assertEqual(dataX, "data2")
|
||||||
|
self.assertEqual(dataY, "data1")
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
appid = "appid"
|
||||||
|
w1 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
self.assertRaises(UsageError, w1.get_verifier)
|
||||||
|
self.assertRaises(UsageError, w1.get_data, "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)
|
||||||
|
d = deferToThread(w2.get_code)
|
||||||
|
def _done(code):
|
||||||
|
self.assertRaises(UsageError, w2.get_code)
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
appid = "appid"
|
||||||
|
w1 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
self.assertRaises(UsageError, w1.serialize) # too early
|
||||||
|
w2 = BlockingWormhole(appid, self.relayurl)
|
||||||
|
d = deferToThread(w1.get_code)
|
||||||
|
def _got_code(code):
|
||||||
|
self.assertRaises(UsageError, w2.serialize) # too early
|
||||||
|
w2.set_code(code)
|
||||||
|
w2.serialize() # ok
|
||||||
|
s = w1.serialize()
|
||||||
|
self.assertEqual(type(s), type(""))
|
||||||
|
unpacked = json.loads(s) # this is supposed to be JSON
|
||||||
|
self.assertEqual(type(unpacked), dict)
|
||||||
|
new_w1 = BlockingWormhole.from_serialized(s)
|
||||||
|
d1 = deferToThread(new_w1.get_data, "data1")
|
||||||
|
d2 = deferToThread(w2.get_data, "data2")
|
||||||
|
return defer.DeferredList([d1,d2], fireOnOneErrback=False)
|
||||||
|
d.addCallback(_got_code)
|
||||||
|
def _done(dl):
|
||||||
|
((success1, dataX), (success2, dataY)) = dl
|
||||||
|
r1,r2 = dl
|
||||||
|
self.assertTrue(success1, dataX)
|
||||||
|
self.assertTrue(success2, dataY)
|
||||||
|
self.assertEqual(dataX, "data2")
|
||||||
|
self.assertEqual(dataY, "data1")
|
||||||
|
self.assertRaises(UsageError, w2.serialize) # too late
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
test_serialize.skip = "not yet implemented for the blocking flavor"
|
||||||
|
|
|
@ -1,28 +1,38 @@
|
||||||
import sys
|
import sys, json
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from .transcribe import SymmetricWormhole
|
from .transcribe import Wormhole
|
||||||
from .. import public_relay
|
from .. import public_relay
|
||||||
|
|
||||||
APPID = "lothar.com/wormhole/text-xfer"
|
APPID = "lothar.com/wormhole/text-xfer"
|
||||||
|
|
||||||
w = SymmetricWormhole(APPID, public_relay.RENDEZVOUS_RELAY)
|
w = Wormhole(APPID, public_relay.RENDEZVOUS_RELAY)
|
||||||
|
|
||||||
if sys.argv[1] == "send-text":
|
if sys.argv[1] == "send-text":
|
||||||
message = sys.argv[2]
|
message = sys.argv[2]
|
||||||
|
data = json.dumps({"message": message}).encode("utf-8")
|
||||||
d = w.get_code()
|
d = w.get_code()
|
||||||
def _got_code(code):
|
def _got_code(code):
|
||||||
print "code is:", code
|
print "code is:", code
|
||||||
return w.get_data(message)
|
return w.get_data(data)
|
||||||
d.addCallback(_got_code)
|
d.addCallback(_got_code)
|
||||||
def _got_data(their_data):
|
def _got_data(them_bytes):
|
||||||
print "ack:", their_data
|
them_d = json.loads(them_bytes.decode("utf-8"))
|
||||||
|
if them_d["message"] == "ok":
|
||||||
|
print "text sent"
|
||||||
|
else:
|
||||||
|
print "error sending text: %r" % (them_d,)
|
||||||
d.addCallback(_got_data)
|
d.addCallback(_got_data)
|
||||||
elif sys.argv[1] == "receive-text":
|
elif sys.argv[1] == "receive-text":
|
||||||
code = sys.argv[2]
|
code = sys.argv[2]
|
||||||
w.set_code(code)
|
w.set_code(code)
|
||||||
d = w.get_data("ok")
|
data = json.dumps({"message": "ok"}).encode("utf-8")
|
||||||
def _got_data(their_data):
|
d = w.get_data(data)
|
||||||
print their_data
|
def _got_data(them_bytes):
|
||||||
|
them_d = json.loads(them_bytes.decode("utf-8"))
|
||||||
|
if "error" in them_d:
|
||||||
|
print >>sys.stderr, "ERROR: " + them_d["error"]
|
||||||
|
return 1
|
||||||
|
print them_d["message"]
|
||||||
d.addCallback(_got_data)
|
d.addCallback(_got_data)
|
||||||
else:
|
else:
|
||||||
raise ValueError("bad command")
|
raise ValueError("bad command")
|
||||||
|
|
|
@ -10,23 +10,13 @@ from nacl.secret import SecretBox
|
||||||
from nacl.exceptions import CryptoError
|
from nacl.exceptions import CryptoError
|
||||||
from nacl import utils
|
from nacl import utils
|
||||||
from spake2 import SPAKE2_Symmetric
|
from spake2 import SPAKE2_Symmetric
|
||||||
from .eventsource import ReconnectingEventSource
|
from .eventsource_twisted import ReconnectingEventSource
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from .. import codes
|
from .. import codes
|
||||||
from ..errors import ServerError
|
from ..errors import (ServerError, WrongPasswordError,
|
||||||
|
ReflectionAttack, UsageError)
|
||||||
from ..util.hkdf import HKDF
|
from ..util.hkdf import HKDF
|
||||||
|
|
||||||
class WrongPasswordError(Exception):
|
|
||||||
"""
|
|
||||||
Key confirmation failed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class ReflectionAttack(Exception):
|
|
||||||
"""An attacker (or bug) reflected our outgoing message back to us."""
|
|
||||||
|
|
||||||
class UsageError(Exception):
|
|
||||||
"""The programmer did something wrong."""
|
|
||||||
|
|
||||||
@implementer(IBodyProducer)
|
@implementer(IBodyProducer)
|
||||||
class DataProducer:
|
class DataProducer:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
|
@ -43,7 +33,10 @@ class DataProducer:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SymmetricWormhole:
|
class Wormhole:
|
||||||
|
motd_displayed = False
|
||||||
|
version_warning_displayed = False
|
||||||
|
|
||||||
def __init__(self, appid, relay):
|
def __init__(self, appid, relay):
|
||||||
self.appid = appid
|
self.appid = appid
|
||||||
self.relay = relay
|
self.relay = relay
|
||||||
|
@ -53,6 +46,61 @@ class SymmetricWormhole:
|
||||||
self.key = None
|
self.key = None
|
||||||
self._started_get_code = False
|
self._started_get_code = False
|
||||||
|
|
||||||
|
def _url(self, verb, msgnum=None):
|
||||||
|
url = "%s%d/%s/%s" % (self.relay, self.channel_id, self.side, verb)
|
||||||
|
if msgnum is not None:
|
||||||
|
url += "/" + msgnum
|
||||||
|
return url
|
||||||
|
|
||||||
|
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)
|
||||||
|
print("Server (at %s) says:\n %s" % (self.relay, motd_formatted),
|
||||||
|
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:
|
||||||
|
raise ServerError(welcome["error"], self.relay)
|
||||||
|
|
||||||
|
def _post_json(self, url, post_json=None):
|
||||||
|
# POST to a URL, parsing the response as JSON. Optionally include a
|
||||||
|
# JSON request body.
|
||||||
|
p = None
|
||||||
|
if post_json:
|
||||||
|
data = json.dumps(post_json).encode("utf-8")
|
||||||
|
p = DataProducer(data)
|
||||||
|
d = self.agent.request("POST", url, bodyProducer=p)
|
||||||
|
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
|
||||||
|
|
||||||
|
def _allocate_channel(self):
|
||||||
|
url = self.relay + "allocate/%s" % self.side
|
||||||
|
d = self._post_json(url)
|
||||||
|
def _got_channel(data):
|
||||||
|
if "welcome" in data:
|
||||||
|
self.handle_welcome(data["welcome"])
|
||||||
|
return data["channel-id"]
|
||||||
|
d.addCallback(_got_channel)
|
||||||
|
return d
|
||||||
|
|
||||||
def get_code(self, code_length=2):
|
def get_code(self, code_length=2):
|
||||||
if self.code is not None: raise UsageError
|
if self.code is not None: raise UsageError
|
||||||
if self._started_get_code: raise UsageError
|
if self._started_get_code: raise UsageError
|
||||||
|
@ -67,16 +115,6 @@ class SymmetricWormhole:
|
||||||
d.addCallback(_got_channel_id)
|
d.addCallback(_got_channel_id)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _allocate_channel(self):
|
|
||||||
url = self.relay + "allocate/%s" % self.side
|
|
||||||
d = self.post(url)
|
|
||||||
def _got_channel(data):
|
|
||||||
if "welcome" in data:
|
|
||||||
self.handle_welcome(data["welcome"])
|
|
||||||
return data["channel-id"]
|
|
||||||
d.addCallback(_got_channel)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def set_code(self, code):
|
def set_code(self, code):
|
||||||
if self.code is not None: raise UsageError
|
if self.code is not None: raise UsageError
|
||||||
if self.side is not None: raise UsageError
|
if self.side is not None: raise UsageError
|
||||||
|
@ -95,8 +133,7 @@ class SymmetricWormhole:
|
||||||
def _start(self):
|
def _start(self):
|
||||||
# allocate the rest now too, so it can be serialized
|
# allocate the rest now too, so it can be serialized
|
||||||
self.sp = SPAKE2_Symmetric(self.code.encode("ascii"),
|
self.sp = SPAKE2_Symmetric(self.code.encode("ascii"),
|
||||||
idA=self.appid+":SymmetricA",
|
idSymmetric=self.appid)
|
||||||
idB=self.appid+":SymmetricB")
|
|
||||||
self.msg1 = self.sp.start()
|
self.msg1 = self.sp.start()
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
|
@ -124,60 +161,21 @@ class SymmetricWormhole:
|
||||||
self.msg1 = d["msg1"].decode("hex")
|
self.msg1 = d["msg1"].decode("hex")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
motd_displayed = False
|
def _post_message(self, url, msg):
|
||||||
version_warning_displayed = False
|
|
||||||
|
|
||||||
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)
|
|
||||||
print("Server (at %s) says:\n %s" % (self.relay, motd_formatted),
|
|
||||||
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:
|
|
||||||
raise ServerError(welcome["error"], self.relay)
|
|
||||||
|
|
||||||
def url(self, verb, msgnum=None):
|
|
||||||
url = "%s%d/%s/%s" % (self.relay, self.channel_id, self.side, verb)
|
|
||||||
if msgnum is not None:
|
|
||||||
url += "/" + msgnum
|
|
||||||
return url
|
|
||||||
|
|
||||||
def post(self, url, post_json=None):
|
|
||||||
# TODO: retry on failure, with exponential backoff. We're guarding
|
# TODO: retry on failure, with exponential backoff. We're guarding
|
||||||
# against the rendezvous server being temporarily offline.
|
# against the rendezvous server being temporarily offline.
|
||||||
p = None
|
if not isinstance(msg, type(b"")): raise UsageError(type(msg))
|
||||||
if post_json:
|
d = self._post_json(url, {"message": hexlify(msg).decode("ascii")})
|
||||||
data = json.dumps(post_json).encode("utf-8")
|
d.addCallback(lambda resp: resp["messages"]) # other_msgs
|
||||||
p = DataProducer(data)
|
|
||||||
d = self.agent.request("POST", url, bodyProducer=p)
|
|
||||||
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
|
return d
|
||||||
|
|
||||||
def _get_msgs(self, old_msgs, verb, msgnum):
|
def _get_message(self, old_msgs, verb, msgnum):
|
||||||
# fire with a list of messages that match verb/msgnum, which either
|
# fire with a bytestring of the first message that matches
|
||||||
# came from old_msgs, or from an EventSource that we attached to the
|
# verb/msgnum, which either came from old_msgs, or from an
|
||||||
# corresponding URL
|
# EventSource that we attached to the corresponding URL
|
||||||
if old_msgs:
|
if old_msgs:
|
||||||
return defer.succeed(old_msgs)
|
msg = unhexlify(old_msgs[0].encode("ascii"))
|
||||||
|
return defer.succeed(msg)
|
||||||
d = defer.Deferred()
|
d = defer.Deferred()
|
||||||
msgs = []
|
msgs = []
|
||||||
def _handle(name, data):
|
def _handle(name, data):
|
||||||
|
@ -186,28 +184,30 @@ class SymmetricWormhole:
|
||||||
if name == "message":
|
if name == "message":
|
||||||
msgs.append(json.loads(data)["message"])
|
msgs.append(json.loads(data)["message"])
|
||||||
d.callback(None)
|
d.callback(None)
|
||||||
es = ReconnectingEventSource(None, lambda: self.url(verb, msgnum),
|
es = ReconnectingEventSource(None, lambda: self._url(verb, msgnum),
|
||||||
_handle)#, agent=self.agent)
|
_handle)#, agent=self.agent)
|
||||||
es.startService() # TODO: .setServiceParent(self)
|
es.startService() # TODO: .setServiceParent(self)
|
||||||
es.activate()
|
es.activate()
|
||||||
d.addCallback(lambda _: es.deactivate())
|
d.addCallback(lambda _: es.deactivate())
|
||||||
d.addCallback(lambda _: es.stopService())
|
d.addCallback(lambda _: es.stopService())
|
||||||
d.addCallback(lambda _: msgs)
|
d.addCallback(lambda _: unhexlify(msgs[0].encode("ascii")))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
|
def derive_key(self, purpose, length=SecretBox.KEY_SIZE):
|
||||||
assert self.key is not None # call after get_verifier() or get_data()
|
if self.key is None:
|
||||||
assert type(purpose) == type(b"")
|
# 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=purpose)
|
||||||
|
|
||||||
def _encrypt_data(self, key, data):
|
def _encrypt_data(self, key, data):
|
||||||
assert len(key) == SecretBox.KEY_SIZE
|
if len(key) != SecretBox.KEY_SIZE: raise UsageError
|
||||||
box = SecretBox(key)
|
box = SecretBox(key)
|
||||||
nonce = utils.random(SecretBox.NONCE_SIZE)
|
nonce = utils.random(SecretBox.NONCE_SIZE)
|
||||||
return box.encrypt(data, nonce)
|
return box.encrypt(data, nonce)
|
||||||
|
|
||||||
def _decrypt_data(self, key, encrypted):
|
def _decrypt_data(self, key, encrypted):
|
||||||
assert len(key) == SecretBox.KEY_SIZE
|
if len(key) != SecretBox.KEY_SIZE: raise UsageError
|
||||||
box = SecretBox(key)
|
box = SecretBox(key)
|
||||||
data = box.decrypt(encrypted)
|
data = box.decrypt(encrypted)
|
||||||
return data
|
return data
|
||||||
|
@ -217,11 +217,9 @@ class SymmetricWormhole:
|
||||||
# TODO: prevent multiple invocation
|
# TODO: prevent multiple invocation
|
||||||
if self.key:
|
if self.key:
|
||||||
return defer.succeed(self.key)
|
return defer.succeed(self.key)
|
||||||
data = {"message": hexlify(self.msg1).decode("ascii")}
|
d = self._post_message(self._url("post", "pake"), self.msg1)
|
||||||
d = self.post(self.url("post", "pake"), data)
|
d.addCallback(lambda msgs: self._get_message(msgs, "poll", "pake"))
|
||||||
d.addCallback(lambda j: self._get_msgs(j["messages"], "poll", "pake"))
|
def _got_pake(pake_msg):
|
||||||
def _got_pake(msgs):
|
|
||||||
pake_msg = unhexlify(msgs[0].encode("ascii"))
|
|
||||||
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+b":Verifier")
|
self.verifier = self.derive_key(self.appid+b":Verifier")
|
||||||
|
@ -240,6 +238,7 @@ class SymmetricWormhole:
|
||||||
if self.code is None: raise UsageError
|
if self.code is None: raise UsageError
|
||||||
d = self._get_key()
|
d = self._get_key()
|
||||||
d.addCallback(self._get_data2, outbound_data)
|
d.addCallback(self._get_data2, outbound_data)
|
||||||
|
d.addBoth(self._deallocate)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _get_data2(self, key, outbound_data):
|
def _get_data2(self, key, outbound_data):
|
||||||
|
@ -247,12 +246,12 @@ class SymmetricWormhole:
|
||||||
# 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 check for reflection.
|
# nonces to keep the messages distinct, and check for reflection.
|
||||||
data_key = self.derive_key(b"data-key")
|
data_key = self.derive_key(b"data-key")
|
||||||
|
|
||||||
outbound_encrypted = self._encrypt_data(data_key, outbound_data)
|
outbound_encrypted = self._encrypt_data(data_key, outbound_data)
|
||||||
data = {"message": hexlify(outbound_encrypted).decode("ascii")}
|
d = self._post_message(self._url("post", "data"), outbound_encrypted)
|
||||||
d = self.post(self.url("post", "data"), data)
|
|
||||||
d.addCallback(lambda j: self._get_msgs(j["messages"], "poll", "data"))
|
d.addCallback(lambda msgs: self._get_message(msgs, "poll", "data"))
|
||||||
def _got_data(msgs):
|
def _got_data(inbound_encrypted):
|
||||||
inbound_encrypted = unhexlify(msgs[0].encode("ascii"))
|
|
||||||
if inbound_encrypted == outbound_encrypted:
|
if inbound_encrypted == outbound_encrypted:
|
||||||
raise ReflectionAttack
|
raise ReflectionAttack
|
||||||
try:
|
try:
|
||||||
|
@ -261,11 +260,10 @@ class SymmetricWormhole:
|
||||||
except CryptoError:
|
except CryptoError:
|
||||||
raise WrongPasswordError
|
raise WrongPasswordError
|
||||||
d.addCallback(_got_data)
|
d.addCallback(_got_data)
|
||||||
d.addBoth(self._deallocate)
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _deallocate(self, res):
|
def _deallocate(self, res):
|
||||||
# only try once, no retries
|
# only try once, no retries
|
||||||
d = self.agent.request("POST", self.url("deallocate"))
|
d = self.agent.request("POST", self._url("deallocate"))
|
||||||
d.addBoth(lambda _: res) # ignore POST failure, pass-through result
|
d.addBoth(lambda _: res) # ignore POST failure, pass-through result
|
||||||
return d
|
return d
|
||||||
|
|
Loading…
Reference in New Issue
Block a user