UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

265 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeviceCurrentSession = void 0; const protobuf_1 = require("@trezor/protobuf"); const schema_utils_1 = require("@trezor/schema-utils"); const transport_1 = require("@trezor/transport"); const errors_groups_1 = require("@trezor/transport/lib/errors-groups"); const utils_1 = require("@trezor/utils"); const constants_1 = require("../constants"); const DataManager_1 = require("../data/DataManager"); const events_1 = require("../events"); const debug_1 = require("../utils/debug"); const blacklist = { PassphraseAck: ['passphrase'], CipheredKeyValue: ['value'], GetPublicKey: ['address_n'], PublicKey: ['node', 'xpub'], DecryptedMessage: ['message', 'address'], Features: true, }; const allowedCallsBeforeInitialize = [ 'Cancel', 'Initialize', 'GetFeatures', 'GetFirmwareHash', 'ChangeLanguage', 'DataChunkAck', 'RebootToBootloader', 'FirmwareErase', 'FirmwareUpload', ]; const filterForLog = (type, msg) => blacklist[type] === true ? '(redacted...)' : (blacklist[type] ?? []).reduce((prev, cur) => ({ ...prev, [cur]: '(redacted...)' }), msg); const logger = (0, debug_1.initLog)('DeviceCommands'); const isExpectedResponse = (response, expected) => (Array.isArray(expected) ? expected : expected.split('|')).includes(response.type); const success = (payload) => ({ success: true, payload }); const error = (error) => ({ success: false, error }); const fail = (msg) => error(new Error(msg, (0, errors_groups_1.isErrorWithoutDeviceInteraction)(msg) ? { cause: 'transport-error' } : undefined)); const getFeaturesTimeout = () => DataManager_1.DataManager.getSettings('env') === 'react-native' ? 20_000 : 3_000; class DeviceCurrentSession { device; transport; session; disposed; callPromise; abortController; constructor(device, transport, session) { this.device = device; this.transport = transport; this.session = session; transport.deviceEvents.once(device.transportPath, e => { if (!this.disposed) { this.disposed = constants_1.ERRORS.TypedError(e.type === transport_1.TRANSPORT.DEVICE_DISCONNECTED ? 'Device_Disconnected' : 'Device_UsedElsewhere'); this.abortController?.abort(this.disposed); } }); } isDisposed() { return !!this.disposed; } async typedCall(type, expectedType, msg = {}) { if (!allowedCallsBeforeInitialize.includes(type) && !this.device?.features?.session_id) { console.error('Runtime', `typedCall: Device not initialized when calling ${type}. call Initialize first`); } (0, schema_utils_1.Assert)(protobuf_1.MessagesSchema.MessageType.properties[type], msg); this.abortController = new AbortController(); const { signal } = this.abortController; const abortPromise = new Promise(resolve => signal.addEventListener('abort', () => resolve(error(signal.reason)))); const callPromise = this.callLoop(type, msg, abortPromise); this.callPromise = callPromise; const response = await callPromise; this.callPromise = undefined; this.abortController = undefined; if (!response.success) throw response.error; const { payload } = response; const receivedType = payload.type; if (isExpectedResponse(payload, expectedType)) { return payload; } else { await (0, utils_1.scheduleAction)(abort => this.transport.receive({ session: this.session, protocol: this.device.protocol, thpState: this.device.getThpState(), signal: abort, }), { timeout: 500 }).catch(() => { }); throw constants_1.ERRORS.TypedError('Runtime', `assertType: Response of unexpected type: ${receivedType}. Should be ${expectedType}`); } } needCancelWorkaround() { return (this.transport.name === 'BridgeTransport' && !utils_1.versionUtils.isNewer(this.transport.version, '2.0.28')); } async callLoop(type, msg, abortPromise) { let [name, data] = [type, msg]; let pinUnlocked = false; while (true) { const timeout = name === 'GetFeatures' ? getFeaturesTimeout() : undefined; const callPromise = this.call(name, data, { timeout }); const [abortedDuringCall, response] = await Promise.race([ callPromise.then(res => [false, res]), abortPromise.then(res => [true, res]), ]); if (name === 'ButtonAck' && abortedDuringCall && !this.disposed) { if (this.needCancelWorkaround()) { try { await (0, utils_1.resolveAfter)(1); await this.device.acquire(); await this.device.getCurrentSession().cancelCall(); await this.device.release(); } catch { } } else { await this.transport.send({ name: 'Cancel', data: {}, session: this.session, protocol: this.device.protocol, thpState: this.device.getThpState(), }); } } await callPromise; if (this.disposed) return error(this.disposed); if (!response.success) return response; const res = response.payload; switch (res.type) { case 'Failure': { const { code, message } = res.message; if (name === 'GetFeatures' && code === 'Failure_UnexpectedMessage') { [name, data] = ['Initialize', {}]; break; } const err = message || (code === 'Failure_FirmwareError' && 'Firmware installation failed') || (code === 'Failure_ActionCancelled' && 'Action cancelled by user') || 'Failure_UnknownMessage'; return error(new constants_1.ERRORS.TrezorError(code || 'Failure_UnknownCode', err)); } case 'ButtonRequest': { if (res.message.code === 'ButtonRequest_PassphraseEntry') { this.device.emit(events_1.DEVICE.PASSPHRASE_ON_DEVICE); } else { this.device.emit(events_1.DEVICE.BUTTON, { device: this.device, payload: res.message, }); } [name, data] = ['ButtonAck', {}]; break; } case 'PinMatrixRequest': { const promptRes = await Promise.race([ this.device.prompt(events_1.DEVICE.PIN, { type: res.message.type }), abortPromise, ]); if (!promptRes.success) { const cancelRes = await this.call('Cancel', {}); return cancelRes.success ? promptRes : cancelRes; } pinUnlocked = true; [name, data] = ['PinMatrixAck', { pin: promptRes.payload }]; break; } case 'PassphraseRequest': { const promptRes = await Promise.race([ this.device.prompt(events_1.DEVICE.PASSPHRASE, {}), abortPromise, ]); if (!promptRes.success) { const cancelRes = await this.call('Cancel', {}); return cancelRes.success ? promptRes : cancelRes; } const payload = promptRes.payload.passphraseOnDevice ? { on_device: true } : { passphrase: promptRes.payload.value.normalize('NFKD') }; [name, data] = ['PassphraseAck', payload]; break; } case 'WordRequest': { const promptRes = await Promise.race([ this.device.prompt(events_1.DEVICE.WORD, { type: res.message.type }), abortPromise, ]); if (!promptRes.success) { const cancelRes = await this.call('Cancel', {}); return cancelRes.success ? promptRes : cancelRes; } [name, data] = ['WordAck', { word: promptRes.payload }]; break; } default: { if (!this.disposed && pinUnlocked && !this.device.features.unlocked) { await this.device.getFeatures(); } return success(res); } } } } async call(name, data, options = {}) { if (this.disposed) return Promise.resolve(error(this.disposed)); logger.debug('Sending', name, filterForLog(name, data)); const result = await this.transport.call({ name, data, session: this.session, protocol: this.device.protocol, thpState: this.device.getThpState(), ...options, }); if (result.success) { const { type, message } = result.payload; logger.debug('Received', type, filterForLog(type, message)); } else { logger.warn('Received transport error', result.error, result.message); } return result.success ? success(result.payload) : fail(result.message || result.error); } async send(name, data, options = {}) { if (this.disposed) return Promise.resolve(error(this.disposed)); const result = await this.transport.send({ name, data, session: this.session, protocol: this.device.protocol, thpState: this.device.getThpState(), ...options, }); return result.success ? success(result.payload) : fail(result.message || result.error); } async receive(options = {}) { if (this.disposed) return Promise.resolve(error(this.disposed)); const result = await this.transport.receive({ session: this.session, protocol: this.device.protocol, thpState: this.device.getThpState(), ...options, }); return result.success ? success(result.payload) : fail(result.message || result.error); } cancelCall() { return this.call('Cancel', {}); } async abort(reason) { this.abortController?.abort(reason); await this.callPromise; this.disposed = reason; } } exports.DeviceCurrentSession = DeviceCurrentSession; //# sourceMappingURL=DeviceCurrentSession.js.map