UNPKG

@trezor/transport

Version:

Low level library facilitating protocol buffers based communication with Trezor devices

445 lines (444 loc) 16 kB
"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 = {}; devicePendingTransferIn = new Map(); constructor({ usbInterface, logger, forceReadSerialOnConnect, debugLink }) { super({ logger, type: 'usb' }); 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 constants_1.DEVICE_TYPE.TypeT2Boot; } else { return constants_1.DEVICE_TYPE.TypeT2; } } else { if (isBootloader) { return constants_1.DEVICE_TYPE.TypeT1WebusbBoot; } else if (device.vendorId === constants_1.T1_HID_VENDOR && device.productId === constants_1.T1_HID_PRODUCT) { return constants_1.DEVICE_TYPE.TypeT1Hid; } else { return constants_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, id: d.device.serialNumber, apiType: this.type })); } 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); } } getTransferIn(device) { let pending = this.devicePendingTransferIn.get(device); if (!pending) { pending = device.transferIn(this.debugLink ? constants_1.DEBUGLINK_ENDPOINT_ID : constants_1.ENDPOINT_ID, this.chunkSize).finally(() => this.devicePendingTransferIn.delete(device)); this.devicePendingTransferIn.set(device, pending); } return pending; } 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(() => this.getTransferIn(device), { 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, options) { for (let i = 0; i < 5; i++) { this.logger?.debug(`usb: openDevice attempt ${i}`); const res = await this.openInternal(path, options); if (res.success || options.signal?.aborted) { return res; } await (0, utils_1.resolveAfter)(100 * i); } return this.openInternal(path, options); } 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, [ERRORS.DEVICE_NOT_FOUND, ERRORS.INTERFACE_UNABLE_TO_OPEN_DEVICE, ERRORS.DEVICE_DISCONNECTED_DURING_ACTION, ERRORS.ABORTED_BY_TIMEOUT, ERRORS.ABORTED_BY_SIGNAL, ERRORS.UNEXPECTED_ERROR]); } dispose() { if (this.usbInterface) { this.usbInterface.onconnect = null; this.usbInterface.ondisconnect = null; } this.abortController.abort(); } } exports.UsbApi = UsbApi; //# sourceMappingURL=usb.js.map