Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4409)

Unified Diff: chrome/browser/resources/cryptotoken/usbenrollhelper.js

Issue 249913002: FIDO U2F component extension (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Merge with HEAD Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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;
+};
« no previous file with comments | « chrome/browser/resources/cryptotoken/singlesigner.js ('k') | chrome/browser/resources/cryptotoken/usbgnubbyfactory.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698