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. 4 5 /** 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. 9 */ 10 'use strict'; 11 12 /** 13 * Creates a new sign handler with an array of gnubby indexes. 14 * @param {!GnubbyFactory} factory Used to create and open the gnubbies. 15 * @param {Array.<llGnubbyDeviceId>} gnubbyIndexes Which gnubbies to open. 16 * @param {boolean} forEnroll Whether this signer is signing for an attempted 17 * enroll operation. 18 * @param {function(boolean, (number|undefined))} completedCb Called when this 19 * signer completes sign attempts, i.e. no further results should be 20 * expected. 21 * @param {function(number, MultipleSignerResult)} gnubbyFoundCb Called with 22 * each gnubby/challenge that yields a successful result. 23 * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the 24 * signer will not attempt any new operations, assuming the caller is no 25 * longer interested in the outcome. 26 * @param {string=} opt_logMsgUrl A URL to post log messages to. 27 * @constructor 28 */ 29 function MultipleGnubbySigner(factory, gnubbyIndexes, forEnroll, completedCb, 30 gnubbyFoundCb, opt_timer, opt_logMsgUrl) { 31 /** @private {!GnubbyFactory} */ 32 this.factory_ = factory; 33 /** @private {Array.<llGnubbyDeviceId>} */ 34 this.gnubbyIndexes_ = gnubbyIndexes; 35 /** @private {boolean} */ 36 this.forEnroll_ = forEnroll; 37 /** @private {function(boolean, (number|undefined))} */ 38 this.completedCb_ = completedCb; 39 /** @private {function(number, MultipleSignerResult)} */ 40 this.gnubbyFoundCb_ = gnubbyFoundCb; 41 /** @private {Countdown|undefined} */ 42 this.timer_ = opt_timer; 43 /** @private {string|undefined} */ 44 this.logMsgUrl_ = opt_logMsgUrl; 45 46 /** @private {Array.<SignHelperChallenge>} */ 47 this.challenges_ = []; 48 /** @private {boolean} */ 49 this.challengesFinal_ = false; 50 51 // Create a signer for each gnubby. 52 /** @private {boolean} */ 53 this.anySucceeded_ = false; 54 /** @private {number} */ 55 this.numComplete_ = 0; 56 /** @private {Array.<SingleGnubbySigner>} */ 57 this.signers_ = []; 58 /** @private {Array.<boolean>} */ 59 this.stillGoing_ = []; 60 /** @private {Array.<number>} */ 61 this.errorStatus_ = []; 62 for (var i = 0; i < gnubbyIndexes.length; i++) { 63 this.addGnubby(gnubbyIndexes[i]); 64 } 65 } 66 67 /** 68 * Attempts to open this signer's gnubbies, if they're not already open. 69 * (This is implicitly done by addChallenges.) 70 */ 71 MultipleGnubbySigner.prototype.open = function() { 72 for (var i = 0; i < this.signers_.length; i++) { 73 this.signers_[i].open(); 74 } 75 }; 76 77 /** 78 * Closes this signer's gnubbies, if any are open. 79 */ 80 MultipleGnubbySigner.prototype.close = function() { 81 for (var i = 0; i < this.signers_.length; i++) { 82 this.signers_[i].close(); 83 } 84 }; 85 86 /** 87 * Adds challenges to the set of challenges being tried by this signer. 88 * The challenges are an array of challenge objects, where each challenge 89 * object's values are base64-encoded. 90 * If the signer is currently idle, begins signing the new challenges. 91 * 92 * @param {Array} challenges Encoded challenges 93 * @param {boolean} finalChallenges True iff there are no more challenges to add 94 * @return {boolean} whether the challenges were successfully added. 95 */ 96 MultipleGnubbySigner.prototype.addEncodedChallenges = 97 function(challenges, finalChallenges) { 98 var decodedChallenges = []; 99 if (challenges) { 100 for (var i = 0; i < challenges.length; i++) { 101 var decodedChallenge = {}; 102 var challenge = challenges[i]; 103 decodedChallenge['challengeHash'] = 104 B64_decode(challenge['challengeHash']); 105 decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']); 106 decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']); 107 if (challenge['version']) { 108 decodedChallenge['version'] = challenge['version']; 109 } 110 decodedChallenges.push(decodedChallenge); 111 } 112 } 113 return this.addChallenges(decodedChallenges, finalChallenges); 114 }; 115 116 /** 117 * Adds challenges to the set of challenges being tried by this signer. 118 * If the signer is currently idle, begins signing the new challenges. 119 * 120 * @param {Array.<SignHelperChallenge>} challenges Challenges to add 121 * @param {boolean} finalChallenges True iff there are no more challnges to add 122 * @return {boolean} whether the challenges were successfully added. 123 */ 124 MultipleGnubbySigner.prototype.addChallenges = 125 function(challenges, finalChallenges) { 126 if (this.challengesFinal_) { 127 // Can't add new challenges once they're finalized. 128 return false; 129 } 130 131 if (challenges) { 132 for (var i = 0; i < challenges.length; i++) { 133 this.challenges_.push(challenges[i]); 134 } 135 } 136 this.challengesFinal_ = finalChallenges; 137 138 for (var i = 0; i < this.signers_.length; i++) { 139 this.stillGoing_[i] = 140 this.signers_[i].addChallenges(challenges, finalChallenges); 141 this.errorStatus_[i] = 0; 142 } 143 return true; 144 }; 145 146 /** 147 * Adds a new gnubby to this signer's list of gnubbies. (Only possible while 148 * this signer is still signing: without this restriction, the morePossible 149 * indication in the callbacks could become violated.) If this signer has 150 * challenges to sign, begins signing on the new gnubby with them. 151 * @param {llGnubbyDeviceId} gnubbyIndex The index of the gnubby to add. 152 * @return {boolean} Whether the gnubby was added successfully. 153 */ 154 MultipleGnubbySigner.prototype.addGnubby = function(gnubbyIndex) { 155 if (this.numComplete_ && this.numComplete_ == this.signers_.length) 156 return false; 157 158 var index = this.signers_.length; 159 this.signers_.push( 160 new SingleGnubbySigner( 161 this.factory_, 162 gnubbyIndex, 163 this.forEnroll_, 164 this.signFailedCallback_.bind(this, index), 165 this.signSucceededCallback_.bind(this, index), 166 this.timer_ ? this.timer_.clone() : null, 167 this.logMsgUrl_)); 168 this.stillGoing_.push(false); 169 170 if (this.challenges_.length) { 171 this.stillGoing_[index] = 172 this.signers_[index].addChallenges(this.challenges_, 173 this.challengesFinal_); 174 } 175 return true; 176 }; 177 178 /** 179 * Called by a SingleGnubbySigner upon failure, i.e. unsuccessful completion of 180 * all its sign operations. 181 * @param {number} index the index of the gnubby whose result this is 182 * @param {number} code the result code of the sign operation 183 * @private 184 */ 185 MultipleGnubbySigner.prototype.signFailedCallback_ = function(index, code) { 186 console.log( 187 UTIL_fmt('failure. gnubby ' + index + ' got code ' + code.toString(16))); 188 if (!this.stillGoing_[index]) { 189 console.log(UTIL_fmt('gnubby ' + index + ' no longer running!')); 190 // Shouldn't ever happen? Disregard. 191 return; 192 } 193 this.stillGoing_[index] = false; 194 this.errorStatus_[index] = code; 195 this.numComplete_++; 196 var morePossible = this.numComplete_ < this.signers_.length; 197 if (!morePossible) 198 this.notifyComplete_(); 199 }; 200 201 /** 202 * Called by a SingleGnubbySigner upon success. 203 * @param {number} index the index of the gnubby whose result this is 204 * @param {usbGnubby} gnubby the underlying gnubby that succeded. 205 * @param {number} code the result code of the sign operation 206 * @param {SingleSignerResult=} signResult Result object 207 * @private 208 */ 209 MultipleGnubbySigner.prototype.signSucceededCallback_ = 210 function(index, gnubby, code, signResult) { 211 console.log(UTIL_fmt('success! gnubby ' + index + ' got code ' + 212 code.toString(16))); 213 if (!this.stillGoing_[index]) { 214 console.log(UTIL_fmt('gnubby ' + index + ' no longer running!')); 215 // Shouldn't ever happen? Disregard. 216 return; 217 } 218 this.anySucceeded_ = true; 219 this.stillGoing_[index] = false; 220 this.notifySuccess_(code, gnubby, index, signResult); 221 this.numComplete_++; 222 var morePossible = this.numComplete_ < this.signers_.length; 223 if (!morePossible) 224 this.notifyComplete_(); 225 }; 226 227 /** 228 * @private 229 */ 230 MultipleGnubbySigner.prototype.notifyComplete_ = function() { 231 // See if any of the signers failed with a strange error. If so, report a 232 // single error to the caller, partly as a diagnostic aid and partly to 233 // distinguish real failures from wrong data. 234 var funnyBusiness; 235 for (var i = 0; i < this.errorStatus_.length; i++) { 236 if (this.errorStatus_[i] && 237 this.errorStatus_[i] != DeviceStatusCodes.WRONG_DATA_STATUS && 238 this.errorStatus_[i] != DeviceStatusCodes.WAIT_TOUCH_STATUS) { 239 funnyBusiness = this.errorStatus_[i]; 240 break; 241 } 242 } 243 if (funnyBusiness) { 244 console.warn(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ', ' + 245 'funny error = ' + funnyBusiness + ')')); 246 } else { 247 console.log(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ')')); 248 } 249 this.completedCb_(this.anySucceeded_, funnyBusiness); 250 }; 251 252 /** 253 * @param {number} code Success status code 254 * @param {usbGnubby} gnubby The gnubby that succeeded 255 * @param {number} gnubbyIndex The gnubby's index 256 * @param {SingleSignerResult=} singleSignerResult Result object 257 * @private 258 */ 259 MultipleGnubbySigner.prototype.notifySuccess_ = 260 function(code, gnubby, gnubbyIndex, singleSignerResult) { 261 console.log(UTIL_fmt('success (' + code.toString(16) + ')')); 262 var signResult = { 263 'gnubby': gnubby, 264 'gnubbyIndex': gnubbyIndex 265 }; 266 if (singleSignerResult && singleSignerResult['challenge']) 267 signResult['challenge'] = singleSignerResult['challenge']; 268 if (singleSignerResult && singleSignerResult['info']) 269 signResult['info'] = singleSignerResult['info']; 270 this.gnubbyFoundCb_(code, signResult); 271 }; 272