text: // Copyright 2014 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  /**
   * @fileoverview Handles web page requests for gnubby sign requests.
   *
   */
  
  'use strict';
  
  var signRequestQueue = new OriginKeyedRequestQueue();
  
  /**
   * Handles a web sign request.
   * @param {MessageSender} messageSender The message sender.
   * @param {Object} request The web page's sign request.
   * @param {Function} sendResponse Called back with the result of the sign.
   * @return {Closeable} Request handler that should be closed when the browser
   *     message channel is closed.
   */
  function handleWebSignRequest(messageSender, request, sendResponse) {
    var sentResponse = false;
    var queuedSignRequest;
  
    function sendErrorResponse(error) {
      sendResponseOnce(sentResponse, queuedSignRequest,
          makeWebErrorResponse(request,
              mapErrorCodeToGnubbyCodeType(error.errorCode, true /* forSign */)),
          sendResponse);
    }
  
    function sendSuccessResponse(challenge, info, browserData) {
      var responseData = makeWebSignResponseDataFromChallenge(challenge);
      addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
          'browserData');
      var response = makeWebSuccessResponse(request, responseData);
      sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
    }
  
    var sender = createSenderFromMessageSender(messageSender);
    if (!sender) {
      sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
      return null;
    }
    if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
      sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
      return null;
    }
  
    queuedSignRequest =
        validateAndEnqueueSignRequest(
            sender, request, 'signData', sendErrorResponse,
            sendSuccessResponse);
    return queuedSignRequest;
  }
  
  /**
   * Handles a U2F sign request.
   * @param {MessageSender} messageSender The message sender.
   * @param {Object} request The web page's sign request.
   * @param {Function} sendResponse Called back with the result of the sign.
   * @return {Closeable} Request handler that should be closed when the browser
   *     message channel is closed.
   */
  function handleU2fSignRequest(messageSender, request, sendResponse) {
    var sentResponse = false;
    var queuedSignRequest;
  
    function sendErrorResponse(error) {
      sendResponseOnce(sentResponse, queuedSignRequest,
          makeU2fErrorResponse(request, error.errorCode, error.errorMessage),
          sendResponse);
    }
  
    function sendSuccessResponse(challenge, info, browserData) {
      var responseData = makeU2fSignResponseDataFromChallenge(challenge);
      addSignatureAndBrowserDataToResponseData(responseData, info, browserData,
          'clientData');
      var response = makeU2fSuccessResponse(request, responseData);
      sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse);
    }
  
    var sender = createSenderFromMessageSender(messageSender);
    if (!sender) {
      sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
      return null;
    }
    if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) {
      sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST});
      return null;
    }
  
    queuedSignRequest =
        validateAndEnqueueSignRequest(
            sender, request, 'signRequests', sendErrorResponse,
            sendSuccessResponse);
    return queuedSignRequest;
  }
  
  /**
   * Creates a base U2F responseData object from the server challenge.
   * @param {SignChallenge} challenge The server challenge.
   * @return {Object} The responseData object.
   */
  function makeU2fSignResponseDataFromChallenge(challenge) {
    var responseData = {
      'keyHandle': challenge['keyHandle']
    };
    return responseData;
  }
  
  /**
   * Creates a base web responseData object from the server challenge.
   * @param {SignChallenge} challenge The server challenge.
   * @return {Object} The responseData object.
   */
  function makeWebSignResponseDataFromChallenge(challenge) {
    var responseData = {};
    for (var k in challenge) {
      responseData[k] = challenge[k];
    }
    return responseData;
  }
  
  /**
   * Adds the browser data and signature values to a responseData object.
   * @param {Object} responseData The "base" responseData object.
   * @param {string} signatureData The signature data.
   * @param {string} browserData The browser data generated from the challenge.
   * @param {string} browserDataName The name of the browser data key in the
   *     responseData object.
   */
  function addSignatureAndBrowserDataToResponseData(responseData, signatureData,
      browserData, browserDataName) {
    responseData[browserDataName] = B64_encode(UTIL_StringToBytes(browserData));
    responseData['signatureData'] = signatureData;
  }
  
  /**
   * Validates a sign request using the given sign challenges name, and, if valid,
   * enqueues the sign request for eventual processing.
   * @param {WebRequestSender} sender The sender of the message.
   * @param {Object} request The web page's sign request.
   * @param {string} signChallengesName The name of the sign challenges value in
   *     the request.
   * @param {function(U2fError)} errorCb Error callback.
   * @param {function(SignChallenge, string, string)} successCb Success callback.
   * @return {Closeable} Request handler that should be closed when the browser
   *     message channel is closed.
   */
  function validateAndEnqueueSignRequest(sender, request,
      signChallengesName, errorCb, successCb) {
    function timeout() {
      errorCb({errorCode: ErrorCodes.TIMEOUT});
    }
  
    if (!isValidSignRequest(request, signChallengesName)) {
      errorCb({errorCode: ErrorCodes.BAD_REQUEST});
      return null;
    }
  
    var signChallenges = request[signChallengesName];
    var appId;
    if (request['appId']) {
      appId = request['appId'];
    } else if (signChallenges.length) {
      appId = signChallenges[0]['appId'];
    }
    // Sanity check
    if (!appId) {
      console.warn(UTIL_fmt('empty sign appId?'));
      errorCb({errorCode: ErrorCodes.BAD_REQUEST});
      return null;
    }
    var timeoutValueSeconds = getTimeoutValueFromRequest(request);
    // Attenuate watchdog timeout value less than the signer's timeout, so the
    // watchdog only fires after the signer could reasonably have called back,
    // not before.
    timeoutValueSeconds = attenuateTimeoutInSeconds(timeoutValueSeconds,
        MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2);
    var watchdog = new WatchdogRequestHandler(timeoutValueSeconds, timeout);
    var wrappedErrorCb = watchdog.wrapCallback(errorCb);
    var wrappedSuccessCb = watchdog.wrapCallback(successCb);
  
    var timer = createAttenuatedTimer(
        FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds);
    var logMsgUrl = request['logMsgUrl'];
  
    // Queue sign requests from the same origin, to protect against simultaneous
    // sign-out on many tabs resulting in repeated sign-in requests.
    var queuedSignRequest = new QueuedSignRequest(signChallenges,
        timer, sender, wrappedErrorCb, wrappedSuccessCb, request['challenge'],
        appId, logMsgUrl);
    var requestToken = signRequestQueue.queueRequest(appId, sender.origin,
        queuedSignRequest.begin.bind(queuedSignRequest), timer);
    queuedSignRequest.setToken(requestToken);
  
    watchdog.setCloseable(queuedSignRequest);
    return watchdog;
  }
  
  /**
   * Returns whether the request appears to be a valid sign request.
   * @param {Object} request The request.
   * @param {string} signChallengesName The name of the sign challenges value in
   *     the request.
   * @return {boolean} Whether the request appears valid.
   */
  function isValidSignRequest(request, signChallengesName) {
    if (!request.hasOwnProperty(signChallengesName))
      return false;
    var signChallenges = request[signChallengesName];
    var hasDefaultChallenge = request.hasOwnProperty('challenge');
    var hasAppId = request.hasOwnProperty('appId');
    // If the sign challenge array is empty, the global appId is required.
    if (!hasAppId && (!signChallenges || !signChallenges.length)) {
      return false;
    }
    return isValidSignChallengeArray(signChallenges, hasDefaultChallenge,
        !hasAppId);
  }
  
  /**
   * Adapter class representing a queued sign request.
   * @param {!Array.<SignChallenge>} signChallenges The sign challenges.
   * @param {Countdown} timer Timeout timer
   * @param {WebRequestSender} sender Message sender.
   * @param {function(U2fError)} errorCb Error callback
   * @param {function(SignChallenge, string, string)} successCb Success callback
   * @param {string|undefined} opt_defaultChallenge A default sign challenge
   *     value, if a request does not provide one.
   * @param {string|undefined} opt_appId The app id for the entire request.
   * @param {string|undefined} opt_logMsgUrl Url to post log messages to
   * @constructor
   * @implements {Closeable}
   */
  function QueuedSignRequest(signChallenges, timer, sender, errorCb,
      successCb, opt_defaultChallenge, opt_appId, opt_logMsgUrl) {
    /** @private {!Array.<SignChallenge>} */
    this.signChallenges_ = signChallenges;
    /** @private {Countdown} */
    this.timer_ = timer.clone(this.close.bind(this));
    /** @private {WebRequestSender} */
    this.sender_ = sender;
    /** @private {function(U2fError)} */
    this.errorCb_ = errorCb;
    /** @private {function(SignChallenge, string, string)} */
    this.successCb_ = successCb;
    /** @private {string|undefined} */
    this.defaultChallenge_ = opt_defaultChallenge;
    /** @private {string|undefined} */
    this.appId_ = opt_appId;
    /** @private {string|undefined} */
    this.logMsgUrl_ = opt_logMsgUrl;
    /** @private {boolean} */
    this.begun_ = false;
    /** @private {boolean} */
    this.closed_ = false;
  }
  
  /** Closes this sign request. */
  QueuedSignRequest.prototype.close = function() {
    if (this.closed_) return;
    var hadBegunSigning = false;
    if (this.begun_ && this.signer_) {
      this.signer_.close();
      hadBegunSigning = true;
    }
    if (this.token_) {
      if (hadBegunSigning) {
        console.log(UTIL_fmt('closing in-progress request'));
      } else {
        console.log(UTIL_fmt('closing timed-out request before processing'));
      }
      this.token_.complete();
    }
    this.closed_ = true;
  };
  
  /**
   * @param {QueuedRequestToken} token Token for this sign request.
   */
  QueuedSignRequest.prototype.setToken = function(token) {
    /** @private {QueuedRequestToken} */
    this.token_ = token;
  };
  
  /**
   * Called when this sign request may begin work.
   * @param {QueuedRequestToken} token Token for this sign request.
   */
  QueuedSignRequest.prototype.begin = function(token) {
    if (this.timer_.expired()) {
      console.log(UTIL_fmt('Queued request begun after timeout'));
      this.close();
      this.errorCb_({errorCode: ErrorCodes.TIMEOUT});
      return;
    }
    this.begun_ = true;
    this.setToken(token);
    this.signer_ = new Signer(this.timer_, this.sender_,
        this.signerFailed_.bind(this), this.signerSucceeded_.bind(this),
        this.logMsgUrl_);
    if (!this.signer_.setChallenges(this.signChallenges_, this.defaultChallenge_,
        this.appId_)) {
      token.complete();
      this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST});
    }
    // Signer now has responsibility for maintaining timeout.
    this.timer_.clearTimeout();
  };
  
  /**
   * Called when this request's signer fails.
   * @param {U2fError} error The failure reported by the signer.
   * @private
   */
  QueuedSignRequest.prototype.signerFailed_ = function(error) {
    this.token_.complete();
    this.errorCb_(error);
  };
  
  /**
   * Called when this request's signer succeeds.
   * @param {SignChallenge} challenge The challenge that was signed.
   * @param {string} info The sign result.
   * @param {string} browserData Browser data JSON
   * @private
   */
  QueuedSignRequest.prototype.signerSucceeded_ =
      function(challenge, info, browserData) {
    this.token_.complete();
    this.successCb_(challenge, info, browserData);
  };
  
  /**
   * Creates an object to track signing with a gnubby.
   * @param {Countdown} timer Timer for sign request.
   * @param {WebRequestSender} sender The message sender.
   * @param {function(U2fError)} errorCb Called when the sign operation fails.
   * @param {function(SignChallenge, string, string)} successCb Called when the
   *     sign operation succeeds.
   * @param {string=} opt_logMsgUrl The url to post log messages to.
   * @constructor
   */
  function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) {
    /** @private {Countdown} */
    this.timer_ = timer.clone();
    /** @private {WebRequestSender} */
    this.sender_ = sender;
    /** @private {function(U2fError)} */
    this.errorCb_ = errorCb;
    /** @private {function(SignChallenge, string, string)} */
    this.successCb_ = successCb;
    /** @private {string|undefined} */
    this.logMsgUrl_ = opt_logMsgUrl;
  
    /** @private {boolean} */
    this.challengesSet_ = false;
    /** @private {boolean} */
    this.done_ = false;
  
    /** @private {Object.<string, string>} */
    this.browserData_ = {};
    /** @private {Object.<string, SignChallenge>} */
    this.serverChallenges_ = {};
    // Allow http appIds for http origins. (Broken, but the caller deserves
    // what they get.)
    /** @private {boolean} */
    this.allowHttp_ = this.sender_.origin ?
        this.sender_.origin.indexOf('http://') == 0 : false;
    /** @private {Closeable} */
    this.handler_ = null;
  }
  
  /**
   * Sets the challenges to be signed.
   * @param {Array.<SignChallenge>} signChallenges The challenges to set.
   * @param {string=} opt_defaultChallenge A default sign challenge
   *     value, if a request does not provide one.
   * @param {string=} opt_appId The app id for the entire request.
   * @return {boolean} Whether the challenges could be set.
   */
  Signer.prototype.setChallenges = function(signChallenges, opt_defaultChallenge,
      opt_appId) {
    if (this.challengesSet_ || this.done_)
      return false;
    if (this.timer_.expired()) {
      this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
      return true;
    }
    /** @private {Array.<SignChallenge>} */
    this.signChallenges_ = signChallenges;
    /** @private {string|undefined} */
    this.defaultChallenge_ = opt_defaultChallenge;
    /** @private {string|undefined} */
    this.appId_ = opt_appId;
    /** @private {boolean} */
    this.challengesSet_ = true;
  
    this.checkAppIds_();
    return true;
  };
  
  /**
   * Checks the app ids of incoming requests.
   * @private
   */
  Signer.prototype.checkAppIds_ = function() {
    var appIds = getDistinctAppIds(this.signChallenges_);
    if (this.appId_) {
      appIds = UTIL_unionArrays([this.appId_], appIds);
    }
    if (!appIds || !appIds.length) {
      var error = {
        errorCode: ErrorCodes.BAD_REQUEST,
        errorMessage: 'missing appId'
      };
      this.notifyError_(error);
      return;
    }
    FACTORY_REGISTRY.getOriginChecker()
        .canClaimAppIds(this.sender_.origin, appIds)
        .then(this.originChecked_.bind(this, appIds));
  };
  
  /**
   * Called with the result of checking the origin. When the origin is allowed
   * to claim the app ids, begins checking whether the app ids also list the
   * origin.
   * @param {!Array.<string>} appIds The app ids.
   * @param {boolean} result Whether the origin could claim the app ids.
   * @private
   */
  Signer.prototype.originChecked_ = function(appIds, result) {
    if (!result) {
      var error = {
        errorCode: ErrorCodes.BAD_REQUEST,
        errorMessage: 'bad appId'
      };
      this.notifyError_(error);
      return;
    }
    /** @private {!AppIdChecker} */
    this.appIdChecker_ = new AppIdChecker(FACTORY_REGISTRY.getTextFetcher(),
        this.timer_.clone(), this.sender_.origin,
        /** @type {!Array.<string>} */ (appIds), this.allowHttp_,
        this.logMsgUrl_);
    this.appIdChecker_.doCheck().then(this.appIdChecked_.bind(this));
  };
  
  /**
   * Called with the result of checking app ids.  When the app ids are valid,
   * adds the sign challenges to those being signed.
   * @param {boolean} result Whether the app ids are valid.
   * @private
   */
  Signer.prototype.appIdChecked_ = function(result) {
    if (!result) {
      var error = {
        errorCode: ErrorCodes.BAD_REQUEST,
        errorMessage: 'bad appId'
      };
      this.notifyError_(error);
      return;
    }
    if (!this.doSign_()) {
      this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST});
      return;
    }
  };
  
  /**
   * Begins signing this signer's challenges.
   * @return {boolean} Whether the challenge could be added.
   * @private
   */
  Signer.prototype.doSign_ = function() {
    // Create the browser data for each challenge.
    for (var i = 0; i < this.signChallenges_.length; i++) {
      var challenge = this.signChallenges_[i];
      var serverChallenge;
      if (challenge.hasOwnProperty('challenge')) {
        serverChallenge = challenge['challenge'];
      } else {
        serverChallenge = this.defaultChallenge_;
      }
      if (!serverChallenge) {
        console.warn(UTIL_fmt('challenge missing'));
        return false;
      }
      var keyHandle = challenge['keyHandle'];
  
      var browserData =
          makeSignBrowserData(serverChallenge, this.sender_.origin,
              this.sender_.tlsChannelId);
      this.browserData_[keyHandle] = browserData;
      this.serverChallenges_[keyHandle] = challenge;
    }
  
    var encodedChallenges = encodeSignChallenges(this.signChallenges_,
        this.defaultChallenge_, this.appId_, this.getChallengeHash_.bind(this));
  
    var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0;
    var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds,
        this.logMsgUrl_);
    this.handler_ =
        FACTORY_REGISTRY.getRequestHelper()
            .getHandler(/** @type {HelperRequest} */ (request));
    if (!this.handler_)
      return false;
    return this.handler_.run(this.helperComplete_.bind(this));
  };
  
  /**
   * @param {string} keyHandle The key handle used with the challenge.
   * @param {string} challenge The challenge.
   * @return {string} The hashed challenge associated with the key
   *     handle/challenge pair.
   * @private
   */
  Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) {
    return B64_encode(sha256HashOfString(this.browserData_[keyHandle]));
  };
  
  /** Closes this signer. */
  Signer.prototype.close = function() {
    this.close_();
  };
  
  /**
   * Closes this signer, and optionally notifies the caller of error.
   * @param {boolean=} opt_notifying When true, this method is being called in the
   *     process of notifying the caller of an existing status. When false,
   *     the caller is notified with a default error value, ErrorCodes.TIMEOUT.
   * @private
   */
  Signer.prototype.close_ = function(opt_notifying) {
    if (this.appIdChecker_) {
      this.appIdChecker_.close();
    }
    if (this.handler_) {
      this.handler_.close();
      this.handler_ = null;
    }
    this.timer_.clearTimeout();
    if (!opt_notifying) {
      this.notifyError_({errorCode: ErrorCodes.TIMEOUT});
    }
  };
  
  /**
   * Notifies the caller of error.
   * @param {U2fError} error Error.
   * @private
   */
  Signer.prototype.notifyError_ = function(error) {
    if (this.done_)
      return;
    this.done_ = true;
    this.close_(true);
    this.errorCb_(error);
  };
  
  /**
   * Notifies the caller of success.
   * @param {SignChallenge} challenge The challenge that was signed.
   * @param {string} info The sign result.
   * @param {string} browserData Browser data JSON
   * @private
   */
  Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
    if (this.done_)
      return;
    this.done_ = true;
    this.close_(true);
    this.successCb_(challenge, info, browserData);
  };
  
  /**
   * Called by the helper upon completion.
   * @param {HelperReply} helperReply The result of the sign request.
   * @param {string=} opt_source The source of the sign result.
   * @private
   */
  Signer.prototype.helperComplete_ = function(helperReply, opt_source) {
    if (helperReply.type != 'sign_helper_reply') {
      this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR});
      return;
    }
    var reply = /** @type {SignHelperReply} */ (helperReply);
  
    if (reply.code) {
      var reportedError = mapDeviceStatusCodeToU2fError(reply.code);
      console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) +
          ', returning ' + reportedError.errorCode));
      this.notifyError_(reportedError);
    } else {
      if (this.logMsgUrl_ && opt_source) {
        var logMsg = 'signed&source=' + opt_source;
        logMessage(logMsg, this.logMsgUrl_);
      }
  
      var key = reply.responseData['keyHandle'];
      var browserData = this.browserData_[key];
      // Notify with server-provided challenge, not the encoded one: the
      // server-provided challenge contains additional fields it relies on.
      var serverChallenge = this.serverChallenges_[key];
      this.notifySuccess_(serverChallenge, reply.responseData.signatureData,
          browserData);
    }
  };
  