• 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 A multiple gnubby signer wraps the process of opening a number
7 * of gnubbies, signing each challenge in an array of challenges until a
8 * success condition is satisfied, and yielding each succeeding gnubby.
9 *
10 */
11'use strict';
12
13/**
14 * @typedef {{
15 *   code: number,
16 *   gnubbyId: GnubbyDeviceId,
17 *   challenge: (SignHelperChallenge|undefined),
18 *   info: (ArrayBuffer|undefined)
19 * }}
20 */
21var MultipleSignerResult;
22
23/**
24 * Creates a new sign handler that manages signing with all the available
25 * gnubbies.
26 * @param {boolean} forEnroll Whether this signer is signing for an attempted
27 *     enroll operation.
28 * @param {function(boolean)} allCompleteCb Called when this signer completes
29 *     sign attempts, i.e. no further results will be produced. The parameter
30 *     indicates whether any gnubbies are present that have not yet produced a
31 *     final result.
32 * @param {function(MultipleSignerResult, boolean)} gnubbyCompleteCb
33 *     Called with each gnubby/challenge that yields a final result, along with
34 *     whether this signer expects to produce more results. The boolean is a
35 *     hint rather than a promise: it's possible for this signer to produce
36 *     further results after saying it doesn't expect more, or to fail to
37 *     produce further results after saying it does.
38 * @param {number} timeoutMillis A timeout value, beyond whose expiration the
39 *     signer will not attempt any new operations, assuming the caller is no
40 *     longer interested in the outcome.
41 * @param {string=} opt_logMsgUrl A URL to post log messages to.
42 * @constructor
43 */
44function MultipleGnubbySigner(forEnroll, allCompleteCb, gnubbyCompleteCb,
45    timeoutMillis, opt_logMsgUrl) {
46  /** @private {boolean} */
47  this.forEnroll_ = forEnroll;
48  /** @private {function(boolean)} */
49  this.allCompleteCb_ = allCompleteCb;
50  /** @private {function(MultipleSignerResult, boolean)} */
51  this.gnubbyCompleteCb_ = gnubbyCompleteCb;
52  /** @private {string|undefined} */
53  this.logMsgUrl_ = opt_logMsgUrl;
54
55  /** @private {Array.<SignHelperChallenge>} */
56  this.challenges_ = [];
57  /** @private {boolean} */
58  this.challengesSet_ = false;
59  /** @private {boolean} */
60  this.complete_ = false;
61  /** @private {number} */
62  this.numComplete_ = 0;
63  /** @private {!Object.<string, GnubbyTracker>} */
64  this.gnubbies_ = {};
65  /** @private {Countdown} */
66  this.timer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory()
67      .createTimer(timeoutMillis);
68  /** @private {Countdown} */
69  this.reenumerateTimer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory()
70      .createTimer(timeoutMillis);
71}
72
73/**
74 * @typedef {{
75 *   index: string,
76 *   signer: SingleGnubbySigner,
77 *   stillGoing: boolean,
78 *   errorStatus: number
79 * }}
80 */
81var GnubbyTracker;
82
83/**
84 * Closes this signer's gnubbies, if any are open.
85 */
86MultipleGnubbySigner.prototype.close = function() {
87  for (var k in this.gnubbies_) {
88    this.gnubbies_[k].signer.close();
89  }
90  this.reenumerateTimer_.clearTimeout();
91  this.timer_.clearTimeout();
92  if (this.reenumerateIntervalTimer_) {
93    this.reenumerateIntervalTimer_.clearTimeout();
94  }
95};
96
97/**
98 * Begins signing the given challenges.
99 * @param {Array.<SignHelperChallenge>} challenges The challenges to sign.
100 * @return {boolean} whether the challenges were successfully added.
101 */
102MultipleGnubbySigner.prototype.doSign = function(challenges) {
103  if (this.challengesSet_) {
104    // Can't add new challenges once they're finalized.
105    return false;
106  }
107
108  if (challenges) {
109    for (var i = 0; i < challenges.length; i++) {
110      var decodedChallenge = {};
111      var challenge = challenges[i];
112      decodedChallenge['challengeHash'] =
113          B64_decode(challenge['challengeHash']);
114      decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
115      decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
116      if (challenge['version']) {
117        decodedChallenge['version'] = challenge['version'];
118      }
119      this.challenges_.push(decodedChallenge);
120    }
121  }
122  this.challengesSet_ = true;
123  this.enumerateGnubbies_();
124  return true;
125};
126
127/**
128 * Signals this signer to rescan for gnubbies. Useful when the caller has
129 * knowledge that the last device has been removed, and can notify this class
130 * before it will discover it on its own.
131 */
132MultipleGnubbySigner.prototype.reScanDevices = function() {
133  if (this.reenumerateIntervalTimer_) {
134    this.reenumerateIntervalTimer_.clearTimeout();
135  }
136  this.maybeReEnumerateGnubbies_(true);
137};
138
139/**
140 * Enumerates gnubbies.
141 * @private
142 */
143MultipleGnubbySigner.prototype.enumerateGnubbies_ = function() {
144  DEVICE_FACTORY_REGISTRY.getGnubbyFactory().enumerate(
145      this.enumerateCallback_.bind(this));
146};
147
148/**
149 * Called with the result of enumerating gnubbies.
150 * @param {number} rc The return code from enumerating.
151 * @param {Array.<GnubbyDeviceId>} ids The gnubbies enumerated.
152 * @private
153 */
154MultipleGnubbySigner.prototype.enumerateCallback_ = function(rc, ids) {
155  if (this.complete_) {
156    return;
157  }
158  if (rc || !ids || !ids.length) {
159    this.maybeReEnumerateGnubbies_(true);
160    return;
161  }
162  for (var i = 0; i < ids.length; i++) {
163    this.addGnubby_(ids[i]);
164  }
165  this.maybeReEnumerateGnubbies_(false);
166};
167
168/**
169 * How frequently to reenumerate gnubbies when none are found, in milliseconds.
170 * @const
171 */
172MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS = 200;
173
174/**
175 * How frequently to reenumerate gnubbies when some are found, in milliseconds.
176 * @const
177 */
178MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS = 3000;
179
180/**
181 * Reenumerates gnubbies if there's still time.
182 * @param {boolean} activeScan Whether to poll more aggressively, e.g. if
183 *     there are no devices present.
184 * @private
185 */
186MultipleGnubbySigner.prototype.maybeReEnumerateGnubbies_ =
187    function(activeScan) {
188  if (this.reenumerateTimer_.expired()) {
189    // If the timer is expired, call timeout_ if there aren't any still-running
190    // gnubbies. (If there are some still running, the last will call timeout_
191    // itself.)
192    if (!this.anyPending_()) {
193      this.timeout_(false);
194    }
195    return;
196  }
197  // Reenumerate more aggressively if there are no gnubbies present than if
198  // there are any.
199  var reenumerateTimeoutMillis;
200  if (activeScan) {
201    reenumerateTimeoutMillis =
202        MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS;
203  } else {
204    reenumerateTimeoutMillis =
205        MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS;
206  }
207  if (reenumerateTimeoutMillis >
208      this.reenumerateTimer_.millisecondsUntilExpired()) {
209    reenumerateTimeoutMillis =
210        this.reenumerateTimer_.millisecondsUntilExpired();
211  }
212  /** @private {Countdown} */
213  this.reenumerateIntervalTimer_ =
214      DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
215          reenumerateTimeoutMillis, this.enumerateGnubbies_.bind(this));
216};
217
218/**
219 * Adds a new gnubby to this signer's list of gnubbies. (Only possible while
220 * this signer is still signing: without this restriction, the completed
221 * callback could be called more than once, in violation of its contract.)
222 * If this signer has challenges to sign, begins signing on the new gnubby with
223 * them.
224 * @param {GnubbyDeviceId} gnubbyId The id of the gnubby to add.
225 * @return {boolean} Whether the gnubby was added successfully.
226 * @private
227 */
228MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) {
229  var index = JSON.stringify(gnubbyId);
230  if (this.gnubbies_.hasOwnProperty(index)) {
231    // Can't add the same gnubby twice.
232    return false;
233  }
234  var tracker = {
235      index: index,
236      errorStatus: 0,
237      stillGoing: false,
238      signer: null
239  };
240  tracker.signer = new SingleGnubbySigner(
241      gnubbyId,
242      this.forEnroll_,
243      this.signCompletedCallback_.bind(this, tracker),
244      this.timer_.clone(),
245      this.logMsgUrl_);
246  this.gnubbies_[index] = tracker;
247  this.gnubbies_[index].stillGoing =
248      tracker.signer.doSign(this.challenges_);
249  if (!this.gnubbies_[index].errorStatus) {
250    this.gnubbies_[index].errorStatus = 0;
251  }
252  return true;
253};
254
255/**
256 * Called by a SingleGnubbySigner upon completion.
257 * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
258 *     this is.
259 * @param {SingleSignerResult} result The result of the sign operation.
260 * @private
261 */
262MultipleGnubbySigner.prototype.signCompletedCallback_ =
263    function(tracker, result) {
264  console.log(
265      UTIL_fmt((result.code ? 'failure.' : 'success!') +
266          ' gnubby ' + tracker.index +
267          ' got code ' + result.code.toString(16)));
268  if (!tracker.stillGoing) {
269    console.log(UTIL_fmt('gnubby ' + tracker.index + ' no longer running!'));
270    // Shouldn't ever happen? Disregard.
271    return;
272  }
273  tracker.stillGoing = false;
274  tracker.errorStatus = result.code;
275  var moreExpected = this.tallyCompletedGnubby_();
276  switch (result.code) {
277    case DeviceStatusCodes.GONE_STATUS:
278      // Squelch removed gnubbies: the caller can't act on them. But if this
279      // was the last one, speed up reenumerating.
280      if (!moreExpected) {
281        this.maybeReEnumerateGnubbies_(true);
282      }
283      break;
284
285    default:
286      // Report any other results directly to the caller.
287      this.notifyGnubbyComplete_(tracker, result, moreExpected);
288      break;
289  }
290  if (!moreExpected && this.timer_.expired()) {
291    this.timeout_(false);
292  }
293};
294
295/**
296 * Counts another gnubby has having completed, and returns whether more results
297 * are expected.
298 * @return {boolean} Whether more gnubbies are still running.
299 * @private
300 */
301MultipleGnubbySigner.prototype.tallyCompletedGnubby_ = function() {
302  this.numComplete_++;
303  return this.anyPending_();
304};
305
306/**
307 * @return {boolean} Whether more gnubbies are still running.
308 * @private
309 */
310MultipleGnubbySigner.prototype.anyPending_ = function() {
311  return this.numComplete_ < Object.keys(this.gnubbies_).length;
312};
313
314/**
315 * Called upon timeout.
316 * @param {boolean} anyPending Whether any gnubbies are awaiting results.
317 * @private
318 */
319MultipleGnubbySigner.prototype.timeout_ = function(anyPending) {
320  if (this.complete_) return;
321  this.complete_ = true;
322  // Defer notifying the caller that all are complete, in case the caller is
323  // doing work in response to a gnubbyFound callback and has an inconsistent
324  // view of the state of this signer.
325  var self = this;
326  window.setTimeout(function() {
327    self.allCompleteCb_(anyPending);
328  }, 0);
329};
330
331/**
332 * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
333 *     this is.
334 * @param {SingleSignerResult} result Result object.
335 * @param {boolean} moreExpected Whether more gnubbies may still produce an
336 *     outcome.
337 * @private
338 */
339MultipleGnubbySigner.prototype.notifyGnubbyComplete_ =
340    function(tracker, result, moreExpected) {
341  console.log(UTIL_fmt('gnubby ' + tracker.index + ' complete (' +
342      result.code.toString(16) + ')'));
343  var signResult = {
344    'code': result.code,
345    'gnubby': result.gnubby,
346    'gnubbyId': tracker.signer.getDeviceId()
347  };
348  if (result['challenge'])
349    signResult['challenge'] = result['challenge'];
350  if (result['info'])
351    signResult['info'] = result['info'];
352  this.gnubbyCompleteCb_(signResult, moreExpected);
353};
354