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