Home | History | Annotate | Download | only in cryptotoken
      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.
     12 var 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  */
     19 function 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  */
     35 usbGnubby.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  */
     44 usbGnubby.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  */
     66 usbGnubby.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  */
    125 usbGnubby.prototype.inUse_ = function() {
    126   return this.commandPending;
    127 };
    128 
    129 /** Closes this gnubby. */
    130 usbGnubby.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  */
    154 usbGnubby.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  */
    168 usbGnubby.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  */
    181 usbGnubby.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  */
    196 usbGnubby.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  */
    218 usbGnubby.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  */
    231 usbGnubby.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  */
    397 usbGnubby.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  */
    413 usbGnubby.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  */
    430 usbGnubby.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  */
    454 usbGnubby.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  */
    467 usbGnubby.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 */
    541 usbGnubby.SHORT_TIMEOUT = 1;
    542 /** Normal timeout value in seconds */
    543 usbGnubby.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 */
    547 usbGnubby.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  */
    554 usbGnubby.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  */
    567 usbGnubby.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  */
    579 usbGnubby.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  */
    589 usbGnubby.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  */
    598 usbGnubby.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  */
    608 usbGnubby.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  */
    617 usbGnubby.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  */
    631 usbGnubby.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  */
    639 usbGnubby.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  */
    652 usbGnubby.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  */
    665 usbGnubby.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