1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview Implements an enroll helper using USB gnubbies.
11 * @param {!GnubbyFactory} factory A factory for Gnubby instances
12 * @param {!Countdown} timer A timer for enroll timeout
13 * @param {function(number, boolean)} errorCb Called when an enroll request
14 * fails with an error code and whether any gnubbies were found.
15 * @param {function(string, string)} successCb Called with the result of a
16 * successful enroll request, along with the version of the gnubby that
18 * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
19 * progress updates to the enroll request.
20 * @param {string=} opt_logMsgUrl A URL to post log messages to.
22 * @implements {EnrollHelper}
24 function UsbEnrollHelper(factory, timer, errorCb, successCb, opt_progressCb,
26 /** @private {!GnubbyFactory} */
27 this.factory_ = factory;
28 /** @private {!Countdown} */
30 /** @private {function(number, boolean)} */
31 this.errorCb_ = errorCb;
32 /** @private {function(string, string)} */
33 this.successCb_ = successCb;
34 /** @private {(function(number, boolean)|undefined)} */
35 this.progressCb_ = opt_progressCb;
36 /** @private {string|undefined} */
37 this.logMsgUrl_ = opt_logMsgUrl;
39 /** @private {Array.<SignHelperChallenge>} */
40 this.signChallenges_ = [];
41 /** @private {boolean} */
42 this.signChallengesFinal_ = false;
43 /** @private {Array.<usbGnubby>} */
44 this.waitingForTouchGnubbies_ = [];
46 /** @private {boolean} */
48 /** @private {boolean} */
49 this.notified_ = false;
50 /** @private {number|undefined} */
51 this.lastProgressUpdate_ = undefined;
52 /** @private {boolean} */
53 this.signerComplete_ = false;
54 this.getSomeGnubbies_();
58 * Attempts to enroll using the provided data.
59 * @param {Object} enrollChallenges a map of version string to enroll
61 * @param {Array.<SignHelperChallenge>} signChallenges a list of sign
62 * challenges for already enrolled gnubbies, to prevent double-enrolling a
65 UsbEnrollHelper.prototype.doEnroll =
66 function(enrollChallenges, signChallenges) {
67 this.enrollChallenges = enrollChallenges;
68 this.signChallengesFinal_ = true;
70 this.signer_.addEncodedChallenges(
71 signChallenges, this.signChallengesFinal_);
73 this.signChallenges_ = signChallenges;
77 /** Closes this helper. */
78 UsbEnrollHelper.prototype.close = function() {
80 for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
81 this.waitingForTouchGnubbies_[i].closeWhenIdle();
83 this.waitingForTouchGnubbies_ = [];
91 * Enumerates gnubbies, and begins processing challenges upon enumeration if
92 * any gnubbies are found.
95 UsbEnrollHelper.prototype.getSomeGnubbies_ = function() {
96 this.factory_.enumerate(this.enumerateCallback_.bind(this));
100 * Called with the result of enumerating gnubbies.
101 * @param {number} rc the result of the enumerate.
102 * @param {Array.<llGnubbyDeviceId>} indexes Device ids of enumerated gnubbies
105 UsbEnrollHelper.prototype.enumerateCallback_ = function(rc, indexes) {
107 // Enumerate failure is rare enough that it might be worth reporting
108 // directly, rather than trying again.
109 this.errorCb_(rc, false);
112 if (!indexes.length) {
113 this.maybeReEnumerateGnubbies_();
116 if (this.timer_.expired()) {
117 this.errorCb_(DeviceStatusCodes.TIMEOUT_STATUS, true);
120 this.gotSomeGnubbies_(indexes);
124 * If there's still time, re-enumerates devices and try with them. Otherwise
125 * reports an error and, implicitly, stops the enroll operation.
128 UsbEnrollHelper.prototype.maybeReEnumerateGnubbies_ = function() {
129 var errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
130 var anyGnubbies = false;
131 // If there's still time and we're still going, retry enumerating.
132 if (!this.closed_ && !this.timer_.expired()) {
133 this.notifyProgress_(errorCode, anyGnubbies);
135 // Use a delayed re-enumerate to prevent hammering the system unnecessarily.
136 window.setTimeout(function() {
137 if (self.timer_.expired()) {
138 self.notifyError_(errorCode, anyGnubbies);
140 self.getSomeGnubbies_();
144 this.notifyError_(errorCode, anyGnubbies);
149 * Called with the result of enumerating gnubby indexes.
150 * @param {Array.<llGnubbyDeviceId>} indexes Device ids of enumerated gnubbies
153 UsbEnrollHelper.prototype.gotSomeGnubbies_ = function(indexes) {
154 this.signer_ = new MultipleGnubbySigner(
157 true /* forEnroll */,
158 this.signerCompleted_.bind(this),
159 this.signerFoundGnubby_.bind(this),
162 if (this.signChallengesFinal_) {
163 this.signer_.addEncodedChallenges(
164 this.signChallenges_, this.signChallengesFinal_);
165 this.pendingSignChallenges_ = [];
170 * Called when a MultipleGnubbySigner completes its sign request.
171 * @param {boolean} anySucceeded whether any sign attempt completed
173 * @param {number=} errorCode an error code from a failing gnubby, if one was
177 UsbEnrollHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) {
178 this.signerComplete_ = true;
179 // The signer is not created unless some gnubbies were enumerated, so
180 // anyGnubbies is mostly always true. The exception is when the last gnubby is
181 // removed, handled shortly.
182 var anyGnubbies = true;
184 if (errorCode == -llGnubby.GONE) {
185 // If the last gnubby was removed, report as though no gnubbies were
187 this.maybeReEnumerateGnubbies_();
189 if (!errorCode) errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
190 this.notifyError_(errorCode, anyGnubbies);
192 } else if (this.anyTimeout) {
193 // Some previously succeeding gnubby timed out: return its error code.
194 this.notifyError_(this.timeoutError, anyGnubbies);
196 // Do nothing: signerFoundGnubby will have been called with each succeeding
202 * Called when a MultipleGnubbySigner finds a gnubby that can enroll.
203 * @param {number} code Status code
204 * @param {MultipleSignerResult} signResult Signature results
207 UsbEnrollHelper.prototype.signerFoundGnubby_ = function(code, signResult) {
208 var gnubby = signResult['gnubby'];
209 this.waitingForTouchGnubbies_.push(gnubby);
210 this.notifyProgress_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true);
211 if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
212 if (signResult['challenge']) {
213 // If the signer yielded a busy open, indicate waiting for touch
214 // immediately, rather than attempting enroll. This allows the UI to
215 // update, since a busy open is a potentially long operation.
216 this.notifyError_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true);
218 this.matchEnrollVersionToGnubby_(gnubby);
224 * Attempts to match the gnubby's U2F version with an appropriate enroll
226 * @param {usbGnubby} gnubby Gnubby instance
229 UsbEnrollHelper.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
231 console.warn(UTIL_fmt('no gnubby, WTF?'));
233 gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
237 * Called with the result of a version command.
238 * @param {usbGnubby} gnubby Gnubby instance
239 * @param {number} rc result of version command.
240 * @param {ArrayBuffer=} data version.
243 UsbEnrollHelper.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
245 this.removeWrongVersionGnubby_(gnubby);
248 var version = UTIL_BytesToString(new Uint8Array(data || null));
249 this.tryEnroll_(gnubby, version);
253 * Drops the gnubby from the list of eligible gnubbies.
254 * @param {usbGnubby} gnubby Gnubby instance
257 UsbEnrollHelper.prototype.removeWaitingGnubby_ = function(gnubby) {
258 gnubby.closeWhenIdle();
259 var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
261 this.waitingForTouchGnubbies_.splice(index, 1);
266 * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
268 * @param {usbGnubby} gnubby Gnubby instance
271 UsbEnrollHelper.prototype.removeWrongVersionGnubby_ = function(gnubby) {
272 this.removeWaitingGnubby_(gnubby);
273 if (!this.waitingForTouchGnubbies_.length && this.signerComplete_) {
274 // Whoops, this was the last gnubby: indicate there are none.
275 this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
280 * Attempts enrolling a particular gnubby with a challenge of the appropriate
282 * @param {usbGnubby} gnubby Gnubby instance
283 * @param {string} version Protocol version
286 UsbEnrollHelper.prototype.tryEnroll_ = function(gnubby, version) {
287 var challenge = this.getChallengeOfVersion_(version);
289 this.removeWrongVersionGnubby_(gnubby);
292 var challengeChallenge = B64_decode(challenge['challenge']);
293 var appIdHash = B64_decode(challenge['appIdHash']);
294 gnubby.enroll(challengeChallenge, appIdHash,
295 this.enrollCallback_.bind(this, gnubby, version));
299 * Finds the (first) challenge of the given version in this helper's challenges.
300 * @param {string} version Protocol version
301 * @return {Object} challenge, if found, or null if not.
304 UsbEnrollHelper.prototype.getChallengeOfVersion_ = function(version) {
305 for (var i = 0; i < this.enrollChallenges.length; i++) {
306 if (this.enrollChallenges[i]['version'] == version) {
307 return this.enrollChallenges[i];
314 * Called with the result of an enroll request to a gnubby.
315 * @param {usbGnubby} gnubby Gnubby instance
316 * @param {string} version Protocol version
317 * @param {number} code Status code
318 * @param {ArrayBuffer=} infoArray Returned data
321 UsbEnrollHelper.prototype.enrollCallback_ =
322 function(gnubby, version, code, infoArray) {
323 if (this.notified_) {
324 // Enroll completed after previous success or failure. Disregard.
329 // Close this gnubby.
330 this.removeWaitingGnubby_(gnubby);
331 if (!this.waitingForTouchGnubbies_.length) {
332 // Last enroll attempt is complete and last gnubby is gone: retry if
334 this.maybeReEnumerateGnubbies_();
338 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
339 case DeviceStatusCodes.BUSY_STATUS:
340 case DeviceStatusCodes.TIMEOUT_STATUS:
341 if (this.timer_.expired()) {
342 // Store any timeout error code, to be returned from the complete
343 // callback if no other eligible gnubbies are found.
344 this.anyTimeout = true;
345 this.timeoutError = code;
346 // Close this gnubby.
347 this.removeWaitingGnubby_(gnubby);
348 if (!this.waitingForTouchGnubbies_.length && !this.notified_) {
349 // Last enroll attempt is complete: return this error.
350 console.log(UTIL_fmt('timeout (' + code.toString(16) +
352 this.notifyError_(code, true);
355 // Notify caller of waiting for touch events.
356 if (code == DeviceStatusCodes.WAIT_TOUCH_STATUS) {
357 this.notifyProgress_(code, true);
359 window.setTimeout(this.tryEnroll_.bind(this, gnubby, version), 200);
363 case DeviceStatusCodes.OK_STATUS:
364 var info = B64_encode(new Uint8Array(infoArray || []));
365 this.notifySuccess_(version, info);
369 console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
370 this.notifyError_(code, true);
376 * @param {number} code Status code
377 * @param {boolean} anyGnubbies If any gnubbies were found
380 UsbEnrollHelper.prototype.notifyError_ = function(code, anyGnubbies) {
381 if (this.notified_ || this.closed_)
383 this.notified_ = true;
385 this.errorCb_(code, anyGnubbies);
389 * @param {string} version Protocol version
390 * @param {string} info B64 encoded success data
393 UsbEnrollHelper.prototype.notifySuccess_ = function(version, info) {
394 if (this.notified_ || this.closed_)
396 this.notified_ = true;
398 this.successCb_(version, info);
402 * @param {number} code Status code
403 * @param {boolean} anyGnubbies If any gnubbies were found
406 UsbEnrollHelper.prototype.notifyProgress_ = function(code, anyGnubbies) {
407 if (this.lastProgressUpdate_ == code || this.notified_ || this.closed_)
409 this.lastProgressUpdate_ = code;
410 if (this.progressCb_) this.progressCb_(code, anyGnubbies);
414 * @param {!GnubbyFactory} gnubbyFactory factory to create gnubbies.
416 * @implements {EnrollHelperFactory}
418 function UsbEnrollHelperFactory(gnubbyFactory) {
419 /** @private {!GnubbyFactory} */
420 this.gnubbyFactory_ = gnubbyFactory;
424 * @param {!Countdown} timer Timeout timer
425 * @param {function(number, boolean)} errorCb Called when an enroll request
426 * fails with an error code and whether any gnubbies were found.
427 * @param {function(string, string)} successCb Called with the result of a
428 * successful enroll request, along with the version of the gnubby that
430 * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
431 * progress updates to the enroll request.
432 * @param {string=} opt_logMsgUrl A URL to post log messages to.
433 * @return {UsbEnrollHelper} the newly created helper.
435 UsbEnrollHelperFactory.prototype.createHelper =
436 function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {
438 new UsbEnrollHelper(this.gnubbyFactory_, timer, errorCb, successCb,
439 opt_progressCb, opt_logMsgUrl);