Upstream version 10.39.225.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       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));
109   }
110 };
111
112 /**
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
117  */
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;
127   }
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);
133 };
134
135 /**
136  * Attempts to match the gnubby's U2F version with an appropriate enroll
137  * challenge.
138  * @param {Gnubby} gnubby Gnubby instance
139  * @private
140  */
141 UsbEnrollHandler.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
142   if (!gnubby) {
143     console.warn(UTIL_fmt('no gnubby, WTF?'));
144     return;
145   }
146   gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
147 };
148
149 /**
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
155  */
156 UsbEnrollHandler.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
157   if (rc) {
158     this.removeWrongVersionGnubby_(gnubby);
159     return;
160   }
161   var version = UTIL_BytesToString(new Uint8Array(data || null));
162   this.tryEnroll_(gnubby, version);
163 };
164
165 /**
166  * Drops the gnubby from the list of eligible gnubbies.
167  * @param {Gnubby} gnubby Gnubby instance
168  * @private
169  */
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);
175   }
176 };
177
178 /**
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
183  */
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();
193     }
194   }
195 };
196
197 /**
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
203  */
204 UsbEnrollHandler.prototype.tryEnroll_ = function(gnubby, version) {
205   var challenge = this.getChallengeOfVersion_(version);
206   if (!challenge) {
207     this.removeWrongVersionGnubby_(gnubby);
208     return;
209   }
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);
217 };
218
219 /**
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
224  */
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];
229     }
230   }
231   return null;
232 };
233
234 /**
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
241  */
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;
247   }
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();
259           }
260         }
261       break;
262
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);
278         }
279       } else {
280         DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
281             UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS,
282             this.tryEnroll_.bind(this, gnubby, version));
283       }
284       break;
285
286     case DeviceStatusCodes.OK_STATUS:
287       var info = B64_encode(new Uint8Array(infoArray || []));
288       this.notifySuccess_(version, info);
289       break;
290
291     default:
292       console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
293       this.notifyError_(code);
294       break;
295   }
296 };
297
298 /**
299  * How long to delay between repeated enroll attempts, in milliseconds.
300  * @const
301  */
302 UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS = 200;
303
304 /**
305  * Notifies the callback with an error code.
306  * @param {number} code The error code to report.
307  * @private
308  */
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
317   };
318   this.cb_(reply);
319 };
320
321 /**
322  * @param {string} version Protocol version
323  * @param {string} info B64 encoded success data
324  * @private
325  */
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
336   };
337   this.cb_(reply);
338 };