| Index: chrome/browser/resources/cryptotoken/gnubby.js
|
| diff --git a/chrome/browser/resources/cryptotoken/gnubby.js b/chrome/browser/resources/cryptotoken/gnubby.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..1e9f45b5a92baf1481742ae4ff6c6ab54246a590
|
| --- /dev/null
|
| +++ b/chrome/browser/resources/cryptotoken/gnubby.js
|
| @@ -0,0 +1,627 @@
|
| +// Copyright (c) 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @fileoverview Low level usb cruft to talk gnubby.
|
| + * @author mschilder@google.com
|
| + */
|
| +
|
| +'use strict';
|
| +
|
| +// Global Gnubby instance counter.
|
| +var gnubby_id = 0;
|
| +
|
| +/**
|
| + * Creates a worker Gnubby instance.
|
| + * @constructor
|
| + * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result.
|
| + */
|
| +function usbGnubby(opt_busySeconds) {
|
| + this.dev = null;
|
| + this.cid = (++gnubby_id) & 0x00ffffff; // Pick unique channel.
|
| + this.rxframes = [];
|
| + this.synccnt = 0;
|
| + this.rxcb = null;
|
| + this.closed = false;
|
| + this.commandPending = false;
|
| + this.notifyOnClose = [];
|
| + this.busyMillis = (opt_busySeconds ? opt_busySeconds * 1000 : 2500);
|
| +}
|
| +
|
| +/**
|
| + * Sets usbGnubby's Gnubbies singleton.
|
| + * @param {Gnubbies} gnubbies
|
| + */
|
| +usbGnubby.setGnubbies = function(gnubbies) {
|
| + /** @private {Gnubbies} */
|
| + usbGnubby.gnubbies_ = gnubbies;
|
| +};
|
| +
|
| +/**
|
| + * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the
|
| + * result of enumerating.
|
| + */
|
| +usbGnubby.prototype.enumerate = function(cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + if (this.closed) {
|
| + cb(-llGnubby.GONE);
|
| + return;
|
| + }
|
| + if (!usbGnubby.gnubbies_) {
|
| + cb(-llGnubby.NODEVICE);
|
| + return;
|
| + }
|
| +
|
| + usbGnubby.gnubbies_.enumerate(cb);
|
| +};
|
| +
|
| +/**
|
| + * Opens the gnubby with the given index, or the first found gnubby if no
|
| + * index is specified.
|
| + * @param {llGnubbyDeviceId|undefined} opt_which The device to open.
|
| + * @param {function(number)|undefined} opt_cb Called with result of opening the
|
| + * gnubby.
|
| + */
|
| +usbGnubby.prototype.open = function(opt_which, opt_cb) {
|
| + var cb = opt_cb ? opt_cb : usbGnubby.defaultCallback;
|
| + if (this.closed) {
|
| + cb(-llGnubby.GONE);
|
| + return;
|
| + }
|
| + this.closingWhenIdle = false;
|
| +
|
| + if (document.location.href.indexOf('_generated_') == -1) {
|
| + // Not background page.
|
| + // Pick more random cid to tell things apart on the usb bus.
|
| + var rnd = UTIL_getRandom(2);
|
| + this.cid ^= (rnd[0] << 16) | (rnd[1] << 8);
|
| + }
|
| +
|
| + var self = this;
|
| + function addSelfAsClient(which) {
|
| + self.cid &= 0x00ffffff;
|
| + self.cid |= ((which.device + 1) << 24); // For debugging.
|
| +
|
| + usbGnubby.gnubbies_.addClient(which, self, function(rc, device) {
|
| + self.dev = device;
|
| + cb(rc);
|
| + });
|
| + }
|
| +
|
| + if (!usbGnubby.gnubbies_) {
|
| + cb(-llGnubby.NODEVICE);
|
| + return;
|
| + }
|
| + if (opt_which) {
|
| + addSelfAsClient(opt_which);
|
| + } else {
|
| + usbGnubby.gnubbies_.enumerate(function(rc, devs) {
|
| + if (rc || !devs.length) {
|
| + cb(-llGnubby.NODEVICE);
|
| + return;
|
| + }
|
| + addSelfAsClient(devs[0]);
|
| + });
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * @return {boolean} Whether this gnubby has any command outstanding.
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.inUse_ = function() {
|
| + return this.commandPending;
|
| +};
|
| +
|
| +/** Closes this gnubby. */
|
| +usbGnubby.prototype.close = function() {
|
| + this.closed = true;
|
| +
|
| + if (this.dev) {
|
| + console.log(UTIL_fmt('usbGnubby.close()'));
|
| + this.rxframes = [];
|
| + this.rxcb = null;
|
| + var dev = this.dev;
|
| + this.dev = null;
|
| + var self = this;
|
| + // Wait a bit in case simpleton client tries open next gnubby.
|
| + // Without delay, gnubbies would drop all idle devices, before client
|
| + // gets to the next one.
|
| + window.setTimeout(
|
| + function() {
|
| + usbGnubby.gnubbies_.removeClient(dev, self);
|
| + }, 300);
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Asks this gnubby to close when it gets a chance.
|
| + * @param {Function=} cb called back when closed.
|
| + */
|
| +usbGnubby.prototype.closeWhenIdle = function(cb) {
|
| + if (!this.inUse_()) {
|
| + this.close();
|
| + if (cb) cb();
|
| + return;
|
| + }
|
| + this.closingWhenIdle = true;
|
| + if (cb) this.notifyOnClose.push(cb);
|
| +};
|
| +
|
| +/**
|
| + * Close and notify every caller that it is now closed.
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.idleClose_ = function() {
|
| + this.close();
|
| + while (this.notifyOnClose.length != 0) {
|
| + var cb = this.notifyOnClose.shift();
|
| + cb();
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Notify callback for every frame received.
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.notifyFrame_ = function(cb) {
|
| + if (this.rxframes.length != 0) {
|
| + // Already have frames; continue.
|
| + if (cb) window.setTimeout(cb, 0);
|
| + } else {
|
| + this.rxcb = cb;
|
| + }
|
| +};
|
| +
|
| +/**
|
| + * Called by low level driver with a frame.
|
| + * @param {ArrayBuffer} frame
|
| + * @return {boolean} Whether this client is still interested in receiving
|
| + * frames from its device.
|
| + */
|
| +usbGnubby.prototype.receivedFrame = function(frame) {
|
| + if (this.closed) return false; // No longer interested.
|
| +
|
| + if (!this.checkCID_(frame)) {
|
| + // Not for me, ignore.
|
| + return true;
|
| + }
|
| +
|
| + this.rxframes.push(frame);
|
| +
|
| + // Callback self in case we were waiting. Once.
|
| + var cb = this.rxcb;
|
| + this.rxcb = null;
|
| + if (cb) window.setTimeout(cb, 0);
|
| +
|
| + return true;
|
| +};
|
| +
|
| +/**
|
| + * @return {ArrayBuffer} oldest received frame. Throw if none.
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.readFrame_ = function() {
|
| + if (this.rxframes.length == 0) throw 'rxframes empty!';
|
| +
|
| + var frame = this.rxframes.shift();
|
| + return frame;
|
| +};
|
| +
|
| +// Poll from rxframes[].
|
| +// timeout in seconds.
|
| +usbGnubby.prototype.read_ = function(cmd, timeout, cb) {
|
| + if (this.closed) { cb(-llGnubby.GONE); return; }
|
| + if (!this.dev) { cb(-llGnubby.NODEVICE); return; }
|
| +
|
| + var tid = null; // timeout timer id.
|
| + var callback = cb;
|
| + var self = this;
|
| +
|
| + var msg = null;
|
| + var seqno = 0;
|
| + var count = 0;
|
| +
|
| + /**
|
| + * Schedule call to cb if not called yet.
|
| + * @param {number} a Return code.
|
| + * @param {Object=} b Optional data.
|
| + */
|
| + function schedule_cb(a, b) {
|
| + self.commandPending = false;
|
| + if (tid) {
|
| + // Cancel timeout timer.
|
| + window.clearTimeout(tid);
|
| + tid = null;
|
| + }
|
| + var c = callback;
|
| + if (c) {
|
| + callback = null;
|
| + window.setTimeout(function() { c(a, b); }, 0);
|
| + }
|
| + if (self.closingWhenIdle) self.idleClose_();
|
| + };
|
| +
|
| + function read_timeout() {
|
| + if (!callback || !tid) return; // Already done.
|
| +
|
| + console.error(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] timeout!'));
|
| +
|
| + if (self.dev) {
|
| + self.dev.destroy(); // Stop pretending this thing works.
|
| + }
|
| +
|
| + tid = null;
|
| +
|
| + schedule_cb(-llGnubby.TIMEOUT);
|
| + };
|
| +
|
| + function cont_frame() {
|
| + if (!callback || !tid) return; // Already done.
|
| +
|
| + var f = new Uint8Array(self.readFrame_());
|
| + var rcmd = f[4];
|
| + var total_len = (f[5] << 8) + f[6];
|
| +
|
| + if (rcmd == llGnubby.CMD_ERROR && total_len == 1) {
|
| + // Error from device; forward.
|
| + console.log(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] error frame ' +
|
| + UTIL_BytesToHex(f)));
|
| + if (f[7] == llGnubby.GONE) {
|
| + self.closed = true;
|
| + }
|
| + schedule_cb(-f[7]);
|
| + return;
|
| + }
|
| +
|
| + if ((rcmd & 0x80)) {
|
| + // Not an CONT frame, ignore.
|
| + console.log(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] ignoring non-cont frame ' +
|
| + UTIL_BytesToHex(f)));
|
| + self.notifyFrame_(cont_frame);
|
| + return;
|
| + }
|
| +
|
| + var seq = (rcmd & 0x7f);
|
| + if (seq != seqno++) {
|
| + console.log(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] bad cont frame ' +
|
| + UTIL_BytesToHex(f)));
|
| + schedule_cb(-llGnubby.INVALID_SEQ);
|
| + return;
|
| + }
|
| +
|
| + // Copy payload.
|
| + for (var i = 5; i < f.length && count < msg.length; ++i) {
|
| + msg[count++] = f[i];
|
| + }
|
| +
|
| + if (count == msg.length) {
|
| + // Done.
|
| + schedule_cb(-llGnubby.OK, msg.buffer);
|
| + } else {
|
| + // Need more CONT frame(s).
|
| + self.notifyFrame_(cont_frame);
|
| + }
|
| + }
|
| +
|
| + function init_frame() {
|
| + if (!callback || !tid) return; // Already done.
|
| +
|
| + var f = new Uint8Array(self.readFrame_());
|
| +
|
| + var rcmd = f[4];
|
| + var total_len = (f[5] << 8) + f[6];
|
| +
|
| + if (rcmd == llGnubby.CMD_ERROR && total_len == 1) {
|
| + // Error from device; forward.
|
| + // Don't log busy frames, they're "normal".
|
| + if (f[7] != llGnubby.BUSY) {
|
| + console.log(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] error frame ' +
|
| + UTIL_BytesToHex(f)));
|
| + }
|
| + if (f[7] == llGnubby.GONE) {
|
| + self.closed = true;
|
| + }
|
| + schedule_cb(-f[7]);
|
| + return;
|
| + }
|
| +
|
| + if (!(rcmd & 0x80)) {
|
| + // Not an init frame, ignore.
|
| + console.log(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] ignoring non-init frame ' +
|
| + UTIL_BytesToHex(f)));
|
| + self.notifyFrame_(init_frame);
|
| + return;
|
| + }
|
| +
|
| + if (rcmd != cmd) {
|
| + // Not expected ack, read more.
|
| + console.log(UTIL_fmt(
|
| + '[' + self.cid.toString(16) + '] ignoring non-ack frame ' +
|
| + UTIL_BytesToHex(f)));
|
| + self.notifyFrame_(init_frame);
|
| + return;
|
| + }
|
| +
|
| + // Copy payload.
|
| + msg = new Uint8Array(total_len);
|
| + for (var i = 7; i < f.length && count < msg.length; ++i) {
|
| + msg[count++] = f[i];
|
| + }
|
| +
|
| + if (count == msg.length) {
|
| + // Done.
|
| + schedule_cb(-llGnubby.OK, msg.buffer);
|
| + } else {
|
| + // Need more CONT frame(s).
|
| + self.notifyFrame_(cont_frame);
|
| + }
|
| + }
|
| +
|
| + // Start timeout timer.
|
| + tid = window.setTimeout(read_timeout, 1000.0 * timeout);
|
| +
|
| + // Schedule read of first frame.
|
| + self.notifyFrame_(init_frame);
|
| +};
|
| +
|
| +/**
|
| + * @param {ArrayBuffer} frame
|
| + * @return {boolean} Whether frame is for my channel.
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.checkCID_ = function(frame) {
|
| + var f = new Uint8Array(frame);
|
| + var c = (f[0] << 24) |
|
| + (f[1] << 16) |
|
| + (f[2] << 8) |
|
| + (f[3]);
|
| + return c === this.cid ||
|
| + c === 0; // Generic notification.
|
| +};
|
| +
|
| +/**
|
| + * Queue command for sending.
|
| + * @param {number} cmd The command to send.
|
| + * @param {ArrayBuffer} data
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.write_ = function(cmd, data) {
|
| + if (this.closed) return;
|
| + if (!this.dev) return;
|
| +
|
| + this.commandPending = true;
|
| +
|
| + this.dev.queueCommand(this.cid, cmd, data);
|
| +};
|
| +
|
| +/**
|
| + * Writes the command, and calls back when the command's reply is received.
|
| + * @param {number} cmd The command to send.
|
| + * @param {ArrayBuffer} data
|
| + * @param {number} timeout Timeout in seconds.
|
| + * @param {function(number, ArrayBuffer=)} cb
|
| + * @private
|
| + */
|
| +usbGnubby.prototype.exchange_ = function(cmd, data, timeout, cb) {
|
| + var busyWait = new CountdownTimer(this.busyMillis);
|
| + var self = this;
|
| +
|
| + function retryBusy(rc, rc_data) {
|
| + if (rc == -llGnubby.BUSY && !busyWait.expired()) {
|
| + if (usbGnubby.gnubbies_) {
|
| + usbGnubby.gnubbies_.resetInactivityTimer(timeout * 1000);
|
| + }
|
| + self.write_(cmd, data);
|
| + self.read_(cmd, timeout, retryBusy);
|
| + } else {
|
| + busyWait.clearTimeout();
|
| + cb(rc, rc_data);
|
| + }
|
| + }
|
| +
|
| + retryBusy(-llGnubby.BUSY, undefined); // Start work.
|
| +};
|
| +
|
| +// For console interaction.
|
| +usbGnubby.defaultCallback = function(rc, data) {
|
| + var msg = 'defaultCallback(' + rc;
|
| + if (data) {
|
| + if (typeof data == 'string') msg += ', ' + data;
|
| + else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data));
|
| + }
|
| + msg += ')';
|
| + console.log(UTIL_fmt(msg));
|
| +};
|
| +
|
| +// Send nonce to device, flush read queue until match.
|
| +usbGnubby.prototype.sync = function(cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + if (this.closed) {
|
| + cb(-llGnubby.GONE);
|
| + return;
|
| + }
|
| +
|
| + var done = false;
|
| + var trycount = 6;
|
| + var tid = null;
|
| + var self = this;
|
| +
|
| + function callback(rc) {
|
| + done = true;
|
| + self.commandPending = false;
|
| + if (tid) {
|
| + window.clearTimeout(tid);
|
| + tid = null;
|
| + }
|
| + if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
|
| + if (cb) cb(rc);
|
| + if (self.closingWhenIdle) self.idleClose_();
|
| + }
|
| +
|
| + function sendSentinel() {
|
| + var data = new Uint8Array(1);
|
| + data[0] = ++self.synccnt;
|
| + self.write_(llGnubby.CMD_SYNC, data.buffer);
|
| + }
|
| +
|
| + function checkSentinel() {
|
| + var f = new Uint8Array(self.readFrame_());
|
| +
|
| + // Device disappeared on us.
|
| + if (f[4] == llGnubby.CMD_ERROR &&
|
| + f[5] == 0 && f[6] == 1 &&
|
| + f[7] == llGnubby.GONE) {
|
| + self.closed = true;
|
| + callback(-llGnubby.GONE);
|
| + return;
|
| + }
|
| +
|
| + // Eat everything else but expected sync reply.
|
| + if (f[4] != llGnubby.CMD_SYNC ||
|
| + (f.length > 7 && /* fw pre-0.2.1 bug: does not echo sentinel */
|
| + f[7] != self.synccnt)) {
|
| + // Read more.
|
| + self.notifyFrame_(checkSentinel);
|
| + return;
|
| + }
|
| +
|
| + // Done.
|
| + callback(-llGnubby.OK);
|
| + };
|
| +
|
| + function timeoutLoop() {
|
| + if (done) return;
|
| +
|
| + if (trycount == 0) {
|
| + // Failed.
|
| + callback(-llGnubby.TIMEOUT);
|
| + return;
|
| + }
|
| +
|
| + --trycount; // Try another one.
|
| + sendSentinel();
|
| + self.notifyFrame_(checkSentinel);
|
| + tid = window.setTimeout(timeoutLoop, 500);
|
| + };
|
| +
|
| + timeoutLoop();
|
| +};
|
| +
|
| +// Communication timeout values in seconds.
|
| +usbGnubby.SHORT_TIMEOUT = 1;
|
| +usbGnubby.NORMAL_TIMEOUT = 3;
|
| +// Max timeout usb firmware has for smartcard response is 30 seconds.
|
| +// Make our application level tolerance a little longer.
|
| +usbGnubby.MAX_TIMEOUT = 31;
|
| +
|
| +usbGnubby.prototype.blink = function(data, cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + if (typeof data == 'number') {
|
| + var d = new Uint8Array([data]);
|
| + data = d.buffer;
|
| + }
|
| + this.exchange_(llGnubby.CMD_PROMPT, data, usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.lock = function(data, cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + if (typeof data == 'number') {
|
| + var d = new Uint8Array([data]);
|
| + data = d.buffer;
|
| + }
|
| + this.exchange_(llGnubby.CMD_LOCK, data, usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.unlock = function(cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + var data = new Uint8Array([0]);
|
| + this.exchange_(llGnubby.CMD_LOCK, data.buffer,
|
| + usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.sysinfo = function(cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + this.exchange_(llGnubby.CMD_SYSINFO, new ArrayBuffer(0),
|
| + usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.wink = function(cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + this.exchange_(llGnubby.CMD_WINK, new ArrayBuffer(0),
|
| + usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.dfu = function(data, cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.ping = function(data, cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + if (typeof data == 'number') {
|
| + var d = new Uint8Array(data);
|
| + window.crypto.getRandomValues(d);
|
| + data = d.buffer;
|
| + }
|
| + this.exchange_(llGnubby.CMD_PING, data, usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.apdu = function(data, cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.reset = function(cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + this.exchange_(llGnubby.CMD_ATR, new ArrayBuffer(0),
|
| + usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +// byte args[3] = [delay-in-ms before disabling interrupts,
|
| +// delay-in-ms before disabling usb (aka remove),
|
| +// delay-in-ms before reboot (aka insert)]
|
| +usbGnubby.prototype.usb_test = function(args, cb) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + var u8 = new Uint8Array(args);
|
| + this.exchange_(llGnubby.CMD_USB_TEST, u8.buffer,
|
| + usbGnubby.NORMAL_TIMEOUT, cb);
|
| +};
|
| +
|
| +usbGnubby.prototype.apduReply_ = function(request, cb, opt_nowink) {
|
| + if (!cb) cb = usbGnubby.defaultCallback;
|
| + var self = this;
|
| +
|
| + this.apdu(request, function(rc, data) {
|
| + if (rc == 0) {
|
| + var r8 = new Uint8Array(data);
|
| + if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) {
|
| + // strip trailing 9000
|
| + var buf = new Uint8Array(r8.subarray(0, r8.length - 2));
|
| + cb(-llGnubby.OK, buf.buffer);
|
| + return;
|
| + } else {
|
| + // return non-9000 as rc
|
| + rc = r8[r8.length - 2] * 256 + r8[r8.length - 1];
|
| + // wink gnubby at hand if it needs touching.
|
| + if (rc == 0x6985 && !opt_nowink) {
|
| + self.wink(function() { cb(rc); });
|
| + return;
|
| + }
|
| + }
|
| + }
|
| + // Warn on errors other than waiting for touch, wrong data, and
|
| + // unrecognized command.
|
| + if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) {
|
| + console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16)));
|
| + }
|
| + cb(rc);
|
| + });
|
| +};
|
|
|