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