diff --git a/packages/protocol/README.md b/packages/protocol/README.md new file mode 100644 index 0000000000..9334334958 --- /dev/null +++ b/packages/protocol/README.md @@ -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 diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index c3daa969c8..0525133931 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -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'; diff --git a/packages/protocol/src/protocol-bridge/decode.ts b/packages/protocol/src/protocol-bridge/decode.ts index c11605485b..4e985dc810 100644 --- a/packages/protocol/src/protocol-bridge/decode.ts +++ b/packages/protocol/src/protocol-bridge/decode.ts @@ -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), diff --git a/packages/protocol/src/protocol-v1/decode.ts b/packages/protocol/src/protocol-v1/decode.ts index 160d084b24..7a5303e8be 100644 --- a/packages/protocol/src/protocol-v1/decode.ts +++ b/packages/protocol/src/protocol-v1/decode.ts @@ -40,6 +40,7 @@ export const decode: TransportProtocolDecode = bytes => { } return { + header: bytes.subarray(0, 3), length, messageType, payload: bytes.subarray(HEADER_SIZE), diff --git a/packages/protocol/src/protocol-v2/constants.ts b/packages/protocol/src/protocol-v2/constants.ts new file mode 100644 index 0000000000..c6b60cf9f6 --- /dev/null +++ b/packages/protocol/src/protocol-v2/constants.ts @@ -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'; diff --git a/packages/protocol/src/protocol-v2/decode.ts b/packages/protocol/src/protocol-v2/decode.ts new file mode 100644 index 0000000000..db43834ae3 --- /dev/null +++ b/packages/protocol/src/protocol-v2/decode.ts @@ -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), + }; +}; diff --git a/packages/protocol/src/protocol-v2/encode.ts b/packages/protocol/src/protocol-v2/encode.ts new file mode 100644 index 0000000000..ba0b8219d2 --- /dev/null +++ b/packages/protocol/src/protocol-v2/encode.ts @@ -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}`); +}; diff --git a/packages/protocol/src/protocol-v2/index.ts b/packages/protocol/src/protocol-v2/index.ts new file mode 100644 index 0000000000..7d0ed1fa32 --- /dev/null +++ b/packages/protocol/src/protocol-v2/index.ts @@ -0,0 +1,3 @@ +export * from './decode'; +export * from './encode'; +export const name = 'v2'; diff --git a/packages/protocol/src/types.ts b/packages/protocol/src/types.ts index ecb9a7fb19..505e48afdb 100644 --- a/packages/protocol/src/types.ts +++ b/packages/protocol/src/types.ts @@ -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; diff --git a/packages/protocol/tests/protocol-v2.test.ts b/packages/protocol/tests/protocol-v2.test.ts new file mode 100644 index 0000000000..c4b98f48df --- /dev/null +++ b/packages/protocol/tests/protocol-v2.test.ts @@ -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'); + }); +});