| // 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); |
| }; |