Index: chrome/browser/resources/cryptotoken/usbenrollhelper.js |
diff --git a/chrome/browser/resources/cryptotoken/usbenrollhelper.js b/chrome/browser/resources/cryptotoken/usbenrollhelper.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..63f87959ad70db916a481dd82a44f1c28974d7e8 |
--- /dev/null |
+++ b/chrome/browser/resources/cryptotoken/usbenrollhelper.js |
@@ -0,0 +1,442 @@ |
+// 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 Implements an enroll helper using USB gnubbies. |
+ * @author juanlang@google.com (Juan Lang) |
+ */ |
+'use strict'; |
+ |
+/** |
+ * @param {!GnubbyFactory} factory |
+ * @param {!Countdown} timer |
+ * @param {function(number, boolean)} errorCb Called when an enroll request |
+ * fails with an error code and whether any gnubbies were found. |
+ * @param {function(string, string)} successCb Called with the result of a |
+ * successful enroll request, along with the version of the gnubby that |
+ * provided it. |
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with |
+ * progress updates to the enroll request. |
+ * @param {string=} opt_logMsgUrl A URL to post log messages to. |
+ * @constructor |
+ * @implements {EnrollHelper} |
+ */ |
+function UsbEnrollHelper(factory, timer, errorCb, successCb, opt_progressCb, |
+ opt_logMsgUrl) { |
+ /** @private {!GnubbyFactory} */ |
+ this.factory_ = factory; |
+ /** @private {!Countdown} */ |
+ this.timer_ = timer; |
+ /** @private {function(number, boolean)} */ |
+ this.errorCb_ = errorCb; |
+ /** @private {function(string, string)} */ |
+ this.successCb_ = successCb; |
+ /** @private {(function(number, boolean)|undefined)} */ |
+ this.progressCb_ = opt_progressCb; |
+ /** @private {string|undefined} */ |
+ this.logMsgUrl_ = opt_logMsgUrl; |
+ |
+ /** @private {Array.<SignHelperChallenge>} */ |
+ this.signChallenges_ = []; |
+ /** @private {boolean} */ |
+ this.signChallengesFinal_ = false; |
+ /** @private {Array.<usbGnubby>} */ |
+ this.waitingForTouchGnubbies_ = []; |
+ |
+ /** @private {boolean} */ |
+ this.closed_ = false; |
+ /** @private {boolean} */ |
+ this.notified_ = false; |
+ /** @private {number|undefined} */ |
+ this.lastProgressUpdate_ = undefined; |
+ /** @private {boolean} */ |
+ this.signerComplete_ = false; |
+ this.getSomeGnubbies_(); |
+} |
+ |
+/** |
+ * Attempts to enroll using the provided data. |
+ * @param {Object} enrollChallenges a map of version string to enroll |
+ * challenges. |
+ * @param {Array.<SignHelperChallenge>} signChallenges a list of sign |
+ * challenges for already enrolled gnubbies, to prevent double-enrolling a |
+ * device. |
+ */ |
+UsbEnrollHelper.prototype.doEnroll = |
+ function(enrollChallenges, signChallenges) { |
+ this.enrollChallenges = enrollChallenges; |
+ this.signChallengesFinal_ = true; |
+ if (this.signer_) { |
+ this.signer_.addEncodedChallenges( |
+ signChallenges, this.signChallengesFinal_); |
+ } else { |
+ this.signChallenges_ = signChallenges; |
+ } |
+}; |
+ |
+/** Closes this helper. */ |
+UsbEnrollHelper.prototype.close = function() { |
+ this.closed_ = true; |
+ for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) { |
+ this.waitingForTouchGnubbies_[i].closeWhenIdle(); |
+ } |
+ this.waitingForTouchGnubbies_ = []; |
+ if (this.signer_) { |
+ this.signer_.close(); |
+ this.signer_ = null; |
+ } |
+}; |
+ |
+/** |
+ * Enumerates gnubbies, and begins processing challenges upon enumeration if |
+ * any gnubbies are found. |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.getSomeGnubbies_ = function() { |
+ this.factory_.enumerate(this.enumerateCallback_.bind(this)); |
+}; |
+ |
+/** |
+ * Called with the result of enumerating gnubbies. |
+ * @param {number} rc the result of the enumerate. |
+ * @param {Array.<llGnubbyDeviceId>} indexes |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.enumerateCallback_ = function(rc, indexes) { |
+ if (rc) { |
+ // Enumerate failure is rare enough that it might be worth reporting |
+ // directly, rather than trying again. |
+ this.errorCb_(rc, false); |
+ return; |
+ } |
+ if (!indexes.length) { |
+ this.maybeReEnumerateGnubbies_(); |
+ return; |
+ } |
+ if (this.timer_.expired()) { |
+ this.errorCb_(DeviceStatusCodes.TIMEOUT_STATUS, true); |
+ return; |
+ } |
+ this.gotSomeGnubbies_(indexes); |
+}; |
+ |
+/** |
+ * If there's still time, re-enumerates devices and try with them. Otherwise |
+ * reports an error and, implicitly, stops the enroll operation. |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.maybeReEnumerateGnubbies_ = function() { |
+ var errorCode = DeviceStatusCodes.WRONG_DATA_STATUS; |
+ var anyGnubbies = false; |
+ // If there's still time and we're still going, retry enumerating. |
+ if (!this.closed_ && !this.timer_.expired()) { |
+ this.notifyProgress_(errorCode, anyGnubbies); |
+ var self = this; |
+ // Use a delayed re-enumerate to prevent hammering the system unnecessarily. |
+ window.setTimeout(function() { |
+ if (self.timer_.expired()) { |
+ self.notifyError_(errorCode, anyGnubbies); |
+ } else { |
+ self.getSomeGnubbies_(); |
+ } |
+ }, 200); |
+ } else { |
+ this.notifyError_(errorCode, anyGnubbies); |
+ } |
+}; |
+ |
+/** |
+ * Called with the result of enumerating gnubby indexes. |
+ * @param {Array.<llGnubbyDeviceId>} indexes |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.gotSomeGnubbies_ = function(indexes) { |
+ this.signer_ = new MultipleGnubbySigner( |
+ this.factory_, |
+ indexes, |
+ true /* forEnroll */, |
+ this.signerCompleted_.bind(this), |
+ this.signerFoundGnubby_.bind(this), |
+ this.timer_, |
+ this.logMsgUrl_); |
+ if (this.signChallengesFinal_) { |
+ this.signer_.addEncodedChallenges( |
+ this.signChallenges_, this.signChallengesFinal_); |
+ this.pendingSignChallenges_ = []; |
+ } |
+}; |
+ |
+/** |
+ * Called when a MultipleGnubbySigner completes its sign request. |
+ * @param {boolean} anySucceeded whether any sign attempt completed |
+ * successfully. |
+ * @param {number=} errorCode an error code from a failing gnubby, if one was |
+ * found. |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) { |
+ this.signerComplete_ = true; |
+ // The signer is not created unless some gnubbies were enumerated, so |
+ // anyGnubbies is mostly always true. The exception is when the last gnubby is |
+ // removed, handled shortly. |
+ var anyGnubbies = true; |
+ if (!anySucceeded) { |
+ if (errorCode == -llGnubby.GONE) { |
+ // If the last gnubby was removed, report as though no gnubbies were |
+ // found. |
+ this.maybeReEnumerateGnubbies_(); |
+ } else { |
+ if (!errorCode) errorCode = DeviceStatusCodes.WRONG_DATA_STATUS; |
+ this.notifyError_(errorCode, anyGnubbies); |
+ } |
+ } else if (this.anyTimeout) { |
+ // Some previously succeeding gnubby timed out: return its error code. |
+ this.notifyError_(this.timeoutError, anyGnubbies); |
+ } else { |
+ // Do nothing: signerFoundGnubby will have been called with each succeeding |
+ // gnubby. |
+ } |
+}; |
+ |
+/** |
+ * Called when a MultipleGnubbySigner finds a gnubby that can enroll. |
+ * @param {number} code |
+ * @param {MultipleSignerResult} signResult |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.signerFoundGnubby_ = function(code, signResult) { |
+ var gnubby = signResult['gnubby']; |
+ this.waitingForTouchGnubbies_.push(gnubby); |
+ this.notifyProgress_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true); |
+ if (code == DeviceStatusCodes.WRONG_DATA_STATUS) { |
+ if (signResult['challenge']) { |
+ // If the signer yielded a busy open, indicate waiting for touch |
+ // immediately, rather than attempting enroll. This allows the UI to |
+ // update, since a busy open is a potentially long operation. |
+ this.notifyError_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true); |
+ } else { |
+ this.matchEnrollVersionToGnubby_(gnubby); |
+ } |
+ } |
+}; |
+ |
+/** |
+ * Attempts to match the gnubby's U2F version with an appropriate enroll |
+ * challenge. |
+ * @param {usbGnubby} gnubby |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.matchEnrollVersionToGnubby_ = function(gnubby) { |
+ if (!gnubby) { |
+ console.warn(UTIL_fmt('no gnubby, WTF?')); |
+ } |
+ gnubby.version(this.gnubbyVersioned_.bind(this, gnubby)); |
+}; |
+ |
+/** |
+ * Called with the result of a version command. |
+ * @param {usbGnubby} gnubby |
+ * @param {number} rc result of version command. |
+ * @param {ArrayBuffer=} data version. |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.gnubbyVersioned_ = function(gnubby, rc, data) { |
+ if (rc) { |
+ this.removeWrongVersionGnubby_(gnubby); |
+ return; |
+ } |
+ var version = UTIL_BytesToString(new Uint8Array(data || null)); |
+ this.tryEnroll_(gnubby, version); |
+}; |
+ |
+/** |
+ * Drops the gnubby from the list of eligible gnubbies. |
+ * @param {usbGnubby} gnubby |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.removeWaitingGnubby_ = function(gnubby) { |
+ gnubby.closeWhenIdle(); |
+ var index = this.waitingForTouchGnubbies_.indexOf(gnubby); |
+ if (index >= 0) { |
+ this.waitingForTouchGnubbies_.splice(index, 1); |
+ } |
+}; |
+ |
+/** |
+ * Drops the gnubby from the list of eligible gnubbies, as it has the wrong |
+ * version. |
+ * @param {usbGnubby} gnubby |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.removeWrongVersionGnubby_ = function(gnubby) { |
+ this.removeWaitingGnubby_(gnubby); |
+ if (!this.waitingForTouchGnubbies_.length && this.signerComplete_) { |
+ // Whoops, this was the last gnubby: indicate there are none. |
+ this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false); |
+ } |
+}; |
+ |
+/** |
+ * Attempts enrolling a particular gnubby with a challenge of the appropriate |
+ * version. |
+ * @param {usbGnubby} gnubby |
+ * @param {string} version |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.tryEnroll_ = function(gnubby, version) { |
+ var challenge = this.getChallengeOfVersion_(version); |
+ if (!challenge) { |
+ this.removeWrongVersionGnubby_(gnubby); |
+ return; |
+ } |
+ var challengeChallenge = B64_decode(challenge['challenge']); |
+ var appIdHash = B64_decode(challenge['appIdHash']); |
+ gnubby.enroll(challengeChallenge, appIdHash, |
+ this.enrollCallback_.bind(this, gnubby, version)); |
+}; |
+ |
+/** |
+ * Finds the (first) challenge of the given version in this helper's challenges. |
+ * @param {string} version |
+ * @return {Object} challenge, if found, or null if not. |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.getChallengeOfVersion_ = function(version) { |
+ for (var i = 0; i < this.enrollChallenges.length; i++) { |
+ if (this.enrollChallenges[i]['version'] == version) { |
+ return this.enrollChallenges[i]; |
+ } |
+ } |
+ return null; |
+}; |
+ |
+/** |
+ * Called with the result of an enroll request to a gnubby. |
+ * @param {usbGnubby} gnubby |
+ * @param {string} version |
+ * @param {number} code |
+ * @param {ArrayBuffer=} infoArray |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.enrollCallback_ = |
+ function(gnubby, version, code, infoArray) { |
+ if (this.notified_) { |
+ // Enroll completed after previous success or failure. Disregard. |
+ return; |
+ } |
+ switch (code) { |
+ case -llGnubby.GONE: |
+ // Close this gnubby. |
+ this.removeWaitingGnubby_(gnubby); |
+ if (!this.waitingForTouchGnubbies_.length) { |
+ // Last enroll attempt is complete and last gnubby is gone: retry if |
+ // possible. |
+ this.maybeReEnumerateGnubbies_(); |
+ } |
+ break; |
+ |
+ case DeviceStatusCodes.WAIT_TOUCH_STATUS: |
+ case DeviceStatusCodes.BUSY_STATUS: |
+ case DeviceStatusCodes.TIMEOUT_STATUS: |
+ if (this.timer_.expired()) { |
+ // Store any timeout error code, to be returned from the complete |
+ // callback if no other eligible gnubbies are found. |
+ this.anyTimeout = true; |
+ this.timeoutError = code; |
+ // Close this gnubby. |
+ this.removeWaitingGnubby_(gnubby); |
+ if (!this.waitingForTouchGnubbies_.length && !this.notified_) { |
+ // Last enroll attempt is complete: return this error. |
+ console.log(UTIL_fmt('timeout (' + code.toString(16) + |
+ ') enrolling')); |
+ this.notifyError_(code, true); |
+ } |
+ } else { |
+ // Notify caller of waiting for touch events. |
+ if (code == DeviceStatusCodes.WAIT_TOUCH_STATUS) { |
+ this.notifyProgress_(code, true); |
+ } |
+ window.setTimeout(this.tryEnroll_.bind(this, gnubby, version), 200); |
+ } |
+ break; |
+ |
+ case DeviceStatusCodes.OK_STATUS: |
+ var info = B64_encode(new Uint8Array(infoArray || [])); |
+ this.notifySuccess_(version, info); |
+ break; |
+ |
+ default: |
+ console.log(UTIL_fmt('Failed to enroll gnubby: ' + code)); |
+ this.notifyError_(code, true); |
+ break; |
+ } |
+}; |
+ |
+/** |
+ * @param {number} code |
+ * @param {boolean} anyGnubbies |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.notifyError_ = function(code, anyGnubbies) { |
+ if (this.notified_ || this.closed_) |
+ return; |
+ this.notified_ = true; |
+ this.close(); |
+ this.errorCb_(code, anyGnubbies); |
+}; |
+ |
+/** |
+ * @param {string} version |
+ * @param {string} info |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.notifySuccess_ = function(version, info) { |
+ if (this.notified_ || this.closed_) |
+ return; |
+ this.notified_ = true; |
+ this.close(); |
+ this.successCb_(version, info); |
+}; |
+ |
+/** |
+ * @param {number} code |
+ * @param {boolean} anyGnubbies |
+ * @private |
+ */ |
+UsbEnrollHelper.prototype.notifyProgress_ = function(code, anyGnubbies) { |
+ if (this.lastProgressUpdate_ == code || this.notified_ || this.closed_) |
+ return; |
+ this.lastProgressUpdate_ = code; |
+ if (this.progressCb_) this.progressCb_(code, anyGnubbies); |
+}; |
+ |
+/** |
+ * @param {!GnubbyFactory} gnubbyFactory factory to create gnubbies. |
+ * @constructor |
+ * @implements {EnrollHelperFactory} |
+ */ |
+function UsbEnrollHelperFactory(gnubbyFactory) { |
+ /** @private {!GnubbyFactory} */ |
+ this.gnubbyFactory_ = gnubbyFactory; |
+} |
+ |
+/** |
+ * @param {!Countdown} timer |
+ * @param {function(number, boolean)} errorCb Called when an enroll request |
+ * fails with an error code and whether any gnubbies were found. |
+ * @param {function(string, string)} successCb Called with the result of a |
+ * successful enroll request, along with the version of the gnubby that |
+ * provided it. |
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with |
+ * progress updates to the enroll request. |
+ * @param {string=} opt_logMsgUrl A URL to post log messages to. |
+ * @return {UsbEnrollHelper} the newly created helper. |
+ */ |
+UsbEnrollHelperFactory.prototype.createHelper = |
+ function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) { |
+ var helper = |
+ new UsbEnrollHelper(this.gnubbyFactory_, timer, errorCb, successCb, |
+ opt_progressCb, opt_logMsgUrl); |
+ return helper; |
+}; |