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 Provides a client view of a gnubby, aka USB security key.
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
16 * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result.
18 function Gnubby(opt_busySeconds
) {
20 this.gnubbyInstance
= ++Gnubby
.gnubbyId_
;
21 this.cid
= Gnubby
.BROADCAST_CID
;
26 this.commandPending
= false;
27 this.notifyOnClose
= [];
28 this.busyMillis
= (opt_busySeconds
? opt_busySeconds
* 1000 : 9500);
32 * Global Gnubby instance counter.
38 * Sets Gnubby's Gnubbies singleton.
39 * @param {Gnubbies} gnubbies Gnubbies singleton instance
41 Gnubby
.setGnubbies = function(gnubbies
) {
42 /** @private {Gnubbies} */
43 Gnubby
.gnubbies_
= gnubbies
;
47 * Return cid as hex string.
48 * @param {number} cid to convert.
49 * @return {string} hexadecimal string.
51 Gnubby
.hexCid = function(cid
) {
52 var tmp
= [(cid
>>> 24) & 255,
56 return UTIL_BytesToHex(tmp
);
60 * Opens the gnubby with the given index, or the first found gnubby if no
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
67 Gnubby
.prototype.open = function(which
, opt_cb
) {
68 var cb
= opt_cb
? opt_cb
: Gnubby
.defaultCallback
;
70 cb(-GnubbyDevice
.NODEVICE
);
73 this.closingWhenIdle
= false;
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
);
82 var enumerateRetriesRemaining
= 3;
83 function enumerated(rc
, devs
) {
85 rc
= -GnubbyDevice
.NODEVICE
;
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?
97 Gnubby
.gnubbies_
.enumerate(enumerated
);
108 Gnubby
.gnubbies_
.addClient(which
, self
, function(rc
, device
) {
113 Gnubby
.gnubbies_
.enumerate(enumerated
);
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
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.
126 Gnubby
.defaultChannelId_ = function(gnubbyInstance
, which
) {
127 var cid
= (gnubbyInstance
) & 0x00ffffff;
128 cid
|= ((which
.device
+ 1) << 24); // For debugging.
133 * @return {boolean} Whether this gnubby has any command outstanding.
136 Gnubby
.prototype.inUse_ = function() {
137 return this.commandPending
;
140 /** Closes this gnubby. */
141 Gnubby
.prototype.close = function() {
145 console
.log(UTIL_fmt('Gnubby.close()'));
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.
156 Gnubby
.gnubbies_
.removeClient(dev
, self
);
162 * Asks this gnubby to close when it gets a chance.
163 * @param {Function=} cb called back when closed.
165 Gnubby
.prototype.closeWhenIdle = function(cb
) {
166 if (!this.inUse_()) {
171 this.closingWhenIdle
= true;
172 if (cb
) this.notifyOnClose
.push(cb
);
176 * Close and notify every caller that it is now closed.
179 Gnubby
.prototype.idleClose_ = function() {
181 while (this.notifyOnClose
.length
!= 0) {
182 var cb
= this.notifyOnClose
.shift();
188 * Notify callback for every frame received.
189 * @param {function()} cb Callback
192 Gnubby
.prototype.notifyFrame_ = function(cb
) {
193 if (this.rxframes
.length
!= 0) {
194 // Already have frames; continue.
195 if (cb
) window
.setTimeout(cb
, 0);
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.
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.
215 this.rxframes
.push(frame
);
217 // Callback self in case we were waiting. Once.
220 if (cb
) window
.setTimeout(cb
, 0);
226 * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none.
229 Gnubby
.prototype.readFrame_ = function() {
230 if (this.rxframes
.length
== 0) throw 'rxframes empty!';
232 var frame
= this.rxframes
.shift();
236 /** Poll from rxframes[].
237 * @param {number} cmd Command
238 * @param {number} timeout timeout in seconds.
239 * @param {?function(...)} cb Callback
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.
255 * Schedule call to cb if not called yet.
256 * @param {number} a Return code.
257 * @param {Object=} b Optional data.
259 function schedule_cb(a
, b
) {
260 self
.commandPending
= false;
262 // Cancel timeout timer.
263 window
.clearTimeout(tid
);
269 window
.setTimeout(function() { c(a
, b
); }, 0);
271 if (self
.closingWhenIdle
) self
.idleClose_();
274 function read_timeout() {
275 if (!callback
|| !tid
) return; // Already done.
277 console
.error(UTIL_fmt(
278 '[' + Gnubby
.hexCid(self
.cid
) + '] timeout!'));
281 self
.dev
.destroy(); // Stop pretending this thing works.
286 schedule_cb(-GnubbyDevice
.TIMEOUT
);
289 function cont_frame() {
290 if (!callback
|| !tid
) return; // Already done.
292 var f
= new Uint8Array(self
.readFrame_());
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
) {
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
);
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
);
327 for (var i
= 5; i
< f
.length
&& count
< msg
.length
; ++i
) {
331 if (count
== msg
.length
) {
333 schedule_cb(-GnubbyDevice
.OK
, msg
.buffer
);
335 // Need more CONT frame(s).
336 self
.notifyFrame_(cont_frame
);
340 function init_frame() {
341 if (!callback
|| !tid
) return; // Already done.
343 var f
= new Uint8Array(self
.readFrame_());
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
)));
356 if (f
[7] == GnubbyDevice
.GONE
) {
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
);
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
);
382 msg
= new Uint8Array(totalLen
);
383 for (var i
= 7; i
< f
.length
&& count
< msg
.length
; ++i
) {
387 if (count
== msg
.length
) {
389 schedule_cb(-GnubbyDevice
.OK
, msg
.buffer
);
391 // Need more CONT frame(s).
392 self
.notifyFrame_(cont_frame
);
396 // Start timeout timer.
397 tid
= window
.setTimeout(read_timeout
, 1000.0 * timeout
);
399 // Schedule read of first frame.
400 self
.notifyFrame_(init_frame
);
406 Gnubby
.NOTIFICATION_CID
= 0;
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.
418 Gnubby
.prototype.checkCID_ = function(frame
) {
419 var f
= new Uint8Array(frame
);
420 var c
= (f
[0] << 24) |
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
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
451 Gnubby
.prototype.exchange_ = function(cmd
, data
, timeout
, cb
) {
452 var busyWait
= new CountdownTimer(Gnubby
.SYS_TIMER_
, this.busyMillis
);
455 function retryBusy(rc
, rc_data
) {
456 if (rc
== -GnubbyDevice
.BUSY
&& !busyWait
.expired()) {
457 if (Gnubby
.gnubbies_
) {
458 Gnubby
.gnubbies_
.resetInactivityTimer(timeout
* 1000);
460 self
.write_(cmd
, data
);
461 self
.read_(cmd
, timeout
, retryBusy
);
463 busyWait
.clearTimeout();
468 retryBusy(-GnubbyDevice
.BUSY
, undefined); // Start work.
472 * Private instance of timers based on window's timer functions.
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
482 Gnubby
.defaultCallback = function(rc
, data
) {
483 var msg
= 'defaultCallback(' + rc
;
485 if (typeof data
== 'string') msg
+= ', ' + data
;
486 else msg
+= ', ' + UTIL_BytesToHex(new Uint8Array(data
));
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
499 Gnubby
.prototype.sync = function(cb
) {
500 if (!cb
) cb
= Gnubby
.defaultCallback
;
502 cb(-GnubbyDevice
.GONE
);
511 function returnValue(rc
) {
513 window
.setTimeout(cb
.bind(null, rc
), 0);
514 if (self
.closingWhenIdle
) self
.idleClose_();
517 function callback(rc
, opt_frame
) {
518 self
.commandPending
= false;
520 window
.clearTimeout(tid
);
523 completionAction(rc
, opt_frame
);
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
);
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
));
539 function syncCompletionAction(rc
, opt_frame
) {
540 if (rc
) console
.warn(UTIL_fmt('sync failed: ' + rc
));
544 function sendInitSentinel() {
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
;
551 var cmd
= GnubbyDevice
.CMD_INIT
;
552 self
.dev
.queueCommand(cid
, cmd
, nonce
);
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
));
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
;
569 function initCompletionAction(rc
, opt_frame
) {
570 // Actual failures: bail out.
571 if (rc
&& !initCmdUnsupported(rc
)) {
572 console
.warn(UTIL_fmt('init failed: ' + rc
));
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.
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) |
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.
607 self
.notifyFrame_(checkSentinel
);
610 if (f
[7] == GnubbyDevice
.GONE
) {
611 // Device disappeared on us.
618 // Eat everything else but expected sentinel reply.
619 if (!sentinelEquals(f
)) {
621 self
.notifyFrame_(checkSentinel
);
626 callback(-GnubbyDevice
.OK
, f
);
629 function timeoutLoop() {
634 callback(-GnubbyDevice
.TIMEOUT
);
638 --trycount
; // Try another one.
640 self
.notifyFrame_(checkSentinel
);
641 tid
= window
.setTimeout(timeoutLoop
, 500);
647 var completionAction
;
650 sendSentinel
= sendInitSentinel
;
651 nonce
= UTIL_getRandom(8);
652 sentinelEquals
= initSentinelEquals
;
653 completionAction
= initCompletionAction
;
657 sendSentinel
= sendSyncSentinel
;
658 sentinelEquals
= syncSentinelEquals
;
659 completionAction
= syncCompletionAction
;
662 if (Gnubby
.gnubbies_
.isSharedAccess(this.which
)) {
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;
680 * @param {number|ArrayBuffer|Uint8Array} data Command data or number
681 * of seconds to blink
682 * @param {?function(...)} cb Callback
684 Gnubby
.prototype.blink = function(data
, cb
) {
685 if (!cb
) cb
= Gnubby
.defaultCallback
;
686 if (typeof data
== 'number') {
687 var d
= new Uint8Array([data
]);
690 this.exchange_(GnubbyDevice
.CMD_PROMPT
, data
, Gnubby
.NORMAL_TIMEOUT
, cb
);
694 * @param {number|ArrayBuffer|Uint8Array} data Command data
695 * @param {?function(...)} cb Callback
697 Gnubby
.prototype.lock = function(data
, cb
) {
698 if (!cb
) cb
= Gnubby
.defaultCallback
;
699 if (typeof data
== 'number') {
700 var d
= new Uint8Array([data
]);
703 this.exchange_(GnubbyDevice
.CMD_LOCK
, data
, Gnubby
.NORMAL_TIMEOUT
, cb
);
706 /** Unlock the gnubby
707 * @param {?function(...)} cb Callback
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
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
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
738 Gnubby
.prototype.dfu = function(data
, cb
) {
739 if (!cb
) cb
= Gnubby
.defaultCallback
;
740 this.exchange_(GnubbyDevice
.CMD_DFU
, data
, Gnubby
.NORMAL_TIMEOUT
, cb
);
744 * @param {number|ArrayBuffer|Uint8Array} data Command data
745 * @param {?function(...)} cb Callback
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
);
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
761 Gnubby
.prototype.apdu = function(data
, cb
) {
762 if (!cb
) cb
= Gnubby
.defaultCallback
;
763 this.exchange_(GnubbyDevice
.CMD_APDU
, data
, Gnubby
.MAX_TIMEOUT
, cb
);
767 * @param {?function(...)} cb Callback
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
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
794 Gnubby
.prototype.apduReply = function(request
, cb
, opt_nowink
) {
795 if (!cb
) cb
= Gnubby
.defaultCallback
;
798 this.apdu(request
, function(rc
, data
) {
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
);
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
); });
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)));