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 A single gnubby signer wraps the process of opening a gnubby,
      7  * signing each challenge in an array of challenges until a success condition
      8  * is satisfied, and finally yielding the gnubby upon success.
      9  */
     10 
     11 'use strict';
     12 
     13 /**
     14  * Creates a new sign handler with a gnubby. This handler will perform a sign
     15  * operation using each challenge in an array of challenges until its success
     16  * condition is satisified, or an error or timeout occurs. The success condition
     17  * is defined differently depending whether this signer is used for enrolling
     18  * or for signing:
     19  *
     20  * For enroll, success is defined as each challenge yielding wrong data. This
     21  * means this gnubby is not currently enrolled for any of the appIds in any
     22  * challenge.
     23  *
     24  * For sign, success is defined as any challenge yielding ok or waiting for
     25  * touch.
     26  *
     27  * At most one of the success or failure callbacks will be called, and it will
     28  * be called at most once. Neither callback is guaranteed to be called: if
     29  * a final set of challenges is never given to this gnubby, or if the gnubby
     30  * stays busy, the signer has no way to know whether the set of challenges it's
     31  * been given has succeeded or failed.
     32  * The callback is called only when the signer reaches success or failure, i.e.
     33  * when there is no need for this signer to continue trying new challenges.
     34  *
     35  * @param {!GnubbyFactory} factory Used to create and open the gnubby.
     36  * @param {llGnubbyDeviceId} gnubbyIndex Which gnubby to open.
     37  * @param {boolean} forEnroll Whether this signer is signing for an attempted
     38  *     enroll operation.
     39  * @param {function(number)} errorCb Called when this signer fails, i.e. no
     40  *     further attempts can succeed.
     41  * @param {function(usbGnubby, number, (SingleSignerResult|undefined))}
     42  *     successCb Called when this signer succeeds.
     43  * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the
     44  *     signer will not attempt any new operations, assuming the caller is no
     45  *     longer interested in the outcome.
     46  * @param {string=} opt_logMsgUrl A URL to post log messages to.
     47  * @constructor
     48  */
     49 function SingleGnubbySigner(factory, gnubbyIndex, forEnroll, errorCb, successCb,
     50     opt_timer, opt_logMsgUrl) {
     51   /** @private {GnubbyFactory} */
     52   this.factory_ = factory;
     53   /** @private {llGnubbyDeviceId} */
     54   this.gnubbyIndex_ = gnubbyIndex;
     55   /** @private {SingleGnubbySigner.State} */
     56   this.state_ = SingleGnubbySigner.State.INIT;
     57   /** @private {boolean} */
     58   this.forEnroll_ = forEnroll;
     59   /** @private {function(number)} */
     60   this.errorCb_ = errorCb;
     61   /** @private {function(usbGnubby, number, (SingleSignerResult|undefined))} */
     62   this.successCb_ = successCb;
     63   /** @private {Countdown|undefined} */
     64   this.timer_ = opt_timer;
     65   /** @private {string|undefined} */
     66   this.logMsgUrl_ = opt_logMsgUrl;
     67 
     68   /** @private {!Array.<!SignHelperChallenge>} */
     69   this.challenges_ = [];
     70   /** @private {number} */
     71   this.challengeIndex_ = 0;
     72   /** @private {boolean} */
     73   this.challengesFinal_ = false;
     74 
     75   /** @private {!Array.<string>} */
     76   this.notForMe_ = [];
     77 }
     78 
     79 /** @enum {number} */
     80 SingleGnubbySigner.State = {
     81   /** Initial state. */
     82   INIT: 0,
     83   /** The signer is attempting to open a gnubby. */
     84   OPENING: 1,
     85   /** The signer's gnubby opened, but is busy. */
     86   BUSY: 2,
     87   /** The signer has an open gnubby, but no challenges to sign. */
     88   IDLE: 3,
     89   /** The signer is currently signing a challenge. */
     90   SIGNING: 4,
     91   /** The signer encountered an error. */
     92   ERROR: 5,
     93   /** The signer got a successful outcome. */
     94   SUCCESS: 6,
     95   /** The signer is closing its gnubby. */
     96   CLOSING: 7,
     97   /** The signer is closed. */
     98   CLOSED: 8
     99 };
    100 
    101 /**
    102  * Attempts to open this signer's gnubby, if it's not already open.
    103  * (This is implicitly done by addChallenges.)
    104  */
    105 SingleGnubbySigner.prototype.open = function() {
    106   if (this.state_ == SingleGnubbySigner.State.INIT) {
    107     this.state_ = SingleGnubbySigner.State.OPENING;
    108     this.factory_.openGnubby(this.gnubbyIndex_,
    109                              this.forEnroll_,
    110                              this.openCallback_.bind(this),
    111                              this.logMsgUrl_);
    112   }
    113 };
    114 
    115 /**
    116  * Closes this signer's gnubby, if it's held.
    117  */
    118 SingleGnubbySigner.prototype.close = function() {
    119   if (!this.gnubby_) return;
    120   this.state_ = SingleGnubbySigner.State.CLOSING;
    121   this.gnubby_.closeWhenIdle(this.closed_.bind(this));
    122 };
    123 
    124 /**
    125  * Called when this signer's gnubby is closed.
    126  * @private
    127  */
    128 SingleGnubbySigner.prototype.closed_ = function() {
    129   this.gnubby_ = null;
    130   this.state_ = SingleGnubbySigner.State.CLOSED;
    131 };
    132 
    133 /**
    134  * Adds challenges to the set of challenges being tried by this signer.
    135  * If the signer is currently idle, begins signing the new challenges.
    136  *
    137  * @param {Array.<SignHelperChallenge>} challenges Sign challenges
    138  * @param {boolean} finalChallenges True if there are no more challenges to add
    139  * @return {boolean} Whether the challenges were accepted.
    140  */
    141 SingleGnubbySigner.prototype.addChallenges =
    142     function(challenges, finalChallenges) {
    143   if (this.challengesFinal_) {
    144     // Can't add new challenges once they're finalized.
    145     return false;
    146   }
    147 
    148   if (challenges) {
    149     console.log(this.gnubby_);
    150     console.log(UTIL_fmt('adding ' + challenges.length + ' challenges'));
    151     for (var i = 0; i < challenges.length; i++) {
    152       this.challenges_.push(challenges[i]);
    153     }
    154   }
    155   this.challengesFinal_ = finalChallenges;
    156 
    157   switch (this.state_) {
    158     case SingleGnubbySigner.State.INIT:
    159       this.open();
    160       break;
    161     case SingleGnubbySigner.State.OPENING:
    162       // The open has already commenced, so accept the added challenges, but
    163       // don't do anything.
    164       break;
    165     case SingleGnubbySigner.State.IDLE:
    166       if (this.challengeIndex_ < challenges.length) {
    167         // New challenges added: restart signing.
    168         this.doSign_(this.challengeIndex_);
    169       } else if (finalChallenges) {
    170         // Finalized with no new challenges can happen when the caller rejects
    171         // the appId for some challenge.
    172         // If this signer is for enroll, the request must be rejected: this
    173         // signer can't determine whether the gnubby is or is not enrolled for
    174         // the origin.
    175         // If this signer is for sign, the request must also be rejected: there
    176         // are no new challenges to sign, and all previous ones did not yield
    177         // success.
    178         var self = this;
    179         window.setTimeout(function() {
    180           self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS);
    181         }, 0);
    182       }
    183       break;
    184     case SingleGnubbySigner.State.SIGNING:
    185       // Already signing, so don't kick off a new sign, but accept the added
    186       // challenges.
    187       break;
    188     default:
    189       return false;
    190   }
    191   return true;
    192 };
    193 
    194 /**
    195  * How long to delay retrying a failed open.
    196  */
    197 SingleGnubbySigner.OPEN_DELAY_MILLIS = 200;
    198 
    199 /**
    200  * @param {number} rc The result of the open operation.
    201  * @param {usbGnubby=} gnubby The opened gnubby, if open was successful (or
    202  *     busy).
    203  * @private
    204  */
    205 SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
    206   if (this.state_ != SingleGnubbySigner.State.OPENING &&
    207       this.state_ != SingleGnubbySigner.State.BUSY) {
    208     // Open completed after close, perhaps? Ignore.
    209     return;
    210   }
    211 
    212   switch (rc) {
    213     case DeviceStatusCodes.OK_STATUS:
    214       if (!gnubby) {
    215         console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?'));
    216       } else {
    217         this.gnubby_ = gnubby;
    218         this.gnubby_.version(this.versionCallback_.bind(this));
    219       }
    220       break;
    221     case DeviceStatusCodes.BUSY_STATUS:
    222       this.gnubby_ = gnubby;
    223       this.openedBusy_ = true;
    224       this.state_ = SingleGnubbySigner.State.BUSY;
    225       // If there's still time, retry the open.
    226       if (!this.timer_ || !this.timer_.expired()) {
    227         var self = this;
    228         window.setTimeout(function() {
    229           if (self.gnubby_) {
    230             self.factory_.openGnubby(self.gnubbyIndex_,
    231                                      self.forEnroll_,
    232                                      self.openCallback_.bind(self),
    233                                      self.logMsgUrl_);
    234           }
    235         }, SingleGnubbySigner.OPEN_DELAY_MILLIS);
    236       } else {
    237         this.goToError_(DeviceStatusCodes.BUSY_STATUS);
    238       }
    239       break;
    240     default:
    241       // TODO: This won't be confused with success, but should it be
    242       // part of the same namespace as the other error codes, which are
    243       // always in DeviceStatusCodes.*?
    244       this.goToError_(rc);
    245   }
    246 };
    247 
    248 /**
    249  * Called with the result of a version command.
    250  * @param {number} rc Result of version command.
    251  * @param {ArrayBuffer=} opt_data Version.
    252  * @private
    253  */
    254 SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
    255   if (rc) {
    256     this.goToError_(rc);
    257     return;
    258   }
    259   this.state_ = SingleGnubbySigner.State.IDLE;
    260   this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || []));
    261   this.doSign_(this.challengeIndex_);
    262 };
    263 
    264 /**
    265  * @param {number} challengeIndex Index of challenge to sign
    266  * @private
    267  */
    268 SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
    269   if (this.timer_ && this.timer_.expired()) {
    270     // If the timer is expired, that means we never got a success or a touch
    271     // required response: either always implies completion of this signer's
    272     // state machine (see signCallback's cases for OK_STATUS and
    273     // WAIT_TOUCH_STATUS.) We could have gotten wrong data on a partial set of
    274     // challenges, but this means we don't yet know the final outcome. In any
    275     // event, we don't yet know the final outcome: return busy.
    276     this.goToError_(DeviceStatusCodes.BUSY_STATUS);
    277     return;
    278   }
    279 
    280   this.state_ = SingleGnubbySigner.State.SIGNING;
    281 
    282   if (challengeIndex >= this.challenges_.length) {
    283     this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
    284     return;
    285   }
    286 
    287   var challenge = this.challenges_[challengeIndex];
    288   var challengeHash = challenge.challengeHash;
    289   var appIdHash = challenge.appIdHash;
    290   var keyHandle = challenge.keyHandle;
    291   if (this.notForMe_.indexOf(keyHandle) != -1) {
    292     // Cache hit: return wrong data again.
    293     this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
    294   } else if (challenge.version && challenge.version != this.version_) {
    295     // Sign challenge for a different version of gnubby: return wrong data.
    296     this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
    297   } else {
    298     var nowink = this.forEnroll_;
    299     this.gnubby_.sign(challengeHash, appIdHash, keyHandle,
    300         this.signCallback_.bind(this, challengeIndex),
    301         nowink);
    302   }
    303 };
    304 
    305 /**
    306  * Called with the result of a single sign operation.
    307  * @param {number} challengeIndex the index of the challenge just attempted
    308  * @param {number} code the result of the sign operation
    309  * @param {ArrayBuffer=} opt_info Optional result data
    310  * @private
    311  */
    312 SingleGnubbySigner.prototype.signCallback_ =
    313     function(challengeIndex, code, opt_info) {
    314   console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyIndex_) +
    315       ', challenge ' + challengeIndex + ' yielded ' + code.toString(16)));
    316   if (this.state_ != SingleGnubbySigner.State.SIGNING) {
    317     console.log(UTIL_fmt('already done!'));
    318     // We're done, the caller's no longer interested.
    319     return;
    320   }
    321 
    322   // Cache wrong data result, re-asking the gnubby to sign it won't produce
    323   // different results.
    324   if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
    325     if (challengeIndex < this.challenges_.length) {
    326       var challenge = this.challenges_[challengeIndex];
    327       if (this.notForMe_.indexOf(challenge.keyHandle) == -1) {
    328         this.notForMe_.push(challenge.keyHandle);
    329       }
    330     }
    331   }
    332 
    333   switch (code) {
    334     case DeviceStatusCodes.GONE_STATUS:
    335       this.goToError_(code);
    336       break;
    337 
    338     case DeviceStatusCodes.TIMEOUT_STATUS:
    339       // TODO: On a TIMEOUT_STATUS, sync first, then retry.
    340     case DeviceStatusCodes.BUSY_STATUS:
    341       this.doSign_(this.challengeIndex_);
    342       break;
    343 
    344     case DeviceStatusCodes.OK_STATUS:
    345       if (this.forEnroll_) {
    346         this.goToError_(code);
    347       } else {
    348         this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info);
    349       }
    350       break;
    351 
    352     case DeviceStatusCodes.WAIT_TOUCH_STATUS:
    353       if (this.forEnroll_) {
    354         this.goToError_(code);
    355       } else {
    356         this.goToSuccess_(code, this.challenges_[challengeIndex]);
    357       }
    358       break;
    359 
    360     case DeviceStatusCodes.WRONG_DATA_STATUS:
    361       if (this.challengeIndex_ < this.challenges_.length - 1) {
    362         this.doSign_(++this.challengeIndex_);
    363       } else if (!this.challengesFinal_) {
    364         this.state_ = SingleGnubbySigner.State.IDLE;
    365       } else if (this.forEnroll_) {
    366         // Signal the caller whether the open was busy, because it may take
    367         // an unusually long time when opened for enroll. Use an empty
    368         // "challenge" as the signal for a busy open.
    369         var challenge = undefined;
    370         if (this.openedBusy) {
    371           challenge = { appIdHash: '', challengeHash: '', keyHandle: '' };
    372         }
    373         this.goToSuccess_(code, challenge);
    374       } else {
    375         this.goToError_(code);
    376       }
    377       break;
    378 
    379     default:
    380       if (this.forEnroll_) {
    381         this.goToError_(code);
    382       } else if (this.challengeIndex_ < this.challenges_.length - 1) {
    383         this.doSign_(++this.challengeIndex_);
    384       } else if (!this.challengesFinal_) {
    385         // Increment the challenge index, as this one isn't useful any longer,
    386         // but a subsequent challenge may appear, and it might be useful.
    387         this.challengeIndex_++;
    388         this.state_ = SingleGnubbySigner.State.IDLE;
    389       } else {
    390         this.goToError_(code);
    391       }
    392   }
    393 };
    394 
    395 /**
    396  * Switches to the error state, and notifies caller.
    397  * @param {number} code Error code
    398  * @private
    399  */
    400 SingleGnubbySigner.prototype.goToError_ = function(code) {
    401   this.state_ = SingleGnubbySigner.State.ERROR;
    402   console.log(UTIL_fmt('failed (' + code.toString(16) + ')'));
    403   this.errorCb_(code);
    404   // Since this gnubby can no longer produce a useful result, go ahead and
    405   // close it.
    406   this.close();
    407 };
    408 
    409 /**
    410  * Switches to the success state, and notifies caller.
    411  * @param {number} code Status code
    412  * @param {SignHelperChallenge=} opt_challenge The challenge signed
    413  * @param {ArrayBuffer=} opt_info Optional result data
    414  * @private
    415  */
    416 SingleGnubbySigner.prototype.goToSuccess_ =
    417     function(code, opt_challenge, opt_info) {
    418   this.state_ = SingleGnubbySigner.State.SUCCESS;
    419   console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
    420   if (opt_challenge || opt_info) {
    421     var singleSignerResult = {};
    422     if (opt_challenge) {
    423       singleSignerResult['challenge'] = opt_challenge;
    424     }
    425     if (opt_info) {
    426       singleSignerResult['info'] = opt_info;
    427     }
    428   }
    429   this.successCb_(this.gnubby_, code, singleSignerResult);
    430   // this.gnubby_ is now owned by successCb.
    431   this.gnubby_ = null;
    432 };
    433