@trezor/transport
Version:
Low level library facilitating protocol buffers based communication with Trezor devices
414 lines (413 loc) • 11.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.AbstractApiTransport = void 0;
const tslib_1 = require("tslib");
const protocol_1 = require("@trezor/protocol");
const abstract_1 = require("./abstract");
const constants_1 = require("../constants");
const ERRORS = tslib_1.__importStar(require("../errors"));
const background_1 = require("../sessions/background");
const client_1 = require("../sessions/client");
const thp_1 = require("../thp");
const receive_1 = require("../utils/receive");
const send_1 = require("../utils/send");
class AbstractApiTransport extends abstract_1.AbstractTransport {
sessionsClient;
sessionsBackground;
api;
constructor({
api,
...rest
}) {
super(rest);
this.api = api;
this.sessionsBackground = new background_1.SessionsBackground();
this.sessionsClient = new client_1.SessionsClient(this.sessionsBackground);
}
get apiType() {
return this.api.type;
}
init({
signal
} = {}) {
return this.scheduleAction(async () => {
this.sessionsClient.setBackground(this.sessionsBackground);
const handshakeRes = await this.sessionsClient.handshake();
this.stopped = !handshakeRes.success;
return handshakeRes;
}, {
signal
});
}
listen() {
if (this.listening) {
return this.error({
error: ERRORS.ALREADY_LISTENING
});
}
this.api.listen();
this.listening = true;
this.api.on('transport-interface-change', descriptors => {
this.logger?.debug('new descriptors from api', descriptors);
this.sessionsClient.enumerateDone({
descriptors
});
});
this.sessionsClient.on('descriptors', descriptors => {
this.logger?.debug('new descriptors from background', descriptors);
this.handleDescriptorsChange(descriptors);
});
this.sessionsClient.on('releaseRequest', descriptor => {
this.deviceEvents.emit(descriptor.path, {
type: constants_1.TRANSPORT.DEVICE_REQUEST_RELEASE
});
});
return this.success(undefined);
}
enumerate({
signal
} = {}) {
return this.scheduleAction(async signal => {
const enumerateResult = await this.api.enumerate(signal);
if (!enumerateResult.success) {
return enumerateResult;
}
const descriptors = enumerateResult.payload;
const enumerateDoneResponse = await this.sessionsClient.enumerateDone({
descriptors
});
return this.success(enumerateDoneResponse.payload.descriptors);
}, {
signal
});
}
acquire({
input,
signal
}) {
return this.scheduleAction(async signal => {
const {
path
} = input;
const acquireIntentResponse = await this.sessionsClient.acquireIntent(input);
if (!acquireIntentResponse.success) {
return this.error({
error: acquireIntentResponse.error
});
}
const reset = !!input.previous;
const openDeviceResult = await this.api.openDevice(acquireIntentResponse.payload.path, {
reset,
signal,
channel: 'read'
});
if (!openDeviceResult.success) {
return openDeviceResult;
}
this.sessionsClient.acquireDone({
path,
sessionOwner: this.id
});
return this.success(acquireIntentResponse.payload.session);
}, {
signal
}, [ERRORS.DEVICE_DISCONNECTED_DURING_ACTION, ERRORS.SESSION_WRONG_PREVIOUS]);
}
subscribe({
path,
channels,
signal
}) {
return this.scheduleAction(async signal => {
const entries = await Promise.all(channels.map(async channel => {
try {
const res = await this.api.openDevice(path, {
reset: false,
signal,
channel
});
return [channel, res.success];
} catch {
return [channel, false];
}
}));
const map = Object.fromEntries(entries);
return this.success(map);
}, {
signal
});
}
release({
path: _,
session,
signal
}) {
return this.scheduleAction(async () => {
const releaseIntentResponse = await this.sessionsClient.releaseIntent({
session
});
if (!releaseIntentResponse.success) {
return this.error({
error: releaseIntentResponse.error
});
}
await this.api.closeDevice(releaseIntentResponse.payload.path, {
channel: 'read'
});
await this.sessionsClient.releaseDone({
path: releaseIntentResponse.payload.path
});
return this.success(null);
}, {
signal
});
}
releaseSync(session) {
this.sessionsClient.releaseIntent({
session
}).then(res => {
if (res.success) this.api.closeDevice(res.payload.path, {
channel: 'read'
});
});
}
call({
session,
name,
data,
protocol: customProtocol,
thpState,
signal,
timeout
}) {
return this.scheduleAction(async signal => {
const handleError = error => {
if (error === ERRORS.DEVICE_DISCONNECTED_DURING_ACTION) {
this.enumerate();
}
};
const getPathBySessionResponse = await this.sessionsClient.getPathBySession({
session
});
if (!getPathBySessionResponse.success) {
if (getPathBySessionResponse.error === 'session not found') {
return this.error({
error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION
});
}
return this.error({
error: ERRORS.UNEXPECTED_ERROR
});
}
const {
path
} = getPathBySessionResponse.payload;
const protocol = customProtocol || protocol_1.v1;
const bytes = (0, send_1.buildMessage)({
messages: this.messages,
name,
data,
protocol,
thpState
});
const [, chunkHeader] = protocol.getHeaders(bytes);
const chunks = (0, send_1.createChunks)(bytes, chunkHeader, this.api.chunkSize);
let progress = 0;
const apiWrite = (chunk, attemptSignal) => {
if (chunks.length > 1) {
progress++;
this.emit(constants_1.TRANSPORT.SEND_MESSAGE_PROGRESS, progress / chunks.length);
}
return this.api.write(path, chunk, attemptSignal || signal);
};
const apiRead = attemptSignal => this.api.read(path, attemptSignal || signal);
if (protocol.name === 'v2') {
const prevNonce = thpState?.sendNonce;
const callResult = await (0, thp_1.callThpMessage)({
thpState,
chunks,
apiWrite,
apiRead,
signal,
graceful: true,
logger: this.logger
});
if (!callResult.success) {
handleError(callResult.error);
return callResult;
}
if (prevNonce === thpState?.sendNonce) {
thpState?.sync('send', name);
}
const message = (0, thp_1.parseThpMessage)({
messages: this.messages,
decoded: callResult.payload,
thpState
});
thpState?.sync('recv', message.type);
return this.success(message);
}
const sendResult = await (0, send_1.sendChunks)(chunks, apiWrite);
if (!sendResult.success) {
handleError(sendResult.error);
return sendResult;
}
const readResult = await (0, receive_1.receiveAndParse)(this.messages, apiRead, protocol);
if (!readResult.success) {
handleError(readResult.error);
return readResult;
}
return readResult;
}, {
signal,
graceful: true,
timeout
});
}
send({
data,
session,
name,
protocol: customProtocol,
thpState,
signal,
timeout
}) {
return this.scheduleAction(async signal => {
const getPathBySessionResponse = await this.sessionsClient.getPathBySession({
session
});
if (!getPathBySessionResponse.success) {
return this.error({
error: getPathBySessionResponse.error
});
}
const {
path
} = getPathBySessionResponse.payload;
const protocol = customProtocol || protocol_1.v1;
const bytes = (0, send_1.buildMessage)({
messages: this.messages,
name,
data,
protocol,
thpState
});
const [_, chunkHeader] = protocol.getHeaders(bytes);
const chunks = (0, send_1.createChunks)(bytes, chunkHeader, this.api.chunkSize);
let progress = 0;
const apiWrite = chunk => {
if (chunks.length > 1) {
progress++;
this.emit(constants_1.TRANSPORT.SEND_MESSAGE_PROGRESS, progress / chunks.length);
}
return this.api.write(path, chunk, signal);
};
let sendResult;
if (protocol.name === 'v2') {
sendResult = await (0, thp_1.sendThpMessage)({
thpState,
skipAck: true,
chunks,
apiWrite,
apiRead: attemptSignal => this.api.read(path, attemptSignal || signal),
signal,
graceful: true,
logger: this.logger
});
thpState?.sync('send', name);
} else {
sendResult = await (0, send_1.sendChunks)(chunks, apiWrite);
}
if (!sendResult.success) {
if (sendResult.error === ERRORS.DEVICE_DISCONNECTED_DURING_ACTION) {
this.enumerate();
}
}
return sendResult;
}, {
signal,
graceful: true,
timeout
});
}
receive({
session,
protocol: customProtocol,
thpState,
signal,
timeout
}) {
return this.scheduleAction(async signal => {
const getPathBySessionResponse = await this.sessionsClient.getPathBySession({
session
});
if (!getPathBySessionResponse.success) {
return this.error({
error: getPathBySessionResponse.error
});
}
const {
path
} = getPathBySessionResponse.payload;
const apiRead = attemptSignal => this.api.read(path, attemptSignal || signal);
const protocol = customProtocol || protocol_1.v1;
if (protocol.name === 'v2') {
const decoded = await (0, thp_1.receiveThpMessage)({
thpState,
skipAck: true,
apiWrite: (chunk, attemptSignal) => this.api.write(path, chunk, attemptSignal || signal),
apiRead,
signal,
graceful: true,
logger: this.logger
});
if (!decoded.success) {
return decoded;
}
const message = (0, thp_1.parseThpMessage)({
messages: this.messages,
decoded: decoded.payload,
thpState
});
return this.success(message);
}
const message = await (0, receive_1.receiveAndParse)(this.messages, apiRead, protocol);
if (!message.success) {
if (message.error === ERRORS.DEVICE_DISCONNECTED_DURING_ACTION) {
this.enumerate();
}
}
return message;
}, {
signal,
graceful: true,
timeout
});
}
releaseDevice(session) {
return this.sessionsClient.getPathBySession({
session
}).then(response => {
if (response.success) {
return this.api.closeDevice(response.payload.path, {
channel: 'read'
});
}
return this.success(undefined);
});
}
stop() {
if (!this.stopped) {
this.api.once('transport-interface-change', () => {
this.logger?.debug('device connected after transport stopped, goodbye...');
});
}
super.stop();
this.api.dispose();
}
}
exports.AbstractApiTransport = AbstractApiTransport;
//# sourceMappingURL=abstractApi.js.map