@hazae41/ledger
Version:
Private and supply-chain hardened Ledger controller for TypeScript
103 lines (100 loc) • 3.75 kB
JavaScript
import { ApduRequest, ApduResponse } from '@hazae41/apdu';
import { Writable, Readable } from '@hazae41/binary';
import { Bytes } from '@hazae41/bytes';
import { HidFrame, HidContainer } from '../hid/index.mjs';
const VENDOR_ID = 0x2c97;
const PACKET_SIZE = 64;
class DeviceInterfaceNotFoundError extends Error {
#class = DeviceInterfaceNotFoundError;
name = this.#class.name;
constructor(options) {
super(`Could not find device interface`, options);
}
static from(cause) {
return new DeviceInterfaceNotFoundError({ cause });
}
}
class DeviceTransferOutError extends Error {
#class = DeviceTransferOutError;
name = this.#class.name;
constructor(options) {
super(`Could not transfer data to device`, options);
}
static from(cause) {
return new DeviceTransferOutError({ cause });
}
}
class DeviceTransferInError extends Error {
#class = DeviceTransferInError;
name = this.#class.name;
constructor(options) {
super(`Could not transfer data from device`, options);
}
static from(cause) {
return new DeviceTransferInError({ cause });
}
}
async function getDevicesOrThrow() {
const devices = await navigator.usb.getDevices();
return devices.filter(x => x.vendorId === VENDOR_ID);
}
async function getOrRequestDeviceOrThrow() {
const devices = await getDevicesOrThrow();
const device = devices[0];
if (device != null)
return device;
return await navigator.usb.requestDevice({ filters: [{ vendorId: VENDOR_ID }] });
}
async function connectOrThrow(device) {
await device.open();
if (device.configuration == null)
await device.selectConfiguration(1);
await device.reset();
const iface = device.configurations[0].interfaces.find(({ alternates }) => alternates.some(x => x.interfaceClass === 255));
if (iface == null)
throw new DeviceInterfaceNotFoundError();
await device.claimInterface(iface.interfaceNumber);
return new Connector(device, iface);
}
class Connector {
device;
iface;
#channel = Math.floor(Math.random() * 0xffff);
constructor(device, iface) {
this.device = device;
this.iface = iface;
}
async #transferOutOrThrow(frame) {
await this.device.transferOut(3, Writable.writeToBytesOrThrow(frame));
}
async #transferInOrThrow(length) {
const result = await this.device.transferIn(3, length);
if (result.data == null)
throw new DeviceTransferInError();
const bytes = Bytes.fromView(result.data);
const frame = Readable.readFromBytesOrThrow(HidFrame, bytes);
return frame;
}
async #sendOrThrow(fragment) {
const container = HidContainer.newOrThrow(fragment);
const bytes = Writable.writeToBytesOrThrow(container);
const frames = HidFrame.splitOrThrow(this.#channel, bytes);
let frame = frames.next();
for (; !frame.done; frame = frames.next())
await this.#transferOutOrThrow(frame.value);
return frame.value;
}
async *#receiveOrThrow() {
while (true)
yield await this.#transferInOrThrow(64);
}
async requestOrThrow(init) {
const request = ApduRequest.fromOrThrow(init);
await this.#sendOrThrow(request);
const bytes = await HidFrame.unsplitOrThrow(this.#channel, this.#receiveOrThrow());
const response = Readable.readFromBytesOrThrow(ApduResponse, bytes);
return response;
}
}
export { Connector, DeviceInterfaceNotFoundError, DeviceTransferInError, DeviceTransferOutError, PACKET_SIZE, VENDOR_ID, connectOrThrow, getDevicesOrThrow, getOrRequestDeviceOrThrow };
//# sourceMappingURL=index.mjs.map