chore(transport): reorganize protocol related logic

This commit is contained in:
Martin Varmuza
2023-08-07 14:00:36 +02:00
committed by martin
parent fb73caa39e
commit cbabe2e2c5
14 changed files with 211 additions and 79 deletions

View File

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

View File

@@ -0,0 +1,20 @@
import ByteBuffer from 'bytebuffer';
/**
* Reads meta information from buffer
*/
const readHeader = (buffer: ByteBuffer) => {
const typeId = buffer.readUint16();
const length = buffer.readUint32();
return { typeId, length };
};
export const decode = (byteBuffer: ByteBuffer) => {
const { typeId } = readHeader(byteBuffer);
return {
typeId,
buffer: byteBuffer,
};
};

View File

@@ -0,0 +1,32 @@
import ByteBuffer from 'bytebuffer';
import { HEADER_SIZE } from '../protocol-v1/constants';
type Options = {
messageType: number;
};
// 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) => {
const { messageType } = options;
const fullSize = HEADER_SIZE - 2 + data.limit;
const encodedByteBuffer = new ByteBuffer(fullSize);
// 2 bytes
encodedByteBuffer.writeUint16(messageType);
// 4 bytes
encodedByteBuffer.writeUint32(data.limit);
// then put in the actual message
encodedByteBuffer.append(data.buffer);
encodedByteBuffer.reset();
// 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;
};

View File

@@ -0,0 +1,2 @@
export * from './decode';
export * from './encode';

View File

@@ -1,3 +1,4 @@
export const MESSAGE_MAGIC_HEADER_BYTE = 63;
export const MESSAGE_HEADER_BYTE = 0x23;
export const HEADER_SIZE = 1 + 1 + 4 + 2;
export const BUFFER_SIZE = 63;
export const BUFFER_SIZE = 64;

View File

@@ -1,37 +1,19 @@
import ByteBuffer from 'bytebuffer';
import * as ERRORS from '../errors';
import { MESSAGE_HEADER_BYTE } from './constants';
/**
* Reads meta information from buffer
*/
const readHeader = (buffer: ByteBuffer) => {
const typeId = buffer.readUint16();
const length = buffer.readUint32();
return { typeId, length };
};
import { MESSAGE_HEADER_BYTE, MESSAGE_MAGIC_HEADER_BYTE } from './constants';
/**
* Reads meta information from chunked buffer
*/
const readHeaderChunked = (buffer: ByteBuffer) => {
const magic = buffer.readByte();
const sharp1 = buffer.readByte();
const sharp2 = buffer.readByte();
const typeId = buffer.readUint16();
const length = buffer.readUint32();
return { sharp1, sharp2, typeId, length };
};
export const decode = (byteBuffer: ByteBuffer) => {
const { typeId } = readHeader(byteBuffer);
return {
typeId,
buffer: byteBuffer,
};
return { magic, sharp1, sharp2, typeId, length };
};
// Parses first raw input that comes from Trezor and returns some information about the whole message.
@@ -40,9 +22,13 @@ export const decodeChunked = (bytes: ArrayBuffer) => {
// convert to ByteBuffer so it's easier to read
const byteBuffer = ByteBuffer.wrap(bytes, undefined, undefined, true);
const { sharp1, sharp2, typeId, length } = readHeaderChunked(byteBuffer);
const { magic, sharp1, sharp2, typeId, length } = readHeaderChunked(byteBuffer);
if (sharp1 !== MESSAGE_HEADER_BYTE || sharp2 !== MESSAGE_HEADER_BYTE) {
if (
magic !== MESSAGE_MAGIC_HEADER_BYTE ||
sharp1 !== MESSAGE_HEADER_BYTE ||
sharp2 !== MESSAGE_HEADER_BYTE
) {
// read-write is out of sync
throw new Error(ERRORS.PROTOCOL_MALFORMED);
}

View File

@@ -1,26 +1,25 @@
import ByteBuffer from 'bytebuffer';
import { HEADER_SIZE, MESSAGE_HEADER_BYTE, BUFFER_SIZE } from './constants';
import {
HEADER_SIZE,
MESSAGE_HEADER_BYTE,
BUFFER_SIZE,
MESSAGE_MAGIC_HEADER_BYTE,
} from './constants';
type Options<Chunked> = {
chunked: Chunked;
addTrezorHeaders: boolean;
type Options = {
messageType: number;
};
function encode(data: ByteBuffer, options: Options<true>): Buffer[];
function encode(data: ByteBuffer, options: Options<false>): Buffer;
function encode(data: any, options: any): any {
const { addTrezorHeaders, chunked, messageType } = options;
const fullSize = (addTrezorHeaders ? HEADER_SIZE : HEADER_SIZE - 2) + data.limit;
export const encode = (data: ByteBuffer, options: Options): Buffer[] => {
const { messageType } = options;
const fullSize = HEADER_SIZE + data.limit;
const encodedByteBuffer = new ByteBuffer(fullSize);
if (addTrezorHeaders) {
// 2*1 byte
encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
}
// 2*1 byte
encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE);
// 2 bytes
encodedByteBuffer.writeUint16(messageType);
@@ -33,26 +32,29 @@ function encode(data: any, options: any): any {
encodedByteBuffer.reset();
if (chunked === false) {
return encodedByteBuffer;
}
const size = BUFFER_SIZE - 1;
const result: Buffer[] = [];
const size = BUFFER_SIZE;
const chunkCount = Math.ceil(encodedByteBuffer.limit / size) || 1;
// size with one reserved byte for header
const result = [];
// How many pieces will there actually be
const count = Math.floor((encodedByteBuffer.limit - 1) / size) + 1 || 1;
// slice and dice
for (let i = 0; i < count; i++) {
for (let i = 0; i < chunkCount; i++) {
const start = i * size;
const end = Math.min((i + 1) * size, encodedByteBuffer.limit);
const buffer = new ByteBuffer(BUFFER_SIZE);
buffer.writeByte(MESSAGE_MAGIC_HEADER_BYTE);
const slice = encodedByteBuffer.slice(start, end);
slice.compact();
result.push(slice.buffer);
buffer.append(slice);
result.push(buffer.buffer);
}
return result;
}
export { encode };
};

View File

@@ -1,8 +1,3 @@
// protocol const
export const MESSAGE_HEADER_BYTE = 0x23;
export const HEADER_SIZE = 1 + 1 + 4 + 2;
export const BUFFER_SIZE = 63;
// usb const
export const CONFIGURATION_ID = 1;
export const INTERFACE_ID = 0;

View File

@@ -113,11 +113,7 @@ export class UsbInterface extends AbstractInterface {
return this.error({ error: ERRORS.INTERFACE_DATA_TRANSFER });
}
if (res.data.byteLength === 0) {
return this.read(path);
}
return this.success(res.data.buffer.slice(1));
return this.success(res.data.buffer);
} catch (err) {
if (err.message === INTERFACE_DEVICE_DISCONNECTED) {
return this.error({ error: ERRORS.DEVICE_DISCONNECTED_DURING_ACTION });
@@ -133,8 +129,7 @@ export class UsbInterface extends AbstractInterface {
}
const newArray = new Uint8Array(64);
newArray[0] = 63;
newArray.set(new Uint8Array(buffer), 1);
newArray.set(new Uint8Array(buffer));
try {
// https://wicg.github.io/webusb/#ref-for-dom-usbdevice-transferout

View File

@@ -2,12 +2,12 @@ import ByteBuffer from 'bytebuffer';
import { Root } from 'protobufjs/light';
import { decode as decodeProtobuf, createMessageFromType } from '@trezor/protobuf';
import { v1 as protocolV1 } from '@trezor/protocol';
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 } = protocolV1.decode(bytebuffer);
const { typeId, buffer } = bridgeProtocol.decode(bytebuffer);
const { Message, messageName } = createMessageFromType(messages, typeId);
const message = decodeProtobuf(Message, buffer);
return {
@@ -29,8 +29,7 @@ async function receiveRest(
if (data == null) {
throw new Error('Received no data.');
}
parsedInput.append(data);
parsedInput.append(data.slice(1));
return receiveRest(parsedInput, receiver, expectedLength);
}
@@ -39,6 +38,7 @@ async function receiveBuffer(receiver: () => Promise<ArrayBuffer>) {
const data = await receiver();
const { length, typeId, restBuffer } = protocolV1.decodeChunked(data);
const decoded = new ByteBuffer(length);
if (length) {
decoded.append(restBuffer);
}

View File

@@ -3,7 +3,7 @@
// 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 } from '@trezor/protocol';
import { v1 as protocolV1, bridge as bridgeProtocol } from '@trezor/protocol';
// Sends more buffers to device.
async function sendBuffers(sender: (data: Buffer) => Promise<void>, buffers: Array<Buffer>) {
@@ -18,9 +18,7 @@ export function buildOne(messages: Root, name: string, data: Record<string, unkn
const { Message, messageType } = createMessageFromName(messages, name);
const buffer = encodeProtobuf(Message, data);
return protocolV1.encode(buffer, {
addTrezorHeaders: false,
chunked: false,
return bridgeProtocol.encode(buffer, {
messageType,
});
}
@@ -29,8 +27,6 @@ export const buildBuffers = (messages: Root, name: string, data: Record<string,
const { Message, messageType } = createMessageFromName(messages, name);
const buffer = encodeProtobuf(Message, data);
return protocolV1.encode(buffer, {
addTrezorHeaders: true,
chunked: true,
messageType,
});
};

View File

@@ -0,0 +1,58 @@
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,
};
}
async function receiveRest(
parsedInput: ByteBuffer,
receiver: () => Promise<ArrayBuffer>,
expectedLength: number,
): Promise<void> {
if (parsedInput.offset >= expectedLength) {
return;
}
const data = await receiver();
// sanity check
if (data == null) {
throw new Error('Received no data.');
}
parsedInput.append(data.slice(1));
return receiveRest(parsedInput, receiver, expectedLength);
}
async function receiveBuffer(receiver: () => Promise<ArrayBuffer>) {
const data = await receiver();
const { length, typeId, restBuffer } = protocolV1.decodeChunked(data);
const decoded = new ByteBuffer(length);
if (length) {
decoded.append(restBuffer);
}
await receiveRest(decoded, receiver, length);
return { received: decoded, typeId };
}
export async function receiveAndParse(messages: Root, receiver: () => Promise<ArrayBuffer>) {
const { received, typeId } = await receiveBuffer(receiver);
const { Message, messageName } = createMessageFromType(messages, typeId);
received.reset();
const message = decodeProtobuf(Message, received);
return {
message,
type: messageName,
};
}

View File

@@ -0,0 +1,44 @@
// Logic of sending data to trezor
//
// 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';
// 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>) => {
const { Message, messageType } = createMessageFromName(messages, name);
const buffer = encodeProtobuf(Message, data);
return protocolV1.encode(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

@@ -73,11 +73,12 @@ const messages = {
},
};
const fixtures = [
{
const fixtures = Array(100)
.fill(undefined)
.map((_u, i) => ({
name: 'StellarPaymentOp',
in: {
source_account: 'meow'.repeat(100), // make message longer then 63 bytes
source_account: 'm'.repeat(13 * i), // make message longer then 64 bytes
destination_account: 'wuff',
asset: {
type: 'NATIVE',
@@ -86,8 +87,7 @@ const fixtures = [
},
amount: 10,
},
},
];
}));
const parsedMessages = protobuf.Root.fromJSON({
nested: { hw: { nested: { trezor: { nested: { messages: { nested: messages } } } } } },
@@ -95,7 +95,7 @@ const parsedMessages = protobuf.Root.fromJSON({
describe('encoding json -> protobuf -> json', () => {
fixtures.forEach(f => {
describe(f.name, () => {
describe(`${f.name} - payload length ${f.in.source_account.length}`, () => {
test('buildOne - receiveOne', () => {
// encoded message
const encodedMessage = buildOne(parsedMessages, f.name, f.in);
@@ -108,7 +108,7 @@ describe('encoding json -> protobuf -> json', () => {
test('buildBuffers - receiveAndParse', async () => {
const result = buildBuffers(parsedMessages, f.name, f.in);
result.forEach(r => {
expect(r.byteLength).toBeLessThanOrEqual(63);
expect(r.byteLength).toBeLessThanOrEqual(64);
});
let i = -1;
const decoded = await receiveAndParse(parsedMessages, () => {