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 Low level usb cruft to talk gnubby. 7 */ 8 9'use strict'; 10 11// Global Gnubby instance counter. 12var gnubbyId = 0; 13 14/** 15 * Creates a worker Gnubby instance. 16 * @constructor 17 * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result. 18 */ 19function usbGnubby(opt_busySeconds) { 20 this.dev = null; 21 this.cid = (++gnubbyId) & 0x00ffffff; // Pick unique channel. 22 this.rxframes = []; 23 this.synccnt = 0; 24 this.rxcb = null; 25 this.closed = false; 26 this.commandPending = false; 27 this.notifyOnClose = []; 28 this.busyMillis = (opt_busySeconds ? opt_busySeconds * 1000 : 2500); 29} 30 31/** 32 * Sets usbGnubby's Gnubbies singleton. 33 * @param {Gnubbies} gnubbies Gnubbies singleton instance 34 */ 35usbGnubby.setGnubbies = function(gnubbies) { 36 /** @private {Gnubbies} */ 37 usbGnubby.gnubbies_ = gnubbies; 38}; 39 40/** 41 * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the 42 * result of enumerating. 43 */ 44usbGnubby.prototype.enumerate = function(cb) { 45 if (!cb) cb = usbGnubby.defaultCallback; 46 if (this.closed) { 47 cb(-llGnubby.NODEVICE); 48 return; 49 } 50 if (!usbGnubby.gnubbies_) { 51 cb(-llGnubby.NODEVICE); 52 return; 53 } 54 55 usbGnubby.gnubbies_.enumerate(cb); 56}; 57 58/** 59 * Opens the gnubby with the given index, or the first found gnubby if no 60 * index is specified. 61 * @param {llGnubbyDeviceId} which The device to open. If null, the first 62 * gnubby found is opened. 63 * @param {function(number)|undefined} opt_cb Called with result of opening the 64 * gnubby. 65 */ 66usbGnubby.prototype.open = function(which, opt_cb) { 67 var cb = opt_cb ? opt_cb : usbGnubby.defaultCallback; 68 if (this.closed) { 69 cb(-llGnubby.NODEVICE); 70 return; 71 } 72 this.closingWhenIdle = false; 73 74 if (document.location.href.indexOf('_generated_') == -1) { 75 // Not background page. 76 // Pick more random cid to tell things apart on the usb bus. 77 var rnd = UTIL_getRandom(2); 78 this.cid ^= (rnd[0] << 16) | (rnd[1] << 8); 79 } 80 81 var self = this; 82 83 function setCid(which) { 84 self.cid &= 0x00ffffff; 85 self.cid |= ((which.device + 1) << 24); // For debugging. 86 } 87 88 var enumerateRetriesRemaining = 3; 89 function enumerated(rc, devs) { 90 if (!devs.length) 91 rc = -llGnubby.NODEVICE; 92 if (rc) { 93 cb(rc); 94 return; 95 } 96 which = devs[0]; 97 setCid(which); 98 usbGnubby.gnubbies_.addClient(which, self, function(rc, device) { 99 if (rc == -llGnubby.NODEVICE && enumerateRetriesRemaining-- > 0) { 100 // We were trying to open the first device, but now it's not there? 101 // Do over. 102 usbGnubby.gnubbies_.enumerate(enumerated); 103 return; 104 } 105 self.dev = device; 106 cb(rc); 107 }); 108 } 109 110 if (which) { 111 setCid(which); 112 usbGnubby.gnubbies_.addClient(which, self, function(rc, device) { 113 self.dev = device; 114 cb(rc); 115 }); 116 } else { 117 usbGnubby.gnubbies_.enumerate(enumerated); 118 } 119}; 120 121/** 122 * @return {boolean} Whether this gnubby has any command outstanding. 123 * @private 124 */ 125usbGnubby.prototype.inUse_ = function() { 126 return this.commandPending; 127}; 128 129/** Closes this gnubby. */ 130usbGnubby.prototype.close = function() { 131 this.closed = true; 132 133 if (this.dev) { 134 console.log(UTIL_fmt('usbGnubby.close()')); 135 this.rxframes = []; 136 this.rxcb = null; 137 var dev = this.dev; 138 this.dev = null; 139 var self = this; 140 // Wait a bit in case simpleton client tries open next gnubby. 141 // Without delay, gnubbies would drop all idle devices, before client 142 // gets to the next one. 143 window.setTimeout( 144 function() { 145 usbGnubby.gnubbies_.removeClient(dev, self); 146 }, 300); 147 } 148}; 149 150/** 151 * Asks this gnubby to close when it gets a chance. 152 * @param {Function=} cb called back when closed. 153 */ 154usbGnubby.prototype.closeWhenIdle = function(cb) { 155 if (!this.inUse_()) { 156 this.close(); 157 if (cb) cb(); 158 return; 159 } 160 this.closingWhenIdle = true; 161 if (cb) this.notifyOnClose.push(cb); 162}; 163 164/** 165 * Close and notify every caller that it is now closed. 166 * @private 167 */ 168usbGnubby.prototype.idleClose_ = function() { 169 this.close(); 170 while (this.notifyOnClose.length != 0) { 171 var cb = this.notifyOnClose.shift(); 172 cb(); 173 } 174}; 175 176/** 177 * Notify callback for every frame received. 178 * @param {function()} cb Callback 179 * @private 180 */ 181usbGnubby.prototype.notifyFrame_ = function(cb) { 182 if (this.rxframes.length != 0) { 183 // Already have frames; continue. 184 if (cb) window.setTimeout(cb, 0); 185 } else { 186 this.rxcb = cb; 187 } 188}; 189 190/** 191 * Called by low level driver with a frame. 192 * @param {ArrayBuffer|Uint8Array} frame Data frame 193 * @return {boolean} Whether this client is still interested in receiving 194 * frames from its device. 195 */ 196usbGnubby.prototype.receivedFrame = function(frame) { 197 if (this.closed) return false; // No longer interested. 198 199 if (!this.checkCID_(frame)) { 200 // Not for me, ignore. 201 return true; 202 } 203 204 this.rxframes.push(frame); 205 206 // Callback self in case we were waiting. Once. 207 var cb = this.rxcb; 208 this.rxcb = null; 209 if (cb) window.setTimeout(cb, 0); 210 211 return true; 212}; 213 214/** 215 * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none. 216 * @private 217 */ 218usbGnubby.prototype.readFrame_ = function() { 219 if (this.rxframes.length == 0) throw 'rxframes empty!'; 220 221 var frame = this.rxframes.shift(); 222 return frame; 223}; 224 225/** Poll from rxframes[]. 226 * @param {number} cmd Command 227 * @param {number} timeout timeout in seconds. 228 * @param {?function(...)} cb Callback 229 * @private 230 */ 231usbGnubby.prototype.read_ = function(cmd, timeout, cb) { 232 if (this.closed) { cb(-llGnubby.GONE); return; } 233 if (!this.dev) { cb(-llGnubby.GONE); return; } 234 235 var tid = null; // timeout timer id. 236 var callback = cb; 237 var self = this; 238 239 var msg = null; 240 var seqno = 0; 241 var count = 0; 242 243 /** 244 * Schedule call to cb if not called yet. 245 * @param {number} a Return code. 246 * @param {Object=} b Optional data. 247 */ 248 function schedule_cb(a, b) { 249 self.commandPending = false; 250 if (tid) { 251 // Cancel timeout timer. 252 window.clearTimeout(tid); 253 tid = null; 254 } 255 var c = callback; 256 if (c) { 257 callback = null; 258 window.setTimeout(function() { c(a, b); }, 0); 259 } 260 if (self.closingWhenIdle) self.idleClose_(); 261 }; 262 263 function read_timeout() { 264 if (!callback || !tid) return; // Already done. 265 266 console.error(UTIL_fmt( 267 '[' + self.cid.toString(16) + '] timeout!')); 268 269 if (self.dev) { 270 self.dev.destroy(); // Stop pretending this thing works. 271 } 272 273 tid = null; 274 275 schedule_cb(-llGnubby.TIMEOUT); 276 }; 277 278 function cont_frame() { 279 if (!callback || !tid) return; // Already done. 280 281 var f = new Uint8Array(self.readFrame_()); 282 var rcmd = f[4]; 283 var totalLen = (f[5] << 8) + f[6]; 284 285 if (rcmd == llGnubby.CMD_ERROR && totalLen == 1) { 286 // Error from device; forward. 287 console.log(UTIL_fmt( 288 '[' + self.cid.toString(16) + '] error frame ' + 289 UTIL_BytesToHex(f))); 290 if (f[7] == llGnubby.GONE) { 291 self.closed = true; 292 } 293 schedule_cb(-f[7]); 294 return; 295 } 296 297 if ((rcmd & 0x80)) { 298 // Not an CONT frame, ignore. 299 console.log(UTIL_fmt( 300 '[' + self.cid.toString(16) + '] ignoring non-cont frame ' + 301 UTIL_BytesToHex(f))); 302 self.notifyFrame_(cont_frame); 303 return; 304 } 305 306 var seq = (rcmd & 0x7f); 307 if (seq != seqno++) { 308 console.log(UTIL_fmt( 309 '[' + self.cid.toString(16) + '] bad cont frame ' + 310 UTIL_BytesToHex(f))); 311 schedule_cb(-llGnubby.INVALID_SEQ); 312 return; 313 } 314 315 // Copy payload. 316 for (var i = 5; i < f.length && count < msg.length; ++i) { 317 msg[count++] = f[i]; 318 } 319 320 if (count == msg.length) { 321 // Done. 322 schedule_cb(-llGnubby.OK, msg.buffer); 323 } else { 324 // Need more CONT frame(s). 325 self.notifyFrame_(cont_frame); 326 } 327 } 328 329 function init_frame() { 330 if (!callback || !tid) return; // Already done. 331 332 var f = new Uint8Array(self.readFrame_()); 333 334 var rcmd = f[4]; 335 var totalLen = (f[5] << 8) + f[6]; 336 337 if (rcmd == llGnubby.CMD_ERROR && totalLen == 1) { 338 // Error from device; forward. 339 // Don't log busy frames, they're "normal". 340 if (f[7] != llGnubby.BUSY) { 341 console.log(UTIL_fmt( 342 '[' + self.cid.toString(16) + '] error frame ' + 343 UTIL_BytesToHex(f))); 344 } 345 if (f[7] == llGnubby.GONE) { 346 self.closed = true; 347 } 348 schedule_cb(-f[7]); 349 return; 350 } 351 352 if (!(rcmd & 0x80)) { 353 // Not an init frame, ignore. 354 console.log(UTIL_fmt( 355 '[' + self.cid.toString(16) + '] ignoring non-init frame ' + 356 UTIL_BytesToHex(f))); 357 self.notifyFrame_(init_frame); 358 return; 359 } 360 361 if (rcmd != cmd) { 362 // Not expected ack, read more. 363 console.log(UTIL_fmt( 364 '[' + self.cid.toString(16) + '] ignoring non-ack frame ' + 365 UTIL_BytesToHex(f))); 366 self.notifyFrame_(init_frame); 367 return; 368 } 369 370 // Copy payload. 371 msg = new Uint8Array(totalLen); 372 for (var i = 7; i < f.length && count < msg.length; ++i) { 373 msg[count++] = f[i]; 374 } 375 376 if (count == msg.length) { 377 // Done. 378 schedule_cb(-llGnubby.OK, msg.buffer); 379 } else { 380 // Need more CONT frame(s). 381 self.notifyFrame_(cont_frame); 382 } 383 } 384 385 // Start timeout timer. 386 tid = window.setTimeout(read_timeout, 1000.0 * timeout); 387 388 // Schedule read of first frame. 389 self.notifyFrame_(init_frame); 390}; 391 392/** 393 * @param {ArrayBuffer|Uint8Array} frame Data frame 394 * @return {boolean} Whether frame is for my channel. 395 * @private 396 */ 397usbGnubby.prototype.checkCID_ = function(frame) { 398 var f = new Uint8Array(frame); 399 var c = (f[0] << 24) | 400 (f[1] << 16) | 401 (f[2] << 8) | 402 (f[3]); 403 return c === this.cid || 404 c === 0; // Generic notification. 405}; 406 407/** 408 * Queue command for sending. 409 * @param {number} cmd The command to send. 410 * @param {ArrayBuffer|Uint8Array} data Command data 411 * @private 412 */ 413usbGnubby.prototype.write_ = function(cmd, data) { 414 if (this.closed) return; 415 if (!this.dev) return; 416 417 this.commandPending = true; 418 419 this.dev.queueCommand(this.cid, cmd, data); 420}; 421 422/** 423 * Writes the command, and calls back when the command's reply is received. 424 * @param {number} cmd The command to send. 425 * @param {ArrayBuffer|Uint8Array} data Command data 426 * @param {number} timeout Timeout in seconds. 427 * @param {function(number, ArrayBuffer=)} cb Callback 428 * @private 429 */ 430usbGnubby.prototype.exchange_ = function(cmd, data, timeout, cb) { 431 var busyWait = new CountdownTimer(this.busyMillis); 432 var self = this; 433 434 function retryBusy(rc, rc_data) { 435 if (rc == -llGnubby.BUSY && !busyWait.expired()) { 436 if (usbGnubby.gnubbies_) { 437 usbGnubby.gnubbies_.resetInactivityTimer(timeout * 1000); 438 } 439 self.write_(cmd, data); 440 self.read_(cmd, timeout, retryBusy); 441 } else { 442 busyWait.clearTimeout(); 443 cb(rc, rc_data); 444 } 445 } 446 447 retryBusy(-llGnubby.BUSY, undefined); // Start work. 448}; 449 450/** Default callback for commands. Simply logs to console. 451 * @param {number} rc Result status code 452 * @param {(ArrayBuffer|Uint8Array|Array.<number>|null)} data Result data 453 */ 454usbGnubby.defaultCallback = function(rc, data) { 455 var msg = 'defaultCallback(' + rc; 456 if (data) { 457 if (typeof data == 'string') msg += ', ' + data; 458 else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data)); 459 } 460 msg += ')'; 461 console.log(UTIL_fmt(msg)); 462}; 463 464/** Send nonce to device, flush read queue until match. 465 * @param {?function(...)} cb Callback 466 */ 467usbGnubby.prototype.sync = function(cb) { 468 if (!cb) cb = usbGnubby.defaultCallback; 469 if (this.closed) { 470 cb(-llGnubby.GONE); 471 return; 472 } 473 474 var done = false; 475 var trycount = 6; 476 var tid = null; 477 var self = this; 478 479 function callback(rc) { 480 done = true; 481 self.commandPending = false; 482 if (tid) { 483 window.clearTimeout(tid); 484 tid = null; 485 } 486 if (rc) console.warn(UTIL_fmt('sync failed: ' + rc)); 487 if (cb) cb(rc); 488 if (self.closingWhenIdle) self.idleClose_(); 489 } 490 491 function sendSentinel() { 492 var data = new Uint8Array(1); 493 data[0] = ++self.synccnt; 494 self.write_(llGnubby.CMD_SYNC, data.buffer); 495 } 496 497 function checkSentinel() { 498 var f = new Uint8Array(self.readFrame_()); 499 500 // Device disappeared on us. 501 if (f[4] == llGnubby.CMD_ERROR && 502 f[5] == 0 && f[6] == 1 && 503 f[7] == llGnubby.GONE) { 504 self.closed = true; 505 callback(-llGnubby.GONE); 506 return; 507 } 508 509 // Eat everything else but expected sync reply. 510 if (f[4] != llGnubby.CMD_SYNC || 511 (f.length > 7 && /* fw pre-0.2.1 bug: does not echo sentinel */ 512 f[7] != self.synccnt)) { 513 // Read more. 514 self.notifyFrame_(checkSentinel); 515 return; 516 } 517 518 // Done. 519 callback(-llGnubby.OK); 520 }; 521 522 function timeoutLoop() { 523 if (done) return; 524 525 if (trycount == 0) { 526 // Failed. 527 callback(-llGnubby.TIMEOUT); 528 return; 529 } 530 531 --trycount; // Try another one. 532 sendSentinel(); 533 self.notifyFrame_(checkSentinel); 534 tid = window.setTimeout(timeoutLoop, 500); 535 }; 536 537 timeoutLoop(); 538}; 539 540/** Short timeout value in seconds */ 541usbGnubby.SHORT_TIMEOUT = 1; 542/** Normal timeout value in seconds */ 543usbGnubby.NORMAL_TIMEOUT = 3; 544// Max timeout usb firmware has for smartcard response is 30 seconds. 545// Make our application level tolerance a little longer. 546/** Maximum timeout in seconds */ 547usbGnubby.MAX_TIMEOUT = 31; 548 549/** Blink led 550 * @param {number|ArrayBuffer|Uint8Array} data Command data or number 551 * of seconds to blink 552 * @param {?function(...)} cb Callback 553 */ 554usbGnubby.prototype.blink = function(data, cb) { 555 if (!cb) cb = usbGnubby.defaultCallback; 556 if (typeof data == 'number') { 557 var d = new Uint8Array([data]); 558 data = d.buffer; 559 } 560 this.exchange_(llGnubby.CMD_PROMPT, data, usbGnubby.NORMAL_TIMEOUT, cb); 561}; 562 563/** Lock the gnubby 564 * @param {number|ArrayBuffer|Uint8Array} data Command data 565 * @param {?function(...)} cb Callback 566 */ 567usbGnubby.prototype.lock = function(data, cb) { 568 if (!cb) cb = usbGnubby.defaultCallback; 569 if (typeof data == 'number') { 570 var d = new Uint8Array([data]); 571 data = d.buffer; 572 } 573 this.exchange_(llGnubby.CMD_LOCK, data, usbGnubby.NORMAL_TIMEOUT, cb); 574}; 575 576/** Unlock the gnubby 577 * @param {?function(...)} cb Callback 578 */ 579usbGnubby.prototype.unlock = function(cb) { 580 if (!cb) cb = usbGnubby.defaultCallback; 581 var data = new Uint8Array([0]); 582 this.exchange_(llGnubby.CMD_LOCK, data.buffer, 583 usbGnubby.NORMAL_TIMEOUT, cb); 584}; 585 586/** Request system information data. 587 * @param {?function(...)} cb Callback 588 */ 589usbGnubby.prototype.sysinfo = function(cb) { 590 if (!cb) cb = usbGnubby.defaultCallback; 591 this.exchange_(llGnubby.CMD_SYSINFO, new ArrayBuffer(0), 592 usbGnubby.NORMAL_TIMEOUT, cb); 593}; 594 595/** Send wink command 596 * @param {?function(...)} cb Callback 597 */ 598usbGnubby.prototype.wink = function(cb) { 599 if (!cb) cb = usbGnubby.defaultCallback; 600 this.exchange_(llGnubby.CMD_WINK, new ArrayBuffer(0), 601 usbGnubby.NORMAL_TIMEOUT, cb); 602}; 603 604/** Send DFU (Device firmware upgrade) command 605 * @param {ArrayBuffer|Uint8Array} data Command data 606 * @param {?function(...)} cb Callback 607 */ 608usbGnubby.prototype.dfu = function(data, cb) { 609 if (!cb) cb = usbGnubby.defaultCallback; 610 this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb); 611}; 612 613/** Ping the gnubby 614 * @param {number|ArrayBuffer|Uint8Array} data Command data 615 * @param {?function(...)} cb Callback 616 */ 617usbGnubby.prototype.ping = function(data, cb) { 618 if (!cb) cb = usbGnubby.defaultCallback; 619 if (typeof data == 'number') { 620 var d = new Uint8Array(data); 621 window.crypto.getRandomValues(d); 622 data = d.buffer; 623 } 624 this.exchange_(llGnubby.CMD_PING, data, usbGnubby.NORMAL_TIMEOUT, cb); 625}; 626 627/** Send a raw APDU command 628 * @param {ArrayBuffer|Uint8Array} data Command data 629 * @param {?function(...)} cb Callback 630 */ 631usbGnubby.prototype.apdu = function(data, cb) { 632 if (!cb) cb = usbGnubby.defaultCallback; 633 this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb); 634}; 635 636/** Reset gnubby 637 * @param {?function(...)} cb Callback 638 */ 639usbGnubby.prototype.reset = function(cb) { 640 if (!cb) cb = usbGnubby.defaultCallback; 641 this.exchange_(llGnubby.CMD_ATR, new ArrayBuffer(0), 642 usbGnubby.NORMAL_TIMEOUT, cb); 643}; 644 645// byte args[3] = [delay-in-ms before disabling interrupts, 646// delay-in-ms before disabling usb (aka remove), 647// delay-in-ms before reboot (aka insert)] 648/** Send usb test command 649 * @param {ArrayBuffer|Uint8Array} args Command data 650 * @param {?function(...)} cb Callback 651 */ 652usbGnubby.prototype.usb_test = function(args, cb) { 653 if (!cb) cb = usbGnubby.defaultCallback; 654 var u8 = new Uint8Array(args); 655 this.exchange_(llGnubby.CMD_USB_TEST, u8.buffer, 656 usbGnubby.NORMAL_TIMEOUT, cb); 657}; 658 659/** APDU command with reply 660 * @param {ArrayBuffer|Uint8Array} request The request 661 * @param {?function(...)} cb Callback 662 * @param {boolean=} opt_nowink Do not wink 663 * @private 664 */ 665usbGnubby.prototype.apduReply_ = function(request, cb, opt_nowink) { 666 if (!cb) cb = usbGnubby.defaultCallback; 667 var self = this; 668 669 this.apdu(request, function(rc, data) { 670 if (rc == 0) { 671 var r8 = new Uint8Array(data); 672 if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) { 673 // strip trailing 9000 674 var buf = new Uint8Array(r8.subarray(0, r8.length - 2)); 675 cb(-llGnubby.OK, buf.buffer); 676 return; 677 } else { 678 // return non-9000 as rc 679 rc = r8[r8.length - 2] * 256 + r8[r8.length - 1]; 680 // wink gnubby at hand if it needs touching. 681 if (rc == 0x6985 && !opt_nowink) { 682 self.wink(function() { cb(rc); }); 683 return; 684 } 685 } 686 } 687 // Warn on errors other than waiting for touch, wrong data, and 688 // unrecognized command. 689 if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) { 690 console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16))); 691 } 692 cb(rc); 693 }); 694}; 695