services/sync/modules/jpakeclient.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
michael@0 6
michael@0 7 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
michael@0 8
michael@0 9 Cu.import("resource://gre/modules/Log.jsm");
michael@0 10 Cu.import("resource://services-common/rest.js");
michael@0 11 Cu.import("resource://services-sync/constants.js");
michael@0 12 Cu.import("resource://services-sync/util.js");
michael@0 13
michael@0 14 const REQUEST_TIMEOUT = 60; // 1 minute
michael@0 15 const KEYEXCHANGE_VERSION = 3;
michael@0 16
michael@0 17 const JPAKE_SIGNERID_SENDER = "sender";
michael@0 18 const JPAKE_SIGNERID_RECEIVER = "receiver";
michael@0 19 const JPAKE_LENGTH_SECRET = 8;
michael@0 20 const JPAKE_LENGTH_CLIENTID = 256;
michael@0 21 const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
michael@0 22
michael@0 23
michael@0 24 /**
michael@0 25 * Client to exchange encrypted data using the J-PAKE algorithm.
michael@0 26 * The exchange between two clients of this type looks like this:
michael@0 27 *
michael@0 28 *
michael@0 29 * Mobile Server Desktop
michael@0 30 * ===================================================================
michael@0 31 * |
michael@0 32 * retrieve channel <---------------|
michael@0 33 * generate random secret |
michael@0 34 * show PIN = secret + channel | ask user for PIN
michael@0 35 * upload Mobile's message 1 ------>|
michael@0 36 * |----> retrieve Mobile's message 1
michael@0 37 * |<----- upload Desktop's message 1
michael@0 38 * retrieve Desktop's message 1 <---|
michael@0 39 * upload Mobile's message 2 ------>|
michael@0 40 * |----> retrieve Mobile's message 2
michael@0 41 * | compute key
michael@0 42 * |<----- upload Desktop's message 2
michael@0 43 * retrieve Desktop's message 2 <---|
michael@0 44 * compute key |
michael@0 45 * encrypt known value ------------>|
michael@0 46 * |-------> retrieve encrypted value
michael@0 47 * | verify against local known value
michael@0 48 *
michael@0 49 * At this point Desktop knows whether the PIN was entered correctly.
michael@0 50 * If it wasn't, Desktop deletes the session. If it was, the account
michael@0 51 * setup can proceed. If Desktop doesn't yet have an account set up,
michael@0 52 * it will keep the channel open and let the user connect to or
michael@0 53 * create an account.
michael@0 54 *
michael@0 55 * | encrypt credentials
michael@0 56 * |<------------- upload credentials
michael@0 57 * retrieve credentials <-----------|
michael@0 58 * verify HMAC |
michael@0 59 * decrypt credentials |
michael@0 60 * delete session ----------------->|
michael@0 61 * start syncing |
michael@0 62 *
michael@0 63 *
michael@0 64 * Create a client object like so:
michael@0 65 *
michael@0 66 * let client = new JPAKEClient(controller);
michael@0 67 *
michael@0 68 * The 'controller' object must implement the following methods:
michael@0 69 *
michael@0 70 * displayPIN(pin) -- Called when a PIN has been generated and is ready to
michael@0 71 * be displayed to the user. Only called on the client where the pairing
michael@0 72 * was initiated with 'receiveNoPIN()'.
michael@0 73 *
michael@0 74 * onPairingStart() -- Called when the pairing has started and messages are
michael@0 75 * being sent back and forth over the channel. Only called on the client
michael@0 76 * where the pairing was initiated with 'receiveNoPIN()'.
michael@0 77 *
michael@0 78 * onPaired() -- Called when the device pairing has been established and
michael@0 79 * we're ready to send the credentials over. To do that, the controller
michael@0 80 * must call 'sendAndComplete()' while the channel is active.
michael@0 81 *
michael@0 82 * onComplete(data) -- Called after transfer has been completed. On
michael@0 83 * the sending side this is called with no parameter and as soon as the
michael@0 84 * data has been uploaded. This does not mean the receiving side has
michael@0 85 * actually retrieved them yet.
michael@0 86 *
michael@0 87 * onAbort(error) -- Called whenever an error is encountered. All errors lead
michael@0 88 * to an abort and the process has to be started again on both sides.
michael@0 89 *
michael@0 90 * To start the data transfer on the receiving side, call
michael@0 91 *
michael@0 92 * client.receiveNoPIN();
michael@0 93 *
michael@0 94 * This will allocate a new channel on the server, generate a PIN, have it
michael@0 95 * displayed and then do the transfer once the protocol has been completed
michael@0 96 * with the sending side.
michael@0 97 *
michael@0 98 * To initiate the transfer from the sending side, call
michael@0 99 *
michael@0 100 * client.pairWithPIN(pin, true);
michael@0 101 *
michael@0 102 * Once the pairing has been established, the controller's 'onPaired()' method
michael@0 103 * will be called. To then transmit the data, call
michael@0 104 *
michael@0 105 * client.sendAndComplete(data);
michael@0 106 *
michael@0 107 * To abort the process, call
michael@0 108 *
michael@0 109 * client.abort();
michael@0 110 *
michael@0 111 * Note that after completion or abort, the 'client' instance may not be reused.
michael@0 112 * You will have to create a new one in case you'd like to restart the process.
michael@0 113 */
michael@0 114 this.JPAKEClient = function JPAKEClient(controller) {
michael@0 115 this.controller = controller;
michael@0 116
michael@0 117 this._log = Log.repository.getLogger("Sync.JPAKEClient");
michael@0 118 this._log.level = Log.Level[Svc.Prefs.get(
michael@0 119 "log.logger.service.jpakeclient", "Debug")];
michael@0 120
michael@0 121 this._serverURL = Svc.Prefs.get("jpake.serverURL");
michael@0 122 this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
michael@0 123 this._maxTries = Svc.Prefs.get("jpake.maxTries");
michael@0 124 if (this._serverURL.slice(-1) != "/") {
michael@0 125 this._serverURL += "/";
michael@0 126 }
michael@0 127
michael@0 128 this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
michael@0 129 .createInstance(Ci.nsISyncJPAKE);
michael@0 130
michael@0 131 this._setClientID();
michael@0 132 }
michael@0 133 JPAKEClient.prototype = {
michael@0 134
michael@0 135 _chain: Async.chain,
michael@0 136
michael@0 137 /*
michael@0 138 * Public API
michael@0 139 */
michael@0 140
michael@0 141 /**
michael@0 142 * Initiate pairing and receive data without providing a PIN. The PIN will
michael@0 143 * be generated and passed on to the controller to be displayed to the user.
michael@0 144 *
michael@0 145 * This is typically called on mobile devices where typing is tedious.
michael@0 146 */
michael@0 147 receiveNoPIN: function receiveNoPIN() {
michael@0 148 this._my_signerid = JPAKE_SIGNERID_RECEIVER;
michael@0 149 this._their_signerid = JPAKE_SIGNERID_SENDER;
michael@0 150
michael@0 151 this._secret = this._createSecret();
michael@0 152
michael@0 153 // Allow a large number of tries first while we wait for the PIN
michael@0 154 // to be entered on the other device.
michael@0 155 this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
michael@0 156 this._chain(this._getChannel,
michael@0 157 this._computeStepOne,
michael@0 158 this._putStep,
michael@0 159 this._getStep,
michael@0 160 function(callback) {
michael@0 161 // We fetched the first response from the other client.
michael@0 162 // Notify controller of the pairing starting.
michael@0 163 Utils.nextTick(this.controller.onPairingStart,
michael@0 164 this.controller);
michael@0 165
michael@0 166 // Now we can switch back to the smaller timeout.
michael@0 167 this._maxTries = Svc.Prefs.get("jpake.maxTries");
michael@0 168 callback();
michael@0 169 },
michael@0 170 this._computeStepTwo,
michael@0 171 this._putStep,
michael@0 172 this._getStep,
michael@0 173 this._computeFinal,
michael@0 174 this._computeKeyVerification,
michael@0 175 this._putStep,
michael@0 176 function(callback) {
michael@0 177 // Allow longer time-out for the last message.
michael@0 178 this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
michael@0 179 callback();
michael@0 180 },
michael@0 181 this._getStep,
michael@0 182 this._decryptData,
michael@0 183 this._complete)();
michael@0 184 },
michael@0 185
michael@0 186 /**
michael@0 187 * Initiate pairing based on the PIN entered by the user.
michael@0 188 *
michael@0 189 * This is typically called on desktop devices where typing is easier than
michael@0 190 * on mobile.
michael@0 191 *
michael@0 192 * @param pin
michael@0 193 * 12 character string (in human-friendly base32) containing the PIN
michael@0 194 * entered by the user.
michael@0 195 * @param expectDelay
michael@0 196 * Flag that indicates that a significant delay between the pairing
michael@0 197 * and the sending should be expected. v2 and earlier of the protocol
michael@0 198 * did not allow for this and the pairing to a v2 or earlier client
michael@0 199 * will be aborted if this flag is 'true'.
michael@0 200 */
michael@0 201 pairWithPIN: function pairWithPIN(pin, expectDelay) {
michael@0 202 this._my_signerid = JPAKE_SIGNERID_SENDER;
michael@0 203 this._their_signerid = JPAKE_SIGNERID_RECEIVER;
michael@0 204
michael@0 205 this._channel = pin.slice(JPAKE_LENGTH_SECRET);
michael@0 206 this._channelURL = this._serverURL + this._channel;
michael@0 207 this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
michael@0 208
michael@0 209 this._chain(this._computeStepOne,
michael@0 210 this._getStep,
michael@0 211 function (callback) {
michael@0 212 // Ensure that the other client can deal with a delay for
michael@0 213 // the last message if that's requested by the caller.
michael@0 214 if (!expectDelay) {
michael@0 215 return callback();
michael@0 216 }
michael@0 217 if (!this._incoming.version || this._incoming.version < 3) {
michael@0 218 return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED);
michael@0 219 }
michael@0 220 return callback();
michael@0 221 },
michael@0 222 this._putStep,
michael@0 223 this._computeStepTwo,
michael@0 224 this._getStep,
michael@0 225 this._putStep,
michael@0 226 this._computeFinal,
michael@0 227 this._getStep,
michael@0 228 this._verifyPairing)();
michael@0 229 },
michael@0 230
michael@0 231 /**
michael@0 232 * Send data after a successful pairing.
michael@0 233 *
michael@0 234 * @param obj
michael@0 235 * Object containing the data to send. It will be serialized as JSON.
michael@0 236 */
michael@0 237 sendAndComplete: function sendAndComplete(obj) {
michael@0 238 if (!this._paired || this._finished) {
michael@0 239 this._log.error("Can't send data, no active pairing!");
michael@0 240 throw "No active pairing!";
michael@0 241 }
michael@0 242 this._data = JSON.stringify(obj);
michael@0 243 this._chain(this._encryptData,
michael@0 244 this._putStep,
michael@0 245 this._complete)();
michael@0 246 },
michael@0 247
michael@0 248 /**
michael@0 249 * Abort the current pairing. The channel on the server will be deleted
michael@0 250 * if the abort wasn't due to a network or server error. The controller's
michael@0 251 * 'onAbort()' method is notified in all cases.
michael@0 252 *
michael@0 253 * @param error [optional]
michael@0 254 * Error constant indicating the reason for the abort. Defaults to
michael@0 255 * user abort.
michael@0 256 */
michael@0 257 abort: function abort(error) {
michael@0 258 this._log.debug("Aborting...");
michael@0 259 this._finished = true;
michael@0 260 let self = this;
michael@0 261
michael@0 262 // Default to "user aborted".
michael@0 263 if (!error) {
michael@0 264 error = JPAKE_ERROR_USERABORT;
michael@0 265 }
michael@0 266
michael@0 267 if (error == JPAKE_ERROR_CHANNEL ||
michael@0 268 error == JPAKE_ERROR_NETWORK ||
michael@0 269 error == JPAKE_ERROR_NODATA) {
michael@0 270 Utils.nextTick(function() { this.controller.onAbort(error); }, this);
michael@0 271 } else {
michael@0 272 this._reportFailure(error, function() { self.controller.onAbort(error); });
michael@0 273 }
michael@0 274 },
michael@0 275
michael@0 276 /*
michael@0 277 * Utilities
michael@0 278 */
michael@0 279
michael@0 280 _setClientID: function _setClientID() {
michael@0 281 let rng = Cc["@mozilla.org/security/random-generator;1"]
michael@0 282 .createInstance(Ci.nsIRandomGenerator);
michael@0 283 let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
michael@0 284 this._clientID = [("0" + byte.toString(16)).slice(-2)
michael@0 285 for each (byte in bytes)].join("");
michael@0 286 },
michael@0 287
michael@0 288 _createSecret: function _createSecret() {
michael@0 289 // 0-9a-z without 1,l,o,0
michael@0 290 const key = "23456789abcdefghijkmnpqrstuvwxyz";
michael@0 291 let rng = Cc["@mozilla.org/security/random-generator;1"]
michael@0 292 .createInstance(Ci.nsIRandomGenerator);
michael@0 293 let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
michael@0 294 return [key[Math.floor(byte * key.length / 256)]
michael@0 295 for each (byte in bytes)].join("");
michael@0 296 },
michael@0 297
michael@0 298 _newRequest: function _newRequest(uri) {
michael@0 299 let request = new RESTRequest(uri);
michael@0 300 request.setHeader("X-KeyExchange-Id", this._clientID);
michael@0 301 request.timeout = REQUEST_TIMEOUT;
michael@0 302 return request;
michael@0 303 },
michael@0 304
michael@0 305 /*
michael@0 306 * Steps of J-PAKE procedure
michael@0 307 */
michael@0 308
michael@0 309 _getChannel: function _getChannel(callback) {
michael@0 310 this._log.trace("Requesting channel.");
michael@0 311 let request = this._newRequest(this._serverURL + "new_channel");
michael@0 312 request.get(Utils.bind2(this, function handleChannel(error) {
michael@0 313 if (this._finished) {
michael@0 314 return;
michael@0 315 }
michael@0 316
michael@0 317 if (error) {
michael@0 318 this._log.error("Error acquiring channel ID. " + error);
michael@0 319 this.abort(JPAKE_ERROR_CHANNEL);
michael@0 320 return;
michael@0 321 }
michael@0 322 if (request.response.status != 200) {
michael@0 323 this._log.error("Error acquiring channel ID. Server responded with HTTP "
michael@0 324 + request.response.status);
michael@0 325 this.abort(JPAKE_ERROR_CHANNEL);
michael@0 326 return;
michael@0 327 }
michael@0 328
michael@0 329 try {
michael@0 330 this._channel = JSON.parse(request.response.body);
michael@0 331 } catch (ex) {
michael@0 332 this._log.error("Server responded with invalid JSON.");
michael@0 333 this.abort(JPAKE_ERROR_CHANNEL);
michael@0 334 return;
michael@0 335 }
michael@0 336 this._log.debug("Using channel " + this._channel);
michael@0 337 this._channelURL = this._serverURL + this._channel;
michael@0 338
michael@0 339 // Don't block on UI code.
michael@0 340 let pin = this._secret + this._channel;
michael@0 341 Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
michael@0 342 callback();
michael@0 343 }));
michael@0 344 },
michael@0 345
michael@0 346 // Generic handler for uploading data.
michael@0 347 _putStep: function _putStep(callback) {
michael@0 348 this._log.trace("Uploading message " + this._outgoing.type);
michael@0 349 let request = this._newRequest(this._channelURL);
michael@0 350 if (this._their_etag) {
michael@0 351 request.setHeader("If-Match", this._their_etag);
michael@0 352 } else {
michael@0 353 request.setHeader("If-None-Match", "*");
michael@0 354 }
michael@0 355 request.put(this._outgoing, Utils.bind2(this, function (error) {
michael@0 356 if (this._finished) {
michael@0 357 return;
michael@0 358 }
michael@0 359
michael@0 360 if (error) {
michael@0 361 this._log.error("Error uploading data. " + error);
michael@0 362 this.abort(JPAKE_ERROR_NETWORK);
michael@0 363 return;
michael@0 364 }
michael@0 365 if (request.response.status != 200) {
michael@0 366 this._log.error("Could not upload data. Server responded with HTTP "
michael@0 367 + request.response.status);
michael@0 368 this.abort(JPAKE_ERROR_SERVER);
michael@0 369 return;
michael@0 370 }
michael@0 371 // There's no point in returning early here since the next step will
michael@0 372 // always be a GET so let's pause for twice the poll interval.
michael@0 373 this._my_etag = request.response.headers["etag"];
michael@0 374 Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
michael@0 375 this, "_pollTimer");
michael@0 376 }));
michael@0 377 },
michael@0 378
michael@0 379 // Generic handler for polling for and retrieving data.
michael@0 380 _pollTries: 0,
michael@0 381 _getStep: function _getStep(callback) {
michael@0 382 this._log.trace("Retrieving next message.");
michael@0 383 let request = this._newRequest(this._channelURL);
michael@0 384 if (this._my_etag) {
michael@0 385 request.setHeader("If-None-Match", this._my_etag);
michael@0 386 }
michael@0 387
michael@0 388 request.get(Utils.bind2(this, function (error) {
michael@0 389 if (this._finished) {
michael@0 390 return;
michael@0 391 }
michael@0 392
michael@0 393 if (error) {
michael@0 394 this._log.error("Error fetching data. " + error);
michael@0 395 this.abort(JPAKE_ERROR_NETWORK);
michael@0 396 return;
michael@0 397 }
michael@0 398
michael@0 399 if (request.response.status == 304) {
michael@0 400 this._log.trace("Channel hasn't been updated yet. Will try again later.");
michael@0 401 if (this._pollTries >= this._maxTries) {
michael@0 402 this._log.error("Tried for " + this._pollTries + " times, aborting.");
michael@0 403 this.abort(JPAKE_ERROR_TIMEOUT);
michael@0 404 return;
michael@0 405 }
michael@0 406 this._pollTries += 1;
michael@0 407 Utils.namedTimer(function() { this._getStep(callback); },
michael@0 408 this._pollInterval, this, "_pollTimer");
michael@0 409 return;
michael@0 410 }
michael@0 411 this._pollTries = 0;
michael@0 412
michael@0 413 if (request.response.status == 404) {
michael@0 414 this._log.error("No data found in the channel.");
michael@0 415 this.abort(JPAKE_ERROR_NODATA);
michael@0 416 return;
michael@0 417 }
michael@0 418 if (request.response.status != 200) {
michael@0 419 this._log.error("Could not retrieve data. Server responded with HTTP "
michael@0 420 + request.response.status);
michael@0 421 this.abort(JPAKE_ERROR_SERVER);
michael@0 422 return;
michael@0 423 }
michael@0 424
michael@0 425 this._their_etag = request.response.headers["etag"];
michael@0 426 if (!this._their_etag) {
michael@0 427 this._log.error("Server did not supply ETag for message: "
michael@0 428 + request.response.body);
michael@0 429 this.abort(JPAKE_ERROR_SERVER);
michael@0 430 return;
michael@0 431 }
michael@0 432
michael@0 433 try {
michael@0 434 this._incoming = JSON.parse(request.response.body);
michael@0 435 } catch (ex) {
michael@0 436 this._log.error("Server responded with invalid JSON.");
michael@0 437 this.abort(JPAKE_ERROR_INVALID);
michael@0 438 return;
michael@0 439 }
michael@0 440 this._log.trace("Fetched message " + this._incoming.type);
michael@0 441 callback();
michael@0 442 }));
michael@0 443 },
michael@0 444
michael@0 445 _reportFailure: function _reportFailure(reason, callback) {
michael@0 446 this._log.debug("Reporting failure to server.");
michael@0 447 let request = this._newRequest(this._serverURL + "report");
michael@0 448 request.setHeader("X-KeyExchange-Cid", this._channel);
michael@0 449 request.setHeader("X-KeyExchange-Log", reason);
michael@0 450 request.post("", Utils.bind2(this, function (error) {
michael@0 451 if (error) {
michael@0 452 this._log.warn("Report failed: " + error);
michael@0 453 } else if (request.response.status != 200) {
michael@0 454 this._log.warn("Report failed. Server responded with HTTP "
michael@0 455 + request.response.status);
michael@0 456 }
michael@0 457
michael@0 458 // Do not block on errors, we're done or aborted by now anyway.
michael@0 459 callback();
michael@0 460 }));
michael@0 461 },
michael@0 462
michael@0 463 _computeStepOne: function _computeStepOne(callback) {
michael@0 464 this._log.trace("Computing round 1.");
michael@0 465 let gx1 = {};
michael@0 466 let gv1 = {};
michael@0 467 let r1 = {};
michael@0 468 let gx2 = {};
michael@0 469 let gv2 = {};
michael@0 470 let r2 = {};
michael@0 471 try {
michael@0 472 this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
michael@0 473 } catch (ex) {
michael@0 474 this._log.error("JPAKE round 1 threw: " + ex);
michael@0 475 this.abort(JPAKE_ERROR_INTERNAL);
michael@0 476 return;
michael@0 477 }
michael@0 478 let one = {gx1: gx1.value,
michael@0 479 gx2: gx2.value,
michael@0 480 zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
michael@0 481 zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
michael@0 482 this._outgoing = {type: this._my_signerid + "1",
michael@0 483 version: KEYEXCHANGE_VERSION,
michael@0 484 payload: one};
michael@0 485 this._log.trace("Generated message " + this._outgoing.type);
michael@0 486 callback();
michael@0 487 },
michael@0 488
michael@0 489 _computeStepTwo: function _computeStepTwo(callback) {
michael@0 490 this._log.trace("Computing round 2.");
michael@0 491 if (this._incoming.type != this._their_signerid + "1") {
michael@0 492 this._log.error("Invalid round 1 message: "
michael@0 493 + JSON.stringify(this._incoming));
michael@0 494 this.abort(JPAKE_ERROR_WRONGMESSAGE);
michael@0 495 return;
michael@0 496 }
michael@0 497
michael@0 498 let step1 = this._incoming.payload;
michael@0 499 if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
michael@0 500 || !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
michael@0 501 this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
michael@0 502 this.abort(JPAKE_ERROR_WRONGMESSAGE);
michael@0 503 return;
michael@0 504 }
michael@0 505
michael@0 506 let A = {};
michael@0 507 let gvA = {};
michael@0 508 let rA = {};
michael@0 509
michael@0 510 try {
michael@0 511 this._jpake.round2(this._their_signerid, this._secret,
michael@0 512 step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
michael@0 513 step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
michael@0 514 A, gvA, rA);
michael@0 515 } catch (ex) {
michael@0 516 this._log.error("JPAKE round 2 threw: " + ex);
michael@0 517 this.abort(JPAKE_ERROR_INTERNAL);
michael@0 518 return;
michael@0 519 }
michael@0 520 let two = {A: A.value,
michael@0 521 zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
michael@0 522 this._outgoing = {type: this._my_signerid + "2",
michael@0 523 version: KEYEXCHANGE_VERSION,
michael@0 524 payload: two};
michael@0 525 this._log.trace("Generated message " + this._outgoing.type);
michael@0 526 callback();
michael@0 527 },
michael@0 528
michael@0 529 _computeFinal: function _computeFinal(callback) {
michael@0 530 if (this._incoming.type != this._their_signerid + "2") {
michael@0 531 this._log.error("Invalid round 2 message: "
michael@0 532 + JSON.stringify(this._incoming));
michael@0 533 this.abort(JPAKE_ERROR_WRONGMESSAGE);
michael@0 534 return;
michael@0 535 }
michael@0 536
michael@0 537 let step2 = this._incoming.payload;
michael@0 538 if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
michael@0 539 this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
michael@0 540 this.abort(JPAKE_ERROR_WRONGMESSAGE);
michael@0 541 return;
michael@0 542 }
michael@0 543
michael@0 544 let aes256Key = {};
michael@0 545 let hmac256Key = {};
michael@0 546
michael@0 547 try {
michael@0 548 this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
michael@0 549 aes256Key, hmac256Key);
michael@0 550 } catch (ex) {
michael@0 551 this._log.error("JPAKE final round threw: " + ex);
michael@0 552 this.abort(JPAKE_ERROR_INTERNAL);
michael@0 553 return;
michael@0 554 }
michael@0 555
michael@0 556 this._crypto_key = aes256Key.value;
michael@0 557 let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
michael@0 558 this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key);
michael@0 559
michael@0 560 callback();
michael@0 561 },
michael@0 562
michael@0 563 _computeKeyVerification: function _computeKeyVerification(callback) {
michael@0 564 this._log.trace("Encrypting key verification value.");
michael@0 565 let iv, ciphertext;
michael@0 566 try {
michael@0 567 iv = Svc.Crypto.generateRandomIV();
michael@0 568 ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
michael@0 569 this._crypto_key, iv);
michael@0 570 } catch (ex) {
michael@0 571 this._log.error("Failed to encrypt key verification value.");
michael@0 572 this.abort(JPAKE_ERROR_INTERNAL);
michael@0 573 return;
michael@0 574 }
michael@0 575 this._outgoing = {type: this._my_signerid + "3",
michael@0 576 version: KEYEXCHANGE_VERSION,
michael@0 577 payload: {ciphertext: ciphertext, IV: iv}};
michael@0 578 this._log.trace("Generated message " + this._outgoing.type);
michael@0 579 callback();
michael@0 580 },
michael@0 581
michael@0 582 _verifyPairing: function _verifyPairing(callback) {
michael@0 583 this._log.trace("Verifying their key.");
michael@0 584 if (this._incoming.type != this._their_signerid + "3") {
michael@0 585 this._log.error("Invalid round 3 data: " +
michael@0 586 JSON.stringify(this._incoming));
michael@0 587 this.abort(JPAKE_ERROR_WRONGMESSAGE);
michael@0 588 return;
michael@0 589 }
michael@0 590 let step3 = this._incoming.payload;
michael@0 591 let ciphertext;
michael@0 592 try {
michael@0 593 ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
michael@0 594 this._crypto_key, step3.IV);
michael@0 595 if (ciphertext != step3.ciphertext) {
michael@0 596 throw "Key mismatch!";
michael@0 597 }
michael@0 598 } catch (ex) {
michael@0 599 this._log.error("Keys don't match!");
michael@0 600 this.abort(JPAKE_ERROR_KEYMISMATCH);
michael@0 601 return;
michael@0 602 }
michael@0 603
michael@0 604 this._log.debug("Verified pairing!");
michael@0 605 this._paired = true;
michael@0 606 Utils.nextTick(function () { this.controller.onPaired(); }, this);
michael@0 607 callback();
michael@0 608 },
michael@0 609
michael@0 610 _encryptData: function _encryptData(callback) {
michael@0 611 this._log.trace("Encrypting data.");
michael@0 612 let iv, ciphertext, hmac;
michael@0 613 try {
michael@0 614 iv = Svc.Crypto.generateRandomIV();
michael@0 615 ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
michael@0 616 hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher));
michael@0 617 } catch (ex) {
michael@0 618 this._log.error("Failed to encrypt data.");
michael@0 619 this.abort(JPAKE_ERROR_INTERNAL);
michael@0 620 return;
michael@0 621 }
michael@0 622 this._outgoing = {type: this._my_signerid + "3",
michael@0 623 version: KEYEXCHANGE_VERSION,
michael@0 624 payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
michael@0 625 this._log.trace("Generated message " + this._outgoing.type);
michael@0 626 callback();
michael@0 627 },
michael@0 628
michael@0 629 _decryptData: function _decryptData(callback) {
michael@0 630 this._log.trace("Verifying their key.");
michael@0 631 if (this._incoming.type != this._their_signerid + "3") {
michael@0 632 this._log.error("Invalid round 3 data: "
michael@0 633 + JSON.stringify(this._incoming));
michael@0 634 this.abort(JPAKE_ERROR_WRONGMESSAGE);
michael@0 635 return;
michael@0 636 }
michael@0 637 let step3 = this._incoming.payload;
michael@0 638 try {
michael@0 639 let hmac = Utils.bytesAsHex(
michael@0 640 Utils.digestUTF8(step3.ciphertext, this._hmac_hasher));
michael@0 641 if (hmac != step3.hmac) {
michael@0 642 throw "HMAC validation failed!";
michael@0 643 }
michael@0 644 } catch (ex) {
michael@0 645 this._log.error("HMAC validation failed.");
michael@0 646 this.abort(JPAKE_ERROR_KEYMISMATCH);
michael@0 647 return;
michael@0 648 }
michael@0 649
michael@0 650 this._log.trace("Decrypting data.");
michael@0 651 let cleartext;
michael@0 652 try {
michael@0 653 cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
michael@0 654 step3.IV);
michael@0 655 } catch (ex) {
michael@0 656 this._log.error("Failed to decrypt data.");
michael@0 657 this.abort(JPAKE_ERROR_INTERNAL);
michael@0 658 return;
michael@0 659 }
michael@0 660
michael@0 661 try {
michael@0 662 this._newData = JSON.parse(cleartext);
michael@0 663 } catch (ex) {
michael@0 664 this._log.error("Invalid data data: " + JSON.stringify(cleartext));
michael@0 665 this.abort(JPAKE_ERROR_INVALID);
michael@0 666 return;
michael@0 667 }
michael@0 668
michael@0 669 this._log.trace("Decrypted data.");
michael@0 670 callback();
michael@0 671 },
michael@0 672
michael@0 673 _complete: function _complete() {
michael@0 674 this._log.debug("Exchange completed.");
michael@0 675 this._finished = true;
michael@0 676 Utils.nextTick(function () { this.controller.onComplete(this._newData); },
michael@0 677 this);
michael@0 678 }
michael@0 679
michael@0 680 };
michael@0 681
michael@0 682
michael@0 683 /**
michael@0 684 * Send credentials over an active J-PAKE channel.
michael@0 685 *
michael@0 686 * This object is designed to take over as the JPAKEClient controller,
michael@0 687 * presumably replacing one that is UI-based which would either cause
michael@0 688 * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
michael@0 689 * context disappears. This object stays alive for the duration of the
michael@0 690 * transfer by being strong-ref'ed as an nsIObserver.
michael@0 691 *
michael@0 692 * Credentials are sent after the first sync has been completed
michael@0 693 * (successfully or not.)
michael@0 694 *
michael@0 695 * Usage:
michael@0 696 *
michael@0 697 * jpakeclient.controller = new SendCredentialsController(jpakeclient,
michael@0 698 * service);
michael@0 699 *
michael@0 700 */
michael@0 701 this.SendCredentialsController =
michael@0 702 function SendCredentialsController(jpakeclient, service) {
michael@0 703 this._log = Log.repository.getLogger("Sync.SendCredentialsController");
michael@0 704 this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
michael@0 705
michael@0 706 this._log.trace("Loading.");
michael@0 707 this.jpakeclient = jpakeclient;
michael@0 708 this.service = service;
michael@0 709
michael@0 710 // Register ourselves as observers the first Sync finishing (either
michael@0 711 // successfully or unsuccessfully, we don't care) or for removing
michael@0 712 // this device's sync configuration, in case that happens while we
michael@0 713 // haven't finished the first sync yet.
michael@0 714 Services.obs.addObserver(this, "weave:service:sync:finish", false);
michael@0 715 Services.obs.addObserver(this, "weave:service:sync:error", false);
michael@0 716 Services.obs.addObserver(this, "weave:service:start-over", false);
michael@0 717 }
michael@0 718 SendCredentialsController.prototype = {
michael@0 719
michael@0 720 unload: function unload() {
michael@0 721 this._log.trace("Unloading.");
michael@0 722 try {
michael@0 723 Services.obs.removeObserver(this, "weave:service:sync:finish");
michael@0 724 Services.obs.removeObserver(this, "weave:service:sync:error");
michael@0 725 Services.obs.removeObserver(this, "weave:service:start-over");
michael@0 726 } catch (ex) {
michael@0 727 // Ignore.
michael@0 728 }
michael@0 729 },
michael@0 730
michael@0 731 observe: function observe(subject, topic, data) {
michael@0 732 switch (topic) {
michael@0 733 case "weave:service:sync:finish":
michael@0 734 case "weave:service:sync:error":
michael@0 735 Utils.nextTick(this.sendCredentials, this);
michael@0 736 break;
michael@0 737 case "weave:service:start-over":
michael@0 738 // This will call onAbort which will call unload().
michael@0 739 this.jpakeclient.abort();
michael@0 740 break;
michael@0 741 }
michael@0 742 },
michael@0 743
michael@0 744 sendCredentials: function sendCredentials() {
michael@0 745 this._log.trace("Sending credentials.");
michael@0 746 let credentials = {account: this.service.identity.account,
michael@0 747 password: this.service.identity.basicPassword,
michael@0 748 synckey: this.service.identity.syncKey,
michael@0 749 serverURL: this.service.serverURL};
michael@0 750 this.jpakeclient.sendAndComplete(credentials);
michael@0 751 },
michael@0 752
michael@0 753 // JPAKEClient controller API
michael@0 754
michael@0 755 onComplete: function onComplete() {
michael@0 756 this._log.debug("Exchange was completed successfully!");
michael@0 757 this.unload();
michael@0 758
michael@0 759 // Schedule a Sync for soonish to fetch the data uploaded by the
michael@0 760 // device with which we just paired.
michael@0 761 this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval);
michael@0 762 },
michael@0 763
michael@0 764 onAbort: function onAbort(error) {
michael@0 765 // It doesn't really matter why we aborted, but the channel is closed
michael@0 766 // for sure, so we won't be able to do anything with it.
michael@0 767 this._log.debug("Exchange was aborted with error: " + error);
michael@0 768 this.unload();
michael@0 769 },
michael@0 770
michael@0 771 // Irrelevant methods for this controller:
michael@0 772 displayPIN: function displayPIN() {},
michael@0 773 onPairingStart: function onPairingStart() {},
michael@0 774 onPaired: function onPaired() {},
michael@0 775 };

mercurial