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

Side by Side 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, 7 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 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.
4
5 /**
6 * @fileoverview Implements an enroll helper using USB gnubbies.
7 * @author juanlang@google.com (Juan Lang)
8 */
9 'use strict';
10
11 /**
12 * @param {!GnubbyFactory} factory
13 * @param {!Countdown} timer
14 * @param {function(number, boolean)} errorCb Called when an enroll request
15 * fails with an error code and whether any gnubbies were found.
16 * @param {function(string, string)} successCb Called with the result of a
17 * successful enroll request, along with the version of the gnubby that
18 * provided it.
19 * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
20 * progress updates to the enroll request.
21 * @param {string=} opt_logMsgUrl A URL to post log messages to.
22 * @constructor
23 * @implements {EnrollHelper}
24 */
25 function UsbEnrollHelper(factory, timer, errorCb, successCb, opt_progressCb,
26 opt_logMsgUrl) {
27 /** @private {!GnubbyFactory} */
28 this.factory_ = factory;
29 /** @private {!Countdown} */
30 this.timer_ = timer;
31 /** @private {function(number, boolean)} */
32 this.errorCb_ = errorCb;
33 /** @private {function(string, string)} */
34 this.successCb_ = successCb;
35 /** @private {(function(number, boolean)|undefined)} */
36 this.progressCb_ = opt_progressCb;
37 /** @private {string|undefined} */
38 this.logMsgUrl_ = opt_logMsgUrl;
39
40 /** @private {Array.<SignHelperChallenge>} */
41 this.signChallenges_ = [];
42 /** @private {boolean} */
43 this.signChallengesFinal_ = false;
44 /** @private {Array.<usbGnubby>} */
45 this.waitingForTouchGnubbies_ = [];
46
47 /** @private {boolean} */
48 this.closed_ = false;
49 /** @private {boolean} */
50 this.notified_ = false;
51 /** @private {number|undefined} */
52 this.lastProgressUpdate_ = undefined;
53 /** @private {boolean} */
54 this.signerComplete_ = false;
55 this.getSomeGnubbies_();
56 }
57
58 /**
59 * Attempts to enroll using the provided data.
60 * @param {Object} enrollChallenges a map of version string to enroll
61 * challenges.
62 * @param {Array.<SignHelperChallenge>} signChallenges a list of sign
63 * challenges for already enrolled gnubbies, to prevent double-enrolling a
64 * device.
65 */
66 UsbEnrollHelper.prototype.doEnroll =
67 function(enrollChallenges, signChallenges) {
68 this.enrollChallenges = enrollChallenges;
69 this.signChallengesFinal_ = true;
70 if (this.signer_) {
71 this.signer_.addEncodedChallenges(
72 signChallenges, this.signChallengesFinal_);
73 } else {
74 this.signChallenges_ = signChallenges;
75 }
76 };
77
78 /** Closes this helper. */
79 UsbEnrollHelper.prototype.close = function() {
80 this.closed_ = true;
81 for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
82 this.waitingForTouchGnubbies_[i].closeWhenIdle();
83 }
84 this.waitingForTouchGnubbies_ = [];
85 if (this.signer_) {
86 this.signer_.close();
87 this.signer_ = null;
88 }
89 };
90
91 /**
92 * Enumerates gnubbies, and begins processing challenges upon enumeration if
93 * any gnubbies are found.
94 * @private
95 */
96 UsbEnrollHelper.prototype.getSomeGnubbies_ = function() {
97 this.factory_.enumerate(this.enumerateCallback_.bind(this));
98 };
99
100 /**
101 * Called with the result of enumerating gnubbies.
102 * @param {number} rc the result of the enumerate.
103 * @param {Array.<llGnubbyDeviceId>} indexes
104 * @private
105 */
106 UsbEnrollHelper.prototype.enumerateCallback_ = function(rc, indexes) {
107 if (rc) {
108 // Enumerate failure is rare enough that it might be worth reporting
109 // directly, rather than trying again.
110 this.errorCb_(rc, false);
111 return;
112 }
113 if (!indexes.length) {
114 this.maybeReEnumerateGnubbies_();
115 return;
116 }
117 if (this.timer_.expired()) {
118 this.errorCb_(DeviceStatusCodes.TIMEOUT_STATUS, true);
119 return;
120 }
121 this.gotSomeGnubbies_(indexes);
122 };
123
124 /**
125 * If there's still time, re-enumerates devices and try with them. Otherwise
126 * reports an error and, implicitly, stops the enroll operation.
127 * @private
128 */
129 UsbEnrollHelper.prototype.maybeReEnumerateGnubbies_ = function() {
130 var errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
131 var anyGnubbies = false;
132 // If there's still time and we're still going, retry enumerating.
133 if (!this.closed_ && !this.timer_.expired()) {
134 this.notifyProgress_(errorCode, anyGnubbies);
135 var self = this;
136 // Use a delayed re-enumerate to prevent hammering the system unnecessarily.
137 window.setTimeout(function() {
138 if (self.timer_.expired()) {
139 self.notifyError_(errorCode, anyGnubbies);
140 } else {
141 self.getSomeGnubbies_();
142 }
143 }, 200);
144 } else {
145 this.notifyError_(errorCode, anyGnubbies);
146 }
147 };
148
149 /**
150 * Called with the result of enumerating gnubby indexes.
151 * @param {Array.<llGnubbyDeviceId>} indexes
152 * @private
153 */
154 UsbEnrollHelper.prototype.gotSomeGnubbies_ = function(indexes) {
155 this.signer_ = new MultipleGnubbySigner(
156 this.factory_,
157 indexes,
158 true /* forEnroll */,
159 this.signerCompleted_.bind(this),
160 this.signerFoundGnubby_.bind(this),
161 this.timer_,
162 this.logMsgUrl_);
163 if (this.signChallengesFinal_) {
164 this.signer_.addEncodedChallenges(
165 this.signChallenges_, this.signChallengesFinal_);
166 this.pendingSignChallenges_ = [];
167 }
168 };
169
170 /**
171 * Called when a MultipleGnubbySigner completes its sign request.
172 * @param {boolean} anySucceeded whether any sign attempt completed
173 * successfully.
174 * @param {number=} errorCode an error code from a failing gnubby, if one was
175 * found.
176 * @private
177 */
178 UsbEnrollHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) {
179 this.signerComplete_ = true;
180 // The signer is not created unless some gnubbies were enumerated, so
181 // anyGnubbies is mostly always true. The exception is when the last gnubby is
182 // removed, handled shortly.
183 var anyGnubbies = true;
184 if (!anySucceeded) {
185 if (errorCode == -llGnubby.GONE) {
186 // If the last gnubby was removed, report as though no gnubbies were
187 // found.
188 this.maybeReEnumerateGnubbies_();
189 } else {
190 if (!errorCode) errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
191 this.notifyError_(errorCode, anyGnubbies);
192 }
193 } else if (this.anyTimeout) {
194 // Some previously succeeding gnubby timed out: return its error code.
195 this.notifyError_(this.timeoutError, anyGnubbies);
196 } else {
197 // Do nothing: signerFoundGnubby will have been called with each succeeding
198 // gnubby.
199 }
200 };
201
202 /**
203 * Called when a MultipleGnubbySigner finds a gnubby that can enroll.
204 * @param {number} code
205 * @param {MultipleSignerResult} signResult
206 * @private
207 */
208 UsbEnrollHelper.prototype.signerFoundGnubby_ = function(code, signResult) {
209 var gnubby = signResult['gnubby'];
210 this.waitingForTouchGnubbies_.push(gnubby);
211 this.notifyProgress_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true);
212 if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
213 if (signResult['challenge']) {
214 // If the signer yielded a busy open, indicate waiting for touch
215 // immediately, rather than attempting enroll. This allows the UI to
216 // update, since a busy open is a potentially long operation.
217 this.notifyError_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true);
218 } else {
219 this.matchEnrollVersionToGnubby_(gnubby);
220 }
221 }
222 };
223
224 /**
225 * Attempts to match the gnubby's U2F version with an appropriate enroll
226 * challenge.
227 * @param {usbGnubby} gnubby
228 * @private
229 */
230 UsbEnrollHelper.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
231 if (!gnubby) {
232 console.warn(UTIL_fmt('no gnubby, WTF?'));
233 }
234 gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
235 };
236
237 /**
238 * Called with the result of a version command.
239 * @param {usbGnubby} gnubby
240 * @param {number} rc result of version command.
241 * @param {ArrayBuffer=} data version.
242 * @private
243 */
244 UsbEnrollHelper.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
245 if (rc) {
246 this.removeWrongVersionGnubby_(gnubby);
247 return;
248 }
249 var version = UTIL_BytesToString(new Uint8Array(data || null));
250 this.tryEnroll_(gnubby, version);
251 };
252
253 /**
254 * Drops the gnubby from the list of eligible gnubbies.
255 * @param {usbGnubby} gnubby
256 * @private
257 */
258 UsbEnrollHelper.prototype.removeWaitingGnubby_ = function(gnubby) {
259 gnubby.closeWhenIdle();
260 var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
261 if (index >= 0) {
262 this.waitingForTouchGnubbies_.splice(index, 1);
263 }
264 };
265
266 /**
267 * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
268 * version.
269 * @param {usbGnubby} gnubby
270 * @private
271 */
272 UsbEnrollHelper.prototype.removeWrongVersionGnubby_ = function(gnubby) {
273 this.removeWaitingGnubby_(gnubby);
274 if (!this.waitingForTouchGnubbies_.length && this.signerComplete_) {
275 // Whoops, this was the last gnubby: indicate there are none.
276 this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
277 }
278 };
279
280 /**
281 * Attempts enrolling a particular gnubby with a challenge of the appropriate
282 * version.
283 * @param {usbGnubby} gnubby
284 * @param {string} version
285 * @private
286 */
287 UsbEnrollHelper.prototype.tryEnroll_ = function(gnubby, version) {
288 var challenge = this.getChallengeOfVersion_(version);
289 if (!challenge) {
290 this.removeWrongVersionGnubby_(gnubby);
291 return;
292 }
293 var challengeChallenge = B64_decode(challenge['challenge']);
294 var appIdHash = B64_decode(challenge['appIdHash']);
295 gnubby.enroll(challengeChallenge, appIdHash,
296 this.enrollCallback_.bind(this, gnubby, version));
297 };
298
299 /**
300 * Finds the (first) challenge of the given version in this helper's challenges.
301 * @param {string} version
302 * @return {Object} challenge, if found, or null if not.
303 * @private
304 */
305 UsbEnrollHelper.prototype.getChallengeOfVersion_ = function(version) {
306 for (var i = 0; i < this.enrollChallenges.length; i++) {
307 if (this.enrollChallenges[i]['version'] == version) {
308 return this.enrollChallenges[i];
309 }
310 }
311 return null;
312 };
313
314 /**
315 * Called with the result of an enroll request to a gnubby.
316 * @param {usbGnubby} gnubby
317 * @param {string} version
318 * @param {number} code
319 * @param {ArrayBuffer=} infoArray
320 * @private
321 */
322 UsbEnrollHelper.prototype.enrollCallback_ =
323 function(gnubby, version, code, infoArray) {
324 if (this.notified_) {
325 // Enroll completed after previous success or failure. Disregard.
326 return;
327 }
328 switch (code) {
329 case -llGnubby.GONE:
330 // Close this gnubby.
331 this.removeWaitingGnubby_(gnubby);
332 if (!this.waitingForTouchGnubbies_.length) {
333 // Last enroll attempt is complete and last gnubby is gone: retry if
334 // possible.
335 this.maybeReEnumerateGnubbies_();
336 }
337 break;
338
339 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
340 case DeviceStatusCodes.BUSY_STATUS:
341 case DeviceStatusCodes.TIMEOUT_STATUS:
342 if (this.timer_.expired()) {
343 // Store any timeout error code, to be returned from the complete
344 // callback if no other eligible gnubbies are found.
345 this.anyTimeout = true;
346 this.timeoutError = code;
347 // Close this gnubby.
348 this.removeWaitingGnubby_(gnubby);
349 if (!this.waitingForTouchGnubbies_.length && !this.notified_) {
350 // Last enroll attempt is complete: return this error.
351 console.log(UTIL_fmt('timeout (' + code.toString(16) +
352 ') enrolling'));
353 this.notifyError_(code, true);
354 }
355 } else {
356 // Notify caller of waiting for touch events.
357 if (code == DeviceStatusCodes.WAIT_TOUCH_STATUS) {
358 this.notifyProgress_(code, true);
359 }
360 window.setTimeout(this.tryEnroll_.bind(this, gnubby, version), 200);
361 }
362 break;
363
364 case DeviceStatusCodes.OK_STATUS:
365 var info = B64_encode(new Uint8Array(infoArray || []));
366 this.notifySuccess_(version, info);
367 break;
368
369 default:
370 console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
371 this.notifyError_(code, true);
372 break;
373 }
374 };
375
376 /**
377 * @param {number} code
378 * @param {boolean} anyGnubbies
379 * @private
380 */
381 UsbEnrollHelper.prototype.notifyError_ = function(code, anyGnubbies) {
382 if (this.notified_ || this.closed_)
383 return;
384 this.notified_ = true;
385 this.close();
386 this.errorCb_(code, anyGnubbies);
387 };
388
389 /**
390 * @param {string} version
391 * @param {string} info
392 * @private
393 */
394 UsbEnrollHelper.prototype.notifySuccess_ = function(version, info) {
395 if (this.notified_ || this.closed_)
396 return;
397 this.notified_ = true;
398 this.close();
399 this.successCb_(version, info);
400 };
401
402 /**
403 * @param {number} code
404 * @param {boolean} anyGnubbies
405 * @private
406 */
407 UsbEnrollHelper.prototype.notifyProgress_ = function(code, anyGnubbies) {
408 if (this.lastProgressUpdate_ == code || this.notified_ || this.closed_)
409 return;
410 this.lastProgressUpdate_ = code;
411 if (this.progressCb_) this.progressCb_(code, anyGnubbies);
412 };
413
414 /**
415 * @param {!GnubbyFactory} gnubbyFactory factory to create gnubbies.
416 * @constructor
417 * @implements {EnrollHelperFactory}
418 */
419 function UsbEnrollHelperFactory(gnubbyFactory) {
420 /** @private {!GnubbyFactory} */
421 this.gnubbyFactory_ = gnubbyFactory;
422 }
423
424 /**
425 * @param {!Countdown} timer
426 * @param {function(number, boolean)} errorCb Called when an enroll request
427 * fails with an error code and whether any gnubbies were found.
428 * @param {function(string, string)} successCb Called with the result of a
429 * successful enroll request, along with the version of the gnubby that
430 * provided it.
431 * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
432 * progress updates to the enroll request.
433 * @param {string=} opt_logMsgUrl A URL to post log messages to.
434 * @return {UsbEnrollHelper} the newly created helper.
435 */
436 UsbEnrollHelperFactory.prototype.createHelper =
437 function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {
438 var helper =
439 new UsbEnrollHelper(this.gnubbyFactory_, timer, errorCb, successCb,
440 opt_progressCb, opt_logMsgUrl);
441 return helper;
442 };
OLDNEW
« 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