blob: a12c84c8a4762a48d703f94c3e5d1be483936311 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview A multiple gnubby signer wraps the process of opening a number
* of gnubbies, signing each challenge in an array of challenges until a
* success condition is satisfied, and yielding each succeeding gnubby.
*
*/
'use strict';
/**
* @typedef {{
* code: number,
* gnubbyId: GnubbyDeviceId,
* challenge: (SignHelperChallenge|undefined),
* info: (ArrayBuffer|undefined)
* }}
*/
var MultipleSignerResult;
/**
* Creates a new sign handler that manages signing with all the available
* gnubbies.
* @param {boolean} forEnroll Whether this signer is signing for an attempted
* enroll operation.
* @param {function(boolean)} allCompleteCb Called when this signer completes
* sign attempts, i.e. no further results will be produced. The parameter
* indicates whether any gnubbies are present that have not yet produced a
* final result.
* @param {function(MultipleSignerResult, boolean)} gnubbyCompleteCb
* Called with each gnubby/challenge that yields a final result, along with
* whether this signer expects to produce more results. The boolean is a
* hint rather than a promise: it's possible for this signer to produce
* further results after saying it doesn't expect more, or to fail to
* produce further results after saying it does.
* @param {number} timeoutMillis A timeout value, beyond whose expiration the
* signer will not attempt any new operations, assuming the caller is no
* longer interested in the outcome.
* @param {string=} opt_logMsgUrl A URL to post log messages to.
* @constructor
*/
function MultipleGnubbySigner(
forEnroll, allCompleteCb, gnubbyCompleteCb, timeoutMillis, opt_logMsgUrl) {
/** @private {boolean} */
this.forEnroll_ = forEnroll;
/** @private {function(boolean)} */
this.allCompleteCb_ = allCompleteCb;
/** @private {function(MultipleSignerResult, boolean)} */
this.gnubbyCompleteCb_ = gnubbyCompleteCb;
/** @private {string|undefined} */
this.logMsgUrl_ = opt_logMsgUrl;
/** @private {Array<DecodedSignHelperChallenge>} */
this.challenges_ = [];
/** @private {boolean} */
this.challengesSet_ = false;
/** @private {boolean} */
this.complete_ = false;
/** @private {number} */
this.numComplete_ = 0;
/** @private {!Object<string, GnubbyTracker>} */
this.gnubbies_ = {};
/** @private {Countdown} */
this.timer_ =
DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(timeoutMillis);
/** @private {Countdown} */
this.reenumerateTimer_ =
DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(timeoutMillis);
}
/**
* @typedef {{
* index: string,
* signer: SingleGnubbySigner,
* stillGoing: boolean,
* errorStatus: number
* }}
*/
var GnubbyTracker;
/**
* Closes this signer's gnubbies, if any are open.
*/
MultipleGnubbySigner.prototype.close = function() {
for (var k in this.gnubbies_) {
this.gnubbies_[k].signer.close();
}
this.reenumerateTimer_.clearTimeout();
this.timer_.clearTimeout();
if (this.reenumerateIntervalTimer_) {
this.reenumerateIntervalTimer_.clearTimeout();
}
};
/**
* Begins signing the given challenges.
* @param {Array<SignHelperChallenge>} challenges The challenges to sign.
* @return {boolean} whether the challenges were successfully added.
*/
MultipleGnubbySigner.prototype.doSign = function(challenges) {
if (this.challengesSet_) {
// Can't add new challenges once they're finalized.
return false;
}
if (challenges) {
for (var i = 0; i < challenges.length; i++) {
var challenge = challenges[i];
var decodedChallenge = {
challengeHash: B64_decode(challenge['challengeHash']),
appIdHash: B64_decode(challenge['appIdHash']),
keyHandle: B64_decode(challenge['keyHandle'])
};
if (challenge['version']) {
decodedChallenge['version'] = challenge['version'];
}
this.challenges_.push(decodedChallenge);
}
}
this.challengesSet_ = true;
this.enumerateGnubbies_();
return true;
};
/**
* Signals this signer to rescan for gnubbies. Useful when the caller has
* knowledge that the last device has been removed, and can notify this class
* before it will discover it on its own.
*/
MultipleGnubbySigner.prototype.reScanDevices = function() {
if (this.reenumerateIntervalTimer_) {
this.reenumerateIntervalTimer_.clearTimeout();
}
this.maybeReEnumerateGnubbies_(true);
};
/**
* Enumerates gnubbies.
* @private
*/
MultipleGnubbySigner.prototype.enumerateGnubbies_ = function() {
DEVICE_FACTORY_REGISTRY.getGnubbyFactory().enumerate(
this.enumerateCallback_.bind(this));
};
/**
* Called with the result of enumerating gnubbies.
* @param {number} rc The return code from enumerating.
* @param {Array<GnubbyDeviceId>} ids The gnubbies enumerated.
* @private
*/
MultipleGnubbySigner.prototype.enumerateCallback_ = function(rc, ids) {
if (this.complete_) {
return;
}
if (rc || !ids || !ids.length) {
this.maybeReEnumerateGnubbies_(true);
return;
}
for (var i = 0; i < ids.length; i++) {
this.addGnubby_(ids[i]);
}
this.maybeReEnumerateGnubbies_(false);
};
/**
* How frequently to reenumerate gnubbies when none are found, in milliseconds.
* @const
*/
MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS = 200;
/**
* How frequently to reenumerate gnubbies when some are found, in milliseconds.
* @const
*/
MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS = 3000;
/**
* Reenumerates gnubbies if there's still time.
* @param {boolean} activeScan Whether to poll more aggressively, e.g. if
* there are no devices present.
* @private
*/
MultipleGnubbySigner.prototype.maybeReEnumerateGnubbies_ = function(
activeScan) {
if (this.reenumerateTimer_.expired()) {
// If the timer is expired, call timeout_ if there aren't any still-running
// gnubbies. (If there are some still running, the last will call timeout_
// itself.)
if (!this.anyPending_()) {
this.timeout_(false);
}
return;
}
// Reenumerate more aggressively if there are no gnubbies present than if
// there are any.
var reenumerateTimeoutMillis;
if (activeScan) {
reenumerateTimeoutMillis =
MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS;
} else {
reenumerateTimeoutMillis =
MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS;
}
if (reenumerateTimeoutMillis >
this.reenumerateTimer_.millisecondsUntilExpired()) {
reenumerateTimeoutMillis =
this.reenumerateTimer_.millisecondsUntilExpired();
}
/** @private {Countdown} */
this.reenumerateIntervalTimer_ =
DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer(
reenumerateTimeoutMillis, this.enumerateGnubbies_.bind(this));
};
/**
* Adds a new gnubby to this signer's list of gnubbies. (Only possible while
* this signer is still signing: without this restriction, the completed
* callback could be called more than once, in violation of its contract.)
* If this signer has challenges to sign, begins signing on the new gnubby with
* them.
* @param {GnubbyDeviceId} gnubbyId The id of the gnubby to add.
* @return {boolean} Whether the gnubby was added successfully.
* @private
*/
MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) {
var index = JSON.stringify(gnubbyId);
if (this.gnubbies_.hasOwnProperty(index)) {
// Can't add the same gnubby twice.
return false;
}
var tracker = {index: index, errorStatus: 0, stillGoing: false, signer: null};
tracker.signer = new SingleGnubbySigner(
gnubbyId, this.forEnroll_,
this.signCompletedCallback_.bind(this, tracker), this.timer_.clone(),
this.logMsgUrl_);
this.gnubbies_[index] = tracker;
this.gnubbies_[index].stillGoing = tracker.signer.doSign(this.challenges_);
if (!this.gnubbies_[index].errorStatus) {
this.gnubbies_[index].errorStatus = 0;
}
return true;
};
/**
* Called by a SingleGnubbySigner upon completion.
* @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
* this is.
* @param {SingleSignerResult} result The result of the sign operation.
* @private
*/
MultipleGnubbySigner.prototype.signCompletedCallback_ = function(
tracker, result) {
console.log(UTIL_fmt(
(result.code ? 'failure.' : 'success!') + ' gnubby ' + tracker.index +
' got code ' + result.code.toString(16)));
if (!tracker.stillGoing) {
console.log(UTIL_fmt('gnubby ' + tracker.index + ' no longer running!'));
// Shouldn't ever happen? Disregard.
return;
}
tracker.stillGoing = false;
tracker.errorStatus = result.code;
var moreExpected = this.tallyCompletedGnubby_();
switch (result.code) {
case DeviceStatusCodes.GONE_STATUS:
// Squelch removed gnubbies: the caller can't act on them. But if this
// was the last one, speed up reenumerating.
if (!moreExpected) {
this.maybeReEnumerateGnubbies_(true);
}
break;
default:
// Report any other results directly to the caller.
this.notifyGnubbyComplete_(tracker, result, moreExpected);
break;
}
if (!moreExpected && this.timer_.expired()) {
this.timeout_(false);
}
};
/**
* Counts another gnubby has having completed, and returns whether more results
* are expected.
* @return {boolean} Whether more gnubbies are still running.
* @private
*/
MultipleGnubbySigner.prototype.tallyCompletedGnubby_ = function() {
this.numComplete_++;
return this.anyPending_();
};
/**
* @return {boolean} Whether more gnubbies are still running.
* @private
*/
MultipleGnubbySigner.prototype.anyPending_ = function() {
return this.numComplete_ < Object.keys(this.gnubbies_).length;
};
/**
* Called upon timeout.
* @param {boolean} anyPending Whether any gnubbies are awaiting results.
* @private
*/
MultipleGnubbySigner.prototype.timeout_ = function(anyPending) {
if (this.complete_) {
return;
}
this.complete_ = true;
// Defer notifying the caller that all are complete, in case the caller is
// doing work in response to a gnubbyFound callback and has an inconsistent
// view of the state of this signer.
var self = this;
window.setTimeout(function() {
self.allCompleteCb_(anyPending);
}, 0);
};
/**
* @param {GnubbyTracker} tracker The tracker object of the gnubby whose result
* this is.
* @param {SingleSignerResult} result Result object.
* @param {boolean} moreExpected Whether more gnubbies may still produce an
* outcome.
* @private
*/
MultipleGnubbySigner.prototype.notifyGnubbyComplete_ = function(
tracker, result, moreExpected) {
console.log(UTIL_fmt(
'gnubby ' + tracker.index + ' complete (' + result.code.toString(16) +
')'));
var signResult = {
'code': result.code,
'gnubby': result.gnubby,
'gnubbyId': tracker.signer.getDeviceId()
};
if (result['challenge']) {
signResult['challenge'] = result['challenge'];
}
if (result['info']) {
signResult['info'] = result['info'];
}
this.gnubbyCompleteCb_(signResult, moreExpected);
};