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 handler using USB gnubbies.
11 * @param {!EnrollHelperRequest} request The enroll request.
13 * @implements {RequestHandler}
15 function UsbEnrollHandler(request
) {
16 /** @private {!EnrollHelperRequest} */
17 this.request_
= request
;
19 /** @private {Array.<Gnubby>} */
20 this.waitingForTouchGnubbies_
= [];
22 /** @private {boolean} */
24 /** @private {boolean} */
25 this.notified_
= false;
29 * Default timeout value in case the caller never provides a valid timeout.
32 UsbEnrollHandler
.DEFAULT_TIMEOUT_MILLIS
= 30 * 1000;
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
) {
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(
47 this.enrollChallenges
= this.request_
.enrollChallenges
;
48 /** @private {RequestHandlerCallback} */
50 this.signer_
= new MultipleGnubbySigner(
52 this.signerCompleted_
.bind(this),
53 this.signerFoundGnubby_
.bind(this),
55 this.request_
.logMsgUrl
);
56 return this.signer_
.doSign(this.request_
.signData
);
59 /** Closes this helper. */
60 UsbEnrollHandler
.prototype.close = function() {
62 for (var i
= 0; i
< this.waitingForTouchGnubbies_
.length
; i
++) {
63 this.waitingForTouchGnubbies_
[i
].closeWhenIdle();
65 this.waitingForTouchGnubbies_
= [];
73 * Called when a MultipleGnubbySigner completes its sign request.
74 * @param {boolean} anyPending Whether any gnubbies are pending.
77 UsbEnrollHandler
.prototype.signerCompleted_ = function(anyPending
) {
78 if (!this.anyGnubbiesFound_
|| this.anyTimeout_
|| anyPending
||
79 this.timer_
.expired()) {
80 this.notifyError_(DeviceStatusCodes
.TIMEOUT_STATUS
);
82 // Do nothing: signerFoundGnubby will have been called with each succeeding
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.
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.
118 UsbEnrollHandler
.prototype.gnubbyPrerequisitesChecked_
=
119 function(rc
, opt_gnubby
) {
120 if (rc
|| this.timer_
.expired()) {
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.
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
138 * @param {Gnubby} gnubby Gnubby instance
141 UsbEnrollHandler
.prototype.matchEnrollVersionToGnubby_ = function(gnubby
) {
143 console
.warn(UTIL_fmt('no gnubby, WTF?'));
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.
156 UsbEnrollHandler
.prototype.gnubbyVersioned_ = function(gnubby
, rc
, data
) {
158 this.removeWrongVersionGnubby_(gnubby
);
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
170 UsbEnrollHandler
.prototype.removeWaitingGnubby_ = function(gnubby
) {
171 gnubby
.closeWhenIdle();
172 var index
= this.waitingForTouchGnubbies_
.indexOf(gnubby
);
174 this.waitingForTouchGnubbies_
.splice(index
, 1);
179 * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
181 * @param {Gnubby} gnubby Gnubby instance
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
200 * @param {Gnubby} gnubby Gnubby instance
201 * @param {string} version Protocol version
204 UsbEnrollHandler
.prototype.tryEnroll_ = function(gnubby
, version
) {
205 var challenge
= this.getChallengeOfVersion_(version
);
207 this.removeWrongVersionGnubby_(gnubby
);
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.
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
];
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
242 UsbEnrollHandler
.prototype.enrollCallback_
=
243 function(gnubby
, version
, code
, infoArray
) {
244 if (this.notified_
) {
245 // Enroll completed after previous success or failure. Disregard.
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();
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) +
277 this.notifyError_(DeviceStatusCodes
.TIMEOUT_STATUS
);
280 DEVICE_FACTORY_REGISTRY
.getCountdownFactory().createTimer(
281 UsbEnrollHandler
.ENUMERATE_DELAY_INTERVAL_MILLIS
,
282 this.tryEnroll_
.bind(this, gnubby
, version
));
286 case DeviceStatusCodes
.OK_STATUS
:
287 var info
= B64_encode(new Uint8Array(infoArray
|| []));
288 this.notifySuccess_(version
, info
);
292 console
.log(UTIL_fmt('Failed to enroll gnubby: ' + code
));
293 this.notifyError_(code
);
299 * How long to delay between repeated enroll attempts, in milliseconds.
302 UsbEnrollHandler
.ENUMERATE_DELAY_INTERVAL_MILLIS
= 200;
305 * Notifies the callback with an error code.
306 * @param {number} code The error code to report.
309 UsbEnrollHandler
.prototype.notifyError_ = function(code
) {
310 if (this.notified_
|| this.closed_
)
312 this.notified_
= true;
315 'type': 'enroll_helper_reply',
322 * @param {string} version Protocol version
323 * @param {string} info B64 encoded success data
326 UsbEnrollHandler
.prototype.notifySuccess_ = function(version
, info
) {
327 if (this.notified_
|| this.closed_
)
329 this.notified_
= true;
332 'type': 'enroll_helper_reply',
333 'code': DeviceStatusCodes
.OK_STATUS
,