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 Handles web page requests for gnubby sign requests.
      7  */
      8 
      9 'use strict';
     10 
     11 var signRequestQueue = new OriginKeyedRequestQueue();
     12 
     13 /**
     14  * Handles a sign request.
     15  * @param {!SignHelperFactory} factory Factory to create a sign helper.
     16  * @param {MessageSender} sender The sender of the message.
     17  * @param {Object} request The web page's sign request.
     18  * @param {Function} sendResponse Called back with the result of the sign.
     19  * @param {boolean} toleratesMultipleResponses Whether the sendResponse
     20  *     callback can be called more than once, e.g. for progress updates.
     21  * @return {Closeable} Request handler that should be closed when the browser
     22  *     message channel is closed.
     23  */
     24 function handleSignRequest(factory, sender, request, sendResponse,
     25     toleratesMultipleResponses) {
     26   var sentResponse = false;
     27   function sendResponseOnce(r) {
     28     if (queuedSignRequest) {
     29       queuedSignRequest.close();
     30       queuedSignRequest = null;
     31     }
     32     if (!sentResponse) {
     33       sentResponse = true;
     34       try {
     35         // If the page has gone away or the connection has otherwise gone,
     36         // sendResponse fails.
     37         sendResponse(r);
     38       } catch (exception) {
     39         console.warn('sendResponse failed: ' + exception);
     40       }
     41     } else {
     42       console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
     43     }
     44   }
     45 
     46   function sendErrorResponse(code) {
     47     var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY, code);
     48     sendResponseOnce(response);
     49   }
     50 
     51   function sendSuccessResponse(challenge, info, browserData) {
     52     var responseData = {};
     53     for (var k in challenge) {
     54       responseData[k] = challenge[k];
     55     }
     56     responseData['browserData'] = B64_encode(UTIL_StringToBytes(browserData));
     57     responseData['signatureData'] = info;
     58     var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY,
     59         GnubbyCodeTypes.OK, responseData);
     60     sendResponseOnce(response);
     61   }
     62 
     63   var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
     64   if (!origin) {
     65     sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
     66     return null;
     67   }
     68   // More closure type inference fail.
     69   var nonNullOrigin = /** @type {string} */ (origin);
     70 
     71   if (!isValidSignRequest(request)) {
     72     sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
     73     return null;
     74   }
     75 
     76   var signData = request['signData'];
     77   // A valid sign data has at least one challenge, so get the first appId from
     78   // the first challenge.
     79   var firstAppId = signData[0]['appId'];
     80   var timeoutMillis = Signer.DEFAULT_TIMEOUT_MILLIS;
     81   if (request['timeout']) {
     82     // Request timeout is in seconds.
     83     timeoutMillis = request['timeout'] * 1000;
     84   }
     85   var timer = new CountdownTimer(timeoutMillis);
     86   var logMsgUrl = request['logMsgUrl'];
     87 
     88   // Queue sign requests from the same origin, to protect against simultaneous
     89   // sign-out on many tabs resulting in repeated sign-in requests.
     90   var queuedSignRequest = new QueuedSignRequest(signData, factory, timer,
     91       nonNullOrigin, sendErrorResponse, sendSuccessResponse,
     92       sender.tlsChannelId, logMsgUrl);
     93   var requestToken = signRequestQueue.queueRequest(firstAppId, nonNullOrigin,
     94       queuedSignRequest.begin.bind(queuedSignRequest), timer);
     95   queuedSignRequest.setToken(requestToken);
     96   return queuedSignRequest;
     97 }
     98 
     99 /**
    100  * Returns whether the request appears to be a valid sign request.
    101  * @param {Object} request the request.
    102  * @return {boolean} whether the request appears valid.
    103  */
    104 function isValidSignRequest(request) {
    105   if (!request.hasOwnProperty('signData'))
    106     return false;
    107   var signData = request['signData'];
    108   // If a sign request contains an empty array of challenges, it could never
    109   // be fulfilled. Fail.
    110   if (!signData.length)
    111     return false;
    112   return isValidSignData(signData);
    113 }
    114 
    115 /**
    116  * Adapter class representing a queued sign request.
    117  * @param {!SignData} signData Signature data
    118  * @param {!SignHelperFactory} factory Factory for SignHelper instances
    119  * @param {Countdown} timer Timeout timer
    120  * @param {string} origin Signature origin
    121  * @param {function(number)} errorCb Error callback
    122  * @param {function(SignChallenge, string, string)} successCb Success callback
    123  * @param {string|undefined} opt_tlsChannelId TLS Channel Id
    124  * @param {string|undefined} opt_logMsgUrl Url to post log messages to
    125  * @constructor
    126  * @implements {Closeable}
    127  */
    128 function QueuedSignRequest(signData, factory, timer, origin, errorCb, successCb,
    129     opt_tlsChannelId, opt_logMsgUrl) {
    130   /** @private {!SignData} */
    131   this.signData_ = signData;
    132   /** @private {!SignHelperFactory} */
    133   this.factory_ = factory;
    134   /** @private {Countdown} */
    135   this.timer_ = timer;
    136   /** @private {string} */
    137   this.origin_ = origin;
    138   /** @private {function(number)} */
    139   this.errorCb_ = errorCb;
    140   /** @private {function(SignChallenge, string, string)} */
    141   this.successCb_ = successCb;
    142   /** @private {string|undefined} */
    143   this.tlsChannelId_ = opt_tlsChannelId;
    144   /** @private {string|undefined} */
    145   this.logMsgUrl_ = opt_logMsgUrl;
    146   /** @private {boolean} */
    147   this.begun_ = false;
    148   /** @private {boolean} */
    149   this.closed_ = false;
    150 }
    151 
    152 /** Closes this sign request. */
    153 QueuedSignRequest.prototype.close = function() {
    154   if (this.closed_) return;
    155   if (this.begun_ && this.signer_) {
    156     this.signer_.close();
    157   }
    158   if (this.token_) {
    159     this.token_.complete();
    160   }
    161   this.closed_ = true;
    162 };
    163 
    164 /**
    165  * @param {QueuedRequestToken} token Token for this sign request.
    166  */
    167 QueuedSignRequest.prototype.setToken = function(token) {
    168   /** @private {QueuedRequestToken} */
    169   this.token_ = token;
    170 };
    171 
    172 /**
    173  * Called when this sign request may begin work.
    174  * @param {QueuedRequestToken} token Token for this sign request.
    175  */
    176 QueuedSignRequest.prototype.begin = function(token) {
    177   this.begun_ = true;
    178   this.setToken(token);
    179   this.signer_ = new Signer(this.factory_, this.timer_, this.origin_,
    180       this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
    181       this.tlsChannelId_, this.logMsgUrl_);
    182   if (!this.signer_.setChallenges(this.signData_)) {
    183     token.complete();
    184     this.errorCb_(GnubbyCodeTypes.BAD_REQUEST);
    185   }
    186 };
    187 
    188 /**
    189  * Called when this request's signer fails.
    190  * @param {number} code The failure code reported by the signer.
    191  * @private
    192  */
    193 QueuedSignRequest.prototype.signerFailed_ = function(code) {
    194   this.token_.complete();
    195   this.errorCb_(code);
    196 };
    197 
    198 /**
    199  * Called when this request's signer succeeds.
    200  * @param {SignChallenge} challenge The challenge that was signed.
    201  * @param {string} info The sign result.
    202  * @param {string} browserData Browser data JSON
    203  * @private
    204  */
    205 QueuedSignRequest.prototype.signerSucceeded_ =
    206     function(challenge, info, browserData) {
    207   this.token_.complete();
    208   this.successCb_(challenge, info, browserData);
    209 };
    210 
    211 /**
    212  * Creates an object to track signing with a gnubby.
    213  * @param {!SignHelperFactory} helperFactory Factory to create a sign helper.
    214  * @param {Countdown} timer Timer for sign request.
    215  * @param {string} origin The origin making the request.
    216  * @param {function(number)} errorCb Called when the sign operation fails.
    217  * @param {function(SignChallenge, string, string)} successCb Called when the
    218  *     sign operation succeeds.
    219  * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
    220  *     making the request.
    221  * @param {string=} opt_logMsgUrl The url to post log messages to.
    222  * @constructor
    223  */
    224 function Signer(helperFactory, timer, origin, errorCb, successCb,
    225     opt_tlsChannelId, opt_logMsgUrl) {
    226   /** @private {Countdown} */
    227   this.timer_ = timer;
    228   /** @private {string} */
    229   this.origin_ = origin;
    230   /** @private {function(number)} */
    231   this.errorCb_ = errorCb;
    232   /** @private {function(SignChallenge, string, string)} */
    233   this.successCb_ = successCb;
    234   /** @private {string|undefined} */
    235   this.tlsChannelId_ = opt_tlsChannelId;
    236   /** @private {string|undefined} */
    237   this.logMsgUrl_ = opt_logMsgUrl;
    238 
    239   /** @private {boolean} */
    240   this.challengesSet_ = false;
    241   /** @private {Array.<SignHelperChallenge>} */
    242   this.pendingChallenges_ = [];
    243   /** @private {boolean} */
    244   this.done_ = false;
    245 
    246   /** @private {Object.<string, string>} */
    247   this.browserData_ = {};
    248   /** @private {Object.<string, SignChallenge>} */
    249   this.serverChallenges_ = {};
    250   // Allow http appIds for http origins. (Broken, but the caller deserves
    251   // what they get.)
    252   /** @private {boolean} */
    253   this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
    254 
    255   // Protect against helper failure with a watchdog.
    256   this.createWatchdog_(timer);
    257   /** @private {SignHelper} */
    258   this.helper_ = helperFactory.createHelper(
    259       timer, this.helperError_.bind(this), this.helperSuccess_.bind(this),
    260           this.logMsgUrl_);
    261 }
    262 
    263 /**
    264  * Creates a timer with an expiry greater than the expiration time of the given
    265  * timer.
    266  * @param {Countdown} timer Timeout timer
    267  * @private
    268  */
    269 Signer.prototype.createWatchdog_ = function(timer) {
    270   var millis = timer.millisecondsUntilExpired();
    271   millis += CountdownTimer.TIMER_INTERVAL_MILLIS;
    272   /** @private {Countdown|undefined} */
    273   this.watchdogTimer_ = new CountdownTimer(millis, this.timeout_.bind(this));
    274 };
    275 
    276 /**
    277  * Default timeout value in case the caller never provides a valid timeout.
    278  */
    279 Signer.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
    280 
    281 /**
    282  * Sets the challenges to be signed.
    283  * @param {SignData} signData The challenges to set.
    284  * @return {boolean} Whether the challenges could be set.
    285  */
    286 Signer.prototype.setChallenges = function(signData) {
    287   if (this.challengesSet_ || this.done_)
    288     return false;
    289   /** @private {SignData} */
    290   this.signData_ = signData;
    291   /** @private {boolean} */
    292   this.challengesSet_ = true;
    293 
    294   this.checkAppIds_();
    295   return true;
    296 };
    297 
    298 /**
    299  * Adds new challenges to the challenges being signed.
    300  * @param {SignData} signData Challenges to add.
    301  * @param {boolean} finalChallenges Whether these are the final challenges.
    302  * @return {boolean} Whether the challenge could be added.
    303  */
    304 Signer.prototype.addChallenges = function(signData, finalChallenges) {
    305   var newChallenges = this.encodeSignChallenges_(signData);
    306   for (var i = 0; i < newChallenges.length; i++) {
    307     this.pendingChallenges_.push(newChallenges[i]);
    308   }
    309   if (!finalChallenges) {
    310     return true;
    311   }
    312   return this.helper_.doSign(this.pendingChallenges_);
    313 };
    314 
    315 /**
    316  * Creates challenges for helper from challenges.
    317  * @param {Array.<SignChallenge>} challenges Challenges to add.
    318  * @return {Array.<SignHelperChallenge>} Encoded challenges
    319  * @private
    320  */
    321 Signer.prototype.encodeSignChallenges_ = function(challenges) {
    322   var newChallenges = [];
    323   for (var i = 0; i < challenges.length; i++) {
    324     var incomingChallenge = challenges[i];
    325     var serverChallenge = incomingChallenge['challenge'];
    326     var appId = incomingChallenge['appId'];
    327     var encodedKeyHandle = incomingChallenge['keyHandle'];
    328     var version = incomingChallenge['version'];
    329 
    330     var browserData =
    331         makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_);
    332     var encodedChallenge = makeChallenge(browserData, appId, encodedKeyHandle,
    333         version);
    334 
    335     var key = encodedKeyHandle + encodedChallenge['challengeHash'];
    336     this.browserData_[key] = browserData;
    337     this.serverChallenges_[key] = incomingChallenge;
    338 
    339     newChallenges.push(encodedChallenge);
    340   }
    341   return newChallenges;
    342 };
    343 
    344 /**
    345  * Checks the app ids of incoming requests, and, when this signer is enforcing
    346  * that app ids are valid, adds successful challenges to those being signed.
    347  * @private
    348  */
    349 Signer.prototype.checkAppIds_ = function() {
    350   // Check the incoming challenges' app ids.
    351   /** @private {Array.<[string, Array.<Request>]>} */
    352   this.orderedRequests_ = requestsByAppId(this.signData_);
    353   if (!this.orderedRequests_.length) {
    354     // Safety check: if the challenges are somehow empty, the helper will never
    355     // be fed any data, so the request could never be satisfied. You lose.
    356     this.notifyError_(GnubbyCodeTypes.BAD_REQUEST);
    357     return;
    358   }
    359   /** @private {number} */
    360   this.fetchedAppIds_ = 0;
    361   /** @private {number} */
    362   this.validAppIds_ = 0;
    363   for (var i = 0, appIdRequestsPair; i < this.orderedRequests_.length; i++) {
    364     var appIdRequestsPair = this.orderedRequests_[i];
    365     var appId = appIdRequestsPair[0];
    366     var requests = appIdRequestsPair[1];
    367     if (appId == this.origin_) {
    368       // Trivially allowed.
    369       this.fetchedAppIds_++;
    370       this.validAppIds_++;
    371       this.addChallenges(requests,
    372           this.fetchedAppIds_ == this.orderedRequests_.length);
    373     } else {
    374       var start = new Date();
    375       fetchAllowedOriginsForAppId(appId, this.allowHttp_,
    376           this.fetchedAllowedOriginsForAppId_.bind(this, appId, start,
    377               requests));
    378     }
    379   }
    380 };
    381 
    382 /**
    383  * Called with the result of an app id fetch.
    384  * @param {string} appId the app id that was fetched.
    385  * @param {Date} start the time the fetch request started.
    386  * @param {Array.<SignChallenge>} challenges Challenges for this app id.
    387  * @param {number} rc The HTTP response code for the app id fetch.
    388  * @param {!Array.<string>} allowedOrigins The origins allowed for this app id.
    389  * @private
    390  */
    391 Signer.prototype.fetchedAllowedOriginsForAppId_ = function(appId, start,
    392     challenges, rc, allowedOrigins) {
    393   var end = new Date();
    394   logFetchAppIdResult(appId, end - start, allowedOrigins, this.logMsgUrl_);
    395   if (rc != 200 && !(rc >= 400 && rc < 500)) {
    396     if (this.timer_.expired()) {
    397       // Act as though the helper timed out.
    398       this.helperError_(DeviceStatusCodes.TIMEOUT_STATUS, false);
    399     } else {
    400       start = new Date();
    401       fetchAllowedOriginsForAppId(appId, this.allowHttp_,
    402           this.fetchedAllowedOriginsForAppId_.bind(this, appId, start,
    403               challenges));
    404     }
    405     return;
    406   }
    407   this.fetchedAppIds_++;
    408   var finalChallenges = (this.fetchedAppIds_ == this.orderedRequests_.length);
    409   if (isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) {
    410     this.validAppIds_++;
    411     this.addChallenges(challenges, finalChallenges);
    412   } else {
    413     logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_);
    414     // If this is the final request, sign the valid challenges.
    415     if (finalChallenges) {
    416       if (!this.helper_.doSign(this.pendingChallenges_)) {
    417         this.notifyError_(GnubbyCodeTypes.BAD_REQUEST);
    418         return;
    419       }
    420     }
    421   }
    422   if (finalChallenges && !this.validAppIds_) {
    423     // If all app ids are invalid, notify the caller, otherwise implicitly
    424     // allow the helper to report whether any of the valid challenges succeeded.
    425     this.notifyError_(GnubbyCodeTypes.BAD_APP_ID);
    426   }
    427 };
    428 
    429 /**
    430  * Called when the timeout expires on this signer.
    431  * @private
    432  */
    433 Signer.prototype.timeout_ = function() {
    434   this.watchdogTimer_ = undefined;
    435   // The web page gets grumpy if it doesn't get WAIT_TOUCH within a reasonable
    436   // time.
    437   this.notifyError_(GnubbyCodeTypes.WAIT_TOUCH);
    438 };
    439 
    440 /** Closes this signer. */
    441 Signer.prototype.close = function() {
    442   if (this.helper_) this.helper_.close();
    443 };
    444 
    445 /**
    446  * Notifies the caller of error with the given error code.
    447  * @param {number} code Error code
    448  * @private
    449  */
    450 Signer.prototype.notifyError_ = function(code) {
    451   if (this.done_)
    452     return;
    453   this.close();
    454   this.done_ = true;
    455   this.errorCb_(code);
    456 };
    457 
    458 /**
    459  * Notifies the caller of success.
    460  * @param {SignChallenge} challenge The challenge that was signed.
    461  * @param {string} info The sign result.
    462  * @param {string} browserData Browser data JSON
    463  * @private
    464  */
    465 Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
    466   if (this.done_)
    467     return;
    468   this.close();
    469   this.done_ = true;
    470   this.successCb_(challenge, info, browserData);
    471 };
    472 
    473 /**
    474  * Maps a sign helper's error code namespace to the page's error code namespace.
    475  * @param {number} code Error code from DeviceStatusCodes namespace.
    476  * @param {boolean} anyGnubbies Whether any gnubbies were found.
    477  * @return {number} A GnubbyCodeTypes error code.
    478  * @private
    479  */
    480 Signer.mapError_ = function(code, anyGnubbies) {
    481   var reportedError;
    482   switch (code) {
    483     case DeviceStatusCodes.WRONG_DATA_STATUS:
    484       reportedError = anyGnubbies ? GnubbyCodeTypes.NONE_PLUGGED_ENROLLED :
    485           GnubbyCodeTypes.NO_GNUBBIES;
    486       break;
    487 
    488     case DeviceStatusCodes.OK_STATUS:
    489       // If the error callback is called with OK, it means the signature was
    490       // empty, which we treat the same as...
    491     case DeviceStatusCodes.WAIT_TOUCH_STATUS:
    492       reportedError = GnubbyCodeTypes.WAIT_TOUCH;
    493       break;
    494 
    495     case DeviceStatusCodes.BUSY_STATUS:
    496       reportedError = GnubbyCodeTypes.BUSY;
    497       break;
    498 
    499     default:
    500       reportedError = GnubbyCodeTypes.UNKNOWN_ERROR;
    501       break;
    502   }
    503   return reportedError;
    504 };
    505 
    506 /**
    507  * Called by the helper upon error.
    508  * @param {number} code Error code
    509  * @param {boolean} anyGnubbies If any gnubbies were found
    510  * @private
    511  */
    512 Signer.prototype.helperError_ = function(code, anyGnubbies) {
    513   this.clearTimeout_();
    514   var reportedError = Signer.mapError_(code, anyGnubbies);
    515   console.log(UTIL_fmt('helper reported ' + code.toString(16) +
    516       ', returning ' + reportedError));
    517   this.notifyError_(reportedError);
    518 };
    519 
    520 /**
    521  * Called by helper upon success.
    522  * @param {SignHelperChallenge} challenge The challenge that was signed.
    523  * @param {string} info The sign result.
    524  * @param {string=} opt_source The source, if any, if the signature.
    525  * @private
    526  */
    527 Signer.prototype.helperSuccess_ = function(challenge, info, opt_source) {
    528   // Got a good reply, kill timer.
    529   this.clearTimeout_();
    530 
    531   if (this.logMsgUrl_ && opt_source) {
    532     var logMsg = 'signed&source=' + opt_source;
    533     logMessage(logMsg, this.logMsgUrl_);
    534   }
    535 
    536   var key = challenge['keyHandle'] + challenge['challengeHash'];
    537   var browserData = this.browserData_[key];
    538   // Notify with server-provided challenge, not the encoded one: the
    539   // server-provided challenge contains additional fields it relies on.
    540   var serverChallenge = this.serverChallenges_[key];
    541   this.notifySuccess_(serverChallenge, info, browserData);
    542 };
    543 
    544 /**
    545  * Clears the timeout for this signer.
    546  * @private
    547  */
    548 Signer.prototype.clearTimeout_ = function() {
    549   if (this.watchdogTimer_) {
    550     this.watchdogTimer_.clearTimeout();
    551     this.watchdogTimer_ = undefined;
    552   }
    553 };
    554