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(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.
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
475 Gnubby.defaultCallback = function(rc, data) {
476 var msg = 'defaultCallback(' + rc;
478 if (typeof data == 'string') msg += ', ' + data;
479 else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data));
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
492 Gnubby.prototype.sync = function(cb) {
493 if (!cb) cb = Gnubby.defaultCallback;
495 cb(-GnubbyDevice.GONE);
504 function returnValue(rc) {
506 window.setTimeout(cb.bind(null, rc), 0);
507 if (self.closingWhenIdle) self.idleClose_();
510 function callback(rc, opt_frame) {
511 self.commandPending = false;
513 window.clearTimeout(tid);
516 completionAction(rc, opt_frame);
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);
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));
532 function syncCompletionAction(rc, opt_frame) {
533 if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
537 function sendInitSentinel() {
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;
544 var cmd = GnubbyDevice.CMD_INIT;
545 self.dev.queueCommand(cid, cmd, nonce);
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));
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;
562 function initCompletionAction(rc, opt_frame) {
563 // Actual failures: bail out.
564 if (rc && !initCmdUnsupported(rc)) {
565 console.warn(UTIL_fmt('init failed: ' + rc));
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.
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) |
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.
600 self.notifyFrame_(checkSentinel);
603 if (f[7] == GnubbyDevice.GONE) {
604 // Device disappeared on us.
611 // Eat everything else but expected sentinel reply.
612 if (!sentinelEquals(f)) {
614 self.notifyFrame_(checkSentinel);
619 callback(-GnubbyDevice.OK, f);
622 function timeoutLoop() {
627 callback(-GnubbyDevice.TIMEOUT);
631 --trycount; // Try another one.
633 self.notifyFrame_(checkSentinel);
634 tid = window.setTimeout(timeoutLoop, 500);
640 var completionAction;
643 sendSentinel = sendInitSentinel;
644 nonce = UTIL_getRandom(8);
645 sentinelEquals = initSentinelEquals;
646 completionAction = initCompletionAction;
650 sendSentinel = sendSyncSentinel;
651 sentinelEquals = syncSentinelEquals;
652 completionAction = syncCompletionAction;
655 if (Gnubby.gnubbies_.isSharedAccess(this.which)) {
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;
673 * @param {number|ArrayBuffer|Uint8Array} data Command data or number
674 * of seconds to blink
675 * @param {?function(...)} cb Callback
677 Gnubby.prototype.blink = function(data, cb) {
678 if (!cb) cb = Gnubby.defaultCallback;
679 if (typeof data == 'number') {
680 var d = new Uint8Array([data]);
683 this.exchange_(GnubbyDevice.CMD_PROMPT, data, Gnubby.NORMAL_TIMEOUT, cb);
687 * @param {number|ArrayBuffer|Uint8Array} data Command data
688 * @param {?function(...)} cb Callback
690 Gnubby.prototype.lock = function(data, cb) {
691 if (!cb) cb = Gnubby.defaultCallback;
692 if (typeof data == 'number') {
693 var d = new Uint8Array([data]);
696 this.exchange_(GnubbyDevice.CMD_LOCK, data, Gnubby.NORMAL_TIMEOUT, cb);
699 /** Unlock the gnubby
700 * @param {?function(...)} cb Callback
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
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
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
731 Gnubby.prototype.dfu = function(data, cb) {
732 if (!cb) cb = Gnubby.defaultCallback;
733 this.exchange_(GnubbyDevice.CMD_DFU, data, Gnubby.NORMAL_TIMEOUT, cb);
737 * @param {number|ArrayBuffer|Uint8Array} data Command data
738 * @param {?function(...)} cb Callback
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);
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
754 Gnubby.prototype.apdu = function(data, cb) {
755 if (!cb) cb = Gnubby.defaultCallback;
756 this.exchange_(GnubbyDevice.CMD_APDU, data, Gnubby.MAX_TIMEOUT, cb);
760 * @param {?function(...)} cb Callback
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
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
787 Gnubby.prototype.apduReply = function(request, cb, opt_nowink) {
788 if (!cb) cb = Gnubby.defaultCallback;
791 this.apdu(request, function(rc, data) {
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);
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); });
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)));