1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_jpakeclient.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,562 @@ 1.4 +Cu.import("resource://gre/modules/Log.jsm"); 1.5 +Cu.import("resource://services-sync/identity.js"); 1.6 +Cu.import("resource://services-sync/jpakeclient.js"); 1.7 +Cu.import("resource://services-sync/constants.js"); 1.8 +Cu.import("resource://services-sync/util.js"); 1.9 +Cu.import("resource://testing-common/services/sync/utils.js"); 1.10 + 1.11 +const JPAKE_LENGTH_SECRET = 8; 1.12 +const JPAKE_LENGTH_CLIENTID = 256; 1.13 +const KEYEXCHANGE_VERSION = 3; 1.14 + 1.15 +/* 1.16 + * Simple server. 1.17 + */ 1.18 + 1.19 +const SERVER_MAX_GETS = 6; 1.20 + 1.21 +function check_headers(request) { 1.22 + let stack = Components.stack.caller; 1.23 + 1.24 + // There shouldn't be any Basic auth 1.25 + do_check_false(request.hasHeader("Authorization"), stack); 1.26 + 1.27 + // Ensure key exchange ID is set and the right length 1.28 + do_check_true(request.hasHeader("X-KeyExchange-Id"), stack); 1.29 + do_check_eq(request.getHeader("X-KeyExchange-Id").length, 1.30 + JPAKE_LENGTH_CLIENTID, stack); 1.31 +} 1.32 + 1.33 +function new_channel() { 1.34 + // Create a new channel and register it with the server. 1.35 + let cid = Math.floor(Math.random() * 10000); 1.36 + while (channels[cid]) { 1.37 + cid = Math.floor(Math.random() * 10000); 1.38 + } 1.39 + let channel = channels[cid] = new ServerChannel(); 1.40 + server.registerPathHandler("/" + cid, channel.handler()); 1.41 + return cid; 1.42 +} 1.43 + 1.44 +let server; 1.45 +let channels = {}; // Map channel -> ServerChannel object 1.46 +function server_new_channel(request, response) { 1.47 + check_headers(request); 1.48 + let cid = new_channel(); 1.49 + let body = JSON.stringify("" + cid); 1.50 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.51 + response.bodyOutputStream.write(body, body.length); 1.52 +} 1.53 + 1.54 +let error_report; 1.55 +function server_report(request, response) { 1.56 + check_headers(request); 1.57 + 1.58 + if (request.hasHeader("X-KeyExchange-Log")) { 1.59 + error_report = request.getHeader("X-KeyExchange-Log"); 1.60 + } 1.61 + 1.62 + if (request.hasHeader("X-KeyExchange-Cid")) { 1.63 + let cid = request.getHeader("X-KeyExchange-Cid"); 1.64 + let channel = channels[cid]; 1.65 + if (channel) { 1.66 + channel.clear(); 1.67 + } 1.68 + } 1.69 + 1.70 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.71 +} 1.72 + 1.73 +// Hook for test code. 1.74 +let hooks = {}; 1.75 +function initHooks() { 1.76 + hooks.onGET = function onGET(request) {}; 1.77 +} 1.78 +initHooks(); 1.79 + 1.80 +function ServerChannel() { 1.81 + this.data = ""; 1.82 + this.etag = ""; 1.83 + this.getCount = 0; 1.84 +} 1.85 +ServerChannel.prototype = { 1.86 + 1.87 + GET: function GET(request, response) { 1.88 + if (!this.data) { 1.89 + response.setStatusLine(request.httpVersion, 404, "Not Found"); 1.90 + return; 1.91 + } 1.92 + 1.93 + if (request.hasHeader("If-None-Match")) { 1.94 + let etag = request.getHeader("If-None-Match"); 1.95 + if (etag == this.etag) { 1.96 + response.setStatusLine(request.httpVersion, 304, "Not Modified"); 1.97 + hooks.onGET(request); 1.98 + return; 1.99 + } 1.100 + } 1.101 + response.setHeader("ETag", this.etag); 1.102 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.103 + response.bodyOutputStream.write(this.data, this.data.length); 1.104 + 1.105 + // Automatically clear the channel after 6 successful GETs. 1.106 + this.getCount += 1; 1.107 + if (this.getCount == SERVER_MAX_GETS) { 1.108 + this.clear(); 1.109 + } 1.110 + hooks.onGET(request); 1.111 + }, 1.112 + 1.113 + PUT: function PUT(request, response) { 1.114 + if (this.data) { 1.115 + do_check_true(request.hasHeader("If-Match")); 1.116 + let etag = request.getHeader("If-Match"); 1.117 + if (etag != this.etag) { 1.118 + response.setHeader("ETag", this.etag); 1.119 + response.setStatusLine(request.httpVersion, 412, "Precondition Failed"); 1.120 + return; 1.121 + } 1.122 + } else { 1.123 + do_check_true(request.hasHeader("If-None-Match")); 1.124 + do_check_eq(request.getHeader("If-None-Match"), "*"); 1.125 + } 1.126 + 1.127 + this.data = readBytesFromInputStream(request.bodyInputStream); 1.128 + this.etag = '"' + Utils.sha1(this.data) + '"'; 1.129 + response.setHeader("ETag", this.etag); 1.130 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.131 + }, 1.132 + 1.133 + clear: function clear() { 1.134 + delete this.data; 1.135 + }, 1.136 + 1.137 + handler: function handler() { 1.138 + let self = this; 1.139 + return function(request, response) { 1.140 + check_headers(request); 1.141 + let method = self[request.method]; 1.142 + return method.apply(self, arguments); 1.143 + }; 1.144 + } 1.145 + 1.146 +}; 1.147 + 1.148 + 1.149 +/** 1.150 + * Controller that throws for everything. 1.151 + */ 1.152 +let BaseController = { 1.153 + displayPIN: function displayPIN() { 1.154 + do_throw("displayPIN() shouldn't have been called!"); 1.155 + }, 1.156 + onPairingStart: function onPairingStart() { 1.157 + do_throw("onPairingStart shouldn't have been called!"); 1.158 + }, 1.159 + onAbort: function onAbort(error) { 1.160 + do_throw("Shouldn't have aborted with " + error + "!"); 1.161 + }, 1.162 + onPaired: function onPaired() { 1.163 + do_throw("onPaired() shouldn't have been called!"); 1.164 + }, 1.165 + onComplete: function onComplete(data) { 1.166 + do_throw("Shouldn't have completed with " + data + "!"); 1.167 + } 1.168 +}; 1.169 + 1.170 + 1.171 +const DATA = {"msg": "eggstreamly sekrit"}; 1.172 +const POLLINTERVAL = 50; 1.173 + 1.174 +function run_test() { 1.175 + server = httpd_setup({"/new_channel": server_new_channel, 1.176 + "/report": server_report}); 1.177 + Svc.Prefs.set("jpake.serverURL", server.baseURI + "/"); 1.178 + Svc.Prefs.set("jpake.pollInterval", POLLINTERVAL); 1.179 + Svc.Prefs.set("jpake.maxTries", 2); 1.180 + Svc.Prefs.set("jpake.firstMsgMaxTries", 5); 1.181 + Svc.Prefs.set("jpake.lastMsgMaxTries", 5); 1.182 + // Ensure clean up 1.183 + Svc.Obs.add("profile-before-change", function() { 1.184 + Svc.Prefs.resetBranch(""); 1.185 + }); 1.186 + 1.187 + // Ensure PSM is initialized. 1.188 + Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports); 1.189 + 1.190 + // Simulate Sync setup with credentials in place. We want to make 1.191 + // sure the J-PAKE requests don't include those data. 1.192 + ensureLegacyIdentityManager(); 1.193 + setBasicCredentials("johndoe", "ilovejane"); 1.194 + 1.195 + initTestLogging("Trace"); 1.196 + Log.repository.getLogger("Sync.JPAKEClient").level = Log.Level.Trace; 1.197 + Log.repository.getLogger("Common.RESTRequest").level = 1.198 + Log.Level.Trace; 1.199 + run_next_test(); 1.200 +} 1.201 + 1.202 + 1.203 +add_test(function test_success_receiveNoPIN() { 1.204 + _("Test a successful exchange started by receiveNoPIN()."); 1.205 + 1.206 + let snd = new JPAKEClient({ 1.207 + __proto__: BaseController, 1.208 + onPaired: function onPaired() { 1.209 + _("Pairing successful, sending final payload."); 1.210 + do_check_true(pairingStartCalledOnReceiver); 1.211 + Utils.nextTick(function() { snd.sendAndComplete(DATA); }); 1.212 + }, 1.213 + onComplete: function onComplete() {} 1.214 + }); 1.215 + 1.216 + let pairingStartCalledOnReceiver = false; 1.217 + let rec = new JPAKEClient({ 1.218 + __proto__: BaseController, 1.219 + displayPIN: function displayPIN(pin) { 1.220 + _("Received PIN " + pin + ". Entering it in the other computer..."); 1.221 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.222 + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); 1.223 + }, 1.224 + onPairingStart: function onPairingStart() { 1.225 + pairingStartCalledOnReceiver = true; 1.226 + }, 1.227 + onComplete: function onComplete(data) { 1.228 + do_check_true(Utils.deepEquals(DATA, data)); 1.229 + // Ensure channel was cleared, no error report. 1.230 + do_check_eq(channels[this.cid].data, undefined); 1.231 + do_check_eq(error_report, undefined); 1.232 + run_next_test(); 1.233 + } 1.234 + }); 1.235 + rec.receiveNoPIN(); 1.236 +}); 1.237 + 1.238 + 1.239 +add_test(function test_firstMsgMaxTries_timeout() { 1.240 + _("Test abort when sender doesn't upload anything."); 1.241 + 1.242 + let rec = new JPAKEClient({ 1.243 + __proto__: BaseController, 1.244 + displayPIN: function displayPIN(pin) { 1.245 + _("Received PIN " + pin + ". Doing nothing..."); 1.246 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.247 + }, 1.248 + onAbort: function onAbort(error) { 1.249 + do_check_eq(error, JPAKE_ERROR_TIMEOUT); 1.250 + // Ensure channel was cleared, error report was sent. 1.251 + do_check_eq(channels[this.cid].data, undefined); 1.252 + do_check_eq(error_report, JPAKE_ERROR_TIMEOUT); 1.253 + error_report = undefined; 1.254 + run_next_test(); 1.255 + } 1.256 + }); 1.257 + rec.receiveNoPIN(); 1.258 +}); 1.259 + 1.260 + 1.261 +add_test(function test_firstMsgMaxTries() { 1.262 + _("Test that receiver can wait longer for the first message."); 1.263 + 1.264 + let snd = new JPAKEClient({ 1.265 + __proto__: BaseController, 1.266 + onPaired: function onPaired() { 1.267 + _("Pairing successful, sending final payload."); 1.268 + Utils.nextTick(function() { snd.sendAndComplete(DATA); }); 1.269 + }, 1.270 + onComplete: function onComplete() {} 1.271 + }); 1.272 + 1.273 + let rec = new JPAKEClient({ 1.274 + __proto__: BaseController, 1.275 + displayPIN: function displayPIN(pin) { 1.276 + // For the purpose of the tests, the poll interval is 50ms and 1.277 + // we're polling up to 5 times for the first exchange (as 1.278 + // opposed to 2 times for most of the other exchanges). So let's 1.279 + // pretend it took 150ms to enter the PIN on the sender, which should 1.280 + // require 3 polls. 1.281 + // Rather than using an imprecise timer, we hook into the channel's 1.282 + // GET handler to know how long to wait. 1.283 + _("Received PIN " + pin + ". Waiting for three polls before entering it into sender..."); 1.284 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.285 + let count = 0; 1.286 + hooks.onGET = function onGET(request) { 1.287 + if (++count == 3) { 1.288 + _("Third GET. Triggering pair."); 1.289 + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); 1.290 + } 1.291 + }; 1.292 + }, 1.293 + onPairingStart: function onPairingStart(pin) {}, 1.294 + onComplete: function onComplete(data) { 1.295 + do_check_true(Utils.deepEquals(DATA, data)); 1.296 + // Ensure channel was cleared, no error report. 1.297 + do_check_eq(channels[this.cid].data, undefined); 1.298 + do_check_eq(error_report, undefined); 1.299 + 1.300 + // Clean up. 1.301 + initHooks(); 1.302 + run_next_test(); 1.303 + } 1.304 + }); 1.305 + rec.receiveNoPIN(); 1.306 +}); 1.307 + 1.308 + 1.309 +add_test(function test_lastMsgMaxTries() { 1.310 + _("Test that receiver can wait longer for the last message."); 1.311 + 1.312 + let snd = new JPAKEClient({ 1.313 + __proto__: BaseController, 1.314 + onPaired: function onPaired() { 1.315 + // For the purpose of the tests, the poll interval is 50ms and 1.316 + // we're polling up to 5 times for the last exchange (as opposed 1.317 + // to 2 times for other exchanges). So let's pretend it took 1.318 + // 150ms to come up with the final payload, which should require 1.319 + // 3 polls. 1.320 + // Rather than using an imprecise timer, we hook into the channel's 1.321 + // GET handler to know how long to wait. 1.322 + let count = 0; 1.323 + hooks.onGET = function onGET(request) { 1.324 + if (++count == 3) { 1.325 + _("Third GET. Triggering send."); 1.326 + Utils.nextTick(function() { snd.sendAndComplete(DATA); }); 1.327 + } 1.328 + }; 1.329 + }, 1.330 + onComplete: function onComplete() {} 1.331 + }); 1.332 + 1.333 + let rec = new JPAKEClient({ 1.334 + __proto__: BaseController, 1.335 + displayPIN: function displayPIN(pin) { 1.336 + _("Received PIN " + pin + ". Entering it in the other computer..."); 1.337 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.338 + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); 1.339 + }, 1.340 + onPairingStart: function onPairingStart(pin) {}, 1.341 + onComplete: function onComplete(data) { 1.342 + do_check_true(Utils.deepEquals(DATA, data)); 1.343 + // Ensure channel was cleared, no error report. 1.344 + do_check_eq(channels[this.cid].data, undefined); 1.345 + do_check_eq(error_report, undefined); 1.346 + 1.347 + // Clean up. 1.348 + initHooks(); 1.349 + run_next_test(); 1.350 + } 1.351 + }); 1.352 + 1.353 + rec.receiveNoPIN(); 1.354 +}); 1.355 + 1.356 + 1.357 +add_test(function test_wrongPIN() { 1.358 + _("Test abort when PINs don't match."); 1.359 + 1.360 + let snd = new JPAKEClient({ 1.361 + __proto__: BaseController, 1.362 + onAbort: function onAbort(error) { 1.363 + do_check_eq(error, JPAKE_ERROR_KEYMISMATCH); 1.364 + do_check_eq(error_report, JPAKE_ERROR_KEYMISMATCH); 1.365 + error_report = undefined; 1.366 + } 1.367 + }); 1.368 + 1.369 + let pairingStartCalledOnReceiver = false; 1.370 + let rec = new JPAKEClient({ 1.371 + __proto__: BaseController, 1.372 + displayPIN: function displayPIN(pin) { 1.373 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.374 + let secret = pin.slice(0, JPAKE_LENGTH_SECRET); 1.375 + secret = [char for each (char in secret)].reverse().join(""); 1.376 + let new_pin = secret + this.cid; 1.377 + _("Received PIN " + pin + ", but I'm entering " + new_pin); 1.378 + 1.379 + Utils.nextTick(function() { snd.pairWithPIN(new_pin, false); }); 1.380 + }, 1.381 + onPairingStart: function onPairingStart() { 1.382 + pairingStartCalledOnReceiver = true; 1.383 + }, 1.384 + onAbort: function onAbort(error) { 1.385 + do_check_true(pairingStartCalledOnReceiver); 1.386 + do_check_eq(error, JPAKE_ERROR_NODATA); 1.387 + // Ensure channel was cleared. 1.388 + do_check_eq(channels[this.cid].data, undefined); 1.389 + run_next_test(); 1.390 + } 1.391 + }); 1.392 + rec.receiveNoPIN(); 1.393 +}); 1.394 + 1.395 + 1.396 +add_test(function test_abort_receiver() { 1.397 + _("Test user abort on receiving side."); 1.398 + 1.399 + let rec = new JPAKEClient({ 1.400 + __proto__: BaseController, 1.401 + onAbort: function onAbort(error) { 1.402 + // Manual abort = userabort. 1.403 + do_check_eq(error, JPAKE_ERROR_USERABORT); 1.404 + // Ensure channel was cleared. 1.405 + do_check_eq(channels[this.cid].data, undefined); 1.406 + do_check_eq(error_report, JPAKE_ERROR_USERABORT); 1.407 + error_report = undefined; 1.408 + run_next_test(); 1.409 + }, 1.410 + displayPIN: function displayPIN(pin) { 1.411 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.412 + Utils.nextTick(function() { rec.abort(); }); 1.413 + } 1.414 + }); 1.415 + rec.receiveNoPIN(); 1.416 +}); 1.417 + 1.418 + 1.419 +add_test(function test_abort_sender() { 1.420 + _("Test user abort on sending side."); 1.421 + 1.422 + let snd = new JPAKEClient({ 1.423 + __proto__: BaseController, 1.424 + onAbort: function onAbort(error) { 1.425 + // Manual abort == userabort. 1.426 + do_check_eq(error, JPAKE_ERROR_USERABORT); 1.427 + do_check_eq(error_report, JPAKE_ERROR_USERABORT); 1.428 + error_report = undefined; 1.429 + } 1.430 + }); 1.431 + 1.432 + let rec = new JPAKEClient({ 1.433 + __proto__: BaseController, 1.434 + onAbort: function onAbort(error) { 1.435 + do_check_eq(error, JPAKE_ERROR_NODATA); 1.436 + // Ensure channel was cleared, no error report. 1.437 + do_check_eq(channels[this.cid].data, undefined); 1.438 + do_check_eq(error_report, undefined); 1.439 + initHooks(); 1.440 + run_next_test(); 1.441 + }, 1.442 + displayPIN: function displayPIN(pin) { 1.443 + _("Received PIN " + pin + ". Entering it in the other computer..."); 1.444 + this.cid = pin.slice(JPAKE_LENGTH_SECRET); 1.445 + Utils.nextTick(function() { snd.pairWithPIN(pin, false); }); 1.446 + 1.447 + // Abort after the first poll. 1.448 + let count = 0; 1.449 + hooks.onGET = function onGET(request) { 1.450 + if (++count >= 1) { 1.451 + _("First GET. Aborting."); 1.452 + Utils.nextTick(function() { snd.abort(); }); 1.453 + } 1.454 + }; 1.455 + }, 1.456 + onPairingStart: function onPairingStart(pin) {} 1.457 + }); 1.458 + rec.receiveNoPIN(); 1.459 +}); 1.460 + 1.461 + 1.462 +add_test(function test_wrongmessage() { 1.463 + let cid = new_channel(); 1.464 + let channel = channels[cid]; 1.465 + channel.data = JSON.stringify({type: "receiver2", 1.466 + version: KEYEXCHANGE_VERSION, 1.467 + payload: {}}); 1.468 + channel.etag = '"fake-etag"'; 1.469 + let snd = new JPAKEClient({ 1.470 + __proto__: BaseController, 1.471 + onComplete: function onComplete(data) { 1.472 + do_throw("onComplete shouldn't be called."); 1.473 + }, 1.474 + onAbort: function onAbort(error) { 1.475 + do_check_eq(error, JPAKE_ERROR_WRONGMESSAGE); 1.476 + run_next_test(); 1.477 + } 1.478 + }); 1.479 + snd.pairWithPIN("01234567" + cid, false); 1.480 +}); 1.481 + 1.482 + 1.483 +add_test(function test_error_channel() { 1.484 + let serverURL = Svc.Prefs.get("jpake.serverURL"); 1.485 + Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); 1.486 + 1.487 + let rec = new JPAKEClient({ 1.488 + __proto__: BaseController, 1.489 + onAbort: function onAbort(error) { 1.490 + do_check_eq(error, JPAKE_ERROR_CHANNEL); 1.491 + Svc.Prefs.set("jpake.serverURL", serverURL); 1.492 + run_next_test(); 1.493 + }, 1.494 + onPairingStart: function onPairingStart(pin) {}, 1.495 + displayPIN: function displayPIN(pin) {} 1.496 + }); 1.497 + rec.receiveNoPIN(); 1.498 +}); 1.499 + 1.500 + 1.501 +add_test(function test_error_network() { 1.502 + let serverURL = Svc.Prefs.get("jpake.serverURL"); 1.503 + Svc.Prefs.set("jpake.serverURL", "http://localhost:12345/"); 1.504 + 1.505 + let snd = new JPAKEClient({ 1.506 + __proto__: BaseController, 1.507 + onAbort: function onAbort(error) { 1.508 + do_check_eq(error, JPAKE_ERROR_NETWORK); 1.509 + Svc.Prefs.set("jpake.serverURL", serverURL); 1.510 + run_next_test(); 1.511 + } 1.512 + }); 1.513 + snd.pairWithPIN("0123456789ab", false); 1.514 +}); 1.515 + 1.516 + 1.517 +add_test(function test_error_server_noETag() { 1.518 + let cid = new_channel(); 1.519 + let channel = channels[cid]; 1.520 + channel.data = JSON.stringify({type: "receiver1", 1.521 + version: KEYEXCHANGE_VERSION, 1.522 + payload: {}}); 1.523 + // This naughty server doesn't supply ETag (well, it supplies empty one). 1.524 + channel.etag = ""; 1.525 + let snd = new JPAKEClient({ 1.526 + __proto__: BaseController, 1.527 + onAbort: function onAbort(error) { 1.528 + do_check_eq(error, JPAKE_ERROR_SERVER); 1.529 + run_next_test(); 1.530 + } 1.531 + }); 1.532 + snd.pairWithPIN("01234567" + cid, false); 1.533 +}); 1.534 + 1.535 + 1.536 +add_test(function test_error_delayNotSupported() { 1.537 + let cid = new_channel(); 1.538 + let channel = channels[cid]; 1.539 + channel.data = JSON.stringify({type: "receiver1", 1.540 + version: 2, 1.541 + payload: {}}); 1.542 + channel.etag = '"fake-etag"'; 1.543 + let snd = new JPAKEClient({ 1.544 + __proto__: BaseController, 1.545 + onAbort: function onAbort(error) { 1.546 + do_check_eq(error, JPAKE_ERROR_DELAYUNSUPPORTED); 1.547 + run_next_test(); 1.548 + } 1.549 + }); 1.550 + snd.pairWithPIN("01234567" + cid, true); 1.551 +}); 1.552 + 1.553 + 1.554 +add_test(function test_sendAndComplete_notPaired() { 1.555 + let snd = new JPAKEClient({__proto__: BaseController}); 1.556 + do_check_throws(function () { 1.557 + snd.sendAndComplete(DATA); 1.558 + }); 1.559 + run_next_test(); 1.560 +}); 1.561 + 1.562 + 1.563 +add_test(function tearDown() { 1.564 + server.stop(run_next_test); 1.565 +});