feat(protocol): add protocol-v2

This commit is contained in:
Szymon Lesisz
2024-08-28 11:55:13 +02:00
committed by martin
parent e120f78be4
commit 7ff4f19571
10 changed files with 168 additions and 1 deletions

View File

@@ -0,0 +1,48 @@
# @trezor/protocol
Library for decoding and encoding messages from/to Trezor
## protocol-bridge
Message format:
```
| 2 bytes | |
| protobuf_message_type | protobuf_message_payload |
```
## protocol-v1
Message format:
```
| 3 bytes | 2 bytes | 2 bytes | `len` - 2 bytes |
| magic | magic | magic | len | len | protobuf_message_type | protobuf_message_payload |
```
Continuation packet format (chunks):
```
| 1 byte | |
| magic | protobuf_message_chunk |
```
## protocol-v2 (TrezorHostProtocol)
Message format:
```
| 1 byte | 2 bytes | 2 bytes | `len` including 4 bytes crc |
| control_byte | channel | channel | len | len | thp_payload + crc |
```
Continuation packet format (chunks):
```
| 1 byte | 2 bytes | |
| continuation_packet | channel | channel | payload_chunk |
```
## protocol-trzd
Decode loaded `@trezor/protobuf` messages

View File

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

View File

@@ -17,6 +17,7 @@ export const decode: TransportProtocolDecode = bytes => {
const { messageType, length } = readHeader(bytes);
return {
header: Buffer.alloc(0), // bridge does not return header
messageType,
length,
payload: bytes.subarray(HEADER_SIZE),

View File

@@ -40,6 +40,7 @@ export const decode: TransportProtocolDecode = bytes => {
}
return {
header: bytes.subarray(0, 3),
length,
messageType,
payload: bytes.subarray(HEADER_SIZE),

View File

@@ -0,0 +1,3 @@
export const HEADER_SIZE = 1 + 2; // 1: control_byte + 2: channel
export const MESSAGE_LEN_SIZE = 2;
export const MESSAGE_TYPE = 'TrezorHostProtocolMessage';

View File

@@ -0,0 +1,22 @@
import * as ERRORS from '../errors';
import { HEADER_SIZE, MESSAGE_LEN_SIZE, MESSAGE_TYPE } from './constants';
import { getChunkHeader } from './encode';
import { TransportProtocolDecode } from '../types';
// Parses raw input from Trezor and returns some information about the whole message
export const decode: TransportProtocolDecode = bytes => {
const buffer = Buffer.from(bytes);
// chunk should have at least 5 bytes. 3 bytes `header` + 2 bytes `length`
if (buffer.byteLength < HEADER_SIZE + MESSAGE_LEN_SIZE) {
throw new Error(ERRORS.PROTOCOL_MALFORMED);
}
return {
header: buffer.subarray(0, HEADER_SIZE),
chunkHeader: getChunkHeader(buffer),
length: buffer.readUint16BE(HEADER_SIZE),
messageType: MESSAGE_TYPE, // will be decoded by `protocol-thp`
payload: buffer.subarray(HEADER_SIZE + MESSAGE_LEN_SIZE),
};
};

View File

@@ -0,0 +1,33 @@
import * as ERRORS from '../errors';
import { HEADER_SIZE, MESSAGE_LEN_SIZE, MESSAGE_TYPE } from './constants';
import { TransportProtocolEncode } from '../types';
export const getChunkHeader = (data: Buffer) => {
// data should have at least 1 control_byte + 2 bytes channel
if (data.byteLength < HEADER_SIZE) {
throw new Error(ERRORS.PROTOCOL_MALFORMED);
}
const channel = data.subarray(1, HEADER_SIZE);
const header = Buffer.concat([Buffer.from([0x80]), channel]); // THP_CONTINUATION_PACKET
return header;
};
// encode `protocol-thp` message
export const encode: TransportProtocolEncode = (data, options) => {
if (options.messageType === MESSAGE_TYPE) {
if (!options.header || options.header.byteLength !== HEADER_SIZE) {
throw new Error(
`${options.messageType} unexpected header ${options.header?.toString('hex')}`,
);
}
const length = Buffer.alloc(MESSAGE_LEN_SIZE);
length.writeUInt16BE(data.length);
return Buffer.concat([options.header, length, data]);
}
throw new Error(`Use protocol-thp.encode for messageType ${options.messageType}`);
};

View File

@@ -0,0 +1,3 @@
export * from './decode';
export * from './encode';
export const name = 'v2';

View File

@@ -1,4 +1,5 @@
export type TransportProtocolDecode = (bytes: Buffer) => {
header: Buffer;
length: number;
messageType: number | string;
payload: Buffer;
@@ -6,6 +7,7 @@ export type TransportProtocolDecode = (bytes: Buffer) => {
export interface TransportProtocolEncodeOptions {
messageType: number | string;
header?: Buffer;
}
export type TransportProtocolEncode = (
@@ -14,7 +16,7 @@ export type TransportProtocolEncode = (
) => Buffer;
export interface TransportProtocol {
name: 'bridge' | 'v1';
name: 'bridge' | 'v1' | 'v2';
encode: TransportProtocolEncode;
decode: TransportProtocolDecode;
getChunkHeader: (data: Buffer) => Buffer;

View File

@@ -0,0 +1,53 @@
import { decode, encode, getChunkHeader } from '../src/protocol-v2';
describe('protocol-v2', () => {
it('encode/decode TrezorHostProtocolMessage', () => {
// ThpCreateNewSession message
const data = Buffer.from(
'04123800230003e80a0870617373313233341000a0a1a2a3a4a5a6a7a8a9b0b1b2b3b4b504db712b',
'hex',
);
const decoded = decode(data);
expect(decoded.messageType).toEqual('TrezorHostProtocolMessage');
expect(decoded.header).toEqual(data.subarray(0, 3));
expect(decoded.length).toEqual(35);
expect(decoded.payload).toEqual(data.subarray(5, 5 + 35));
const encoded = encode(decoded.payload, decoded);
expect(encoded).toEqual(data);
});
it('encode with error', () => {
expect(() => encode(Buffer.alloc(0), { messageType: 1 })).toThrow(
'Use protocol-thp.encode',
);
expect(() => encode(Buffer.alloc(0), { messageType: 'TrezorHostProtocolMessage' })).toThrow(
'unexpected header undefined',
);
expect(() =>
encode(Buffer.alloc(0), {
messageType: 'TrezorHostProtocolMessage',
header: Buffer.alloc(1),
}),
).toThrow('unexpected header 00');
expect(() =>
encode(Buffer.alloc(0), {
messageType: 'TrezorHostProtocolMessage',
header: Buffer.alloc(4),
}),
).toThrow('unexpected header 00000000');
});
it('decode with error', () => {
expect(() => decode(Buffer.alloc(0))).toThrow('Malformed protocol format');
});
it('getChunkHeader', () => {
expect(getChunkHeader(Buffer.from('0412380000', 'hex'))).toEqual(
Buffer.from('801238', 'hex'),
);
// with error
expect(() => getChunkHeader(Buffer.alloc(0))).toThrow('Malformed protocol format');
});
});