diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 7e3626e3a3..623fe8bf79 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -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'; diff --git a/packages/protocol/src/protocol-bridge/decode.ts b/packages/protocol/src/protocol-bridge/decode.ts new file mode 100644 index 0000000000..5dce283977 --- /dev/null +++ b/packages/protocol/src/protocol-bridge/decode.ts @@ -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, + }; +}; diff --git a/packages/protocol/src/protocol-bridge/encode.ts b/packages/protocol/src/protocol-bridge/encode.ts new file mode 100644 index 0000000000..d64f4be09d --- /dev/null +++ b/packages/protocol/src/protocol-bridge/encode.ts @@ -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; +}; diff --git a/packages/protocol/src/protocol-bridge/index.ts b/packages/protocol/src/protocol-bridge/index.ts new file mode 100644 index 0000000000..6267e16e3d --- /dev/null +++ b/packages/protocol/src/protocol-bridge/index.ts @@ -0,0 +1,2 @@ +export * from './decode'; +export * from './encode'; diff --git a/packages/protocol/src/protocol-v1/constants.ts b/packages/protocol/src/protocol-v1/constants.ts index 9ac6534bba..919b412d0c 100644 --- a/packages/protocol/src/protocol-v1/constants.ts +++ b/packages/protocol/src/protocol-v1/constants.ts @@ -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; diff --git a/packages/protocol/src/protocol-v1/decode.ts b/packages/protocol/src/protocol-v1/decode.ts index 9d669dccbc..781903c210 100644 --- a/packages/protocol/src/protocol-v1/decode.ts +++ b/packages/protocol/src/protocol-v1/decode.ts @@ -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); } diff --git a/packages/protocol/src/protocol-v1/encode.ts b/packages/protocol/src/protocol-v1/encode.ts index 517fc1342b..cf07017066 100644 --- a/packages/protocol/src/protocol-v1/encode.ts +++ b/packages/protocol/src/protocol-v1/encode.ts @@ -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; - addTrezorHeaders: boolean; +type Options = { messageType: number; }; -function encode(data: ByteBuffer, options: Options): Buffer[]; -function encode(data: ByteBuffer, options: Options): 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 }; +}; diff --git a/packages/transport/src/constants.ts b/packages/transport/src/constants.ts index 44bacae3f9..9b69974097 100644 --- a/packages/transport/src/constants.ts +++ b/packages/transport/src/constants.ts @@ -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; diff --git a/packages/transport/src/interfaces/usb.ts b/packages/transport/src/interfaces/usb.ts index e3a300e31c..c1e09e17b0 100644 --- a/packages/transport/src/interfaces/usb.ts +++ b/packages/transport/src/interfaces/usb.ts @@ -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 diff --git a/packages/transport/src/lowlevel/receive.ts b/packages/transport/src/lowlevel/receive.ts index 3f508aa167..cfd3d8f81a 100644 --- a/packages/transport/src/lowlevel/receive.ts +++ b/packages/transport/src/lowlevel/receive.ts @@ -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) { const data = await receiver(); const { length, typeId, restBuffer } = protocolV1.decodeChunked(data); const decoded = new ByteBuffer(length); + if (length) { decoded.append(restBuffer); } diff --git a/packages/transport/src/lowlevel/send.ts b/packages/transport/src/lowlevel/send.ts index cfd5807c1b..b4ef34c934 100644 --- a/packages/transport/src/lowlevel/send.ts +++ b/packages/transport/src/lowlevel/send.ts @@ -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, buffers: Array) { @@ -18,9 +18,7 @@ export function buildOne(messages: Root, name: string, data: Record Promise, + expectedLength: number, +): Promise { + 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) { + 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) { + const { received, typeId } = await receiveBuffer(receiver); + const { Message, messageName } = createMessageFromType(messages, typeId); + received.reset(); + const message = decodeProtobuf(Message, received); + return { + message, + type: messageName, + }; +} diff --git a/packages/transport/src/utils/send.ts b/packages/transport/src/utils/send.ts new file mode 100644 index 0000000000..b4ef34c934 --- /dev/null +++ b/packages/transport/src/utils/send.ts @@ -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, buffers: Array) { + 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) { + 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) => { + 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, + name: string, + data: Record, +) { + const buffers = buildBuffers(messages, name, data); + return sendBuffers(sender, buffers); +} diff --git a/packages/transport/tests/build-receive.test.ts b/packages/transport/tests/build-receive.test.ts index 8540bafcab..676047b1b6 100644 --- a/packages/transport/tests/build-receive.test.ts +++ b/packages/transport/tests/build-receive.test.ts @@ -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, () => {