@trezor/transport
Version:
Low level library facilitating protocol buffers based communication with Trezor devices
420 lines • 17.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsbApi = void 0;
const tslib_1 = require("tslib");
const utils_1 = require("@trezor/utils");
const abstract_1 = require("./abstract");
const constants_1 = require("../constants");
const ERRORS = tslib_1.__importStar(require("../errors"));
const types_1 = require("../types");
class UsbApi extends abstract_1.AbstractApi {
chunkSize = 64;
devices = [];
usbInterface;
forceReadSerialOnConnect;
abortController = new AbortController();
debugLink;
synchronizeCreateDevices = (0, utils_1.getSynchronize)();
synchronizeGetDevices = (0, utils_1.getSynchronize)();
synchronizeResetDevice = (0, utils_1.getSynchronize)();
deviceResetMap = {};
constructor({ usbInterface, logger, forceReadSerialOnConnect, debugLink }) {
super({ logger });
this.usbInterface = usbInterface;
this.forceReadSerialOnConnect = forceReadSerialOnConnect;
this.debugLink = debugLink;
}
listen() {
this.usbInterface.onconnect = async (event) => {
this.logger?.debug(`usb: onconnect: ${this.formatDeviceForLog(event.device)}`);
if (event.device.opened) {
this.logger?.debug('usb: onconnect: device already opened, closing');
await event.device.close();
}
return this.createDevices([event.device], this.abortController.signal)
.then(newDevices => {
this.devices = [...this.devices, ...newDevices];
this.emit('transport-interface-change', this.devicesToDescriptors());
})
.catch(err => {
this.logger?.error(`usb: createDevices error: ${err.message}`);
});
};
this.usbInterface.ondisconnect = event => {
const { device } = event;
if (!device.serialNumber) {
this.logger?.debug(`usb: ondisconnect: device without serial number:, ${device.productName}, ${device.manufacturerName}`);
return this.enumerate();
}
const index = this.devices.findIndex(d => d.path === device.serialNumber);
if (index > -1) {
this.devices.splice(index, 1);
this.emit('transport-interface-change', this.devicesToDescriptors());
}
else {
this.logger?.error('usb: device that should be removed does not exist in state');
}
};
}
formatDeviceForLog(device) {
return JSON.stringify({
productName: device.productName,
manufacturerName: device.manufacturerName,
serialNumber: device.serialNumber,
vendorId: device.vendorId,
productId: device.productId,
deviceVersionMajor: device.deviceVersionMajor,
deviceVersionMinor: device.deviceVersionMinor,
opened: device.opened,
});
}
matchDeviceType(device) {
const isBootloader = device.productId === constants_1.WEBUSB_BOOTLOADER_PRODUCT;
if (device.deviceVersionMajor === 2) {
if (isBootloader) {
return abstract_1.DEVICE_TYPE.TypeT2Boot;
}
else {
return abstract_1.DEVICE_TYPE.TypeT2;
}
}
else {
if (isBootloader) {
return abstract_1.DEVICE_TYPE.TypeT1WebusbBoot;
}
else if (device.vendorId === constants_1.T1_HID_VENDOR && device.productId === constants_1.T1_HID_PRODUCT) {
return abstract_1.DEVICE_TYPE.TypeT1Hid;
}
else {
return abstract_1.DEVICE_TYPE.TypeT1Webusb;
}
}
}
devicesToDescriptors() {
return this.devices.map(d => ({
path: (0, types_1.PathInternal)(d.path),
type: this.matchDeviceType(d.device),
product: d.device.productId,
vendor: d.device.vendorId,
}));
}
abortableMethod(method, { signal, onAbort }) {
if (!signal) {
return method();
}
if (signal.aborted) {
return Promise.reject(new Error(ERRORS.ABORTED_BY_SIGNAL));
}
const dfd = (0, utils_1.createDeferred)();
const abortListener = async () => {
this.logger?.debug('usb: abortableMethod onAbort start');
try {
await onAbort?.();
}
catch {
}
this.logger?.debug('usb: abortableMethod onAbort done');
dfd.reject(new Error(ERRORS.ABORTED_BY_SIGNAL));
};
signal?.addEventListener('abort', abortListener);
const methodPromise = method().catch(error => {
this.logger?.debug(`usb: abortableMethod method() aborted: ${signal.aborted} ${error}`);
if (signal.aborted) {
return dfd.promise;
}
dfd.reject(error);
throw error;
});
return Promise.race([methodPromise, dfd.promise])
.then(r => {
dfd.resolve(r);
return r;
})
.finally(() => {
signal?.removeEventListener('abort', abortListener);
});
}
async enumerate(signal) {
try {
this.logger?.debug('usb: enumerate');
const devices = await this.abortableMethod(() => this.synchronizeGetDevices(() => this.usbInterface.getDevices()), { signal });
this.devices = await this.createDevices(devices, signal);
return this.success(this.devicesToDescriptors());
}
catch (err) {
return this.unknownError(err);
}
}
async read(path, signal) {
const device = this.findDevice(path);
if (!device) {
return this.error({ error: ERRORS.DEVICE_NOT_FOUND });
}
try {
this.logger?.debug('usb: device.transferIn');
const res = await this.abortableMethod(() => device.transferIn(this.debugLink ? constants_1.DEBUGLINK_ENDPOINT_ID : constants_1.ENDPOINT_ID, this.chunkSize), { signal, onAbort: () => this.resetDevice(path) });
this.logger?.debug(`usb: device.transferIn done. status: ${res.status}, byteLength: ${res.data?.byteLength}.`);
if (!res.data?.byteLength) {
this.logger?.warn(`usb: device.transferIn error: empty data buffer`);
return this.success(Buffer.alloc(0));
}
return this.success(Buffer.from(res.data.buffer));
}
catch (err) {
this.logger?.error(`usb: device.transferIn error ${err}`);
return this.handleReadWriteError(err);
}
}
async write(path, buffer, signal) {
const device = this.findDevice(path);
if (!device) {
return this.error({ error: ERRORS.DEVICE_NOT_FOUND });
}
const newArray = new Uint8Array(this.chunkSize);
newArray.set(new Uint8Array(buffer));
const timeout = setTimeout(() => {
this.logger?.debug('usb: device.transfer out take suspiciously long. timing out.');
this.resetDevice(path).catch(() => { });
}, 1000);
try {
this.logger?.debug('usb: device.transferOut');
const result = await this.abortableMethod(() => device.transferOut(this.debugLink ? constants_1.DEBUGLINK_ENDPOINT_ID : constants_1.ENDPOINT_ID, newArray), { signal, onAbort: () => this.resetDevice(path) });
this.logger?.debug(`usb: device.transferOut done.`);
if (result.status !== 'ok') {
this.logger?.error(`usb: device.transferOut status not ok: ${result.status}`);
throw new Error('transfer out status not ok');
}
return this.success(undefined);
}
catch (err) {
return this.handleReadWriteError(err);
}
finally {
clearTimeout(timeout);
}
}
async openDevice(path, reset, signal) {
for (let i = 0; i < 5; i++) {
this.logger?.debug(`usb: openDevice attempt ${i}`);
const res = await this.openInternal(path, reset, signal);
if (res.success || signal?.aborted) {
return res;
}
await (0, utils_1.resolveAfter)(100 * i);
}
return this.openInternal(path, reset, signal);
}
async openInternal(path, reset, signal) {
const device = this.findDevice(path);
if (!device) {
return this.error({ error: ERRORS.DEVICE_NOT_FOUND });
}
try {
this.logger?.debug(`usb: device.open`);
await this.abortableMethod(() => device.open(), { signal });
this.logger?.debug(`usb: device.open done. device: ${this.formatDeviceForLog(device)}`);
}
catch (err) {
this.logger?.error(`usb: device.open error ${err}`);
if (err.message.includes('LIBUSB_ERROR_ACCESS')) {
return this.error({ error: ERRORS.LIBUSB_ERROR_ACCESS });
}
return this.error({
error: ERRORS.INTERFACE_UNABLE_TO_OPEN_DEVICE,
message: err.message,
});
}
if (device.configuration?.configurationValue !== constants_1.CONFIGURATION_ID) {
try {
this.logger?.debug(`usb: device.selectConfiguration ${constants_1.CONFIGURATION_ID}`);
await this.abortableMethod(() => device.selectConfiguration(constants_1.CONFIGURATION_ID), {
signal,
});
this.logger?.debug(`usb: device.selectConfiguration done: ${constants_1.CONFIGURATION_ID}.`);
}
catch (err) {
this.logger?.error(`usb: device.selectConfiguration error ${err}. device: ${this.formatDeviceForLog(device)}`);
}
}
if (reset) {
try {
this.logger?.debug('usb: device.reset');
await this.resetDevice(path);
this.logger?.debug(`usb: device.reset done.`);
}
catch (err) {
this.logger?.error(`usb: device.reset error ${err}. device: ${this.formatDeviceForLog(device)}`);
}
}
const interfaceId = this.debugLink ? constants_1.DEBUGLINK_INTERFACE_ID : constants_1.INTERFACE_ID;
if (!this.isInterfaceClaimed(device, interfaceId)) {
try {
this.logger?.debug(`usb: device.claimInterface: ${interfaceId}`);
await this.abortableMethod(() => device.claimInterface(interfaceId), { signal });
this.logger?.debug(`usb: device.claimInterface done: ${interfaceId}.`);
}
catch (err) {
this.logger?.error(`usb: device.claimInterface error ${err}.`);
return this.error({
error: ERRORS.INTERFACE_UNABLE_TO_OPEN_DEVICE,
message: err.message,
});
}
}
return this.success(undefined);
}
async closeDevice(path) {
let device = this.findDevice(path);
if (!device) {
return this.error({ error: ERRORS.DEVICE_NOT_FOUND });
}
this.logger?.debug(`usb: closeDevice. device.opened: ${device.opened}`);
if (device.opened) {
if (!this.debugLink) {
try {
await this.resetDevice(path);
}
catch (err) {
this.logger?.error(`usb: device.reset error ${err}. device: ${this.formatDeviceForLog(device)}`);
}
}
}
device = this.findDevice(path);
const interfaceId = this.debugLink ? constants_1.DEBUGLINK_INTERFACE_ID : constants_1.INTERFACE_ID;
if (device?.opened && this.isInterfaceClaimed(device, interfaceId)) {
try {
this.logger?.debug(`usb: device.releaseInterface: ${interfaceId}`);
await this.synchronizeResetDevice(() => device?.releaseInterface(interfaceId));
this.logger?.debug(`usb: device.releaseInterface done: ${interfaceId}.`);
}
catch (err) {
this.logger?.error(`usb: releaseInterface error ${err}.`);
}
}
device = this.findDevice(path);
if (device?.opened) {
try {
this.logger?.debug(`usb: device.close`);
await this.synchronizeResetDevice(() => device.close());
this.logger?.debug(`usb: device.close done.`);
}
catch (err) {
this.logger?.debug(`usb: device.close error ${err}.`);
return this.error({
error: ERRORS.INTERFACE_UNABLE_TO_CLOSE_DEVICE,
message: err.message,
});
}
}
return this.success(undefined);
}
findDevice(path) {
const device = this.devices.find(d => d.path === path);
if (!device) {
return;
}
return device.device;
}
createDevices(devices, signal) {
return this.synchronizeCreateDevices(async () => {
let bootloaderId = 0;
const getPathFromUsbDevice = (device) => {
const { serialNumber } = device;
let path = serialNumber == null || serialNumber === '' ? 'bootloader' : serialNumber;
if (path === 'bootloader') {
this.logger?.debug('usb: device without serial number!');
bootloaderId++;
path += bootloaderId;
}
return path;
};
const [hidDevices, nonHidDevices] = this.filterDevices(devices);
const loadedDevices = await Promise.all(nonHidDevices.map(async (device) => {
this.logger?.debug(`usb: creating device ${this.formatDeviceForLog(device)}`);
if (this.forceReadSerialOnConnect &&
!device.opened &&
!device.serialNumber) {
await this.loadSerialNumber(device, signal);
}
const path = getPathFromUsbDevice(device);
return { path, device };
}));
return [
...loadedDevices,
...hidDevices.map(d => ({
path: getPathFromUsbDevice(d),
device: d,
})),
];
});
}
async loadSerialNumber(device, signal) {
try {
this.logger?.debug(`usb: loadSerialNumber`);
await this.abortableMethod(() => device.open(), { signal });
await this.abortableMethod(() => device
.getStringDescriptor(device.device.deviceDescriptor.iSerialNumber), { signal });
this.logger?.debug(`usb: loadSerialNumber done, serialNumber: ${device.serialNumber}`);
await this.abortableMethod(() => device.close(), { signal });
}
catch (err) {
this.logger?.error(`usb: loadSerialNumber error: ${err.message}`);
throw err;
}
}
async resetDevice(path) {
const device = this.findDevice(path);
if (!device) {
this.logger?.debug(`usb: resetDevice: device not found`);
return;
}
if (this.deviceResetMap[path]) {
this.logger?.debug(`usb: resetDevice: device reset already running`);
return;
}
this.deviceResetMap[path] = true;
try {
this.logger?.debug(`usb: resetDevice: device.reset`);
await this.synchronizeResetDevice(() => device.reset());
this.logger?.debug(`usb: resetDevice: device.reset done`);
}
catch (err) {
this.logger?.error(`usb: resetDevice: device.reset error: ${err.message}`);
}
finally {
delete this.deviceResetMap[path];
}
}
filterDevices(devices) {
const trezorDevices = devices.filter(dev => constants_1.TREZOR_USB_DESCRIPTORS.some(desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId));
const [hidDevices, nonHidDevices] = (0, utils_1.arrayPartition)(trezorDevices, device => device.vendorId === constants_1.T1_HID_VENDOR);
return [hidDevices, nonHidDevices];
}
isInterfaceClaimed(device, interfaceId) {
return device.configuration?.interfaces.find(i => i.interfaceNumber === interfaceId)
?.claimed;
}
handleReadWriteError(err) {
if ([
'LIBUSB_TRANSFER_ERROR',
'LIBUSB_ERROR_PIPE',
'LIBUSB_ERROR_IO',
'LIBUSB_ERROR_NO_DEVICE',
'LIBUSB_ERROR_OTHER',
ERRORS.INTERFACE_DATA_TRANSFER,
'The device was disconnected.',
].some(disconnectedErr => err.message.includes(disconnectedErr))) {
return this.error({ error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION });
}
return this.unknownError(err);
}
dispose() {
if (this.usbInterface) {
this.usbInterface.onconnect = null;
this.usbInterface.ondisconnect = null;
}
this.abortController.abort();
}
}
exports.UsbApi = UsbApi;
//# sourceMappingURL=usb.js.map