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 A single gnubby signer wraps the process of opening a gnubby, |
| 7 * signing each challenge in an array of challenges until a success condition |
| 8 * is satisfied, and finally yielding the gnubby upon success. |
| 9 * |
| 10 * @author juanlang@google.com (Juan Lang) |
| 11 */ |
| 12 |
| 13 'use strict'; |
| 14 |
| 15 /** |
| 16 * Creates a new sign handler with a gnubby. This handler will perform a sign |
| 17 * operation using each challenge in an array of challenges until its success |
| 18 * condition is satisified, or an error or timeout occurs. The success condition |
| 19 * is defined differently depending whether this signer is used for enrolling |
| 20 * or for signing: |
| 21 * |
| 22 * For enroll, success is defined as each challenge yielding wrong data. This |
| 23 * means this gnubby is not currently enrolled for any of the appIds in any |
| 24 * challenge. |
| 25 * |
| 26 * For sign, success is defined as any challenge yielding ok or waiting for |
| 27 * touch. |
| 28 * |
| 29 * At most one of the success or failure callbacks will be called, and it will |
| 30 * be called at most once. Neither callback is guaranteed to be called: if |
| 31 * a final set of challenges is never given to this gnubby, or if the gnubby |
| 32 * stays busy, the signer has no way to know whether the set of challenges it's |
| 33 * been given has succeeded or failed. |
| 34 * The callback is called only when the signer reaches success or failure, i.e. |
| 35 * when there is no need for this signer to continue trying new challenges. |
| 36 * |
| 37 * @param {!GnubbyFactory} factory Used to create and open the gnubby. |
| 38 * @param {llGnubbyDeviceId} gnubbyIndex Which gnubby to open. |
| 39 * @param {boolean} forEnroll Whether this signer is signing for an attempted |
| 40 * enroll operation. |
| 41 * @param {function(number)} errorCb Called when this signer fails, i.e. no |
| 42 * further attempts can succeed. |
| 43 * @param {function(usbGnubby, number, (SingleSignerResult|undefined))} |
| 44 * successCb Called when this signer succeeds. |
| 45 * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the |
| 46 * signer will not attempt any new operations, assuming the caller is no |
| 47 * longer interested in the outcome. |
| 48 * @param {string=} opt_logMsgUrl A URL to post log messages to. |
| 49 * @constructor |
| 50 */ |
| 51 function SingleGnubbySigner(factory, gnubbyIndex, forEnroll, errorCb, successCb, |
| 52 opt_timer, opt_logMsgUrl) { |
| 53 /** @private {GnubbyFactory} */ |
| 54 this.factory_ = factory; |
| 55 /** @private {llGnubbyDeviceId} */ |
| 56 this.gnubbyIndex_ = gnubbyIndex; |
| 57 /** @private {SingleGnubbySigner.State} */ |
| 58 this.state_ = SingleGnubbySigner.State.INIT; |
| 59 /** @private {boolean} */ |
| 60 this.forEnroll_ = forEnroll; |
| 61 /** @private {function(number)} */ |
| 62 this.errorCb_ = errorCb; |
| 63 /** @private {function(usbGnubby, number, (SingleSignerResult|undefined))} */ |
| 64 this.successCb_ = successCb; |
| 65 /** @private {Countdown|undefined} */ |
| 66 this.timer_ = opt_timer; |
| 67 /** @private {string|undefined} */ |
| 68 this.logMsgUrl_ = opt_logMsgUrl; |
| 69 |
| 70 /** @private {!Array.<!SignHelperChallenge>} */ |
| 71 this.challenges_ = []; |
| 72 /** @private {number} */ |
| 73 this.challengeIndex_ = 0; |
| 74 /** @private {boolean} */ |
| 75 this.challengesFinal_ = false; |
| 76 |
| 77 /** @private {!Array.<string>} */ |
| 78 this.notForMe_ = []; |
| 79 } |
| 80 |
| 81 /** @enum {number} */ |
| 82 SingleGnubbySigner.State = { |
| 83 /** Initial state. */ |
| 84 INIT: 0, |
| 85 /** The signer is attempting to open a gnubby. */ |
| 86 OPENING: 1, |
| 87 /** The signer's gnubby opened, but is busy. */ |
| 88 BUSY: 2, |
| 89 /** The signer has an open gnubby, but no challenges to sign. */ |
| 90 IDLE: 3, |
| 91 /** The signer is currently signing a challenge. */ |
| 92 SIGNING: 4, |
| 93 /** The signer encountered an error. */ |
| 94 ERROR: 5, |
| 95 /** The signer got a successful outcome. */ |
| 96 SUCCESS: 6, |
| 97 /** The signer is closing its gnubby. */ |
| 98 CLOSING: 7, |
| 99 /** The signer is closed. */ |
| 100 CLOSED: 8 |
| 101 }; |
| 102 |
| 103 /** |
| 104 * Attempts to open this signer's gnubby, if it's not already open. |
| 105 * (This is implicitly done by addChallenges.) |
| 106 */ |
| 107 SingleGnubbySigner.prototype.open = function() { |
| 108 if (this.state_ == SingleGnubbySigner.State.INIT) { |
| 109 this.state_ = SingleGnubbySigner.State.OPENING; |
| 110 this.factory_.openGnubby(this.gnubbyIndex_, |
| 111 this.forEnroll_, |
| 112 this.openCallback_.bind(this), |
| 113 this.logMsgUrl_); |
| 114 } |
| 115 }; |
| 116 |
| 117 /** |
| 118 * Closes this signer's gnubby, if it's held. |
| 119 */ |
| 120 SingleGnubbySigner.prototype.close = function() { |
| 121 if (!this.gnubby_) return; |
| 122 this.state_ = SingleGnubbySigner.State.CLOSING; |
| 123 this.gnubby_.closeWhenIdle(this.closed_.bind(this)); |
| 124 }; |
| 125 |
| 126 /** |
| 127 * Called when this signer's gnubby is closed. |
| 128 * @private |
| 129 */ |
| 130 SingleGnubbySigner.prototype.closed_ = function() { |
| 131 this.gnubby_ = null; |
| 132 this.state_ = SingleGnubbySigner.State.CLOSED; |
| 133 }; |
| 134 |
| 135 /** |
| 136 * Adds challenges to the set of challenges being tried by this signer. |
| 137 * If the signer is currently idle, begins signing the new challenges. |
| 138 * |
| 139 * @param {Array.<SignHelperChallenge>} challenges |
| 140 * @param {boolean} finalChallenges |
| 141 * @return {boolean} Whether the challenges were accepted. |
| 142 */ |
| 143 SingleGnubbySigner.prototype.addChallenges = |
| 144 function(challenges, finalChallenges) { |
| 145 if (this.challengesFinal_) { |
| 146 // Can't add new challenges once they're finalized. |
| 147 return false; |
| 148 } |
| 149 |
| 150 if (challenges) { |
| 151 console.log(this.gnubby_); |
| 152 console.log(UTIL_fmt('adding ' + challenges.length + ' challenges')); |
| 153 for (var i = 0; i < challenges.length; i++) { |
| 154 this.challenges_.push(challenges[i]); |
| 155 } |
| 156 } |
| 157 this.challengesFinal_ = finalChallenges; |
| 158 |
| 159 switch (this.state_) { |
| 160 case SingleGnubbySigner.State.INIT: |
| 161 this.open(); |
| 162 break; |
| 163 case SingleGnubbySigner.State.OPENING: |
| 164 // The open has already commenced, so accept the added challenges, but |
| 165 // don't do anything. |
| 166 break; |
| 167 case SingleGnubbySigner.State.IDLE: |
| 168 if (this.challengeIndex_ < challenges.length) { |
| 169 // New challenges added: restart signing. |
| 170 this.doSign_(this.challengeIndex_); |
| 171 } else if (finalChallenges) { |
| 172 // Finalized with no new challenges can happen when the caller rejects |
| 173 // the appId for some challenge. |
| 174 // If this signer is for enroll, the request must be rejected: this |
| 175 // signer can't determine whether the gnubby is or is not enrolled for |
| 176 // the origin. |
| 177 // If this signer is for sign, the request must also be rejected: there |
| 178 // are no new challenges to sign, and all previous ones did not yield |
| 179 // success. |
| 180 var self = this; |
| 181 window.setTimeout(function() { |
| 182 self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS); |
| 183 }, 0); |
| 184 } |
| 185 break; |
| 186 case SingleGnubbySigner.State.SIGNING: |
| 187 // Already signing, so don't kick off a new sign, but accept the added |
| 188 // challenges. |
| 189 break; |
| 190 default: |
| 191 return false; |
| 192 } |
| 193 return true; |
| 194 }; |
| 195 |
| 196 /** |
| 197 * How long to delay retrying a failed open. |
| 198 */ |
| 199 SingleGnubbySigner.OPEN_DELAY_MILLIS = 200; |
| 200 |
| 201 /** |
| 202 * @param {number} rc The result of the open operation. |
| 203 * @param {usbGnubby=} gnubby The opened gnubby, if open was successful (or |
| 204 * busy). |
| 205 * @private |
| 206 */ |
| 207 SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) { |
| 208 if (this.state_ != SingleGnubbySigner.State.OPENING && |
| 209 this.state_ != SingleGnubbySigner.State.BUSY) { |
| 210 // Open completed after close, perhaps? Ignore. |
| 211 return; |
| 212 } |
| 213 |
| 214 switch (rc) { |
| 215 case DeviceStatusCodes.OK_STATUS: |
| 216 if (!gnubby) { |
| 217 console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?')); |
| 218 } else { |
| 219 this.gnubby_ = gnubby; |
| 220 this.gnubby_.version(this.versionCallback_.bind(this)); |
| 221 } |
| 222 break; |
| 223 case DeviceStatusCodes.BUSY_STATUS: |
| 224 this.gnubby_ = gnubby; |
| 225 this.openedBusy_ = true; |
| 226 this.state_ = SingleGnubbySigner.State.BUSY; |
| 227 // If there's still time, retry the open. |
| 228 if (!this.timer_ || !this.timer_.expired()) { |
| 229 var self = this; |
| 230 window.setTimeout(function() { |
| 231 if (self.gnubby_) { |
| 232 self.factory_.openGnubby(self.gnubbyIndex_, |
| 233 self.forEnroll_, |
| 234 self.openCallback_.bind(self), |
| 235 self.logMsgUrl_); |
| 236 } |
| 237 }, SingleGnubbySigner.OPEN_DELAY_MILLIS); |
| 238 } else { |
| 239 this.goToError_(DeviceStatusCodes.BUSY_STATUS); |
| 240 } |
| 241 break; |
| 242 default: |
| 243 // TODO(juanlang): This won't be confused with success, but should it be |
| 244 // part of the same namespace as the other error codes, which are |
| 245 // always in DeviceStatusCodes.*? |
| 246 this.goToError_(rc); |
| 247 } |
| 248 }; |
| 249 |
| 250 /** |
| 251 * Called with the result of a version command. |
| 252 * @param {number} rc Result of version command. |
| 253 * @param {ArrayBuffer=} opt_data Version. |
| 254 * @private |
| 255 */ |
| 256 SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) { |
| 257 if (rc) { |
| 258 this.goToError_(rc); |
| 259 return; |
| 260 } |
| 261 this.state_ = SingleGnubbySigner.State.IDLE; |
| 262 this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || [])); |
| 263 this.doSign_(this.challengeIndex_); |
| 264 }; |
| 265 |
| 266 /** |
| 267 * @param {number} challengeIndex |
| 268 * @private |
| 269 */ |
| 270 SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) { |
| 271 if (this.timer_ && this.timer_.expired()) { |
| 272 // If the timer is expired, that means we never got a success or a touch |
| 273 // required response: either always implies completion of this signer's |
| 274 // state machine (see signCallback's cases for OK_STATUS and |
| 275 // WAIT_TOUCH_STATUS.) We could have gotten wrong data on a partial set of |
| 276 // challenges, but this means we don't yet know the final outcome. In any |
| 277 // event, we don't yet know the final outcome: return busy. |
| 278 this.goToError_(DeviceStatusCodes.BUSY_STATUS); |
| 279 return; |
| 280 } |
| 281 |
| 282 this.state_ = SingleGnubbySigner.State.SIGNING; |
| 283 |
| 284 if (challengeIndex >= this.challenges_.length) { |
| 285 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS); |
| 286 return; |
| 287 } |
| 288 |
| 289 var challenge = this.challenges_[challengeIndex]; |
| 290 var challengeHash = challenge.challengeHash; |
| 291 var appIdHash = challenge.appIdHash; |
| 292 var keyHandle = challenge.keyHandle; |
| 293 if (this.notForMe_.indexOf(keyHandle) != -1) { |
| 294 // Cache hit: return wrong data again. |
| 295 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS); |
| 296 } else if (challenge.version && challenge.version != this.version_) { |
| 297 // Sign challenge for a different version of gnubby: return wrong data. |
| 298 this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS); |
| 299 } else { |
| 300 var opt_nowink = this.forEnroll_; |
| 301 this.gnubby_.sign(challengeHash, appIdHash, keyHandle, |
| 302 this.signCallback_.bind(this, challengeIndex), |
| 303 opt_nowink); |
| 304 } |
| 305 }; |
| 306 |
| 307 /** |
| 308 * Called with the result of a single sign operation. |
| 309 * @param {number} challengeIndex the index of the challenge just attempted |
| 310 * @param {number} code the result of the sign operation |
| 311 * @param {ArrayBuffer=} opt_info |
| 312 * @private |
| 313 */ |
| 314 SingleGnubbySigner.prototype.signCallback_ = |
| 315 function(challengeIndex, code, opt_info) { |
| 316 console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyIndex_) + |
| 317 ', challenge ' + challengeIndex + ' yielded ' + code.toString(16))); |
| 318 if (this.state_ != SingleGnubbySigner.State.SIGNING) { |
| 319 console.log(UTIL_fmt('already done!')); |
| 320 // We're done, the caller's no longer interested. |
| 321 return; |
| 322 } |
| 323 |
| 324 // Cache wrong data result, re-asking the gnubby to sign it won't produce |
| 325 // different results. |
| 326 if (code == DeviceStatusCodes.WRONG_DATA_STATUS) { |
| 327 if (challengeIndex < this.challenges_.length) { |
| 328 var challenge = this.challenges_[challengeIndex]; |
| 329 if (this.notForMe_.indexOf(challenge.keyHandle) == -1) { |
| 330 this.notForMe_.push(challenge.keyHandle); |
| 331 } |
| 332 } |
| 333 } |
| 334 |
| 335 switch (code) { |
| 336 case DeviceStatusCodes.GONE_STATUS: |
| 337 this.goToError_(code); |
| 338 break; |
| 339 |
| 340 case DeviceStatusCodes.TIMEOUT_STATUS: |
| 341 // TODO(juanlang): On a TIMEOUT_STATUS, sync first, then retry. |
| 342 case DeviceStatusCodes.BUSY_STATUS: |
| 343 this.doSign_(this.challengeIndex_); |
| 344 break; |
| 345 |
| 346 case DeviceStatusCodes.OK_STATUS: |
| 347 if (this.forEnroll_) { |
| 348 this.goToError_(code); |
| 349 } else { |
| 350 this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info); |
| 351 } |
| 352 break; |
| 353 |
| 354 case DeviceStatusCodes.WAIT_TOUCH_STATUS: |
| 355 if (this.forEnroll_) { |
| 356 this.goToError_(code); |
| 357 } else { |
| 358 this.goToSuccess_(code, this.challenges_[challengeIndex]); |
| 359 } |
| 360 break; |
| 361 |
| 362 case DeviceStatusCodes.WRONG_DATA_STATUS: |
| 363 if (this.challengeIndex_ < this.challenges_.length - 1) { |
| 364 this.doSign_(++this.challengeIndex_); |
| 365 } else if (!this.challengesFinal_) { |
| 366 this.state_ = SingleGnubbySigner.State.IDLE; |
| 367 } else if (this.forEnroll_) { |
| 368 // Signal the caller whether the open was busy, because it may take |
| 369 // an unusually long time when opened for enroll. Use an empty |
| 370 // "challenge" as the signal for a busy open. |
| 371 var challenge = undefined; |
| 372 if (this.openedBusy) { |
| 373 challenge = { appIdHash: '', challengeHash: '', keyHandle: '' }; |
| 374 } |
| 375 this.goToSuccess_(code, challenge); |
| 376 } else { |
| 377 this.goToError_(code); |
| 378 } |
| 379 break; |
| 380 |
| 381 default: |
| 382 if (this.forEnroll_) { |
| 383 this.goToError_(code); |
| 384 } else if (this.challengeIndex_ < this.challenges_.length - 1) { |
| 385 this.doSign_(++this.challengeIndex_); |
| 386 } else if (!this.challengesFinal_) { |
| 387 // Increment the challenge index, as this one isn't useful any longer, |
| 388 // but a subsequent challenge may appear, and it might be useful. |
| 389 this.challengeIndex_++; |
| 390 this.state_ = SingleGnubbySigner.State.IDLE; |
| 391 } else { |
| 392 this.goToError_(code); |
| 393 } |
| 394 } |
| 395 }; |
| 396 |
| 397 /** |
| 398 * Switches to the error state, and notifies caller. |
| 399 * @param {number} code |
| 400 * @private |
| 401 */ |
| 402 SingleGnubbySigner.prototype.goToError_ = function(code) { |
| 403 this.state_ = SingleGnubbySigner.State.ERROR; |
| 404 console.log(UTIL_fmt('failed (' + code.toString(16) + ')')); |
| 405 this.errorCb_(code); |
| 406 // Since this gnubby can no longer produce a useful result, go ahead and |
| 407 // close it. |
| 408 this.close(); |
| 409 }; |
| 410 |
| 411 /** |
| 412 * Switches to the success state, and notifies caller. |
| 413 * @param {number} code |
| 414 * @param {SignHelperChallenge=} opt_challenge |
| 415 * @param {ArrayBuffer=} opt_info |
| 416 * @private |
| 417 */ |
| 418 SingleGnubbySigner.prototype.goToSuccess_ = |
| 419 function(code, opt_challenge, opt_info) { |
| 420 this.state_ = SingleGnubbySigner.State.SUCCESS; |
| 421 console.log(UTIL_fmt('success (' + code.toString(16) + ')')); |
| 422 if (opt_challenge || opt_info) { |
| 423 var singleSignerResult = {}; |
| 424 if (opt_challenge) { |
| 425 singleSignerResult['challenge'] = opt_challenge; |
| 426 } |
| 427 if (opt_info) { |
| 428 singleSignerResult['info'] = opt_info; |
| 429 } |
| 430 } |
| 431 this.successCb_(this.gnubby_, code, singleSignerResult); |
| 432 // this.gnubby_ is now owned by successCb. |
| 433 this.gnubby_ = null; |
| 434 }; |
OLD | NEW |