Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / usbenrollhandler.js
blob55f5d06586c3dd22e766760a43b45d5199188108
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.
5 /**
6 * @fileoverview Implements an enroll handler using USB gnubbies.
7 */
8 'use strict';
10 /**
11 * @param {!EnrollHelperRequest} request The enroll request.
12 * @constructor
13 * @implements {RequestHandler}
15 function UsbEnrollHandler(request) {
16 /** @private {!EnrollHelperRequest} */
17 this.request_ = request;
19 /** @private {Array.<Gnubby>} */
20 this.waitingForTouchGnubbies_ = [];
22 /** @private {boolean} */
23 this.closed_ = false;
24 /** @private {boolean} */
25 this.notified_ = false;
28 /**
29 * Default timeout value in case the caller never provides a valid timeout.
30 * @const
32 UsbEnrollHandler.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
34 /**
35 * @param {RequestHandlerCallback} cb Called back with the result of the
36 * request, and an optional source for the result.
37 * @return {boolean} Whether this handler could be run.
39 UsbEnrollHandler.prototype.run = function(cb) {
40 var timeoutMillis =
41 this.request_.timeoutSeconds ?
42 this.request_.timeoutSeconds * 1000 :
43 UsbEnrollHandler.DEFAULT_TIMEOUT_MILLIS;
44 /** @private {Countdown} */
45 this.timer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
46 timeoutMillis);
47 this.enrollChallenges = this.request_.enrollChallenges;
48 /** @private {RequestHandlerCallback} */
49 this.cb_ = cb;
50 this.signer_ = new MultipleGnubbySigner(
51 true /* forEnroll */,
52 this.signerCompleted_.bind(this),
53 this.signerFoundGnubby_.bind(this),
54 timeoutMillis,
55 this.request_.logMsgUrl);
56 return this.signer_.doSign(this.request_.signData);
59 /** Closes this helper. */
60 UsbEnrollHandler.prototype.close = function() {
61 this.closed_ = true;
62 for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
63 this.waitingForTouchGnubbies_[i].closeWhenIdle();
65 this.waitingForTouchGnubbies_ = [];
66 if (this.signer_) {
67 this.signer_.close();
68 this.signer_ = null;
72 /**
73 * Called when a MultipleGnubbySigner completes its sign request.
74 * @param {boolean} anyPending Whether any gnubbies are pending.
75 * @private
77 UsbEnrollHandler.prototype.signerCompleted_ = function(anyPending) {
78 if (!this.anyGnubbiesFound_ || this.anyTimeout_ || anyPending ||
79 this.timer_.expired()) {
80 this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
81 } else {
82 // Do nothing: signerFoundGnubby will have been called with each succeeding
83 // gnubby.
87 /**
88 * Called when a MultipleGnubbySigner finds a gnubby that can enroll.
89 * @param {MultipleSignerResult} signResult Signature results
90 * @param {boolean} moreExpected Whether the signer expects to report
91 * results from more gnubbies.
92 * @private
94 UsbEnrollHandler.prototype.signerFoundGnubby_ =
95 function(signResult, moreExpected) {
96 if (!signResult.code) {
97 // If the signer reports a gnubby can sign, report this immediately to the
98 // caller, as the gnubby is already enrolled. Map ok to WRONG_DATA, so the
99 // caller knows what to do.
100 this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS);
101 } else if (signResult.code == DeviceStatusCodes.WRONG_DATA_STATUS ||
102 signResult.code == DeviceStatusCodes.WRONG_LENGTH_STATUS) {
103 var gnubby = signResult['gnubby'];
104 // A valid helper request contains at least one enroll challenge, so use
105 // the app id hash from the first challenge.
106 var appIdHash = this.request_.enrollChallenges[0].appIdHash;
107 DEVICE_FACTORY_REGISTRY.getGnubbyFactory().notEnrolledPrerequisiteCheck(
108 gnubby, appIdHash, this.gnubbyPrerequisitesChecked_.bind(this));
113 * Called with the result of a gnubby prerequisite check.
114 * @param {number} rc The result of the prerequisite check.
115 * @param {Gnubby=} opt_gnubby The gnubby whose prerequisites were checked.
116 * @private
118 UsbEnrollHandler.prototype.gnubbyPrerequisitesChecked_ =
119 function(rc, opt_gnubby) {
120 if (rc || this.timer_.expired()) {
121 // Do nothing:
122 // If the timer is expired, the signerCompleted_ callback will indicate
123 // timeout to the caller.
124 // If there's an error, this gnubby is ineligible, but there's nothing we
125 // can do about that here.
126 return;
128 // If the callback succeeded, the gnubby is not null.
129 var gnubby = /** @type {Gnubby} */ (opt_gnubby);
130 this.anyGnubbiesFound_ = true;
131 this.waitingForTouchGnubbies_.push(gnubby);
132 this.matchEnrollVersionToGnubby_(gnubby);
136 * Attempts to match the gnubby's U2F version with an appropriate enroll
137 * challenge.
138 * @param {Gnubby} gnubby Gnubby instance
139 * @private
141 UsbEnrollHandler.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
142 if (!gnubby) {
143 console.warn(UTIL_fmt('no gnubby, WTF?'));
144 return;
146 gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
150 * Called with the result of a version command.
151 * @param {Gnubby} gnubby Gnubby instance
152 * @param {number} rc result of version command.
153 * @param {ArrayBuffer=} data version.
154 * @private
156 UsbEnrollHandler.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
157 if (rc) {
158 this.removeWrongVersionGnubby_(gnubby);
159 return;
161 var version = UTIL_BytesToString(new Uint8Array(data || null));
162 this.tryEnroll_(gnubby, version);
166 * Drops the gnubby from the list of eligible gnubbies.
167 * @param {Gnubby} gnubby Gnubby instance
168 * @private
170 UsbEnrollHandler.prototype.removeWaitingGnubby_ = function(gnubby) {
171 gnubby.closeWhenIdle();
172 var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
173 if (index >= 0) {
174 this.waitingForTouchGnubbies_.splice(index, 1);
179 * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
180 * version.
181 * @param {Gnubby} gnubby Gnubby instance
182 * @private
184 UsbEnrollHandler.prototype.removeWrongVersionGnubby_ = function(gnubby) {
185 this.removeWaitingGnubby_(gnubby);
186 if (!this.waitingForTouchGnubbies_.length) {
187 // Whoops, this was the last gnubby.
188 this.anyGnubbiesFound_ = false;
189 if (this.timer_.expired()) {
190 this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
191 } else if (this.signer_) {
192 this.signer_.reScanDevices();
198 * Attempts enrolling a particular gnubby with a challenge of the appropriate
199 * version.
200 * @param {Gnubby} gnubby Gnubby instance
201 * @param {string} version Protocol version
202 * @private
204 UsbEnrollHandler.prototype.tryEnroll_ = function(gnubby, version) {
205 var challenge = this.getChallengeOfVersion_(version);
206 if (!challenge) {
207 this.removeWrongVersionGnubby_(gnubby);
208 return;
210 var challengeValue = B64_decode(challenge['challengeHash']);
211 var appIdHash = challenge['appIdHash'];
212 var individualAttest =
213 DEVICE_FACTORY_REGISTRY.getIndividualAttestation().
214 requestIndividualAttestation(appIdHash);
215 gnubby.enroll(challengeValue, B64_decode(appIdHash),
216 this.enrollCallback_.bind(this, gnubby, version), individualAttest);
220 * Finds the (first) challenge of the given version in this helper's challenges.
221 * @param {string} version Protocol version
222 * @return {Object} challenge, if found, or null if not.
223 * @private
225 UsbEnrollHandler.prototype.getChallengeOfVersion_ = function(version) {
226 for (var i = 0; i < this.enrollChallenges.length; i++) {
227 if (this.enrollChallenges[i]['version'] == version) {
228 return this.enrollChallenges[i];
231 return null;
235 * Called with the result of an enroll request to a gnubby.
236 * @param {Gnubby} gnubby Gnubby instance
237 * @param {string} version Protocol version
238 * @param {number} code Status code
239 * @param {ArrayBuffer=} infoArray Returned data
240 * @private
242 UsbEnrollHandler.prototype.enrollCallback_ =
243 function(gnubby, version, code, infoArray) {
244 if (this.notified_) {
245 // Enroll completed after previous success or failure. Disregard.
246 return;
248 switch (code) {
249 case -GnubbyDevice.GONE:
250 // Close this gnubby.
251 this.removeWaitingGnubby_(gnubby);
252 if (!this.waitingForTouchGnubbies_.length) {
253 // Last enroll attempt is complete and last gnubby is gone.
254 this.anyGnubbiesFound_ = false;
255 if (this.timer_.expired()) {
256 this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
257 } else if (this.signer_) {
258 this.signer_.reScanDevices();
261 break;
263 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
264 case DeviceStatusCodes.BUSY_STATUS:
265 case DeviceStatusCodes.TIMEOUT_STATUS:
266 if (this.timer_.expired()) {
267 // Record that at least one gnubby timed out, to return a timeout status
268 // from the complete callback if no other eligible gnubbies are found.
269 /** @private {boolean} */
270 this.anyTimeout_ = true;
271 // Close this gnubby.
272 this.removeWaitingGnubby_(gnubby);
273 if (!this.waitingForTouchGnubbies_.length) {
274 // Last enroll attempt is complete: return this error.
275 console.log(UTIL_fmt('timeout (' + code.toString(16) +
276 ') enrolling'));
277 this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
279 } else {
280 DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
281 UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS,
282 this.tryEnroll_.bind(this, gnubby, version));
284 break;
286 case DeviceStatusCodes.OK_STATUS:
287 var info = B64_encode(new Uint8Array(infoArray || []));
288 this.notifySuccess_(version, info);
289 break;
291 default:
292 console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
293 this.notifyError_(code);
294 break;
299 * How long to delay between repeated enroll attempts, in milliseconds.
300 * @const
302 UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS = 200;
305 * Notifies the callback with an error code.
306 * @param {number} code The error code to report.
307 * @private
309 UsbEnrollHandler.prototype.notifyError_ = function(code) {
310 if (this.notified_ || this.closed_)
311 return;
312 this.notified_ = true;
313 this.close();
314 var reply = {
315 'type': 'enroll_helper_reply',
316 'code': code
318 this.cb_(reply);
322 * @param {string} version Protocol version
323 * @param {string} info B64 encoded success data
324 * @private
326 UsbEnrollHandler.prototype.notifySuccess_ = function(version, info) {
327 if (this.notified_ || this.closed_)
328 return;
329 this.notified_ = true;
330 this.close();
331 var reply = {
332 'type': 'enroll_helper_reply',
333 'code': DeviceStatusCodes.OK_STATUS,
334 'version': version,
335 'enrollData': info
337 this.cb_(reply);