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<string, 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
);