diff options
Diffstat (limited to 'services/sync/tests/unit/test_jpakeclient.js')
-rw-r--r-- | services/sync/tests/unit/test_jpakeclient.js | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/services/sync/tests/unit/test_jpakeclient.js b/services/sync/tests/unit/test_jpakeclient.js new file mode 100644 index 000000000..783edb460 --- /dev/null +++ b/services/sync/tests/unit/test_jpakeclient.js @@ -0,0 +1,562 @@ +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://services-sync/identity.js"); +Cu.import("resource://services-sync/jpakeclient.js"); +Cu.import("resource://services-sync/constants.js"); +Cu.import("resource://services-sync/util.js"); +Cu.import("resource://testing-common/services/sync/utils.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"), stack); + + // Ensure key exchange ID is set and the right length + do_check_true(request.hasHeader("X-KeyExchange-Id"), stack); + do_check_eq(request.getHeader("X-KeyExchange-Id").length, + 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]) { + cid = Math.floor(Math.random() * 10000); + } + let channel = channels[cid] = new ServerChannel(); + server.registerPathHandler("/" + cid, channel.handler()); + return cid; +} + +var server; +var channels = {}; // Map channel -> ServerChannel object +function server_new_channel(request, response) { + check_headers(request); + let cid = new_channel(); + let body = JSON.stringify("" + cid); + response.setStatusLine(request.httpVersion, 200, "OK"); + response.bodyOutputStream.write(body, body.length); +} + +var error_report; +function server_report(request, response) { + check_headers(request); + + 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) { + channel.clear(); + } + } + + response.setStatusLine(request.httpVersion, 200, "OK"); +} + +// Hook for test code. +var hooks = {}; +function initHooks() { + hooks.onGET = function onGET(request) {}; +} +initHooks(); + +function ServerChannel() { + this.data = ""; + this.etag = ""; + this.getCount = 0; +} +ServerChannel.prototype = { + + GET: function GET(request, response) { + if (!this.data) { + 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) { + response.setStatusLine(request.httpVersion, 304, "Not Modified"); + hooks.onGET(request); + 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 == SERVER_MAX_GETS) { + this.clear(); + } + hooks.onGET(request); + }, + + 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); + response.setStatusLine(request.httpVersion, 200, "OK"); + }, + + clear: function clear() { + delete this.data; + }, + + handler: function handler() { + let self = this; + return function(request, response) { + check_headers(request); + let method = self[request.method]; + return method.apply(self, arguments); + }; + } + +}; + + +/** + * Controller that throws for everything. + */ +var BaseController = { + displayPIN: function displayPIN() { + do_throw("displayPIN() shouldn't have been called!"); + }, + onPairingStart: function onPairingStart() { + do_throw("onPairingStart 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() { + server = httpd_setup({"/new_channel": server_new_channel, + "/report": server_report}); + Svc.Prefs.set("jpake.serverURL", server.baseURI + "/"); + Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL); + 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(""); + }); + + // Ensure PSM is initialized. + Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); + + // Simulate Sync setup with credentials in place. We want to make + // sure the J-PAKE requests don't include those data. + ensureLegacyIdentityManager(); + setBasicCredentials("johndoe", "ilovejane"); + + initTestLogging("Trace"); + Log.repository.getLogger("Sync.JPAKEClient").level = Log.Level.Trace; + Log.repository.getLogger("Common.RESTRequest").level = + Log.Level.Trace; + run_next_test(); +} + + +add_test(function test_success_receiveNoPIN() { + _("Test a successful exchange started by receiveNoPIN()."); + + let snd = new JPAKEClient({ + __proto__: BaseController, + onPaired: function onPaired() { + _("Pairing successful, sending final payload."); + do_check_true(pairingStartCalledOnReceiver); + Utils.nextTick(function() { snd.sendAndComplete(DATA); }); + }, + onComplete: function onComplete() {} + }); + + let pairingStartCalledOnReceiver = false; + 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); }); + }, + onPairingStart: function onPairingStart() { + pairingStartCalledOnReceiver = true; + }, + onComplete: function onComplete(data) { + do_check_true(Utils.deepEquals(DATA, data)); + // 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_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); + }, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_TIMEOUT); + // Ensure channel was cleared, error report was sent. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, JPAKE_ERROR_TIMEOUT); + error_report = undefined; + run_next_test(); + } + }); + 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, which should + // require 3 polls. + // Rather than using an imprecise timer, we hook into the channel's + // GET handler to know how long to wait. + _("Received PIN " + pin + ". Waiting for three polls before entering it into sender..."); + this.cid = pin.slice(JPAKE_LENGTH_SECRET); + let count = 0; + hooks.onGET = function onGET(request) { + if (++count == 3) { + _("Third GET. Triggering pair."); + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); + } + }; + }, + onPairingStart: function onPairingStart(pin) {}, + onComplete: function onComplete(data) { + do_check_true(Utils.deepEquals(DATA, data)); + // Ensure channel was cleared, no error report. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, undefined); + + // Clean up. + initHooks(); + 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. + // Rather than using an imprecise timer, we hook into the channel's + // GET handler to know how long to wait. + let count = 0; + hooks.onGET = function onGET(request) { + if (++count == 3) { + _("Third GET. Triggering send."); + 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.pairWithPIN(pin, false); }); + }, + onPairingStart: function onPairingStart(pin) {}, + onComplete: function onComplete(data) { + do_check_true(Utils.deepEquals(DATA, data)); + // Ensure channel was cleared, no error report. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, undefined); + + // Clean up. + initHooks(); + run_next_test(); + } + }); + + rec.receiveNoPIN(); +}); + + +add_test(function test_wrongPIN() { + _("Test abort when PINs don't match."); + + let snd = new JPAKEClient({ + __proto__: BaseController, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_KEYMISMATCH); + do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH); + error_report = undefined; + } + }); + + let pairingStartCalledOnReceiver = false; + 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); + secret = Array.prototype.slice.call(secret).reverse().join(""); + let new_pin = secret + this.cid; + _("Received PIN " + pin + ", but I'm entering " + new_pin); + + Utils.nextTick(function() { snd.pairWithPIN(new_pin, false); }); + }, + onPairingStart: function onPairingStart() { + pairingStartCalledOnReceiver = true; + }, + onAbort: function onAbort(error) { + do_check_true(pairingStartCalledOnReceiver); + do_check_eq(error, JPAKE_ERROR_NODATA); + // Ensure channel was cleared. + do_check_eq(channels[this.cid].data, undefined); + run_next_test(); + } + }); + rec.receiveNoPIN(); +}); + + +add_test(function test_abort_receiver() { + _("Test user abort on receiving side."); + + let rec = new JPAKEClient({ + __proto__: BaseController, + onAbort: function onAbort(error) { + // Manual abort = userabort. + do_check_eq(error, JPAKE_ERROR_USERABORT); + // Ensure channel was cleared. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, JPAKE_ERROR_USERABORT); + error_report = undefined; + run_next_test(); + }, + displayPIN: function displayPIN(pin) { + this.cid = pin.slice(JPAKE_LENGTH_SECRET); + Utils.nextTick(function() { rec.abort(); }); + } + }); + rec.receiveNoPIN(); +}); + + +add_test(function test_abort_sender() { + _("Test user abort on sending side."); + + let snd = new JPAKEClient({ + __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; + } + }); + + let rec = new JPAKEClient({ + __proto__: BaseController, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_NODATA); + // Ensure channel was cleared, no error report. + do_check_eq(channels[this.cid].data, undefined); + do_check_eq(error_report, undefined); + initHooks(); + run_next_test(); + }, + 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); }); + + // Abort after the first poll. + let count = 0; + hooks.onGET = function onGET(request) { + if (++count >= 1) { + _("First GET. Aborting."); + Utils.nextTick(function() { snd.abort(); }); + } + }; + }, + onPairingStart: function onPairingStart(pin) {} + }); + rec.receiveNoPIN(); +}); + + +add_test(function test_wrongmessage() { + let cid = new_channel(); + 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."); + }, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_WRONGMESSAGE); + run_next_test(); + } + }); + 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({ + __proto__: BaseController, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_CHANNEL); + Svc.Prefs.set("jpake.serverURL", serverURL); + run_next_test(); + }, + onPairingStart: function onPairingStart(pin) {}, + displayPIN: function displayPIN(pin) {} + }); + rec.receiveNoPIN(); +}); + + +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({ + __proto__: BaseController, + onAbort: function onAbort(error) { + do_check_eq(error, JPAKE_ERROR_NETWORK); + Svc.Prefs.set("jpake.serverURL", serverURL); + run_next_test(); + } + }); + 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(); +}); + + +add_test(function tearDown() { + server.stop(run_next_test); +}); |