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 multiple gnubby signer wraps the process of opening a number
      7  * of gnubbies, signing each challenge in an array of challenges until a
      8  * success condition is satisfied, and yielding each succeeding gnubby.
      9  *
     10  */
     11 'use strict';
     12 
     13 /**
     14  * @typedef {{
     15  *   code: number,
     16  *   gnubbyId: GnubbyDeviceId,
     17  *   challenge: (SignHelperChallenge|undefined),
     18  *   info: (ArrayBuffer|undefined)
     19  * }}
     20  */
     21 var MultipleSignerResult;
     22 
     23 /**
     24  * Creates a new sign handler that manages signing with all the available
     25  * gnubbies.
     26  * @param {boolean} forEnroll Whether this signer is signing for an attempted
     27  *     enroll operation.
     28  * @param {function(boolean)} allCompleteCb Called when this signer completes
     29  *     sign attempts, i.e. no further results will be produced. The parameter
     30  *     indicates whether any gnubbies are present that have not yet produced a
     31  *     final result.
     32  * @param {function(MultipleSignerResult, boolean)} gnubbyCompleteCb
     33  *     Called with each gnubby/challenge that yields a final result, along with
     34  *     whether this signer expects to produce more results. The boolean is a
     35  *     hint rather than a promise: it's possible for this signer to produce
     36  *     further results after saying it doesn't expect more, or to fail to
     37  *     produce further results after saying it does.
     38  * @param {number} timeoutMillis A timeout value, beyond whose expiration the
     39  *     signer will not attempt any new operations, assuming the caller is no
     40  *     longer interested in the outcome.
     41  * @param {string=} opt_logMsgUrl A URL to post log messages to.
     42  * @constructor
     43  */
     44 function MultipleGnubbySigner(forEnroll, allCompleteCb, gnubbyCompleteCb,
     45     timeoutMillis, opt_logMsgUrl) {
     46   /** @private {boolean} */
     47   this.forEnroll_ = forEnroll;
     48   /** @private {function(boolean)} */
     49   this.allCompleteCb_ = allCompleteCb;
     50   /** @private {function(MultipleSignerResult, boolean)} */
     51   this.gnubbyCompleteCb_ = gnubbyCompleteCb;
     52   /** @private {string|undefined} */
     53   this.logMsgUrl_ = opt_logMsgUrl;
     54 
     55   /** @private {Array.<SignHelperChallenge>} */
     56   this.challenges_ = [];
     57   /** @private {boolean} */
     58   this.challengesSet_ = false;
     59   /** @private {boolean} */
     60   this.complete_ = false;
     61   /** @private {number} */
     62   this.numComplete_ = 0;
     63   /** @private {!Object.<string, GnubbyTracker>} */
     64   this.gnubbies_ = {};
     65   /** @private {Countdown} */
     66   this.timer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory()
     67       .createTimer(timeoutMillis);
     68   /** @private {Countdown} */
     69   this.reenumerateTimer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory()
     70       .createTimer(timeoutMillis);
     71 }
     72 
     73 /**
     74  * @typedef {{
     75  *   index: string,
     76  *   signer: SingleGnubbySigner,
     77  *   stillGoing: boolean,
     78  *   errorStatus: number
     79  * }}
     80  */
     81 var GnubbyTracker;
     82 
     83 /**
     84  * Closes this signer's gnubbies, if any are open.
     85  */
     86 MultipleGnubbySigner.prototype.close = function() {
     87   for (var k in this.gnubbies_) {
     88     this.gnubbies_[k].signer.close();
     89   }
     90   this.reenumerateTimer_.clearTimeout();
     91   this.timer_.clearTimeout();
     92   if (this.reenumerateIntervalTimer_) {
     93     this.reenumerateIntervalTimer_.clearTimeout();
     94   }
     95 };
     96 
     97 /**
     98  * Begins signing the given challenges.
     99  * @param {Array.<SignHelperChallenge>} challenges The challenges to sign.
    100  * @return {boolean} whether the challenges were successfully added.
    101  */
    102 MultipleGnubbySigner.prototype.doSign = function(challenges) {
    103   if (this.challengesSet_) {
    104     // Can't add new challenges once they're finalized.
    105     return false;
    106   }
    107 
    108   if (challenges) {
    109     for (var i = 0; i < challenges.length; i++) {
    110       var decodedChallenge = {};
    111       var challenge = challenges[i];
    112       decodedChallenge['challengeHash'] =
    113           B64_decode(challenge['challengeHash']);
    114       decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
    115       decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
    116       if (challenge['version']) {
    117         decodedChallenge['version'] = challenge['version'];
    118       }
    119       this.challenges_.push(decodedChallenge);
    120     }
    121   }
    122   this.challengesSet_ = true;
    123   this.enumerateGnubbies_();
    124   return true;
    125 };
    126 
    127 /**
    128  * Signals this signer to rescan for gnubbies. Useful when the caller has
    129  * knowledge that the last device has been removed, and can notify this class
    130  * before it will discover it on its own.
    131  */
    132 MultipleGnubbySigner.prototype.reScanDevices = function() {
    133   if (this.reenumerateIntervalTimer_) {
    134     this.reenumerateIntervalTimer_.clearTimeout();
    135   }
    136   this.maybeReEnumerateGnubbies_(true);
    137 };
    138 
    139 /**
    140  * Enumerates gnubbies.
    141  * @private
    142  */
    143 MultipleGnubbySigner.prototype.enumerateGnubbies_ = function() {
    144   DEVICE_FACTORY_REGISTRY.getGnubbyFactory().enumerate(
    145       this.enumerateCallback_.bind(this));
    146 };
    147 
    148 /**
    149  * Called with the result of enumerating gnubbies.
    150  * @param {number} rc The return code from enumerating.
    151  * @param {Array.<GnubbyDeviceId>} ids The gnubbies enumerated.
    152  * @private
    153  */
    154 MultipleGnubbySigner.prototype.enumerateCallback_ = function(rc, ids) {
    155   if (this.complete_) {
    156     return;
    157   }
    158   if (rc || !ids || !ids.length) {
    159     this.maybeReEnumerateGnubbies_(true);
    160     return;
    161   }
    162   for (var i = 0; i < ids.length; i++) {
    163     this.addGnubby_(ids[i]);
    164   }
    165   this.maybeReEnumerateGnubbies_(false);
    166 };
    167 
    168 /**
    169  * How frequently to reenumerate gnubbies when none are found, in milliseconds.
    170  * @const
    171  */
    172 MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS = 200;
    173 
    174 /**
    175  * How frequently to reenumerate gnubbies when some are found, in milliseconds.
    176  * @const
    177  */
    178 MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS = 3000;
    179 
    180 /**
    181  * Reenumerates gnubbies if there's still time.
    182  * @param {boolean} activeScan Whether to poll more aggressively, e.g. if
    183  *     there are no devices present.
    184  * @private
    185  */
    186 MultipleGnubbySigner.prototype.maybeReEnumerateGnubbies_ =
    187     function(activeScan) {
    188   if (this.reenumerateTimer_.expired()) {
    189     // If the timer is expired, call timeout_ if there aren't any still-running
    190     // gnubbies. (If there are some still running, the last will call timeout_
    191     // itself.)
    192     if (!this.anyPending_()) {
    193       this.timeout_(false);
    194     }
    195     return;
    196   }
    197   // Reenumerate more aggressively if there are no gnubbies present than if
    198   // there are any.
    199   var reenumerateTimeoutMillis;
    200   if (activeScan) {
    201     reenumerateTimeoutMillis =
    202         MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS;
    203   } else {
    204     reenumerateTimeoutMillis =
    205         MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS;
    206   }
    207   if (reenumerateTimeoutMillis >
    208       this.reenumerateTimer_.millisecondsUntilExpired()) {
    209     reenumerateTimeoutMillis =
    210         this.reenumerateTimer_.millisecondsUntilExpired();
    211   }
    212   /** @private {Countdown} */
    213   this.reenumerateIntervalTimer_ =
    214       DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
    215           reenumerateTimeoutMillis, this.enumerateGnubbies_.bind(this));
    216 };
    217 
    218 /**
    219  * Adds a new gnubby to this signer's list of gnubbies. (Only possible while
    220  * this signer is still signing: without this restriction, the completed
    221  * callback could be called more than once, in violation of its contract.)
    222  * If this signer has challenges to sign, begins signing on the new gnubby with
    223  * them.
    224  * @param {GnubbyDeviceId} gnubbyId The id of the gnubby to add.
    225  * @return {boolean} Whether the gnubby was added successfully.
    226  * @private
    227  */
    228 MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) {
    229   var index = JSON.stringify(gnubbyId);
    230   if (this.gnubbies_.hasOwnProperty(index)) {
    231     // Can't add the same gnubby twice.
    232     return false;
    233   }
    234   var tracker = {
    235       index: index,
    236       errorStatus: 0,
    237       stillGoing: false,
    238       signer: null
    239   };
    240   tracker.signer = new SingleGnubbySigner(
    241       gnubbyId,
    242       this.forEnroll_,
    243       this.signCompletedCallback_.bind(this, tracker),
    244       this.timer_.clone(),
    245       this.logMsgUrl_);
    246   this.gnubbies_[index] = tracker;
    247   this.gnubbies_[index].stillGoing =
    248       tracker.signer.doSign(this.challenges_);
    249   if (!this.gnubbies_[index].errorStatus) {
    250     this.gnubbies_[index].errorStatus = 0;
    251   }
    252   return true;
    253 };
    254 
    255 /**
    256  * Called by a SingleGnubbySigner upon completion.
    257  * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
    258  *     this is.
    259  * @param {SingleSignerResult} result The result of the sign operation.
    260  * @private
    261  */
    262 MultipleGnubbySigner.prototype.signCompletedCallback_ =
    263     function(tracker, result) {
    264   console.log(
    265       UTIL_fmt((result.code ? 'failure.' : 'success!') +
    266           ' gnubby ' + tracker.index +
    267           ' got code ' + result.code.toString(16)));
    268   if (!tracker.stillGoing) {
    269     console.log(UTIL_fmt('gnubby ' + tracker.index + ' no longer running!'));
    270     // Shouldn't ever happen? Disregard.
    271     return;
    272   }
    273   tracker.stillGoing = false;
    274   tracker.errorStatus = result.code;
    275   var moreExpected = this.tallyCompletedGnubby_();
    276   switch (result.code) {
    277     case DeviceStatusCodes.GONE_STATUS:
    278       // Squelch removed gnubbies: the caller can't act on them. But if this
    279       // was the last one, speed up reenumerating.
    280       if (!moreExpected) {
    281         this.maybeReEnumerateGnubbies_(true);
    282       }
    283       break;
    284 
    285     default:
    286       // Report any other results directly to the caller.
    287       this.notifyGnubbyComplete_(tracker, result, moreExpected);
    288       break;
    289   }
    290   if (!moreExpected && this.timer_.expired()) {
    291     this.timeout_(false);
    292   }
    293 };
    294 
    295 /**
    296  * Counts another gnubby has having completed, and returns whether more results
    297  * are expected.
    298  * @return {boolean} Whether more gnubbies are still running.
    299  * @private
    300  */
    301 MultipleGnubbySigner.prototype.tallyCompletedGnubby_ = function() {
    302   this.numComplete_++;
    303   return this.anyPending_();
    304 };
    305 
    306 /**
    307  * @return {boolean} Whether more gnubbies are still running.
    308  * @private
    309  */
    310 MultipleGnubbySigner.prototype.anyPending_ = function() {
    311   return this.numComplete_ < Object.keys(this.gnubbies_).length;
    312 };
    313 
    314 /**
    315  * Called upon timeout.
    316  * @param {boolean} anyPending Whether any gnubbies are awaiting results.
    317  * @private
    318  */
    319 MultipleGnubbySigner.prototype.timeout_ = function(anyPending) {
    320   if (this.complete_) return;
    321   this.complete_ = true;
    322   // Defer notifying the caller that all are complete, in case the caller is
    323   // doing work in response to a gnubbyFound callback and has an inconsistent
    324   // view of the state of this signer.
    325   var self = this;
    326   window.setTimeout(function() {
    327     self.allCompleteCb_(anyPending);
    328   }, 0);
    329 };
    330 
    331 /**
    332  * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
    333  *     this is.
    334  * @param {SingleSignerResult} result Result object.
    335  * @param {boolean} moreExpected Whether more gnubbies may still produce an
    336  *     outcome.
    337  * @private
    338  */
    339 MultipleGnubbySigner.prototype.notifyGnubbyComplete_ =
    340     function(tracker, result, moreExpected) {
    341   console.log(UTIL_fmt('gnubby ' + tracker.index + ' complete (' +
    342       result.code.toString(16) + ')'));
    343   var signResult = {
    344     'code': result.code,
    345     'gnubby': result.gnubby,
    346     'gnubbyId': tracker.signer.getDeviceId()
    347   };
    348   if (result['challenge'])
    349     signResult['challenge'] = result['challenge'];
    350   if (result['info'])
    351     signResult['info'] = result['info'];
    352   this.gnubbyCompleteCb_(signResult, moreExpected);
    353 };
    354