diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 47b097a..9cf4fe2 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -236,10 +236,10 @@ class Basic(unittest.TestCase): self.assertNoResult(v) # hearing a valid confirmation message doesn't throw an error - confkey = w.derive_key(u"wormhole:confirmation", SecretBox.KEY_SIZE) - nonce = os.urandom(wormhole.CONFMSG_NONCE_LENGTH) - confirm2 = wormhole.make_confmsg(confkey, nonce) - confirm2_hex = hexlify(confirm2).decode("ascii") + plaintext = json.dumps({}).encode("utf-8") + data_key = w._derive_phase_key(side2, u"confirm") + confmsg = w._encrypt_data(data_key, plaintext) + confirm2_hex = hexlify(confmsg).decode("ascii") response(w, type=u"message", phase=u"confirm", body=confirm2_hex, side=side2) @@ -524,15 +524,16 @@ class Basic(unittest.TestCase): w._drop_connection = mock.Mock() w._ws_send_command = mock.Mock() w._mailbox_state = wormhole.OPEN + side2 = u"side2" d = None if success: w._key = b"key" else: w._key = b"wrongkey" - confkey = w._derive_confirmation_key() - nonce = os.urandom(wormhole.CONFMSG_NONCE_LENGTH) - confmsg = wormhole.make_confmsg(confkey, nonce) + plaintext = json.dumps({}).encode("utf-8") + data_key = w._derive_phase_key(side2, u"confirm") + confmsg = w._encrypt_data(data_key, plaintext) w._key = None if when == "early": @@ -543,7 +544,7 @@ class Basic(unittest.TestCase): w._key = b"key" w._event_established_key() else: - w._event_received_confirm(confmsg) + w._event_received_confirm(side2, confmsg) if when == "middle": d = w.verify() @@ -554,7 +555,7 @@ class Basic(unittest.TestCase): w._key = b"key" w._event_established_key() else: - w._event_received_confirm(confmsg) + w._event_received_confirm(side2, confmsg) if when == "late": d = w.verify() @@ -824,6 +825,22 @@ class Wormholes(ServerBase, unittest.TestCase): yield w1.close() yield w2.close() + @inlineCallbacks + def test_versions(self): + # there's no API for this yet, but make sure the internals work + w1 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1._my_versions = {u"w1": 123} + w2 = wormhole.wormhole(APPID, self.relayurl, reactor) + w2._my_versions = {u"w2": 456} + code = yield w1.get_code() + w2.set_code(code) + yield w1.verify() + self.assertEqual(w1._their_versions, {u"w2": 456}) + yield w2.verify() + self.assertEqual(w2._their_versions, {u"w1": 123}) + yield w1.close() + yield w2.close() + class Errors(ServerBase, unittest.TestCase): @inlineCallbacks def test_codes_1(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 8513c3c..73ae54b 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -245,6 +245,9 @@ class _Wormhole: self._verify_result = None # bytes or a Failure self._verifier_waiter = None + self._my_versions = {} # sent + self._their_versions = {} # received + self._close_called = False # the close() API has been called self._closing = False # we've started shutdown self._disconnect_waiter = defer.Deferred() @@ -545,10 +548,7 @@ class _Wormhole: self._timing.add("key established") # both sides send different (random) confirmation messages - confkey = self._derive_confirmation_key() - nonce = os.urandom(CONFMSG_NONCE_LENGTH) - confmsg = make_confmsg(confkey, nonce) - self._msg_send(u"confirm", confmsg) + self._send_confirmation_message() verifier = self._derive_key(b"wormhole:verifier") self._event_computed_verifier(verifier) @@ -556,6 +556,16 @@ class _Wormhole: self._maybe_check_confirmation() self._maybe_send_phase_messages() + def _send_confirmation_message(self): + # this is encrypted like a normal phase message, and includes a + # dictionary of version flags to let the other Wormhole know what + # we're capable of (for future expansion) + plaintext = json.dumps(self._my_versions).encode("utf-8") + phase = u"confirm" + data_key = self._derive_phase_key(self._side, phase) + encrypted = self._encrypt_data(data_key, plaintext) + self._msg_send(phase, encrypted) + def _API_verify(self): if self._error: return defer.fail(self._error) if self._get_verifier_called: raise UsageError @@ -579,13 +589,13 @@ class _Wormhole: if self._verifier_waiter and not self._verifier_waiter.called: self._verifier_waiter.callback(self._verify_result) - def _event_received_confirm(self, body): + def _event_received_confirm(self, side, body): # We ought to have the master key by now, because sensible peers # should always send "pake" before sending "confirm". It might be # nice to relax this requirement, which means storing the received # confirmation message, and having _event_established_key call # _check_confirmation() - self._confirmation_message = body + self._confirmation_message = (side, body) self._maybe_check_confirmation() def _maybe_check_confirmation(self): @@ -593,16 +603,24 @@ class _Wormhole: return if self._confirmation_checked: return - confkey = self._derive_confirmation_key() - body = self._confirmation_message - nonce = body[:CONFMSG_NONCE_LENGTH] - if body != make_confmsg(confkey, nonce): + self._confirmation_checked = True + + side, body = self._confirmation_message + data_key = self._derive_phase_key(side, u"confirm") + try: + plaintext = self._decrypt_data(data_key, body) + except CryptoError: # this makes all API calls fail if self.DEBUG: print("CONFIRM FAILED") self._signal_error(WrongPasswordError(), u"scary") - self._confirmation_checked = True + return + msg = json.loads(plaintext.decode("utf-8")) + self._version_received(msg) + self._maybe_notify_verify() + def _version_received(self, msg): + self._their_versions = msg def _API_send(self, outbound_data): if self._error: raise self._error @@ -703,7 +721,7 @@ class _Wormhole: if phase == u"pake": return self._event_received_pake(body) if phase == u"confirm": - return self._event_received_confirm(body) + return self._event_received_confirm(side, body) if re.search(r'^\d+$', phase): return self._event_received_phase_message(side, phase, body) # ignore unrecognized phases, for forwards-compatibility