• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
15function 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 */
32UsbEnrollHandler.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 */
39UsbEnrollHandler.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. */
60UsbEnrollHandler.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 */
77UsbEnrollHandler.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 */
94UsbEnrollHandler.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 */
118UsbEnrollHandler.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 */
141UsbEnrollHandler.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 */
156UsbEnrollHandler.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 */
170UsbEnrollHandler.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 */
184UsbEnrollHandler.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 */
204UsbEnrollHandler.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 */
225UsbEnrollHandler.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 */
242UsbEnrollHandler.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 */
302UsbEnrollHandler.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 */
309UsbEnrollHandler.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 */
326UsbEnrollHandler.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};
339