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 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