blob: 1f22c71cec99c3636ee7e45dede3382d2f593d88 [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';
/**
* Creates a new sign handler with an array of gnubby indexes.
* @param {!GnubbyFactory} factory Used to create and open the gnubbies.
* @param {Array.<llGnubbyDeviceId>} gnubbyIndexes Which gnubbies to open.
* @param {boolean} forEnroll Whether this signer is signing for an attempted
* enroll operation.
* @param {function(boolean, (number|undefined))} completedCb Called when this
* signer completes sign attempts, i.e. no further results should be
* expected.
* @param {function(number, MultipleSignerResult)} gnubbyFoundCb Called with
* each gnubby/challenge that yields a successful result.
* @param {Countdown=} opt_timer An advisory timer, 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(factory, gnubbyIndexes, forEnroll, completedCb,
gnubbyFoundCb, opt_timer, opt_logMsgUrl) {
/** @private {!GnubbyFactory} */
this.factory_ = factory;
/** @private {Array.<llGnubbyDeviceId>} */
this.gnubbyIndexes_ = gnubbyIndexes;
/** @private {boolean} */
this.forEnroll_ = forEnroll;
/** @private {function(boolean, (number|undefined))} */
this.completedCb_ = completedCb;
/** @private {function(number, MultipleSignerResult)} */
this.gnubbyFoundCb_ = gnubbyFoundCb;
/** @private {Countdown|undefined} */
this.timer_ = opt_timer;
/** @private {string|undefined} */
this.logMsgUrl_ = opt_logMsgUrl;
/** @private {Array.<SignHelperChallenge>} */
this.challenges_ = [];
/** @private {boolean} */
this.challengesFinal_ = false;
// Create a signer for each gnubby.
/** @private {boolean} */
this.anySucceeded_ = false;
/** @private {number} */
this.numComplete_ = 0;
/** @private {Array.<SingleGnubbySigner>} */
this.signers_ = [];
/** @private {Array.<boolean>} */
this.stillGoing_ = [];
/** @private {Array.<number>} */
this.errorStatus_ = [];
for (var i = 0; i < gnubbyIndexes.length; i++) {
this.addGnubby(gnubbyIndexes[i]);
}
}
/**
* Attempts to open this signer's gnubbies, if they're not already open.
* (This is implicitly done by addChallenges.)
*/
MultipleGnubbySigner.prototype.open = function() {
for (var i = 0; i < this.signers_.length; i++) {
this.signers_[i].open();
}
};
/**
* Closes this signer's gnubbies, if any are open.
*/
MultipleGnubbySigner.prototype.close = function() {
for (var i = 0; i < this.signers_.length; i++) {
this.signers_[i].close();
}
};
/**
* Adds challenges to the set of challenges being tried by this signer.
* The challenges are an array of challenge objects, where each challenge
* object's values are base64-encoded.
* If the signer is currently idle, begins signing the new challenges.
*
* @param {Array} challenges Encoded challenges
* @param {boolean} finalChallenges True iff there are no more challenges to add
* @return {boolean} whether the challenges were successfully added.
*/
MultipleGnubbySigner.prototype.addEncodedChallenges =
function(challenges, finalChallenges) {
var decodedChallenges = [];
if (challenges) {
for (var i = 0; i < challenges.length; i++) {
var decodedChallenge = {};
var challenge = challenges[i];
decodedChallenge['challengeHash'] =
B64_decode(challenge['challengeHash']);
decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
if (challenge['version']) {
decodedChallenge['version'] = challenge['version'];
}
decodedChallenges.push(decodedChallenge);
}
}
return this.addChallenges(decodedChallenges, finalChallenges);
};
/**
* Adds challenges to the set of challenges being tried by this signer.
* If the signer is currently idle, begins signing the new challenges.
*
* @param {Array.<SignHelperChallenge>} challenges Challenges to add
* @param {boolean} finalChallenges True iff there are no more challnges to add
* @return {boolean} whether the challenges were successfully added.
*/
MultipleGnubbySigner.prototype.addChallenges =
function(challenges, finalChallenges) {
if (this.challengesFinal_) {
// Can't add new challenges once they're finalized.
return false;
}
if (challenges) {
for (var i = 0; i < challenges.length; i++) {
this.challenges_.push(challenges[i]);
}
}
this.challengesFinal_ = finalChallenges;
for (var i = 0; i < this.signers_.length; i++) {
this.stillGoing_[i] =
this.signers_[i].addChallenges(challenges, finalChallenges);
this.errorStatus_[i] = 0;
}
return true;
};
/**
* Adds a new gnubby to this signer's list of gnubbies. (Only possible while
* this signer is still signing: without this restriction, the morePossible
* indication in the callbacks could become violated.) If this signer has
* challenges to sign, begins signing on the new gnubby with them.
* @param {llGnubbyDeviceId} gnubbyIndex The index of the gnubby to add.
* @return {boolean} Whether the gnubby was added successfully.
*/
MultipleGnubbySigner.prototype.addGnubby = function(gnubbyIndex) {
if (this.numComplete_ && this.numComplete_ == this.signers_.length)
return false;
var index = this.signers_.length;
this.signers_.push(
new SingleGnubbySigner(
this.factory_,
gnubbyIndex,
this.forEnroll_,
this.signFailedCallback_.bind(this, index),
this.signSucceededCallback_.bind(this, index),
this.timer_ ? this.timer_.clone() : null,
this.logMsgUrl_));
this.stillGoing_.push(false);
if (this.challenges_.length) {
this.stillGoing_[index] =
this.signers_[index].addChallenges(this.challenges_,
this.challengesFinal_);
}
return true;
};
/**
* Called by a SingleGnubbySigner upon failure, i.e. unsuccessful completion of
* all its sign operations.
* @param {number} index the index of the gnubby whose result this is
* @param {number} code the result code of the sign operation
* @private
*/
MultipleGnubbySigner.prototype.signFailedCallback_ = function(index, code) {
console.log(
UTIL_fmt('failure. gnubby ' + index + ' got code ' + code.toString(16)));
if (!this.stillGoing_[index]) {
console.log(UTIL_fmt('gnubby ' + index + ' no longer running!'));
// Shouldn't ever happen? Disregard.
return;
}
this.stillGoing_[index] = false;
this.errorStatus_[index] = code;
this.numComplete_++;
var morePossible = this.numComplete_ < this.signers_.length;
if (!morePossible)
this.notifyComplete_();
};
/**
* Called by a SingleGnubbySigner upon success.
* @param {number} index the index of the gnubby whose result this is
* @param {usbGnubby} gnubby the underlying gnubby that succeded.
* @param {number} code the result code of the sign operation
* @param {SingleSignerResult=} signResult Result object
* @private
*/
MultipleGnubbySigner.prototype.signSucceededCallback_ =
function(index, gnubby, code, signResult) {
console.log(UTIL_fmt('success! gnubby ' + index + ' got code ' +
code.toString(16)));
if (!this.stillGoing_[index]) {
console.log(UTIL_fmt('gnubby ' + index + ' no longer running!'));
// Shouldn't ever happen? Disregard.
return;
}
this.anySucceeded_ = true;
this.stillGoing_[index] = false;
this.notifySuccess_(code, gnubby, index, signResult);
this.numComplete_++;
var morePossible = this.numComplete_ < this.signers_.length;
if (!morePossible)
this.notifyComplete_();
};
/**
* @private
*/
MultipleGnubbySigner.prototype.notifyComplete_ = function() {
// See if any of the signers failed with a strange error. If so, report a
// single error to the caller, partly as a diagnostic aid and partly to
// distinguish real failures from wrong data.
var funnyBusiness;
for (var i = 0; i < this.errorStatus_.length; i++) {
if (this.errorStatus_[i] &&
this.errorStatus_[i] != DeviceStatusCodes.WRONG_DATA_STATUS &&
this.errorStatus_[i] != DeviceStatusCodes.WAIT_TOUCH_STATUS) {
funnyBusiness = this.errorStatus_[i];
break;
}
}
if (funnyBusiness) {
console.warn(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ', ' +
'funny error = ' + funnyBusiness + ')'));
} else {
console.log(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ')'));
}
this.completedCb_(this.anySucceeded_, funnyBusiness);
};
/**
* @param {number} code Success status code
* @param {usbGnubby} gnubby The gnubby that succeeded
* @param {number} gnubbyIndex The gnubby's index
* @param {SingleSignerResult=} singleSignerResult Result object
* @private
*/
MultipleGnubbySigner.prototype.notifySuccess_ =
function(code, gnubby, gnubbyIndex, singleSignerResult) {
console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
var signResult = {
'gnubby': gnubby,
'gnubbyIndex': gnubbyIndex
};
if (singleSignerResult && singleSignerResult['challenge'])
signResult['challenge'] = singleSignerResult['challenge'];
if (singleSignerResult && singleSignerResult['info'])
signResult['info'] = singleSignerResult['info'];
this.gnubbyFoundCb_(code, signResult);
};