@trezor/connect
Version:
High-level javascript interface for Trezor hardware wallet.
260 lines (259 loc) • 9.9 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 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