UNPKG

@trezor/connect

Version:

High-level javascript interface for Trezor hardware wallet.

260 lines (259 loc) 9.9 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 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', 'RecoveryDevice']; 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 nestedError = cause => error(constants_1.ERRORS.nestError(cause)); const fail = msg => error((0, errors_groups_1.isErrorWithoutDeviceInteraction)(msg) ? new constants_1.ERRORS.TransportError(msg) : new Error(msg)); 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 = {}) { const deviceSessionId = this.device.getThpState()?.sessionId || this.device?.features?.session_id; if (!allowedCallsBeforeInitialize.includes(type) && !deviceSessionId) { 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(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 = this.device.possibleT1 && name === 'GetFeatures' ? 3_000 : undefined; const callPromise = this.call(name, data, { timeout }); const [abortedDuringCall, response] = await Promise.race([callPromise.then(res => [false, res]), abortPromise.then(res => [true, nestedError(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 { this.device.getThpState()?.sync('send', 'Cancel'); await this.send('Cancel', {}); } } await callPromise; if (this.disposed) return nestedError(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.then(nestedError)]); 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.then(nestedError)]); 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.then(nestedError)]); 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().catch(() => {}); } return success(res); } } } } async call(name, data, options = {}) { if (this.disposed) return Promise.resolve(nestedError(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(nestedError(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(nestedError(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