Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / usbenrollhelper.js
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.
4
5 /**
6  * @fileoverview Implements an enroll helper using USB gnubbies.
7  */
8 'use strict';
9
10 /**
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
17  *     provided it.
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.
21  * @constructor
22  * @implements {EnrollHelper}
23  */
24 function UsbEnrollHelper(factory, timer, errorCb, successCb, opt_progressCb,
25     opt_logMsgUrl) {
26   /** @private {!GnubbyFactory} */
27   this.factory_ = factory;
28   /** @private {!Countdown} */
29   this.timer_ = timer;
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;
38
39   /** @private {Array.<SignHelperChallenge>} */
40   this.signChallenges_ = [];
41   /** @private {boolean} */
42   this.signChallengesFinal_ = false;
43   /** @private {Array.<usbGnubby>} */
44   this.waitingForTouchGnubbies_ = [];
45
46   /** @private {boolean} */
47   this.closed_ = false;
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_();
55 }
56
57 /**
58  * Attempts to enroll using the provided data.
59  * @param {Object} enrollChallenges a map of version string to enroll
60  *     challenges.
61  * @param {Array.<SignHelperChallenge>} signChallenges a list of sign
62  *     challenges for already enrolled gnubbies, to prevent double-enrolling a
63  *     device.
64  */
65 UsbEnrollHelper.prototype.doEnroll =
66     function(enrollChallenges, signChallenges) {
67   this.enrollChallenges = enrollChallenges;
68   this.signChallengesFinal_ = true;
69   if (this.signer_) {
70     this.signer_.addEncodedChallenges(
71         signChallenges, this.signChallengesFinal_);
72   } else {
73     this.signChallenges_ = signChallenges;
74   }
75 };
76
77 /** Closes this helper. */
78 UsbEnrollHelper.prototype.close = function() {
79   this.closed_ = true;
80   for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
81     this.waitingForTouchGnubbies_[i].closeWhenIdle();
82   }
83   this.waitingForTouchGnubbies_ = [];
84   if (this.signer_) {
85     this.signer_.close();
86     this.signer_ = null;
87   }
88 };
89
90 /**
91  * Enumerates gnubbies, and begins processing challenges upon enumeration if
92  * any gnubbies are found.
93  * @private
94  */
95 UsbEnrollHelper.prototype.getSomeGnubbies_ = function() {
96   this.factory_.enumerate(this.enumerateCallback_.bind(this));
97 };
98
99 /**
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
103  * @private
104  */
105 UsbEnrollHelper.prototype.enumerateCallback_ = function(rc, indexes) {
106   if (rc) {
107     // Enumerate failure is rare enough that it might be worth reporting
108     // directly, rather than trying again.
109     this.errorCb_(rc, false);
110     return;
111   }
112   if (!indexes.length) {
113     this.maybeReEnumerateGnubbies_();
114     return;
115   }
116   if (this.timer_.expired()) {
117     this.errorCb_(DeviceStatusCodes.TIMEOUT_STATUS, true);
118     return;
119   }
120   this.gotSomeGnubbies_(indexes);
121 };
122
123 /**
124  * If there's still time, re-enumerates devices and try with them. Otherwise
125  * reports an error and, implicitly, stops the enroll operation.
126  * @private
127  */
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);
134     var self = this;
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);
139       } else {
140         self.getSomeGnubbies_();
141       }
142     }, 200);
143   } else {
144     this.notifyError_(errorCode, anyGnubbies);
145   }
146 };
147
148 /**
149  * Called with the result of enumerating gnubby indexes.
150  * @param {Array.<llGnubbyDeviceId>} indexes Device ids of enumerated gnubbies
151  * @private
152  */
153 UsbEnrollHelper.prototype.gotSomeGnubbies_ = function(indexes) {
154   this.signer_ = new MultipleGnubbySigner(
155       this.factory_,
156       indexes,
157       true /* forEnroll */,
158       this.signerCompleted_.bind(this),
159       this.signerFoundGnubby_.bind(this),
160       this.timer_,
161       this.logMsgUrl_);
162   if (this.signChallengesFinal_) {
163     this.signer_.addEncodedChallenges(
164         this.signChallenges_, this.signChallengesFinal_);
165     this.pendingSignChallenges_ = [];
166   }
167 };
168
169 /**
170  * Called when a MultipleGnubbySigner completes its sign request.
171  * @param {boolean} anySucceeded whether any sign attempt completed
172  *     successfully.
173  * @param {number=} errorCode an error code from a failing gnubby, if one was
174  *     found.
175  * @private
176  */
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;
183   if (!anySucceeded) {
184     if (errorCode == -llGnubby.GONE) {
185       // If the last gnubby was removed, report as though no gnubbies were
186       // found.
187       this.maybeReEnumerateGnubbies_();
188     } else {
189       if (!errorCode) errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
190       this.notifyError_(errorCode, anyGnubbies);
191     }
192   } else if (this.anyTimeout) {
193     // Some previously succeeding gnubby timed out: return its error code.
194     this.notifyError_(this.timeoutError, anyGnubbies);
195   } else {
196     // Do nothing: signerFoundGnubby will have been called with each succeeding
197     // gnubby.
198   }
199 };
200
201 /**
202  * Called when a MultipleGnubbySigner finds a gnubby that can enroll.
203  * @param {number} code Status code
204  * @param {MultipleSignerResult} signResult Signature results
205  * @private
206  */
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);
217     } else {
218       this.matchEnrollVersionToGnubby_(gnubby);
219     }
220   }
221 };
222
223 /**
224  * Attempts to match the gnubby's U2F version with an appropriate enroll
225  * challenge.
226  * @param {usbGnubby} gnubby Gnubby instance
227  * @private
228  */
229 UsbEnrollHelper.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
230   if (!gnubby) {
231     console.warn(UTIL_fmt('no gnubby, WTF?'));
232   }
233   gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
234 };
235
236 /**
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.
241  * @private
242  */
243 UsbEnrollHelper.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
244   if (rc) {
245     this.removeWrongVersionGnubby_(gnubby);
246     return;
247   }
248   var version = UTIL_BytesToString(new Uint8Array(data || null));
249   this.tryEnroll_(gnubby, version);
250 };
251
252 /**
253  * Drops the gnubby from the list of eligible gnubbies.
254  * @param {usbGnubby} gnubby Gnubby instance
255  * @private
256  */
257 UsbEnrollHelper.prototype.removeWaitingGnubby_ = function(gnubby) {
258   gnubby.closeWhenIdle();
259   var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
260   if (index >= 0) {
261     this.waitingForTouchGnubbies_.splice(index, 1);
262   }
263 };
264
265 /**
266  * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
267  * version.
268  * @param {usbGnubby} gnubby Gnubby instance
269  * @private
270  */
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);
276   }
277 };
278
279 /**
280  * Attempts enrolling a particular gnubby with a challenge of the appropriate
281  * version.
282  * @param {usbGnubby} gnubby Gnubby instance
283  * @param {string} version Protocol version
284  * @private
285  */
286 UsbEnrollHelper.prototype.tryEnroll_ = function(gnubby, version) {
287   var challenge = this.getChallengeOfVersion_(version);
288   if (!challenge) {
289     this.removeWrongVersionGnubby_(gnubby);
290     return;
291   }
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));
296 };
297
298 /**
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.
302  * @private
303  */
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];
308     }
309   }
310   return null;
311 };
312
313 /**
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
319  * @private
320  */
321 UsbEnrollHelper.prototype.enrollCallback_ =
322     function(gnubby, version, code, infoArray) {
323   if (this.notified_) {
324     // Enroll completed after previous success or failure. Disregard.
325     return;
326   }
327   switch (code) {
328     case -llGnubby.GONE:
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
333           // possible.
334           this.maybeReEnumerateGnubbies_();
335         }
336       break;
337
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) +
351               ') enrolling'));
352           this.notifyError_(code, true);
353         }
354       } else {
355         // Notify caller of waiting for touch events.
356         if (code == DeviceStatusCodes.WAIT_TOUCH_STATUS) {
357           this.notifyProgress_(code, true);
358         }
359         window.setTimeout(this.tryEnroll_.bind(this, gnubby, version), 200);
360       }
361       break;
362
363     case DeviceStatusCodes.OK_STATUS:
364       var info = B64_encode(new Uint8Array(infoArray || []));
365       this.notifySuccess_(version, info);
366       break;
367
368     default:
369       console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
370       this.notifyError_(code, true);
371       break;
372   }
373 };
374
375 /**
376  * @param {number} code Status code
377  * @param {boolean} anyGnubbies If any gnubbies were found
378  * @private
379  */
380 UsbEnrollHelper.prototype.notifyError_ = function(code, anyGnubbies) {
381   if (this.notified_ || this.closed_)
382     return;
383   this.notified_ = true;
384   this.close();
385   this.errorCb_(code, anyGnubbies);
386 };
387
388 /**
389  * @param {string} version Protocol version
390  * @param {string} info B64 encoded success data
391  * @private
392  */
393 UsbEnrollHelper.prototype.notifySuccess_ = function(version, info) {
394   if (this.notified_ || this.closed_)
395     return;
396   this.notified_ = true;
397   this.close();
398   this.successCb_(version, info);
399 };
400
401 /**
402  * @param {number} code Status code
403  * @param {boolean} anyGnubbies If any gnubbies were found
404  * @private
405  */
406 UsbEnrollHelper.prototype.notifyProgress_ = function(code, anyGnubbies) {
407   if (this.lastProgressUpdate_ == code || this.notified_ || this.closed_)
408     return;
409   this.lastProgressUpdate_ = code;
410   if (this.progressCb_) this.progressCb_(code, anyGnubbies);
411 };
412
413 /**
414  * @param {!GnubbyFactory} gnubbyFactory factory to create gnubbies.
415  * @constructor
416  * @implements {EnrollHelperFactory}
417  */
418 function UsbEnrollHelperFactory(gnubbyFactory) {
419   /** @private {!GnubbyFactory} */
420   this.gnubbyFactory_ = gnubbyFactory;
421 }
422
423 /**
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
429  *     provided it.
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.
434  */
435 UsbEnrollHelperFactory.prototype.createHelper =
436     function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {
437   var helper =
438       new UsbEnrollHelper(this.gnubbyFactory_, timer, errorCb, successCb,
439           opt_progressCb, opt_logMsgUrl);
440   return helper;
441 };