services/sync/modules/jpakeclient.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/services/sync/modules/jpakeclient.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,775 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
     1.9 +
    1.10 +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
    1.11 +
    1.12 +Cu.import("resource://gre/modules/Log.jsm");
    1.13 +Cu.import("resource://services-common/rest.js");
    1.14 +Cu.import("resource://services-sync/constants.js");
    1.15 +Cu.import("resource://services-sync/util.js");
    1.16 +
    1.17 +const REQUEST_TIMEOUT         = 60; // 1 minute
    1.18 +const KEYEXCHANGE_VERSION     = 3;
    1.19 +
    1.20 +const JPAKE_SIGNERID_SENDER   = "sender";
    1.21 +const JPAKE_SIGNERID_RECEIVER = "receiver";
    1.22 +const JPAKE_LENGTH_SECRET     = 8;
    1.23 +const JPAKE_LENGTH_CLIENTID   = 256;
    1.24 +const JPAKE_VERIFY_VALUE      = "0123456789ABCDEF";
    1.25 +
    1.26 +
    1.27 +/**
    1.28 + * Client to exchange encrypted data using the J-PAKE algorithm.
    1.29 + * The exchange between two clients of this type looks like this:
    1.30 + * 
    1.31 + * 
    1.32 + *  Mobile                        Server                        Desktop
    1.33 + *  ===================================================================
    1.34 + *                                   |
    1.35 + *  retrieve channel <---------------|
    1.36 + *  generate random secret           |
    1.37 + *  show PIN = secret + channel      |                 ask user for PIN
    1.38 + *  upload Mobile's message 1 ------>|
    1.39 + *                                   |----> retrieve Mobile's message 1
    1.40 + *                                   |<----- upload Desktop's message 1
    1.41 + *  retrieve Desktop's message 1 <---|
    1.42 + *  upload Mobile's message 2 ------>|
    1.43 + *                                   |----> retrieve Mobile's message 2
    1.44 + *                                   |                      compute key
    1.45 + *                                   |<----- upload Desktop's message 2
    1.46 + *  retrieve Desktop's message 2 <---|
    1.47 + *  compute key                      |
    1.48 + *  encrypt known value ------------>|
    1.49 + *                                   |-------> retrieve encrypted value
    1.50 + *                                   | verify against local known value
    1.51 + *
    1.52 + *   At this point Desktop knows whether the PIN was entered correctly.
    1.53 + *   If it wasn't, Desktop deletes the session. If it was, the account
    1.54 + *   setup can proceed. If Desktop doesn't yet have an account set up,
    1.55 + *   it will keep the channel open and let the user connect to or
    1.56 + *   create an account.
    1.57 + *
    1.58 + *                                   |              encrypt credentials
    1.59 + *                                   |<------------- upload credentials
    1.60 + *  retrieve credentials <-----------|
    1.61 + *  verify HMAC                      |
    1.62 + *  decrypt credentials              |
    1.63 + *  delete session ----------------->|
    1.64 + *  start syncing                    |
    1.65 + * 
    1.66 + * 
    1.67 + * Create a client object like so:
    1.68 + * 
    1.69 + *   let client = new JPAKEClient(controller);
    1.70 + * 
    1.71 + * The 'controller' object must implement the following methods:
    1.72 + * 
    1.73 + *   displayPIN(pin) -- Called when a PIN has been generated and is ready to
    1.74 + *     be displayed to the user. Only called on the client where the pairing
    1.75 + *     was initiated with 'receiveNoPIN()'.
    1.76 + * 
    1.77 + *   onPairingStart() -- Called when the pairing has started and messages are
    1.78 + *     being sent back and forth over the channel. Only called on the client
    1.79 + *     where the pairing was initiated with 'receiveNoPIN()'.
    1.80 + * 
    1.81 + *   onPaired() -- Called when the device pairing has been established and
    1.82 + *     we're ready to send the credentials over. To do that, the controller
    1.83 + *     must call 'sendAndComplete()' while the channel is active.
    1.84 + * 
    1.85 + *   onComplete(data) -- Called after transfer has been completed. On
    1.86 + *     the sending side this is called with no parameter and as soon as the
    1.87 + *     data has been uploaded. This does not mean the receiving side has
    1.88 + *     actually retrieved them yet.
    1.89 + *
    1.90 + *   onAbort(error) -- Called whenever an error is encountered. All errors lead
    1.91 + *     to an abort and the process has to be started again on both sides.
    1.92 + * 
    1.93 + * To start the data transfer on the receiving side, call
    1.94 + * 
    1.95 + *   client.receiveNoPIN();
    1.96 + * 
    1.97 + * This will allocate a new channel on the server, generate a PIN, have it
    1.98 + * displayed and then do the transfer once the protocol has been completed
    1.99 + * with the sending side.
   1.100 + * 
   1.101 + * To initiate the transfer from the sending side, call
   1.102 + * 
   1.103 + *   client.pairWithPIN(pin, true);
   1.104 + * 
   1.105 + * Once the pairing has been established, the controller's 'onPaired()' method
   1.106 + * will be called. To then transmit the data, call
   1.107 + * 
   1.108 + *   client.sendAndComplete(data);
   1.109 + * 
   1.110 + * To abort the process, call
   1.111 + * 
   1.112 + *   client.abort();
   1.113 + * 
   1.114 + * Note that after completion or abort, the 'client' instance may not be reused.
   1.115 + * You will have to create a new one in case you'd like to restart the process.
   1.116 + */
   1.117 +this.JPAKEClient = function JPAKEClient(controller) {
   1.118 +  this.controller = controller;
   1.119 +
   1.120 +  this._log = Log.repository.getLogger("Sync.JPAKEClient");
   1.121 +  this._log.level = Log.Level[Svc.Prefs.get(
   1.122 +    "log.logger.service.jpakeclient", "Debug")];
   1.123 +
   1.124 +  this._serverURL = Svc.Prefs.get("jpake.serverURL");
   1.125 +  this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
   1.126 +  this._maxTries = Svc.Prefs.get("jpake.maxTries");
   1.127 +  if (this._serverURL.slice(-1) != "/") {
   1.128 +    this._serverURL += "/";
   1.129 +  }
   1.130 +
   1.131 +  this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
   1.132 +                  .createInstance(Ci.nsISyncJPAKE);
   1.133 +
   1.134 +  this._setClientID();
   1.135 +}
   1.136 +JPAKEClient.prototype = {
   1.137 +
   1.138 +  _chain: Async.chain,
   1.139 +
   1.140 +  /*
   1.141 +   * Public API
   1.142 +   */
   1.143 +
   1.144 +  /**
   1.145 +   * Initiate pairing and receive data without providing a PIN. The PIN will
   1.146 +   * be generated and passed on to the controller to be displayed to the user.
   1.147 +   * 
   1.148 +   * This is typically called on mobile devices where typing is tedious.
   1.149 +   */
   1.150 +  receiveNoPIN: function receiveNoPIN() {
   1.151 +    this._my_signerid = JPAKE_SIGNERID_RECEIVER;
   1.152 +    this._their_signerid = JPAKE_SIGNERID_SENDER;
   1.153 +
   1.154 +    this._secret = this._createSecret();
   1.155 +
   1.156 +    // Allow a large number of tries first while we wait for the PIN
   1.157 +    // to be entered on the other device.
   1.158 +    this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
   1.159 +    this._chain(this._getChannel,
   1.160 +                this._computeStepOne,
   1.161 +                this._putStep,
   1.162 +                this._getStep,
   1.163 +                function(callback) {
   1.164 +                  // We fetched the first response from the other client.
   1.165 +                  // Notify controller of the pairing starting.
   1.166 +                  Utils.nextTick(this.controller.onPairingStart,
   1.167 +                                 this.controller);
   1.168 +
   1.169 +                  // Now we can switch back to the smaller timeout.
   1.170 +                  this._maxTries = Svc.Prefs.get("jpake.maxTries");
   1.171 +                  callback();
   1.172 +                },
   1.173 +                this._computeStepTwo,
   1.174 +                this._putStep,
   1.175 +                this._getStep,
   1.176 +                this._computeFinal,
   1.177 +                this._computeKeyVerification,
   1.178 +                this._putStep,
   1.179 +                function(callback) {
   1.180 +                  // Allow longer time-out for the last message.
   1.181 +                  this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
   1.182 +                  callback();
   1.183 +                },
   1.184 +                this._getStep,
   1.185 +                this._decryptData,
   1.186 +                this._complete)();
   1.187 +  },
   1.188 +
   1.189 +  /**
   1.190 +   * Initiate pairing based on the PIN entered by the user.
   1.191 +   * 
   1.192 +   * This is typically called on desktop devices where typing is easier than
   1.193 +   * on mobile.
   1.194 +   * 
   1.195 +   * @param pin
   1.196 +   *        12 character string (in human-friendly base32) containing the PIN
   1.197 +   *        entered by the user.
   1.198 +   * @param expectDelay
   1.199 +   *        Flag that indicates that a significant delay between the pairing
   1.200 +   *        and the sending should be expected. v2 and earlier of the protocol
   1.201 +   *        did not allow for this and the pairing to a v2 or earlier client
   1.202 +   *        will be aborted if this flag is 'true'.
   1.203 +   */
   1.204 +  pairWithPIN: function pairWithPIN(pin, expectDelay) {
   1.205 +    this._my_signerid = JPAKE_SIGNERID_SENDER;
   1.206 +    this._their_signerid = JPAKE_SIGNERID_RECEIVER;
   1.207 +
   1.208 +    this._channel = pin.slice(JPAKE_LENGTH_SECRET);
   1.209 +    this._channelURL = this._serverURL + this._channel;
   1.210 +    this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
   1.211 +
   1.212 +    this._chain(this._computeStepOne,
   1.213 +                this._getStep,
   1.214 +                function (callback) {
   1.215 +                  // Ensure that the other client can deal with a delay for
   1.216 +                  // the last message if that's requested by the caller.
   1.217 +                  if (!expectDelay) {
   1.218 +                    return callback();
   1.219 +                  }
   1.220 +                  if (!this._incoming.version || this._incoming.version < 3) {
   1.221 +                    return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED);
   1.222 +                  }
   1.223 +                  return callback();
   1.224 +                },
   1.225 +                this._putStep,
   1.226 +                this._computeStepTwo,
   1.227 +                this._getStep,
   1.228 +                this._putStep,
   1.229 +                this._computeFinal,
   1.230 +                this._getStep,
   1.231 +                this._verifyPairing)();
   1.232 +  },
   1.233 +
   1.234 +  /**
   1.235 +   * Send data after a successful pairing.
   1.236 +   * 
   1.237 +   * @param obj
   1.238 +   *        Object containing the data to send. It will be serialized as JSON.
   1.239 +   */
   1.240 +  sendAndComplete: function sendAndComplete(obj) {
   1.241 +    if (!this._paired || this._finished) {
   1.242 +      this._log.error("Can't send data, no active pairing!");
   1.243 +      throw "No active pairing!";
   1.244 +    }
   1.245 +    this._data = JSON.stringify(obj);
   1.246 +    this._chain(this._encryptData,
   1.247 +                this._putStep,
   1.248 +                this._complete)();
   1.249 +  },
   1.250 +
   1.251 +  /**
   1.252 +   * Abort the current pairing. The channel on the server will be deleted
   1.253 +   * if the abort wasn't due to a network or server error. The controller's
   1.254 +   * 'onAbort()' method is notified in all cases.
   1.255 +   * 
   1.256 +   * @param error [optional]
   1.257 +   *        Error constant indicating the reason for the abort. Defaults to
   1.258 +   *        user abort.
   1.259 +   */
   1.260 +  abort: function abort(error) {
   1.261 +    this._log.debug("Aborting...");
   1.262 +    this._finished = true;
   1.263 +    let self = this;
   1.264 +
   1.265 +    // Default to "user aborted".
   1.266 +    if (!error) {
   1.267 +      error = JPAKE_ERROR_USERABORT;
   1.268 +    }
   1.269 +
   1.270 +    if (error == JPAKE_ERROR_CHANNEL ||
   1.271 +        error == JPAKE_ERROR_NETWORK ||
   1.272 +        error == JPAKE_ERROR_NODATA) {
   1.273 +      Utils.nextTick(function() { this.controller.onAbort(error); }, this);
   1.274 +    } else {
   1.275 +      this._reportFailure(error, function() { self.controller.onAbort(error); });
   1.276 +    }
   1.277 +  },
   1.278 +
   1.279 +  /*
   1.280 +   * Utilities
   1.281 +   */
   1.282 +
   1.283 +  _setClientID: function _setClientID() {
   1.284 +    let rng = Cc["@mozilla.org/security/random-generator;1"]
   1.285 +                .createInstance(Ci.nsIRandomGenerator);
   1.286 +    let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
   1.287 +    this._clientID = [("0" + byte.toString(16)).slice(-2)
   1.288 +                      for each (byte in bytes)].join("");
   1.289 +  },
   1.290 +
   1.291 +  _createSecret: function _createSecret() {
   1.292 +    // 0-9a-z without 1,l,o,0
   1.293 +    const key = "23456789abcdefghijkmnpqrstuvwxyz";
   1.294 +    let rng = Cc["@mozilla.org/security/random-generator;1"]
   1.295 +                .createInstance(Ci.nsIRandomGenerator);
   1.296 +    let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
   1.297 +    return [key[Math.floor(byte * key.length / 256)]
   1.298 +            for each (byte in bytes)].join("");
   1.299 +  },
   1.300 +
   1.301 +  _newRequest: function _newRequest(uri) {
   1.302 +    let request = new RESTRequest(uri);
   1.303 +    request.setHeader("X-KeyExchange-Id", this._clientID);
   1.304 +    request.timeout = REQUEST_TIMEOUT;
   1.305 +    return request;
   1.306 +  },
   1.307 +
   1.308 +  /*
   1.309 +   * Steps of J-PAKE procedure
   1.310 +   */
   1.311 +
   1.312 +  _getChannel: function _getChannel(callback) {
   1.313 +    this._log.trace("Requesting channel.");
   1.314 +    let request = this._newRequest(this._serverURL + "new_channel");
   1.315 +    request.get(Utils.bind2(this, function handleChannel(error) {
   1.316 +      if (this._finished) {
   1.317 +        return;
   1.318 +      }
   1.319 +
   1.320 +      if (error) {
   1.321 +        this._log.error("Error acquiring channel ID. " + error);
   1.322 +        this.abort(JPAKE_ERROR_CHANNEL);
   1.323 +        return;
   1.324 +      }
   1.325 +      if (request.response.status != 200) {
   1.326 +        this._log.error("Error acquiring channel ID. Server responded with HTTP "
   1.327 +                        + request.response.status);
   1.328 +        this.abort(JPAKE_ERROR_CHANNEL);
   1.329 +        return;
   1.330 +      }
   1.331 +
   1.332 +      try {
   1.333 +        this._channel = JSON.parse(request.response.body);
   1.334 +      } catch (ex) {
   1.335 +        this._log.error("Server responded with invalid JSON.");
   1.336 +        this.abort(JPAKE_ERROR_CHANNEL);
   1.337 +        return;
   1.338 +      }
   1.339 +      this._log.debug("Using channel " + this._channel);
   1.340 +      this._channelURL = this._serverURL + this._channel;
   1.341 +
   1.342 +      // Don't block on UI code.
   1.343 +      let pin = this._secret + this._channel;
   1.344 +      Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
   1.345 +      callback();
   1.346 +    }));
   1.347 +  },
   1.348 +
   1.349 +  // Generic handler for uploading data.
   1.350 +  _putStep: function _putStep(callback) {
   1.351 +    this._log.trace("Uploading message " + this._outgoing.type);
   1.352 +    let request = this._newRequest(this._channelURL);
   1.353 +    if (this._their_etag) {
   1.354 +      request.setHeader("If-Match", this._their_etag);
   1.355 +    } else {
   1.356 +      request.setHeader("If-None-Match", "*");
   1.357 +    }
   1.358 +    request.put(this._outgoing, Utils.bind2(this, function (error) {
   1.359 +      if (this._finished) {
   1.360 +        return;
   1.361 +      }
   1.362 +
   1.363 +      if (error) {
   1.364 +        this._log.error("Error uploading data. " + error);
   1.365 +        this.abort(JPAKE_ERROR_NETWORK);
   1.366 +        return;
   1.367 +      }
   1.368 +      if (request.response.status != 200) {
   1.369 +        this._log.error("Could not upload data. Server responded with HTTP "
   1.370 +                        + request.response.status);
   1.371 +        this.abort(JPAKE_ERROR_SERVER);
   1.372 +        return;
   1.373 +      }
   1.374 +      // There's no point in returning early here since the next step will
   1.375 +      // always be a GET so let's pause for twice the poll interval.
   1.376 +      this._my_etag = request.response.headers["etag"];
   1.377 +      Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
   1.378 +                       this, "_pollTimer");
   1.379 +    }));
   1.380 +  },
   1.381 +
   1.382 +  // Generic handler for polling for and retrieving data.
   1.383 +  _pollTries: 0,
   1.384 +  _getStep: function _getStep(callback) {
   1.385 +    this._log.trace("Retrieving next message.");
   1.386 +    let request = this._newRequest(this._channelURL);
   1.387 +    if (this._my_etag) {
   1.388 +      request.setHeader("If-None-Match", this._my_etag);
   1.389 +    }
   1.390 +
   1.391 +    request.get(Utils.bind2(this, function (error) {
   1.392 +      if (this._finished) {
   1.393 +        return;
   1.394 +      }
   1.395 +
   1.396 +      if (error) {
   1.397 +        this._log.error("Error fetching data. " + error);
   1.398 +        this.abort(JPAKE_ERROR_NETWORK);
   1.399 +        return;
   1.400 +      }
   1.401 +
   1.402 +      if (request.response.status == 304) {
   1.403 +        this._log.trace("Channel hasn't been updated yet. Will try again later.");
   1.404 +        if (this._pollTries >= this._maxTries) {
   1.405 +          this._log.error("Tried for " + this._pollTries + " times, aborting.");
   1.406 +          this.abort(JPAKE_ERROR_TIMEOUT);
   1.407 +          return;
   1.408 +        }
   1.409 +        this._pollTries += 1;
   1.410 +        Utils.namedTimer(function() { this._getStep(callback); },
   1.411 +                         this._pollInterval, this, "_pollTimer");
   1.412 +        return;
   1.413 +      }
   1.414 +      this._pollTries = 0;
   1.415 +
   1.416 +      if (request.response.status == 404) {
   1.417 +        this._log.error("No data found in the channel.");
   1.418 +        this.abort(JPAKE_ERROR_NODATA);
   1.419 +        return;
   1.420 +      }
   1.421 +      if (request.response.status != 200) {
   1.422 +        this._log.error("Could not retrieve data. Server responded with HTTP "
   1.423 +                        + request.response.status);
   1.424 +        this.abort(JPAKE_ERROR_SERVER);
   1.425 +        return;
   1.426 +      }
   1.427 +
   1.428 +      this._their_etag = request.response.headers["etag"];
   1.429 +      if (!this._their_etag) {
   1.430 +        this._log.error("Server did not supply ETag for message: "
   1.431 +                        + request.response.body);
   1.432 +        this.abort(JPAKE_ERROR_SERVER);
   1.433 +        return;
   1.434 +      }
   1.435 +
   1.436 +      try {
   1.437 +        this._incoming = JSON.parse(request.response.body);
   1.438 +      } catch (ex) {
   1.439 +        this._log.error("Server responded with invalid JSON.");
   1.440 +        this.abort(JPAKE_ERROR_INVALID);
   1.441 +        return;
   1.442 +      }
   1.443 +      this._log.trace("Fetched message " + this._incoming.type);
   1.444 +      callback();
   1.445 +    }));
   1.446 +  },
   1.447 +
   1.448 +  _reportFailure: function _reportFailure(reason, callback) {
   1.449 +    this._log.debug("Reporting failure to server.");
   1.450 +    let request = this._newRequest(this._serverURL + "report");
   1.451 +    request.setHeader("X-KeyExchange-Cid", this._channel);
   1.452 +    request.setHeader("X-KeyExchange-Log", reason);
   1.453 +    request.post("", Utils.bind2(this, function (error) {
   1.454 +      if (error) {
   1.455 +        this._log.warn("Report failed: " + error);
   1.456 +      } else if (request.response.status != 200) {
   1.457 +        this._log.warn("Report failed. Server responded with HTTP "
   1.458 +                       + request.response.status);
   1.459 +      }
   1.460 +
   1.461 +      // Do not block on errors, we're done or aborted by now anyway.
   1.462 +      callback();
   1.463 +    }));
   1.464 +  },
   1.465 +
   1.466 +  _computeStepOne: function _computeStepOne(callback) {
   1.467 +    this._log.trace("Computing round 1.");
   1.468 +    let gx1 = {};
   1.469 +    let gv1 = {};
   1.470 +    let r1 = {};
   1.471 +    let gx2 = {};
   1.472 +    let gv2 = {};
   1.473 +    let r2 = {};
   1.474 +    try {
   1.475 +      this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
   1.476 +    } catch (ex) {
   1.477 +      this._log.error("JPAKE round 1 threw: " + ex);
   1.478 +      this.abort(JPAKE_ERROR_INTERNAL);
   1.479 +      return;
   1.480 +    }
   1.481 +    let one = {gx1: gx1.value,
   1.482 +               gx2: gx2.value,
   1.483 +               zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
   1.484 +               zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
   1.485 +    this._outgoing = {type: this._my_signerid + "1",
   1.486 +                      version: KEYEXCHANGE_VERSION,
   1.487 +                      payload: one};
   1.488 +    this._log.trace("Generated message " + this._outgoing.type);
   1.489 +    callback();
   1.490 +  },
   1.491 +
   1.492 +  _computeStepTwo: function _computeStepTwo(callback) {
   1.493 +    this._log.trace("Computing round 2.");
   1.494 +    if (this._incoming.type != this._their_signerid + "1") {
   1.495 +      this._log.error("Invalid round 1 message: "
   1.496 +                      + JSON.stringify(this._incoming));
   1.497 +      this.abort(JPAKE_ERROR_WRONGMESSAGE);
   1.498 +      return;
   1.499 +    }
   1.500 +
   1.501 +    let step1 = this._incoming.payload;
   1.502 +    if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
   1.503 +        || !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
   1.504 +      this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
   1.505 +      this.abort(JPAKE_ERROR_WRONGMESSAGE);
   1.506 +      return;
   1.507 +    }
   1.508 +
   1.509 +    let A = {};
   1.510 +    let gvA = {};
   1.511 +    let rA = {};
   1.512 +
   1.513 +    try {
   1.514 +      this._jpake.round2(this._their_signerid, this._secret,
   1.515 +                         step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
   1.516 +                         step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
   1.517 +                         A, gvA, rA);
   1.518 +    } catch (ex) {
   1.519 +      this._log.error("JPAKE round 2 threw: " + ex);
   1.520 +      this.abort(JPAKE_ERROR_INTERNAL);
   1.521 +      return;
   1.522 +    }
   1.523 +    let two = {A: A.value,
   1.524 +               zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
   1.525 +    this._outgoing = {type: this._my_signerid + "2",
   1.526 +                      version: KEYEXCHANGE_VERSION,
   1.527 +                      payload: two};
   1.528 +    this._log.trace("Generated message " + this._outgoing.type);
   1.529 +    callback();
   1.530 +  },
   1.531 +
   1.532 +  _computeFinal: function _computeFinal(callback) {
   1.533 +    if (this._incoming.type != this._their_signerid + "2") {
   1.534 +      this._log.error("Invalid round 2 message: "
   1.535 +                      + JSON.stringify(this._incoming));
   1.536 +      this.abort(JPAKE_ERROR_WRONGMESSAGE);
   1.537 +      return;
   1.538 +    }
   1.539 +
   1.540 +    let step2 = this._incoming.payload;
   1.541 +    if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
   1.542 +      this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
   1.543 +      this.abort(JPAKE_ERROR_WRONGMESSAGE);
   1.544 +      return;
   1.545 +    }
   1.546 +
   1.547 +    let aes256Key = {};
   1.548 +    let hmac256Key = {};
   1.549 +
   1.550 +    try {
   1.551 +      this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
   1.552 +                        aes256Key, hmac256Key);
   1.553 +    } catch (ex) {
   1.554 +      this._log.error("JPAKE final round threw: " + ex);
   1.555 +      this.abort(JPAKE_ERROR_INTERNAL);
   1.556 +      return;
   1.557 +    }
   1.558 +
   1.559 +    this._crypto_key = aes256Key.value;
   1.560 +    let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
   1.561 +    this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key);
   1.562 +
   1.563 +    callback();
   1.564 +  },
   1.565 +
   1.566 +  _computeKeyVerification: function _computeKeyVerification(callback) {
   1.567 +    this._log.trace("Encrypting key verification value.");
   1.568 +    let iv, ciphertext;
   1.569 +    try {
   1.570 +      iv = Svc.Crypto.generateRandomIV();
   1.571 +      ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
   1.572 +                                      this._crypto_key, iv);
   1.573 +    } catch (ex) {
   1.574 +      this._log.error("Failed to encrypt key verification value.");
   1.575 +      this.abort(JPAKE_ERROR_INTERNAL);
   1.576 +      return;
   1.577 +    }
   1.578 +    this._outgoing = {type: this._my_signerid + "3",
   1.579 +                      version: KEYEXCHANGE_VERSION,
   1.580 +                      payload: {ciphertext: ciphertext, IV: iv}};
   1.581 +    this._log.trace("Generated message " + this._outgoing.type);
   1.582 +    callback();
   1.583 +  },
   1.584 +
   1.585 +  _verifyPairing: function _verifyPairing(callback) {
   1.586 +    this._log.trace("Verifying their key.");
   1.587 +    if (this._incoming.type != this._their_signerid + "3") {
   1.588 +      this._log.error("Invalid round 3 data: " +
   1.589 +                      JSON.stringify(this._incoming));
   1.590 +      this.abort(JPAKE_ERROR_WRONGMESSAGE);
   1.591 +      return;
   1.592 +    }
   1.593 +    let step3 = this._incoming.payload;
   1.594 +    let ciphertext;
   1.595 +    try {
   1.596 +      ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
   1.597 +                                      this._crypto_key, step3.IV);
   1.598 +      if (ciphertext != step3.ciphertext) {
   1.599 +        throw "Key mismatch!";
   1.600 +      }
   1.601 +    } catch (ex) {
   1.602 +      this._log.error("Keys don't match!");
   1.603 +      this.abort(JPAKE_ERROR_KEYMISMATCH);
   1.604 +      return;
   1.605 +    }
   1.606 +
   1.607 +    this._log.debug("Verified pairing!");
   1.608 +    this._paired = true;
   1.609 +    Utils.nextTick(function () { this.controller.onPaired(); }, this);
   1.610 +    callback();
   1.611 +  },
   1.612 +
   1.613 +  _encryptData: function _encryptData(callback) {
   1.614 +    this._log.trace("Encrypting data.");
   1.615 +    let iv, ciphertext, hmac;
   1.616 +    try {
   1.617 +      iv = Svc.Crypto.generateRandomIV();
   1.618 +      ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
   1.619 +      hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher));
   1.620 +    } catch (ex) {
   1.621 +      this._log.error("Failed to encrypt data.");
   1.622 +      this.abort(JPAKE_ERROR_INTERNAL);
   1.623 +      return;
   1.624 +    }
   1.625 +    this._outgoing = {type: this._my_signerid + "3",
   1.626 +                      version: KEYEXCHANGE_VERSION,
   1.627 +                      payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
   1.628 +    this._log.trace("Generated message " + this._outgoing.type);
   1.629 +    callback();
   1.630 +  },
   1.631 +
   1.632 +  _decryptData: function _decryptData(callback) {
   1.633 +    this._log.trace("Verifying their key.");
   1.634 +    if (this._incoming.type != this._their_signerid + "3") {
   1.635 +      this._log.error("Invalid round 3 data: "
   1.636 +                      + JSON.stringify(this._incoming));
   1.637 +      this.abort(JPAKE_ERROR_WRONGMESSAGE);
   1.638 +      return;
   1.639 +    }
   1.640 +    let step3 = this._incoming.payload;
   1.641 +    try {
   1.642 +      let hmac = Utils.bytesAsHex(
   1.643 +        Utils.digestUTF8(step3.ciphertext, this._hmac_hasher));
   1.644 +      if (hmac != step3.hmac) {
   1.645 +        throw "HMAC validation failed!";
   1.646 +      }
   1.647 +    } catch (ex) {
   1.648 +      this._log.error("HMAC validation failed.");
   1.649 +      this.abort(JPAKE_ERROR_KEYMISMATCH);
   1.650 +      return;
   1.651 +    }
   1.652 +
   1.653 +    this._log.trace("Decrypting data.");
   1.654 +    let cleartext;
   1.655 +    try {      
   1.656 +      cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
   1.657 +                                     step3.IV);
   1.658 +    } catch (ex) {
   1.659 +      this._log.error("Failed to decrypt data.");
   1.660 +      this.abort(JPAKE_ERROR_INTERNAL);
   1.661 +      return;
   1.662 +    }
   1.663 +
   1.664 +    try {
   1.665 +      this._newData = JSON.parse(cleartext);
   1.666 +    } catch (ex) {
   1.667 +      this._log.error("Invalid data data: " + JSON.stringify(cleartext));
   1.668 +      this.abort(JPAKE_ERROR_INVALID);
   1.669 +      return;
   1.670 +    }
   1.671 +
   1.672 +    this._log.trace("Decrypted data.");
   1.673 +    callback();
   1.674 +  },
   1.675 +
   1.676 +  _complete: function _complete() {
   1.677 +    this._log.debug("Exchange completed.");
   1.678 +    this._finished = true;
   1.679 +    Utils.nextTick(function () { this.controller.onComplete(this._newData); },
   1.680 +                   this);
   1.681 +  }
   1.682 +
   1.683 +};
   1.684 +
   1.685 +
   1.686 +/**
   1.687 + * Send credentials over an active J-PAKE channel.
   1.688 + *
   1.689 + * This object is designed to take over as the JPAKEClient controller,
   1.690 + * presumably replacing one that is UI-based which would either cause
   1.691 + * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
   1.692 + * context disappears. This object stays alive for the duration of the
   1.693 + * transfer by being strong-ref'ed as an nsIObserver.
   1.694 + *
   1.695 + * Credentials are sent after the first sync has been completed
   1.696 + * (successfully or not.)
   1.697 + *
   1.698 + * Usage:
   1.699 + *
   1.700 + *   jpakeclient.controller = new SendCredentialsController(jpakeclient,
   1.701 + *                                                          service);
   1.702 + *
   1.703 + */
   1.704 +this.SendCredentialsController =
   1.705 + function SendCredentialsController(jpakeclient, service) {
   1.706 +  this._log = Log.repository.getLogger("Sync.SendCredentialsController");
   1.707 +  this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
   1.708 +
   1.709 +  this._log.trace("Loading.");
   1.710 +  this.jpakeclient = jpakeclient;
   1.711 +  this.service = service;
   1.712 +
   1.713 +  // Register ourselves as observers the first Sync finishing (either
   1.714 +  // successfully or unsuccessfully, we don't care) or for removing
   1.715 +  // this device's sync configuration, in case that happens while we
   1.716 +  // haven't finished the first sync yet.
   1.717 +  Services.obs.addObserver(this, "weave:service:sync:finish", false);
   1.718 +  Services.obs.addObserver(this, "weave:service:sync:error",  false);
   1.719 +  Services.obs.addObserver(this, "weave:service:start-over",  false);
   1.720 +}
   1.721 +SendCredentialsController.prototype = {
   1.722 +
   1.723 +  unload: function unload() {
   1.724 +    this._log.trace("Unloading.");
   1.725 +    try {
   1.726 +      Services.obs.removeObserver(this, "weave:service:sync:finish");
   1.727 +      Services.obs.removeObserver(this, "weave:service:sync:error");
   1.728 +      Services.obs.removeObserver(this, "weave:service:start-over");
   1.729 +    } catch (ex) {
   1.730 +      // Ignore.
   1.731 +    }
   1.732 +  },
   1.733 +
   1.734 +  observe: function observe(subject, topic, data) {
   1.735 +    switch (topic) {
   1.736 +      case "weave:service:sync:finish":
   1.737 +      case "weave:service:sync:error":
   1.738 +        Utils.nextTick(this.sendCredentials, this);
   1.739 +        break;
   1.740 +      case "weave:service:start-over":
   1.741 +        // This will call onAbort which will call unload().
   1.742 +        this.jpakeclient.abort();
   1.743 +        break;
   1.744 +    }
   1.745 +  },
   1.746 +
   1.747 +  sendCredentials: function sendCredentials() {
   1.748 +    this._log.trace("Sending credentials.");
   1.749 +    let credentials = {account:   this.service.identity.account,
   1.750 +                       password:  this.service.identity.basicPassword,
   1.751 +                       synckey:   this.service.identity.syncKey,
   1.752 +                       serverURL: this.service.serverURL};
   1.753 +    this.jpakeclient.sendAndComplete(credentials);
   1.754 +  },
   1.755 +
   1.756 +  // JPAKEClient controller API
   1.757 +
   1.758 +  onComplete: function onComplete() {
   1.759 +    this._log.debug("Exchange was completed successfully!");
   1.760 +    this.unload();
   1.761 +
   1.762 +    // Schedule a Sync for soonish to fetch the data uploaded by the
   1.763 +    // device with which we just paired.
   1.764 +    this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval);
   1.765 +  },
   1.766 +
   1.767 +  onAbort: function onAbort(error) {
   1.768 +    // It doesn't really matter why we aborted, but the channel is closed
   1.769 +    // for sure, so we won't be able to do anything with it.
   1.770 +    this._log.debug("Exchange was aborted with error: " + error);
   1.771 +    this.unload();
   1.772 +  },
   1.773 +
   1.774 +  // Irrelevant methods for this controller:
   1.775 +  displayPIN: function displayPIN() {},
   1.776 +  onPairingStart: function onPairingStart() {},
   1.777 +  onPaired: function onPaired() {},
   1.778 +};

mercurial