• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
18function 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 */
35Gnubby.gnubbyId_ = 0;
36
37/**
38 * Sets Gnubby's Gnubbies singleton.
39 * @param {Gnubbies} gnubbies Gnubbies singleton instance
40 */
41Gnubby.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 */
54Gnubby.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 */
113Gnubby.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 */
123Gnubby.prototype.inUse_ = function() {
124  return this.commandPending;
125};
126
127/** Closes this gnubby. */
128Gnubby.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 */
152Gnubby.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 */
166Gnubby.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 */
179Gnubby.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 */
194Gnubby.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 */
216Gnubby.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 */
229Gnubby.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  */
393Gnubby.NOTIFICATION_CID = 0;
394
395/**
396  * @const
397  */
398Gnubby.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 */
405Gnubby.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 */
422Gnubby.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 */
439Gnubby.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 */
463Gnubby.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 */
480Gnubby.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 */
643Gnubby.SHORT_TIMEOUT = 1;
644/** Normal timeout value in seconds */
645Gnubby.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 */
649Gnubby.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 */
656Gnubby.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 */
669Gnubby.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 */
681Gnubby.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 */
691Gnubby.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 */
700Gnubby.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 */
710Gnubby.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 */
719Gnubby.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 */
733Gnubby.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 */
741Gnubby.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 */
754Gnubby.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 */
766Gnubby.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