Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / gnubby.js
blobf3d449be8c97416c4b0290d5baf6aba9f7dd986b
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(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.
471 /** Default callback for commands. Simply logs to console.
472  * @param {number} rc Result status code
473  * @param {(ArrayBuffer|Uint8Array|Array<number>|null)} data Result data
474  */
475 Gnubby.defaultCallback = function(rc, data) {
476   var msg = 'defaultCallback(' + rc;
477   if (data) {
478     if (typeof data == 'string') msg += ', ' + data;
479     else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data));
480   }
481   msg += ')';
482   console.log(UTIL_fmt(msg));
486  * Ensures this device has temporary ownership of the USB device, by:
487  * 1. Using the INIT command to allocate an unique channel id, if one hasn't
488  *    been retrieved before, or
489  * 2. Sending a nonce to device, flushing read queue until match.
490  * @param {?function(...)} cb Callback
491  */
492 Gnubby.prototype.sync = function(cb) {
493   if (!cb) cb = Gnubby.defaultCallback;
494   if (this.closed) {
495     cb(-GnubbyDevice.GONE);
496     return;
497   }
499   var done = false;
500   var trycount = 6;
501   var tid = null;
502   var self = this;
504   function returnValue(rc) {
505     done = true;
506     window.setTimeout(cb.bind(null, rc), 0);
507     if (self.closingWhenIdle) self.idleClose_();
508   }
510   function callback(rc, opt_frame) {
511     self.commandPending = false;
512     if (tid) {
513       window.clearTimeout(tid);
514       tid = null;
515     }
516     completionAction(rc, opt_frame);
517   }
519   function sendSyncSentinel() {
520     var cmd = GnubbyDevice.CMD_SYNC;
521     var data = new Uint8Array(1);
522     data[0] = ++self.synccnt;
523     self.dev.queueCommand(self.cid, cmd, data.buffer);
524   }
526   function syncSentinelEquals(f) {
527     return (f[4] == GnubbyDevice.CMD_SYNC &&
528         (f.length == 7 || /* fw pre-0.2.1 bug: does not echo sentinel */
529          f[7] == self.synccnt));
530   }
532   function syncCompletionAction(rc, opt_frame) {
533     if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
534     returnValue(rc);
535   }
537   function sendInitSentinel() {
538     var cid = self.cid;
539     // If we do not have a specific CID yet, reset to BROADCAST for init.
540     if (self.cid == Gnubby.defaultChannelId_(self.gnubbyInstance, self.which)) {
541       self.cid = Gnubby.BROADCAST_CID;
542       cid = self.cid;
543     }
544     var cmd = GnubbyDevice.CMD_INIT;
545     self.dev.queueCommand(cid, cmd, nonce);
546   }
548   function initSentinelEquals(f) {
549     return (f[4] == GnubbyDevice.CMD_INIT &&
550         f.length >= nonce.length + 7 &&
551         UTIL_equalArrays(f.subarray(7, nonce.length + 7), nonce));
552   }
554   function initCmdUnsupported(rc) {
555     // Different firmwares fail differently on different inputs, so treat any
556     // of the following errors as indicating the INIT command isn't supported.
557     return rc == -GnubbyDevice.INVALID_CMD ||
558         rc == -GnubbyDevice.INVALID_PAR ||
559         rc == -GnubbyDevice.INVALID_LEN;
560   }
562   function initCompletionAction(rc, opt_frame) {
563     // Actual failures: bail out.
564     if (rc && !initCmdUnsupported(rc)) {
565       console.warn(UTIL_fmt('init failed: ' + rc));
566       returnValue(rc);
567     }
569     var HEADER_LENGTH = 7;
570     var MIN_LENGTH = HEADER_LENGTH + 4;  // 4 bytes for the channel id
571     if (rc || !opt_frame || opt_frame.length < nonce.length + MIN_LENGTH) {
572       // INIT command not supported or is missing the returned channel id:
573       // Pick a random cid to try to prevent collisions on the USB bus.
574       var rnd = UTIL_getRandom(2);
575       self.cid = Gnubby.defaultChannelId_(self.gnubbyInstance, self.which);
576       self.cid ^= (rnd[0] << 16) | (rnd[1] << 8);
577       // Now sync with that cid, to make sure we've got it.
578       setSync();
579       timeoutLoop();
580       return;
581     }
582     // Accept the provided cid.
583     var offs = HEADER_LENGTH + nonce.length;
584     self.cid = (opt_frame[offs] << 24) |
585                (opt_frame[offs + 1] << 16) |
586                (opt_frame[offs + 2] << 8) |
587                opt_frame[offs + 3];
588     returnValue(rc);
589   }
591   function checkSentinel() {
592     var f = new Uint8Array(self.readFrame_());
594     // Stop on errors and return them.
595     if (f[4] == GnubbyDevice.CMD_ERROR &&
596         f[5] == 0 && f[6] == 1) {
597       if (f[7] == GnubbyDevice.BUSY) {
598         // Not spec but some devices do this; retry.
599         sendSentinel();
600         self.notifyFrame_(checkSentinel);
601         return;
602       }
603       if (f[7] == GnubbyDevice.GONE) {
604         // Device disappeared on us.
605         self.closed = true;
606       }
607       callback(-f[7]);
608       return;
609     }
611     // Eat everything else but expected sentinel reply.
612     if (!sentinelEquals(f)) {
613       // Read more.
614       self.notifyFrame_(checkSentinel);
615       return;
616     }
618     // Done.
619     callback(-GnubbyDevice.OK, f);
620   };
622   function timeoutLoop() {
623     if (done) return;
625     if (trycount == 0) {
626       // Failed.
627       callback(-GnubbyDevice.TIMEOUT);
628       return;
629     }
631     --trycount;  // Try another one.
632     sendSentinel();
633     self.notifyFrame_(checkSentinel);
634     tid = window.setTimeout(timeoutLoop, 500);
635   };
637   var sendSentinel;
638   var sentinelEquals;
639   var nonce;
640   var completionAction;
642   function setInit() {
643     sendSentinel = sendInitSentinel;
644     nonce = UTIL_getRandom(8);
645     sentinelEquals = initSentinelEquals;
646     completionAction = initCompletionAction;
647   }
649   function setSync() {
650     sendSentinel = sendSyncSentinel;
651     sentinelEquals = syncSentinelEquals;
652     completionAction = syncCompletionAction;
653   }
655   if (Gnubby.gnubbies_.isSharedAccess(this.which)) {
656     setInit();
657   } else {
658     setSync();
659   }
660   timeoutLoop();
663 /** Short timeout value in seconds */
664 Gnubby.SHORT_TIMEOUT = 1;
665 /** Normal timeout value in seconds */
666 Gnubby.NORMAL_TIMEOUT = 3;
667 // Max timeout usb firmware has for smartcard response is 30 seconds.
668 // Make our application level tolerance a little longer.
669 /** Maximum timeout in seconds */
670 Gnubby.MAX_TIMEOUT = 31;
672 /** Blink led
673  * @param {number|ArrayBuffer|Uint8Array} data Command data or number
674  *     of seconds to blink
675  * @param {?function(...)} cb Callback
676  */
677 Gnubby.prototype.blink = function(data, cb) {
678   if (!cb) cb = Gnubby.defaultCallback;
679   if (typeof data == 'number') {
680     var d = new Uint8Array([data]);
681     data = d.buffer;
682   }
683   this.exchange_(GnubbyDevice.CMD_PROMPT, data, Gnubby.NORMAL_TIMEOUT, cb);
686 /** Lock the gnubby
687  * @param {number|ArrayBuffer|Uint8Array} data Command data
688  * @param {?function(...)} cb Callback
689  */
690 Gnubby.prototype.lock = function(data, cb) {
691   if (!cb) cb = Gnubby.defaultCallback;
692   if (typeof data == 'number') {
693     var d = new Uint8Array([data]);
694     data = d.buffer;
695   }
696   this.exchange_(GnubbyDevice.CMD_LOCK, data, Gnubby.NORMAL_TIMEOUT, cb);
699 /** Unlock the gnubby
700  * @param {?function(...)} cb Callback
701  */
702 Gnubby.prototype.unlock = function(cb) {
703   if (!cb) cb = Gnubby.defaultCallback;
704   var data = new Uint8Array([0]);
705   this.exchange_(GnubbyDevice.CMD_LOCK, data.buffer,
706       Gnubby.NORMAL_TIMEOUT, cb);
709 /** Request system information data.
710  * @param {?function(...)} cb Callback
711  */
712 Gnubby.prototype.sysinfo = function(cb) {
713   if (!cb) cb = Gnubby.defaultCallback;
714   this.exchange_(GnubbyDevice.CMD_SYSINFO, new ArrayBuffer(0),
715       Gnubby.NORMAL_TIMEOUT, cb);
718 /** Send wink command
719  * @param {?function(...)} cb Callback
720  */
721 Gnubby.prototype.wink = function(cb) {
722   if (!cb) cb = Gnubby.defaultCallback;
723   this.exchange_(GnubbyDevice.CMD_WINK, new ArrayBuffer(0),
724       Gnubby.NORMAL_TIMEOUT, cb);
727 /** Send DFU (Device firmware upgrade) command
728  * @param {ArrayBuffer|Uint8Array} data Command data
729  * @param {?function(...)} cb Callback
730  */
731 Gnubby.prototype.dfu = function(data, cb) {
732   if (!cb) cb = Gnubby.defaultCallback;
733   this.exchange_(GnubbyDevice.CMD_DFU, data, Gnubby.NORMAL_TIMEOUT, cb);
736 /** Ping the gnubby
737  * @param {number|ArrayBuffer|Uint8Array} data Command data
738  * @param {?function(...)} cb Callback
739  */
740 Gnubby.prototype.ping = function(data, cb) {
741   if (!cb) cb = Gnubby.defaultCallback;
742   if (typeof data == 'number') {
743     var d = new Uint8Array(data);
744     window.crypto.getRandomValues(d);
745     data = d.buffer;
746   }
747   this.exchange_(GnubbyDevice.CMD_PING, data, Gnubby.NORMAL_TIMEOUT, cb);
750 /** Send a raw APDU command
751  * @param {ArrayBuffer|Uint8Array} data Command data
752  * @param {?function(...)} cb Callback
753  */
754 Gnubby.prototype.apdu = function(data, cb) {
755   if (!cb) cb = Gnubby.defaultCallback;
756   this.exchange_(GnubbyDevice.CMD_APDU, data, Gnubby.MAX_TIMEOUT, cb);
759 /** Reset gnubby
760  * @param {?function(...)} cb Callback
761  */
762 Gnubby.prototype.reset = function(cb) {
763   if (!cb) cb = Gnubby.defaultCallback;
764   this.exchange_(GnubbyDevice.CMD_ATR, new ArrayBuffer(0),
765       Gnubby.NORMAL_TIMEOUT, cb);
768 // byte args[3] = [delay-in-ms before disabling interrupts,
769 //                 delay-in-ms before disabling usb (aka remove),
770 //                 delay-in-ms before reboot (aka insert)]
771 /** Send usb test command
772  * @param {ArrayBuffer|Uint8Array} args Command data
773  * @param {?function(...)} cb Callback
774  */
775 Gnubby.prototype.usb_test = function(args, cb) {
776   if (!cb) cb = Gnubby.defaultCallback;
777   var u8 = new Uint8Array(args);
778   this.exchange_(GnubbyDevice.CMD_USB_TEST, u8.buffer,
779       Gnubby.NORMAL_TIMEOUT, cb);
782 /** APDU command with reply
783  * @param {ArrayBuffer|Uint8Array} request The request
784  * @param {?function(...)} cb Callback
785  * @param {boolean=} opt_nowink Do not wink
786  */
787 Gnubby.prototype.apduReply = function(request, cb, opt_nowink) {
788   if (!cb) cb = Gnubby.defaultCallback;
789   var self = this;
791   this.apdu(request, function(rc, data) {
792     if (rc == 0) {
793       var r8 = new Uint8Array(data);
794       if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) {
795         // strip trailing 9000
796         var buf = new Uint8Array(r8.subarray(0, r8.length - 2));
797         cb(-GnubbyDevice.OK, buf.buffer);
798         return;
799       } else {
800         // return non-9000 as rc
801         rc = r8[r8.length - 2] * 256 + r8[r8.length - 1];
802         // wink gnubby at hand if it needs touching.
803         if (rc == 0x6985 && !opt_nowink) {
804           self.wink(function() { cb(rc); });
805           return;
806         }
807       }
808     }
809     // Warn on errors other than waiting for touch, wrong data, and
810     // unrecognized command.
811     if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) {
812       console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16)));
813     }
814     cb(rc);
815   });