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