feat(transport): unify protocol encode/decode functions

This commit is contained in:
Szymon Lesisz
2023-11-20 16:10:16 +01:00
committed by martin
parent 5073f49215
commit b4f08409cb
14 changed files with 172 additions and 150 deletions

View File

@@ -2,3 +2,4 @@ export * as v1 from './protocol-v1';
export * as bridge from './protocol-bridge';
export * as trzd from './protocol-trzd';
export * from './errors';
export * from './types';

View File

@@ -1,5 +1,7 @@
import ByteBuffer from 'bytebuffer';
import { TransportProtocolDecode } from '../types';
/**
* Reads meta information from buffer
*/
@@ -10,11 +12,13 @@ const readHeader = (buffer: ByteBuffer) => {
return { typeId, length };
};
export const decode = (byteBuffer: ByteBuffer) => {
const { typeId } = readHeader(byteBuffer);
export const decode: TransportProtocolDecode = bytes => {
const byteBuffer = ByteBuffer.wrap(bytes, undefined, undefined, true);
const { typeId, length } = readHeader(byteBuffer);
return {
typeId,
length,
buffer: byteBuffer,
};
};

View File

@@ -1,15 +1,12 @@
import ByteBuffer from 'bytebuffer';
import { HEADER_SIZE } from '../protocol-v1/constants';
type Options = {
messageType: number;
};
import { TransportProtocolEncode } from '../types';
// this file is basically combination of "trezor v1 protocol" and "bridge protocol"
// there is actually no officially described bridge protocol, but in fact there is one
// it is because bridge does some parts of the protocol itself (like chunking)
export const encode = (data: ByteBuffer, options: Options) => {
export const encode: TransportProtocolEncode = (data, options) => {
const { messageType } = options;
const fullSize = HEADER_SIZE - 2 + data.limit;
@@ -28,5 +25,5 @@ export const encode = (data: ByteBuffer, options: Options) => {
// todo: it would be nicer to return Buffer instead of ByteBuffer. The problem is that ByteBuffer.Buffer.toString behaves differently in web and node.
// anyway, for now we can keep this legacy behavior
return encodedByteBuffer;
return [encodedByteBuffer];
};

View File

@@ -1,11 +1,20 @@
import ByteBuffer from 'bytebuffer';
// Decode `trzd` protocol used for decoding of dynamically loaded `@trezor/protobuf` messages
// https://github.com/trezor/trezor-firmware/blob/087becd2caa5618eecab37ac3f2ca51172e52eb9/docs/common/ethereum-definitions.md#definition-format
export const decode = (bytes: ArrayBuffer) => {
const byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.LE(true); // use little endian byte order
// 5 bytes magic `trzd`
const magic = byteBuffer.readBytes(5).toUTF8();
// 1 byte
const definitionType = byteBuffer.readUint8();
// 4 bytes
const dataVersion = byteBuffer.readUint32();
const protobufLength = byteBuffer.readUint8();
// 2 bytes
const protobufLength = byteBuffer.readUint16();
// N bytes
const protobufPayload = byteBuffer.slice(12, 12 + protobufLength);
return {

View File

@@ -2,6 +2,7 @@ import ByteBuffer from 'bytebuffer';
import * as ERRORS from '../errors';
import { MESSAGE_HEADER_BYTE, MESSAGE_MAGIC_HEADER_BYTE } from './constants';
import { TransportProtocolDecode } from '../types';
/**
* Reads meta information from chunked buffer
@@ -18,7 +19,7 @@ const readHeaderChunked = (buffer: ByteBuffer) => {
// Parses first raw input that comes from Trezor and returns some information about the whole message.
// [compatibility]: accept Buffer just like decode does. But this would require changes in lower levels
export const decodeChunked = (bytes: ArrayBuffer) => {
export const decode: TransportProtocolDecode = bytes => {
// convert to ByteBuffer so it's easier to read
const byteBuffer = ByteBuffer.wrap(bytes, undefined, undefined, true);
@@ -33,5 +34,5 @@ export const decodeChunked = (bytes: ArrayBuffer) => {
throw new Error(ERRORS.PROTOCOL_MALFORMED);
}
return { length, typeId, restBuffer: byteBuffer };
return { length, typeId, buffer: byteBuffer };
};

View File

@@ -6,12 +6,9 @@ import {
BUFFER_SIZE,
MESSAGE_MAGIC_HEADER_BYTE,
} from './constants';
import { TransportProtocolEncode } from '../types';
type Options = {
messageType: number;
};
export const encode = (data: ByteBuffer, options: Options): Buffer[] => {
export const encode: TransportProtocolEncode = (data, options) => {
const { messageType } = options;
const fullSize = HEADER_SIZE + data.limit;
@@ -38,7 +35,7 @@ export const encode = (data: ByteBuffer, options: Options): Buffer[] => {
// size with one reserved byte for header
const result = [];
const result: ByteBuffer[] = [];
// How many pieces will there actually be
// slice and dice
for (let i = 0; i < chunkCount; i++) {
@@ -53,7 +50,7 @@ export const encode = (data: ByteBuffer, options: Options): Buffer[] => {
slice.compact();
buffer.append(slice);
result.push(buffer.buffer);
result.push(buffer);
}
return result;

View File

@@ -0,0 +1,19 @@
export type TransportProtocolDecode = (bytes: ArrayBuffer) => {
length: number;
typeId: number;
buffer: ByteBuffer;
};
export interface TransportProtocolEncodeOptions {
messageType: number;
}
export type TransportProtocolEncode = (
data: ByteBuffer,
options: TransportProtocolEncodeOptions,
) => ByteBuffer[];
export interface TransportProtocol {
encode: TransportProtocolEncode;
decode: TransportProtocolDecode;
}

View File

@@ -7,13 +7,15 @@ describe('protocol-bridge', () => {
let chunks;
// encode small chunk, message without data
chunks = bridge.encode(new ByteBuffer(0), { messageType: 55 });
expect(chunks.limit).toEqual(6);
expect(chunks.length).toEqual(1);
expect(chunks[0].limit).toEqual(6);
// encode big chunk, message with data
chunks = bridge.encode(new ByteBuffer(371), { messageType: 55 });
expect(chunks.slice(0, 6).toString('hex')).toEqual('003700000173');
expect(chunks.readUint32(2)).toEqual(371);
expect(chunks.buffer.length).toEqual(371 + 6);
expect(chunks.length).toEqual(1);
expect(chunks[0].slice(0, 6).toString('hex')).toEqual('003700000173');
expect(chunks[0].readUint32(2)).toEqual(371);
expect(chunks[0].buffer.length).toEqual(371 + 6);
});
it('decode', () => {
@@ -22,7 +24,8 @@ describe('protocol-bridge', () => {
data.fill(getFeatures, 0, 2);
data.writeUint32BE(379, 2);
const read = bridge.decode(ByteBuffer.fromHex(data.toString('hex')));
const read = bridge.decode(data);
expect(read.typeId).toEqual(55);
expect(read.length).toEqual(379);
});
});

View File

@@ -8,17 +8,17 @@ describe('protocol-v1', () => {
// encode only one chunk, message without data
chunks = v1.encode(new ByteBuffer(0), { messageType: 55 });
expect(chunks.length).toEqual(1);
expect(chunks[0].length).toEqual(64);
expect(chunks[0].limit).toEqual(64);
// encode multiple chunks, message with data
chunks = v1.encode(new ByteBuffer(371), { messageType: 55 });
expect(chunks.length).toEqual(7);
chunks.forEach((chunk, index) => {
expect(chunk.length).toEqual(64);
expect(chunk.limit).toEqual(64);
if (index === 0) {
// first chunk with additional data
expect(chunk.slice(0, 9).toString('hex')).toEqual('3f2323003700000173');
expect(chunk.readUInt32BE(5)).toEqual(371);
expect(chunk.readUint32(5)).toEqual(371);
} else {
// following chunk starts with MESSAGE_MAGIC_HEADER_BYTE
expect(chunk.slice(0, 5).toString('hex')).toEqual('3f00000000');
@@ -32,7 +32,7 @@ describe('protocol-v1', () => {
data.fill(getFeatures, 0, 5);
data.writeUint32BE(379, 5);
const read = v1.decodeChunked(data);
const read = v1.decode(data);
expect(read.typeId).toEqual(55);
expect(read.length).toEqual(379);
});

View File

@@ -1,8 +1,9 @@
import { createDeferred, Deferred } from '@trezor/utils';
import { v1 as v1Protocol } from '@trezor/protocol';
import { AbstractTransport, AbstractTransportParams, AcquireInput, ReleaseInput } from './abstract';
import { AbstractApi } from '../api/abstract';
import { buildAndSend } from '../utils/send';
import { buildBuffers } from '../utils/send';
import { receiveAndParse } from '../utils/receive';
import { SessionsClient } from '../sessions/client';
import * as ERRORS from '../errors';
@@ -192,26 +193,25 @@ export abstract class AbstractApiTransport extends AbstractTransport {
const { path } = getPathBySessionResponse.payload;
try {
await buildAndSend(
this.messages,
(buffer: Buffer) =>
this.api.write(path, buffer).then(result => {
if (!result.success) {
// todo:
throw new Error(result.error);
}
}),
name,
data,
);
const message = await receiveAndParse(this.messages, () =>
this.api.read(path).then(result => {
if (result.success) {
return result.payload;
const buffers = buildBuffers(this.messages, name, data, v1Protocol.encode);
for (const chunk of buffers) {
this.api.write(path, chunk.buffer).then(result => {
if (!result.success) {
throw new Error(result.error);
}
throw new Error(result.error);
}),
});
}
const message = await receiveAndParse(
this.messages,
() =>
this.api.read(path).then(result => {
if (result.success) {
return result.payload;
}
throw new Error(result.error);
}),
v1Protocol.decode,
);
return this.success(message);
@@ -252,17 +252,14 @@ export abstract class AbstractApiTransport extends AbstractTransport {
const { path } = getPathBySessionResponse.payload;
try {
await buildAndSend(
this.messages,
(buffer: Buffer) =>
this.api.write(path, buffer).then(result => {
if (!result.success) {
throw new Error(result.error);
}
}),
name,
data,
);
const buffers = buildBuffers(this.messages, name, data, v1Protocol.encode);
for (const chunk of buffers) {
this.api.write(path, chunk.buffer).then(result => {
if (!result.success) {
throw new Error(result.error);
}
});
}
return this.success(undefined);
} catch (err) {
if (err.message === ERRORS.DEVICE_DISCONNECTED_DURING_ACTION) {
@@ -285,13 +282,16 @@ export abstract class AbstractApiTransport extends AbstractTransport {
const { path } = getPathBySessionResponse.payload;
try {
const message = await receiveAndParse(this.messages, () =>
this.api.read(path).then(result => {
if (!result.success) {
throw new Error(result.error);
}
return result.payload;
}),
const message = await receiveAndParse(
this.messages,
() =>
this.api.read(path).then(result => {
if (!result.success) {
throw new Error(result.error);
}
return result.payload;
}),
v1Protocol.decode,
);
return this.success(message);

View File

@@ -1,9 +1,9 @@
import { versionUtils, createDeferred, Deferred, createTimeoutPromise } from '@trezor/utils';
import { PROTOCOL_MALFORMED } from '@trezor/protocol';
import { PROTOCOL_MALFORMED, bridge as bridgeProtocol } from '@trezor/protocol';
import { bridgeApiCall } from '../utils/bridgeApiCall';
import * as bridgeApiResult from '../utils/bridgeApiResult';
import { buildOne } from '../utils/send';
import { receiveOne } from '../utils/receive';
import { buildBuffers } from '../utils/send';
import { receiveAndParse } from '../utils/receive';
import { AbstractTransport, AbstractTransportParams, AcquireInput, ReleaseInput } from './abstract';
import * as ERRORS from '../errors';
@@ -228,19 +228,21 @@ export class BridgeTransport extends AbstractTransport {
}) {
return this.scheduleAction(
async signal => {
const { messages } = this;
const o = buildOne(messages, name, data);
const outData = o.toString('hex');
const [bytes] = buildBuffers(this.messages, name, data, bridgeProtocol.encode);
const response = await this._post(`/call`, {
params: session,
body: outData,
body: bytes.toString('hex'),
signal,
});
if (!response.success) {
return response;
}
const jsonData = receiveOne(this.messages, response.payload);
return this.success(jsonData);
const message = await receiveAndParse(
this.messages,
() => Promise.resolve(Buffer.from(response.payload, 'hex')),
bridgeProtocol.decode,
);
return this.success(message);
},
{ timeout: undefined },
);
@@ -256,12 +258,10 @@ export class BridgeTransport extends AbstractTransport {
name: string;
}) {
return this.scheduleAction(async signal => {
const { messages } = this;
const outData = buildOne(messages, name, data).toString('hex');
const [bytes] = buildBuffers(this.messages, name, data, bridgeProtocol.encode);
const response = await this._post('/post', {
params: session,
body: outData,
body: bytes.toString('hex'),
signal,
});
if (!response.success) {
@@ -275,16 +275,18 @@ export class BridgeTransport extends AbstractTransport {
return this.scheduleAction(async signal => {
const response = await this._post('/read', {
params: session,
signal,
});
if (!response.success) {
return response;
}
const jsonData = receiveOne(this.messages, response.payload);
return this.success(jsonData);
const message = await receiveAndParse(
this.messages,
() => Promise.resolve(Buffer.from(response.payload, 'hex')),
bridgeProtocol.decode,
);
return this.success(message);
});
}

View File

@@ -2,19 +2,7 @@ import ByteBuffer from 'bytebuffer';
import { Root } from 'protobufjs/light';
import { decode as decodeProtobuf, createMessageFromType } from '@trezor/protobuf';
import { v1 as protocolV1, bridge as bridgeProtocol } from '@trezor/protocol';
export function receiveOne(messages: Root, data: string) {
const bytebuffer = ByteBuffer.wrap(data, 'hex');
const { typeId, buffer } = bridgeProtocol.decode(bytebuffer);
const { Message, messageName } = createMessageFromType(messages, typeId);
const message = decodeProtobuf(Message, buffer);
return {
message,
type: messageName,
};
}
import { TransportProtocolDecode } from '@trezor/protocol';
async function receiveRest(
parsedInput: ByteBuffer,
@@ -34,20 +22,27 @@ async function receiveRest(
return receiveRest(parsedInput, receiver, expectedLength);
}
async function receiveBuffer(receiver: () => Promise<ArrayBuffer>) {
async function receiveBuffer(
receiver: () => Promise<ArrayBuffer>,
decoder: TransportProtocolDecode,
) {
const data = await receiver();
const { length, typeId, restBuffer } = protocolV1.decodeChunked(data);
const { length, typeId, buffer } = decoder(data);
const decoded = new ByteBuffer(length);
if (length) {
decoded.append(restBuffer);
decoded.append(buffer);
}
await receiveRest(decoded, receiver, length);
return { received: decoded, typeId };
}
export async function receiveAndParse(messages: Root, receiver: () => Promise<ArrayBuffer>) {
const { received, typeId } = await receiveBuffer(receiver);
export async function receiveAndParse(
messages: Root,
receiver: () => Promise<ArrayBuffer>,
decoder: TransportProtocolDecode,
) {
const { received, typeId } = await receiveBuffer(receiver, decoder);
const { Message, messageName } = createMessageFromType(messages, typeId);
received.reset();
const message = decodeProtobuf(Message, received);

View File

@@ -3,42 +3,17 @@
// Logic of "call" is broken to two parts - sending and receiving
import { Root } from 'protobufjs/light';
import { encode as encodeProtobuf, createMessageFromName } from '@trezor/protobuf';
import { v1 as protocolV1, bridge as bridgeProtocol } from '@trezor/protocol';
import { TransportProtocolEncode } from '@trezor/protocol';
// Sends more buffers to device.
async function sendBuffers(sender: (data: Buffer) => Promise<void>, buffers: Array<Buffer>) {
for (const buffer of buffers) {
await sender(buffer);
}
}
// Sends message to device.
// Resolves if everything gets sent
export function buildOne(messages: Root, name: string, data: Record<string, unknown>) {
const { Message, messageType } = createMessageFromName(messages, name);
const buffer = encodeProtobuf(Message, data);
return bridgeProtocol.encode(buffer, {
messageType,
});
}
export const buildBuffers = (messages: Root, name: string, data: Record<string, unknown>) => {
export const buildBuffers = (
messages: Root,
name: string,
data: Record<string, unknown>,
encoder: TransportProtocolEncode,
) => {
const { Message, messageType } = createMessageFromName(messages, name);
const buffer = encodeProtobuf(Message, data);
return protocolV1.encode(buffer, {
return encoder(buffer, {
messageType,
});
};
// Sends message to device.
// Resolves if everything gets sent
export function buildAndSend(
messages: Root,
sender: (data: Buffer) => Promise<void>,
name: string,
data: Record<string, unknown>,
) {
const buffers = buildBuffers(messages, name, data);
return sendBuffers(sender, buffers);
}

View File

@@ -1,7 +1,7 @@
import * as protobuf from 'protobufjs/light';
import { buildOne, buildBuffers } from '../src/utils/send';
import { receiveOne, receiveAndParse } from '../src/utils/receive';
import { v1 as v1Protocol, bridge as bridgeProtocol } from '@trezor/protocol';
import { buildBuffers } from '../src/utils/send';
import { receiveAndParse } from '../src/utils/receive';
const messages = {
StellarPaymentOp: {
@@ -96,25 +96,44 @@ const parsedMessages = protobuf.Root.fromJSON({
describe('encoding json -> protobuf -> json', () => {
fixtures.forEach(f => {
describe(`${f.name} - payload length ${f.in.source_account.length}`, () => {
test('buildOne - receiveOne', () => {
// encoded message
const encodedMessage = buildOne(parsedMessages, f.name, f.in);
test('bridgeProtocol: buildBuffers - receiveAndParse', async () => {
const result = buildBuffers(parsedMessages, f.name, f.in, bridgeProtocol.encode);
// bridgeProtocol returns only one big chunk
expect(result.length).toBe(1);
const [chunk] = result;
const { length } = Buffer.from(f.in.source_account);
// chunk length cannot be less than message header/constant (28) + variable source_account length
// additional bytes are expected (encoded Uint32) if message length is greater
expect(chunk.buffer.length).toBeGreaterThanOrEqual(28 + length);
let i = -1;
const decoded = await receiveAndParse(
parsedMessages,
() => {
i++;
return Promise.resolve(result[i].buffer);
},
bridgeProtocol.decode,
);
// then decode message and check, whether decoded message matches original json
const decodedMessage = receiveOne(parsedMessages, encodedMessage.toString('hex'));
expect(decodedMessage.type).toEqual(f.name);
expect(decodedMessage.message).toEqual(f.in);
expect(decoded.type).toEqual(f.name);
expect(decoded.message).toEqual(f.in);
});
test('buildBuffers - receiveAndParse', async () => {
const result = buildBuffers(parsedMessages, f.name, f.in);
result.forEach(r => {
expect(r.byteLength).toBeLessThanOrEqual(64);
test('v1Protocol: buildBuffers - receiveAndParse', async () => {
const result = buildBuffers(parsedMessages, f.name, f.in, v1Protocol.encode);
// each protocol chunks are equal 64 bytes
result.forEach(chunk => {
expect(chunk.buffer.length).toEqual(64);
});
let i = -1;
const decoded = await receiveAndParse(parsedMessages, () => {
i++;
return Promise.resolve(result[i]);
});
const decoded = await receiveAndParse(
parsedMessages,
() => {
i++;
return Promise.resolve(result[i].buffer);
},
v1Protocol.decode,
);
// then decode message and check, whether decoded message matches original json
expect(decoded.type).toEqual(f.name);
expect(decoded.message).toEqual(f.in);