Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / usbenrollhandler.js
blobcf113d2ae22442283eab1dd0a19163cf6a3d6531
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}
14  */
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
31  */
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.
38  */
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();
64   }
65   this.waitingForTouchGnubbies_ = [];
66   if (this.signer_) {
67     this.signer_.close();
68     this.signer_ = null;
69   }
72 /**
73  * Called when a MultipleGnubbySigner completes its sign request.
74  * @param {boolean} anyPending Whether any gnubbies are pending.
75  * @private
76  */
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.
84   }
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
93  */
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 (SingleGnubbySigner.signErrorIndicatesInvalidKeyHandle(
102       signResult.code)) {
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));
109   } else {
110     // Unexpected error in signing? Send this immediately to the caller.
111     this.notifyError_(signResult.code);
112   }
116  * Called with the result of a gnubby prerequisite check.
117  * @param {number} rc The result of the prerequisite check.
118  * @param {Gnubby=} opt_gnubby The gnubby whose prerequisites were checked.
119  * @private
120  */
121 UsbEnrollHandler.prototype.gnubbyPrerequisitesChecked_ =
122     function(rc, opt_gnubby) {
123   if (rc || this.timer_.expired()) {
124     // Do nothing:
125     // If the timer is expired, the signerCompleted_ callback will indicate
126     // timeout to the caller.
127     // If there's an error, this gnubby is ineligible, but there's nothing we
128     // can do about that here.
129     return;
130   }
131   // If the callback succeeded, the gnubby is not null.
132   var gnubby = /** @type {Gnubby} */ (opt_gnubby);
133   this.anyGnubbiesFound_ = true;
134   this.waitingForTouchGnubbies_.push(gnubby);
135   this.matchEnrollVersionToGnubby_(gnubby);
139  * Attempts to match the gnubby's U2F version with an appropriate enroll
140  * challenge.
141  * @param {Gnubby} gnubby Gnubby instance
142  * @private
143  */
144 UsbEnrollHandler.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
145   if (!gnubby) {
146     console.warn(UTIL_fmt('no gnubby, WTF?'));
147     return;
148   }
149   gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
153  * Called with the result of a version command.
154  * @param {Gnubby} gnubby Gnubby instance
155  * @param {number} rc result of version command.
156  * @param {ArrayBuffer=} data version.
157  * @private
158  */
159 UsbEnrollHandler.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
160   if (rc) {
161     this.removeWrongVersionGnubby_(gnubby);
162     return;
163   }
164   var version = UTIL_BytesToString(new Uint8Array(data || null));
165   this.tryEnroll_(gnubby, version);
169  * Drops the gnubby from the list of eligible gnubbies.
170  * @param {Gnubby} gnubby Gnubby instance
171  * @private
172  */
173 UsbEnrollHandler.prototype.removeWaitingGnubby_ = function(gnubby) {
174   gnubby.closeWhenIdle();
175   var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
176   if (index >= 0) {
177     this.waitingForTouchGnubbies_.splice(index, 1);
178   }
182  * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
183  * version.
184  * @param {Gnubby} gnubby Gnubby instance
185  * @private
186  */
187 UsbEnrollHandler.prototype.removeWrongVersionGnubby_ = function(gnubby) {
188   this.removeWaitingGnubby_(gnubby);
189   if (!this.waitingForTouchGnubbies_.length) {
190     // Whoops, this was the last gnubby.
191     this.anyGnubbiesFound_ = false;
192     if (this.timer_.expired()) {
193       this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
194     } else if (this.signer_) {
195       this.signer_.reScanDevices();
196     }
197   }
201  * Attempts enrolling a particular gnubby with a challenge of the appropriate
202  * version.
203  * @param {Gnubby} gnubby Gnubby instance
204  * @param {string} version Protocol version
205  * @private
206  */
207 UsbEnrollHandler.prototype.tryEnroll_ = function(gnubby, version) {
208   var challenge = this.getChallengeOfVersion_(version);
209   if (!challenge) {
210     this.removeWrongVersionGnubby_(gnubby);
211     return;
212   }
213   var challengeValue = B64_decode(challenge['challengeHash']);
214   var appIdHash = challenge['appIdHash'];
215   var individualAttest =
216       DEVICE_FACTORY_REGISTRY.getIndividualAttestation().
217           requestIndividualAttestation(appIdHash);
218   gnubby.enroll(challengeValue, B64_decode(appIdHash),
219       this.enrollCallback_.bind(this, gnubby, version), individualAttest);
223  * Finds the (first) challenge of the given version in this helper's challenges.
224  * @param {string} version Protocol version
225  * @return {Object} challenge, if found, or null if not.
226  * @private
227  */
228 UsbEnrollHandler.prototype.getChallengeOfVersion_ = function(version) {
229   for (var i = 0; i < this.enrollChallenges.length; i++) {
230     if (this.enrollChallenges[i]['version'] == version) {
231       return this.enrollChallenges[i];
232     }
233   }
234   return null;
238  * Called with the result of an enroll request to a gnubby.
239  * @param {Gnubby} gnubby Gnubby instance
240  * @param {string} version Protocol version
241  * @param {number} code Status code
242  * @param {ArrayBuffer=} infoArray Returned data
243  * @private
244  */
245 UsbEnrollHandler.prototype.enrollCallback_ =
246     function(gnubby, version, code, infoArray) {
247   if (this.notified_) {
248     // Enroll completed after previous success or failure. Disregard.
249     return;
250   }
251   switch (code) {
252     case -GnubbyDevice.GONE:
253         // Close this gnubby.
254         this.removeWaitingGnubby_(gnubby);
255         if (!this.waitingForTouchGnubbies_.length) {
256           // Last enroll attempt is complete and last gnubby is gone.
257           this.anyGnubbiesFound_ = false;
258           if (this.timer_.expired()) {
259             this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
260           } else if (this.signer_) {
261             this.signer_.reScanDevices();
262           }
263         }
264       break;
266     case DeviceStatusCodes.WAIT_TOUCH_STATUS:
267     case DeviceStatusCodes.BUSY_STATUS:
268     case DeviceStatusCodes.TIMEOUT_STATUS:
269       if (this.timer_.expired()) {
270         // Record that at least one gnubby timed out, to return a timeout status
271         // from the complete callback if no other eligible gnubbies are found.
272         /** @private {boolean} */
273         this.anyTimeout_ = true;
274         // Close this gnubby.
275         this.removeWaitingGnubby_(gnubby);
276         if (!this.waitingForTouchGnubbies_.length) {
277           // Last enroll attempt is complete: return this error.
278           console.log(UTIL_fmt('timeout (' + code.toString(16) +
279               ') enrolling'));
280           this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
281         }
282       } else {
283         DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
284             UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS,
285             this.tryEnroll_.bind(this, gnubby, version));
286       }
287       break;
289     case DeviceStatusCodes.OK_STATUS:
290       var info = B64_encode(new Uint8Array(infoArray || []));
291       this.notifySuccess_(version, info);
292       break;
294     default:
295       console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
296       this.notifyError_(code);
297       break;
298   }
302  * How long to delay between repeated enroll attempts, in milliseconds.
303  * @const
304  */
305 UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS = 200;
308  * Notifies the callback with an error code.
309  * @param {number} code The error code to report.
310  * @private
311  */
312 UsbEnrollHandler.prototype.notifyError_ = function(code) {
313   if (this.notified_ || this.closed_)
314     return;
315   this.notified_ = true;
316   this.close();
317   var reply = {
318     'type': 'enroll_helper_reply',
319     'code': code
320   };
321   this.cb_(reply);
325  * @param {string} version Protocol version
326  * @param {string} info B64 encoded success data
327  * @private
328  */
329 UsbEnrollHandler.prototype.notifySuccess_ = function(version, info) {
330   if (this.notified_ || this.closed_)
331     return;
332   this.notified_ = true;
333   this.close();
334   var reply = {
335     'type': 'enroll_helper_reply',
336     'code': DeviceStatusCodes.OK_STATUS,
337     'version': version,
338     'enrollData': info
339   };
340   this.cb_(reply);