summaryrefslogtreecommitdiffstats
path: root/services/sync/modules/jpakeclient.js
diff options
context:
space:
mode:
Diffstat (limited to 'services/sync/modules/jpakeclient.js')
-rw-r--r--services/sync/modules/jpakeclient.js773
1 files changed, 773 insertions, 0 deletions
diff --git a/services/sync/modules/jpakeclient.js b/services/sync/modules/jpakeclient.js
new file mode 100644
index 000000000..625dc91b6
--- /dev/null
+++ b/services/sync/modules/jpakeclient.js
@@ -0,0 +1,773 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
+
+var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/util.js");
+
+const REQUEST_TIMEOUT = 60; // 1 minute
+const KEYEXCHANGE_VERSION = 3;
+
+const JPAKE_SIGNERID_SENDER = "sender";
+const JPAKE_SIGNERID_RECEIVER = "receiver";
+const JPAKE_LENGTH_SECRET = 8;
+const JPAKE_LENGTH_CLIENTID = 256;
+const JPAKE_VERIFY_VALUE = "0123456789ABCDEF";
+
+
+/**
+ * Client to exchange encrypted data using the J-PAKE algorithm.
+ * The exchange between two clients of this type looks like this:
+ *
+ *
+ * Mobile Server Desktop
+ * ===================================================================
+ * |
+ * retrieve channel <---------------|
+ * generate random secret |
+ * show PIN = secret + channel | ask user for PIN
+ * upload Mobile's message 1 ------>|
+ * |----> retrieve Mobile's message 1
+ * |<----- upload Desktop's message 1
+ * retrieve Desktop's message 1 <---|
+ * upload Mobile's message 2 ------>|
+ * |----> retrieve Mobile's message 2
+ * | compute key
+ * |<----- upload Desktop's message 2
+ * retrieve Desktop's message 2 <---|
+ * compute key |
+ * encrypt known value ------------>|
+ * |-------> retrieve encrypted value
+ * | verify against local known value
+ *
+ * At this point Desktop knows whether the PIN was entered correctly.
+ * If it wasn't, Desktop deletes the session. If it was, the account
+ * setup can proceed. If Desktop doesn't yet have an account set up,
+ * it will keep the channel open and let the user connect to or
+ * create an account.
+ *
+ * | encrypt credentials
+ * |<------------- upload credentials
+ * retrieve credentials <-----------|
+ * verify HMAC |
+ * decrypt credentials |
+ * delete session ----------------->|
+ * start syncing |
+ *
+ *
+ * Create a client object like so:
+ *
+ * let client = new JPAKEClient(controller);
+ *
+ * The 'controller' object must implement the following methods:
+ *
+ * displayPIN(pin) -- Called when a PIN has been generated and is ready to
+ * be displayed to the user. Only called on the client where the pairing
+ * was initiated with 'receiveNoPIN()'.
+ *
+ * onPairingStart() -- Called when the pairing has started and messages are
+ * being sent back and forth over the channel. Only called on the client
+ * where the pairing was initiated with 'receiveNoPIN()'.
+ *
+ * onPaired() -- Called when the device pairing has been established and
+ * we're ready to send the credentials over. To do that, the controller
+ * must call 'sendAndComplete()' while the channel is active.
+ *
+ * onComplete(data) -- Called after transfer has been completed. On
+ * the sending side this is called with no parameter and as soon as the
+ * data has been uploaded. This does not mean the receiving side has
+ * actually retrieved them yet.
+ *
+ * onAbort(error) -- Called whenever an error is encountered. All errors lead
+ * to an abort and the process has to be started again on both sides.
+ *
+ * To start the data transfer on the receiving side, call
+ *
+ * client.receiveNoPIN();
+ *
+ * This will allocate a new channel on the server, generate a PIN, have it
+ * displayed and then do the transfer once the protocol has been completed
+ * with the sending side.
+ *
+ * To initiate the transfer from the sending side, call
+ *
+ * client.pairWithPIN(pin, true);
+ *
+ * Once the pairing has been established, the controller's 'onPaired()' method
+ * will be called. To then transmit the data, call
+ *
+ * client.sendAndComplete(data);
+ *
+ * To abort the process, call
+ *
+ * client.abort();
+ *
+ * Note that after completion or abort, the 'client' instance may not be reused.
+ * You will have to create a new one in case you'd like to restart the process.
+ */
+this.JPAKEClient = function JPAKEClient(controller) {
+ this.controller = controller;
+
+ this._log = Log.repository.getLogger("Sync.JPAKEClient");
+ this._log.level = Log.Level[Svc.Prefs.get(
+ "log.logger.service.jpakeclient", "Debug")];
+
+ this._serverURL = Svc.Prefs.get("jpake.serverURL");
+ this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
+ this._maxTries = Svc.Prefs.get("jpake.maxTries");
+ if (this._serverURL.slice(-1) != "/") {
+ this._serverURL += "/";
+ }
+
+ this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
+ .createInstance(Ci.nsISyncJPAKE);
+
+ this._setClientID();
+}
+JPAKEClient.prototype = {
+
+ _chain: Async.chain,
+
+ /*
+ * Public API
+ */
+
+ /**
+ * Initiate pairing and receive data without providing a PIN. The PIN will
+ * be generated and passed on to the controller to be displayed to the user.
+ *
+ * This is typically called on mobile devices where typing is tedious.
+ */
+ receiveNoPIN: function receiveNoPIN() {
+ this._my_signerid = JPAKE_SIGNERID_RECEIVER;
+ this._their_signerid = JPAKE_SIGNERID_SENDER;
+
+ this._secret = this._createSecret();
+
+ // Allow a large number of tries first while we wait for the PIN
+ // to be entered on the other device.
+ this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
+ this._chain(this._getChannel,
+ this._computeStepOne,
+ this._putStep,
+ this._getStep,
+ function(callback) {
+ // We fetched the first response from the other client.
+ // Notify controller of the pairing starting.
+ Utils.nextTick(this.controller.onPairingStart,
+ this.controller);
+
+ // Now we can switch back to the smaller timeout.
+ this._maxTries = Svc.Prefs.get("jpake.maxTries");
+ callback();
+ },
+ this._computeStepTwo,
+ this._putStep,
+ this._getStep,
+ this._computeFinal,
+ this._computeKeyVerification,
+ this._putStep,
+ function(callback) {
+ // Allow longer time-out for the last message.
+ this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
+ callback();
+ },
+ this._getStep,
+ this._decryptData,
+ this._complete)();
+ },
+
+ /**
+ * Initiate pairing based on the PIN entered by the user.
+ *
+ * This is typically called on desktop devices where typing is easier than
+ * on mobile.
+ *
+ * @param pin
+ * 12 character string (in human-friendly base32) containing the PIN
+ * entered by the user.
+ * @param expectDelay
+ * Flag that indicates that a significant delay between the pairing
+ * and the sending should be expected. v2 and earlier of the protocol
+ * did not allow for this and the pairing to a v2 or earlier client
+ * will be aborted if this flag is 'true'.
+ */
+ pairWithPIN: function pairWithPIN(pin, expectDelay) {
+ this._my_signerid = JPAKE_SIGNERID_SENDER;
+ this._their_signerid = JPAKE_SIGNERID_RECEIVER;
+
+ this._channel = pin.slice(JPAKE_LENGTH_SECRET);
+ this._channelURL = this._serverURL + this._channel;
+ this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
+
+ this._chain(this._computeStepOne,
+ this._getStep,
+ function (callback) {
+ // Ensure that the other client can deal with a delay for
+ // the last message if that's requested by the caller.
+ if (!expectDelay) {
+ return callback();
+ }
+ if (!this._incoming.version || this._incoming.version < 3) {
+ return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED);
+ }
+ return callback();
+ },
+ this._putStep,
+ this._computeStepTwo,
+ this._getStep,
+ this._putStep,
+ this._computeFinal,
+ this._getStep,
+ this._verifyPairing)();
+ },
+
+ /**
+ * Send data after a successful pairing.
+ *
+ * @param obj
+ * Object containing the data to send. It will be serialized as JSON.
+ */
+ sendAndComplete: function sendAndComplete(obj) {
+ if (!this._paired || this._finished) {
+ this._log.error("Can't send data, no active pairing!");
+ throw "No active pairing!";
+ }
+ this._data = JSON.stringify(obj);
+ this._chain(this._encryptData,
+ this._putStep,
+ this._complete)();
+ },
+
+ /**
+ * Abort the current pairing. The channel on the server will be deleted
+ * if the abort wasn't due to a network or server error. The controller's
+ * 'onAbort()' method is notified in all cases.
+ *
+ * @param error [optional]
+ * Error constant indicating the reason for the abort. Defaults to
+ * user abort.
+ */
+ abort: function abort(error) {
+ this._log.debug("Aborting...");
+ this._finished = true;
+ let self = this;
+
+ // Default to "user aborted".
+ if (!error) {
+ error = JPAKE_ERROR_USERABORT;
+ }
+
+ if (error == JPAKE_ERROR_CHANNEL ||
+ error == JPAKE_ERROR_NETWORK ||
+ error == JPAKE_ERROR_NODATA) {
+ Utils.nextTick(function() { this.controller.onAbort(error); }, this);
+ } else {
+ this._reportFailure(error, function() { self.controller.onAbort(error); });
+ }
+ },
+
+ /*
+ * Utilities
+ */
+
+ _setClientID: function _setClientID() {
+ let rng = Cc["@mozilla.org/security/random-generator;1"]
+ .createInstance(Ci.nsIRandomGenerator);
+ let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
+ this._clientID = bytes.map(byte => ("0" + byte.toString(16)).slice(-2)).join("");
+ },
+
+ _createSecret: function _createSecret() {
+ // 0-9a-z without 1,l,o,0
+ const key = "23456789abcdefghijkmnpqrstuvwxyz";
+ let rng = Cc["@mozilla.org/security/random-generator;1"]
+ .createInstance(Ci.nsIRandomGenerator);
+ let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
+ return bytes.map(byte => key[Math.floor(byte * key.length / 256)]).join("");
+ },
+
+ _newRequest: function _newRequest(uri) {
+ let request = new RESTRequest(uri);
+ request.setHeader("X-KeyExchange-Id", this._clientID);
+ request.timeout = REQUEST_TIMEOUT;
+ return request;
+ },
+
+ /*
+ * Steps of J-PAKE procedure
+ */
+
+ _getChannel: function _getChannel(callback) {
+ this._log.trace("Requesting channel.");
+ let request = this._newRequest(this._serverURL + "new_channel");
+ request.get(Utils.bind2(this, function handleChannel(error) {
+ if (this._finished) {
+ return;
+ }
+
+ if (error) {
+ this._log.error("Error acquiring channel ID. " + error);
+ this.abort(JPAKE_ERROR_CHANNEL);
+ return;
+ }
+ if (request.response.status != 200) {
+ this._log.error("Error acquiring channel ID. Server responded with HTTP "
+ + request.response.status);
+ this.abort(JPAKE_ERROR_CHANNEL);
+ return;
+ }
+
+ try {
+ this._channel = JSON.parse(request.response.body);
+ } catch (ex) {
+ this._log.error("Server responded with invalid JSON.");
+ this.abort(JPAKE_ERROR_CHANNEL);
+ return;
+ }
+ this._log.debug("Using channel " + this._channel);
+ this._channelURL = this._serverURL + this._channel;
+
+ // Don't block on UI code.
+ let pin = this._secret + this._channel;
+ Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
+ callback();
+ }));
+ },
+
+ // Generic handler for uploading data.
+ _putStep: function _putStep(callback) {
+ this._log.trace("Uploading message " + this._outgoing.type);
+ let request = this._newRequest(this._channelURL);
+ if (this._their_etag) {
+ request.setHeader("If-Match", this._their_etag);
+ } else {
+ request.setHeader("If-None-Match", "*");
+ }
+ request.put(this._outgoing, Utils.bind2(this, function (error) {
+ if (this._finished) {
+ return;
+ }
+
+ if (error) {
+ this._log.error("Error uploading data. " + error);
+ this.abort(JPAKE_ERROR_NETWORK);
+ return;
+ }
+ if (request.response.status != 200) {
+ this._log.error("Could not upload data. Server responded with HTTP "
+ + request.response.status);
+ this.abort(JPAKE_ERROR_SERVER);
+ return;
+ }
+ // There's no point in returning early here since the next step will
+ // always be a GET so let's pause for twice the poll interval.
+ this._my_etag = request.response.headers["etag"];
+ Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
+ this, "_pollTimer");
+ }));
+ },
+
+ // Generic handler for polling for and retrieving data.
+ _pollTries: 0,
+ _getStep: function _getStep(callback) {
+ this._log.trace("Retrieving next message.");
+ let request = this._newRequest(this._channelURL);
+ if (this._my_etag) {
+ request.setHeader("If-None-Match", this._my_etag);
+ }
+
+ request.get(Utils.bind2(this, function (error) {
+ if (this._finished) {
+ return;
+ }
+
+ if (error) {
+ this._log.error("Error fetching data. " + error);
+ this.abort(JPAKE_ERROR_NETWORK);
+ return;
+ }
+
+ if (request.response.status == 304) {
+ this._log.trace("Channel hasn't been updated yet. Will try again later.");
+ if (this._pollTries >= this._maxTries) {
+ this._log.error("Tried for " + this._pollTries + " times, aborting.");
+ this.abort(JPAKE_ERROR_TIMEOUT);
+ return;
+ }
+ this._pollTries += 1;
+ Utils.namedTimer(function() { this._getStep(callback); },
+ this._pollInterval, this, "_pollTimer");
+ return;
+ }
+ this._pollTries = 0;
+
+ if (request.response.status == 404) {
+ this._log.error("No data found in the channel.");
+ this.abort(JPAKE_ERROR_NODATA);
+ return;
+ }
+ if (request.response.status != 200) {
+ this._log.error("Could not retrieve data. Server responded with HTTP "
+ + request.response.status);
+ this.abort(JPAKE_ERROR_SERVER);
+ return;
+ }
+
+ this._their_etag = request.response.headers["etag"];
+ if (!this._their_etag) {
+ this._log.error("Server did not supply ETag for message: "
+ + request.response.body);
+ this.abort(JPAKE_ERROR_SERVER);
+ return;
+ }
+
+ try {
+ this._incoming = JSON.parse(request.response.body);
+ } catch (ex) {
+ this._log.error("Server responded with invalid JSON.");
+ this.abort(JPAKE_ERROR_INVALID);
+ return;
+ }
+ this._log.trace("Fetched message " + this._incoming.type);
+ callback();
+ }));
+ },
+
+ _reportFailure: function _reportFailure(reason, callback) {
+ this._log.debug("Reporting failure to server.");
+ let request = this._newRequest(this._serverURL + "report");
+ request.setHeader("X-KeyExchange-Cid", this._channel);
+ request.setHeader("X-KeyExchange-Log", reason);
+ request.post("", Utils.bind2(this, function (error) {
+ if (error) {
+ this._log.warn("Report failed: " + error);
+ } else if (request.response.status != 200) {
+ this._log.warn("Report failed. Server responded with HTTP "
+ + request.response.status);
+ }
+
+ // Do not block on errors, we're done or aborted by now anyway.
+ callback();
+ }));
+ },
+
+ _computeStepOne: function _computeStepOne(callback) {
+ this._log.trace("Computing round 1.");
+ let gx1 = {};
+ let gv1 = {};
+ let r1 = {};
+ let gx2 = {};
+ let gv2 = {};
+ let r2 = {};
+ try {
+ this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
+ } catch (ex) {
+ this._log.error("JPAKE round 1 threw: " + ex);
+ this.abort(JPAKE_ERROR_INTERNAL);
+ return;
+ }
+ let one = {gx1: gx1.value,
+ gx2: gx2.value,
+ zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
+ zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
+ this._outgoing = {type: this._my_signerid + "1",
+ version: KEYEXCHANGE_VERSION,
+ payload: one};
+ this._log.trace("Generated message " + this._outgoing.type);
+ callback();
+ },
+
+ _computeStepTwo: function _computeStepTwo(callback) {
+ this._log.trace("Computing round 2.");
+ if (this._incoming.type != this._their_signerid + "1") {
+ this._log.error("Invalid round 1 message: "
+ + JSON.stringify(this._incoming));
+ this.abort(JPAKE_ERROR_WRONGMESSAGE);
+ return;
+ }
+
+ let step1 = this._incoming.payload;
+ if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
+ || !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
+ this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
+ this.abort(JPAKE_ERROR_WRONGMESSAGE);
+ return;
+ }
+
+ let A = {};
+ let gvA = {};
+ let rA = {};
+
+ try {
+ this._jpake.round2(this._their_signerid, this._secret,
+ step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
+ step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
+ A, gvA, rA);
+ } catch (ex) {
+ this._log.error("JPAKE round 2 threw: " + ex);
+ this.abort(JPAKE_ERROR_INTERNAL);
+ return;
+ }
+ let two = {A: A.value,
+ zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
+ this._outgoing = {type: this._my_signerid + "2",
+ version: KEYEXCHANGE_VERSION,
+ payload: two};
+ this._log.trace("Generated message " + this._outgoing.type);
+ callback();
+ },
+
+ _computeFinal: function _computeFinal(callback) {
+ if (this._incoming.type != this._their_signerid + "2") {
+ this._log.error("Invalid round 2 message: "
+ + JSON.stringify(this._incoming));
+ this.abort(JPAKE_ERROR_WRONGMESSAGE);
+ return;
+ }
+
+ let step2 = this._incoming.payload;
+ if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
+ this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
+ this.abort(JPAKE_ERROR_WRONGMESSAGE);
+ return;
+ }
+
+ let aes256Key = {};
+ let hmac256Key = {};
+
+ try {
+ this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
+ aes256Key, hmac256Key);
+ } catch (ex) {
+ this._log.error("JPAKE final round threw: " + ex);
+ this.abort(JPAKE_ERROR_INTERNAL);
+ return;
+ }
+
+ this._crypto_key = aes256Key.value;
+ let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
+ this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key);
+
+ callback();
+ },
+
+ _computeKeyVerification: function _computeKeyVerification(callback) {
+ this._log.trace("Encrypting key verification value.");
+ let iv, ciphertext;
+ try {
+ iv = Svc.Crypto.generateRandomIV();
+ ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
+ this._crypto_key, iv);
+ } catch (ex) {
+ this._log.error("Failed to encrypt key verification value.");
+ this.abort(JPAKE_ERROR_INTERNAL);
+ return;
+ }
+ this._outgoing = {type: this._my_signerid + "3",
+ version: KEYEXCHANGE_VERSION,
+ payload: {ciphertext: ciphertext, IV: iv}};
+ this._log.trace("Generated message " + this._outgoing.type);
+ callback();
+ },
+
+ _verifyPairing: function _verifyPairing(callback) {
+ this._log.trace("Verifying their key.");
+ if (this._incoming.type != this._their_signerid + "3") {
+ this._log.error("Invalid round 3 data: " +
+ JSON.stringify(this._incoming));
+ this.abort(JPAKE_ERROR_WRONGMESSAGE);
+ return;
+ }
+ let step3 = this._incoming.payload;
+ let ciphertext;
+ try {
+ ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
+ this._crypto_key, step3.IV);
+ if (ciphertext != step3.ciphertext) {
+ throw "Key mismatch!";
+ }
+ } catch (ex) {
+ this._log.error("Keys don't match!");
+ this.abort(JPAKE_ERROR_KEYMISMATCH);
+ return;
+ }
+
+ this._log.debug("Verified pairing!");
+ this._paired = true;
+ Utils.nextTick(function () { this.controller.onPaired(); }, this);
+ callback();
+ },
+
+ _encryptData: function _encryptData(callback) {
+ this._log.trace("Encrypting data.");
+ let iv, ciphertext, hmac;
+ try {
+ iv = Svc.Crypto.generateRandomIV();
+ ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
+ hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher));
+ } catch (ex) {
+ this._log.error("Failed to encrypt data.");
+ this.abort(JPAKE_ERROR_INTERNAL);
+ return;
+ }
+ this._outgoing = {type: this._my_signerid + "3",
+ version: KEYEXCHANGE_VERSION,
+ payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
+ this._log.trace("Generated message " + this._outgoing.type);
+ callback();
+ },
+
+ _decryptData: function _decryptData(callback) {
+ this._log.trace("Verifying their key.");
+ if (this._incoming.type != this._their_signerid + "3") {
+ this._log.error("Invalid round 3 data: "
+ + JSON.stringify(this._incoming));
+ this.abort(JPAKE_ERROR_WRONGMESSAGE);
+ return;
+ }
+ let step3 = this._incoming.payload;
+ try {
+ let hmac = Utils.bytesAsHex(
+ Utils.digestUTF8(step3.ciphertext, this._hmac_hasher));
+ if (hmac != step3.hmac) {
+ throw "HMAC validation failed!";
+ }
+ } catch (ex) {
+ this._log.error("HMAC validation failed.");
+ this.abort(JPAKE_ERROR_KEYMISMATCH);
+ return;
+ }
+
+ this._log.trace("Decrypting data.");
+ let cleartext;
+ try {
+ cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
+ step3.IV);
+ } catch (ex) {
+ this._log.error("Failed to decrypt data.");
+ this.abort(JPAKE_ERROR_INTERNAL);
+ return;
+ }
+
+ try {
+ this._newData = JSON.parse(cleartext);
+ } catch (ex) {
+ this._log.error("Invalid data data: " + JSON.stringify(cleartext));
+ this.abort(JPAKE_ERROR_INVALID);
+ return;
+ }
+
+ this._log.trace("Decrypted data.");
+ callback();
+ },
+
+ _complete: function _complete() {
+ this._log.debug("Exchange completed.");
+ this._finished = true;
+ Utils.nextTick(function () { this.controller.onComplete(this._newData); },
+ this);
+ }
+
+};
+
+
+/**
+ * Send credentials over an active J-PAKE channel.
+ *
+ * This object is designed to take over as the JPAKEClient controller,
+ * presumably replacing one that is UI-based which would either cause
+ * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
+ * context disappears. This object stays alive for the duration of the
+ * transfer by being strong-ref'ed as an nsIObserver.
+ *
+ * Credentials are sent after the first sync has been completed
+ * (successfully or not.)
+ *
+ * Usage:
+ *
+ * jpakeclient.controller = new SendCredentialsController(jpakeclient,
+ * service);
+ *
+ */
+this.SendCredentialsController =
+ function SendCredentialsController(jpakeclient, service) {
+ this._log = Log.repository.getLogger("Sync.SendCredentialsController");
+ this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
+
+ this._log.trace("Loading.");
+ this.jpakeclient = jpakeclient;
+ this.service = service;
+
+ // Register ourselves as observers the first Sync finishing (either
+ // successfully or unsuccessfully, we don't care) or for removing
+ // this device's sync configuration, in case that happens while we
+ // haven't finished the first sync yet.
+ Services.obs.addObserver(this, "weave:service:sync:finish", false);
+ Services.obs.addObserver(this, "weave:service:sync:error", false);
+ Services.obs.addObserver(this, "weave:service:start-over", false);
+}
+SendCredentialsController.prototype = {
+
+ unload: function unload() {
+ this._log.trace("Unloading.");
+ try {
+ Services.obs.removeObserver(this, "weave:service:sync:finish");
+ Services.obs.removeObserver(this, "weave:service:sync:error");
+ Services.obs.removeObserver(this, "weave:service:start-over");
+ } catch (ex) {
+ // Ignore.
+ }
+ },
+
+ observe: function observe(subject, topic, data) {
+ switch (topic) {
+ case "weave:service:sync:finish":
+ case "weave:service:sync:error":
+ Utils.nextTick(this.sendCredentials, this);
+ break;
+ case "weave:service:start-over":
+ // This will call onAbort which will call unload().
+ this.jpakeclient.abort();
+ break;
+ }
+ },
+
+ sendCredentials: function sendCredentials() {
+ this._log.trace("Sending credentials.");
+ let credentials = {account: this.service.identity.account,
+ password: this.service.identity.basicPassword,
+ synckey: this.service.identity.syncKey,
+ serverURL: this.service.serverURL};
+ this.jpakeclient.sendAndComplete(credentials);
+ },
+
+ // JPAKEClient controller API
+
+ onComplete: function onComplete() {
+ this._log.debug("Exchange was completed successfully!");
+ this.unload();
+
+ // Schedule a Sync for soonish to fetch the data uploaded by the
+ // device with which we just paired.
+ this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval);
+ },
+
+ onAbort: function onAbort(error) {
+ // It doesn't really matter why we aborted, but the channel is closed
+ // for sure, so we won't be able to do anything with it.
+ this._log.debug("Exchange was aborted with error: " + error);
+ this.unload();
+ },
+
+ // Irrelevant methods for this controller:
+ displayPIN: function displayPIN() {},
+ onPairingStart: function onPairingStart() {},
+ onPaired: function onPaired() {},
+};