diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js index 7d6e82d615c..f172f80d781 100644 --- a/services/sync/modules/constants.js +++ b/services/sync/modules/constants.js @@ -185,6 +185,7 @@ JPAKE_ERROR_NODATA: "jpake.error.nodata", JPAKE_ERROR_KEYMISMATCH: "jpake.error.keymismatch", JPAKE_ERROR_WRONGMESSAGE: "jpake.error.wrongmessage", JPAKE_ERROR_USERABORT: "jpake.error.userabort", +JPAKE_ERROR_DELAYUNSUPPORTED: "jpake.error.delayunsupported", // info types for Service.getStorageInfo INFO_COLLECTIONS: "collections", diff --git a/services/sync/modules/jpakeclient.js b/services/sync/modules/jpakeclient.js index 85e54c6ddb2..13524fcc81b 100644 --- a/services/sync/modules/jpakeclient.js +++ b/services/sync/modules/jpakeclient.js @@ -48,6 +48,8 @@ Cu.import("resource://services-sync/util.js"); const EXPORTED_SYMBOLS = ["JPAKEClient"]; const REQUEST_TIMEOUT = 60; // 1 minute +const KEYEXCHANGE_VERSION = 3; + const JPAKE_SIGNERID_SENDER = "sender"; const JPAKE_SIGNERID_RECEIVER = "receiver"; const JPAKE_LENGTH_SECRET = 8; @@ -55,50 +57,63 @@ const JPAKE_LENGTH_CLIENTID = 256; const JPAKE_VERIFY_VALUE = "0123456789ABCDEF"; -/* +/** * Client to exchange encrypted data using the J-PAKE algorithm. * The exchange between two clients of this type looks like this: * * - * Client A Server Client B - * ================================================================== - * | - * retrieve channel <---------------| - * generate random secret | - * show PIN = secret + channel | ask user for PIN - * upload A's message 1 ----------->| - * |--------> retrieve A's message 1 - * |<---------- upload B's message 1 - * retrieve B's message 1 <---------| - * upload A's message 2 ----------->| - * |--------> retrieve A's message 2 - * | compute key - * |<---------- upload B's message 2 - * retrieve B's message 2 <---------| - * compute key | - * upload sha256d(key) ------------>| - * |---------> retrieve sha256d(key) - * | verify against own key - * | encrypt data - * |<------------------- upload data - * retrieve data <------------------| - * verify HMAC | - * decrypt data | + * Mobile Server Desktop + * =================================================================== + * | + * retrieve channel <---------------| + * generate random secret | + * show PIN = secret + channel | ask user for PIN + * upload Mobile's message 1 ------>| + * |----> retrieve Mobile's message 1 + * |<----- upload Desktop's message 1 + * retrieve Desktop's message 1 <---| + * upload Mobile's message 2 ------>| + * |----> retrieve Mobile's message 2 + * | compute key + * |<----- upload Desktop's message 2 + * retrieve Desktop's message 2 <---| + * compute key | + * encrypt known value ------------>| + * |-------> retrieve encrypted value + * | verify against local known value + * + * At this point Desktop knows whether the PIN was entered correctly. + * If it wasn't, Desktop deletes the session. If it was, the account + * setup can proceed. If Desktop doesn't yet have an account set up, + * it will keep the channel open and let the user connect to or + * create an account. + * + * | encrypt credentials + * |<------------- upload credentials + * retrieve credentials <-----------| + * verify HMAC | + * decrypt credentials | + * delete session ----------------->| + * start syncing | * * * Create a client object like so: * - * let client = new JPAKEClient(observer); + * let client = new JPAKEClient(controller); * - * The 'observer' object must implement the following methods: + * The 'controller' object must implement the following methods: * * displayPIN(pin) -- Display the PIN to the user, only called on the client * that didn't provide the PIN. * + * onPaired() -- Called when the device pairing has been established and + * we're ready to send the credentials over. To do that, the controller + * must call 'sendAndComplete()' while the channel is active. + * * onComplete(data) -- Called after transfer has been completed. On * the sending side this is called with no parameter and as soon as the - * data has been uploaded, which this doesn't mean the receiving side - * has actually retrieved them yet. + * data has been uploaded. This does not mean the receiving side has + * actually retrieved them yet. * * onAbort(error) -- Called whenever an error is encountered. All errors lead * to an abort and the process has to be started again on both sides. @@ -113,7 +128,12 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF"; * * To initiate the transfer from the sending side, call * - * client.sendWithPIN(pin, data) + * client.pairWithPIN(pin, true); + * + * Once the pairing has been established, the controller's 'onPaired()' method + * will be called. To then transmit the data, call + * + * client.sendAndComplete(data); * * To abort the process, call * @@ -122,8 +142,8 @@ const JPAKE_VERIFY_VALUE = "0123456789ABCDEF"; * Note that after completion or abort, the 'client' instance may not be reused. * You will have to create a new one in case you'd like to restart the process. */ -function JPAKEClient(observer) { - this.observer = observer; +function JPAKEClient(controller) { + this.controller = controller; this._log = Log4Moz.repository.getLogger("Sync.JPAKEClient"); this._log.level = Log4Moz.Level[Svc.Prefs.get( @@ -149,6 +169,12 @@ JPAKEClient.prototype = { * Public API */ + /** + * Initiate pairing and receive data without providing a PIN. The PIN will + * be generated and passed on to the controller to be displayed to the user. + * + * This is typically called on mobile devices where typing is tedious. + */ receiveNoPIN: function receiveNoPIN() { this._my_signerid = JPAKE_SIGNERID_RECEIVER; this._their_signerid = JPAKE_SIGNERID_SENDER; @@ -173,33 +199,87 @@ JPAKEClient.prototype = { this._computeFinal, this._computeKeyVerification, this._putStep, + function(callback) { + // Allow longer time-out for the last message. + this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries"); + callback(); + }, this._getStep, this._decryptData, this._complete)(); }, - sendWithPIN: function sendWithPIN(pin, obj) { + /** + * Initiate pairing based on the PIN entered by the user. + * + * This is typically called on desktop devices where typing is easier than + * on mobile. + * + * @param pin + * 12 character string (in human-friendly base32) containing the PIN + * entered by the user. + * @param expectDelay + * Flag that indicates that a significant delay between the pairing + * and the sending should be expected. v2 and earlier of the protocol + * did not allow for this and the pairing to a v2 or earlier client + * will be aborted if this flag is 'true'. + */ + pairWithPIN: function pairWithPIN(pin, expectDelay) { this._my_signerid = JPAKE_SIGNERID_SENDER; this._their_signerid = JPAKE_SIGNERID_RECEIVER; this._channel = pin.slice(JPAKE_LENGTH_SECRET); this._channelURL = this._serverURL + this._channel; this._secret = pin.slice(0, JPAKE_LENGTH_SECRET); - this._data = JSON.stringify(obj); this._chain(this._computeStepOne, this._getStep, + function (callback) { + // Ensure that the other client can deal with a delay for + // the last message if that's requested by the caller. + if (!expectDelay) { + return callback(); + } + if (!this._incoming.version || this._incoming.version < 3) { + return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED); + } + return callback(); + }, this._putStep, this._computeStepTwo, this._getStep, this._putStep, this._computeFinal, this._getStep, - this._encryptData, + this._verifyPairing)(); + }, + + /** + * Send data after a successful pairing. + * + * @param obj + * Object containing the data to send. It will be serialized as JSON. + */ + sendAndComplete: function sendAndComplete(obj) { + if (!this._paired || this._finished) { + this._log.error("Can't send data, no active pairing!"); + throw "No active pairing!"; + } + this._data = JSON.stringify(obj); + this._chain(this._encryptData, this._putStep, this._complete)(); }, + /** + * Abort the current pairing. The channel on the server will be deleted + * if the abort wasn't due to a network or server error. The controller's + * 'onAbort()' method is notified in all cases. + * + * @param error [optional] + * Error constant indicating the reason for the abort. Defaults to + * user abort. + */ abort: function abort(error) { this._log.debug("Aborting..."); this._finished = true; @@ -213,10 +293,9 @@ JPAKEClient.prototype = { if (error == JPAKE_ERROR_CHANNEL || error == JPAKE_ERROR_NETWORK || error == JPAKE_ERROR_NODATA) { - Utils.namedTimer(function() { this.observer.onAbort(error); }, 0, - this, "_timer_onAbort"); + Utils.nextTick(function() { this.controller.onAbort(error); }, this); } else { - this._reportFailure(error, function() { self.observer.onAbort(error); }); + this._reportFailure(error, function() { self.controller.onAbort(error); }); } }, @@ -285,8 +364,7 @@ JPAKEClient.prototype = { // Don't block on UI code. let pin = this._secret + this._channel; - Utils.namedTimer(function() { this.observer.displayPIN(pin); }, 0, - this, "_timer_displayPIN"); + Utils.nextTick(function() { this.controller.displayPIN(pin); }, this); callback(); })); }, @@ -295,6 +373,11 @@ JPAKEClient.prototype = { _putStep: function _putStep(callback) { this._log.trace("Uploading message " + this._outgoing.type); let request = this._newRequest(this._channelURL); + if (this._their_etag) { + request.setHeader("If-Match", this._their_etag); + } else { + request.setHeader("If-None-Match", "*"); + } request.put(this._outgoing, Utils.bind2(this, function (error) { if (this._finished) { return; @@ -313,7 +396,7 @@ JPAKEClient.prototype = { } // There's no point in returning early here since the next step will // always be a GET so let's pause for twice the poll interval. - this._etag = request.response.headers["etag"]; + this._my_etag = request.response.headers["etag"]; Utils.namedTimer(function () { callback(); }, this._pollInterval * 2, this, "_pollTimer"); })); @@ -324,8 +407,8 @@ JPAKEClient.prototype = { _getStep: function _getStep(callback) { this._log.trace("Retrieving next message."); let request = this._newRequest(this._channelURL); - if (this._etag) { - request.setHeader("If-None-Match", this._etag); + if (this._my_etag) { + request.setHeader("If-None-Match", this._my_etag); } request.get(Utils.bind2(this, function (error) { @@ -365,6 +448,14 @@ JPAKEClient.prototype = { return; } + this._their_etag = request.response.headers["etag"]; + if (!this._their_etag) { + this._log.error("Server did not supply ETag for message: " + + request.response.body); + this.abort(JPAKE_ERROR_SERVER); + return; + } + try { this._incoming = JSON.parse(request.response.body); } catch (ex) { @@ -414,7 +505,9 @@ JPAKEClient.prototype = { gx2: gx2.value, zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid}, zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}}; - this._outgoing = {type: this._my_signerid + "1", payload: one}; + this._outgoing = {type: this._my_signerid + "1", + version: KEYEXCHANGE_VERSION, + payload: one}; this._log.trace("Generated message " + this._outgoing.type); callback(); }, @@ -452,7 +545,9 @@ JPAKEClient.prototype = { } let two = {A: A.value, zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}}; - this._outgoing = {type: this._my_signerid + "2", payload: two}; + this._outgoing = {type: this._my_signerid + "2", + version: KEYEXCHANGE_VERSION, + payload: two}; this._log.trace("Generated message " + this._outgoing.type); callback(); }, @@ -504,12 +599,13 @@ JPAKEClient.prototype = { return; } this._outgoing = {type: this._my_signerid + "3", + version: KEYEXCHANGE_VERSION, payload: {ciphertext: ciphertext, IV: iv}}; this._log.trace("Generated message " + this._outgoing.type); callback(); }, - _encryptData: function _encryptData(callback) { + _verifyPairing: function _verifyPairing(callback) { this._log.trace("Verifying their key."); if (this._incoming.type != this._their_signerid + "3") { this._log.error("Invalid round 3 data: " + @@ -518,6 +614,7 @@ JPAKEClient.prototype = { return; } let step3 = this._incoming.payload; + let ciphertext; try { ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE, this._crypto_key, step3.IV); @@ -530,6 +627,13 @@ JPAKEClient.prototype = { return; } + this._log.debug("Verified pairing!"); + this._paired = true; + Utils.nextTick(function () { this.controller.onPaired(); }, this); + callback(); + }, + + _encryptData: function _encryptData(callback) { this._log.trace("Encrypting data."); let iv, ciphertext, hmac; try { @@ -542,6 +646,7 @@ JPAKEClient.prototype = { return; } this._outgoing = {type: this._my_signerid + "3", + version: KEYEXCHANGE_VERSION, payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}}; this._log.trace("Generated message " + this._outgoing.type); callback(); @@ -594,8 +699,8 @@ JPAKEClient.prototype = { _complete: function _complete() { this._log.debug("Exchange completed."); this._finished = true; - Utils.namedTimer(function () { this.observer.onComplete(this._newData); }, - 0, this, "_timer_onComplete"); + Utils.nextTick(function () { this.controller.onComplete(this._newData); }, + this); } }; diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js index 361d3658868..4348743a935 100644 --- a/services/sync/services-sync.js +++ b/services/sync/services-sync.js @@ -26,7 +26,8 @@ pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyc pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/"); pref("services.sync.jpake.pollInterval", 1000); -pref("services.sync.jpake.firstMsgMaxTries", 300); +pref("services.sync.jpake.firstMsgMaxTries", 300); // 5 minutes +pref("services.sync.jpake.lastMsgMaxTries", 300); // 5 minutes pref("services.sync.jpake.maxTries", 10); pref("services.sync.log.appender.console", "Warn"); diff --git a/services/sync/tests/unit/test_jpakeclient.js b/services/sync/tests/unit/test_jpakeclient.js index 161c182acb8..1ec359c5d70 100644 --- a/services/sync/tests/unit/test_jpakeclient.js +++ b/services/sync/tests/unit/test_jpakeclient.js @@ -6,26 +6,32 @@ Cu.import("resource://services-sync/util.js"); const JPAKE_LENGTH_SECRET = 8; const JPAKE_LENGTH_CLIENTID = 256; +const KEYEXCHANGE_VERSION = 3; /* * Simple server. */ +const SERVER_MAX_GETS = 6; + function check_headers(request) { + let stack = Components.stack.caller; + // There shouldn't be any Basic auth - do_check_false(request.hasHeader("Authorization")); + do_check_false(request.hasHeader("Authorization"), stack); // Ensure key exchange ID is set and the right length - do_check_true(request.hasHeader("X-KeyExchange-Id")); + do_check_true(request.hasHeader("X-KeyExchange-Id"), stack); do_check_eq(request.getHeader("X-KeyExchange-Id").length, - JPAKE_LENGTH_CLIENTID); + JPAKE_LENGTH_CLIENTID, stack); } function new_channel() { // Create a new channel and register it with the server. let cid = Math.floor(Math.random() * 10000); - while (channels[cid]) + while (channels[cid]) { cid = Math.floor(Math.random() * 10000); + } let channel = channels[cid] = new ServerChannel(); server.registerPathHandler("/" + cid, channel.handler()); return cid; @@ -45,21 +51,24 @@ let error_report; function server_report(request, response) { check_headers(request); - if (request.hasHeader("X-KeyExchange-Log")) + if (request.hasHeader("X-KeyExchange-Log")) { error_report = request.getHeader("X-KeyExchange-Log"); + } if (request.hasHeader("X-KeyExchange-Cid")) { let cid = request.getHeader("X-KeyExchange-Cid"); let channel = channels[cid]; - if (channel) + if (channel) { channel.clear(); + } } response.setStatusLine(request.httpVersion, 200, "OK"); } function ServerChannel() { - this.data = "{}"; + this.data = ""; + this.etag = ""; this.getCount = 0; } ServerChannel.prototype = { @@ -69,26 +78,42 @@ ServerChannel.prototype = { response.setStatusLine(request.httpVersion, 404, "Not Found"); return; } + if (request.hasHeader("If-None-Match")) { let etag = request.getHeader("If-None-Match"); - if (etag == this._etag) { + if (etag == this.etag) { response.setStatusLine(request.httpVersion, 304, "Not Modified"); return; } } + response.setHeader("ETag", this.etag); response.setStatusLine(request.httpVersion, 200, "OK"); response.bodyOutputStream.write(this.data, this.data.length); // Automatically clear the channel after 6 successful GETs. this.getCount += 1; - if (this.getCount == 6) + if (this.getCount == SERVER_MAX_GETS) { this.clear(); + } }, PUT: function PUT(request, response) { + if (this.data) { + do_check_true(request.hasHeader("If-Match")); + let etag = request.getHeader("If-Match"); + if (etag != this.etag) { + response.setHeader("ETag", this.etag); + response.setStatusLine(request.httpVersion, 412, "Precondition Failed"); + return; + } + } else { + do_check_true(request.hasHeader("If-None-Match")); + do_check_eq(request.getHeader("If-None-Match"), "*"); + } + this.data = readBytesFromInputStream(request.bodyInputStream); - this._etag = '"' + Utils.sha1(this.data) + '"'; - response.setHeader("ETag", this._etag); + this.etag = '"' + Utils.sha1(this.data) + '"'; + response.setHeader("ETag", this.etag); response.setStatusLine(request.httpVersion, 200, "OK"); }, @@ -108,14 +133,34 @@ ServerChannel.prototype = { }; +/** + * Controller that throws for everything. + */ +let BaseController = { + displayPIN: function displayPIN() { + do_throw("displayPIN() shouldn't have been called!"); + }, + onAbort: function onAbort(error) { + do_throw("Shouldn't have aborted with " + error + "!"); + }, + onPaired: function onPaired() { + do_throw("onPaired() shouldn't have been called!"); + }, + onComplete: function onComplete(data) { + do_throw("Shouldn't have completed with " + data + "!"); + } +}; + + const DATA = {"msg": "eggstreamly sekrit"}; const POLLINTERVAL = 50; function run_test() { Svc.Prefs.set("jpake.serverURL", "http://localhost:8080/"); Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL); - Svc.Prefs.set("jpake.maxTries", 5); + Svc.Prefs.set("jpake.maxTries", 2); Svc.Prefs.set("jpake.firstMsgMaxTries", 5); + Svc.Prefs.set("jpake.lastMsgMaxTries", 5); // Ensure clean up Svc.Obs.add("profile-before-change", function() { Svc.Prefs.resetBranch(""); @@ -134,6 +179,8 @@ function run_test() { "/report": server_report}); initTestLogging("Trace"); + Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace; + Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace; run_next_test(); } @@ -142,23 +189,20 @@ add_test(function test_success_receiveNoPIN() { _("Test a successful exchange started by receiveNoPIN()."); let snd = new JPAKEClient({ - displayPIN: function displayPIN() { - do_throw("displayPIN shouldn't have been called!"); - }, - onAbort: function onAbort(error) { - do_throw("Shouldn't have aborted!" + error); + __proto__: BaseController, + onPaired: function onPaired() { + _("Pairing successful, sending final payload."); + Utils.nextTick(function() { snd.sendAndComplete(DATA); }); }, onComplete: function onComplete() {} }); let rec = new JPAKEClient({ + __proto__: BaseController, displayPIN: function displayPIN(pin) { _("Received PIN " + pin + ". Entering it in the other computer..."); this.cid = pin.slice(JPAKE_LENGTH_SECRET); - Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); }); - }, - onAbort: function onAbort(error) { - do_throw("Shouldn't have aborted! " + error); + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); }, onComplete: function onComplete(a) { // Ensure channel was cleared, no error report. @@ -171,10 +215,11 @@ add_test(function test_success_receiveNoPIN() { }); -add_test(function test_firstMsgMaxTries() { +add_test(function test_firstMsgMaxTries_timeout() { _("Test abort when sender doesn't upload anything."); let rec = new JPAKEClient({ + __proto__: BaseController, displayPIN: function displayPIN(pin) { _("Received PIN " + pin + ". Doing nothing..."); this.cid = pin.slice(JPAKE_LENGTH_SECRET); @@ -186,33 +231,98 @@ add_test(function test_firstMsgMaxTries() { do_check_eq(error_report, JPAKE_ERROR_TIMEOUT); error_report = undefined; run_next_test(); - }, - onComplete: function onComplete() { - do_throw("Shouldn't have completed! "); } }); rec.receiveNoPIN(); }); +add_test(function test_firstMsgMaxTries() { + _("Test that receiver can wait longer for the first message."); + + let snd = new JPAKEClient({ + __proto__: BaseController, + onPaired: function onPaired() { + _("Pairing successful, sending final payload."); + Utils.nextTick(function() { snd.sendAndComplete(DATA); }); + }, + onComplete: function onComplete() {} + }); + + let rec = new JPAKEClient({ + __proto__: BaseController, + displayPIN: function displayPIN(pin) { + // For the purpose of the tests, the poll interval is 50ms and + // we're polling up to 5 times for the first exchange (as + // opposed to 2 times for most of the other exchanges). So let's + // pretend it took 150ms to enter the PIN on the sender. + _("Received PIN " + pin + ". Waiting 150ms before entering it into sender..."); + this.cid = pin.slice(JPAKE_LENGTH_SECRET); + Utils.namedTimer(function() { snd.pairWithPIN(pin, false); }, + 150, this, "_sendTimer"); + }, + onComplete: function onComplete(a) { + // Ensure channel was cleared, no error report. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, undefined); + run_next_test(); + } + }); + rec.receiveNoPIN(); +}); + + +add_test(function test_lastMsgMaxTries() { + _("Test that receiver can wait longer for the last message."); + + let snd = new JPAKEClient({ + __proto__: BaseController, + onPaired: function onPaired() { + // For the purpose of the tests, the poll interval is 50ms and + // we're polling up to 5 times for the last exchange (as opposed + // to 2 times for other exchanges). So let's pretend it took + // 150ms to come up with the final payload, which should require + // 3 polls. + _("Pairing successful, waiting 150ms to send final payload."); + Utils.namedTimer(function() { snd.sendAndComplete(DATA); }, + 150, this, "_sendTimer"); + }, + onComplete: function onComplete() {} + }); + + let rec = new JPAKEClient({ + __proto__: BaseController, + displayPIN: function displayPIN(pin) { + _("Received PIN " + pin + ". Entering it in the other computer..."); + this.cid = pin.slice(JPAKE_LENGTH_SECRET); + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); + }, + onComplete: function onComplete(a) { + // Ensure channel was cleared, no error report. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, undefined); + run_next_test(); + } + }); + + rec.receiveNoPIN(); +}); + + add_test(function test_wrongPIN() { _("Test abort when PINs don't match."); let snd = new JPAKEClient({ - displayPIN: function displayPIN() { - do_throw("displayPIN shouldn't have been called!"); - }, + __proto__: BaseController, onAbort: function onAbort(error) { do_check_eq(error, JPAKE_ERROR_KEYMISMATCH); do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH); error_report = undefined; - }, - onComplete: function onComplete() { - do_throw("Shouldn't have completed!"); } }); let rec = new JPAKEClient({ + __proto__: BaseController, displayPIN: function displayPIN(pin) { this.cid = pin.slice(JPAKE_LENGTH_SECRET); let secret = pin.slice(0, JPAKE_LENGTH_SECRET); @@ -220,16 +330,13 @@ add_test(function test_wrongPIN() { let new_pin = secret + this.cid; _("Received PIN " + pin + ", but I'm entering " + new_pin); - Utils.nextTick(function() { snd.sendWithPIN(new_pin, DATA); }); + Utils.nextTick(function() { snd.pairWithPIN(new_pin, false); }); }, onAbort: function onAbort(error) { do_check_eq(error, JPAKE_ERROR_NODATA); // Ensure channel was cleared. do_check_eq(channels[this.cid].data, undefined); run_next_test(); - }, - onComplete: function onComplete() { - do_throw("Shouldn't have completed! "); } }); rec.receiveNoPIN(); @@ -240,9 +347,7 @@ add_test(function test_abort_receiver() { _("Test user abort on receiving side."); let rec = new JPAKEClient({ - onComplete: function onComplete(data) { - do_throw("onComplete shouldn't be called."); - }, + __proto__: BaseController, onAbort: function onAbort(error) { // Manual abort = userabort. do_check_eq(error, JPAKE_ERROR_USERABORT); @@ -265,24 +370,17 @@ add_test(function test_abort_sender() { _("Test user abort on sending side."); let snd = new JPAKEClient({ - displayPIN: function displayPIN() { - do_throw("displayPIN shouldn't have been called!"); - }, + __proto__: BaseController, onAbort: function onAbort(error) { // Manual abort == userabort. do_check_eq(error, JPAKE_ERROR_USERABORT); do_check_eq(error_report, JPAKE_ERROR_USERABORT); error_report = undefined; - }, - onComplete: function onComplete() { - do_throw("Shouldn't have completed!"); } }); let rec = new JPAKEClient({ - onComplete: function onComplete(data) { - do_throw("onComplete shouldn't be called."); - }, + __proto__: BaseController, onAbort: function onAbort(error) { do_check_eq(error, JPAKE_ERROR_NODATA); // Ensure channel was cleared, no error report. @@ -293,7 +391,7 @@ add_test(function test_abort_sender() { displayPIN: function displayPIN(pin) { _("Received PIN " + pin + ". Entering it in the other computer..."); this.cid = pin.slice(JPAKE_LENGTH_SECRET); - Utils.nextTick(function() { snd.sendWithPIN(pin, DATA); }); + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); Utils.namedTimer(function() { snd.abort(); }, POLLINTERVAL, this, "_abortTimer"); } @@ -304,8 +402,13 @@ add_test(function test_abort_sender() { add_test(function test_wrongmessage() { let cid = new_channel(); - channels[cid].data = JSON.stringify({type: "receiver2", payload: {}}); + let channel = channels[cid]; + channel.data = JSON.stringify({type: "receiver2", + version: KEYEXCHANGE_VERSION, + payload: {}}); + channel.etag = '"fake-etag"'; let snd = new JPAKEClient({ + __proto__: BaseController, onComplete: function onComplete(data) { do_throw("onComplete shouldn't be called."); }, @@ -314,20 +417,19 @@ add_test(function test_wrongmessage() { run_next_test(); } }); - snd.sendWithPIN("01234567" + cid, DATA); + snd.pairWithPIN("01234567" + cid, false); }); add_test(function test_error_channel() { + let serverURL = Svc.Prefs.get("jpake.serverURL"); Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); let rec = new JPAKEClient({ - onComplete: function onComplete(data) { - do_throw("onComplete shouldn't be called."); - }, + __proto__: BaseController, onAbort: function onAbort(error) { do_check_eq(error, JPAKE_ERROR_CHANNEL); - Svc.Prefs.reset("jpake.serverURL"); + Svc.Prefs.set("jpake.serverURL", serverURL); run_next_test(); }, displayPIN: function displayPIN(pin) {} @@ -337,19 +439,64 @@ add_test(function test_error_channel() { add_test(function test_error_network() { + let serverURL = Svc.Prefs.get("jpake.serverURL"); Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); let snd = new JPAKEClient({ - onComplete: function onComplete(data) { - do_throw("onComplete shouldn't be called."); - }, + __proto__: BaseController, onAbort: function onAbort(error) { do_check_eq(error, JPAKE_ERROR_NETWORK); - Svc.Prefs.reset("jpake.serverURL"); + Svc.Prefs.set("jpake.serverURL", serverURL); run_next_test(); } }); - snd.sendWithPIN("0123456789ab", DATA); + snd.pairWithPIN("0123456789ab", false); +}); + + +add_test(function test_error_server_noETag() { + let cid = new_channel(); + let channel = channels[cid]; + channel.data = JSON.stringify({type: "receiver1", + version: KEYEXCHANGE_VERSION, + payload: {}}); + // This naughty server doesn't supply ETag (well, it supplies empty one). + channel.etag = ""; + let snd = new JPAKEClient({ + __proto__: BaseController, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_SERVER); + run_next_test(); + } + }); + snd.pairWithPIN("01234567" + cid, false); +}); + + +add_test(function test_error_delayNotSupported() { + let cid = new_channel(); + let channel = channels[cid]; + channel.data = JSON.stringify({type: "receiver1", + version: 2, + payload: {}}); + channel.etag = '"fake-etag"'; + let snd = new JPAKEClient({ + __proto__: BaseController, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_DELAYUNSUPPORTED); + run_next_test(); + } + }); + snd.pairWithPIN("01234567" + cid, true); +}); + + +add_test(function test_sendAndComplete_notPaired() { + let snd = new JPAKEClient({__proto__: BaseController}); + do_check_throws(function () { + snd.sendAndComplete(DATA); + }); + run_next_test(); });