UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

748 lines 29.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Device = void 0; const crypto_1 = require("crypto"); const device_utils_1 = require("@trezor/device-utils"); const protocol_1 = require("@trezor/protocol"); const transport_1 = require("@trezor/transport"); const utils_1 = require("@trezor/utils"); const DeviceCommands_1 = require("./DeviceCommands"); const constants_1 = require("../constants"); const DeviceCurrentSession_1 = require("./DeviceCurrentSession"); const checkFirmwareRevision_1 = require("./checkFirmwareRevision"); const thp_1 = require("./thp"); const firmware_1 = require("../api/firmware"); const DataManager_1 = require("../data/DataManager"); const coinInfo_1 = require("../data/coinInfo"); const firmwareInfo_1 = require("../data/firmwareInfo"); const getLanguage_1 = require("../data/getLanguage"); const events_1 = require("../events"); const types_1 = require("../types"); const handshake_1 = require("./workflow/handshake"); const debug_1 = require("../utils/debug"); const deviceFeaturesUtils_1 = require("../utils/deviceFeaturesUtils"); const _log = (0, debug_1.initLog)('Device'); class Device extends utils_1.TypedEmitter { transport; transportPath; bluetoothProps; thp; possibleHIDdevice; sessionAcquired; _protocol; get protocol() { return this._protocol; } getThpState() { return this.thp; } unreadableError; _firmwareStatus; get firmwareStatus() { return this._firmwareStatus; } _firmwareRelease; get firmwareRelease() { return this._firmwareRelease; } _features; get features() { return this._features; } wasUsedElsewhere = false; acquirePromise; releasePromise; runAbort; runPromise; keepTransportSession = false; currentSession; instance = 0; state = []; stateStorage = undefined; _unavailableCapabilities = {}; get unavailableCapabilities() { return this._unavailableCapabilities; } _firmwareType; get firmwareType() { return this._firmwareType; } name = 'Trezor'; color; availableTranslations = []; authenticityChecks = { firmwareRevision: null, firmwareHash: null, }; uniquePath; lifecycle = new utils_1.TypedEmitter(); sessionDfd; constructor({ id, transport, descriptor }) { super(); this._protocol = protocol_1.v1; this.uniquePath = id; this.transport = transport; this.transportPath = descriptor.path; this.possibleHIDdevice = [0, 2].includes(descriptor.type); this.bluetoothProps = descriptor.id ? { id: descriptor.id } : undefined; this.sessionAcquired = null; transport.on(transport_1.TRANSPORT.STOPPED, this.onTransportStopped); transport.deviceEvents.on(this.transportPath, this.onTransportDeviceEvent); } onTransportStopped = () => this.disconnect(); onTransportDeviceEvent = (event) => { switch (event.type) { case transport_1.TRANSPORT.DEVICE_SESSION_CHANGED: return this.updateDescriptor(event.descriptor); case transport_1.TRANSPORT.DEVICE_REQUEST_RELEASE: return this.usedElsewhere(); case transport_1.TRANSPORT.DEVICE_DISCONNECTED: { return this.disconnect(); } } }; getSessionChangePromise() { if (!this.sessionDfd) { this.sessionDfd = (0, utils_1.createDeferred)(); this.sessionDfd.promise .catch(() => { }) .finally(() => { this.sessionDfd = undefined; }); } return this.sessionDfd.promise; } async waitAndCompareSession(response, sessionPromise) { if (response.success) { try { if ((await sessionPromise) !== response.payload) { return { success: false, error: transport_1.TRANSPORT_ERROR.SESSION_WRONG_PREVIOUS, }; } } catch { return { success: false, error: transport_1.TRANSPORT_ERROR.DEVICE_DISCONNECTED_DURING_ACTION, }; } } return response; } acquire() { const sessionPromise = this.getSessionChangePromise(); const previous = this.transport.getDescriptor(this.transportPath)?.session ?? null; this.acquirePromise = this.transport .acquire({ input: { path: this.transportPath, previous } }) .then(result => this.waitAndCompareSession(result, sessionPromise)) .then(result => { if (result.success) { this.wasUsedElsewhere = false; this.sessionAcquired = result.payload; this.currentSession = new DeviceCurrentSession_1.DeviceCurrentSession(this, this.transport, this.sessionAcquired); return result; } else { throw new Error(result.error); } }) .finally(() => { this.acquirePromise = undefined; }); return this.acquirePromise; } release() { if (!this.sessionAcquired || this.keepTransportSession || this.releasePromise) { return; } const sessionPromise = this.getSessionChangePromise(); this.releasePromise = this.transport .release({ session: this.sessionAcquired, path: this.transportPath }) .then(result => this.waitAndCompareSession(result, sessionPromise)) .then(result => { if (result.success) { this.sessionAcquired = null; } return result; }) .finally(() => { this.releasePromise = undefined; }); return this.releasePromise; } async setupThp() { _log.info('Setup THP device'); this._protocol = protocol_1.v2; if (this.transport.name === 'BridgeTransport' && !utils_1.versionUtils.isNewerOrEqual(this.transport.version, '3.0.0')) { this.unreadableError = 'THP incompatible with bridge ' + this.transport.version; } else { try { await this.transport.loadMessages('thp', protocol_1.thp.getProtobufDefinitions); this.thp = new protocol_1.thp.ThpState(); } catch (error) { this.unreadableError = error.message; } } } async handshake() { if (this.isUsedElsewhere()) { return true; } try { await this.run(); } catch (error) { _log.warn(`device.run error.message: ${error.message}, code: ${error.code}`); if (error.code === 'Device_NotFound' || error.code === 'Device_Disconnected' || error.message === transport_1.TRANSPORT_ERROR.DEVICE_NOT_FOUND || error.message === transport_1.TRANSPORT_ERROR.DEVICE_DISCONNECTED_DURING_ACTION || error.message === transport_1.TRANSPORT_ERROR.HTTP_ERROR) { return false; } if ((this.possibleHIDdevice && error.message === transport_1.TRANSPORT_ERROR.INTERFACE_UNABLE_TO_OPEN_DEVICE) || error.message === transport_1.TRANSPORT_ERROR.LIBUSB_ERROR_ACCESS) { this.unreadableError = error.message; } } return true; } async updateDescriptor(descriptor) { this.sessionDfd?.resolve(descriptor.session); await Promise.all([this.acquirePromise, this.releasePromise]); if (descriptor.session && descriptor.session !== this.sessionAcquired) { this.usedElsewhere(); } if (!descriptor.session) { this.keepTransportSession = false; } this.lifecycle.emit(events_1.DEVICE.CHANGED); } run(fn, options = {}) { if (this.runPromise) { _log.warn('Previous call is still running'); throw constants_1.ERRORS.TypedError('Device_CallInProgress'); } const wasUnacquired = this.isUnacquired(); this.runAbort = new AbortController(); const { signal } = this.runAbort; this.runPromise = Promise.race([ this._runInner(fn, options, signal), new Promise((_, reject) => { signal.addEventListener('abort', () => reject(signal.reason)); }), ]) .catch(async (err) => { this.keepTransportSession = false; await this.acquirePromise; await this.release(); throw err; }) .finally(() => { this.runAbort = undefined; this.runPromise = undefined; }) .then(() => { if (wasUnacquired && !this.isUnacquired()) { this.lifecycle.emit(events_1.DEVICE.CONNECT); } }); return this.runPromise; } async interrupt(reason) { await this.currentSession?.abort(reason); this.runAbort?.abort(reason); await this.currentRun; } get currentRun() { return this.runPromise?.catch(() => { }); } usedElsewhere() { this.wasUsedElsewhere = true; if (!this.sessionAcquired) { return; } this.transport.releaseDevice(this.sessionAcquired); this.sessionAcquired = null; _log.debug('interruptionFromOutside'); this.runAbort?.abort(constants_1.ERRORS.TypedError('Device_UsedElsewhere')); } async _runInner(fn, options, abortSignal) { if (this.releasePromise) { await this.releasePromise; } const acquireNeeded = !this.isUsedHere() || this.currentSession?.isDisposed(); if (acquireNeeded) { await this.acquire(); } if (abortSignal.aborted) throw abortSignal.reason; const { staticSessionId, deriveCardano } = this.getState() || {}; if (acquireNeeded || !staticSessionId || (!deriveCardano && options.useCardanoDerivation)) { try { await (0, handshake_1.handshakeCancel)({ device: this, logger: _log, signal: abortSignal }); if (this.protocol.name === 'v2') { const withInteraction = !!fn; await (0, thp_1.getThpChannel)(this, withInteraction); } else if (fn) { await this.initialize(!!options.useCardanoDerivation); } else { await this.getFeatures(); } } catch (error) { _log.warn('Device._runInner error: ', error.message); if (error.code === 'Device_ThpPairingTagInvalid') { return Promise.reject(error); } return Promise.reject(constants_1.ERRORS.TypedError('Device_InitializeFailed', `Initialize failed: ${error.message}${error.code ? `, code: ${error.code}` : ''}`)); } } if (!options.skipFirmwareChecks) { await this.checkFirmwareHashWithRetries(); await this.checkFirmwareRevisionWithRetries(); } if (!options.skipLanguageChecks && this.features?.language && !this.features.language_version_matches && this.atLeast('2.7.0')) { _log.info('language version mismatch. silently updating...'); try { await this.changeLanguage({ language: this.features.language }); } catch (err) { _log.error('change language failed silently', err); } } if (options.keepSession) { this.keepTransportSession = true; } if (fn) { await fn(); if (!options.skipFinalReload) { await this.getFeatures(); } } if ((!this.keepTransportSession && typeof options.keepSession !== 'boolean') || options.keepSession === false) { this.keepTransportSession = false; await this.release(); } } getCurrentSession() { if (!this.currentSession) { throw constants_1.ERRORS.TypedError('Runtime', `Device: commands not defined`); } return this.currentSession; } getCommands() { return (0, DeviceCommands_1.DeviceCommands)(this.getCurrentSession()); } setInstance(instance = 0) { if (this.instance !== instance) { if (this.keepTransportSession) { this.sessionAcquired = null; this.keepTransportSession = false; } } this.instance = instance; } getInstance() { return this.instance; } getState() { return this.state[this.instance]; } setState(state) { if (!state) { delete this.state[this.instance]; } else { const prevState = this.state[this.instance]; const newState = { ...prevState, ...state, }; this.state[this.instance] = newState; this.stateStorage?.saveState(this, newState); } } async initialize(useCardanoDerivation) { let payload; if (this.features) { const { sessionId, deriveCardano } = this.getState() || {}; payload = { derive_cardano: deriveCardano || useCardanoDerivation, }; if (sessionId) { payload.session_id = sessionId; } } const { message } = await this.getCurrentSession().typedCall('Initialize', 'Features', payload); this._updateFeatures(message); this.setState({ deriveCardano: payload?.derive_cardano }); } initStorage(storage) { this.stateStorage = storage; this.setState(storage.loadState(this)); } async getFeatures() { const { message } = await this.getCurrentSession().typedCall('GetFeatures', 'Features', {}); this._updateFeatures(message); } async checkFirmwareHashWithRetries() { const lastResult = this.authenticityChecks.firmwareHash; const notDoneYet = lastResult === null; const attemptsDone = lastResult?.attemptCount ?? 0; if (attemptsDone >= constants_1.FIRMWARE.HASH_CHECK_MAX_ATTEMPTS) return; const wasError = lastResult !== null && !lastResult.success; const wasErrorRetriable = wasError && (0, utils_1.isArrayMember)(lastResult.error, constants_1.FIRMWARE.HASH_CHECK_RETRIABLE_ERRORS); const lastErrorPayload = wasError ? lastResult?.errorPayload : null; if (notDoneYet || wasErrorRetriable) { const result = await this.checkFirmwareHash(); this.authenticityChecks = { ...this.authenticityChecks, firmwareHash: result, }; if (result === null) return; result.attemptCount = attemptsDone + 1; if (result.success && lastErrorPayload) { result.warningPayload = { lastErrorPayload, successOnAttempt: result.attemptCount }; } } } async checkFirmwareHash() { const createFailResult = (error, errorPayload) => ({ success: false, error, errorPayload, }); const baseUrl = DataManager_1.DataManager.getSettings('binFilesBaseUrl'); const enabled = DataManager_1.DataManager.getSettings('enableFirmwareHashCheck'); if (!enabled || baseUrl === undefined) return createFailResult('check-skipped'); const firmwareVersion = this.getVersion(); if (firmwareVersion === undefined || !this.features || this.features.bootloader_mode) { return null; } const checkSupported = !this.unavailableCapabilities.getFirmwareHash; if (!checkSupported) return createFailResult('check-unsupported'); const release = (0, firmwareInfo_1.getReleases)(this.features.internal_model).find(r => utils_1.versionUtils.isEqual(r.version, firmwareVersion)); if (release === undefined) return createFailResult('unknown-release'); const btcOnly = this.firmwareType === types_1.FirmwareType.BitcoinOnly; const binary = await (0, firmware_1.getBinaryOptional)({ baseUrl, btcOnly, release }); if (binary === null) { return createFailResult('check-unsupported'); } if (binary.byteLength < 200) { _log.warn(`Firmware binary for hash check suspiciously small (< 200 b)`); return createFailResult('check-unsupported'); } const strippedBinary = (0, firmware_1.stripFwHeaders)(binary); const { hash: expectedHash, challenge } = (0, firmware_1.calculateFirmwareHash)(this.features.major_version, strippedBinary, (0, crypto_1.randomBytes)(32)); try { const deviceResponse = await this.getCurrentSession().typedCall('GetFirmwareHash', 'FirmwareHash', { challenge }); if (!deviceResponse?.message?.hash) { return createFailResult('other-error', 'Device response is missing hash'); } if (deviceResponse.message.hash !== expectedHash) { return createFailResult('hash-mismatch'); } return { success: true }; } catch (errorPayload) { return createFailResult('other-error', (0, utils_1.serializeError)(errorPayload)); } } async checkFirmwareRevisionWithRetries() { const lastResult = this.authenticityChecks.firmwareRevision; const notDoneYet = lastResult === null; const wasError = lastResult !== null && !lastResult.success; const wasErrorRetriable = wasError && (0, utils_1.isArrayMember)(lastResult.error, constants_1.FIRMWARE.REVISION_CHECK_RETRIABLE_ERRORS); if (notDoneYet || wasErrorRetriable) { await this.checkFirmwareRevision(); } } async checkFirmwareRevision() { const firmwareVersion = this.getVersion(); if (!firmwareVersion || !this.features) { return; } if (this.features.bootloader_mode === true) { return; } const release = (0, firmwareInfo_1.getRelease)(this.features.internal_model, firmwareVersion); const result = await (0, checkFirmwareRevision_1.checkFirmwareRevision)({ internalModel: this.features.internal_model, deviceRevision: this.features.revision, firmwareVersion, expectedRevision: release?.firmware_revision, }); this.authenticityChecks = { ...this.authenticityChecks, firmwareRevision: result, }; } async changeLanguage({ language, binary, }) { if (language === 'en-US') { return this._uploadTranslationData(null); } if (binary) { return this._uploadTranslationData(binary); } const version = this.getVersion(); if (!version) { throw constants_1.ERRORS.TypedError('Runtime', 'changeLanguage: device version unknown'); } const downloadedBinary = await (0, getLanguage_1.getLanguage)({ language, version, internal_model: this.features.internal_model, }); if (!downloadedBinary) { throw constants_1.ERRORS.TypedError('Runtime', 'changeLanguage: translation not found'); } return this._uploadTranslationData(downloadedBinary); } async _uploadTranslationData(payload) { if (payload === null) { const response = await this.getCurrentSession().typedCall('ChangeLanguage', ['Success'], { data_length: 0 }); return response.message; } const length = payload.byteLength; let response = await this.getCurrentSession().typedCall('ChangeLanguage', ['DataChunkRequest', 'Success'], { data_length: length }); while (response.type !== 'Success') { const start = response.message.data_offset; const end = response.message.data_offset + response.message.data_length; const chunk = payload.slice(start, end); response = await this.getCurrentSession().typedCall('DataChunkAck', ['DataChunkRequest', 'Success'], { data_chunk: Buffer.from(chunk).toString('hex'), }); } return response.message; } _updateFeatures(feat) { const capabilities = (0, deviceFeaturesUtils_1.parseCapabilities)(feat); feat.capabilities = capabilities; if (this.features && this.features.session_id && !feat.session_id) { feat.session_id = this.features.session_id; } feat.unlocked = feat.unlocked ?? true; const revision = (0, deviceFeaturesUtils_1.parseRevision)(feat); feat.revision = revision; if (!feat.model && feat.major_version === 1) { feat.model = '1'; } if (!feat.internal_model || !device_utils_1.DeviceModelInternal[feat.internal_model]) { feat.internal_model = (0, deviceFeaturesUtils_1.ensureInternalModelFeature)(feat.model); } const version = this.getVersion(); const newVersion = [ feat.major_version, feat.minor_version, feat.patch_version, ]; if (!version || !utils_1.versionUtils.isEqual(version, newVersion)) { if (version) { this.emit(events_1.DEVICE.FIRMWARE_VERSION_CHANGED, { oldVersion: version, newVersion, device: this.toMessageObject(), }); } this._unavailableCapabilities = (0, deviceFeaturesUtils_1.getUnavailableCapabilities)(feat, (0, coinInfo_1.getAllNetworks)()); this._firmwareStatus = (0, firmwareInfo_1.getFirmwareStatus)(feat); this._firmwareRelease = (0, firmwareInfo_1.getReleaseInfo)(feat); this.availableTranslations = this.firmwareRelease?.translations ?? []; } this._features = feat; if (feat.fw_vendor === 'Trezor Bitcoin-only') { this._firmwareType = types_1.FirmwareType.BitcoinOnly; } else if (feat.fw_vendor === 'Trezor') { this._firmwareType = types_1.FirmwareType.Universal; } else if (this.getMode() !== 'bootloader') { this._firmwareType = feat.capabilities && feat.capabilities.length > 0 && !feat.capabilities.includes('Capability_Bitcoin_like') ? types_1.FirmwareType.BitcoinOnly : types_1.FirmwareType.Universal; } const deviceInfo = device_utils_1.models[feat.internal_model] ?? { name: `Unknown ${feat.internal_model}`, colors: {}, }; this.name = deviceInfo.name; if (feat?.unit_color) { const deviceUnitColor = feat.unit_color.toString(); if (deviceUnitColor in deviceInfo.colors) { this.color = deviceInfo.colors[deviceUnitColor]; } } } prompt(type, args) { return new Promise(callback => { if (!this.listenerCount(type)) { const payload = { success: false, error: new Error(`${type} callback not configured`), }; callback(payload); } else { this.emit(type, { callback, ...args }); } }); } isUnacquired() { return this.features === undefined; } isUnreadable() { return !!this.unreadableError; } disconnect() { _log.debug('Disconnect cleanup'); this.transport.off(transport_1.TRANSPORT.STOPPED, this.onTransportStopped); this.transport.deviceEvents.off(this.transportPath, this.onTransportDeviceEvent); this.removeAllListeners(); this.sessionDfd?.reject(new Error()); if (this.sessionAcquired) { this.transport.releaseSync(this.sessionAcquired); this.sessionAcquired = null; } this.lifecycle.emit(events_1.DEVICE.DISCONNECT); return this.interrupt(constants_1.ERRORS.TypedError('Device_Disconnected')); } isBootloader() { return this.features && !!this.features.bootloader_mode; } isInitialized() { return this.features && !!this.features.initialized; } isSeedless() { return this.features && !!this.features.no_backup; } getVersion() { if (!this.features) return; return [ this.features.major_version, this.features.minor_version, this.features.patch_version, ]; } atLeast(versions) { const version = this.getVersion(); if (!this.features || !version) return false; const modelVersion = typeof versions === 'string' ? versions : versions[this.features.major_version - 1]; return utils_1.versionUtils.isNewerOrEqual(version, modelVersion); } isUsed() { return !!this.transport.getDescriptor(this.transportPath)?.session; } isUsedHere() { return !!this.sessionAcquired; } isUsedElsewhere() { return this.isUsed() && !this.isUsedHere(); } getUniquePath() { return this.uniquePath; } isT1() { return this.features ? this.features.major_version === 1 : false; } hasUnexpectedMode(allow, require) { if (this.features) { if (this.isBootloader() && !allow.includes(events_1.UI.BOOTLOADER)) { return events_1.UI.BOOTLOADER; } if (!this.isInitialized() && !allow.includes(events_1.UI.INITIALIZE)) { return events_1.UI.INITIALIZE; } if (this.isSeedless() && !allow.includes(events_1.UI.SEEDLESS)) { return events_1.UI.SEEDLESS; } if (!this.isBootloader() && require.includes(events_1.UI.BOOTLOADER)) { return events_1.UI.NOT_IN_BOOTLOADER; } } return null; } getMode() { if (this.features.bootloader_mode) return 'bootloader'; if (!this.features.initialized) return 'initialize'; if (this.features.no_backup) return 'seedless'; return 'normal'; } toMessageObject() { const { name, uniquePath: path } = this; const base = { path, name }; if (this.unreadableError) { return { ...base, type: 'unreadable', error: this.unreadableError, label: 'Unreadable device', hid: this.possibleHIDdevice, }; } if (this.isUnacquired()) { const sessionOwner = this.transport.getDescriptor(this.transportPath)?.sessionOwner; return { ...base, type: 'unacquired', label: 'Unacquired device', name: this.name, transportSessionOwner: this.sessionAcquired ? undefined : sessionOwner, bluetoothProps: this.bluetoothProps, thp: this.thp?.serialize(), }; } const defaultLabel = 'My Trezor'; const label = this.features.label === '' || !this.features.label ? defaultLabel : this.features.label; const status = this.isUsedElsewhere() ? 'occupied' : (this.wasUsedElsewhere && 'used') || 'available'; return { ...base, type: 'acquired', id: this.features.device_id, label, _state: this.getState(), state: this.getState()?.staticSessionId, status, mode: this.getMode(), color: this.color, firmware: this.firmwareStatus, firmwareRelease: this.firmwareRelease, firmwareType: this.firmwareType, features: this.features, unavailableCapabilities: this.unavailableCapabilities, availableTranslations: this.availableTranslations, authenticityChecks: this.authenticityChecks, bluetoothProps: this.bluetoothProps, thp: this.thp?.serialize(), }; } } exports.Device = Device; //# sourceMappingURL=Device.js.map