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.
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.
16 * gnubbyId: GnubbyDeviceId,
17 * challenge: (SignHelperChallenge|undefined),
18 * info: (ArrayBuffer|undefined)
21 var MultipleSignerResult;
24 * Creates a new sign handler that manages signing with all the available
26 * @param {boolean} forEnroll Whether this signer is signing for an attempted
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
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.
44 function 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;
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<GnubbyTracker>} */
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);
76 * signer: SingleGnubbySigner,
77 * stillGoing: boolean,
84 * Closes this signer's gnubbies, if any are open.
86 MultipleGnubbySigner.prototype.close = function() {
87 for (var k in this.gnubbies_) {
88 this.gnubbies_[k].signer.close();
90 this.reenumerateTimer_.clearTimeout();
91 this.timer_.clearTimeout();
92 if (this.reenumerateIntervalTimer_) {
93 this.reenumerateIntervalTimer_.clearTimeout();
98 * Begins signing the given challenges.
99 * @param {Array<SignHelperChallenge>} challenges The challenges to sign.
100 * @return {boolean} whether the challenges were successfully added.
102 MultipleGnubbySigner.prototype.doSign = function(challenges) {
103 if (this.challengesSet_) {
104 // Can't add new challenges once they're finalized.
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'];
119 this.challenges_.push(decodedChallenge);
122 this.challengesSet_ = true;
123 this.enumerateGnubbies_();
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.
132 MultipleGnubbySigner.prototype.reScanDevices = function() {
133 if (this.reenumerateIntervalTimer_) {
134 this.reenumerateIntervalTimer_.clearTimeout();
136 this.maybeReEnumerateGnubbies_(true);
140 * Enumerates gnubbies.
143 MultipleGnubbySigner.prototype.enumerateGnubbies_ = function() {
144 DEVICE_FACTORY_REGISTRY.getGnubbyFactory().enumerate(
145 this.enumerateCallback_.bind(this));
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.
154 MultipleGnubbySigner.prototype.enumerateCallback_ = function(rc, ids) {
155 if (this.complete_) {
158 if (rc || !ids || !ids.length) {
159 this.maybeReEnumerateGnubbies_(true);
162 for (var i = 0; i < ids.length; i++) {
163 this.addGnubby_(ids[i]);
165 this.maybeReEnumerateGnubbies_(false);
169 * How frequently to reenumerate gnubbies when none are found, in milliseconds.
172 MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS = 200;
175 * How frequently to reenumerate gnubbies when some are found, in milliseconds.
178 MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS = 3000;
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.
186 MultipleGnubbySigner.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_
192 if (!this.anyPending_()) {
193 this.timeout_(false);
197 // Reenumerate more aggressively if there are no gnubbies present than if
199 var reenumerateTimeoutMillis;
201 reenumerateTimeoutMillis =
202 MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS;
204 reenumerateTimeoutMillis =
205 MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS;
207 if (reenumerateTimeoutMillis >
208 this.reenumerateTimer_.millisecondsUntilExpired()) {
209 reenumerateTimeoutMillis =
210 this.reenumerateTimer_.millisecondsUntilExpired();
212 /** @private {Countdown} */
213 this.reenumerateIntervalTimer_ =
214 DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
215 reenumerateTimeoutMillis, this.enumerateGnubbies_.bind(this));
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
224 * @param {GnubbyDeviceId} gnubbyId The id of the gnubby to add.
225 * @return {boolean} Whether the gnubby was added successfully.
228 MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) {
229 var index = JSON.stringify(gnubbyId);
230 if (this.gnubbies_.hasOwnProperty(index)) {
231 // Can't add the same gnubby twice.
240 tracker.signer = new SingleGnubbySigner(
243 this.signCompletedCallback_.bind(this, tracker),
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;
256 * Called by a SingleGnubbySigner upon completion.
257 * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
259 * @param {SingleSignerResult} result The result of the sign operation.
262 MultipleGnubbySigner.prototype.signCompletedCallback_ =
263 function(tracker, result) {
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.
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.
281 this.maybeReEnumerateGnubbies_(true);
286 // Report any other results directly to the caller.
287 this.notifyGnubbyComplete_(tracker, result, moreExpected);
290 if (!moreExpected && this.timer_.expired()) {
291 this.timeout_(false);
296 * Counts another gnubby has having completed, and returns whether more results
298 * @return {boolean} Whether more gnubbies are still running.
301 MultipleGnubbySigner.prototype.tallyCompletedGnubby_ = function() {
303 return this.anyPending_();
307 * @return {boolean} Whether more gnubbies are still running.
310 MultipleGnubbySigner.prototype.anyPending_ = function() {
311 return this.numComplete_ < Object.keys(this.gnubbies_).length;
315 * Called upon timeout.
316 * @param {boolean} anyPending Whether any gnubbies are awaiting results.
319 MultipleGnubbySigner.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.
326 window.setTimeout(function() {
327 self.allCompleteCb_(anyPending);
332 * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
334 * @param {SingleSignerResult} result Result object.
335 * @param {boolean} moreExpected Whether more gnubbies may still produce an
339 MultipleGnubbySigner.prototype.notifyGnubbyComplete_ =
340 function(tracker, result, moreExpected) {
341 console.log(UTIL_fmt('gnubby ' + tracker.index + ' complete (' +
342 result.code.toString(16) + ')'));
345 'gnubby': result.gnubby,
346 'gnubbyId': tracker.signer.getDeviceId()
348 if (result['challenge'])
349 signResult['challenge'] = result['challenge'];
351 signResult['info'] = result['info'];
352 this.gnubbyCompleteCb_(signResult, moreExpected);