diff options
Diffstat (limited to 'chromium/chrome/browser/resources/cryptotoken/signer.js')
-rw-r--r-- | chromium/chrome/browser/resources/cryptotoken/signer.js | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/cryptotoken/signer.js b/chromium/chrome/browser/resources/cryptotoken/signer.js new file mode 100644 index 00000000000..b427fcc7968 --- /dev/null +++ b/chromium/chrome/browser/resources/cryptotoken/signer.js @@ -0,0 +1,553 @@ +// 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 sign request. + * @param {!SignHelperFactory} factory Factory to create a sign helper. + * @param {MessageSender} sender The sender of the message. + * @param {Object} request The web page's sign request. + * @param {Function} sendResponse Called back with the result of the sign. + * @param {boolean} toleratesMultipleResponses Whether the sendResponse + * callback can be called more than once, e.g. for progress updates. + * @return {Closeable} Request handler that should be closed when the browser + * message channel is closed. + */ +function handleSignRequest(factory, sender, request, sendResponse, + toleratesMultipleResponses) { + var sentResponse = false; + function sendResponseOnce(r) { + if (queuedSignRequest) { + queuedSignRequest.close(); + queuedSignRequest = null; + } + if (!sentResponse) { + sentResponse = true; + try { + // If the page has gone away or the connection has otherwise gone, + // sendResponse fails. + sendResponse(r); + } catch (exception) { + console.warn('sendResponse failed: ' + exception); + } + } else { + console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME')); + } + } + + function sendErrorResponse(code) { + var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY, code); + sendResponseOnce(response); + } + + function sendSuccessResponse(challenge, info, browserData) { + var responseData = {}; + for (var k in challenge) { + responseData[k] = challenge[k]; + } + responseData['browserData'] = B64_encode(UTIL_StringToBytes(browserData)); + responseData['signatureData'] = info; + var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY, + GnubbyCodeTypes.OK, responseData); + sendResponseOnce(response); + } + + var origin = getOriginFromUrl(/** @type {string} */ (sender.url)); + if (!origin) { + sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST); + return null; + } + // More closure type inference fail. + var nonNullOrigin = /** @type {string} */ (origin); + + if (!isValidSignRequest(request)) { + sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST); + return null; + } + + var signData = request['signData']; + // A valid sign data has at least one challenge, so get the first appId from + // the first challenge. + var firstAppId = signData[0]['appId']; + var timeoutMillis = Signer.DEFAULT_TIMEOUT_MILLIS; + if (request['timeout']) { + // Request timeout is in seconds. + timeoutMillis = request['timeout'] * 1000; + } + var timer = new CountdownTimer(timeoutMillis); + 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(signData, factory, timer, + nonNullOrigin, sendErrorResponse, sendSuccessResponse, + sender.tlsChannelId, logMsgUrl); + var requestToken = signRequestQueue.queueRequest(firstAppId, nonNullOrigin, + queuedSignRequest.begin.bind(queuedSignRequest), timer); + queuedSignRequest.setToken(requestToken); + return queuedSignRequest; +} + +/** + * Returns whether the request appears to be a valid sign request. + * @param {Object} request the request. + * @return {boolean} whether the request appears valid. + */ +function isValidSignRequest(request) { + if (!request.hasOwnProperty('signData')) + return false; + var signData = request['signData']; + // If a sign request contains an empty array of challenges, it could never + // be fulfilled. Fail. + if (!signData.length) + return false; + return isValidSignData(signData); +} + +/** + * Adapter class representing a queued sign request. + * @param {!SignData} signData Signature data + * @param {!SignHelperFactory} factory Factory for SignHelper instances + * @param {Countdown} timer Timeout timer + * @param {string} origin Signature origin + * @param {function(number)} errorCb Error callback + * @param {function(SignChallenge, string, string)} successCb Success callback + * @param {string|undefined} opt_tlsChannelId TLS Channel Id + * @param {string|undefined} opt_logMsgUrl Url to post log messages to + * @constructor + * @implements {Closeable} + */ +function QueuedSignRequest(signData, factory, timer, origin, errorCb, successCb, + opt_tlsChannelId, opt_logMsgUrl) { + /** @private {!SignData} */ + this.signData_ = signData; + /** @private {!SignHelperFactory} */ + this.factory_ = factory; + /** @private {Countdown} */ + this.timer_ = timer; + /** @private {string} */ + this.origin_ = origin; + /** @private {function(number)} */ + this.errorCb_ = errorCb; + /** @private {function(SignChallenge, string, string)} */ + this.successCb_ = successCb; + /** @private {string|undefined} */ + this.tlsChannelId_ = opt_tlsChannelId; + /** @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; + if (this.begun_ && this.signer_) { + this.signer_.close(); + } + if (this.token_) { + 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) { + this.begun_ = true; + this.setToken(token); + this.signer_ = new Signer(this.factory_, this.timer_, this.origin_, + this.signerFailed_.bind(this), this.signerSucceeded_.bind(this), + this.tlsChannelId_, this.logMsgUrl_); + if (!this.signer_.setChallenges(this.signData_)) { + token.complete(); + this.errorCb_(GnubbyCodeTypes.BAD_REQUEST); + } +}; + +/** + * Called when this request's signer fails. + * @param {number} code The failure code reported by the signer. + * @private + */ +QueuedSignRequest.prototype.signerFailed_ = function(code) { + this.token_.complete(); + this.errorCb_(code); +}; + +/** + * 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 {!SignHelperFactory} helperFactory Factory to create a sign helper. + * @param {Countdown} timer Timer for sign request. + * @param {string} origin The origin making the request. + * @param {function(number)} errorCb Called when the sign operation fails. + * @param {function(SignChallenge, string, string)} successCb Called when the + * sign operation succeeds. + * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin + * making the request. + * @param {string=} opt_logMsgUrl The url to post log messages to. + * @constructor + */ +function Signer(helperFactory, timer, origin, errorCb, successCb, + opt_tlsChannelId, opt_logMsgUrl) { + /** @private {Countdown} */ + this.timer_ = timer; + /** @private {string} */ + this.origin_ = origin; + /** @private {function(number)} */ + this.errorCb_ = errorCb; + /** @private {function(SignChallenge, string, string)} */ + this.successCb_ = successCb; + /** @private {string|undefined} */ + this.tlsChannelId_ = opt_tlsChannelId; + /** @private {string|undefined} */ + this.logMsgUrl_ = opt_logMsgUrl; + + /** @private {boolean} */ + this.challengesSet_ = false; + /** @private {Array.<SignHelperChallenge>} */ + this.pendingChallenges_ = []; + /** @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.origin_ ? this.origin_.indexOf('http://') == 0 : false; + + // Protect against helper failure with a watchdog. + this.createWatchdog_(timer); + /** @private {SignHelper} */ + this.helper_ = helperFactory.createHelper( + timer, this.helperError_.bind(this), this.helperSuccess_.bind(this), + this.logMsgUrl_); +} + +/** + * Creates a timer with an expiry greater than the expiration time of the given + * timer. + * @param {Countdown} timer Timeout timer + * @private + */ +Signer.prototype.createWatchdog_ = function(timer) { + var millis = timer.millisecondsUntilExpired(); + millis += CountdownTimer.TIMER_INTERVAL_MILLIS; + /** @private {Countdown|undefined} */ + this.watchdogTimer_ = new CountdownTimer(millis, this.timeout_.bind(this)); +}; + +/** + * Default timeout value in case the caller never provides a valid timeout. + */ +Signer.DEFAULT_TIMEOUT_MILLIS = 30 * 1000; + +/** + * Sets the challenges to be signed. + * @param {SignData} signData The challenges to set. + * @return {boolean} Whether the challenges could be set. + */ +Signer.prototype.setChallenges = function(signData) { + if (this.challengesSet_ || this.done_) + return false; + /** @private {SignData} */ + this.signData_ = signData; + /** @private {boolean} */ + this.challengesSet_ = true; + + this.checkAppIds_(); + return true; +}; + +/** + * Adds new challenges to the challenges being signed. + * @param {SignData} signData Challenges to add. + * @param {boolean} finalChallenges Whether these are the final challenges. + * @return {boolean} Whether the challenge could be added. + */ +Signer.prototype.addChallenges = function(signData, finalChallenges) { + var newChallenges = this.encodeSignChallenges_(signData); + for (var i = 0; i < newChallenges.length; i++) { + this.pendingChallenges_.push(newChallenges[i]); + } + if (!finalChallenges) { + return true; + } + return this.helper_.doSign(this.pendingChallenges_); +}; + +/** + * Creates challenges for helper from challenges. + * @param {Array.<SignChallenge>} challenges Challenges to add. + * @return {Array.<SignHelperChallenge>} Encoded challenges + * @private + */ +Signer.prototype.encodeSignChallenges_ = function(challenges) { + var newChallenges = []; + for (var i = 0; i < challenges.length; i++) { + var incomingChallenge = challenges[i]; + var serverChallenge = incomingChallenge['challenge']; + var appId = incomingChallenge['appId']; + var encodedKeyHandle = incomingChallenge['keyHandle']; + var version = incomingChallenge['version']; + + var browserData = + makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_); + var encodedChallenge = makeChallenge(browserData, appId, encodedKeyHandle, + version); + + var key = encodedKeyHandle + encodedChallenge['challengeHash']; + this.browserData_[key] = browserData; + this.serverChallenges_[key] = incomingChallenge; + + newChallenges.push(encodedChallenge); + } + return newChallenges; +}; + +/** + * Checks the app ids of incoming requests, and, when this signer is enforcing + * that app ids are valid, adds successful challenges to those being signed. + * @private + */ +Signer.prototype.checkAppIds_ = function() { + // Check the incoming challenges' app ids. + /** @private {Array.<[string, Array.<Request>]>} */ + this.orderedRequests_ = requestsByAppId(this.signData_); + if (!this.orderedRequests_.length) { + // Safety check: if the challenges are somehow empty, the helper will never + // be fed any data, so the request could never be satisfied. You lose. + this.notifyError_(GnubbyCodeTypes.BAD_REQUEST); + return; + } + /** @private {number} */ + this.fetchedAppIds_ = 0; + /** @private {number} */ + this.validAppIds_ = 0; + for (var i = 0, appIdRequestsPair; i < this.orderedRequests_.length; i++) { + var appIdRequestsPair = this.orderedRequests_[i]; + var appId = appIdRequestsPair[0]; + var requests = appIdRequestsPair[1]; + if (appId == this.origin_) { + // Trivially allowed. + this.fetchedAppIds_++; + this.validAppIds_++; + this.addChallenges(requests, + this.fetchedAppIds_ == this.orderedRequests_.length); + } else { + var start = new Date(); + fetchAllowedOriginsForAppId(appId, this.allowHttp_, + this.fetchedAllowedOriginsForAppId_.bind(this, appId, start, + requests)); + } + } +}; + +/** + * Called with the result of an app id fetch. + * @param {string} appId the app id that was fetched. + * @param {Date} start the time the fetch request started. + * @param {Array.<SignChallenge>} challenges Challenges for this app id. + * @param {number} rc The HTTP response code for the app id fetch. + * @param {!Array.<string>} allowedOrigins The origins allowed for this app id. + * @private + */ +Signer.prototype.fetchedAllowedOriginsForAppId_ = function(appId, start, + challenges, rc, allowedOrigins) { + var end = new Date(); + logFetchAppIdResult(appId, end - start, allowedOrigins, this.logMsgUrl_); + if (rc != 200 && !(rc >= 400 && rc < 500)) { + if (this.timer_.expired()) { + // Act as though the helper timed out. + this.helperError_(DeviceStatusCodes.TIMEOUT_STATUS, false); + } else { + start = new Date(); + fetchAllowedOriginsForAppId(appId, this.allowHttp_, + this.fetchedAllowedOriginsForAppId_.bind(this, appId, start, + challenges)); + } + return; + } + this.fetchedAppIds_++; + var finalChallenges = (this.fetchedAppIds_ == this.orderedRequests_.length); + if (isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) { + this.validAppIds_++; + this.addChallenges(challenges, finalChallenges); + } else { + logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_); + // If this is the final request, sign the valid challenges. + if (finalChallenges) { + if (!this.helper_.doSign(this.pendingChallenges_)) { + this.notifyError_(GnubbyCodeTypes.BAD_REQUEST); + return; + } + } + } + if (finalChallenges && !this.validAppIds_) { + // If all app ids are invalid, notify the caller, otherwise implicitly + // allow the helper to report whether any of the valid challenges succeeded. + this.notifyError_(GnubbyCodeTypes.BAD_APP_ID); + } +}; + +/** + * Called when the timeout expires on this signer. + * @private + */ +Signer.prototype.timeout_ = function() { + this.watchdogTimer_ = undefined; + // The web page gets grumpy if it doesn't get WAIT_TOUCH within a reasonable + // time. + this.notifyError_(GnubbyCodeTypes.WAIT_TOUCH); +}; + +/** Closes this signer. */ +Signer.prototype.close = function() { + if (this.helper_) this.helper_.close(); +}; + +/** + * Notifies the caller of error with the given error code. + * @param {number} code Error code + * @private + */ +Signer.prototype.notifyError_ = function(code) { + if (this.done_) + return; + this.close(); + this.done_ = true; + this.errorCb_(code); +}; + +/** + * 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.close(); + this.done_ = true; + this.successCb_(challenge, info, browserData); +}; + +/** + * Maps a sign helper's error code namespace to the page's error code namespace. + * @param {number} code Error code from DeviceStatusCodes namespace. + * @param {boolean} anyGnubbies Whether any gnubbies were found. + * @return {number} A GnubbyCodeTypes error code. + * @private + */ +Signer.mapError_ = function(code, anyGnubbies) { + var reportedError; + switch (code) { + case DeviceStatusCodes.WRONG_DATA_STATUS: + reportedError = anyGnubbies ? GnubbyCodeTypes.NONE_PLUGGED_ENROLLED : + GnubbyCodeTypes.NO_GNUBBIES; + break; + + case DeviceStatusCodes.OK_STATUS: + // If the error callback is called with OK, it means the signature was + // empty, which we treat the same as... + case DeviceStatusCodes.WAIT_TOUCH_STATUS: + reportedError = GnubbyCodeTypes.WAIT_TOUCH; + break; + + case DeviceStatusCodes.BUSY_STATUS: + reportedError = GnubbyCodeTypes.BUSY; + break; + + default: + reportedError = GnubbyCodeTypes.UNKNOWN_ERROR; + break; + } + return reportedError; +}; + +/** + * Called by the helper upon error. + * @param {number} code Error code + * @param {boolean} anyGnubbies If any gnubbies were found + * @private + */ +Signer.prototype.helperError_ = function(code, anyGnubbies) { + this.clearTimeout_(); + var reportedError = Signer.mapError_(code, anyGnubbies); + console.log(UTIL_fmt('helper reported ' + code.toString(16) + + ', returning ' + reportedError)); + this.notifyError_(reportedError); +}; + +/** + * Called by helper upon success. + * @param {SignHelperChallenge} challenge The challenge that was signed. + * @param {string} info The sign result. + * @param {string=} opt_source The source, if any, if the signature. + * @private + */ +Signer.prototype.helperSuccess_ = function(challenge, info, opt_source) { + // Got a good reply, kill timer. + this.clearTimeout_(); + + if (this.logMsgUrl_ && opt_source) { + var logMsg = 'signed&source=' + opt_source; + logMessage(logMsg, this.logMsgUrl_); + } + + var key = challenge['keyHandle'] + challenge['challengeHash']; + 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, info, browserData); +}; + +/** + * Clears the timeout for this signer. + * @private + */ +Signer.prototype.clearTimeout_ = function() { + if (this.watchdogTimer_) { + this.watchdogTimer_.clearTimeout(); + this.watchdogTimer_ = undefined; + } +}; |