diff options
Diffstat (limited to 'chromium/chrome/browser/resources/cryptotoken/gnubby.js')
-rw-r--r-- | chromium/chrome/browser/resources/cryptotoken/gnubby.js | 694 |
1 files changed, 694 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/cryptotoken/gnubby.js b/chromium/chrome/browser/resources/cryptotoken/gnubby.js new file mode 100644 index 00000000000..dd013fb69e8 --- /dev/null +++ b/chromium/chrome/browser/resources/cryptotoken/gnubby.js @@ -0,0 +1,694 @@ +// Copyright 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. + */ + +'use strict'; + +// Global Gnubby instance counter. +var gnubbyId = 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 = (++gnubbyId) & 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 Gnubbies singleton instance + */ +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.NODEVICE); + 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} which The device to open. If null, the first + * gnubby found is opened. + * @param {function(number)|undefined} opt_cb Called with result of opening the + * gnubby. + */ +usbGnubby.prototype.open = function(which, opt_cb) { + var cb = opt_cb ? opt_cb : usbGnubby.defaultCallback; + if (this.closed) { + cb(-llGnubby.NODEVICE); + 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 setCid(which) { + self.cid &= 0x00ffffff; + self.cid |= ((which.device + 1) << 24); // For debugging. + } + + var enumerateRetriesRemaining = 3; + function enumerated(rc, devs) { + if (!devs.length) + rc = -llGnubby.NODEVICE; + if (rc) { + cb(rc); + return; + } + which = devs[0]; + setCid(which); + usbGnubby.gnubbies_.addClient(which, self, function(rc, device) { + if (rc == -llGnubby.NODEVICE && enumerateRetriesRemaining-- > 0) { + // We were trying to open the first device, but now it's not there? + // Do over. + usbGnubby.gnubbies_.enumerate(enumerated); + return; + } + self.dev = device; + cb(rc); + }); + } + + if (which) { + setCid(which); + usbGnubby.gnubbies_.addClient(which, self, function(rc, device) { + self.dev = device; + cb(rc); + }); + } else { + usbGnubby.gnubbies_.enumerate(enumerated); + } +}; + +/** + * @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. + * @param {function()} cb Callback + * @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|Uint8Array} frame Data 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|Uint8Array} 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[]. + * @param {number} cmd Command + * @param {number} timeout timeout in seconds. + * @param {?function(...)} cb Callback + * @private + */ +usbGnubby.prototype.read_ = function(cmd, timeout, cb) { + if (this.closed) { cb(-llGnubby.GONE); return; } + if (!this.dev) { cb(-llGnubby.GONE); 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 totalLen = (f[5] << 8) + f[6]; + + if (rcmd == llGnubby.CMD_ERROR && totalLen == 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 totalLen = (f[5] << 8) + f[6]; + + if (rcmd == llGnubby.CMD_ERROR && totalLen == 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(totalLen); + 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|Uint8Array} frame Data 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|Uint8Array} data Command 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|Uint8Array} data Command data + * @param {number} timeout Timeout in seconds. + * @param {function(number, ArrayBuffer=)} cb Callback + * @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. +}; + +/** Default callback for commands. Simply logs to console. + * @param {number} rc Result status code + * @param {(ArrayBuffer|Uint8Array|Array.<number>|null)} data Result data + */ +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. + * @param {?function(...)} cb Callback + */ +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(); +}; + +/** Short timeout value in seconds */ +usbGnubby.SHORT_TIMEOUT = 1; +/** Normal timeout value in seconds */ +usbGnubby.NORMAL_TIMEOUT = 3; +// Max timeout usb firmware has for smartcard response is 30 seconds. +// Make our application level tolerance a little longer. +/** Maximum timeout in seconds */ +usbGnubby.MAX_TIMEOUT = 31; + +/** Blink led + * @param {number|ArrayBuffer|Uint8Array} data Command data or number + * of seconds to blink + * @param {?function(...)} cb Callback + */ +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); +}; + +/** Lock the gnubby + * @param {number|ArrayBuffer|Uint8Array} data Command data + * @param {?function(...)} cb Callback + */ +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); +}; + +/** Unlock the gnubby + * @param {?function(...)} cb Callback + */ +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); +}; + +/** Request system information data. + * @param {?function(...)} cb Callback + */ +usbGnubby.prototype.sysinfo = function(cb) { + if (!cb) cb = usbGnubby.defaultCallback; + this.exchange_(llGnubby.CMD_SYSINFO, new ArrayBuffer(0), + usbGnubby.NORMAL_TIMEOUT, cb); +}; + +/** Send wink command + * @param {?function(...)} cb Callback + */ +usbGnubby.prototype.wink = function(cb) { + if (!cb) cb = usbGnubby.defaultCallback; + this.exchange_(llGnubby.CMD_WINK, new ArrayBuffer(0), + usbGnubby.NORMAL_TIMEOUT, cb); +}; + +/** Send DFU (Device firmware upgrade) command + * @param {ArrayBuffer|Uint8Array} data Command data + * @param {?function(...)} cb Callback + */ +usbGnubby.prototype.dfu = function(data, cb) { + if (!cb) cb = usbGnubby.defaultCallback; + this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb); +}; + +/** Ping the gnubby + * @param {number|ArrayBuffer|Uint8Array} data Command data + * @param {?function(...)} cb Callback + */ +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); +}; + +/** Send a raw APDU command + * @param {ArrayBuffer|Uint8Array} data Command data + * @param {?function(...)} cb Callback + */ +usbGnubby.prototype.apdu = function(data, cb) { + if (!cb) cb = usbGnubby.defaultCallback; + this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb); +}; + +/** Reset gnubby + * @param {?function(...)} cb Callback + */ +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)] +/** Send usb test command + * @param {ArrayBuffer|Uint8Array} args Command data + * @param {?function(...)} cb Callback + */ +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); +}; + +/** APDU command with reply + * @param {ArrayBuffer|Uint8Array} request The request + * @param {?function(...)} cb Callback + * @param {boolean=} opt_nowink Do not wink + * @private + */ +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); + }); +}; |