Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / usbenrollhandler.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 handler using USB gnubbies.
7  */
8 'use strict';
9
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;
18
19   /** @private {Array.<Gnubby>} */
20   this.waitingForTouchGnubbies_ = [];
21
22   /** @private {boolean} */
23   this.closed_ = false;
24   /** @private {boolean} */
25   this.notified_ = false;
26 }
27
28 /**
29  * Default timeout value in case the caller never provides a valid timeout.
30  * @const
31  */
32 UsbEnrollHandler.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
33
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);
57 };
58
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   }
70 };
71
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   }
85 };
86
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 (signResult.code == DeviceStatusCodes.WRONG_DATA_STATUS) {
102     var gnubby = signResult['gnubby'];
103     // A valid helper request contains at least one enroll challenge, so use
104     // the app id hash from the first challenge.
105     var appIdHash = this.request_.enrollChallenges[0].appIdHash;
106     DEVICE_FACTORY_REGISTRY.getGnubbyFactory().notEnrolledPrerequisiteCheck(
107         gnubby, appIdHash, this.gnubbyPrerequisitesChecked_.bind(this));
108   }
109 };
110
111 /**
112  * Called with the result of a gnubby prerequisite check.
113  * @param {number} rc The result of the prerequisite check.
114  * @param {Gnubby=} opt_gnubby The gnubby whose prerequisites were checked.
115  * @private
116  */
117 UsbEnrollHandler.prototype.gnubbyPrerequisitesChecked_ =
118     function(rc, opt_gnubby) {
119   if (rc || this.timer_.expired()) {
120     // Do nothing:
121     // If the timer is expired, the signerCompleted_ callback will indicate
122     // timeout to the caller.
123     // If there's an error, this gnubby is ineligible, but there's nothing we
124     // can do about that here.
125     return;
126   }
127   // If the callback succeeded, the gnubby is not null.
128   var gnubby = /** @type {Gnubby} */ (opt_gnubby);
129   this.anyGnubbiesFound_ = true;
130   this.waitingForTouchGnubbies_.push(gnubby);
131   this.matchEnrollVersionToGnubby_(gnubby);
132 };
133
134 /**
135  * Attempts to match the gnubby's U2F version with an appropriate enroll
136  * challenge.
137  * @param {Gnubby} gnubby Gnubby instance
138  * @private
139  */
140 UsbEnrollHandler.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
141   if (!gnubby) {
142     console.warn(UTIL_fmt('no gnubby, WTF?'));
143     return;
144   }
145   gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
146 };
147
148 /**
149  * Called with the result of a version command.
150  * @param {Gnubby} gnubby Gnubby instance
151  * @param {number} rc result of version command.
152  * @param {ArrayBuffer=} data version.
153  * @private
154  */
155 UsbEnrollHandler.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
156   if (rc) {
157     this.removeWrongVersionGnubby_(gnubby);
158     return;
159   }
160   var version = UTIL_BytesToString(new Uint8Array(data || null));
161   this.tryEnroll_(gnubby, version);
162 };
163
164 /**
165  * Drops the gnubby from the list of eligible gnubbies.
166  * @param {Gnubby} gnubby Gnubby instance
167  * @private
168  */
169 UsbEnrollHandler.prototype.removeWaitingGnubby_ = function(gnubby) {
170   gnubby.closeWhenIdle();
171   var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
172   if (index >= 0) {
173     this.waitingForTouchGnubbies_.splice(index, 1);
174   }
175 };
176
177 /**
178  * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
179  * version.
180  * @param {Gnubby} gnubby Gnubby instance
181  * @private
182  */
183 UsbEnrollHandler.prototype.removeWrongVersionGnubby_ = function(gnubby) {
184   this.removeWaitingGnubby_(gnubby);
185   if (!this.waitingForTouchGnubbies_.length) {
186     // Whoops, this was the last gnubby.
187     this.anyGnubbiesFound_ = false;
188     if (this.timer_.expired()) {
189       this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
190     } else if (this.signer_) {
191       this.signer_.reScanDevices();
192     }
193   }
194 };
195
196 /**
197  * Attempts enrolling a particular gnubby with a challenge of the appropriate
198  * version.
199  * @param {Gnubby} gnubby Gnubby instance
200  * @param {string} version Protocol version
201  * @private
202  */
203 UsbEnrollHandler.prototype.tryEnroll_ = function(gnubby, version) {
204   var challenge = this.getChallengeOfVersion_(version);
205   if (!challenge) {
206     this.removeWrongVersionGnubby_(gnubby);
207     return;
208   }
209   var challengeChallenge = B64_decode(challenge['challenge']);
210   var appIdHash = B64_decode(challenge['appIdHash']);
211   gnubby.enroll(challengeChallenge, appIdHash,
212       this.enrollCallback_.bind(this, gnubby, version));
213 };
214
215 /**
216  * Finds the (first) challenge of the given version in this helper's challenges.
217  * @param {string} version Protocol version
218  * @return {Object} challenge, if found, or null if not.
219  * @private
220  */
221 UsbEnrollHandler.prototype.getChallengeOfVersion_ = function(version) {
222   for (var i = 0; i < this.enrollChallenges.length; i++) {
223     if (this.enrollChallenges[i]['version'] == version) {
224       return this.enrollChallenges[i];
225     }
226   }
227   return null;
228 };
229
230 /**
231  * Called with the result of an enroll request to a gnubby.
232  * @param {Gnubby} gnubby Gnubby instance
233  * @param {string} version Protocol version
234  * @param {number} code Status code
235  * @param {ArrayBuffer=} infoArray Returned data
236  * @private
237  */
238 UsbEnrollHandler.prototype.enrollCallback_ =
239     function(gnubby, version, code, infoArray) {
240   if (this.notified_) {
241     // Enroll completed after previous success or failure. Disregard.
242     return;
243   }
244   switch (code) {
245     case -GnubbyDevice.GONE:
246         // Close this gnubby.
247         this.removeWaitingGnubby_(gnubby);
248         if (!this.waitingForTouchGnubbies_.length) {
249           // Last enroll attempt is complete and last gnubby is gone.
250           this.anyGnubbiesFound_ = false;
251           if (this.timer_.expired()) {
252             this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
253           } else if (this.signer_) {
254             this.signer_.reScanDevices();
255           }
256         }
257       break;
258
259     case DeviceStatusCodes.WAIT_TOUCH_STATUS:
260     case DeviceStatusCodes.BUSY_STATUS:
261     case DeviceStatusCodes.TIMEOUT_STATUS:
262       if (this.timer_.expired()) {
263         // Record that at least one gnubby timed out, to return a timeout status
264         // from the complete callback if no other eligible gnubbies are found.
265         /** @private {boolean} */
266         this.anyTimeout_ = true;
267         // Close this gnubby.
268         this.removeWaitingGnubby_(gnubby);
269         if (!this.waitingForTouchGnubbies_.length) {
270           // Last enroll attempt is complete: return this error.
271           console.log(UTIL_fmt('timeout (' + code.toString(16) +
272               ') enrolling'));
273           this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS);
274         }
275       } else {
276         DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
277             UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS,
278             this.tryEnroll_.bind(this, gnubby, version));
279       }
280       break;
281
282     case DeviceStatusCodes.OK_STATUS:
283       var info = B64_encode(new Uint8Array(infoArray || []));
284       this.notifySuccess_(version, info);
285       break;
286
287     default:
288       console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
289       this.notifyError_(code);
290       break;
291   }
292 };
293
294 /**
295  * How long to delay between repeated enroll attempts, in milliseconds.
296  * @const
297  */
298 UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS = 200;
299
300 /**
301  * Notifies the callback with an error code.
302  * @param {number} code The error code to report.
303  * @private
304  */
305 UsbEnrollHandler.prototype.notifyError_ = function(code) {
306   if (this.notified_ || this.closed_)
307     return;
308   this.notified_ = true;
309   this.close();
310   var reply = {
311     'type': 'enroll_helper_reply',
312     'code': code
313   };
314   this.cb_(reply);
315 };
316
317 /**
318  * @param {string} version Protocol version
319  * @param {string} info B64 encoded success data
320  * @private
321  */
322 UsbEnrollHandler.prototype.notifySuccess_ = function(version, info) {
323   if (this.notified_ || this.closed_)
324     return;
325   this.notified_ = true;
326   this.close();
327   var reply = {
328     'type': 'enroll_helper_reply',
329     'code': DeviceStatusCodes.OK_STATUS,
330     'version': version,
331     'enrollData': info
332   };
333   this.cb_(reply);
334 };