@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
265 lines • 11.4 kB
JavaScript
"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