Home | History | Annotate | Download | only in cryptotoken
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview Implements a sign helper using USB gnubbies.
      7  */
      8 'use strict';
      9 
     10 var CORRUPT_sign = false;
     11 
     12 /**
     13  * @param {!GnubbyFactory} factory Factory for gnubby instances
     14  * @param {Countdown} timer Timer after whose expiration the caller is no longer
     15  *     interested in the result of a sign request.
     16  * @param {function(number, boolean)} errorCb Called when a sign request fails
     17  *     with an error code and whether any gnubbies were found.
     18  * @param {function(SignHelperChallenge, string, string=)} successCb Called with
     19  *     the signature produced by a successful sign request.
     20  * @param {string=} opt_logMsgUrl A URL to post log messages to.
     21  * @constructor
     22  * @implements {SignHelper}
     23  */
     24 function UsbSignHelper(factory, timer, errorCb, successCb, opt_logMsgUrl) {
     25   /** @private {!GnubbyFactory} */
     26   this.factory_ = factory;
     27   /** @private {Countdown} */
     28   this.timer_ = timer;
     29   /** @private {function(number, boolean)} */
     30   this.errorCb_ = errorCb;
     31   /** @private {function(SignHelperChallenge, string, string=)} */
     32   this.successCb_ = successCb;
     33   /** @private {string|undefined} */
     34   this.logMsgUrl_ = opt_logMsgUrl;
     35 
     36   /** @private {Array.<SignHelperChallenge>} */
     37   this.pendingChallenges_ = [];
     38   /** @private {Array.<usbGnubby>} */
     39   this.waitingForTouchGnubbies_ = [];
     40 
     41   /** @private {boolean} */
     42   this.notified_ = false;
     43   /** @private {boolean} */
     44   this.signerComplete_ = false;
     45 }
     46 
     47 /**
     48  * Attempts to sign the provided challenges.
     49  * @param {Array.<SignHelperChallenge>} challenges Challenges to sign
     50  * @return {boolean} whether this set of challenges was accepted.
     51  */
     52 UsbSignHelper.prototype.doSign = function(challenges) {
     53   if (!challenges.length) {
     54     // Fail a sign request with an empty set of challenges, and pretend to have
     55     // alerted the caller in case the enumerate is still pending.
     56     this.notified_ = true;
     57     return false;
     58   } else {
     59     this.pendingChallenges_ = challenges;
     60     this.getSomeGnubbies_();
     61     return true;
     62   }
     63 };
     64 
     65 /**
     66  * Enumerates gnubbies, and begins processing challenges upon enumeration if
     67  * any gnubbies are found.
     68  * @private
     69  */
     70 UsbSignHelper.prototype.getSomeGnubbies_ = function() {
     71   this.factory_.enumerate(this.enumerateCallback.bind(this));
     72 };
     73 
     74 /**
     75  * Called with the result of enumerating gnubbies.
     76  * @param {number} rc the result of the enumerate.
     77  * @param {Array.<llGnubbyDeviceId>} indexes Indexes of found gnubbies
     78  */
     79 UsbSignHelper.prototype.enumerateCallback = function(rc, indexes) {
     80   if (rc) {
     81     this.notifyError_(rc, false);
     82     return;
     83   }
     84   if (!indexes.length) {
     85     this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
     86     return;
     87   }
     88   if (this.timer_.expired()) {
     89     this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS, true);
     90     return;
     91   }
     92   this.gotSomeGnubbies_(indexes);
     93 };
     94 
     95 /**
     96  * Called with the result of enumerating gnubby indexes.
     97  * @param {Array.<llGnubbyDeviceId>} indexes Indexes of found gnubbies
     98  * @private
     99  */
    100 UsbSignHelper.prototype.gotSomeGnubbies_ = function(indexes) {
    101   /** @private {MultipleGnubbySigner} */
    102   this.signer_ = new MultipleGnubbySigner(
    103       this.factory_,
    104       indexes,
    105       false /* forEnroll */,
    106       this.signerCompleted_.bind(this),
    107       this.signerFoundGnubby_.bind(this),
    108       this.timer_,
    109       this.logMsgUrl_);
    110   this.signer_.addEncodedChallenges(this.pendingChallenges_, true);
    111 };
    112 
    113 /**
    114  * Called when a MultipleGnubbySigner completes its sign request.
    115  * @param {boolean} anySucceeded whether any sign attempt completed
    116  *     successfully.
    117  * @param {number=} errorCode an error code from a failing gnubby, if one was
    118  *     found.
    119  * @private
    120  */
    121 UsbSignHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) {
    122   this.signerComplete_ = true;
    123   // The signer is not created unless some gnubbies were enumerated, so
    124   // anyGnubbies is mostly always true. The exception is when the last gnubby is
    125   // removed, handled shortly.
    126   var anyGnubbies = true;
    127   if (!anySucceeded) {
    128     if (!errorCode) {
    129       errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
    130     } else if (errorCode == -llGnubby.GONE) {
    131       // If the last gnubby was removed, report as though no gnubbies were
    132       // found.
    133       errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
    134       anyGnubbies = false;
    135     }
    136     this.notifyError_(errorCode, anyGnubbies);
    137   } else if (this.anyTimeout_) {
    138     // Some previously succeeding gnubby timed out: return its error code.
    139     this.notifyError_(this.timeoutError_, anyGnubbies);
    140   } else {
    141     // Do nothing: signerFoundGnubby_ will have been called with each
    142     // succeeding gnubby.
    143   }
    144 };
    145 
    146 /**
    147  * Called when a MultipleGnubbySigner finds a gnubby that has successfully
    148  * signed, or can successfully sign, one of the challenges.
    149  * @param {number} code Status code
    150  * @param {MultipleSignerResult} signResult Signer result object
    151  * @private
    152  */
    153 UsbSignHelper.prototype.signerFoundGnubby_ = function(code, signResult) {
    154   var gnubby = signResult['gnubby'];
    155   var challenge = signResult['challenge'];
    156   var info = new Uint8Array(signResult['info']);
    157   if (code == DeviceStatusCodes.OK_STATUS && info.length > 0 && info[0]) {
    158     this.notifySuccess_(gnubby, challenge, info);
    159   } else {
    160     this.waitingForTouchGnubbies_.push(gnubby);
    161     this.retrySignIfNotTimedOut_(gnubby, challenge, code);
    162   }
    163 };
    164 
    165 /**
    166  * Reports the result of a successful sign operation.
    167  * @param {usbGnubby} gnubby Gnubby instance
    168  * @param {SignHelperChallenge} challenge Challenge signed
    169  * @param {Uint8Array} info Result data
    170  * @private
    171  */
    172 UsbSignHelper.prototype.notifySuccess_ = function(gnubby, challenge, info) {
    173   if (this.notified_)
    174     return;
    175   this.notified_ = true;
    176 
    177   gnubby.closeWhenIdle();
    178   this.close();
    179 
    180   if (CORRUPT_sign) {
    181     CORRUPT_sign = false;
    182     info[info.length - 1] = info[info.length - 1] ^ 0xff;
    183   }
    184   var encodedChallenge = {};
    185   encodedChallenge['challengeHash'] = B64_encode(challenge['challengeHash']);
    186   encodedChallenge['appIdHash'] = B64_encode(challenge['appIdHash']);
    187   encodedChallenge['keyHandle'] = B64_encode(challenge['keyHandle']);
    188   this.successCb_(
    189       /** @type {SignHelperChallenge} */ (encodedChallenge), B64_encode(info),
    190       'USB');
    191 };
    192 
    193 /**
    194  * Reports error to the caller.
    195  * @param {number} code error to report
    196  * @param {boolean} anyGnubbies If any gnubbies were found
    197  * @private
    198  */
    199 UsbSignHelper.prototype.notifyError_ = function(code, anyGnubbies) {
    200   if (this.notified_)
    201     return;
    202   this.notified_ = true;
    203   this.close();
    204   this.errorCb_(code, anyGnubbies);
    205 };
    206 
    207 /**
    208  * Retries signing a particular challenge on a gnubby.
    209  * @param {usbGnubby} gnubby Gnubby instance
    210  * @param {SignHelperChallenge} challenge Challenge to retry
    211  * @private
    212  */
    213 UsbSignHelper.prototype.retrySign_ = function(gnubby, challenge) {
    214   var challengeHash = challenge['challengeHash'];
    215   var appIdHash = challenge['appIdHash'];
    216   var keyHandle = challenge['keyHandle'];
    217   gnubby.sign(challengeHash, appIdHash, keyHandle,
    218       this.signCallback_.bind(this, gnubby, challenge));
    219 };
    220 
    221 /**
    222  * Called when a gnubby completes a sign request.
    223  * @param {usbGnubby} gnubby Gnubby instance
    224  * @param {SignHelperChallenge} challenge Challenge to retry
    225  * @param {number} code Previous status code
    226  * @private
    227  */
    228 UsbSignHelper.prototype.retrySignIfNotTimedOut_ =
    229     function(gnubby, challenge, code) {
    230   if (this.timer_.expired()) {
    231     // Store any timeout error code, to be returned from the complete
    232     // callback if no other eligible gnubbies are found.
    233     /** @private {boolean} */
    234     this.anyTimeout_ = true;
    235     /** @private {number} */
    236     this.timeoutError_ = code;
    237     this.removePreviouslyEligibleGnubby_(gnubby, code);
    238   } else {
    239     window.setTimeout(this.retrySign_.bind(this, gnubby, challenge), 200);
    240   }
    241 };
    242 
    243 /**
    244  * Removes a gnubby that was waiting for touch from the list, with the given
    245  * error code. If this is the last gnubby, notifies the caller of the error.
    246  * @param {usbGnubby} gnubby Gnubby instance
    247  * @param {number} code Previous status code
    248  * @private
    249  */
    250 UsbSignHelper.prototype.removePreviouslyEligibleGnubby_ =
    251     function(gnubby, code) {
    252   // Close this gnubby.
    253   gnubby.closeWhenIdle();
    254   var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
    255   if (index >= 0) {
    256     this.waitingForTouchGnubbies_.splice(index, 1);
    257   }
    258   if (!this.waitingForTouchGnubbies_.length && this.signerComplete_ &&
    259       !this.notified_) {
    260     // Last sign attempt is complete: return this error.
    261     console.log(UTIL_fmt('timeout or error (' + code.toString(16) +
    262         ') signing'));
    263     // If the last device is gone, report as if no gnubbies were found.
    264     if (code == -llGnubby.GONE) {
    265       this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
    266       return;
    267     }
    268     this.notifyError_(code, true);
    269   }
    270 };
    271 
    272 /**
    273  * Called when a gnubby completes a sign request.
    274  * @param {usbGnubby} gnubby Gnubby instance
    275  * @param {SignHelperChallenge} challenge Challenge signed
    276  * @param {number} code Status code
    277  * @param {ArrayBuffer=} infoArray Result data
    278  * @private
    279  */
    280 UsbSignHelper.prototype.signCallback_ =
    281     function(gnubby, challenge, code, infoArray) {
    282   if (this.notified_) {
    283     // Individual sign completed after previous success or failure. Disregard.
    284     return;
    285   }
    286   var info = new Uint8Array(infoArray || []);
    287   if (code == DeviceStatusCodes.OK_STATUS && info.length > 0 && info[0]) {
    288     this.notifySuccess_(gnubby, challenge, info);
    289   } else if (code == DeviceStatusCodes.OK_STATUS ||
    290       code == DeviceStatusCodes.WAIT_TOUCH_STATUS ||
    291       code == DeviceStatusCodes.BUSY_STATUS) {
    292     this.retrySignIfNotTimedOut_(gnubby, challenge, code);
    293   } else {
    294     console.log(UTIL_fmt('got error ' + code.toString(16) + ' signing'));
    295     this.removePreviouslyEligibleGnubby_(gnubby, code);
    296   }
    297 };
    298 
    299 /**
    300  * Closes the MultipleGnubbySigner, if any.
    301  */
    302 UsbSignHelper.prototype.close = function() {
    303   if (this.signer_) {
    304     this.signer_.close();
    305     this.signer_ = null;
    306   }
    307   for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
    308     this.waitingForTouchGnubbies_[i].closeWhenIdle();
    309   }
    310   this.waitingForTouchGnubbies_ = [];
    311 };
    312 
    313 /**
    314  * @param {!GnubbyFactory} gnubbyFactory Factory to create gnubbies.
    315  * @constructor
    316  * @implements {SignHelperFactory}
    317  */
    318 function UsbSignHelperFactory(gnubbyFactory) {
    319   /** @private {!GnubbyFactory} */
    320   this.gnubbyFactory_ = gnubbyFactory;
    321 }
    322 
    323 /**
    324  * @param {Countdown} timer Timer after whose expiration the caller is no longer
    325  *     interested in the result of a sign request.
    326  * @param {function(number, boolean)} errorCb Called when a sign request fails
    327  *     with an error code and whether any gnubbies were found.
    328  * @param {function(SignHelperChallenge, string)} successCb Called with the
    329  *     signature produced by a successful sign request.
    330  * @param {string=} opt_logMsgUrl A URL to post log messages to.
    331  * @return {UsbSignHelper} the newly created helper.
    332  */
    333 UsbSignHelperFactory.prototype.createHelper =
    334     function(timer, errorCb, successCb, opt_logMsgUrl) {
    335   var helper =
    336       new UsbSignHelper(this.gnubbyFactory_, timer, errorCb, successCb,
    337           opt_logMsgUrl);
    338   return helper;
    339 };
    340