blob: d11df2239f7aa82de9207302811145df2dcb7768 [file] [log] [blame]
// 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);
});
};