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 Keyboard from "./input/keyboard.js";
|
||||
import initializePrinterRelay from "./output/printer.js";
|
||||
import initializeSmartcardRelay from "./output/smartcard.js";
|
||||
import GestureHandler from "./input/gesturehandler.js";
|
||||
import Cursor from "./util/cursor.js";
|
||||
import Websock from "./websock.js";
|
||||
|
|
@ -3102,6 +3103,7 @@ export default class RFB extends EventTargetMixin {
|
|||
|
||||
//Register pipe based extensions
|
||||
initializePrinterRelay(this);
|
||||
initializeSmartcardRelay(this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue