| // 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 Provides a client view of a gnubby, aka USB security key. |
| */ |
| 'use strict'; |
| |
| /** |
| * Creates a Gnubby client. There may be more than one simultaneous Gnubby |
| * client of a physical device. This client manages multiplexing access to the |
| * low-level device to maintain the illusion that it is the only client of the |
| * device. |
| * @constructor |
| * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result. |
| */ |
| function Gnubby(opt_busySeconds) { |
| this.dev = null; |
| this.gnubbyInstance = ++Gnubby.gnubbyId_; |
| this.cid = Gnubby.BROADCAST_CID; |
| this.rxframes = []; |
| this.synccnt = 0; |
| this.rxcb = null; |
| this.closed = false; |
| this.commandPending = false; |
| this.notifyOnClose = []; |
| this.busyMillis = (opt_busySeconds ? opt_busySeconds * 1000 : 9500); |
| } |
| |
| /** |
| * Global Gnubby instance counter. |
| * @private {number} |
| */ |
| Gnubby.gnubbyId_ = 0; |
| |
| /** |
| * Sets Gnubby's Gnubbies singleton. |
| * @param {Gnubbies} gnubbies Gnubbies singleton instance |
| */ |
| Gnubby.setGnubbies = function(gnubbies) { |
| /** @private {Gnubbies} */ |
| Gnubby.gnubbies_ = gnubbies; |
| }; |
| |
| /** |
| * Return cid as hex string. |
| * @param {number} cid to convert. |
| * @return {string} hexadecimal string. |
| */ |
| Gnubby.hexCid = function(cid) { |
| var tmp = [ |
| (cid >>> 24) & 255, (cid >>> 16) & 255, (cid >>> 8) & 255, (cid >>> 0) & 255 |
| ]; |
| return UTIL_BytesToHex(tmp); |
| }; |
| |
| /** |
| * Cancels open attempt for this gnubby, if available. |
| */ |
| Gnubby.prototype.cancelOpen = function() { |
| if (this.which) |
| Gnubby.gnubbies_.cancelAddClient(this.which); |
| }; |
| |
| /** |
| * Opens the gnubby with the given index, or the first found gnubby if no |
| * index is specified. |
| * @param {?GnubbyDeviceId} which The device to open. If null, the first |
| * gnubby found is opened. |
| * @param {GnubbyEnumerationTypes=} opt_type Which type of device to enumerate. |
| * @param {function(number)=} opt_cb Called with result of opening the |
| * gnubby. |
| * @param {string=} opt_caller Identifier for the caller. |
| */ |
| Gnubby.prototype.open = function(which, opt_type, opt_cb, opt_caller) { |
| var cb = opt_cb ? opt_cb : Gnubby.defaultCallback; |
| if (this.closed) { |
| cb(-GnubbyDevice.NODEVICE); |
| return; |
| } |
| this.closingWhenIdle = false; |
| if (opt_caller) { |
| this.caller_ = opt_caller; |
| } |
| |
| var self = this; |
| |
| function setCid(which) { |
| // Set a default channel ID, in case the caller never sets a better one. |
| self.cid = Gnubby.defaultChannelId_(self.gnubbyInstance, which); |
| } |
| |
| var enumerateRetriesRemaining = 3; |
| function enumerated(rc, devs) { |
| if (!devs.length) |
| rc = -GnubbyDevice.NODEVICE; |
| if (rc) { |
| cb(rc); |
| return; |
| } |
| which = devs[0]; |
| setCid(which); |
| self.which = which; |
| Gnubby.gnubbies_.addClient(which, self, function(rc, device) { |
| if (rc == -GnubbyDevice.NODEVICE && enumerateRetriesRemaining-- > 0) { |
| // We were trying to open the first device, but now it's not there? |
| // Do over. |
| Gnubby.gnubbies_.enumerate(enumerated, opt_type); |
| return; |
| } |
| self.dev = device; |
| if (self.closeHook_) { |
| self.dev.setDestroyHook(self.closeHook_); |
| } |
| cb.call(self, rc); |
| }); |
| } |
| |
| if (which) { |
| setCid(which); |
| self.which = which; |
| Gnubby.gnubbies_.addClient( |
| /** @type {GnubbyDeviceId} */ (which), self, function(rc, device) { |
| if (!rc) { |
| self.dev = device; |
| if (self.closeHook_) { |
| self.dev.setDestroyHook(self.closeHook_); |
| } |
| } |
| cb.call(self, rc); |
| }); |
| } else { |
| Gnubby.gnubbies_.enumerate(enumerated, opt_type); |
| } |
| }; |
| |
| /** |
| * Generates a default channel id value for a gnubby instance that won't |
| * collide within this application, but may when others simultaneously access |
| * the device. |
| * @param {number} gnubbyInstance An instance identifier for a gnubby. |
| * @param {GnubbyDeviceId} which The device identifier for the gnubby device. |
| * @return {number} The channel id. |
| * @private |
| */ |
| Gnubby.defaultChannelId_ = function(gnubbyInstance, which) { |
| var cid = (gnubbyInstance) & 0x00ffffff; |
| cid |= ((which.device + 1) << 24); // For debugging. |
| return cid; |
| }; |
| |
| /** |
| * @return {boolean} Whether this gnubby has any command outstanding. |
| * @private |
| */ |
| Gnubby.prototype.inUse_ = function() { |
| return this.commandPending; |
| }; |
| |
| /** Closes this gnubby. */ |
| Gnubby.prototype.close = function() { |
| this.closed = true; |
| |
| if (this.dev) { |
| console.log(UTIL_fmt('Gnubby.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() { |
| Gnubby.gnubbies_.removeClient(dev, self); |
| }, 300); |
| } |
| }; |
| |
| /** |
| * Asks this gnubby to close when it gets a chance. |
| * @param {Function=} cb called back when closed. |
| */ |
| Gnubby.prototype.closeWhenIdle = function(cb) { |
| if (!this.inUse_()) { |
| this.close(); |
| if (cb) |
| cb(); |
| return; |
| } |
| this.closingWhenIdle = true; |
| if (cb) |
| this.notifyOnClose.push(cb); |
| }; |
| |
| /** |
| * Sets a callback that will get called when this gnubby is closed. |
| * @param {function() : ?Promise} cb Called back when closed. Callback |
| * may yield a promise that resolves when the close hook completes. |
| */ |
| Gnubby.prototype.setCloseHook = function(cb) { |
| this.closeHook_ = cb; |
| }; |
| |
| /** |
| * Close and notify every caller that it is now closed. |
| * @private |
| */ |
| Gnubby.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 |
| */ |
| Gnubby.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. |
| */ |
| Gnubby.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 {number|undefined} The last read error seen by this device. |
| */ |
| Gnubby.prototype.getLastReadError = function() { |
| return this.lastReadError_; |
| }; |
| |
| /** |
| * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none. |
| * @private |
| */ |
| Gnubby.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 |
| */ |
| Gnubby.prototype.read_ = function(cmd, timeout, cb) { |
| if (this.closed) { |
| cb(-GnubbyDevice.GONE); |
| return; |
| } |
| if (!this.dev) { |
| cb(-GnubbyDevice.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; |
| } |
| self.lastReadError_ = /** @private {number|undefined} */ (a); |
| 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('[' + Gnubby.hexCid(self.cid) + '] timeout!')); |
| |
| if (self.dev) { |
| self.dev.destroy(); // Stop pretending this thing works. |
| } |
| |
| tid = null; |
| |
| schedule_cb(-GnubbyDevice.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 == GnubbyDevice.CMD_ERROR && totalLen == 1) { |
| // Error from device; forward. |
| console.log(UTIL_fmt( |
| '[' + Gnubby.hexCid(self.cid) + '] error frame ' + |
| UTIL_BytesToHex(f))); |
| if (f[7] == GnubbyDevice.GONE) { |
| self.closed = true; |
| } |
| schedule_cb(-f[7]); |
| return; |
| } |
| |
| if ((rcmd & 0x80)) { |
| // Not an CONT frame, ignore. |
| console.log(UTIL_fmt( |
| '[' + Gnubby.hexCid(self.cid) + '] ignoring non-cont frame ' + |
| UTIL_BytesToHex(f))); |
| self.notifyFrame_(cont_frame); |
| return; |
| } |
| |
| var seq = (rcmd & 0x7f); |
| if (seq != seqno++) { |
| console.log(UTIL_fmt( |
| '[' + Gnubby.hexCid(self.cid) + '] bad cont frame ' + |
| UTIL_BytesToHex(f))); |
| schedule_cb(-GnubbyDevice.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(-GnubbyDevice.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 == GnubbyDevice.CMD_ERROR && totalLen == 1) { |
| // Error from device; forward. |
| // Don't log busy frames, they're "normal". |
| if (f[7] != GnubbyDevice.BUSY) { |
| console.log(UTIL_fmt( |
| '[' + Gnubby.hexCid(self.cid) + '] error frame ' + |
| UTIL_BytesToHex(f))); |
| } |
| if (f[7] == GnubbyDevice.GONE) { |
| self.closed = true; |
| } |
| schedule_cb(-f[7]); |
| return; |
| } |
| |
| if (!(rcmd & 0x80)) { |
| // Not an init frame, ignore. |
| console.log(UTIL_fmt( |
| '[' + Gnubby.hexCid(self.cid) + '] ignoring non-init frame ' + |
| UTIL_BytesToHex(f))); |
| self.notifyFrame_(init_frame); |
| return; |
| } |
| |
| if (rcmd != cmd) { |
| // Not expected ack, read more. |
| console.log(UTIL_fmt( |
| '[' + Gnubby.hexCid(self.cid) + '] 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(-GnubbyDevice.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); |
| }; |
| |
| /** |
| * @const |
| */ |
| Gnubby.NOTIFICATION_CID = 0; |
| |
| /** |
| * @const |
| */ |
| Gnubby.BROADCAST_CID = (0xff << 24) | (0xff << 16) | (0xff << 8) | 0xff; |
| |
| /** |
| * @param {ArrayBuffer|Uint8Array} frame Data frame |
| * @return {boolean} Whether frame is for my channel. |
| * @private |
| */ |
| Gnubby.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 === Gnubby.NOTIFICATION_CID; |
| }; |
| |
| /** |
| * Queue command for sending. |
| * @param {number} cmd The command to send. |
| * @param {ArrayBuffer|Uint8Array} data Command data |
| * @private |
| */ |
| Gnubby.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 |
| */ |
| Gnubby.prototype.exchange = function(cmd, data, timeout, cb) { |
| var busyWait = new CountdownTimer(Gnubby.SYS_TIMER_, this.busyMillis); |
| var self = this; |
| |
| function retryBusy(rc, rc_data) { |
| if (rc == -GnubbyDevice.BUSY && !busyWait.expired()) { |
| if (Gnubby.gnubbies_) { |
| Gnubby.gnubbies_.resetInactivityTimer(timeout * 1000); |
| } |
| self.write_(cmd, data); |
| self.read_(cmd, timeout, retryBusy); |
| } else { |
| busyWait.clearTimeout(); |
| cb(rc, rc_data); |
| } |
| } |
| |
| retryBusy(-GnubbyDevice.BUSY, undefined); // Start work. |
| }; |
| |
| /** |
| * Private instance of timers based on window's timer functions. |
| * @const |
| * @private |
| */ |
| Gnubby.SYS_TIMER_ = new WindowTimer(); |
| |
| /** Default callback for commands. Simply logs to console. |
| * @param {number} rc Result status code |
| * @param {(ArrayBuffer|Uint8Array|Array<number>|null)} data Result data |
| */ |
| Gnubby.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)); |
| }; |
| |
| /** |
| * Ensures this device has temporary ownership of the USB device, by: |
| * 1. Using the INIT command to allocate an unique channel id, if one hasn't |
| * been retrieved before, or |
| * 2. Sending a nonce to device, flushing read queue until match. |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.sync = function(cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| if (this.closed) { |
| cb(-GnubbyDevice.GONE); |
| return; |
| } |
| |
| var done = false; |
| var trycount = 6; |
| var tid = null; |
| var self = this; |
| |
| function returnValue(rc) { |
| done = true; |
| window.setTimeout(cb.bind(null, rc), 0); |
| if (self.closingWhenIdle) |
| self.idleClose_(); |
| } |
| |
| function callback(rc, opt_frame) { |
| self.commandPending = false; |
| if (tid) { |
| window.clearTimeout(tid); |
| tid = null; |
| } |
| completionAction(rc, opt_frame); |
| } |
| |
| function sendSyncSentinel() { |
| var cmd = GnubbyDevice.CMD_SYNC; |
| var data = new Uint8Array(1); |
| data[0] = ++self.synccnt; |
| self.dev.queueCommand(self.cid, cmd, data.buffer); |
| } |
| |
| function syncSentinelEquals(f) { |
| return ( |
| f[4] == GnubbyDevice.CMD_SYNC && |
| (f.length == 7 || /* fw pre-0.2.1 bug: does not echo sentinel */ |
| f[7] == self.synccnt)); |
| } |
| |
| function syncCompletionAction(rc, opt_frame) { |
| if (rc) |
| console.warn(UTIL_fmt('sync failed: ' + rc)); |
| returnValue(rc); |
| } |
| |
| function sendInitSentinel() { |
| var cid = self.cid; |
| // If we do not have a specific CID yet, reset to BROADCAST for init. |
| if (self.cid == Gnubby.defaultChannelId_(self.gnubbyInstance, self.which)) { |
| self.cid = Gnubby.BROADCAST_CID; |
| cid = self.cid; |
| } |
| var cmd = GnubbyDevice.CMD_INIT; |
| self.dev.queueCommand(cid, cmd, nonce); |
| } |
| |
| function initSentinelEquals(f) { |
| return ( |
| f[4] == GnubbyDevice.CMD_INIT && f.length >= nonce.length + 7 && |
| UTIL_equalArrays(f.subarray(7, nonce.length + 7), nonce)); |
| } |
| |
| function initCmdUnsupported(rc) { |
| // Different firmwares fail differently on different inputs, so treat any |
| // of the following errors as indicating the INIT command isn't supported. |
| return rc == -GnubbyDevice.INVALID_CMD || rc == -GnubbyDevice.INVALID_PAR || |
| rc == -GnubbyDevice.INVALID_LEN; |
| } |
| |
| function initCompletionAction(rc, opt_frame) { |
| // Actual failures: bail out. |
| if (rc && !initCmdUnsupported(rc)) { |
| console.warn(UTIL_fmt('init failed: ' + rc)); |
| returnValue(rc); |
| } |
| |
| var HEADER_LENGTH = 7; |
| var MIN_LENGTH = HEADER_LENGTH + 4; // 4 bytes for the channel id |
| if (rc || !opt_frame || opt_frame.length < nonce.length + MIN_LENGTH) { |
| // INIT command not supported or is missing the returned channel id: |
| // Pick a random cid to try to prevent collisions on the USB bus. |
| var rnd = UTIL_getRandom(2); |
| self.cid = Gnubby.defaultChannelId_(self.gnubbyInstance, self.which); |
| self.cid ^= (rnd[0] << 16) | (rnd[1] << 8); |
| // Now sync with that cid, to make sure we've got it. |
| setSync(); |
| timeoutLoop(); |
| return; |
| } |
| // Accept the provided cid. |
| var offs = HEADER_LENGTH + nonce.length; |
| self.cid = (opt_frame[offs] << 24) | (opt_frame[offs + 1] << 16) | |
| (opt_frame[offs + 2] << 8) | opt_frame[offs + 3]; |
| returnValue(rc); |
| } |
| |
| function checkSentinel() { |
| var f = new Uint8Array(self.readFrame_()); |
| |
| // Stop on errors and return them. |
| if (f[4] == GnubbyDevice.CMD_ERROR && f[5] == 0 && f[6] == 1) { |
| if (f[7] == GnubbyDevice.BUSY) { |
| // Not spec but some devices do this; retry. |
| sendSentinel(); |
| self.notifyFrame_(checkSentinel); |
| return; |
| } |
| if (f[7] == GnubbyDevice.GONE) { |
| // Device disappeared on us. |
| self.closed = true; |
| } |
| callback(-f[7]); |
| return; |
| } |
| |
| // Eat everything else but expected sentinel reply. |
| if (!sentinelEquals(f)) { |
| // Read more. |
| self.notifyFrame_(checkSentinel); |
| return; |
| } |
| |
| // Done. |
| callback(-GnubbyDevice.OK, f); |
| } |
| |
| function timeoutLoop() { |
| if (done) |
| return; |
| |
| if (trycount == 0) { |
| // Failed. |
| callback(-GnubbyDevice.TIMEOUT); |
| return; |
| } |
| |
| --trycount; // Try another one. |
| sendSentinel(); |
| self.notifyFrame_(checkSentinel); |
| tid = window.setTimeout(timeoutLoop, 500); |
| } |
| |
| var sendSentinel; |
| var sentinelEquals; |
| var nonce; |
| var completionAction; |
| |
| function setInit() { |
| sendSentinel = sendInitSentinel; |
| nonce = UTIL_getRandom(8); |
| sentinelEquals = initSentinelEquals; |
| completionAction = initCompletionAction; |
| } |
| |
| function setSync() { |
| sendSentinel = sendSyncSentinel; |
| sentinelEquals = syncSentinelEquals; |
| completionAction = syncCompletionAction; |
| } |
| |
| if (Gnubby.gnubbies_.isSharedAccess(this.which)) { |
| setInit(); |
| } else { |
| setSync(); |
| } |
| timeoutLoop(); |
| }; |
| |
| /** Short timeout value in seconds */ |
| Gnubby.SHORT_TIMEOUT = 1; |
| /** Normal timeout value in seconds */ |
| Gnubby.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 */ |
| Gnubby.MAX_TIMEOUT = 31; |
| |
| /** Blink led |
| * @param {number|ArrayBuffer|Uint8Array} data Command data or number |
| * of seconds to blink |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.blink = function(data, cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| if (typeof data == 'number') { |
| var d = new Uint8Array([data]); |
| data = d.buffer; |
| } |
| this.exchange(GnubbyDevice.CMD_PROMPT, data, Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Lock the gnubby |
| * @param {number|ArrayBuffer|Uint8Array} data Command data |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.lock = function(data, cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| if (typeof data == 'number') { |
| var d = new Uint8Array([data]); |
| data = d.buffer; |
| } |
| this.exchange(GnubbyDevice.CMD_LOCK, data, Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Unlock the gnubby |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.unlock = function(cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| var data = new Uint8Array([0]); |
| this.exchange(GnubbyDevice.CMD_LOCK, data.buffer, Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Request system information data. |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.sysinfo = function(cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| this.exchange( |
| GnubbyDevice.CMD_SYSINFO, new ArrayBuffer(0), Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Send wink command |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.wink = function(cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| this.exchange( |
| GnubbyDevice.CMD_WINK, new ArrayBuffer(0), Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Send DFU (Device firmware upgrade) command |
| * @param {ArrayBuffer|Uint8Array} data Command data |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.dfu = function(data, cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| this.exchange(GnubbyDevice.CMD_DFU, data, Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Ping the gnubby |
| * @param {number|ArrayBuffer|Uint8Array} data Command data |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.ping = function(data, cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| if (typeof data == 'number') { |
| var d = new Uint8Array(data); |
| window.crypto.getRandomValues(d); |
| data = d.buffer; |
| } |
| this.exchange(GnubbyDevice.CMD_PING, data, Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** Send a raw APDU command |
| * @param {ArrayBuffer|Uint8Array} data Command data |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.apdu = function(data, cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| this.exchange(GnubbyDevice.CMD_APDU, data, Gnubby.MAX_TIMEOUT, cb); |
| }; |
| |
| /** Reset gnubby |
| * @param {?function(...)} cb Callback |
| */ |
| Gnubby.prototype.reset = function(cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| this.exchange( |
| GnubbyDevice.CMD_ATR, new ArrayBuffer(0), Gnubby.MAX_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 |
| */ |
| Gnubby.prototype.usb_test = function(args, cb) { |
| if (!cb) |
| cb = Gnubby.defaultCallback; |
| var u8 = new Uint8Array(args); |
| this.exchange( |
| GnubbyDevice.CMD_USB_TEST, u8.buffer, Gnubby.NORMAL_TIMEOUT, cb); |
| }; |
| |
| /** APDU command with reply |
| * @param {ArrayBuffer|Uint8Array} request The request |
| * @param {?function(...)} cb Callback |
| * @param {boolean=} opt_nowink Do not wink |
| */ |
| Gnubby.prototype.apduReply = function(request, cb, opt_nowink) { |
| if (!cb) |
| cb = Gnubby.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(-GnubbyDevice.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); |
| }); |
| }; |