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 
     10 'use strict';
     11 
     12 var signRequestQueue = new OriginKeyedRequestQueue();
     13 
     14 /**
     15  * Handles a web sign request.
     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  * @return {Closeable} Request handler that should be closed when the browser
     20  *     message channel is closed.
     21  */
     22 function handleWebSignRequest(sender, request, sendResponse) {
     23   var sentResponse = false;
     24   var queuedSignRequest;
     25 
     26   function sendErrorResponse(error) {
     27     sendResponseOnce(sentResponse, queuedSignRequest,
     28         makeWebErrorResponse(request,
     29             mapErrorCodeToGnubbyCodeType(error.errorCode, true /* forSign */)),
     30         sendResponse);
     31   }
     32 
     33   function sendSuccessResponse(challenge, info, browserData) {
     34     var responseData = makeWebSignResponseDataFromChallenge(challenge);
     35     addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
     36         'browserData');
     37     var response = makeWebSuccessResponse(request, responseData);
     38     sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
     39   }
     40 
     41   queuedSignRequest =
     42       validateAndEnqueueSignRequest(
     43           sender, request, 'signData', sendErrorResponse,
     44           sendSuccessResponse);
     45   return queuedSignRequest;
     46 }
     47 
     48 /**
     49  * Handles a U2F sign request.
     50  * @param {MessageSender} sender The sender of the message.
     51  * @param {Object} request The web page's sign request.
     52  * @param {Function} sendResponse Called back with the result of the sign.
     53  * @return {Closeable} Request handler that should be closed when the browser
     54  *     message channel is closed.
     55  */
     56 function handleU2fSignRequest(sender, request, sendResponse) {
     57   var sentResponse = false;
     58   var queuedSignRequest;
     59 
     60   function sendErrorResponse(error) {
     61     sendResponseOnce(sentResponse, queuedSignRequest,
     62         makeU2fErrorResponse(request, error.errorCode, error.errorMessage),
     63         sendResponse);
     64   }
     65 
     66   function sendSuccessResponse(challenge, info, browserData) {
     67     var responseData = makeU2fSignResponseDataFromChallenge(challenge);
     68     addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
     69         'clientData');
     70     var response = makeU2fSuccessResponse(request, responseData);
     71     sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
     72   }
     73 
     74   queuedSignRequest =
     75       validateAndEnqueueSignRequest(
     76           sender, request, 'signRequests', sendErrorResponse,
     77           sendSuccessResponse);
     78   return queuedSignRequest;
     79 }
     80 
     81 /**
     82  * Creates a base U2F responseData object from the server challenge.
     83  * @param {SignChallenge} challenge The server challenge.
     84  * @return {Object} The responseData object.
     85  */
     86 function makeU2fSignResponseDataFromChallenge(challenge) {
     87   var responseData = {
     88     'keyHandle': challenge['keyHandle']
     89   };
     90   return responseData;
     91 }
     92 
     93 /**
     94  * Creates a base web responseData object from the server challenge.
     95  * @param {SignChallenge} challenge The server challenge.
     96  * @return {Object} The responseData object.
     97  */
     98 function makeWebSignResponseDataFromChallenge(challenge) {
     99   var responseData = {};
    100   for (var k in challenge) {
    101     responseData[k] = challenge[k];
    102   }
    103   return responseData;
    104 }
    105 
    106 /**
    107  * Adds the browser data and signature values to a responseData object.
    108  * @param {Object} responseData The "base" responseData object.
    109  * @param {string} signatureData The signature data.
    110  * @param {string} browserData The browser data generated from the challenge.
    111  * @param {string} browserDataName The name of the browser data key in the
    112  *     responseData object.
    113  */
    114 function addSignatureAndBrowserDataToResponseData(responseData, signatureData,
    115     browserData, browserDataName) {
    116   responseData[browserDataName] = B64_encode(UTIL_StringToBytes(browserData));
    117   responseData['signatureData'] = signatureData;
    118 }
    119 
    120 /**
    121  * Validates a sign request using the given sign challenges name, and, if valid,
    122  * enqueues the sign request for eventual processing.
    123  * @param {MessageSender} sender The sender of the message.
    124  * @param {Object} request The web page's sign request.
    125  * @param {string} signChallengesName The name of the sign challenges value in
    126  *     the request.
    127  * @param {function(U2fError)} errorCb Error callback.
    128  * @param {function(SignChallenge, string, string)} successCb Success callback.
    129  * @return {Closeable} Request handler that should be closed when the browser
    130  *     message channel is closed.
    131  */
    132 function validateAndEnqueueSignRequest(sender, request,
    133     signChallengesName, errorCb, successCb) {
    134   var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
    135   if (!origin) {
    136     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
    137     return null;
    138   }
    139   // More closure type inference fail.
    140   var nonNullOrigin = /** @type {string} */ (origin);
    141 
    142   if (!isValidSignRequest(request, signChallengesName)) {
    143     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
    144     return null;
    145   }
    146 
    147   var signChallenges = request[signChallengesName];
    148   var appId;
    149   if (request['appId']) {
    150     appId = request['appId'];
    151   } else {
    152     // A valid sign data has at least one challenge, so get the appId from
    153     // the first challenge.
    154     appId = signChallenges[0]['appId'];
    155   }
    156   // Sanity check
    157   if (!appId) {
    158     console.warn(UTIL_fmt('empty sign appId?'));
    159     errorCb({errorCode: ErrorCodes.BAD_REQUEST});
    160     return null;
    161   }
    162   var timer = createTimerForRequest(
    163       FACTORY_REGISTRY.getCountdownFactory(), request);
    164   var logMsgUrl = request['logMsgUrl'];
    165 
    166   // Queue sign requests from the same origin, to protect against simultaneous
    167   // sign-out on many tabs resulting in repeated sign-in requests.
    168   var queuedSignRequest = new QueuedSignRequest(signChallenges,
    169       timer, nonNullOrigin, errorCb, successCb, appId, sender.tlsChannelId,
    170       logMsgUrl);
    171   var requestToken = signRequestQueue.queueRequest(appId, nonNullOrigin,
    172       queuedSignRequest.begin.bind(queuedSignRequest), timer);
    173   queuedSignRequest.setToken(requestToken);
    174   return queuedSignRequest;
    175 }
    176 
    177 /**
    178  * Returns whether the request appears to be a valid sign request.
    179  * @param {Object} request The request.
    180  * @param {string} signChallengesName The name of the sign challenges value in
    181  *     the request.
    182  * @return {boolean} Whether the request appears valid.
    183  */
    184 function isValidSignRequest(request, signChallengesName) {
    185   if (!request.hasOwnProperty(signChallengesName))
    186     return false;
    187   var signChallenges = request[signChallengesName];
    188   // If a sign request contains an empty array of challenges, it could never
    189   // be fulfilled. Fail.
    190   if (!signChallenges.length)
    191     return false;
    192   var hasAppId = request.hasOwnProperty('appId');
    193   return isValidSignChallengeArray(signChallenges, !hasAppId);
    194 }
    195 
    196 /**
    197  * Adapter class representing a queued sign request.
    198  * @param {!Array.<SignChallenge>} signChallenges The sign challenges.
    199  * @param {Countdown} timer Timeout timer
    200  * @param {string} origin Signature origin
    201  * @param {function(U2fError)} errorCb Error callback
    202  * @param {function(SignChallenge, string, string)} successCb Success callback
    203  * @param {string|undefined} opt_appId The app id for the entire request.
    204  * @param {string|undefined} opt_tlsChannelId TLS Channel Id
    205  * @param {string|undefined} opt_logMsgUrl Url to post log messages to
    206  * @constructor
    207  * @implements {Closeable}
    208  */
    209 function QueuedSignRequest(signChallenges, timer, origin, errorCb,
    210     successCb, opt_appId, opt_tlsChannelId, opt_logMsgUrl) {
    211   /** @private {!Array.<SignChallenge>} */
    212   this.signChallenges_ = signChallenges;
    213   /** @private {Countdown} */
    214   this.timer_ = timer;
    215   /** @private {string} */
    216   this.origin_ = origin;
    217   /** @private {function(U2fError)} */
    218   this.errorCb_ = errorCb;
    219   /** @private {function(SignChallenge, string, string)} */
    220   this.successCb_ = successCb;
    221   /** @private {string|undefined} */
    222   this.appId_ = opt_appId;
    223   /** @private {string|undefined} */
    224   this.tlsChannelId_ = opt_tlsChannelId;
    225   /** @private {string|undefined} */
    226   this.logMsgUrl_ = opt_logMsgUrl;
    227   /** @private {boolean} */
    228   this.begun_ = false;
    229   /** @private {boolean} */
    230   this.closed_ = false;
    231 }
    232 
    233 /** Closes this sign request. */
    234 QueuedSignRequest.prototype.close = function() {
    235   if (this.closed_) return;
    236   if (this.begun_ && this.signer_) {
    237     this.signer_.close();
    238   }
    239   if (this.token_) {
    240     this.token_.complete();
    241   }
    242   this.closed_ = true;
    243 };
    244 
    245 /**
    246  * @param {QueuedRequestToken} token Token for this sign request.
    247  */
    248 QueuedSignRequest.prototype.setToken = function(token) {
    249   /** @private {QueuedRequestToken} */
    250   this.token_ = token;
    251 };
    252 
    253 /**
    254  * Called when this sign request may begin work.
    255  * @param {QueuedRequestToken} token Token for this sign request.
    256  */
    257 QueuedSignRequest.prototype.begin = function(token) {
    258   this.begun_ = true;
    259   this.setToken(token);
    260   this.signer_ = new Signer(this.timer_, this.origin_,
    261       this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
    262       this.tlsChannelId_, this.logMsgUrl_);
    263   if (!this.signer_.setChallenges(this.signChallenges_, this.appId_)) {
    264     token.complete();
    265     this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST});
    266   }
    267 };
    268 
    269 /**
    270  * Called when this request's signer fails.
    271  * @param {U2fError} error The failure reported by the signer.
    272  * @private
    273  */
    274 QueuedSignRequest.prototype.signerFailed_ = function(error) {
    275   this.token_.complete();
    276   this.errorCb_(error);
    277 };
    278 
    279 /**
    280  * Called when this request's signer succeeds.
    281  * @param {SignChallenge} challenge The challenge that was signed.
    282  * @param {string} info The sign result.
    283  * @param {string} browserData Browser data JSON
    284  * @private
    285  */
    286 QueuedSignRequest.prototype.signerSucceeded_ =
    287     function(challenge, info, browserData) {
    288   this.token_.complete();
    289   this.successCb_(challenge, info, browserData);
    290 };
    291 
    292 /**
    293  * Creates an object to track signing with a gnubby.
    294  * @param {Countdown} timer Timer for sign request.
    295  * @param {string} origin The origin making the request.
    296  * @param {function(U2fError)} errorCb Called when the sign operation fails.
    297  * @param {function(SignChallenge, string, string)} successCb Called when the
    298  *     sign operation succeeds.
    299  * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
    300  *     making the request.
    301  * @param {string=} opt_logMsgUrl The url to post log messages to.
    302  * @constructor
    303  */
    304 function Signer(timer, origin, errorCb, successCb,
    305     opt_tlsChannelId, opt_logMsgUrl) {
    306   /** @private {Countdown} */
    307   this.timer_ = timer;
    308   /** @private {string} */
    309   this.origin_ = origin;
    310   /** @private {function(U2fError)} */
    311   this.errorCb_ = errorCb;
    312   /** @private {function(SignChallenge, string, string)} */
    313   this.successCb_ = successCb;
    314   /** @private {string|undefined} */
    315   this.tlsChannelId_ = opt_tlsChannelId;
    316   /** @private {string|undefined} */
    317   this.logMsgUrl_ = opt_logMsgUrl;
    318 
    319   /** @private {boolean} */
    320   this.challengesSet_ = false;
    321   /** @private {boolean} */
    322   this.done_ = false;
    323 
    324   /** @private {Object.<string, string>} */
    325   this.browserData_ = {};
    326   /** @private {Object.<string, SignChallenge>} */
    327   this.serverChallenges_ = {};
    328   // Allow http appIds for http origins. (Broken, but the caller deserves
    329   // what they get.)
    330   /** @private {boolean} */
    331   this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
    332   /** @private {Closeable} */
    333   this.handler_ = null;
    334 }
    335 
    336 /**
    337  * Sets the challenges to be signed.
    338  * @param {Array.<SignChallenge>} signChallenges The challenges to set.
    339  * @param {string=} opt_appId The app id for the entire request.
    340  * @return {boolean} Whether the challenges could be set.
    341  */
    342 Signer.prototype.setChallenges = function(signChallenges, opt_appId) {
    343   if (this.challengesSet_ || this.done_)
    344     return false;
    345   /** @private {Array.<SignChallenge>} */
    346   this.signChallenges_ = signChallenges;
    347   /** @private {string|undefined} */
    348   this.appId_ = opt_appId;
    349   /** @private {boolean} */
    350   this.challengesSet_ = true;
    351 
    352   this.checkAppIds_();
    353   return true;
    354 };
    355 
    356 /**
    357  * Checks the app ids of incoming requests.
    358  * @private
    359  */
    360 Signer.prototype.checkAppIds_ = function() {
    361   var appIds = getDistinctAppIds(this.signChallenges_);
    362   if (this.appId_) {
    363     appIds = UTIL_unionArrays([this.appId_], appIds);
    364   }
    365   if (!appIds || !appIds.length) {
    366     var error = {
    367       errorCode: ErrorCodes.BAD_REQUEST,
    368       errorMessage: 'missing appId'
    369     };
    370     this.notifyError_(error);
    371     return;
    372   }
    373   FACTORY_REGISTRY.getOriginChecker().canClaimAppIds(this.origin_, appIds)
    374       .then(this.originChecked_.bind(this, appIds));
    375 };
    376 
    377 /**
    378  * Called with the result of checking the origin. When the origin is allowed
    379  * to claim the app ids, begins checking whether the app ids also list the
    380  * origin.
    381  * @param {!Array.<string>} appIds The app ids.
    382  * @param {boolean} result Whether the origin could claim the app ids.
    383  * @private
    384  */
    385 Signer.prototype.originChecked_ = function(appIds, result) {
    386   if (!result) {
    387     var error = {
    388       errorCode: ErrorCodes.BAD_REQUEST,
    389       errorMessage: 'bad appId'
    390     };
    391     this.notifyError_(error);
    392     return;
    393   }
    394   /** @private {!AppIdChecker} */
    395   this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
    396       this.timer_.clone(), this.origin_,
    397       /** @type {!Array.<string>} */ (appIds), this.allowHttp_,
    398       this.logMsgUrl_);
    399   this.appIdChecker_.doCheck().then(this.appIdChecked_.bind(this));
    400 };
    401 
    402 /**
    403  * Called with the result of checking app ids.  When the app ids are valid,
    404  * adds the sign challenges to those being signed.
    405  * @param {boolean} result Whether the app ids are valid.
    406  * @private
    407  */
    408 Signer.prototype.appIdChecked_ = function(result) {
    409   if (!result) {
    410     var error = {
    411       errorCode: ErrorCodes.BAD_REQUEST,
    412       errorMessage: 'bad appId'
    413     };
    414     this.notifyError_(error);
    415     return;
    416   }
    417   if (!this.doSign_()) {
    418     this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
    419     return;
    420   }
    421 };
    422 
    423 /**
    424  * Begins signing this signer's challenges.
    425  * @return {boolean} Whether the challenge could be added.
    426  * @private
    427  */
    428 Signer.prototype.doSign_ = function() {
    429   // Create the browser data for each challenge.
    430   for (var i = 0; i < this.signChallenges_.length; i++) {
    431     var challenge = this.signChallenges_[i];
    432     var serverChallenge = challenge['challenge'];
    433     var keyHandle = challenge['keyHandle'];
    434 
    435     var browserData =
    436         makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_);
    437     this.browserData_[keyHandle] = browserData;
    438     this.serverChallenges_[keyHandle] = challenge;
    439   }
    440 
    441   var encodedChallenges = encodeSignChallenges(this.signChallenges_,
    442       this.appId_, this.getChallengeHash_.bind(this));
    443 
    444   var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
    445   var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds,
    446       this.logMsgUrl_);
    447   this.handler_ =
    448       FACTORY_REGISTRY.getRequestHelper()
    449           .getHandler(/** @type {HelperRequest} */ (request));
    450   if (!this.handler_)
    451     return false;
    452   return this.handler_.run(this.helperComplete_.bind(this));
    453 };
    454 
    455 /**
    456  * @param {string} keyHandle The key handle used with the challenge.
    457  * @param {string} challenge The challenge.
    458  * @return {string} The hashed challenge associated with the key
    459  *     handle/challenge pair.
    460  * @private
    461  */
    462 Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) {
    463   return B64_encode(sha256HashOfString(this.browserData_[keyHandle]));
    464 };
    465 
    466 /** Closes this signer. */
    467 Signer.prototype.close = function() {
    468   if (this.appIdChecker_) {
    469     this.appIdChecker_.close();
    470   }
    471   if (this.handler_) {
    472     this.handler_.close();
    473     this.handler_ = null;
    474   }
    475   this.timer_.clearTimeout();
    476 };
    477 
    478 /**
    479  * Notifies the caller of error.
    480  * @param {U2fError} error Error.
    481  * @private
    482  */
    483 Signer.prototype.notifyError_ = function(error) {
    484   if (this.done_)
    485     return;
    486   this.close();
    487   this.done_ = true;
    488   this.errorCb_(error);
    489 };
    490 
    491 /**
    492  * Notifies the caller of success.
    493  * @param {SignChallenge} challenge The challenge that was signed.
    494  * @param {string} info The sign result.
    495  * @param {string} browserData Browser data JSON
    496  * @private
    497  */
    498 Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
    499   if (this.done_)
    500     return;
    501   this.close();
    502   this.done_ = true;
    503   this.successCb_(challenge, info, browserData);
    504 };
    505 
    506 /**
    507  * Called by the helper upon completion.
    508  * @param {HelperReply} helperReply The result of the sign request.
    509  * @param {string=} opt_source The source of the sign result.
    510  * @private
    511  */
    512 Signer.prototype.helperComplete_ = function(helperReply, opt_source) {
    513   if (helperReply.type != 'sign_helper_reply') {
    514     this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
    515     return;
    516   }
    517   var reply = /** @type {SignHelperReply} */ (helperReply);
    518 
    519   if (reply.code) {
    520     var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
    521     console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
    522         ', returning ' + reportedError.errorCode));
    523     this.notifyError_(reportedError);
    524   } else {
    525     if (this.logMsgUrl_ && opt_source) {
    526       var logMsg = 'signed&source=' + opt_source;
    527       logMessage(logMsg, this.logMsgUrl_);
    528     }
    529 
    530     var key = reply.responseData['keyHandle'];
    531     var browserData = this.browserData_[key];
    532     // Notify with server-provided challenge, not the encoded one: the
    533     // server-provided challenge contains additional fields it relies on.
    534     var serverChallenge = this.serverChallenges_[key];
    535     this.notifySuccess_(serverChallenge, reply.responseData.signatureData,
    536         browserData);
    537   }
    538 };
    539