Index: chrome/browser/resources/cryptotoken/signer.js |
diff --git a/chrome/browser/resources/cryptotoken/signer.js b/chrome/browser/resources/cryptotoken/signer.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..bd255598c1ea2326e933bf784bdf19209ab08794 |
--- /dev/null |
+++ b/chrome/browser/resources/cryptotoken/signer.js |
@@ -0,0 +1,626 @@ |
+// Copyright (c) 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. |
+ * |
+ * @author juanlang@google.com (Juan Lang) |
+ */ |
+ |
+'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 {boolean} enforceAppIdValid Whether to enforce that the app id in the |
+ * request matches the sender's origin. |
+ * @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} |
+ */ |
+function handleSignRequest(factory, sender, request, enforceAppIdValid, |
+ 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); |
+ } |
+ |
+ function sendNotification(code) { |
+ console.log(UTIL_fmt('notification, code=' + code)); |
+ // Can the callback handle progress updates? If so, send one. |
+ if (toleratesMultipleResponses) { |
+ var response = formatWebPageResponse( |
+ GnubbyMsgTypes.SIGN_WEB_NOTIFICATION, code); |
+ if (request['requestId']) { |
+ response['requestId'] = request['requestId']; |
+ } |
+ sendResponse(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, enforceAppIdValid, sendErrorResponse, sendSuccessResponse, |
+ sendNotification, 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 |
+ * @param {!SignHelperFactory} factory |
+ * @param {Countdown} timer |
+ * @param {string} origin |
+ * @param {boolean} enforceAppIdValid |
+ * @param {function(number)} errorCb |
+ * @param {function(SignChallenge, string, string)} successCb |
+ * @param {(function(number)|undefined)} opt_progressCb |
+ * @param {string|undefined} opt_tlsChannelId |
+ * @param {string|undefined} opt_logMsgUrl |
+ * @constructor |
+ * @implements {Closeable} |
+ */ |
+function QueuedSignRequest(signData, factory, timer, origin, enforceAppIdValid, |
+ errorCb, successCb, opt_progressCb, 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 {boolean} */ |
+ this.enforceAppIdValid_ = enforceAppIdValid; |
+ /** @private {function(number)} */ |
+ this.errorCb_ = errorCb; |
+ /** @private {function(SignChallenge, string, string)} */ |
+ this.successCb_ = successCb; |
+ /** @private {(function(number)|undefined)} */ |
+ this.progressCb_ = opt_progressCb; |
+ /** @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.enforceAppIdValid_, this.signerFailed_.bind(this), |
+ this.signerSucceeded_.bind(this), this.progressCb_, |
+ 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 |
+ * @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 {boolean} enforceAppIdValid Whether to enforce that the appId in the |
+ * request matches the sender's origin. |
+ * @param {function(number)} errorCb Called when the sign operation fails. |
+ * @param {function(SignChallenge, string, string)} successCb Called when the |
+ * sign operation succeeds. |
+ * @param {(function(number)|undefined)} opt_progressCb Called with progress |
+ * updates to the sign request. |
+ * @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, enforceAppIdValid, |
+ errorCb, successCb, opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) { |
+ /** @private {Countdown} */ |
+ this.timer_ = timer; |
+ /** @private {string} */ |
+ this.origin_ = origin; |
+ /** @private {boolean} */ |
+ this.enforceAppIdValid_ = enforceAppIdValid; |
+ /** @private {function(number)} */ |
+ this.errorCb_ = errorCb; |
+ /** @private {function(SignChallenge, string, string)} */ |
+ this.successCb_ = successCb; |
+ /** @private {(function(number)|undefined)} */ |
+ this.progressCb_ = opt_progressCb; |
+ /** @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.helperProgress_.bind(this), this.logMsgUrl_); |
+} |
+ |
+/** |
+ * Creates a timer with an expiry greater than the expiration time of the given |
+ * timer. |
+ * @param {Countdown} 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; |
+ |
+ // If app id enforcing isn't in effect, go ahead and start the helper with |
+ // all of the incoming challenges. |
+ var success = true; |
+ if (!this.enforceAppIdValid_) { |
+ success = this.addChallenges(signData, true /* finalChallenges */); |
+ } |
+ |
+ this.checkAppIds_(); |
+ return success; |
+}; |
+ |
+/** |
+ * 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>} |
+ * @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_++; |
+ // Only add challenges if in enforcing mode, i.e. they weren't added |
+ // earlier. |
+ if (this.enforceAppIdValid_) { |
+ 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_++; |
+ // Only add challenges if in enforcing mode, i.e. they weren't added |
+ // earlier. |
+ if (this.enforceAppIdValid_) { |
+ this.addChallenges(challenges, finalChallenges); |
+ } |
+ } else { |
+ logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_); |
+ // If in enforcing mode and this is the final request, sign the valid |
+ // challenges. |
+ if (this.enforceAppIdValid_ && finalChallenges) { |
+ if (!this.helper_.doSign(this.pendingChallenges_)) { |
+ this.notifyError_(GnubbyCodeTypes.BAD_REQUEST); |
+ return; |
+ } |
+ } |
+ } |
+ if (this.enforceAppIdValid_ && 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 |
+ * @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 |
+ * @private |
+ */ |
+Signer.prototype.notifySuccess_ = function(challenge, info, browserData) { |
+ if (this.done_) |
+ return; |
+ this.close(); |
+ this.done_ = true; |
+ this.successCb_(challenge, info, browserData); |
+}; |
+ |
+/** |
+ * Notifies the caller of progress with the error code. |
+ * @param {number} code |
+ * @private |
+ */ |
+Signer.prototype.notifyProgress_ = function(code) { |
+ if (this.done_) |
+ return; |
+ if (code != this.lastProgressUpdate_) { |
+ this.lastProgressUpdate_ = code; |
+ // If there is no progress callback, treat it like an error and clean up. |
+ if (this.progressCb_) { |
+ this.progressCb_(code); |
+ } else { |
+ this.notifyError_(code); |
+ } |
+ } |
+}; |
+ |
+/** |
+ * 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 |
+ * @param {boolean} anyGnubbies |
+ * @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. |
+ * @private |
+ */ |
+Signer.prototype.helperSuccess_ = function(challenge, info) { |
+ // Got a good reply, kill timer. |
+ this.clearTimeout_(); |
+ |
+ 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); |
+}; |
+ |
+/** |
+ * Called by helper to notify progress. |
+ * @param {number} code |
+ * @param {boolean} anyGnubbies |
+ * @private |
+ */ |
+Signer.prototype.helperProgress_ = function(code, anyGnubbies) { |
+ var reportedError = Signer.mapError_(code, anyGnubbies); |
+ console.log(UTIL_fmt('helper notified ' + code.toString(16) + |
+ ', returning ' + reportedError)); |
+ this.notifyProgress_(reportedError); |
+}; |
+ |
+/** |
+ * Clears the timeout for this signer. |
+ * @private |
+ */ |
+Signer.prototype.clearTimeout_ = function() { |
+ if (this.watchdogTimer_) { |
+ this.watchdogTimer_.clearTimeout(); |
+ this.watchdogTimer_ = undefined; |
+ } |
+}; |