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.
6 * @fileoverview Low level usb cruft to talk gnubby.
11 // Global Gnubby instance counter.
15 * Creates a worker Gnubby instance.
17 * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result.
19 function usbGnubby(opt_busySeconds
) {
21 this.cid
= (++gnubbyId
) & 0x00ffffff; // Pick unique channel.
26 this.commandPending
= false;
27 this.notifyOnClose
= [];
28 this.busyMillis
= (opt_busySeconds
? opt_busySeconds
* 1000 : 2500);
32 * Sets usbGnubby's Gnubbies singleton.
33 * @param {Gnubbies} gnubbies Gnubbies singleton instance
35 usbGnubby
.setGnubbies = function(gnubbies
) {
36 /** @private {Gnubbies} */
37 usbGnubby
.gnubbies_
= gnubbies
;
41 * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the
42 * result of enumerating.
44 usbGnubby
.prototype.enumerate = function(cb
) {
45 if (!cb
) cb
= usbGnubby
.defaultCallback
;
50 if (!usbGnubby
.gnubbies_
) {
51 cb(-llGnubby
.NODEVICE
);
55 usbGnubby
.gnubbies_
.enumerate(cb
);
59 * Opens the gnubby with the given index, or the first found gnubby if no
61 * @param {llGnubbyDeviceId|undefined} opt_which The device to open.
62 * @param {function(number)|undefined} opt_cb Called with result of opening the
65 usbGnubby
.prototype.open = function(opt_which
, opt_cb
) {
66 var cb
= opt_cb
? opt_cb
: usbGnubby
.defaultCallback
;
71 this.closingWhenIdle
= false;
73 if (document
.location
.href
.indexOf('_generated_') == -1) {
74 // Not background page.
75 // Pick more random cid to tell things apart on the usb bus.
76 var rnd
= UTIL_getRandom(2);
77 this.cid
^= (rnd
[0] << 16) | (rnd
[1] << 8);
81 function addSelfAsClient(which
) {
82 self
.cid
&= 0x00ffffff;
83 self
.cid
|= ((which
.device
+ 1) << 24); // For debugging.
85 usbGnubby
.gnubbies_
.addClient(which
, self
, function(rc
, device
) {
91 if (!usbGnubby
.gnubbies_
) {
92 cb(-llGnubby
.NODEVICE
);
96 addSelfAsClient(opt_which
);
98 usbGnubby
.gnubbies_
.enumerate(function(rc
, devs
) {
99 if (rc
|| !devs
.length
) {
100 cb(-llGnubby
.NODEVICE
);
103 addSelfAsClient(devs
[0]);
109 * @return {boolean} Whether this gnubby has any command outstanding.
112 usbGnubby
.prototype.inUse_ = function() {
113 return this.commandPending
;
116 /** Closes this gnubby. */
117 usbGnubby
.prototype.close = function() {
121 console
.log(UTIL_fmt('usbGnubby.close()'));
127 // Wait a bit in case simpleton client tries open next gnubby.
128 // Without delay, gnubbies would drop all idle devices, before client
129 // gets to the next one.
132 usbGnubby
.gnubbies_
.removeClient(dev
, self
);
138 * Asks this gnubby to close when it gets a chance.
139 * @param {Function=} cb called back when closed.
141 usbGnubby
.prototype.closeWhenIdle = function(cb
) {
142 if (!this.inUse_()) {
147 this.closingWhenIdle
= true;
148 if (cb
) this.notifyOnClose
.push(cb
);
152 * Close and notify every caller that it is now closed.
155 usbGnubby
.prototype.idleClose_ = function() {
157 while (this.notifyOnClose
.length
!= 0) {
158 var cb
= this.notifyOnClose
.shift();
164 * Notify callback for every frame received.
165 * @param {function()} cb Callback
168 usbGnubby
.prototype.notifyFrame_ = function(cb
) {
169 if (this.rxframes
.length
!= 0) {
170 // Already have frames; continue.
171 if (cb
) window
.setTimeout(cb
, 0);
178 * Called by low level driver with a frame.
179 * @param {ArrayBuffer|Uint8Array} frame Data frame
180 * @return {boolean} Whether this client is still interested in receiving
181 * frames from its device.
183 usbGnubby
.prototype.receivedFrame = function(frame
) {
184 if (this.closed
) return false; // No longer interested.
186 if (!this.checkCID_(frame
)) {
187 // Not for me, ignore.
191 this.rxframes
.push(frame
);
193 // Callback self in case we were waiting. Once.
196 if (cb
) window
.setTimeout(cb
, 0);
202 * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none.
205 usbGnubby
.prototype.readFrame_ = function() {
206 if (this.rxframes
.length
== 0) throw 'rxframes empty!';
208 var frame
= this.rxframes
.shift();
212 /** Poll from rxframes[].
213 * @param {number} cmd Command
214 * @param {number} timeout timeout in seconds.
215 * @param {?function(...)} cb Callback
218 usbGnubby
.prototype.read_ = function(cmd
, timeout
, cb
) {
219 if (this.closed
) { cb(-llGnubby
.GONE
); return; }
220 if (!this.dev
) { cb(-llGnubby
.NODEVICE
); return; }
222 var tid
= null; // timeout timer id.
231 * Schedule call to cb if not called yet.
232 * @param {number} a Return code.
233 * @param {Object=} b Optional data.
235 function schedule_cb(a
, b
) {
236 self
.commandPending
= false;
238 // Cancel timeout timer.
239 window
.clearTimeout(tid
);
245 window
.setTimeout(function() { c(a
, b
); }, 0);
247 if (self
.closingWhenIdle
) self
.idleClose_();
250 function read_timeout() {
251 if (!callback
|| !tid
) return; // Already done.
253 console
.error(UTIL_fmt(
254 '[' + self
.cid
.toString(16) + '] timeout!'));
257 self
.dev
.destroy(); // Stop pretending this thing works.
262 schedule_cb(-llGnubby
.TIMEOUT
);
265 function cont_frame() {
266 if (!callback
|| !tid
) return; // Already done.
268 var f
= new Uint8Array(self
.readFrame_());
270 var totalLen
= (f
[5] << 8) + f
[6];
272 if (rcmd
== llGnubby
.CMD_ERROR
&& totalLen
== 1) {
273 // Error from device; forward.
274 console
.log(UTIL_fmt(
275 '[' + self
.cid
.toString(16) + '] error frame ' +
276 UTIL_BytesToHex(f
)));
277 if (f
[7] == llGnubby
.GONE
) {
285 // Not an CONT frame, ignore.
286 console
.log(UTIL_fmt(
287 '[' + self
.cid
.toString(16) + '] ignoring non-cont frame ' +
288 UTIL_BytesToHex(f
)));
289 self
.notifyFrame_(cont_frame
);
293 var seq
= (rcmd
& 0x7f);
294 if (seq
!= seqno
++) {
295 console
.log(UTIL_fmt(
296 '[' + self
.cid
.toString(16) + '] bad cont frame ' +
297 UTIL_BytesToHex(f
)));
298 schedule_cb(-llGnubby
.INVALID_SEQ
);
303 for (var i
= 5; i
< f
.length
&& count
< msg
.length
; ++i
) {
307 if (count
== msg
.length
) {
309 schedule_cb(-llGnubby
.OK
, msg
.buffer
);
311 // Need more CONT frame(s).
312 self
.notifyFrame_(cont_frame
);
316 function init_frame() {
317 if (!callback
|| !tid
) return; // Already done.
319 var f
= new Uint8Array(self
.readFrame_());
322 var totalLen
= (f
[5] << 8) + f
[6];
324 if (rcmd
== llGnubby
.CMD_ERROR
&& totalLen
== 1) {
325 // Error from device; forward.
326 // Don't log busy frames, they're "normal".
327 if (f
[7] != llGnubby
.BUSY
) {
328 console
.log(UTIL_fmt(
329 '[' + self
.cid
.toString(16) + '] error frame ' +
330 UTIL_BytesToHex(f
)));
332 if (f
[7] == llGnubby
.GONE
) {
339 if (!(rcmd
& 0x80)) {
340 // Not an init frame, ignore.
341 console
.log(UTIL_fmt(
342 '[' + self
.cid
.toString(16) + '] ignoring non-init frame ' +
343 UTIL_BytesToHex(f
)));
344 self
.notifyFrame_(init_frame
);
349 // Not expected ack, read more.
350 console
.log(UTIL_fmt(
351 '[' + self
.cid
.toString(16) + '] ignoring non-ack frame ' +
352 UTIL_BytesToHex(f
)));
353 self
.notifyFrame_(init_frame
);
358 msg
= new Uint8Array(totalLen
);
359 for (var i
= 7; i
< f
.length
&& count
< msg
.length
; ++i
) {
363 if (count
== msg
.length
) {
365 schedule_cb(-llGnubby
.OK
, msg
.buffer
);
367 // Need more CONT frame(s).
368 self
.notifyFrame_(cont_frame
);
372 // Start timeout timer.
373 tid
= window
.setTimeout(read_timeout
, 1000.0 * timeout
);
375 // Schedule read of first frame.
376 self
.notifyFrame_(init_frame
);
380 * @param {ArrayBuffer|Uint8Array} frame Data frame
381 * @return {boolean} Whether frame is for my channel.
384 usbGnubby
.prototype.checkCID_ = function(frame
) {
385 var f
= new Uint8Array(frame
);
386 var c
= (f
[0] << 24) |
390 return c
=== this.cid
||
391 c
=== 0; // Generic notification.
395 * Queue command for sending.
396 * @param {number} cmd The command to send.
397 * @param {ArrayBuffer|Uint8Array} data Command data
400 usbGnubby
.prototype.write_ = function(cmd
, data
) {
401 if (this.closed
) return;
402 if (!this.dev
) return;
404 this.commandPending
= true;
406 this.dev
.queueCommand(this.cid
, cmd
, data
);
410 * Writes the command, and calls back when the command's reply is received.
411 * @param {number} cmd The command to send.
412 * @param {ArrayBuffer|Uint8Array} data Command data
413 * @param {number} timeout Timeout in seconds.
414 * @param {function(number, ArrayBuffer=)} cb Callback
417 usbGnubby
.prototype.exchange_ = function(cmd
, data
, timeout
, cb
) {
418 var busyWait
= new CountdownTimer(this.busyMillis
);
421 function retryBusy(rc
, rc_data
) {
422 if (rc
== -llGnubby
.BUSY
&& !busyWait
.expired()) {
423 if (usbGnubby
.gnubbies_
) {
424 usbGnubby
.gnubbies_
.resetInactivityTimer(timeout
* 1000);
426 self
.write_(cmd
, data
);
427 self
.read_(cmd
, timeout
, retryBusy
);
429 busyWait
.clearTimeout();
434 retryBusy(-llGnubby
.BUSY
, undefined); // Start work.
437 /** Default callback for commands. Simply logs to console.
438 * @param {number} rc Result status code
439 * @param {*} data Result data
441 usbGnubby
.defaultCallback = function(rc
, data
) {
442 var msg
= 'defaultCallback(' + rc
;
444 if (typeof data
== 'string') msg
+= ', ' + data
;
445 else msg
+= ', ' + UTIL_BytesToHex(new Uint8Array(data
));
448 console
.log(UTIL_fmt(msg
));
451 /** Send nonce to device, flush read queue until match.
452 * @param {?function(...)} cb Callback
454 usbGnubby
.prototype.sync = function(cb
) {
455 if (!cb
) cb
= usbGnubby
.defaultCallback
;
466 function callback(rc
) {
468 self
.commandPending
= false;
470 window
.clearTimeout(tid
);
473 if (rc
) console
.warn(UTIL_fmt('sync failed: ' + rc
));
475 if (self
.closingWhenIdle
) self
.idleClose_();
478 function sendSentinel() {
479 var data
= new Uint8Array(1);
480 data
[0] = ++self
.synccnt
;
481 self
.write_(llGnubby
.CMD_SYNC
, data
.buffer
);
484 function checkSentinel() {
485 var f
= new Uint8Array(self
.readFrame_());
487 // Device disappeared on us.
488 if (f
[4] == llGnubby
.CMD_ERROR
&&
489 f
[5] == 0 && f
[6] == 1 &&
490 f
[7] == llGnubby
.GONE
) {
492 callback(-llGnubby
.GONE
);
496 // Eat everything else but expected sync reply.
497 if (f
[4] != llGnubby
.CMD_SYNC
||
498 (f
.length
> 7 && /* fw pre-0.2.1 bug: does not echo sentinel */
499 f
[7] != self
.synccnt
)) {
501 self
.notifyFrame_(checkSentinel
);
506 callback(-llGnubby
.OK
);
509 function timeoutLoop() {
514 callback(-llGnubby
.TIMEOUT
);
518 --trycount
; // Try another one.
520 self
.notifyFrame_(checkSentinel
);
521 tid
= window
.setTimeout(timeoutLoop
, 500);
527 /** Short timeout value in seconds */
528 usbGnubby
.SHORT_TIMEOUT
= 1;
529 /** Normal timeout value in seconds */
530 usbGnubby
.NORMAL_TIMEOUT
= 3;
531 // Max timeout usb firmware has for smartcard response is 30 seconds.
532 // Make our application level tolerance a little longer.
533 /** Maximum timeout in seconds */
534 usbGnubby
.MAX_TIMEOUT
= 31;
537 * @param {number|ArrayBuffer|Uint8Array} data Command data or number
538 * of seconds to blink
539 * @param {?function(...)} cb Callback
541 usbGnubby
.prototype.blink = function(data
, cb
) {
542 if (!cb
) cb
= usbGnubby
.defaultCallback
;
543 if (typeof data
== 'number') {
544 var d
= new Uint8Array([data
]);
547 this.exchange_(llGnubby
.CMD_PROMPT
, data
, usbGnubby
.NORMAL_TIMEOUT
, cb
);
551 * @param {number|ArrayBuffer|Uint8Array} data Command data
552 * @param {?function(...)} cb Callback
554 usbGnubby
.prototype.lock = function(data
, cb
) {
555 if (!cb
) cb
= usbGnubby
.defaultCallback
;
556 if (typeof data
== 'number') {
557 var d
= new Uint8Array([data
]);
560 this.exchange_(llGnubby
.CMD_LOCK
, data
, usbGnubby
.NORMAL_TIMEOUT
, cb
);
563 /** Unlock the gnubby
564 * @param {?function(...)} cb Callback
566 usbGnubby
.prototype.unlock = function(cb
) {
567 if (!cb
) cb
= usbGnubby
.defaultCallback
;
568 var data
= new Uint8Array([0]);
569 this.exchange_(llGnubby
.CMD_LOCK
, data
.buffer
,
570 usbGnubby
.NORMAL_TIMEOUT
, cb
);
573 /** Request system information data.
574 * @param {?function(...)} cb Callback
576 usbGnubby
.prototype.sysinfo = function(cb
) {
577 if (!cb
) cb
= usbGnubby
.defaultCallback
;
578 this.exchange_(llGnubby
.CMD_SYSINFO
, new ArrayBuffer(0),
579 usbGnubby
.NORMAL_TIMEOUT
, cb
);
582 /** Send wink command
583 * @param {?function(...)} cb Callback
585 usbGnubby
.prototype.wink = function(cb
) {
586 if (!cb
) cb
= usbGnubby
.defaultCallback
;
587 this.exchange_(llGnubby
.CMD_WINK
, new ArrayBuffer(0),
588 usbGnubby
.NORMAL_TIMEOUT
, cb
);
591 /** Send DFU (Device firmware upgrade) command
592 * @param {ArrayBuffer|Uint8Array} data Command data
593 * @param {?function(...)} cb Callback
595 usbGnubby
.prototype.dfu = function(data
, cb
) {
596 if (!cb
) cb
= usbGnubby
.defaultCallback
;
597 this.exchange_(llGnubby
.CMD_DFU
, data
, usbGnubby
.NORMAL_TIMEOUT
, cb
);
601 * @param {number|ArrayBuffer|Uint8Array} data Command data
602 * @param {?function(...)} cb Callback
604 usbGnubby
.prototype.ping = function(data
, cb
) {
605 if (!cb
) cb
= usbGnubby
.defaultCallback
;
606 if (typeof data
== 'number') {
607 var d
= new Uint8Array(data
);
608 window
.crypto
.getRandomValues(d
);
611 this.exchange_(llGnubby
.CMD_PING
, data
, usbGnubby
.NORMAL_TIMEOUT
, cb
);
614 /** Send a raw APDU command
615 * @param {ArrayBuffer|Uint8Array} data Command data
616 * @param {?function(...)} cb Callback
618 usbGnubby
.prototype.apdu = function(data
, cb
) {
619 if (!cb
) cb
= usbGnubby
.defaultCallback
;
620 this.exchange_(llGnubby
.CMD_APDU
, data
, usbGnubby
.MAX_TIMEOUT
, cb
);
624 * @param {?function(...)} cb Callback
626 usbGnubby
.prototype.reset = function(cb
) {
627 if (!cb
) cb
= usbGnubby
.defaultCallback
;
628 this.exchange_(llGnubby
.CMD_ATR
, new ArrayBuffer(0),
629 usbGnubby
.NORMAL_TIMEOUT
, cb
);
632 // byte args[3] = [delay-in-ms before disabling interrupts,
633 // delay-in-ms before disabling usb (aka remove),
634 // delay-in-ms before reboot (aka insert)]
635 /** Send usb test command
636 * @param {ArrayBuffer|Uint8Array} args Command data
637 * @param {?function(...)} cb Callback
639 usbGnubby
.prototype.usb_test = function(args
, cb
) {
640 if (!cb
) cb
= usbGnubby
.defaultCallback
;
641 var u8
= new Uint8Array(args
);
642 this.exchange_(llGnubby
.CMD_USB_TEST
, u8
.buffer
,
643 usbGnubby
.NORMAL_TIMEOUT
, cb
);
646 /** APDU command with reply
647 * @param {ArrayBuffer|Uint8Array} request The request
648 * @param {?function(...)} cb Callback
649 * @param {boolean=} opt_nowink Do not wink
652 usbGnubby
.prototype.apduReply_ = function(request
, cb
, opt_nowink
) {
653 if (!cb
) cb
= usbGnubby
.defaultCallback
;
656 this.apdu(request
, function(rc
, data
) {
658 var r8
= new Uint8Array(data
);
659 if (r8
[r8
.length
- 2] == 0x90 && r8
[r8
.length
- 1] == 0x00) {
660 // strip trailing 9000
661 var buf
= new Uint8Array(r8
.subarray(0, r8
.length
- 2));
662 cb(-llGnubby
.OK
, buf
.buffer
);
665 // return non-9000 as rc
666 rc
= r8
[r8
.length
- 2] * 256 + r8
[r8
.length
- 1];
667 // wink gnubby at hand if it needs touching.
668 if (rc
== 0x6985 && !opt_nowink
) {
669 self
.wink(function() { cb(rc
); });
674 // Warn on errors other than waiting for touch, wrong data, and
675 // unrecognized command.
676 if (rc
!= 0x6985 && rc
!= 0x6a80 && rc
!= 0x6d00) {
677 console
.warn(UTIL_fmt('apduReply_ fail: ' + rc
.toString(16)));