services/sync/modules/jpakeclient.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 this.EXPORTED_SYMBOLS = ["JPAKEClient", "SendCredentialsController"];
     7 const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
     9 Cu.import("resource://gre/modules/Log.jsm");
    10 Cu.import("resource://services-common/rest.js");
    11 Cu.import("resource://services-sync/constants.js");
    12 Cu.import("resource://services-sync/util.js");
    14 const REQUEST_TIMEOUT         = 60; // 1 minute
    15 const KEYEXCHANGE_VERSION     = 3;
    17 const JPAKE_SIGNERID_SENDER   = "sender";
    18 const JPAKE_SIGNERID_RECEIVER = "receiver";
    19 const JPAKE_LENGTH_SECRET     = 8;
    20 const JPAKE_LENGTH_CLIENTID   = 256;
    21 const JPAKE_VERIFY_VALUE      = "0123456789ABCDEF";
    24 /**
    25  * Client to exchange encrypted data using the J-PAKE algorithm.
    26  * The exchange between two clients of this type looks like this:
    27  * 
    28  * 
    29  *  Mobile                        Server                        Desktop
    30  *  ===================================================================
    31  *                                   |
    32  *  retrieve channel <---------------|
    33  *  generate random secret           |
    34  *  show PIN = secret + channel      |                 ask user for PIN
    35  *  upload Mobile's message 1 ------>|
    36  *                                   |----> retrieve Mobile's message 1
    37  *                                   |<----- upload Desktop's message 1
    38  *  retrieve Desktop's message 1 <---|
    39  *  upload Mobile's message 2 ------>|
    40  *                                   |----> retrieve Mobile's message 2
    41  *                                   |                      compute key
    42  *                                   |<----- upload Desktop's message 2
    43  *  retrieve Desktop's message 2 <---|
    44  *  compute key                      |
    45  *  encrypt known value ------------>|
    46  *                                   |-------> retrieve encrypted value
    47  *                                   | verify against local known value
    48  *
    49  *   At this point Desktop knows whether the PIN was entered correctly.
    50  *   If it wasn't, Desktop deletes the session. If it was, the account
    51  *   setup can proceed. If Desktop doesn't yet have an account set up,
    52  *   it will keep the channel open and let the user connect to or
    53  *   create an account.
    54  *
    55  *                                   |              encrypt credentials
    56  *                                   |<------------- upload credentials
    57  *  retrieve credentials <-----------|
    58  *  verify HMAC                      |
    59  *  decrypt credentials              |
    60  *  delete session ----------------->|
    61  *  start syncing                    |
    62  * 
    63  * 
    64  * Create a client object like so:
    65  * 
    66  *   let client = new JPAKEClient(controller);
    67  * 
    68  * The 'controller' object must implement the following methods:
    69  * 
    70  *   displayPIN(pin) -- Called when a PIN has been generated and is ready to
    71  *     be displayed to the user. Only called on the client where the pairing
    72  *     was initiated with 'receiveNoPIN()'.
    73  * 
    74  *   onPairingStart() -- Called when the pairing has started and messages are
    75  *     being sent back and forth over the channel. Only called on the client
    76  *     where the pairing was initiated with 'receiveNoPIN()'.
    77  * 
    78  *   onPaired() -- Called when the device pairing has been established and
    79  *     we're ready to send the credentials over. To do that, the controller
    80  *     must call 'sendAndComplete()' while the channel is active.
    81  * 
    82  *   onComplete(data) -- Called after transfer has been completed. On
    83  *     the sending side this is called with no parameter and as soon as the
    84  *     data has been uploaded. This does not mean the receiving side has
    85  *     actually retrieved them yet.
    86  *
    87  *   onAbort(error) -- Called whenever an error is encountered. All errors lead
    88  *     to an abort and the process has to be started again on both sides.
    89  * 
    90  * To start the data transfer on the receiving side, call
    91  * 
    92  *   client.receiveNoPIN();
    93  * 
    94  * This will allocate a new channel on the server, generate a PIN, have it
    95  * displayed and then do the transfer once the protocol has been completed
    96  * with the sending side.
    97  * 
    98  * To initiate the transfer from the sending side, call
    99  * 
   100  *   client.pairWithPIN(pin, true);
   101  * 
   102  * Once the pairing has been established, the controller's 'onPaired()' method
   103  * will be called. To then transmit the data, call
   104  * 
   105  *   client.sendAndComplete(data);
   106  * 
   107  * To abort the process, call
   108  * 
   109  *   client.abort();
   110  * 
   111  * Note that after completion or abort, the 'client' instance may not be reused.
   112  * You will have to create a new one in case you'd like to restart the process.
   113  */
   114 this.JPAKEClient = function JPAKEClient(controller) {
   115   this.controller = controller;
   117   this._log = Log.repository.getLogger("Sync.JPAKEClient");
   118   this._log.level = Log.Level[Svc.Prefs.get(
   119     "log.logger.service.jpakeclient", "Debug")];
   121   this._serverURL = Svc.Prefs.get("jpake.serverURL");
   122   this._pollInterval = Svc.Prefs.get("jpake.pollInterval");
   123   this._maxTries = Svc.Prefs.get("jpake.maxTries");
   124   if (this._serverURL.slice(-1) != "/") {
   125     this._serverURL += "/";
   126   }
   128   this._jpake = Cc["@mozilla.org/services-crypto/sync-jpake;1"]
   129                   .createInstance(Ci.nsISyncJPAKE);
   131   this._setClientID();
   132 }
   133 JPAKEClient.prototype = {
   135   _chain: Async.chain,
   137   /*
   138    * Public API
   139    */
   141   /**
   142    * Initiate pairing and receive data without providing a PIN. The PIN will
   143    * be generated and passed on to the controller to be displayed to the user.
   144    * 
   145    * This is typically called on mobile devices where typing is tedious.
   146    */
   147   receiveNoPIN: function receiveNoPIN() {
   148     this._my_signerid = JPAKE_SIGNERID_RECEIVER;
   149     this._their_signerid = JPAKE_SIGNERID_SENDER;
   151     this._secret = this._createSecret();
   153     // Allow a large number of tries first while we wait for the PIN
   154     // to be entered on the other device.
   155     this._maxTries = Svc.Prefs.get("jpake.firstMsgMaxTries");
   156     this._chain(this._getChannel,
   157                 this._computeStepOne,
   158                 this._putStep,
   159                 this._getStep,
   160                 function(callback) {
   161                   // We fetched the first response from the other client.
   162                   // Notify controller of the pairing starting.
   163                   Utils.nextTick(this.controller.onPairingStart,
   164                                  this.controller);
   166                   // Now we can switch back to the smaller timeout.
   167                   this._maxTries = Svc.Prefs.get("jpake.maxTries");
   168                   callback();
   169                 },
   170                 this._computeStepTwo,
   171                 this._putStep,
   172                 this._getStep,
   173                 this._computeFinal,
   174                 this._computeKeyVerification,
   175                 this._putStep,
   176                 function(callback) {
   177                   // Allow longer time-out for the last message.
   178                   this._maxTries = Svc.Prefs.get("jpake.lastMsgMaxTries");
   179                   callback();
   180                 },
   181                 this._getStep,
   182                 this._decryptData,
   183                 this._complete)();
   184   },
   186   /**
   187    * Initiate pairing based on the PIN entered by the user.
   188    * 
   189    * This is typically called on desktop devices where typing is easier than
   190    * on mobile.
   191    * 
   192    * @param pin
   193    *        12 character string (in human-friendly base32) containing the PIN
   194    *        entered by the user.
   195    * @param expectDelay
   196    *        Flag that indicates that a significant delay between the pairing
   197    *        and the sending should be expected. v2 and earlier of the protocol
   198    *        did not allow for this and the pairing to a v2 or earlier client
   199    *        will be aborted if this flag is 'true'.
   200    */
   201   pairWithPIN: function pairWithPIN(pin, expectDelay) {
   202     this._my_signerid = JPAKE_SIGNERID_SENDER;
   203     this._their_signerid = JPAKE_SIGNERID_RECEIVER;
   205     this._channel = pin.slice(JPAKE_LENGTH_SECRET);
   206     this._channelURL = this._serverURL + this._channel;
   207     this._secret = pin.slice(0, JPAKE_LENGTH_SECRET);
   209     this._chain(this._computeStepOne,
   210                 this._getStep,
   211                 function (callback) {
   212                   // Ensure that the other client can deal with a delay for
   213                   // the last message if that's requested by the caller.
   214                   if (!expectDelay) {
   215                     return callback();
   216                   }
   217                   if (!this._incoming.version || this._incoming.version < 3) {
   218                     return this.abort(JPAKE_ERROR_DELAYUNSUPPORTED);
   219                   }
   220                   return callback();
   221                 },
   222                 this._putStep,
   223                 this._computeStepTwo,
   224                 this._getStep,
   225                 this._putStep,
   226                 this._computeFinal,
   227                 this._getStep,
   228                 this._verifyPairing)();
   229   },
   231   /**
   232    * Send data after a successful pairing.
   233    * 
   234    * @param obj
   235    *        Object containing the data to send. It will be serialized as JSON.
   236    */
   237   sendAndComplete: function sendAndComplete(obj) {
   238     if (!this._paired || this._finished) {
   239       this._log.error("Can't send data, no active pairing!");
   240       throw "No active pairing!";
   241     }
   242     this._data = JSON.stringify(obj);
   243     this._chain(this._encryptData,
   244                 this._putStep,
   245                 this._complete)();
   246   },
   248   /**
   249    * Abort the current pairing. The channel on the server will be deleted
   250    * if the abort wasn't due to a network or server error. The controller's
   251    * 'onAbort()' method is notified in all cases.
   252    * 
   253    * @param error [optional]
   254    *        Error constant indicating the reason for the abort. Defaults to
   255    *        user abort.
   256    */
   257   abort: function abort(error) {
   258     this._log.debug("Aborting...");
   259     this._finished = true;
   260     let self = this;
   262     // Default to "user aborted".
   263     if (!error) {
   264       error = JPAKE_ERROR_USERABORT;
   265     }
   267     if (error == JPAKE_ERROR_CHANNEL ||
   268         error == JPAKE_ERROR_NETWORK ||
   269         error == JPAKE_ERROR_NODATA) {
   270       Utils.nextTick(function() { this.controller.onAbort(error); }, this);
   271     } else {
   272       this._reportFailure(error, function() { self.controller.onAbort(error); });
   273     }
   274   },
   276   /*
   277    * Utilities
   278    */
   280   _setClientID: function _setClientID() {
   281     let rng = Cc["@mozilla.org/security/random-generator;1"]
   282                 .createInstance(Ci.nsIRandomGenerator);
   283     let bytes = rng.generateRandomBytes(JPAKE_LENGTH_CLIENTID / 2);
   284     this._clientID = [("0" + byte.toString(16)).slice(-2)
   285                       for each (byte in bytes)].join("");
   286   },
   288   _createSecret: function _createSecret() {
   289     // 0-9a-z without 1,l,o,0
   290     const key = "23456789abcdefghijkmnpqrstuvwxyz";
   291     let rng = Cc["@mozilla.org/security/random-generator;1"]
   292                 .createInstance(Ci.nsIRandomGenerator);
   293     let bytes = rng.generateRandomBytes(JPAKE_LENGTH_SECRET);
   294     return [key[Math.floor(byte * key.length / 256)]
   295             for each (byte in bytes)].join("");
   296   },
   298   _newRequest: function _newRequest(uri) {
   299     let request = new RESTRequest(uri);
   300     request.setHeader("X-KeyExchange-Id", this._clientID);
   301     request.timeout = REQUEST_TIMEOUT;
   302     return request;
   303   },
   305   /*
   306    * Steps of J-PAKE procedure
   307    */
   309   _getChannel: function _getChannel(callback) {
   310     this._log.trace("Requesting channel.");
   311     let request = this._newRequest(this._serverURL + "new_channel");
   312     request.get(Utils.bind2(this, function handleChannel(error) {
   313       if (this._finished) {
   314         return;
   315       }
   317       if (error) {
   318         this._log.error("Error acquiring channel ID. " + error);
   319         this.abort(JPAKE_ERROR_CHANNEL);
   320         return;
   321       }
   322       if (request.response.status != 200) {
   323         this._log.error("Error acquiring channel ID. Server responded with HTTP "
   324                         + request.response.status);
   325         this.abort(JPAKE_ERROR_CHANNEL);
   326         return;
   327       }
   329       try {
   330         this._channel = JSON.parse(request.response.body);
   331       } catch (ex) {
   332         this._log.error("Server responded with invalid JSON.");
   333         this.abort(JPAKE_ERROR_CHANNEL);
   334         return;
   335       }
   336       this._log.debug("Using channel " + this._channel);
   337       this._channelURL = this._serverURL + this._channel;
   339       // Don't block on UI code.
   340       let pin = this._secret + this._channel;
   341       Utils.nextTick(function() { this.controller.displayPIN(pin); }, this);
   342       callback();
   343     }));
   344   },
   346   // Generic handler for uploading data.
   347   _putStep: function _putStep(callback) {
   348     this._log.trace("Uploading message " + this._outgoing.type);
   349     let request = this._newRequest(this._channelURL);
   350     if (this._their_etag) {
   351       request.setHeader("If-Match", this._their_etag);
   352     } else {
   353       request.setHeader("If-None-Match", "*");
   354     }
   355     request.put(this._outgoing, Utils.bind2(this, function (error) {
   356       if (this._finished) {
   357         return;
   358       }
   360       if (error) {
   361         this._log.error("Error uploading data. " + error);
   362         this.abort(JPAKE_ERROR_NETWORK);
   363         return;
   364       }
   365       if (request.response.status != 200) {
   366         this._log.error("Could not upload data. Server responded with HTTP "
   367                         + request.response.status);
   368         this.abort(JPAKE_ERROR_SERVER);
   369         return;
   370       }
   371       // There's no point in returning early here since the next step will
   372       // always be a GET so let's pause for twice the poll interval.
   373       this._my_etag = request.response.headers["etag"];
   374       Utils.namedTimer(function () { callback(); }, this._pollInterval * 2,
   375                        this, "_pollTimer");
   376     }));
   377   },
   379   // Generic handler for polling for and retrieving data.
   380   _pollTries: 0,
   381   _getStep: function _getStep(callback) {
   382     this._log.trace("Retrieving next message.");
   383     let request = this._newRequest(this._channelURL);
   384     if (this._my_etag) {
   385       request.setHeader("If-None-Match", this._my_etag);
   386     }
   388     request.get(Utils.bind2(this, function (error) {
   389       if (this._finished) {
   390         return;
   391       }
   393       if (error) {
   394         this._log.error("Error fetching data. " + error);
   395         this.abort(JPAKE_ERROR_NETWORK);
   396         return;
   397       }
   399       if (request.response.status == 304) {
   400         this._log.trace("Channel hasn't been updated yet. Will try again later.");
   401         if (this._pollTries >= this._maxTries) {
   402           this._log.error("Tried for " + this._pollTries + " times, aborting.");
   403           this.abort(JPAKE_ERROR_TIMEOUT);
   404           return;
   405         }
   406         this._pollTries += 1;
   407         Utils.namedTimer(function() { this._getStep(callback); },
   408                          this._pollInterval, this, "_pollTimer");
   409         return;
   410       }
   411       this._pollTries = 0;
   413       if (request.response.status == 404) {
   414         this._log.error("No data found in the channel.");
   415         this.abort(JPAKE_ERROR_NODATA);
   416         return;
   417       }
   418       if (request.response.status != 200) {
   419         this._log.error("Could not retrieve data. Server responded with HTTP "
   420                         + request.response.status);
   421         this.abort(JPAKE_ERROR_SERVER);
   422         return;
   423       }
   425       this._their_etag = request.response.headers["etag"];
   426       if (!this._their_etag) {
   427         this._log.error("Server did not supply ETag for message: "
   428                         + request.response.body);
   429         this.abort(JPAKE_ERROR_SERVER);
   430         return;
   431       }
   433       try {
   434         this._incoming = JSON.parse(request.response.body);
   435       } catch (ex) {
   436         this._log.error("Server responded with invalid JSON.");
   437         this.abort(JPAKE_ERROR_INVALID);
   438         return;
   439       }
   440       this._log.trace("Fetched message " + this._incoming.type);
   441       callback();
   442     }));
   443   },
   445   _reportFailure: function _reportFailure(reason, callback) {
   446     this._log.debug("Reporting failure to server.");
   447     let request = this._newRequest(this._serverURL + "report");
   448     request.setHeader("X-KeyExchange-Cid", this._channel);
   449     request.setHeader("X-KeyExchange-Log", reason);
   450     request.post("", Utils.bind2(this, function (error) {
   451       if (error) {
   452         this._log.warn("Report failed: " + error);
   453       } else if (request.response.status != 200) {
   454         this._log.warn("Report failed. Server responded with HTTP "
   455                        + request.response.status);
   456       }
   458       // Do not block on errors, we're done or aborted by now anyway.
   459       callback();
   460     }));
   461   },
   463   _computeStepOne: function _computeStepOne(callback) {
   464     this._log.trace("Computing round 1.");
   465     let gx1 = {};
   466     let gv1 = {};
   467     let r1 = {};
   468     let gx2 = {};
   469     let gv2 = {};
   470     let r2 = {};
   471     try {
   472       this._jpake.round1(this._my_signerid, gx1, gv1, r1, gx2, gv2, r2);
   473     } catch (ex) {
   474       this._log.error("JPAKE round 1 threw: " + ex);
   475       this.abort(JPAKE_ERROR_INTERNAL);
   476       return;
   477     }
   478     let one = {gx1: gx1.value,
   479                gx2: gx2.value,
   480                zkp_x1: {gr: gv1.value, b: r1.value, id: this._my_signerid},
   481                zkp_x2: {gr: gv2.value, b: r2.value, id: this._my_signerid}};
   482     this._outgoing = {type: this._my_signerid + "1",
   483                       version: KEYEXCHANGE_VERSION,
   484                       payload: one};
   485     this._log.trace("Generated message " + this._outgoing.type);
   486     callback();
   487   },
   489   _computeStepTwo: function _computeStepTwo(callback) {
   490     this._log.trace("Computing round 2.");
   491     if (this._incoming.type != this._their_signerid + "1") {
   492       this._log.error("Invalid round 1 message: "
   493                       + JSON.stringify(this._incoming));
   494       this.abort(JPAKE_ERROR_WRONGMESSAGE);
   495       return;
   496     }
   498     let step1 = this._incoming.payload;
   499     if (!step1 || !step1.zkp_x1 || step1.zkp_x1.id != this._their_signerid
   500         || !step1.zkp_x2 || step1.zkp_x2.id != this._their_signerid) {
   501       this._log.error("Invalid round 1 payload: " + JSON.stringify(step1));
   502       this.abort(JPAKE_ERROR_WRONGMESSAGE);
   503       return;
   504     }
   506     let A = {};
   507     let gvA = {};
   508     let rA = {};
   510     try {
   511       this._jpake.round2(this._their_signerid, this._secret,
   512                          step1.gx1, step1.zkp_x1.gr, step1.zkp_x1.b,
   513                          step1.gx2, step1.zkp_x2.gr, step1.zkp_x2.b,
   514                          A, gvA, rA);
   515     } catch (ex) {
   516       this._log.error("JPAKE round 2 threw: " + ex);
   517       this.abort(JPAKE_ERROR_INTERNAL);
   518       return;
   519     }
   520     let two = {A: A.value,
   521                zkp_A: {gr: gvA.value, b: rA.value, id: this._my_signerid}};
   522     this._outgoing = {type: this._my_signerid + "2",
   523                       version: KEYEXCHANGE_VERSION,
   524                       payload: two};
   525     this._log.trace("Generated message " + this._outgoing.type);
   526     callback();
   527   },
   529   _computeFinal: function _computeFinal(callback) {
   530     if (this._incoming.type != this._their_signerid + "2") {
   531       this._log.error("Invalid round 2 message: "
   532                       + JSON.stringify(this._incoming));
   533       this.abort(JPAKE_ERROR_WRONGMESSAGE);
   534       return;
   535     }
   537     let step2 = this._incoming.payload;
   538     if (!step2 || !step2.zkp_A || step2.zkp_A.id != this._their_signerid) {
   539       this._log.error("Invalid round 2 payload: " + JSON.stringify(step1));
   540       this.abort(JPAKE_ERROR_WRONGMESSAGE);
   541       return;
   542     }
   544     let aes256Key = {};
   545     let hmac256Key = {};
   547     try {
   548       this._jpake.final(step2.A, step2.zkp_A.gr, step2.zkp_A.b, HMAC_INPUT,
   549                         aes256Key, hmac256Key);
   550     } catch (ex) {
   551       this._log.error("JPAKE final round threw: " + ex);
   552       this.abort(JPAKE_ERROR_INTERNAL);
   553       return;
   554     }
   556     this._crypto_key = aes256Key.value;
   557     let hmac_key = Utils.makeHMACKey(Utils.safeAtoB(hmac256Key.value));
   558     this._hmac_hasher = Utils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256, hmac_key);
   560     callback();
   561   },
   563   _computeKeyVerification: function _computeKeyVerification(callback) {
   564     this._log.trace("Encrypting key verification value.");
   565     let iv, ciphertext;
   566     try {
   567       iv = Svc.Crypto.generateRandomIV();
   568       ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
   569                                       this._crypto_key, iv);
   570     } catch (ex) {
   571       this._log.error("Failed to encrypt key verification value.");
   572       this.abort(JPAKE_ERROR_INTERNAL);
   573       return;
   574     }
   575     this._outgoing = {type: this._my_signerid + "3",
   576                       version: KEYEXCHANGE_VERSION,
   577                       payload: {ciphertext: ciphertext, IV: iv}};
   578     this._log.trace("Generated message " + this._outgoing.type);
   579     callback();
   580   },
   582   _verifyPairing: function _verifyPairing(callback) {
   583     this._log.trace("Verifying their key.");
   584     if (this._incoming.type != this._their_signerid + "3") {
   585       this._log.error("Invalid round 3 data: " +
   586                       JSON.stringify(this._incoming));
   587       this.abort(JPAKE_ERROR_WRONGMESSAGE);
   588       return;
   589     }
   590     let step3 = this._incoming.payload;
   591     let ciphertext;
   592     try {
   593       ciphertext = Svc.Crypto.encrypt(JPAKE_VERIFY_VALUE,
   594                                       this._crypto_key, step3.IV);
   595       if (ciphertext != step3.ciphertext) {
   596         throw "Key mismatch!";
   597       }
   598     } catch (ex) {
   599       this._log.error("Keys don't match!");
   600       this.abort(JPAKE_ERROR_KEYMISMATCH);
   601       return;
   602     }
   604     this._log.debug("Verified pairing!");
   605     this._paired = true;
   606     Utils.nextTick(function () { this.controller.onPaired(); }, this);
   607     callback();
   608   },
   610   _encryptData: function _encryptData(callback) {
   611     this._log.trace("Encrypting data.");
   612     let iv, ciphertext, hmac;
   613     try {
   614       iv = Svc.Crypto.generateRandomIV();
   615       ciphertext = Svc.Crypto.encrypt(this._data, this._crypto_key, iv);
   616       hmac = Utils.bytesAsHex(Utils.digestUTF8(ciphertext, this._hmac_hasher));
   617     } catch (ex) {
   618       this._log.error("Failed to encrypt data.");
   619       this.abort(JPAKE_ERROR_INTERNAL);
   620       return;
   621     }
   622     this._outgoing = {type: this._my_signerid + "3",
   623                       version: KEYEXCHANGE_VERSION,
   624                       payload: {ciphertext: ciphertext, IV: iv, hmac: hmac}};
   625     this._log.trace("Generated message " + this._outgoing.type);
   626     callback();
   627   },
   629   _decryptData: function _decryptData(callback) {
   630     this._log.trace("Verifying their key.");
   631     if (this._incoming.type != this._their_signerid + "3") {
   632       this._log.error("Invalid round 3 data: "
   633                       + JSON.stringify(this._incoming));
   634       this.abort(JPAKE_ERROR_WRONGMESSAGE);
   635       return;
   636     }
   637     let step3 = this._incoming.payload;
   638     try {
   639       let hmac = Utils.bytesAsHex(
   640         Utils.digestUTF8(step3.ciphertext, this._hmac_hasher));
   641       if (hmac != step3.hmac) {
   642         throw "HMAC validation failed!";
   643       }
   644     } catch (ex) {
   645       this._log.error("HMAC validation failed.");
   646       this.abort(JPAKE_ERROR_KEYMISMATCH);
   647       return;
   648     }
   650     this._log.trace("Decrypting data.");
   651     let cleartext;
   652     try {      
   653       cleartext = Svc.Crypto.decrypt(step3.ciphertext, this._crypto_key,
   654                                      step3.IV);
   655     } catch (ex) {
   656       this._log.error("Failed to decrypt data.");
   657       this.abort(JPAKE_ERROR_INTERNAL);
   658       return;
   659     }
   661     try {
   662       this._newData = JSON.parse(cleartext);
   663     } catch (ex) {
   664       this._log.error("Invalid data data: " + JSON.stringify(cleartext));
   665       this.abort(JPAKE_ERROR_INVALID);
   666       return;
   667     }
   669     this._log.trace("Decrypted data.");
   670     callback();
   671   },
   673   _complete: function _complete() {
   674     this._log.debug("Exchange completed.");
   675     this._finished = true;
   676     Utils.nextTick(function () { this.controller.onComplete(this._newData); },
   677                    this);
   678   }
   680 };
   683 /**
   684  * Send credentials over an active J-PAKE channel.
   685  *
   686  * This object is designed to take over as the JPAKEClient controller,
   687  * presumably replacing one that is UI-based which would either cause
   688  * DOM objects to leak or the JPAKEClient to be GC'ed when the DOM
   689  * context disappears. This object stays alive for the duration of the
   690  * transfer by being strong-ref'ed as an nsIObserver.
   691  *
   692  * Credentials are sent after the first sync has been completed
   693  * (successfully or not.)
   694  *
   695  * Usage:
   696  *
   697  *   jpakeclient.controller = new SendCredentialsController(jpakeclient,
   698  *                                                          service);
   699  *
   700  */
   701 this.SendCredentialsController =
   702  function SendCredentialsController(jpakeclient, service) {
   703   this._log = Log.repository.getLogger("Sync.SendCredentialsController");
   704   this._log.level = Log.Level[Svc.Prefs.get("log.logger.service.main")];
   706   this._log.trace("Loading.");
   707   this.jpakeclient = jpakeclient;
   708   this.service = service;
   710   // Register ourselves as observers the first Sync finishing (either
   711   // successfully or unsuccessfully, we don't care) or for removing
   712   // this device's sync configuration, in case that happens while we
   713   // haven't finished the first sync yet.
   714   Services.obs.addObserver(this, "weave:service:sync:finish", false);
   715   Services.obs.addObserver(this, "weave:service:sync:error",  false);
   716   Services.obs.addObserver(this, "weave:service:start-over",  false);
   717 }
   718 SendCredentialsController.prototype = {
   720   unload: function unload() {
   721     this._log.trace("Unloading.");
   722     try {
   723       Services.obs.removeObserver(this, "weave:service:sync:finish");
   724       Services.obs.removeObserver(this, "weave:service:sync:error");
   725       Services.obs.removeObserver(this, "weave:service:start-over");
   726     } catch (ex) {
   727       // Ignore.
   728     }
   729   },
   731   observe: function observe(subject, topic, data) {
   732     switch (topic) {
   733       case "weave:service:sync:finish":
   734       case "weave:service:sync:error":
   735         Utils.nextTick(this.sendCredentials, this);
   736         break;
   737       case "weave:service:start-over":
   738         // This will call onAbort which will call unload().
   739         this.jpakeclient.abort();
   740         break;
   741     }
   742   },
   744   sendCredentials: function sendCredentials() {
   745     this._log.trace("Sending credentials.");
   746     let credentials = {account:   this.service.identity.account,
   747                        password:  this.service.identity.basicPassword,
   748                        synckey:   this.service.identity.syncKey,
   749                        serverURL: this.service.serverURL};
   750     this.jpakeclient.sendAndComplete(credentials);
   751   },
   753   // JPAKEClient controller API
   755   onComplete: function onComplete() {
   756     this._log.debug("Exchange was completed successfully!");
   757     this.unload();
   759     // Schedule a Sync for soonish to fetch the data uploaded by the
   760     // device with which we just paired.
   761     this.service.scheduler.scheduleNextSync(this.service.scheduler.activeInterval);
   762   },
   764   onAbort: function onAbort(error) {
   765     // It doesn't really matter why we aborted, but the channel is closed
   766     // for sure, so we won't be able to do anything with it.
   767     this._log.debug("Exchange was aborted with error: " + error);
   768     this.unload();
   769   },
   771   // Irrelevant methods for this controller:
   772   displayPIN: function displayPIN() {},
   773   onPairingStart: function onPairingStart() {},
   774   onPaired: function onPaired() {},
   775 };

mercurial