UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

766 lines (765 loc) 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Device = void 0; 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 checkFirmwareHashWithRetries_1 = require("./workflow/checkFirmwareHashWithRetries"); const coinInfo_1 = require("../data/coinInfo"); const firmwareInfo_1 = require("../data/firmwareInfo"); const events_1 = require("../events"); const types_1 = require("../types"); const handshake_1 = require("./workflow/handshake"); const assetUtils_1 = require("../utils/assetUtils"); const debug_1 = require("../utils/debug"); const deviceFeaturesUtils_1 = require("../utils/deviceFeaturesUtils"); const firmwareUtils_1 = require("../utils/firmwareUtils"); const _log = (0, debug_1.initLog)('Device'); class Device extends utils_1.TypedEmitter { transport; thp; descriptor; sessionAcquired; _protocol; get protocol() { return this._protocol; } getThpState() { return this.thp; } unreadableError; _firmwareStatus; get firmwareStatus() { return this._firmwareStatus; } _currentRelease; get currentRelease() { return this._currentRelease; } _firmwareReleaseConfigInfo; get firmwareReleaseConfigInfo() { return this._firmwareReleaseConfigInfo; } _features; get features() { return this._features; } wasUsedElsewhere = false; acquirePromise; releasePromise; runAbort; runPromise; keepTransportSession = false; currentSession; instance = 0; state = []; stateStorage = undefined; busy; _unavailableCapabilities = {}; get unavailableCapabilities() { return this._unavailableCapabilities; } _firmwareType; get firmwareType() { return this._firmwareType; } get possibleHIDdevice() { return this.descriptor.type === 0 || this.descriptor.type === 2; } get possibleT1() { return (this.descriptor.type ?? 0) <= 2; } 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.descriptor = { id: descriptor.id, apiType: descriptor.apiType, type: descriptor.type, path: descriptor.path }; this.sessionAcquired = null; transport.on(transport_1.TRANSPORT.STOPPED, this.onTransportStopped); transport.deviceEvents.on(this.descriptor.path, this.onTransportDeviceEvent); } get transportPath() { return this.descriptor.path; } 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.descriptor.path)?.session ?? null; this.acquirePromise = this.transport.acquire({ input: { path: this.descriptor.path, 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; } reset() { _log.info(`Resetting Features and ThpState`); this._features = undefined; this._protocol = protocol_1.v1; this.thp?.resetState(); this.thp = undefined; } setBusy(value) { this.busy = value; } release() { if (!this.sessionAcquired || this.keepTransportSession || this.releasePromise) { return; } const sessionPromise = this.getSessionChangePromise(); this.releasePromise = this.transport.release({ session: this.sessionAcquired, path: this.descriptor.path }).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 (0, thp_1.abortThpWorkflow)(this); 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; this.busy = await (0, thp_1.getThpChannel)(this, withInteraction); if (!this.busy) { await this.getFeatures(); } } else if (fn) { await this.initialize(!!options.useCardanoDerivation); } else { await this.getFeatures(); } } catch (error) { _log.warn('Device._runInner error: ', error.message); if (error.code === 'Failure_Busy') { this.busy = 'busy'; } if (error.code === 'ThpDeviceLocked') { this.busy = 'pin-locked'; } 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 (0, checkFirmwareHashWithRetries_1.checkFirmwareHashWithRetries)({ device: this, logger: _log }); 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._updateCurrentRelease(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); this._updateCurrentRelease(message); } getAuthenticityChecks() { return this.authenticityChecks; } setAuthenticityChecks(firmwareHash) { this.authenticityChecks.firmwareHash = firmwareHash; } 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 || !this.firmwareType) { return; } if (this.features && this.features.bootloader_mode === true) { return; } const release = (0, assetUtils_1.getReleaseAsset)(this.features.internal_model, firmwareVersion, this.firmwareType); const result = await (0, checkFirmwareRevision_1.checkFirmwareRevision)({ internalModel: this.features.internal_model, deviceRevision: this.features.revision, firmwareVersion, expectedRevision: release?.firmware_revision, firmwareType: this.firmwareType }); 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'); } if (!this.firmwareType) { throw constants_1.ERRORS.TypedError('Runtime', 'changeLanguage: firmware type unknown'); } if (!this._currentRelease) { throw constants_1.ERRORS.TypedError('Runtime', 'changeLanguage: release not found'); } const languageBinPath = this._currentRelease.translations[language]; const downloadedBinary = await (0, firmwareInfo_1.getLanguage)(languageBinPath); if (!downloadedBinary) { throw constants_1.ERRORS.TypedError('Runtime', 'changeLanguage: translation not found'); } let dataToSend; if (Buffer.isBuffer(downloadedBinary)) { dataToSend = new Uint8Array(downloadedBinary).buffer; } else { dataToSend = downloadedBinary; } return this._uploadTranslationData(dataToSend); } 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; } async _updateCurrentRelease(feat) { const { firmwareVersion } = (0, firmwareInfo_1.getCurrentVersion)(feat); const newFirmwareType = (0, firmwareUtils_1.getFirmwareType)(feat); if (!firmwareVersion) { return; } if (this._currentRelease && newFirmwareType === this.firmwareType && utils_1.versionUtils.isEqual(this._currentRelease.version, firmwareVersion)) { return; } const release = await (0, firmwareInfo_1.getReleaseByVersion)(feat, firmwareVersion, newFirmwareType); this._currentRelease = release; this.availableTranslations = this._currentRelease?.translations ?? {}; } _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, (0, firmwareUtils_1.getFirmwareType)(feat)); this._firmwareReleaseConfigInfo = (0, firmwareInfo_1.getFirmwareReleaseConfigInfo)(feat, (0, firmwareUtils_1.getFirmwareType)(feat)); this._currentRelease = (0, assetUtils_1.getReleaseAsset)(feat.internal_model, newVersion, (0, firmwareUtils_1.getFirmwareType)(feat)); this.availableTranslations = this._currentRelease?.translations ?? {}; } this._features = feat; this._firmwareType = (0, firmwareUtils_1.getFirmwareType)(feat); const deviceInfo = device_utils_1.models[feat.internal_model] ?? { name: `Unknown ${feat.internal_model}`, colors: {} }; this.name = deviceInfo.name; this.busy = undefined; if (feat?.unit_color) { const deviceUnitColor = feat.unit_color.toString(); if (deviceUnitColor in deviceInfo.colors) { this.color = deviceInfo.colors[deviceUnitColor]; } } } updateFeature(key, value) { if (this._features) { this._features = { ...this._features, [key]: value }; this.lifecycle.emit(events_1.DEVICE.CHANGED); } } 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.descriptor.path, 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.descriptor.path)?.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; } getStatus() { if (this.isUsedElsewhere()) return 'occupied'; if (this.wasUsedElsewhere) return 'used'; if (this.busy) return this.busy; return 'available'; } getDeviceThp() { const state = this.thp?.serialize(); return state ? { properties: state.properties, credentials: state.credentials, channel: state.channel } : undefined; } toMessageObject() { const { name, uniquePath: path, descriptor } = this; const { apiType, id } = descriptor; const base = { path, name, descriptor: { apiType, id } }; const bluetoothProps = this.descriptor.id && this.descriptor.apiType === 'bluetooth' ? { id: (0, types_1.asBluetoothDeviceId)(this.descriptor.id) } : undefined; 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.descriptor.path)?.sessionOwner; return { ...base, type: 'unacquired', label: 'Unacquired device', name: this.name, transportSessionOwner: this.sessionAcquired ? undefined : sessionOwner, bluetoothProps, thp: this.getDeviceThp(), status: this.busy ? this.busy : undefined }; } const defaultLabel = 'My Trezor'; const label = this.features.label === '' || !this.features.label ? defaultLabel : this.features.label; return { ...base, type: 'acquired', id: this.features.device_id, label, _state: this.getState(), state: this.getState()?.staticSessionId, status: this.getStatus(), mode: (0, firmwareUtils_1.getFirmwareMode)(this.features), color: this.color, firmware: this.firmwareStatus, firmwareReleaseConfigInfo: this.firmwareReleaseConfigInfo, firmwareType: this.firmwareType, features: this.features, unavailableCapabilities: this.unavailableCapabilities, availableTranslations: this.availableTranslations, authenticityChecks: this.authenticityChecks, bluetoothProps, thp: this.getDeviceThp() }; } } exports.Device = Device; //# sourceMappingURL=Device.js.map