mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-06 23:39:38 +01:00
feat(protocol): separate chunking from encoding
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { HEADER_SIZE } from './constants';
|
||||
import { TransportProtocolEncode } from '../types';
|
||||
|
||||
// for type compatibility, bridge doesn't send chunks
|
||||
export const getChunkHeader = (_data: Buffer) => Buffer.alloc(0);
|
||||
|
||||
// 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)
|
||||
@@ -21,5 +24,5 @@ export const encode: TransportProtocolEncode = (data, options) => {
|
||||
// then put in the actual message
|
||||
data.copy(encodedBuffer, HEADER_SIZE);
|
||||
|
||||
return [encodedBuffer];
|
||||
return encodedBuffer;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export const MESSAGE_MAGIC_HEADER_BYTE = 63;
|
||||
export const MESSAGE_HEADER_BYTE = 0x23;
|
||||
export const HEADER_SIZE = 1 + 1 + 2 + 4; // MESSAGE_HEADER_BYTE + MESSAGE_HEADER_BYTE + messageType + dataLength
|
||||
export const BUFFER_SIZE = 64;
|
||||
export const HEADER_SIZE = 1 + 1 + 1 + 2 + 4; // MESSAGE_MAGIC_HEADER_BYTE + MESSAGE_HEADER_BYTE + MESSAGE_HEADER_BYTE + messageType + dataLength
|
||||
|
||||
@@ -38,6 +38,6 @@ export const decode: TransportProtocolDecode = bytes => {
|
||||
return {
|
||||
length,
|
||||
messageType,
|
||||
payload: buffer.subarray(HEADER_SIZE + 1), // each chunk is prefixed by magic byte
|
||||
payload: buffer.subarray(HEADER_SIZE),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
HEADER_SIZE,
|
||||
MESSAGE_HEADER_BYTE,
|
||||
BUFFER_SIZE,
|
||||
MESSAGE_MAGIC_HEADER_BYTE,
|
||||
} from './constants';
|
||||
import { HEADER_SIZE, MESSAGE_HEADER_BYTE, MESSAGE_MAGIC_HEADER_BYTE } from './constants';
|
||||
import { TransportProtocolEncode } from '../types';
|
||||
|
||||
export const getChunkHeader = (_data: Buffer) => {
|
||||
const header = Buffer.alloc(1);
|
||||
header.writeUInt8(MESSAGE_MAGIC_HEADER_BYTE);
|
||||
|
||||
return header;
|
||||
};
|
||||
|
||||
export const encode: TransportProtocolEncode = (data, options) => {
|
||||
const { messageType } = options;
|
||||
if (typeof messageType === 'string') {
|
||||
@@ -13,42 +15,23 @@ export const encode: TransportProtocolEncode = (data, options) => {
|
||||
}
|
||||
|
||||
const fullSize = HEADER_SIZE + data.length;
|
||||
const chunkSize = options.chunkSize || BUFFER_SIZE;
|
||||
|
||||
const encodedBuffer = Buffer.alloc(fullSize);
|
||||
// 1 byte
|
||||
encodedBuffer.writeUInt8(MESSAGE_MAGIC_HEADER_BYTE, 0);
|
||||
|
||||
// 2*1 byte
|
||||
encodedBuffer.writeUInt8(MESSAGE_HEADER_BYTE, 0);
|
||||
encodedBuffer.writeUInt8(MESSAGE_HEADER_BYTE, 1);
|
||||
encodedBuffer.writeUInt8(MESSAGE_HEADER_BYTE, 2);
|
||||
|
||||
// 2 bytes
|
||||
encodedBuffer.writeUInt16BE(messageType, 2);
|
||||
encodedBuffer.writeUInt16BE(messageType, 3);
|
||||
|
||||
// 4 bytes (so 8 in total)
|
||||
encodedBuffer.writeUInt32BE(data.length, 4);
|
||||
// 4 bytes (so 9 in total)
|
||||
encodedBuffer.writeUInt32BE(data.length, 5);
|
||||
|
||||
// then put in the actual message
|
||||
data.copy(encodedBuffer, HEADER_SIZE);
|
||||
|
||||
const size = chunkSize - 1; // chunkSize - 1 byte of MESSAGE_MAGIC_HEADER_BYTE
|
||||
|
||||
const chunkCount = Math.ceil(encodedBuffer.length / size) || 1;
|
||||
|
||||
// size with one reserved byte for header
|
||||
|
||||
const result: Buffer[] = [];
|
||||
// How many pieces will there actually be
|
||||
// slice and dice
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const start = i * size;
|
||||
const end = Math.min((i + 1) * size, encodedBuffer.length);
|
||||
|
||||
const buffer = Buffer.alloc(chunkSize);
|
||||
buffer.writeUInt8(MESSAGE_MAGIC_HEADER_BYTE);
|
||||
encodedBuffer.copy(buffer, 1, start, end);
|
||||
|
||||
result.push(buffer);
|
||||
}
|
||||
|
||||
return result;
|
||||
return encodedBuffer;
|
||||
};
|
||||
|
||||
@@ -6,15 +6,15 @@ export type TransportProtocolDecode = (bytes: ArrayBuffer) => {
|
||||
|
||||
export interface TransportProtocolEncodeOptions {
|
||||
messageType: number | string;
|
||||
chunkSize?: number;
|
||||
}
|
||||
|
||||
export type TransportProtocolEncode = (
|
||||
data: Buffer,
|
||||
options: TransportProtocolEncodeOptions,
|
||||
) => Buffer[];
|
||||
) => Buffer;
|
||||
|
||||
export interface TransportProtocol {
|
||||
encode: TransportProtocolEncode;
|
||||
decode: TransportProtocolDecode;
|
||||
getChunkHeader: (data: Buffer) => Buffer;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,16 @@ import { bridge } from '../src/index';
|
||||
|
||||
describe('protocol-bridge', () => {
|
||||
it('encode', () => {
|
||||
let chunks;
|
||||
let result;
|
||||
// encode small chunk, message without data
|
||||
chunks = bridge.encode(Buffer.alloc(0), { messageType: 55 });
|
||||
expect(chunks.length).toEqual(1);
|
||||
expect(chunks[0].length).toEqual(6);
|
||||
result = bridge.encode(Buffer.alloc(0), { messageType: 55 });
|
||||
expect(result.length).toEqual(6);
|
||||
|
||||
// encode big chunk, message with data
|
||||
chunks = bridge.encode(Buffer.alloc(371), { messageType: 55 });
|
||||
expect(chunks.length).toEqual(1);
|
||||
expect(chunks[0].subarray(0, 6).toString('hex')).toEqual('003700000173');
|
||||
expect(chunks[0].readUint32BE(2)).toEqual(371);
|
||||
expect(chunks[0].length).toEqual(371 + 6);
|
||||
result = bridge.encode(Buffer.alloc(371), { messageType: 55 });
|
||||
expect(result.subarray(0, 6).toString('hex')).toEqual('003700000173');
|
||||
expect(result.readUint32BE(2)).toEqual(371);
|
||||
expect(result.length).toEqual(371 + 6);
|
||||
|
||||
// fail to encode unsupported messageType (string)
|
||||
expect(() => bridge.encode(Buffer.alloc(64), { messageType: 'Initialize' })).toThrow(
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import { v1 } from '../src/index';
|
||||
import { HEADER_SIZE } from '../src/protocol-v1/constants';
|
||||
|
||||
describe('protocol-v1', () => {
|
||||
it('encode', () => {
|
||||
let chunks;
|
||||
// encode only one chunk, message without data
|
||||
chunks = v1.encode(Buffer.alloc(0), { messageType: 55 });
|
||||
expect(chunks.length).toEqual(1);
|
||||
expect(chunks[0].length).toEqual(64);
|
||||
let result;
|
||||
// encode message without data
|
||||
result = v1.encode(Buffer.alloc(0), { messageType: 55 });
|
||||
expect(result.length).toEqual(HEADER_SIZE);
|
||||
|
||||
// encode multiple chunks, message with data
|
||||
chunks = v1.encode(Buffer.alloc(371), { messageType: 55 });
|
||||
expect(chunks.length).toEqual(7);
|
||||
chunks.forEach((chunk, index) => {
|
||||
expect(chunk.length).toEqual(64);
|
||||
if (index === 0) {
|
||||
// first chunk with additional data
|
||||
expect(chunk.subarray(0, 9).toString('hex')).toEqual('3f2323003700000173');
|
||||
expect(chunk.readUint32BE(5)).toEqual(371);
|
||||
} else {
|
||||
// following chunk starts with MESSAGE_MAGIC_HEADER_BYTE
|
||||
expect(chunk.subarray(0, 5).toString('hex')).toEqual('3f00000000');
|
||||
}
|
||||
});
|
||||
// encode message with data
|
||||
result = v1.encode(Buffer.alloc(371).fill(0xa3), { messageType: 55 });
|
||||
expect(result.length).toEqual(371 + HEADER_SIZE);
|
||||
expect(result.subarray(0, HEADER_SIZE).toString('hex')).toEqual('3f2323003700000173');
|
||||
expect(result.subarray(HEADER_SIZE).toString('hex')).toEqual('a3'.repeat(371));
|
||||
|
||||
// fail to encode unsupported messageType (string)
|
||||
expect(() => v1.encode(Buffer.alloc(64), { messageType: 'Initialize' })).toThrow(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { WebUSB } from 'usb';
|
||||
|
||||
import { v1 as protocolV1, bridge as protocolBridge } from '@trezor/protocol';
|
||||
import { receive as receiveUtil } from '@trezor/transport/src/utils/receive';
|
||||
import { createChunks } from '@trezor/transport/src/utils/send';
|
||||
import { SessionsBackground } from '@trezor/transport/src/sessions/background';
|
||||
import { SessionsClient } from '@trezor/transport/src/sessions/client';
|
||||
import { UsbApi } from '@trezor/transport/src/api/usb';
|
||||
@@ -41,7 +42,12 @@ export const createApi = (apiStr: 'usb' | 'udp', logger?: Log) => {
|
||||
new Uint8Array(Buffer.from(data, 'hex')),
|
||||
);
|
||||
|
||||
const buffers = protocolV1.encode(payload, { messageType });
|
||||
const encodedMessage = protocolV1.encode(payload, { messageType });
|
||||
const buffers = createChunks(
|
||||
encodedMessage,
|
||||
protocolV1.getChunkHeader(encodedMessage),
|
||||
api.chunkSize,
|
||||
);
|
||||
|
||||
for (let i = 0; i < buffers.length; i++) {
|
||||
const bufferSegment = buffers[i];
|
||||
@@ -65,12 +71,12 @@ export const createApi = (apiStr: 'usb' | 'udp', logger?: Log) => {
|
||||
}
|
||||
throw new Error(result.error);
|
||||
}),
|
||||
protocolV1.decode,
|
||||
protocolV1,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true as const,
|
||||
payload: protocolBridge.encode(payload, { messageType })[0].toString('hex'),
|
||||
payload: protocolBridge.encode(payload, { messageType }).toString('hex'),
|
||||
};
|
||||
} catch (err) {
|
||||
return { success: false as const, error: err.message as string };
|
||||
|
||||
@@ -108,6 +108,11 @@ export abstract class AbstractApi extends TypedEmitter<{
|
||||
| typeof ERRORS.UNEXPECTED_ERROR
|
||||
>;
|
||||
|
||||
/**
|
||||
* packet size for api
|
||||
*/
|
||||
abstract chunkSize: number;
|
||||
|
||||
protected success<T>(payload: T): Success<T> {
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AsyncResultWithTypedError, ResultWithTypedError } from '../types';
|
||||
import * as ERRORS from '../errors';
|
||||
|
||||
export class UdpApi extends AbstractApi {
|
||||
chunkSize = 64;
|
||||
interface = UDP.createSocket('udp4');
|
||||
protected communicating = false;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ interface TransportInterfaceDevice {
|
||||
const INTERFACE_DEVICE_DISCONNECTED = 'The device was disconnected.' as const;
|
||||
|
||||
export class UsbApi extends AbstractApi {
|
||||
chunkSize = 64;
|
||||
devices: TransportInterfaceDevice[] = [];
|
||||
usbInterface: ConstructorParams['usbInterface'];
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
AbstractTransportMethodParams,
|
||||
} from './abstract';
|
||||
import { AbstractApi } from '../api/abstract';
|
||||
import { buildBuffers } from '../utils/send';
|
||||
import { buildMessage, createChunks } from '../utils/send';
|
||||
import { receiveAndParse } from '../utils/receive';
|
||||
import { SessionsClient } from '../sessions/client';
|
||||
import * as ERRORS from '../errors';
|
||||
@@ -176,7 +176,12 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
});
|
||||
}
|
||||
|
||||
public call({ session, name, data, protocol }: AbstractTransportMethodParams<'call'>) {
|
||||
public call({
|
||||
session,
|
||||
name,
|
||||
data,
|
||||
protocol: customProtocol,
|
||||
}: AbstractTransportMethodParams<'call'>) {
|
||||
return this.scheduleAction(
|
||||
async () => {
|
||||
const getPathBySessionResponse = await this.sessionsClient.getPathBySession({
|
||||
@@ -193,8 +198,18 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
const { path } = getPathBySessionResponse.payload;
|
||||
|
||||
try {
|
||||
const { encode, decode } = protocol || v1Protocol;
|
||||
const buffers = buildBuffers(this.messages, name, data, encode);
|
||||
const protocol = customProtocol || v1Protocol;
|
||||
const bytes = buildMessage({
|
||||
messages: this.messages,
|
||||
name,
|
||||
data,
|
||||
encode: protocol.encode,
|
||||
});
|
||||
const buffers = createChunks(
|
||||
bytes,
|
||||
protocol.getChunkHeader(bytes),
|
||||
this.api.chunkSize,
|
||||
);
|
||||
for (let i = 0; i < buffers.length; i++) {
|
||||
const chunk = buffers[i];
|
||||
|
||||
@@ -214,7 +229,7 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
}
|
||||
throw new Error(result.error);
|
||||
}),
|
||||
decode,
|
||||
protocol,
|
||||
);
|
||||
|
||||
return this.success(message);
|
||||
@@ -247,8 +262,14 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
const { path } = getPathBySessionResponse.payload;
|
||||
|
||||
try {
|
||||
const { encode } = protocol || v1Protocol;
|
||||
const buffers = buildBuffers(this.messages, name, data, encode);
|
||||
const { encode, getChunkHeader } = protocol || v1Protocol;
|
||||
const bytes = buildMessage({
|
||||
messages: this.messages,
|
||||
name,
|
||||
data,
|
||||
encode,
|
||||
});
|
||||
const buffers = createChunks(bytes, getChunkHeader(bytes), this.api.chunkSize);
|
||||
for (let i = 0; i < buffers.length; i++) {
|
||||
const chunk = buffers[i];
|
||||
|
||||
@@ -270,7 +291,10 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
});
|
||||
}
|
||||
|
||||
public receive({ session, protocol }: AbstractTransportMethodParams<'receive'>) {
|
||||
public receive({
|
||||
session,
|
||||
protocol: customProtocol,
|
||||
}: AbstractTransportMethodParams<'receive'>) {
|
||||
return this.scheduleAction(async () => {
|
||||
const getPathBySessionResponse = await this.sessionsClient.getPathBySession({
|
||||
session,
|
||||
@@ -281,7 +305,7 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
const { path } = getPathBySessionResponse.payload;
|
||||
|
||||
try {
|
||||
const { decode } = protocol || v1Protocol;
|
||||
const protocol = customProtocol || v1Protocol;
|
||||
const message = await receiveAndParse(
|
||||
this.messages,
|
||||
() =>
|
||||
@@ -292,7 +316,7 @@ export abstract class AbstractApiTransport extends AbstractTransport {
|
||||
|
||||
return result.payload;
|
||||
}),
|
||||
decode,
|
||||
protocol,
|
||||
);
|
||||
|
||||
return this.success(message);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { versionUtils, createDeferred, Deferred, createTimeoutPromise } from '@t
|
||||
import { PROTOCOL_MALFORMED, bridge as bridgeProtocol } from '@trezor/protocol';
|
||||
import { bridgeApiCall } from '../utils/bridgeApiCall';
|
||||
import * as bridgeApiResult from '../utils/bridgeApiResult';
|
||||
import { buildBuffers } from '../utils/send';
|
||||
import { buildMessage } from '../utils/send';
|
||||
import { receiveAndParse } from '../utils/receive';
|
||||
import {
|
||||
AbstractTransport,
|
||||
@@ -225,11 +225,21 @@ export class BridgeTransport extends AbstractTransport {
|
||||
}
|
||||
|
||||
// https://github.dev/trezor/trezord-go/blob/f559ee5079679aeb5f897c65318d3310f78223ca/core/core.go#L534
|
||||
public call({ session, name, data, protocol }: AbstractTransportMethodParams<'call'>) {
|
||||
public call({
|
||||
session,
|
||||
name,
|
||||
data,
|
||||
protocol: customProtocol,
|
||||
}: AbstractTransportMethodParams<'call'>) {
|
||||
return this.scheduleAction(
|
||||
async signal => {
|
||||
const { encode, decode } = protocol || bridgeProtocol;
|
||||
const [bytes] = buildBuffers(this.messages, name, data, encode);
|
||||
const protocol = customProtocol || bridgeProtocol;
|
||||
const bytes = buildMessage({
|
||||
messages: this.messages,
|
||||
name,
|
||||
data,
|
||||
encode: protocol.encode,
|
||||
});
|
||||
const response = await this.post(`/call`, {
|
||||
params: session,
|
||||
body: bytes.toString('hex'),
|
||||
@@ -241,7 +251,7 @@ export class BridgeTransport extends AbstractTransport {
|
||||
const message = await receiveAndParse(
|
||||
this.messages,
|
||||
() => Promise.resolve(Buffer.from(response.payload, 'hex')),
|
||||
decode,
|
||||
protocol,
|
||||
);
|
||||
|
||||
return this.success(message);
|
||||
@@ -253,7 +263,12 @@ export class BridgeTransport extends AbstractTransport {
|
||||
public send({ session, name, data, protocol }: AbstractTransportMethodParams<'send'>) {
|
||||
return this.scheduleAction(async signal => {
|
||||
const { encode } = protocol || bridgeProtocol;
|
||||
const [bytes] = buildBuffers(this.messages, name, data, encode);
|
||||
const bytes = buildMessage({
|
||||
messages: this.messages,
|
||||
name,
|
||||
data,
|
||||
encode,
|
||||
});
|
||||
const response = await this.post('/post', {
|
||||
params: session,
|
||||
body: bytes.toString('hex'),
|
||||
@@ -267,7 +282,10 @@ export class BridgeTransport extends AbstractTransport {
|
||||
});
|
||||
}
|
||||
|
||||
public receive({ session, protocol }: AbstractTransportMethodParams<'receive'>) {
|
||||
public receive({
|
||||
session,
|
||||
protocol: customProtocol,
|
||||
}: AbstractTransportMethodParams<'receive'>) {
|
||||
return this.scheduleAction(async signal => {
|
||||
const response = await this.post('/read', {
|
||||
params: session,
|
||||
@@ -277,11 +295,11 @@ export class BridgeTransport extends AbstractTransport {
|
||||
if (!response.success) {
|
||||
return response;
|
||||
}
|
||||
const { decode } = protocol || bridgeProtocol;
|
||||
const protocol = customProtocol || bridgeProtocol;
|
||||
const message = await receiveAndParse(
|
||||
this.messages,
|
||||
() => Promise.resolve(Buffer.from(response.payload, 'hex')),
|
||||
decode,
|
||||
protocol,
|
||||
);
|
||||
|
||||
return this.success(message);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Root } from 'protobufjs/light';
|
||||
|
||||
import { decode as decodeProtobuf, createMessageFromType } from '@trezor/protobuf';
|
||||
import { TransportProtocolDecode } from '@trezor/protocol';
|
||||
import { TransportProtocol } from '@trezor/protocol';
|
||||
|
||||
async function receiveRest(
|
||||
result: Buffer,
|
||||
receiver: () => Promise<ArrayBuffer>,
|
||||
offset: number,
|
||||
expectedLength: number,
|
||||
chunkHeader: Buffer,
|
||||
): Promise<void> {
|
||||
if (offset >= expectedLength) {
|
||||
return;
|
||||
@@ -17,25 +18,23 @@ async function receiveRest(
|
||||
if (data == null) {
|
||||
throw new Error('Received no data.');
|
||||
}
|
||||
const length = offset + data.byteLength - 1;
|
||||
Buffer.from(data).copy(result, offset, 1, length);
|
||||
const length = offset + data.byteLength - chunkHeader.byteLength;
|
||||
Buffer.from(data).copy(result, offset, chunkHeader.byteLength, length);
|
||||
|
||||
return receiveRest(result, receiver, length, expectedLength);
|
||||
return receiveRest(result, receiver, length, expectedLength, chunkHeader);
|
||||
}
|
||||
|
||||
export async function receive(
|
||||
receiver: () => Promise<ArrayBuffer>,
|
||||
decoder: TransportProtocolDecode,
|
||||
) {
|
||||
export async function receive(receiver: () => Promise<ArrayBuffer>, protocol: TransportProtocol) {
|
||||
const data = await receiver();
|
||||
const { length, messageType, payload } = decoder(data);
|
||||
const { length, messageType, payload } = protocol.decode(data);
|
||||
const result = Buffer.alloc(length);
|
||||
const chunkHeader = protocol.getChunkHeader(Buffer.from(data));
|
||||
|
||||
if (length) {
|
||||
payload.copy(result);
|
||||
}
|
||||
|
||||
await receiveRest(result, receiver, payload.length, length);
|
||||
await receiveRest(result, receiver, payload.length, length, chunkHeader);
|
||||
|
||||
return { messageType, payload: result };
|
||||
}
|
||||
@@ -43,9 +42,9 @@ export async function receive(
|
||||
export async function receiveAndParse(
|
||||
messages: Root,
|
||||
receiver: () => Promise<ArrayBuffer>,
|
||||
decoder: TransportProtocolDecode,
|
||||
protocol: TransportProtocol,
|
||||
) {
|
||||
const { messageType, payload } = await receive(receiver, decoder);
|
||||
const { messageType, payload } = await receive(receiver, protocol);
|
||||
const { Message, messageName } = createMessageFromType(messages, messageType);
|
||||
const message = decodeProtobuf(Message, payload);
|
||||
|
||||
|
||||
@@ -5,16 +5,41 @@ import { Root } from 'protobufjs/light';
|
||||
import { encode as encodeProtobuf, createMessageFromName } from '@trezor/protobuf';
|
||||
import { TransportProtocolEncode } from '@trezor/protocol';
|
||||
|
||||
export const buildBuffers = (
|
||||
messages: Root,
|
||||
name: string,
|
||||
data: Record<string, unknown>,
|
||||
encoder: TransportProtocolEncode,
|
||||
) => {
|
||||
export const createChunks = (data: Buffer, chunkHeader: Buffer, chunkSize: number) => {
|
||||
if (!chunkSize || data.byteLength <= chunkSize) {
|
||||
const buffer = Buffer.alloc(Math.max(chunkSize, data.byteLength));
|
||||
data.copy(buffer);
|
||||
|
||||
return [buffer];
|
||||
}
|
||||
|
||||
// create first chunk without chunkHeader
|
||||
const chunks = [data.subarray(0, chunkSize)];
|
||||
// create following chunks prefixed with chunkHeader
|
||||
let position = chunkSize;
|
||||
while (position < data.byteLength) {
|
||||
const sliceEnd = Math.min(position + chunkSize - chunkHeader.byteLength, data.byteLength);
|
||||
const slice = data.subarray(position, sliceEnd);
|
||||
const chunk = Buffer.concat([chunkHeader, slice]);
|
||||
chunks.push(Buffer.alloc(chunkSize).fill(chunk, 0, chunk.byteLength));
|
||||
position = sliceEnd;
|
||||
}
|
||||
|
||||
return chunks;
|
||||
};
|
||||
|
||||
interface BuildMessageProps {
|
||||
messages: Root;
|
||||
name: string;
|
||||
data: Record<string, unknown>;
|
||||
encode: TransportProtocolEncode;
|
||||
}
|
||||
|
||||
export const buildMessage = ({ messages, name, data, encode }: BuildMessageProps) => {
|
||||
const { Message, messageType } = createMessageFromName(messages, name);
|
||||
const buffer = encodeProtobuf(Message, data);
|
||||
|
||||
return encoder(buffer, {
|
||||
return encode(buffer, {
|
||||
messageType,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -383,34 +383,28 @@ describe('Usb', () => {
|
||||
.spyOn(testUsbApi, 'write')
|
||||
.mockImplementation(() => Promise.resolve({ success: true, payload: undefined }));
|
||||
|
||||
// override protocol options
|
||||
const overrideProtocol = (protocol: typeof v1Protocol, chunkSize: number) =>
|
||||
({
|
||||
...protocol,
|
||||
encode: (...[data, options]: Parameters<typeof protocol.encode>) =>
|
||||
protocol.encode(data, { ...options, chunkSize }),
|
||||
}) as typeof protocol;
|
||||
|
||||
const send = (chunkSize: number) =>
|
||||
const send = () =>
|
||||
transport.send({
|
||||
name: 'SignMessage',
|
||||
data: {
|
||||
message: '00'.repeat(200),
|
||||
},
|
||||
session: acquireRes.payload,
|
||||
protocol: overrideProtocol(v1Protocol, chunkSize),
|
||||
protocol: v1Protocol,
|
||||
}).promise;
|
||||
|
||||
// count encoded/sent chunks
|
||||
await send(64); // default usb
|
||||
await send(); // 64 default chunkSize for usb
|
||||
expect(writeSpy).toHaveBeenCalledTimes(4);
|
||||
writeSpy.mockClear();
|
||||
|
||||
await send(16); // smaller chunks
|
||||
testUsbApi.chunkSize = 16;
|
||||
await send(); // smaller chunks
|
||||
expect(writeSpy).toHaveBeenCalledTimes(15);
|
||||
writeSpy.mockClear();
|
||||
|
||||
await send(128); // bigger chunks
|
||||
testUsbApi.chunkSize = 128;
|
||||
await send(); // bigger chunks
|
||||
expect(writeSpy).toHaveBeenCalledTimes(2);
|
||||
writeSpy.mockClear();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as protobuf from 'protobufjs/light';
|
||||
import { v1 as v1Protocol, bridge as bridgeProtocol } from '@trezor/protocol';
|
||||
import { buildBuffers } from '../src/utils/send';
|
||||
import { buildMessage, createChunks } from '../src/utils/send';
|
||||
import { receiveAndParse } from '../src/utils/receive';
|
||||
|
||||
const messages = {
|
||||
@@ -97,34 +97,38 @@ const parsedMessages = protobuf.Root.fromJSON({
|
||||
describe('encoding json -> protobuf -> json', () => {
|
||||
fixtures.forEach(f => {
|
||||
describe(`${f.name} - payload length ${f.in.source_account.length}`, () => {
|
||||
test('bridgeProtocol: buildBuffers - receiveAndParse', async () => {
|
||||
const result = buildBuffers(parsedMessages, f.name, f.in, bridgeProtocol.encode);
|
||||
// bridgeProtocol returns only one big chunk
|
||||
expect(result.length).toBe(1);
|
||||
const [chunk] = result;
|
||||
test('bridgeProtocol: buildMessage - receiveAndParse', async () => {
|
||||
const result = buildMessage({
|
||||
messages: parsedMessages,
|
||||
name: f.name,
|
||||
data: f.in,
|
||||
encode: bridgeProtocol.encode,
|
||||
});
|
||||
const { length } = Buffer.from(f.in.source_account);
|
||||
// chunk length cannot be less than message header/constant (28) + variable source_account length
|
||||
// result length cannot be less than message header/constant (28) + variable source_account length
|
||||
// additional bytes are expected (encoded Uint32) if message length is greater
|
||||
expect(chunk.length).toBeGreaterThanOrEqual(28 + length);
|
||||
let i = -1;
|
||||
expect(result.length).toBeGreaterThanOrEqual(28 + length);
|
||||
const decoded = await receiveAndParse(
|
||||
parsedMessages,
|
||||
() => {
|
||||
i++;
|
||||
|
||||
return Promise.resolve(result[i]);
|
||||
},
|
||||
bridgeProtocol.decode,
|
||||
() => Promise.resolve(result),
|
||||
bridgeProtocol,
|
||||
);
|
||||
// then decode message and check, whether decoded message matches original json
|
||||
expect(decoded.type).toEqual(f.name);
|
||||
expect(decoded.message).toEqual(f.in);
|
||||
});
|
||||
|
||||
test('v1Protocol: buildBuffers - receiveAndParse', async () => {
|
||||
const result = buildBuffers(parsedMessages, f.name, f.in, v1Protocol.encode);
|
||||
test('v1Protocol: buildMessage - createChunks - receiveAndParse', async () => {
|
||||
const result = buildMessage({
|
||||
messages: parsedMessages,
|
||||
name: f.name,
|
||||
data: f.in,
|
||||
encode: v1Protocol.encode,
|
||||
});
|
||||
|
||||
const chunks = createChunks(result, v1Protocol.getChunkHeader(result), 64);
|
||||
// each protocol chunks are equal 64 bytes
|
||||
result.forEach(chunk => {
|
||||
chunks.forEach(chunk => {
|
||||
expect(chunk.length).toEqual(64);
|
||||
});
|
||||
let i = -1;
|
||||
@@ -133,9 +137,9 @@ describe('encoding json -> protobuf -> json', () => {
|
||||
() => {
|
||||
i++;
|
||||
|
||||
return Promise.resolve(result[i]);
|
||||
return Promise.resolve(chunks[i]);
|
||||
},
|
||||
v1Protocol.decode,
|
||||
v1Protocol,
|
||||
);
|
||||
// then decode message and check, whether decoded message matches original json
|
||||
expect(decoded.type).toEqual(f.name);
|
||||
@@ -144,3 +148,53 @@ describe('encoding json -> protobuf -> json', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createChunks', () => {
|
||||
const chunkHeader = Buffer.from([63]);
|
||||
|
||||
test('small packet = one chunk', () => {
|
||||
const result = createChunks(Buffer.alloc(63).fill(0x12), chunkHeader, 64);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].toString('hex')).toBe('12'.repeat(63) + '00');
|
||||
});
|
||||
|
||||
test('exact packet = one chunk', () => {
|
||||
const result = createChunks(Buffer.alloc(64), chunkHeader, 64);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
|
||||
test('byte overflow = two chunks', () => {
|
||||
const result = createChunks(Buffer.alloc(65).fill('a0a1'), chunkHeader, 64);
|
||||
expect(result.length).toBe(2);
|
||||
// header + last byte from data
|
||||
expect(result[1].subarray(0, 2).toString('hex')).toBe('3f61');
|
||||
// the rest is filled with 00
|
||||
expect(result[1].subarray(2).toString('hex')).toBe('00'.repeat(62));
|
||||
});
|
||||
|
||||
test('exact packet, big chunkHeader = two chunks', () => {
|
||||
const result = createChunks(
|
||||
Buffer.alloc(64 + 64 - 7).fill(0x12),
|
||||
Buffer.alloc(7).fill(0x73),
|
||||
64,
|
||||
);
|
||||
expect(result.length).toBe(2);
|
||||
});
|
||||
|
||||
test('byte overflow, big chunkHeader = three chunks', () => {
|
||||
const result = createChunks(
|
||||
Buffer.alloc(64 * 2 - 6).fill(0x12),
|
||||
Buffer.alloc(7).fill(0x73),
|
||||
64,
|
||||
);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[2].subarray(0, 8).toString('hex')).toBe('7373737373737312');
|
||||
expect(result[2].subarray(8).toString('hex')).toBe('00'.repeat(64 - 8));
|
||||
});
|
||||
|
||||
test('chunkSize not set = one chunk', () => {
|
||||
const result = createChunks(Buffer.alloc(128).fill(0x12), Buffer.alloc(7).fill(0x73), 0);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].byteLength).toBe(128);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user