KASM-7297 Smartcard Passthrough (#157)
* KASM-7297 Smartcard relay * KASM-7297 Update reference to production extension * KASM-7297 Wrap SmartcardSession.transmit in beginTransaction/endTransaction blocks * KASM-7297 Update Kasm UI with reader/card status * KASM-7297 Properly clear context on SmartcardSession._releaseContext * KASM-7297 Workaround for incorrect list_readers return value in smartcard native client * KASM-7287 Workaround for list_readers for ChromeOS handling * KASM-7287 Improvements to smartcard status refresh * KASM-7297 Remove unreachable code * KASM-7297 Updated fromHex function to work for empty values
This commit is contained in:
parent
6a8e7349b1
commit
77f29babbc
|
|
@ -0,0 +1,318 @@
|
||||||
|
import * as Log from "../../core/util/logging.js";
|
||||||
|
import * as WebUtil from "../../app/webutil.js";
|
||||||
|
|
||||||
|
// utilities
|
||||||
|
const toHex = (data) => {
|
||||||
|
return Array.from(data)
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
|
||||||
|
.join("")
|
||||||
|
.replace(/ /g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const fromHex = (data = "") => {
|
||||||
|
if (data.length === 0) return new Uint8Array(0);
|
||||||
|
if (!/^[0-9a-fA-F]*$/.test(data)) throw new Error(`invalid_hex_string: ${data}`);
|
||||||
|
return new Uint8Array(data.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// smartcard relay packets
|
||||||
|
const REQUEST_STATUS = 0x01;
|
||||||
|
const REQUEST_POWER_ON = 0x02;
|
||||||
|
const REQUEST_POWER_OFF = 0x03;
|
||||||
|
const REQUEST_RESET = 0x04;
|
||||||
|
const REQUEST_TRANSMIT = 0x05;
|
||||||
|
const REQUEST_INITIALIZE = 0x06;
|
||||||
|
const RESPONSE_ACK = 0x80;
|
||||||
|
const RESPONSE_ERROR = 0x81;
|
||||||
|
|
||||||
|
const commandToString = (command) => {
|
||||||
|
return {
|
||||||
|
[REQUEST_STATUS]: "REQUEST_STATUS",
|
||||||
|
[REQUEST_POWER_ON]: "REQUEST_POWER_ON",
|
||||||
|
[REQUEST_POWER_OFF]: "REQUEST_POWER_OFF",
|
||||||
|
[REQUEST_RESET]: "REQUEST_RESET",
|
||||||
|
[REQUEST_TRANSMIT]: "REQUEST_TRANSMIT",
|
||||||
|
[REQUEST_INITIALIZE]: "REQUEST_INITIALIZE",
|
||||||
|
[RESPONSE_ACK]: "RESPONSE_ACK",
|
||||||
|
[RESPONSE_ERROR]: "RESPONSE_ERROR",
|
||||||
|
}[command] || `0x${command.toString(16).toUpperCase()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRelayPacket = (command, payload = new Uint8Array(0)) => {
|
||||||
|
if (command !== RESPONSE_ACK && command !== RESPONSE_ERROR) {
|
||||||
|
throw new Error("invalid_relay_response");
|
||||||
|
}
|
||||||
|
|
||||||
|
const packet = new Uint8Array(3 + payload.length);
|
||||||
|
packet[0] = command;
|
||||||
|
packet[1] = (payload.length >> 8) & 0xff;
|
||||||
|
packet[2] = payload.length & 0xff;
|
||||||
|
packet.set(payload, 3);
|
||||||
|
return packet;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRelayPacket = (data) => {
|
||||||
|
if (data.length < 3) {
|
||||||
|
throw new Error("invalid_relay_packet");
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = data[0];
|
||||||
|
const payloadLength = (data[1] << 8) | data[2];
|
||||||
|
|
||||||
|
if (data.length < 3 + payloadLength) {
|
||||||
|
throw new Error("invalid_relay_packet");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
payload: data.slice(3, 3 + payloadLength),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const KASM_SMARTCARD_EXTENSION_ID = "cjkohjfgidilbllbjkdhpoeonjanpomo";
|
||||||
|
|
||||||
|
class SmartcardSession {
|
||||||
|
constructor() {
|
||||||
|
this.context = null;
|
||||||
|
this.cardAtr = null;
|
||||||
|
this.cardHandle = null;
|
||||||
|
this.activeProtocol = null;
|
||||||
|
this.lastTransmitAt = null;
|
||||||
|
this.lastRefreshAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
// skip check if we have refreshed recently
|
||||||
|
if (this.lastRefreshAt && Date.now() - this.lastRefreshAt < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip check if we have sent data recently
|
||||||
|
if (this.lastTransmitAt && Date.now() - this.lastTransmitAt < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// query state
|
||||||
|
let refreshContext = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refreshContext = await this._establishContext();
|
||||||
|
|
||||||
|
this.readers = await this._listReaders(refreshContext);
|
||||||
|
if (this.readers.length == 0) {
|
||||||
|
throw new Error("no_readers");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cardAtr = await this._getStatusChange(refreshContext, this.readers[0]).then(({ atr }) => atr);
|
||||||
|
} catch (error) {
|
||||||
|
this.context = null;
|
||||||
|
this.readers = [];
|
||||||
|
this.cardAtr = null;
|
||||||
|
this.cardHandle = null;
|
||||||
|
this.activeProtocol = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update web ui
|
||||||
|
const smartcardStatus = {
|
||||||
|
isExtensionEnabled: !!refreshContext,
|
||||||
|
isReaderConnected: this.readers.length > 0,
|
||||||
|
isCardPresent: !!this.cardAtr,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (WebUtil.isInsideKasmVDI()) {
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: "smartcard_status",
|
||||||
|
value: smartcardStatus,
|
||||||
|
}, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debug(`smartcard.refresh: ${JSON.stringify(smartcardStatus, null, 2)}`);
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
this.lastRefreshAt = Date.now();
|
||||||
|
|
||||||
|
if (refreshContext) {
|
||||||
|
await this._releaseContext(refreshContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async powerOn() {
|
||||||
|
this.context = this.context || (await this._establishContext());
|
||||||
|
|
||||||
|
if (!this.cardHandle || !this.activeProtocol) {
|
||||||
|
const { cardHandle, activeProtocol } = await this._connect(this.context, this.readers[0]);
|
||||||
|
this.cardHandle = cardHandle;
|
||||||
|
this.activeProtocol = activeProtocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async powerOff() {
|
||||||
|
if (this.context && this.cardHandle) {
|
||||||
|
await this._disconnect(this.context, this.cardHandle);
|
||||||
|
await this._releaseContext(this.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context = null;
|
||||||
|
this.cardHandle = null;
|
||||||
|
this.cardAtr = null;
|
||||||
|
this.activeProtocol = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async transmit(apdu) {
|
||||||
|
try {
|
||||||
|
await this._beginTransaction();
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.lastTransmitAt = Date.now();
|
||||||
|
return await this._transmit(apdu);
|
||||||
|
} catch (error) {
|
||||||
|
this.lastTransmitAt = null;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await this._endTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _establishContext() {
|
||||||
|
return await this._callExtension("establish_context", 0).then(([status, context]) => context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _releaseContext(context) {
|
||||||
|
return await this._callExtension("release_context", context).then(([status]) => status);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async _listReaders(context) {
|
||||||
|
return await this._callExtension("list_readers", context).then(([status, readers]) => {
|
||||||
|
return Array.isArray(readers) ? readers : readers.split(",").filter(Boolean);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getStatusChange(context, reader) {
|
||||||
|
return await this._callExtension("get_status_change", context, 0, 1, 0, 0, reader).then(
|
||||||
|
([status, readerCount, currentState, eventState, atr]) => ({
|
||||||
|
status,
|
||||||
|
readerCount,
|
||||||
|
currentState,
|
||||||
|
eventState,
|
||||||
|
atr,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _connect(context, reader) {
|
||||||
|
return await this._callExtension("connect", context, 2, 3, reader).then(
|
||||||
|
([status, cardContext, cardHandle, activeProtocol]) => ({
|
||||||
|
cardHandle,
|
||||||
|
activeProtocol,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _disconnect(context, cardHandle) {
|
||||||
|
return await this._callExtension("disconnect", context, cardHandle, 0).then(([status]) => status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _beginTransaction() {
|
||||||
|
return await this._callExtension("begin_transaction", this.context, this.cardHandle).then(([status]) => status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _transmit(apdu) {
|
||||||
|
return await this._callExtension("transmit", this.context, this.cardHandle, this.activeProtocol, toHex(apdu)).then(
|
||||||
|
([status, context, card, protocol, response]) => fromHex(response)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _endTransaction(disposition = 0) {
|
||||||
|
return await this._callExtension("end_transaction", this.context, this.cardHandle, disposition).then(
|
||||||
|
([status]) => status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _callExtension(name, ...args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const deviceId = "smartcard-relay";
|
||||||
|
const completionId = Date.now().toString() + Math.random().toString(36);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
deviceId,
|
||||||
|
completionId,
|
||||||
|
type: name,
|
||||||
|
args: args.join(","),
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResponse = (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === "error" || response.result[0] !== "0x00000000") {
|
||||||
|
reject(new Error(response.result[0] || "0x80100001"));
|
||||||
|
} else {
|
||||||
|
resolve(response.result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage(KASM_SMARTCARD_EXTENSION_ID, message, onResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (rfb) => {
|
||||||
|
Log.Debug("smartcard.initializeSmartcardRelay");
|
||||||
|
|
||||||
|
const sendSmartcardResponse = (command, payload = new Uint8Array(0)) => {
|
||||||
|
Log.Debug(`smartcard.response: command=${commandToString(command)}, payloadLen=${payload.length}`);
|
||||||
|
const packet = createRelayPacket(command, payload);
|
||||||
|
rfb.sendUnixRelayData("smartcard", packet);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientSession = new SmartcardSession();
|
||||||
|
await clientSession.refresh();
|
||||||
|
|
||||||
|
rfb.subscribeUnixRelay("smartcard", async (data) => {
|
||||||
|
try {
|
||||||
|
const { command, payload } = parseRelayPacket(data);
|
||||||
|
Log.Debug(`smartcard.request: command=${commandToString(command)}, payloadLen=${payload.length}`);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case REQUEST_INITIALIZE:
|
||||||
|
sendSmartcardResponse(RESPONSE_ACK);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REQUEST_STATUS:
|
||||||
|
await clientSession.refresh();
|
||||||
|
sendSmartcardResponse(RESPONSE_ACK, clientSession.cardAtr ? fromHex(clientSession.cardAtr) : new Uint8Array(0));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REQUEST_POWER_ON:
|
||||||
|
await clientSession.powerOn();
|
||||||
|
sendSmartcardResponse(RESPONSE_ACK);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REQUEST_POWER_OFF:
|
||||||
|
await clientSession.powerOff();
|
||||||
|
sendSmartcardResponse(RESPONSE_ACK);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REQUEST_RESET:
|
||||||
|
sendSmartcardResponse(RESPONSE_ACK);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case REQUEST_TRANSMIT:
|
||||||
|
await clientSession.powerOn();
|
||||||
|
const response = await clientSession.transmit(payload);
|
||||||
|
sendSmartcardResponse(RESPONSE_ACK, response);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown binary command: 0x${command.toString(16)}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Log.Error(`Failed to process command: ${error.message}`);
|
||||||
|
sendSmartcardResponse(RESPONSE_ERROR, new TextEncoder().encode(error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -21,6 +21,7 @@ import Inflator from "./inflator.js";
|
||||||
import Deflator from "./deflator.js";
|
import Deflator from "./deflator.js";
|
||||||
import Keyboard from "./input/keyboard.js";
|
import Keyboard from "./input/keyboard.js";
|
||||||
import initializePrinterRelay from "./output/printer.js";
|
import initializePrinterRelay from "./output/printer.js";
|
||||||
|
import initializeSmartcardRelay from "./output/smartcard.js";
|
||||||
import GestureHandler from "./input/gesturehandler.js";
|
import GestureHandler from "./input/gesturehandler.js";
|
||||||
import Cursor from "./util/cursor.js";
|
import Cursor from "./util/cursor.js";
|
||||||
import Websock from "./websock.js";
|
import Websock from "./websock.js";
|
||||||
|
|
@ -3102,6 +3103,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
//Register pipe based extensions
|
//Register pipe based extensions
|
||||||
initializePrinterRelay(this);
|
initializePrinterRelay(this);
|
||||||
|
initializeSmartcardRelay(this);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue