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