зеркало из https://github.com/mozilla/gecko-dev.git
776 строки
26 KiB
JavaScript
776 строки
26 KiB
JavaScript
/* 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"];
|
|
|
|
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://services-common/log4moz.js");
|
|
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 = Log4Moz.repository.getLogger("Sync.JPAKEClient");
|
|
this._log.level = Log4Moz.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 = [("0" + byte.toString(16)).slice(-2)
|
|
for each (byte in bytes)].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 [key[Math.floor(byte * key.length / 256)]
|
|
for each (byte in bytes)].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 = Log4Moz.repository.getLogger("Sync.SendCredentialsController");
|
|
this._log.level = Log4Moz.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() {},
|
|
};
|