Skip to content

Feat: protocol-thp encode/decode #19073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 128 additions & 1 deletion packages/protocol/src/protocol-thp/ThpState.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { ThpDeviceProperties } from './messages';
import { ThpCredentials, ThpDeviceProperties, ThpMessageSyncBit } from './messages';

export type ThpStateSerialized = {
properties?: ThpDeviceProperties;
credentials: ThpCredentials[];
channel: string; // 2 bytes as hex
sendBit: ThpMessageSyncBit; // host synchronization bit
recvBit: ThpMessageSyncBit; // device synchronization bit
sendNonce: number; // host nonce
recvNonce: number; // device nonce
expectedResponses: number[]; // expected responses from the device
};

export class ThpState {
private _properties?: ThpDeviceProperties;
private _pairingCredentials: ThpCredentials[] = [];
private _channel: Buffer = Buffer.alloc(0);
private _sendBit: ThpMessageSyncBit = 0;
private _sendNonce: number = 0;
private _recvBit: ThpMessageSyncBit = 0;
private _recvNonce: number = 1;
private _expectedResponses: number[] = [];

get properties() {
return this._properties;
Expand All @@ -15,9 +29,122 @@ export class ThpState {
this._properties = props;
}

get pairingCredentials() {
return this._pairingCredentials;
}

setPairingCredentials(credentials?: ThpCredentials[]) {
if (credentials) {
this._pairingCredentials.push(...credentials);
} else {
this._pairingCredentials = [];
}
}

get channel() {
return this._channel;
}

setChannel(channel: Buffer) {
this._channel = channel;
}

get sendBit() {
return this._sendBit;
}

get sendNonce() {
return this._sendNonce;
}

get recvBit() {
return this._recvBit;
}

get recvNonce() {
return this._recvNonce;
}

updateSyncBit(type: 'send' | 'recv') {
if (type === 'send') {
this._sendBit = this._sendBit > 0 ? 0 : 1;
} else {
this._recvBit = this._recvBit > 0 ? 0 : 1;
}
}

updateNonce(type: 'send' | 'recv') {
if (type === 'send') {
this._sendNonce += 1;
} else {
this._recvNonce += 1;
}
}

serialize(): ThpStateSerialized {
return {
properties: this._properties,
channel: this.channel.toString('hex'),
sendBit: this.sendBit,
recvBit: this.recvBit,
sendNonce: this.sendNonce,
recvNonce: this.recvNonce,
expectedResponses: this._expectedResponses,
credentials: this._pairingCredentials,
};
}

deserialize(json: ReturnType<(typeof this)['serialize']>) {
// simple fields validation
const error = new Error('ThpState.deserialize invalid state');
if (!json || typeof json !== 'object') {
throw error;
}
if (!Array.isArray(json.expectedResponses)) {
throw error;
}
if (typeof json.channel !== 'string') {
throw error;
}
[
json.sendBit,
json.recvBit,
json.sendNonce,
json.recvNonce,
...json.expectedResponses,
].forEach(nr => {
if (typeof nr !== 'number') {
throw error;
}
});

this._channel = Buffer.from(json.channel, 'hex');
this._expectedResponses = json.expectedResponses;
this._sendBit = json.sendBit;
this._recvBit = json.recvBit;
this._sendNonce = json.sendNonce;
this._recvNonce = json.recvNonce;
}

get expectedResponses() {
return this._expectedResponses;
}

setExpectedResponses(expected: number[]) {
this._expectedResponses = expected;
}

resetState() {
this._channel = Buffer.alloc(0);
this._sendBit = 0;
this._sendNonce = 0;
this._recvBit = 0;
this._recvNonce = 1;
this._expectedResponses = [];
this._pairingCredentials = [];
}

toString() {
return JSON.stringify(this.serialize());
}
}
16 changes: 16 additions & 0 deletions packages/protocol/src/protocol-thp/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const THP_CREATE_CHANNEL_REQUEST = 0x40;
export const THP_CREATE_CHANNEL_RESPONSE = 0x41;
export const THP_HANDSHAKE_INIT_REQUEST = 0x00;
export const THP_HANDSHAKE_INIT_RESPONSE = 0x01;
export const THP_HANDSHAKE_COMPLETION_REQUEST = 0x02;
export const THP_HANDSHAKE_COMPLETION_RESPONSE = 0x03;
export const THP_ERROR_HEADER_BYTE = 0x42;
export const THP_READ_ACK_HEADER_BYTE = 0x20; // [0x20, 0x30];
export const THP_CONTROL_BYTE_ENCRYPTED = 0x04; // [0x04, 0x14];
export const THP_CONTROL_BYTE_DECRYPTED = 0x05; // [0x05, 0x15];
export const THP_CONTINUATION_PACKET = 0x80;

export const THP_DEFAULT_CHANNEL = Buffer.from([0xff, 0xff]);

export const CRC_LENGTH = 4;
export const TAG_LENGTH = 16;
63 changes: 63 additions & 0 deletions packages/protocol/src/protocol-thp/crypto/crc32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// inspired by
// https://github.com/brianloveswords/buffer-crc32/blob/master/index.js
// optimized by
// https://stackoverflow.com/a/18639975

// we don't want to have dependency in @trezor/protocol package + our implementation is simpler and faster

const getCrcTable = () =>
new Int32Array([
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
]);

export const crc32 = (buf: Buffer): Buffer => {
if (!Buffer.isBuffer(buf)) {
throw new Error('Invalid crc input');
}

const table = getCrcTable();
let crc = -1;
for (let i = 0; i < buf.length; i++) {
crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
}
const buffer = Buffer.alloc(4);
buffer.writeInt32BE(crc ^ -1, 0);

return buffer;
};
Loading