mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-06 23:39:38 +01:00
chore(transport): reorganize protocol related logic
This commit is contained in:
@@ -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';
|
||||
|
||||
20
packages/protocol/src/protocol-bridge/decode.ts
Normal file
20
packages/protocol/src/protocol-bridge/decode.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
32
packages/protocol/src/protocol-bridge/encode.ts
Normal file
32
packages/protocol/src/protocol-bridge/encode.ts
Normal 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;
|
||||
};
|
||||
2
packages/protocol/src/protocol-bridge/index.ts
Normal file
2
packages/protocol/src/protocol-bridge/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './decode';
|
||||
export * from './encode';
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
58
packages/transport/src/utils/receive.ts
Normal file
58
packages/transport/src/utils/receive.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
44
packages/transport/src/utils/send.ts
Normal file
44
packages/transport/src/utils/send.ts
Normal 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);
|
||||
}
|
||||
@@ -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, () => {
|
||||
|
||||
Reference in New Issue
Block a user