Merge branch 'confirm-msg'
This commit is contained in:
commit
d1cf1c6da0
|
@ -16,6 +16,11 @@ from ..channel_monitor import monitor
|
||||||
SECOND = 1
|
SECOND = 1
|
||||||
MINUTE = 60*SECOND
|
MINUTE = 60*SECOND
|
||||||
|
|
||||||
|
CONFMSG_NONCE_LENGTH = 128//8
|
||||||
|
CONFMSG_MAC_LENGTH = 256//8
|
||||||
|
def make_confmsg(confkey, nonce):
|
||||||
|
return nonce+HKDF(confkey, CONFMSG_MAC_LENGTH, nonce)
|
||||||
|
|
||||||
def to_bytes(u):
|
def to_bytes(u):
|
||||||
return unicodedata.normalize("NFC", u).encode("utf-8")
|
return unicodedata.normalize("NFC", u).encode("utf-8")
|
||||||
|
|
||||||
|
@ -194,6 +199,7 @@ def close_on_error(f): # method decorator
|
||||||
class Wormhole:
|
class Wormhole:
|
||||||
motd_displayed = False
|
motd_displayed = False
|
||||||
version_warning_displayed = False
|
version_warning_displayed = False
|
||||||
|
_send_confirm = True
|
||||||
|
|
||||||
def __init__(self, appid, relay_url, wait=0.5*SECOND, timeout=3*MINUTE):
|
def __init__(self, appid, relay_url, wait=0.5*SECOND, timeout=3*MINUTE):
|
||||||
if not isinstance(appid, type(u"")): raise TypeError(type(appid))
|
if not isinstance(appid, type(u"")): raise TypeError(type(appid))
|
||||||
|
@ -214,6 +220,7 @@ class Wormhole:
|
||||||
self.verifier = None
|
self.verifier = None
|
||||||
self._sent_data = set() # phases
|
self._sent_data = set() # phases
|
||||||
self._got_data = set()
|
self._got_data = set()
|
||||||
|
self._got_confirmation = False
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
@ -314,8 +321,12 @@ class Wormhole:
|
||||||
pake_msg = self._channel.get(u"pake")
|
pake_msg = self._channel.get(u"pake")
|
||||||
self.key = self.sp.finish(pake_msg)
|
self.key = self.sp.finish(pake_msg)
|
||||||
self.verifier = self.derive_key(u"wormhole:verifier")
|
self.verifier = self.derive_key(u"wormhole:verifier")
|
||||||
conf = self.derive_key(u"wormhole:confirmation")
|
if not self._send_confirm:
|
||||||
self._channel.send(u"_confirm", conf)
|
return
|
||||||
|
confkey = self.derive_key(u"wormhole:confirmation")
|
||||||
|
nonce = os.urandom(CONFMSG_NONCE_LENGTH)
|
||||||
|
confmsg = make_confmsg(confkey, nonce)
|
||||||
|
self._channel.send(u"_confirm", confmsg)
|
||||||
|
|
||||||
@close_on_error
|
@close_on_error
|
||||||
def get_verifier(self):
|
def get_verifier(self):
|
||||||
|
@ -355,10 +366,22 @@ class Wormhole:
|
||||||
if self._channel is None: raise UsageError
|
if self._channel is None: raise UsageError
|
||||||
self._got_data.add(phase)
|
self._got_data.add(phase)
|
||||||
self._get_key()
|
self._get_key()
|
||||||
data_key = self.derive_key(u"wormhole:phase:%s" % phase)
|
phases = []
|
||||||
inbound_encrypted = self._channel.get(phase)
|
if not self._got_confirmation:
|
||||||
|
phases.append(u"_confirm")
|
||||||
|
phases.append(phase)
|
||||||
|
(got_phase, body) = self._channel.get_first_of(phases)
|
||||||
|
if got_phase == u"_confirm":
|
||||||
|
confkey = self.derive_key(u"wormhole:confirmation")
|
||||||
|
nonce = body[:CONFMSG_NONCE_LENGTH]
|
||||||
|
if body != make_confmsg(confkey, nonce):
|
||||||
|
raise WrongPasswordError
|
||||||
|
self._got_confirmation = True
|
||||||
|
(got_phase, body) = self._channel.get_first_of([phase])
|
||||||
|
assert got_phase == phase
|
||||||
try:
|
try:
|
||||||
inbound_data = self._decrypt_data(data_key, inbound_encrypted)
|
data_key = self.derive_key(u"wormhole:phase:%s" % phase)
|
||||||
|
inbound_data = self._decrypt_data(data_key, body)
|
||||||
return inbound_data
|
return inbound_data
|
||||||
except CryptoError:
|
except CryptoError:
|
||||||
raise WrongPasswordError
|
raise WrongPasswordError
|
||||||
|
|
|
@ -3,7 +3,8 @@ import json
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet.defer import gatherResults, succeed
|
from twisted.internet.defer import gatherResults, succeed
|
||||||
from twisted.internet.threads import deferToThread
|
from twisted.internet.threads import deferToThread
|
||||||
from ..blocking.transcribe import Wormhole, UsageError, ChannelManager
|
from ..blocking.transcribe import (Wormhole, UsageError, ChannelManager,
|
||||||
|
WrongPasswordError)
|
||||||
from ..blocking.eventsource import EventSourceFollower
|
from ..blocking.eventsource import EventSourceFollower
|
||||||
from .common import ServerBase
|
from .common import ServerBase
|
||||||
|
|
||||||
|
@ -246,6 +247,53 @@ class Blocking(ServerBase, unittest.TestCase):
|
||||||
d.addCallback(_got_2)
|
d.addCallback(_got_2)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_wrong_password(self):
|
||||||
|
w1 = Wormhole(APPID, self.relayurl)
|
||||||
|
w2 = Wormhole(APPID, self.relayurl)
|
||||||
|
|
||||||
|
# make sure we can detect WrongPasswordError even if one side only
|
||||||
|
# does get_data() and not send_data(), like "wormhole receive" does
|
||||||
|
d = deferToThread(w1.get_code)
|
||||||
|
d.addCallback(lambda code: w2.set_code(code+"not"))
|
||||||
|
|
||||||
|
# w2 can't throw WrongPasswordError until it sees a CONFIRM message,
|
||||||
|
# and w1 won't send CONFIRM until it sees a PAKE message, which w2
|
||||||
|
# won't send until we call get_data. So we need both sides to be
|
||||||
|
# running at the same time for this test.
|
||||||
|
def _w1_sends():
|
||||||
|
w1.send_data(b"data1")
|
||||||
|
def _w2_gets():
|
||||||
|
self.assertRaises(WrongPasswordError, w2.get_data)
|
||||||
|
d.addCallback(lambda _: self.doBoth([_w1_sends], [_w2_gets]))
|
||||||
|
|
||||||
|
# and now w1 should have enough information to throw too
|
||||||
|
d.addCallback(lambda _: deferToThread(self.assertRaises,
|
||||||
|
WrongPasswordError, w1.get_data))
|
||||||
|
def _done(_):
|
||||||
|
# both sides are closed automatically upon error, but it's still
|
||||||
|
# legal to call .close(), and should be idempotent
|
||||||
|
return self.doBoth([w1.close], [w2.close])
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_no_confirm(self):
|
||||||
|
# newer versions (which check confirmations) should will work with
|
||||||
|
# older versions (that don't send confirmations)
|
||||||
|
w1 = Wormhole(APPID, self.relayurl)
|
||||||
|
w1._send_confirm = False
|
||||||
|
w2 = Wormhole(APPID, self.relayurl)
|
||||||
|
|
||||||
|
d = deferToThread(w1.get_code)
|
||||||
|
d.addCallback(lambda code: w2.set_code(code))
|
||||||
|
d.addCallback(lambda _: self.doBoth([w1.send_data, b"data1"],
|
||||||
|
[w2.get_data]))
|
||||||
|
d.addCallback(lambda dl: self.assertEqual(dl[1], b"data1"))
|
||||||
|
d.addCallback(lambda _: self.doBoth([w1.get_data],
|
||||||
|
[w2.send_data, b"data2"]))
|
||||||
|
d.addCallback(lambda dl: self.assertEqual(dl[0], b"data2"))
|
||||||
|
d.addCallback(lambda _: self.doBoth([w1.close], [w2.close]))
|
||||||
|
return d
|
||||||
|
|
||||||
def test_verifier(self):
|
def test_verifier(self):
|
||||||
w1 = Wormhole(APPID, self.relayurl)
|
w1 = Wormhole(APPID, self.relayurl)
|
||||||
w2 = Wormhole(APPID, self.relayurl)
|
w2 = Wormhole(APPID, self.relayurl)
|
||||||
|
|
64
src/wormhole/test/test_interop.py
Normal file
64
src/wormhole/test/test_interop.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
from __future__ import print_function
|
||||||
|
import sys
|
||||||
|
from twisted.trial import unittest
|
||||||
|
from twisted.internet.defer import gatherResults
|
||||||
|
from twisted.internet.threads import deferToThread
|
||||||
|
from ..twisted.transcribe import Wormhole as twisted_Wormhole
|
||||||
|
from ..blocking.transcribe import Wormhole as blocking_Wormhole
|
||||||
|
from .common import ServerBase
|
||||||
|
|
||||||
|
# make sure the two implementations (Twisted-style and blocking-style) can
|
||||||
|
# interoperate
|
||||||
|
|
||||||
|
APPID = u"appid"
|
||||||
|
|
||||||
|
class Basic(ServerBase, unittest.TestCase):
|
||||||
|
|
||||||
|
def doBoth(self, call1, d2):
|
||||||
|
f1 = call1[0]
|
||||||
|
f1args = call1[1:]
|
||||||
|
return gatherResults([deferToThread(f1, *f1args), d2], True)
|
||||||
|
|
||||||
|
def test_twisted_to_blocking(self):
|
||||||
|
tw = twisted_Wormhole(APPID, self.relayurl)
|
||||||
|
bw = blocking_Wormhole(APPID, self.relayurl)
|
||||||
|
d = tw.get_code()
|
||||||
|
def _got_code(code):
|
||||||
|
bw.set_code(code)
|
||||||
|
return self.doBoth([bw.send_data, b"data2"], tw.send_data(b"data1"))
|
||||||
|
d.addCallback(_got_code)
|
||||||
|
def _sent(res):
|
||||||
|
return self.doBoth([bw.get_data], tw.get_data())
|
||||||
|
d.addCallback(_sent)
|
||||||
|
def _done(dl):
|
||||||
|
(dataX, dataY) = dl
|
||||||
|
self.assertEqual(dataX, b"data1")
|
||||||
|
self.assertEqual(dataY, b"data2")
|
||||||
|
return self.doBoth([bw.close], tw.close())
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_blocking_to_twisted(self):
|
||||||
|
bw = blocking_Wormhole(APPID, self.relayurl)
|
||||||
|
tw = twisted_Wormhole(APPID, self.relayurl)
|
||||||
|
d = deferToThread(bw.get_code)
|
||||||
|
def _got_code(code):
|
||||||
|
tw.set_code(code)
|
||||||
|
return self.doBoth([bw.send_data, b"data1"], tw.send_data(b"data2"))
|
||||||
|
d.addCallback(_got_code)
|
||||||
|
def _sent(res):
|
||||||
|
return self.doBoth([bw.get_data], tw.get_data())
|
||||||
|
d.addCallback(_sent)
|
||||||
|
def _done(dl):
|
||||||
|
(dataX, dataY) = dl
|
||||||
|
self.assertEqual(dataX, b"data2")
|
||||||
|
self.assertEqual(dataY, b"data1")
|
||||||
|
return self.doBoth([bw.close], tw.close())
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
Basic.skip = "twisted is not yet sufficiently ported to py3"
|
||||||
|
# as of 15.4.0, Twisted is still missing:
|
||||||
|
# * web.client.Agent (for all non-EventSource POSTs in transcribe.py)
|
||||||
|
# * python.logfile (to allow daemonization of 'wormhole server')
|
|
@ -2,7 +2,8 @@ from __future__ import print_function
|
||||||
import sys, json
|
import sys, json
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet.defer import gatherResults, succeed
|
from twisted.internet.defer import gatherResults, succeed
|
||||||
from ..twisted.transcribe import Wormhole, UsageError, ChannelManager
|
from ..twisted.transcribe import (Wormhole, UsageError, ChannelManager,
|
||||||
|
WrongPasswordError)
|
||||||
from ..twisted.eventsource_twisted import EventSourceParser
|
from ..twisted.eventsource_twisted import EventSourceParser
|
||||||
from .common import ServerBase
|
from .common import ServerBase
|
||||||
|
|
||||||
|
@ -229,6 +230,50 @@ class Basic(ServerBase, unittest.TestCase):
|
||||||
d.addCallback(_got_2)
|
d.addCallback(_got_2)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def test_wrong_password(self):
|
||||||
|
w1 = Wormhole(APPID, self.relayurl)
|
||||||
|
w2 = Wormhole(APPID, self.relayurl)
|
||||||
|
d = w1.get_code()
|
||||||
|
d.addCallback(lambda code: w2.set_code(code+"not"))
|
||||||
|
|
||||||
|
# w2 can't throw WrongPasswordError until it sees a CONFIRM message,
|
||||||
|
# and w1 won't send CONFIRM until it sees a PAKE message, which w2
|
||||||
|
# won't send until we call get_data. So we need both sides to be
|
||||||
|
# running at the same time for this test.
|
||||||
|
def _w1_sends():
|
||||||
|
return w1.send_data(b"data1")
|
||||||
|
def _w2_gets():
|
||||||
|
return self.assertFailure(w2.get_data(), WrongPasswordError)
|
||||||
|
d.addCallback(lambda _: self.doBoth(_w1_sends(), _w2_gets()))
|
||||||
|
|
||||||
|
# and now w1 should have enough information to throw too
|
||||||
|
d.addCallback(lambda _: self.assertFailure(w1.get_data(),
|
||||||
|
WrongPasswordError))
|
||||||
|
def _done(_):
|
||||||
|
# both sides are closed automatically upon error, but it's still
|
||||||
|
# legal to call .close(), and should be idempotent
|
||||||
|
return self.doBoth(w1.close(), w2.close())
|
||||||
|
d.addCallback(_done)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def test_no_confirm(self):
|
||||||
|
# newer versions (which check confirmations) should will work with
|
||||||
|
# older versions (that don't send confirmations)
|
||||||
|
w1 = Wormhole(APPID, self.relayurl)
|
||||||
|
w1._send_confirm = False
|
||||||
|
w2 = Wormhole(APPID, self.relayurl)
|
||||||
|
|
||||||
|
d = w1.get_code()
|
||||||
|
d.addCallback(lambda code: w2.set_code(code))
|
||||||
|
d.addCallback(lambda _: self.doBoth(w1.send_data(b"data1"),
|
||||||
|
w2.get_data()))
|
||||||
|
d.addCallback(lambda dl: self.assertEqual(dl[1], b"data1"))
|
||||||
|
d.addCallback(lambda _: self.doBoth(w1.get_data(),
|
||||||
|
w2.send_data(b"data2")))
|
||||||
|
d.addCallback(lambda dl: self.assertEqual(dl[0], b"data2"))
|
||||||
|
d.addCallback(lambda _: self.doBoth(w1.close(), w2.close()))
|
||||||
|
return d
|
||||||
|
|
||||||
def test_verifier(self):
|
def test_verifier(self):
|
||||||
w1 = Wormhole(APPID, self.relayurl)
|
w1 = Wormhole(APPID, self.relayurl)
|
||||||
w2 = Wormhole(APPID, self.relayurl)
|
w2 = Wormhole(APPID, self.relayurl)
|
||||||
|
|
|
@ -18,6 +18,11 @@ from ..errors import ServerError, WrongPasswordError, UsageError
|
||||||
from ..util.hkdf import HKDF
|
from ..util.hkdf import HKDF
|
||||||
from ..channel_monitor import monitor
|
from ..channel_monitor import monitor
|
||||||
|
|
||||||
|
CONFMSG_NONCE_LENGTH = 128//8
|
||||||
|
CONFMSG_MAC_LENGTH = 256//8
|
||||||
|
def make_confmsg(confkey, nonce):
|
||||||
|
return nonce+HKDF(confkey, CONFMSG_MAC_LENGTH, nonce)
|
||||||
|
|
||||||
def to_bytes(u):
|
def to_bytes(u):
|
||||||
return unicodedata.normalize("NFC", u).encode("utf-8")
|
return unicodedata.normalize("NFC", u).encode("utf-8")
|
||||||
|
|
||||||
|
@ -192,6 +197,7 @@ class ChannelManager:
|
||||||
class Wormhole:
|
class Wormhole:
|
||||||
motd_displayed = False
|
motd_displayed = False
|
||||||
version_warning_displayed = False
|
version_warning_displayed = False
|
||||||
|
_send_confirm = True
|
||||||
|
|
||||||
def __init__(self, appid, relay_url):
|
def __init__(self, appid, relay_url):
|
||||||
if not isinstance(appid, type(u"")): raise TypeError(type(appid))
|
if not isinstance(appid, type(u"")): raise TypeError(type(appid))
|
||||||
|
@ -206,6 +212,7 @@ class Wormhole:
|
||||||
self._started_get_code = False
|
self._started_get_code = False
|
||||||
self._sent_data = set() # phases
|
self._sent_data = set() # phases
|
||||||
self._got_data = set()
|
self._got_data = set()
|
||||||
|
self._got_confirmation = False
|
||||||
|
|
||||||
def _set_side(self, side):
|
def _set_side(self, side):
|
||||||
self._side = side
|
self._side = side
|
||||||
|
@ -332,8 +339,12 @@ class Wormhole:
|
||||||
key = self.sp.finish(pake_msg)
|
key = self.sp.finish(pake_msg)
|
||||||
self.key = key
|
self.key = key
|
||||||
self.verifier = self.derive_key(u"wormhole:verifier")
|
self.verifier = self.derive_key(u"wormhole:verifier")
|
||||||
conf = self.derive_key(u"wormhole:confirmation")
|
if not self._send_confirm:
|
||||||
d1 = self._channel.send(u"_confirm", conf)
|
return key
|
||||||
|
confkey = self.derive_key(u"wormhole:confirmation")
|
||||||
|
nonce = os.urandom(CONFMSG_NONCE_LENGTH)
|
||||||
|
confmsg = make_confmsg(confkey, nonce)
|
||||||
|
d1 = self._channel.send(u"_confirm", confmsg)
|
||||||
d1.addCallback(lambda _: key)
|
d1.addCallback(lambda _: key)
|
||||||
return d1
|
return d1
|
||||||
d.addCallback(_got_pake)
|
d.addCallback(_got_pake)
|
||||||
|
@ -375,16 +386,32 @@ class Wormhole:
|
||||||
self._got_data.add(phase)
|
self._got_data.add(phase)
|
||||||
d = self._get_key()
|
d = self._get_key()
|
||||||
def _get(key):
|
def _get(key):
|
||||||
data_key = self.derive_key(u"wormhole:phase:%s" % phase)
|
phases = []
|
||||||
d1 = self._channel.get(phase)
|
if not self._got_confirmation:
|
||||||
def _decrypt(inbound_encrypted):
|
phases.append(u"_confirm")
|
||||||
|
phases.append(phase)
|
||||||
|
d1 = self._channel.get_first_of(phases)
|
||||||
|
def _maybe_got_confirm(phase_and_body):
|
||||||
|
(got_phase, body) = phase_and_body
|
||||||
|
if got_phase == u"_confirm":
|
||||||
|
confkey = self.derive_key(u"wormhole:confirmation")
|
||||||
|
nonce = body[:CONFMSG_NONCE_LENGTH]
|
||||||
|
if body != make_confmsg(confkey, nonce):
|
||||||
|
raise WrongPasswordError
|
||||||
|
self._got_confirmation = True
|
||||||
|
return self._channel.get_first_of([phase])
|
||||||
|
return phase_and_body
|
||||||
|
d1.addCallback(_maybe_got_confirm)
|
||||||
|
def _got(phase_and_body):
|
||||||
|
(got_phase, body) = phase_and_body
|
||||||
|
assert got_phase == phase
|
||||||
try:
|
try:
|
||||||
inbound_data = self._decrypt_data(data_key,
|
data_key = self.derive_key(u"wormhole:phase:%s" % phase)
|
||||||
inbound_encrypted)
|
inbound_data = self._decrypt_data(data_key, body)
|
||||||
return inbound_data
|
return inbound_data
|
||||||
except CryptoError:
|
except CryptoError:
|
||||||
raise WrongPasswordError
|
raise WrongPasswordError
|
||||||
d1.addCallback(_decrypt)
|
d1.addCallback(_got)
|
||||||
return d1
|
return d1
|
||||||
d.addCallback(_get)
|
d.addCallback(_get)
|
||||||
return d
|
return d
|
||||||
|
|
Loading…
Reference in New Issue
Block a user