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; |
+}; |