UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

396 lines (395 loc) 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.onCallFirmwareUpdate = void 0; const utils_1 = require("@trezor/utils"); const versionUtils_1 = require("@trezor/utils/lib/versionUtils"); const firmware_1 = require("../api/firmware"); const constants_1 = require("../constants"); const firmwareInfo_1 = require("../data/firmwareInfo"); const events_1 = require("../events"); const types_1 = require("../types"); const firmwareUtils_1 = require("../utils/firmwareUtils"); const waitForReconnectedDevice = async ({ bootloader, method, intermediary }, { deviceList, device, registerEvents, postMessage, log, abortSignal, uiPromises }) => { const target = intermediary || !bootloader ? 'normal' : 'bootloader'; let i = 0; if (method !== 'auto') { log.debug('onCallFirmwareUpdate', 'waiting for device to disconnect'); postMessage((0, events_1.createUiMessage)(events_1.UI.FIRMWARE_RECONNECT, { device: device.toMessageObject(), disconnected: false, method, target, i })); await new Promise(resolve => { deviceList.once('device-disconnect', resolve); }); } log.debug('onCallFirmwareUpdate', `waiting for device to reconnect in ${bootloader ? 'bootloader' : 'normal'} mode`); let reconnectedDevice; let thpPairingError = false; do { postMessage((0, events_1.createUiMessage)(events_1.UI.FIRMWARE_RECONNECT, { device: device.toMessageObject(), disconnected: true, method, target, i })); await (0, utils_1.resolveAfter)(2000); try { reconnectedDevice = deviceList.getOnlyDevice(); } catch {} if (reconnectedDevice && !reconnectedDevice.features) { log.debug('onCallFirmwareUpdate', 'we were unable to read device.features on the first interaction after seeing it, retrying...'); let runFn; if (reconnectedDevice.getThpState()?.properties) { const uiPromise = uiPromises.create(events_1.UI.RECEIVE_CONFIRMATION, reconnectedDevice); postMessage((0, events_1.createUiMessage)(events_1.UI.REQUEST_CONFIRMATION, { view: thpPairingError ? 'thp-pairing-failed' : 'thp-pairing-start' })); const uiResp = await uiPromise.promise; if (!uiResp.payload) { throw constants_1.ERRORS.TypedError('Method_PermissionsNotGranted'); } runFn = () => Promise.resolve(); } try { registerEvents(reconnectedDevice); await reconnectedDevice.run(runFn, { skipFirmwareChecks: true, skipLanguageChecks: true }); } catch (error) { uiPromises.rejectAll(error); if (error.code === 'Device_ThpPairingTagInvalid') { thpPairingError = true; } } } i++; log.debug('onCallFirmwareUpdate', '...still waiting for device to reconnect', i); } while (!abortSignal.aborted && (!reconnectedDevice?.features || bootloader === !reconnectedDevice.features.bootloader_mode || intermediary && !(0, versionUtils_1.isNewer)([reconnectedDevice.features.major_version, reconnectedDevice.features.minor_version, reconnectedDevice.features.patch_version], [device.features.major_version, device.features.minor_version, device.features.patch_version]))); if (!reconnectedDevice) { throw constants_1.ERRORS.TypedError('Method_Interrupted'); } registerEvents(reconnectedDevice); await reconnectedDevice.currentRun; if (!reconnectedDevice.isUsedHere()) { await reconnectedDevice.acquire(); } return reconnectedDevice; }; const waitForBluetoothReboot = ({ device, target, postMessage }) => new Promise(resolve => { postMessage((0, events_1.createUiMessage)(events_1.UI.FIRMWARE_RECONNECT, { device: device.toMessageObject(), disconnected: false, method: 'auto', target, i: 0 })); const handler = () => { const deviceIsReady = target === 'bootloader' && device.features?.bootloader_mode || target === 'normal' && device.getThpState()?.properties; if (deviceIsReady) { device.lifecycle.off('device-changed', handler); resolve(); } }; device.lifecycle.on('device-changed', handler); }); const getInstallationParams = (device, params) => { const btcOnly = params.btcOnly ?? device.firmwareType === types_1.FirmwareType.BitcoinOnly; if (!device.features.bootloader_mode) { const version = params.binary ? (0, firmware_1.parseFirmwareHeaders)(Buffer.from(params.binary)).version : undefined; const isUpdatingToNewerVersion = !version ? device.firmwareReleaseConfigInfo?.isNewer : (0, versionUtils_1.isNewer)(version, [device.features.major_version, device.features.minor_version, device.features.patch_version]); const isUpdatingToEqualFirmwareType = device.firmwareType === types_1.FirmwareType.BitcoinOnly === btcOnly; const upgrade = device.atLeast('2.6.3') && isUpdatingToNewerVersion && isUpdatingToEqualFirmwareType; const manual = !device.atLeast(['1.10.0', '2.6.0']) && !upgrade; const getUpdateFlowType = () => { if (manual) return 'manual'; return upgrade ? 'reboot_and_upgrade' : 'reboot_and_wait'; }; return { manual, upgrade, updateFlowType: getUpdateFlowType(), btcOnly }; } else { return { manual: false, upgrade: false, updateFlowType: 'unknown_flow', btcOnly }; } }; const getFwHeader = binary => Buffer.from(binary.slice(0, 6000)).toString('hex'); const getBinaryHelper = async ({ device, params, firmwareType, isIntermediary, log }) => { if (params.binary) { return Promise.resolve({ binary: params.binary, binaryVersion: (0, firmware_1.parseFirmwareHeaders)(Buffer.from(params.binary)).version, release: undefined }); } if (!device.firmwareReleaseConfigInfo) { throw constants_1.ERRORS.TypedError('Runtime', 'device.firmwareReleaseConfigInfo is not set'); } const deviceModel = device.features?.internal_model; const { release: { version }, intermediary } = device.firmwareReleaseConfigInfo; log.debug('onCallFirmwareUpdate loading binary', 'isIntermediary', isIntermediary, 'version', version, 'firmwareType', firmwareType, 'deviceModel', deviceModel); const release = await (0, firmwareInfo_1.getReleaseByVersion)(device.features, version, firmwareType); if (!release) { throw new Error('Missing Firmware release for device'); } const { baseUrl, path } = (0, firmwareInfo_1.getFirmwareLocation)({ firmwareVersion: version, remotePath: release.url, deviceModel, firmwareType, intermediaryVersion: isIntermediary && intermediary ? intermediary.version : undefined }); return (0, firmware_1.getBinary)({ baseUrl, path, release }); }; const onCallFirmwareUpdate = async ({ params, context }) => { const { deviceList, registerEvents, postMessage, initDevice, log } = context; log.debug('onCallFirmwareUpdate with params: ', params); const firmwareType = params.btcOnly ? types_1.FirmwareType.BitcoinOnly : types_1.FirmwareType.Universal; const device = await initDevice(params?.device?.path); if (!device.features) { throw constants_1.ERRORS.TypedError('Device_NotFound', 'Device missing features'); } if (deviceList.getDeviceCount() > 1) { throw constants_1.ERRORS.TypedError('Device_MultipleNotSupported', 'Firmware update allowed with only 1 device connected'); } log.debug('onCallFirmwareUpdate', 'device', device); registerEvents(device); const { manual, upgrade, updateFlowType, btcOnly } = getInstallationParams(device, params); log.debug('onCallFirmwareUpdate', 'installation params', { manual, upgrade, updateFlowType, btcOnly }); const intermediary = !params.binary && device?.firmwareReleaseConfigInfo?.intermediary; postMessage((0, events_1.createUiMessage)(events_1.UI.FIRMWARE_PROGRESS, { device: device.toMessageObject(), operation: 'downloading', progress: 0 })); let intermediaryBinaryInfo; let finalBinaryInfo; const fwFetchPromises = []; if (intermediary) { fwFetchPromises.push(getBinaryHelper({ device, params, firmwareType, isIntermediary: true, log })); } fwFetchPromises.push(getBinaryHelper({ device, params, firmwareType, isIntermediary: false, log })); const [firstResult, finalResult] = await Promise.all(fwFetchPromises); if (intermediary) { intermediaryBinaryInfo = firstResult; finalBinaryInfo = finalResult; } else { finalBinaryInfo = firstResult; } const firstBinaryInfo = intermediary ? intermediaryBinaryInfo : finalBinaryInfo; if (!firstBinaryInfo) { throw new Error('Missing binary, something went wrong.'); } postMessage((0, events_1.createUiMessage)(events_1.UI.FIRMWARE_PROGRESS, { device: device.toMessageObject(), operation: 'downloading', progress: 100 })); if ((0, firmwareUtils_1.isFirmwareCacheUsedForSelectedSource)() && finalBinaryInfo.release) { const message = (0, events_1.createUiMessage)(events_1.UI.FIRMWARE_DOWNLOADED, { binary: finalBinaryInfo.binary, binaryVersion: finalBinaryInfo.binaryVersion, releaseVersion: finalBinaryInfo.release?.version, firmwareType, release: finalBinaryInfo.release, internalModel: device.features.internal_model }); postMessage(message); } const deviceInitiallyConnectedInBootloader = device.features.bootloader_mode; let reconnectedDevice = device; if (deviceInitiallyConnectedInBootloader) { log.warn('onCallFirmwareUpdate', 'device is already in bootloader mode.'); await device.acquire(); } else if (manual) { reconnectedDevice = await waitForReconnectedDevice({ bootloader: true, method: 'manual' }, { ...context, device }); } else { const rebootParams = upgrade ? { boot_command: constants_1.PROTO.BootCommand.INSTALL_UPGRADE, firmware_header: getFwHeader(firstBinaryInfo.binary) } : {}; await device.acquire(); const disconnectedPromise = new Promise(resolve => { deviceList.once('device-disconnect', resolve); }); await device.getCommands().typedCall('RebootToBootloader', 'Success', rebootParams); log.info('onCallFirmwareUpdate', 'waiting for disconnected event after rebootToBootloader...'); if (device.descriptor.apiType === 'bluetooth') { await device.release(); await waitForBluetoothReboot({ device, target: 'bootloader', postMessage }); } else { await disconnectedPromise; if (device.features.major_version === 1) { await (0, utils_1.resolveAfter)(2000); } } reconnectedDevice = await waitForReconnectedDevice({ bootloader: true, method: 'auto' }, { ...context, device }); } const bootloaderVersion = reconnectedDevice.getVersion(); await reconnectedDevice.initialize(false); let stripped = (0, firmware_1.stripFwHeaders)(firstBinaryInfo.binary); const payload = !intermediary && (0, firmware_1.shouldStripFwHeaders)(device.features) ? stripped : firstBinaryInfo.binary; await (0, firmware_1.uploadFirmware)({ typedCall: reconnectedDevice.getCommands().typedCall, postMessage, device: reconnectedDevice, firmwareUploadRequest: { payload }, updateFlowType }); log.info('onCallFirmwareUpdate', 'firmware uploaded'); if (intermediary) { log.info('onCallFirmwareUpdate', '...but it was the intermediary firmware, so one more go'); reconnectedDevice = await waitForReconnectedDevice({ bootloader: true, method: 'manual', intermediary: true }, { ...context, device: reconnectedDevice }); stripped = (0, firmware_1.stripFwHeaders)(finalBinaryInfo.binary); await reconnectedDevice.initialize(false); await (0, firmware_1.uploadFirmware)({ typedCall: reconnectedDevice.getCommands().typedCall, postMessage, device: reconnectedDevice, firmwareUploadRequest: { payload: stripped }, updateFlowType }); } let method = 'wait'; if (device.descriptor.apiType === 'bluetooth') { await waitForBluetoothReboot({ device, target: 'normal', postMessage }); method = 'auto'; } reconnectedDevice = await waitForReconnectedDevice({ bootloader: false, method }, { ...context, device: reconnectedDevice }); const installedVersion = reconnectedDevice.getVersion(); if (!bootloaderVersion || !installedVersion) { throw constants_1.ERRORS.TypedError('Runtime', 'reconnectedDevice.installedVersion is not set'); } const { binaryVersion, release } = finalBinaryInfo; const assertBinaryVersion = (0, versionUtils_1.isEqual)(installedVersion, binaryVersion); const assertReleaseVersion = release?.version ? (0, versionUtils_1.isEqual)(installedVersion, release?.version) : true; await reconnectedDevice.release(); log.info('onCallFirmwareUpdate', `firmware updated to version ${installedVersion}`); return { versionCheck: assertBinaryVersion && assertReleaseVersion, bootloaderVersion, installedVersion, binaryVersion, releaseVersion: release?.version }; }; exports.onCallFirmwareUpdate = onCallFirmwareUpdate; //# sourceMappingURL=onCallFirmwareUpdate.js.map