UNPKG

@hazae41/ledger

Version:

Private and supply-chain hardened Ledger controller for TypeScript

103 lines (100 loc) 3.75 kB
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