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 +};