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

Unified Diff: chrome/browser/resources/cryptotoken/singlesigner.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/singlesigner.js
diff --git a/chrome/browser/resources/cryptotoken/singlesigner.js b/chrome/browser/resources/cryptotoken/singlesigner.js
new file mode 100644
index 0000000000000000000000000000000000000000..895cd6185da79fc9bc35bc8c23fac09df012a8ad
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/singlesigner.js
@@ -0,0 +1,434 @@
+// 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 A single gnubby signer wraps the process of opening a gnubby,
+ * signing each challenge in an array of challenges until a success condition
+ * is satisfied, and finally yielding the gnubby upon success.
+ *
+ * @author juanlang@google.com (Juan Lang)
+ */
+
+'use strict';
+
+/**
+ * Creates a new sign handler with a gnubby. This handler will perform a sign
+ * operation using each challenge in an array of challenges until its success
+ * condition is satisified, or an error or timeout occurs. The success condition
+ * is defined differently depending whether this signer is used for enrolling
+ * or for signing:
+ *
+ * For enroll, success is defined as each challenge yielding wrong data. This
+ * means this gnubby is not currently enrolled for any of the appIds in any
+ * challenge.
+ *
+ * For sign, success is defined as any challenge yielding ok or waiting for
+ * touch.
+ *
+ * At most one of the success or failure callbacks will be called, and it will
+ * be called at most once. Neither callback is guaranteed to be called: if
+ * a final set of challenges is never given to this gnubby, or if the gnubby
+ * stays busy, the signer has no way to know whether the set of challenges it's
+ * been given has succeeded or failed.
+ * The callback is called only when the signer reaches success or failure, i.e.
+ * when there is no need for this signer to continue trying new challenges.
+ *
+ * @param {!GnubbyFactory} factory Used to create and open the gnubby.
+ * @param {llGnubbyDeviceId} gnubbyIndex Which gnubby to open.
+ * @param {boolean} forEnroll Whether this signer is signing for an attempted
+ * enroll operation.
+ * @param {function(number)} errorCb Called when this signer fails, i.e. no
+ * further attempts can succeed.
+ * @param {function(usbGnubby, number, (SingleSignerResult|undefined))}
+ * successCb Called when this signer succeeds.
+ * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the
+ * signer will not attempt any new operations, assuming the caller is no
+ * longer interested in the outcome.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @constructor
+ */
+function SingleGnubbySigner(factory, gnubbyIndex, forEnroll, errorCb, successCb,
+ opt_timer, opt_logMsgUrl) {
+ /** @private {GnubbyFactory} */
+ this.factory_ = factory;
+ /** @private {llGnubbyDeviceId} */
+ this.gnubbyIndex_ = gnubbyIndex;
+ /** @private {SingleGnubbySigner.State} */
+ this.state_ = SingleGnubbySigner.State.INIT;
+ /** @private {boolean} */
+ this.forEnroll_ = forEnroll;
+ /** @private {function(number)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(usbGnubby, number, (SingleSignerResult|undefined))} */
+ this.successCb_ = successCb;
+ /** @private {Countdown|undefined} */
+ this.timer_ = opt_timer;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {!Array.<!SignHelperChallenge>} */
+ this.challenges_ = [];
+ /** @private {number} */
+ this.challengeIndex_ = 0;
+ /** @private {boolean} */
+ this.challengesFinal_ = false;
+
+ /** @private {!Array.<string>} */
+ this.notForMe_ = [];
+}
+
+/** @enum {number} */
+SingleGnubbySigner.State = {
+ /** Initial state. */
+ INIT: 0,
+ /** The signer is attempting to open a gnubby. */
+ OPENING: 1,
+ /** The signer's gnubby opened, but is busy. */
+ BUSY: 2,
+ /** The signer has an open gnubby, but no challenges to sign. */
+ IDLE: 3,
+ /** The signer is currently signing a challenge. */
+ SIGNING: 4,
+ /** The signer encountered an error. */
+ ERROR: 5,
+ /** The signer got a successful outcome. */
+ SUCCESS: 6,
+ /** The signer is closing its gnubby. */
+ CLOSING: 7,
+ /** The signer is closed. */
+ CLOSED: 8
+};
+
+/**
+ * Attempts to open this signer's gnubby, if it's not already open.
+ * (This is implicitly done by addChallenges.)
+ */
+SingleGnubbySigner.prototype.open = function() {
+ if (this.state_ == SingleGnubbySigner.State.INIT) {
+ this.state_ = SingleGnubbySigner.State.OPENING;
+ this.factory_.openGnubby(this.gnubbyIndex_,
+ this.forEnroll_,
+ this.openCallback_.bind(this),
+ this.logMsgUrl_);
+ }
+};
+
+/**
+ * Closes this signer's gnubby, if it's held.
+ */
+SingleGnubbySigner.prototype.close = function() {
+ if (!this.gnubby_) return;
+ this.state_ = SingleGnubbySigner.State.CLOSING;
+ this.gnubby_.closeWhenIdle(this.closed_.bind(this));
+};
+
+/**
+ * Called when this signer's gnubby is closed.
+ * @private
+ */
+SingleGnubbySigner.prototype.closed_ = function() {
+ this.gnubby_ = null;
+ this.state_ = SingleGnubbySigner.State.CLOSED;
+};
+
+/**
+ * Adds challenges to the set of challenges being tried by this signer.
+ * If the signer is currently idle, begins signing the new challenges.
+ *
+ * @param {Array.<SignHelperChallenge>} challenges
+ * @param {boolean} finalChallenges
+ * @return {boolean} Whether the challenges were accepted.
+ */
+SingleGnubbySigner.prototype.addChallenges =
+ function(challenges, finalChallenges) {
+ if (this.challengesFinal_) {
+ // Can't add new challenges once they're finalized.
+ return false;
+ }
+
+ if (challenges) {
+ console.log(this.gnubby_);
+ console.log(UTIL_fmt('adding ' + challenges.length + ' challenges'));
+ for (var i = 0; i < challenges.length; i++) {
+ this.challenges_.push(challenges[i]);
+ }
+ }
+ this.challengesFinal_ = finalChallenges;
+
+ switch (this.state_) {
+ case SingleGnubbySigner.State.INIT:
+ this.open();
+ break;
+ case SingleGnubbySigner.State.OPENING:
+ // The open has already commenced, so accept the added challenges, but
+ // don't do anything.
+ break;
+ case SingleGnubbySigner.State.IDLE:
+ if (this.challengeIndex_ < challenges.length) {
+ // New challenges added: restart signing.
+ this.doSign_(this.challengeIndex_);
+ } else if (finalChallenges) {
+ // Finalized with no new challenges can happen when the caller rejects
+ // the appId for some challenge.
+ // If this signer is for enroll, the request must be rejected: this
+ // signer can't determine whether the gnubby is or is not enrolled for
+ // the origin.
+ // If this signer is for sign, the request must also be rejected: there
+ // are no new challenges to sign, and all previous ones did not yield
+ // success.
+ var self = this;
+ window.setTimeout(function() {
+ self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS);
+ }, 0);
+ }
+ break;
+ case SingleGnubbySigner.State.SIGNING:
+ // Already signing, so don't kick off a new sign, but accept the added
+ // challenges.
+ break;
+ default:
+ return false;
+ }
+ return true;
+};
+
+/**
+ * How long to delay retrying a failed open.
+ */
+SingleGnubbySigner.OPEN_DELAY_MILLIS = 200;
+
+/**
+ * @param {number} rc The result of the open operation.
+ * @param {usbGnubby=} gnubby The opened gnubby, if open was successful (or
+ * busy).
+ * @private
+ */
+SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
+ if (this.state_ != SingleGnubbySigner.State.OPENING &&
+ this.state_ != SingleGnubbySigner.State.BUSY) {
+ // Open completed after close, perhaps? Ignore.
+ return;
+ }
+
+ switch (rc) {
+ case DeviceStatusCodes.OK_STATUS:
+ if (!gnubby) {
+ console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?'));
+ } else {
+ this.gnubby_ = gnubby;
+ this.gnubby_.version(this.versionCallback_.bind(this));
+ }
+ break;
+ case DeviceStatusCodes.BUSY_STATUS:
+ this.gnubby_ = gnubby;
+ this.openedBusy_ = true;
+ this.state_ = SingleGnubbySigner.State.BUSY;
+ // If there's still time, retry the open.
+ if (!this.timer_ || !this.timer_.expired()) {
+ var self = this;
+ window.setTimeout(function() {
+ if (self.gnubby_) {
+ self.factory_.openGnubby(self.gnubbyIndex_,
+ self.forEnroll_,
+ self.openCallback_.bind(self),
+ self.logMsgUrl_);
+ }
+ }, SingleGnubbySigner.OPEN_DELAY_MILLIS);
+ } else {
+ this.goToError_(DeviceStatusCodes.BUSY_STATUS);
+ }
+ break;
+ default:
+ // TODO(juanlang): This won't be confused with success, but should it be
+ // part of the same namespace as the other error codes, which are
+ // always in DeviceStatusCodes.*?
+ this.goToError_(rc);
+ }
+};
+
+/**
+ * Called with the result of a version command.
+ * @param {number} rc Result of version command.
+ * @param {ArrayBuffer=} opt_data Version.
+ * @private
+ */
+SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
+ if (rc) {
+ this.goToError_(rc);
+ return;
+ }
+ this.state_ = SingleGnubbySigner.State.IDLE;
+ this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || []));
+ this.doSign_(this.challengeIndex_);
+};
+
+/**
+ * @param {number} challengeIndex
+ * @private
+ */
+SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
+ if (this.timer_ && this.timer_.expired()) {
+ // If the timer is expired, that means we never got a success or a touch
+ // required response: either always implies completion of this signer's
+ // state machine (see signCallback's cases for OK_STATUS and
+ // WAIT_TOUCH_STATUS.) We could have gotten wrong data on a partial set of
+ // challenges, but this means we don't yet know the final outcome. In any
+ // event, we don't yet know the final outcome: return busy.
+ this.goToError_(DeviceStatusCodes.BUSY_STATUS);
+ return;
+ }
+
+ this.state_ = SingleGnubbySigner.State.SIGNING;
+
+ if (challengeIndex >= this.challenges_.length) {
+ this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
+ return;
+ }
+
+ var challenge = this.challenges_[challengeIndex];
+ var challengeHash = challenge.challengeHash;
+ var appIdHash = challenge.appIdHash;
+ var keyHandle = challenge.keyHandle;
+ if (this.notForMe_.indexOf(keyHandle) != -1) {
+ // Cache hit: return wrong data again.
+ this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
+ } else if (challenge.version && challenge.version != this.version_) {
+ // Sign challenge for a different version of gnubby: return wrong data.
+ this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
+ } else {
+ var opt_nowink = this.forEnroll_;
+ this.gnubby_.sign(challengeHash, appIdHash, keyHandle,
+ this.signCallback_.bind(this, challengeIndex),
+ opt_nowink);
+ }
+};
+
+/**
+ * Called with the result of a single sign operation.
+ * @param {number} challengeIndex the index of the challenge just attempted
+ * @param {number} code the result of the sign operation
+ * @param {ArrayBuffer=} opt_info
+ * @private
+ */
+SingleGnubbySigner.prototype.signCallback_ =
+ function(challengeIndex, code, opt_info) {
+ console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyIndex_) +
+ ', challenge ' + challengeIndex + ' yielded ' + code.toString(16)));
+ if (this.state_ != SingleGnubbySigner.State.SIGNING) {
+ console.log(UTIL_fmt('already done!'));
+ // We're done, the caller's no longer interested.
+ return;
+ }
+
+ // Cache wrong data result, re-asking the gnubby to sign it won't produce
+ // different results.
+ if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
+ if (challengeIndex < this.challenges_.length) {
+ var challenge = this.challenges_[challengeIndex];
+ if (this.notForMe_.indexOf(challenge.keyHandle) == -1) {
+ this.notForMe_.push(challenge.keyHandle);
+ }
+ }
+ }
+
+ switch (code) {
+ case DeviceStatusCodes.GONE_STATUS:
+ this.goToError_(code);
+ break;
+
+ case DeviceStatusCodes.TIMEOUT_STATUS:
+ // TODO(juanlang): On a TIMEOUT_STATUS, sync first, then retry.
+ case DeviceStatusCodes.BUSY_STATUS:
+ this.doSign_(this.challengeIndex_);
+ break;
+
+ case DeviceStatusCodes.OK_STATUS:
+ if (this.forEnroll_) {
+ this.goToError_(code);
+ } else {
+ this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info);
+ }
+ break;
+
+ case DeviceStatusCodes.WAIT_TOUCH_STATUS:
+ if (this.forEnroll_) {
+ this.goToError_(code);
+ } else {
+ this.goToSuccess_(code, this.challenges_[challengeIndex]);
+ }
+ break;
+
+ case DeviceStatusCodes.WRONG_DATA_STATUS:
+ if (this.challengeIndex_ < this.challenges_.length - 1) {
+ this.doSign_(++this.challengeIndex_);
+ } else if (!this.challengesFinal_) {
+ this.state_ = SingleGnubbySigner.State.IDLE;
+ } else if (this.forEnroll_) {
+ // Signal the caller whether the open was busy, because it may take
+ // an unusually long time when opened for enroll. Use an empty
+ // "challenge" as the signal for a busy open.
+ var challenge = undefined;
+ if (this.openedBusy) {
+ challenge = { appIdHash: '', challengeHash: '', keyHandle: '' };
+ }
+ this.goToSuccess_(code, challenge);
+ } else {
+ this.goToError_(code);
+ }
+ break;
+
+ default:
+ if (this.forEnroll_) {
+ this.goToError_(code);
+ } else if (this.challengeIndex_ < this.challenges_.length - 1) {
+ this.doSign_(++this.challengeIndex_);
+ } else if (!this.challengesFinal_) {
+ // Increment the challenge index, as this one isn't useful any longer,
+ // but a subsequent challenge may appear, and it might be useful.
+ this.challengeIndex_++;
+ this.state_ = SingleGnubbySigner.State.IDLE;
+ } else {
+ this.goToError_(code);
+ }
+ }
+};
+
+/**
+ * Switches to the error state, and notifies caller.
+ * @param {number} code
+ * @private
+ */
+SingleGnubbySigner.prototype.goToError_ = function(code) {
+ this.state_ = SingleGnubbySigner.State.ERROR;
+ console.log(UTIL_fmt('failed (' + code.toString(16) + ')'));
+ this.errorCb_(code);
+ // Since this gnubby can no longer produce a useful result, go ahead and
+ // close it.
+ this.close();
+};
+
+/**
+ * Switches to the success state, and notifies caller.
+ * @param {number} code
+ * @param {SignHelperChallenge=} opt_challenge
+ * @param {ArrayBuffer=} opt_info
+ * @private
+ */
+SingleGnubbySigner.prototype.goToSuccess_ =
+ function(code, opt_challenge, opt_info) {
+ this.state_ = SingleGnubbySigner.State.SUCCESS;
+ console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
+ if (opt_challenge || opt_info) {
+ var singleSignerResult = {};
+ if (opt_challenge) {
+ singleSignerResult['challenge'] = opt_challenge;
+ }
+ if (opt_info) {
+ singleSignerResult['info'] = opt_info;
+ }
+ }
+ this.successCb_(this.gnubby_, code, singleSignerResult);
+ // this.gnubby_ is now owned by successCb.
+ this.gnubby_ = null;
+};
« no previous file with comments | « chrome/browser/resources/cryptotoken/signhelper.js ('k') | chrome/browser/resources/cryptotoken/usbenrollhelper.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698