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 Implements a sign helper using USB gnubbies. 7 */ 8 'use strict'; 9 10 var CORRUPT_sign = false; 11 12 /** 13 * @param {!GnubbyFactory} factory Factory for gnubby instances 14 * @param {Countdown} timer Timer after whose expiration the caller is no longer 15 * interested in the result of a sign request. 16 * @param {function(number, boolean)} errorCb Called when a sign request fails 17 * with an error code and whether any gnubbies were found. 18 * @param {function(SignHelperChallenge, string, string=)} successCb Called with 19 * the signature produced by a successful sign request. 20 * @param {string=} opt_logMsgUrl A URL to post log messages to. 21 * @constructor 22 * @implements {SignHelper} 23 */ 24 function UsbSignHelper(factory, timer, errorCb, successCb, opt_logMsgUrl) { 25 /** @private {!GnubbyFactory} */ 26 this.factory_ = factory; 27 /** @private {Countdown} */ 28 this.timer_ = timer; 29 /** @private {function(number, boolean)} */ 30 this.errorCb_ = errorCb; 31 /** @private {function(SignHelperChallenge, string, string=)} */ 32 this.successCb_ = successCb; 33 /** @private {string|undefined} */ 34 this.logMsgUrl_ = opt_logMsgUrl; 35 36 /** @private {Array.<SignHelperChallenge>} */ 37 this.pendingChallenges_ = []; 38 /** @private {Array.<usbGnubby>} */ 39 this.waitingForTouchGnubbies_ = []; 40 41 /** @private {boolean} */ 42 this.notified_ = false; 43 /** @private {boolean} */ 44 this.signerComplete_ = false; 45 } 46 47 /** 48 * Attempts to sign the provided challenges. 49 * @param {Array.<SignHelperChallenge>} challenges Challenges to sign 50 * @return {boolean} whether this set of challenges was accepted. 51 */ 52 UsbSignHelper.prototype.doSign = function(challenges) { 53 if (!challenges.length) { 54 // Fail a sign request with an empty set of challenges, and pretend to have 55 // alerted the caller in case the enumerate is still pending. 56 this.notified_ = true; 57 return false; 58 } else { 59 this.pendingChallenges_ = challenges; 60 this.getSomeGnubbies_(); 61 return true; 62 } 63 }; 64 65 /** 66 * Enumerates gnubbies, and begins processing challenges upon enumeration if 67 * any gnubbies are found. 68 * @private 69 */ 70 UsbSignHelper.prototype.getSomeGnubbies_ = function() { 71 this.factory_.enumerate(this.enumerateCallback.bind(this)); 72 }; 73 74 /** 75 * Called with the result of enumerating gnubbies. 76 * @param {number} rc the result of the enumerate. 77 * @param {Array.<llGnubbyDeviceId>} indexes Indexes of found gnubbies 78 */ 79 UsbSignHelper.prototype.enumerateCallback = function(rc, indexes) { 80 if (rc) { 81 this.notifyError_(rc, false); 82 return; 83 } 84 if (!indexes.length) { 85 this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false); 86 return; 87 } 88 if (this.timer_.expired()) { 89 this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS, true); 90 return; 91 } 92 this.gotSomeGnubbies_(indexes); 93 }; 94 95 /** 96 * Called with the result of enumerating gnubby indexes. 97 * @param {Array.<llGnubbyDeviceId>} indexes Indexes of found gnubbies 98 * @private 99 */ 100 UsbSignHelper.prototype.gotSomeGnubbies_ = function(indexes) { 101 /** @private {MultipleGnubbySigner} */ 102 this.signer_ = new MultipleGnubbySigner( 103 this.factory_, 104 indexes, 105 false /* forEnroll */, 106 this.signerCompleted_.bind(this), 107 this.signerFoundGnubby_.bind(this), 108 this.timer_, 109 this.logMsgUrl_); 110 this.signer_.addEncodedChallenges(this.pendingChallenges_, true); 111 }; 112 113 /** 114 * Called when a MultipleGnubbySigner completes its sign request. 115 * @param {boolean} anySucceeded whether any sign attempt completed 116 * successfully. 117 * @param {number=} errorCode an error code from a failing gnubby, if one was 118 * found. 119 * @private 120 */ 121 UsbSignHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) { 122 this.signerComplete_ = true; 123 // The signer is not created unless some gnubbies were enumerated, so 124 // anyGnubbies is mostly always true. The exception is when the last gnubby is 125 // removed, handled shortly. 126 var anyGnubbies = true; 127 if (!anySucceeded) { 128 if (!errorCode) { 129 errorCode = DeviceStatusCodes.WRONG_DATA_STATUS; 130 } else if (errorCode == -llGnubby.GONE) { 131 // If the last gnubby was removed, report as though no gnubbies were 132 // found. 133 errorCode = DeviceStatusCodes.WRONG_DATA_STATUS; 134 anyGnubbies = false; 135 } 136 this.notifyError_(errorCode, anyGnubbies); 137 } else if (this.anyTimeout_) { 138 // Some previously succeeding gnubby timed out: return its error code. 139 this.notifyError_(this.timeoutError_, anyGnubbies); 140 } else { 141 // Do nothing: signerFoundGnubby_ will have been called with each 142 // succeeding gnubby. 143 } 144 }; 145 146 /** 147 * Called when a MultipleGnubbySigner finds a gnubby that has successfully 148 * signed, or can successfully sign, one of the challenges. 149 * @param {number} code Status code 150 * @param {MultipleSignerResult} signResult Signer result object 151 * @private 152 */ 153 UsbSignHelper.prototype.signerFoundGnubby_ = function(code, signResult) { 154 var gnubby = signResult['gnubby']; 155 var challenge = signResult['challenge']; 156 var info = new Uint8Array(signResult['info']); 157 if (code == DeviceStatusCodes.OK_STATUS && info.length > 0 && info[0]) { 158 this.notifySuccess_(gnubby, challenge, info); 159 } else { 160 this.waitingForTouchGnubbies_.push(gnubby); 161 this.retrySignIfNotTimedOut_(gnubby, challenge, code); 162 } 163 }; 164 165 /** 166 * Reports the result of a successful sign operation. 167 * @param {usbGnubby} gnubby Gnubby instance 168 * @param {SignHelperChallenge} challenge Challenge signed 169 * @param {Uint8Array} info Result data 170 * @private 171 */ 172 UsbSignHelper.prototype.notifySuccess_ = function(gnubby, challenge, info) { 173 if (this.notified_) 174 return; 175 this.notified_ = true; 176 177 gnubby.closeWhenIdle(); 178 this.close(); 179 180 if (CORRUPT_sign) { 181 CORRUPT_sign = false; 182 info[info.length - 1] = info[info.length - 1] ^ 0xff; 183 } 184 var encodedChallenge = {}; 185 encodedChallenge['challengeHash'] = B64_encode(challenge['challengeHash']); 186 encodedChallenge['appIdHash'] = B64_encode(challenge['appIdHash']); 187 encodedChallenge['keyHandle'] = B64_encode(challenge['keyHandle']); 188 this.successCb_( 189 /** @type {SignHelperChallenge} */ (encodedChallenge), B64_encode(info), 190 'USB'); 191 }; 192 193 /** 194 * Reports error to the caller. 195 * @param {number} code error to report 196 * @param {boolean} anyGnubbies If any gnubbies were found 197 * @private 198 */ 199 UsbSignHelper.prototype.notifyError_ = function(code, anyGnubbies) { 200 if (this.notified_) 201 return; 202 this.notified_ = true; 203 this.close(); 204 this.errorCb_(code, anyGnubbies); 205 }; 206 207 /** 208 * Retries signing a particular challenge on a gnubby. 209 * @param {usbGnubby} gnubby Gnubby instance 210 * @param {SignHelperChallenge} challenge Challenge to retry 211 * @private 212 */ 213 UsbSignHelper.prototype.retrySign_ = function(gnubby, challenge) { 214 var challengeHash = challenge['challengeHash']; 215 var appIdHash = challenge['appIdHash']; 216 var keyHandle = challenge['keyHandle']; 217 gnubby.sign(challengeHash, appIdHash, keyHandle, 218 this.signCallback_.bind(this, gnubby, challenge)); 219 }; 220 221 /** 222 * Called when a gnubby completes a sign request. 223 * @param {usbGnubby} gnubby Gnubby instance 224 * @param {SignHelperChallenge} challenge Challenge to retry 225 * @param {number} code Previous status code 226 * @private 227 */ 228 UsbSignHelper.prototype.retrySignIfNotTimedOut_ = 229 function(gnubby, challenge, code) { 230 if (this.timer_.expired()) { 231 // Store any timeout error code, to be returned from the complete 232 // callback if no other eligible gnubbies are found. 233 /** @private {boolean} */ 234 this.anyTimeout_ = true; 235 /** @private {number} */ 236 this.timeoutError_ = code; 237 this.removePreviouslyEligibleGnubby_(gnubby, code); 238 } else { 239 window.setTimeout(this.retrySign_.bind(this, gnubby, challenge), 200); 240 } 241 }; 242 243 /** 244 * Removes a gnubby that was waiting for touch from the list, with the given 245 * error code. If this is the last gnubby, notifies the caller of the error. 246 * @param {usbGnubby} gnubby Gnubby instance 247 * @param {number} code Previous status code 248 * @private 249 */ 250 UsbSignHelper.prototype.removePreviouslyEligibleGnubby_ = 251 function(gnubby, code) { 252 // Close this gnubby. 253 gnubby.closeWhenIdle(); 254 var index = this.waitingForTouchGnubbies_.indexOf(gnubby); 255 if (index >= 0) { 256 this.waitingForTouchGnubbies_.splice(index, 1); 257 } 258 if (!this.waitingForTouchGnubbies_.length && this.signerComplete_ && 259 !this.notified_) { 260 // Last sign attempt is complete: return this error. 261 console.log(UTIL_fmt('timeout or error (' + code.toString(16) + 262 ') signing')); 263 // If the last device is gone, report as if no gnubbies were found. 264 if (code == -llGnubby.GONE) { 265 this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false); 266 return; 267 } 268 this.notifyError_(code, true); 269 } 270 }; 271 272 /** 273 * Called when a gnubby completes a sign request. 274 * @param {usbGnubby} gnubby Gnubby instance 275 * @param {SignHelperChallenge} challenge Challenge signed 276 * @param {number} code Status code 277 * @param {ArrayBuffer=} infoArray Result data 278 * @private 279 */ 280 UsbSignHelper.prototype.signCallback_ = 281 function(gnubby, challenge, code, infoArray) { 282 if (this.notified_) { 283 // Individual sign completed after previous success or failure. Disregard. 284 return; 285 } 286 var info = new Uint8Array(infoArray || []); 287 if (code == DeviceStatusCodes.OK_STATUS && info.length > 0 && info[0]) { 288 this.notifySuccess_(gnubby, challenge, info); 289 } else if (code == DeviceStatusCodes.OK_STATUS || 290 code == DeviceStatusCodes.WAIT_TOUCH_STATUS || 291 code == DeviceStatusCodes.BUSY_STATUS) { 292 this.retrySignIfNotTimedOut_(gnubby, challenge, code); 293 } else { 294 console.log(UTIL_fmt('got error ' + code.toString(16) + ' signing')); 295 this.removePreviouslyEligibleGnubby_(gnubby, code); 296 } 297 }; 298 299 /** 300 * Closes the MultipleGnubbySigner, if any. 301 */ 302 UsbSignHelper.prototype.close = function() { 303 if (this.signer_) { 304 this.signer_.close(); 305 this.signer_ = null; 306 } 307 for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) { 308 this.waitingForTouchGnubbies_[i].closeWhenIdle(); 309 } 310 this.waitingForTouchGnubbies_ = []; 311 }; 312 313 /** 314 * @param {!GnubbyFactory} gnubbyFactory Factory to create gnubbies. 315 * @constructor 316 * @implements {SignHelperFactory} 317 */ 318 function UsbSignHelperFactory(gnubbyFactory) { 319 /** @private {!GnubbyFactory} */ 320 this.gnubbyFactory_ = gnubbyFactory; 321 } 322 323 /** 324 * @param {Countdown} timer Timer after whose expiration the caller is no longer 325 * interested in the result of a sign request. 326 * @param {function(number, boolean)} errorCb Called when a sign request fails 327 * with an error code and whether any gnubbies were found. 328 * @param {function(SignHelperChallenge, string)} successCb Called with the 329 * signature produced by a successful sign request. 330 * @param {string=} opt_logMsgUrl A URL to post log messages to. 331 * @return {UsbSignHelper} the newly created helper. 332 */ 333 UsbSignHelperFactory.prototype.createHelper = 334 function(timer, errorCb, successCb, opt_logMsgUrl) { 335 var helper = 336 new UsbSignHelper(this.gnubbyFactory_, timer, errorCb, successCb, 337 opt_logMsgUrl); 338 return helper; 339 }; 340