What changed, and why it matters
This commit adds a brand-new Rust implementation of the Trezor Host Protocol (THP), a low-level transport layer that handles packet headers, fragmentation/reassembly, checksums, sequence/ack bits, and basic message types such as ping/pong, acknowledgments, and handshakes. It is a feature commit with no changelog entry and no accompanying security disclosure. The code is purely a new library plus tests/example; it does not patch any reported vulnerability.
No immediate security action required. Treat as normal feature code: review the new parser for robustness, ensure fuzzing covers malformed headers/fragment sequences, and verify that future commits wire this transport into higher-level code with appropriate authentication/encryption. The example ping-emulator uses localhost UDP only.
Security signals we found
New network-facing transport protocol parser/serializer added
Bounds checks present for payload_len (<= 60000) and channel_id (<= 0xFFEF or broadcast)
CRC32 verification included in reassembly path
Role-based parsing rejects directionally invalid messages (Host vs Device)
No unsafe code; #![forbid(unsafe_code)]
No changelog entry and no vendor security disclosure
Evidence from the diff
The diff introduces rust/trezor-thp, a no_std Rust crate implementing THP framing. Key components: control_byte.rs (message type constants and masks), header.rs (Header enum with parse/serialize, role-based Host/Device validation, payload length and channel-id bounds), fragment.rs (Fragmenter/Reassembler with CRC32 appended to payload), alternating_bit.rs (ABP state machine for seq/ack bits), crc32.rs (nibble-table CRC32), error.rs (Error/TransportError enums), and an example UDP ping emulator. The Makefile change is cosmetic (echo string). No unsafe code is used. There are no references to CVEs, advisories, or security fixes in the commit message or diff.
Changed components
rust/trezor-thp (new crate)rust/Makefile (cosmetic style_check label only)Inspect captured patch +1565 / −1
diff --git a/rust/Makefile b/rust/Makefile
index 4e488f5f..2d6ab389 100644
--- a/rust/Makefile
+++ b/rust/Makefile
@@ -13,7 +13,7 @@ style:
style_check:
@for D in $(CRATES); do ( \
- echo "[STYLE $$D]"; \
+ echo "[STYLE_CHECK $$D]"; \
cd $$D; \
cargo fmt -- --check \
); done
diff --git a/rust/trezor-thp/Cargo.lock b/rust/trezor-thp/Cargo.lock
new file mode 100644
index 00000000..8de728f4
--- /dev/null
+++ b/rust/trezor-thp/Cargo.lock
@@ -0,0 +1,48 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "hash32"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "heapless"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
+dependencies = [
+ "hash32",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "trezor-thp"
+version = "0.1.0"
+dependencies = [
+ "heapless",
+ "hex",
+]
diff --git a/rust/trezor-thp/Cargo.toml b/rust/trezor-thp/Cargo.toml
new file mode 100644
index 00000000..d070e492
--- /dev/null
+++ b/rust/trezor-thp/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "trezor-thp"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+
+[dev-dependencies]
+heapless = { version = "0.9.2", default-features = false }
+hex = "0.4.3"
diff --git a/rust/trezor-thp/examples/ping-emulator.rs b/rust/trezor-thp/examples/ping-emulator.rs
new file mode 100644
index 00000000..3ec07778
--- /dev/null
+++ b/rust/trezor-thp/examples/ping-emulator.rs
@@ -0,0 +1,44 @@
+use std::env;
+use std::net::{SocketAddr, UdpSocket};
+use std::str::FromStr;
+
+use trezor_thp::{
+ Host,
+ alternating_bit::SyncBits,
+ fragment::{Fragmenter, Reassembler},
+ header::Header,
+};
+
+const REPEAT: u8 = 1;
+const PACKET_LEN: usize = 64;
+
+pub fn main() -> std::io::Result<()> {
+ let port_str = env::args().nth(1).unwrap_or("21324".to_string());
+ let port = u16::from_str(&port_str).expect("UDP port number");
+ let emu_addr = SocketAddr::from(([127, 0, 0, 1], port));
+ let socket = UdpSocket::bind("127.0.0.1:0")?;
+
+ let mut sockbuf = [0u8; PACKET_LEN];
+ let mut reply_data = [0u8; PACKET_LEN];
+
+ for i in 0..REPEAT {
+ let nonce = [i; 8]; // not a good nonce
+ let sb = SyncBits::new(); // no ABP for ping
+ Fragmenter::<Host>::single(Header::new_ping(), sb, &nonce, &mut sockbuf).unwrap();
+ socket.send_to(&sockbuf, &emu_addr)?;
+
+ let (reply_len, _src_addr) = socket.recv_from(&mut sockbuf).unwrap();
+ assert!(reply_len > 0);
+ let (header, payload) =
+ Reassembler::<Host>::single(&sockbuf[..reply_len], &mut reply_data).unwrap();
+
+ if header.is_pong() && payload == &nonce {
+ println!("Pong OK");
+ } else {
+ println!("Invalid reply {}", hex::encode(sockbuf));
+ break;
+ }
+ }
+
+ Ok(())
+}
diff --git a/rust/trezor-thp/src/alternating_bit.rs b/rust/trezor-thp/src/alternating_bit.rs
new file mode 100644
index 00000000..39d3554b
--- /dev/null
+++ b/rust/trezor-thp/src/alternating_bit.rs
@@ -0,0 +1,279 @@
+use crate::control_byte::{ACK_BIT, SEQ_BIT};
+
+#[derive(Clone, Copy)]
+pub struct SyncBits(u8);
+
+impl SyncBits {
+ pub const fn new() -> Self {
+ Self(0u8)
+ }
+
+ pub const fn seq_bit(&self) -> bool {
+ (self.0 & SEQ_BIT) != 0
+ }
+
+ pub const fn ack_bit(&self) -> bool {
+ (self.0 & ACK_BIT) != 0
+ }
+
+ pub const fn with_seq_bit(self, seq_bit: bool) -> Self {
+ Self(if seq_bit {
+ self.0 | SEQ_BIT
+ } else {
+ self.0 & !SEQ_BIT
+ })
+ }
+
+ pub const fn with_ack_bit(self, ack_bit: bool) -> Self {
+ Self(if ack_bit {
+ self.0 | ACK_BIT
+ } else {
+ self.0 & !ACK_BIT
+ })
+ }
+}
+
+impl From<u8> for SyncBits {
+ fn from(byte: u8) -> Self {
+ Self(byte)
+ }
+}
+
+impl From<SyncBits> for u8 {
+ fn from(sb: SyncBits) -> Self {
+ sb.0
+ }
+}
+
+impl Default for SyncBits {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Alternating Bit Protocol state for a single channel.
+#[cfg_attr(any(test, debug_assertions), derive(Debug, PartialEq))]
+pub struct ChannelSync {
+ /// If true we are waiting for an ACK and cannot send further messages.
+ can_send: bool,
+ /// Receive bit.
+ sync_receive: bool,
+ /// Send bit.
+ sync_send: bool,
+ /// Preparation for https://github.com/trezor/trezor-firmware/issues/6135
+ ack_piggybacking: bool,
+}
+
+impl ChannelSync {
+ pub fn new() -> Self {
+ Self {
+ can_send: true,
+ sync_receive: false,
+ sync_send: false,
+ ack_piggybacking: false,
+ }
+ }
+
+ /// Returns false if we're waiting for an ACK.
+ pub fn can_send(&self) -> bool {
+ self.can_send
+ }
+
+ /// Call before sending a message.
+ /// Returns none if the previous transmission hasn't finished yet.
+ /// If we can send returns SyncBits to be used when serializing message.
+ pub fn send_start(&self) -> Option<SyncBits> {
+ if !self.can_send() {
+ return None; // sending in progress, don't send
+ }
+
+ let sb = SyncBits::new().with_seq_bit(self.sync_send);
+ Some(sb) // start sending, use these bits
+ }
+
+ /// Call after sending last message fragment, wait for ACK before allowing next message.
+ /// NOTE: Consider saving some kind of timestamp or identifier instead of bool.
+ pub fn send_finish(&mut self) {
+ self.can_send = false;
+ }
+
+ /// Call after receiving an ACK message.
+ pub fn send_mark_delivered(&mut self, sb: SyncBits) {
+ if self.sync_send == sb.ack_bit() {
+ self.sync_send.increment();
+ self.can_send = true;
+ }
+ }
+
+ /// Call after receving initial fragment of a message.
+ /// Returns true when seq_bit is correct and we should reassemble the message.
+ /// If the function returns false all following continuation packets should be discarded.
+ pub fn receive_start(&mut self, sb: SyncBits) -> bool {
+ if sb.seq_bit() != self.sync_receive {
+ // Either this message is a duplicate or previous one was dropped.
+ return false;
+ }
+
+ // NOTE: consider intercepting ACKs here and passing them to send_mark_delivered
+ true
+ }
+
+ /// Call after receiving last fragment and successfully verifying CRC of the message.
+ /// Caller needs to send ACK with the returned SyncBits.
+ pub fn receive_acknowledge(&mut self) -> SyncBits {
+ let sb = SyncBits::new().with_ack_bit(self.sync_receive);
+ self.sync_receive.increment();
+ sb
+ }
+
+ /// Serialize for storage.
+ /// can_send_bit | sync_receive_bit | sync_send_bit | ack_piggybacking | rfu(4)
+ pub fn to_u8(&self) -> u8 {
+ let mut res = 0u8;
+ if self.can_send {
+ res |= 0x80;
+ }
+ if self.sync_receive {
+ res |= 0x40;
+ }
+ if self.sync_send {
+ res |= 0x20;
+ }
+ if self.ack_piggybacking {
+ res |= 0x10;
+ }
+ res
+ }
+
+ /// Deserialize from storage.
+ pub fn from_u8(val: u8) -> Self {
+ Self {
+ can_send: (val & 0x80 != 0),
+ sync_receive: (val & 0x40 != 0),
+ sync_send: (val & 0x20 != 0),
+ ack_piggybacking: (val & 0x10 != 0),
+ }
+ }
+}
+
+impl Default for ChannelSync {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+trait BoolExt {
+ fn increment(&mut self);
+}
+
+impl BoolExt for bool {
+ fn increment(&mut self) {
+ *self = !*self;
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_simple_rt() {
+ let mut host_sync = ChannelSync::new();
+ let mut trezor_sync = ChannelSync::new();
+
+ // request
+ assert!(host_sync.can_send());
+ let sb = host_sync.send_start().unwrap();
+ assert_eq!(sb.seq_bit(), false);
+ host_sync.send_finish();
+ assert!(!host_sync.can_send());
+ // H->T
+ let ok = trezor_sync.receive_start(sb);
+ assert!(ok);
+ let sb = trezor_sync.receive_acknowledge();
+ assert_eq!(sb.ack_bit(), false);
+ // T->H ACK
+ host_sync.send_mark_delivered(sb);
+ assert!(host_sync.can_send());
+
+ // reply
+ assert!(trezor_sync.can_send());
+ let sb = trezor_sync.send_start().unwrap();
+ assert_eq!(sb.seq_bit(), false);
+ trezor_sync.send_finish();
+ assert!(!trezor_sync.can_send());
+ // T->H
+ let ok = host_sync.receive_start(sb);
+ assert!(ok);
+ let sb = host_sync.receive_acknowledge();
+ assert_eq!(sb.ack_bit(), false);
+ // H->T ACK
+ trezor_sync.send_mark_delivered(sb);
+ assert!(trezor_sync.can_send());
+
+ // confirm alternation
+ assert_eq!(host_sync.send_start().unwrap().seq_bit(), true);
+ assert_eq!(trezor_sync.send_start().unwrap().seq_bit(), true);
+ }
+
+ #[test]
+ fn test_oneway() {
+ let mut a_sync = ChannelSync::new();
+ let mut b_sync = ChannelSync::new();
+
+ for i in 0..32 {
+ let expected: bool = i % 2 != 0;
+ assert!(a_sync.can_send());
+ let sb = a_sync.send_start().unwrap();
+ a_sync.send_finish();
+ assert_eq!(sb.seq_bit(), expected);
+ assert!(!a_sync.can_send());
+ assert!(b_sync.receive_start(sb));
+ let sb = b_sync.receive_acknowledge();
+ assert_eq!(sb.ack_bit(), expected);
+ a_sync.send_mark_delivered(sb);
+ assert!(a_sync.can_send());
+ }
+ }
+
+ #[test]
+ fn test_serialize_rt() {
+ for cs in 0..1 {
+ for sr in 0..1 {
+ for ss in 0..1 {
+ for ap in 0..1 {
+ let orig = ChannelSync {
+ can_send: cs != 0,
+ sync_receive: sr != 0,
+ sync_send: ss != 0,
+ ack_piggybacking: ap != 0,
+ };
+ assert_eq!(ChannelSync::from_u8(orig.to_u8()), orig);
+ assert_eq!(ChannelSync::from_u8(orig.to_u8()).to_u8(), orig.to_u8());
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
+ fn test_serialize_rfu() {
+ let orig = ChannelSync::from_u8(0);
+ for u in 0..0x0fu8 {
+ assert_eq!(ChannelSync::from_u8(u), orig);
+ }
+ }
+
+ #[test]
+ fn test_serialize_simple() {
+ let mut sync = ChannelSync::new();
+ assert_eq!(sync.to_u8(), 0b10000000); // can_send=true
+ sync.send_finish();
+ assert_eq!(sync.to_u8(), 0b00000000);
+ sync.send_mark_delivered(SyncBits::new());
+ assert_eq!(sync.to_u8(), 0b10100000);
+ sync.receive_start(SyncBits::new());
+ sync.receive_acknowledge();
+ assert_eq!(sync.to_u8(), 0b11100000);
+ }
+}
diff --git a/rust/trezor-thp/src/control_byte.rs b/rust/trezor-thp/src/control_byte.rs
new file mode 100644
index 00000000..0b227e8b
--- /dev/null
+++ b/rust/trezor-thp/src/control_byte.rs
@@ -0,0 +1,88 @@
+use crate::alternating_bit::SyncBits;
+
+pub const CODEC_V1: u8 = 0x3F;
+pub const CONTINUATION_PACKET: u8 = 0x80;
+
+pub const CONTINUATION_PACKET_MASK: u8 = 0x80;
+
+pub const ACK_MASK: u8 = 0xF7;
+pub const ACK_MESSAGE: u8 = 0x20;
+
+pub const DATA_MASK: u8 = 0xE7;
+pub const HANDSHAKE_INIT_REQ: u8 = 0x00;
+pub const HANDSHAKE_INIT_RES: u8 = 0x01;
+pub const HANDSHAKE_COMP_REQ: u8 = 0x02;
+pub const HANDSHAKE_COMP_RES: u8 = 0x03;
+pub const ENCRYPTED_TRANSPORT: u8 = 0x04;
+
+pub const CHANNEL_ALLOCATION_REQ: u8 = 0x40;
+pub const CHANNEL_ALLOCATION_RES: u8 = 0x41;
+pub const ERROR: u8 = 0x42;
+pub const PING: u8 = 0x43;
+pub const PONG: u8 = 0x44;
+
+pub const ACK_BIT: u8 = 0x08;
+pub const SEQ_BIT: u8 = 0x10;
+pub const SYNC_MASK: u8 = ACK_BIT | SEQ_BIT;
+
+#[derive(Clone, Copy)]
+pub struct ControlByte(u8);
+
+impl ControlByte {
+ pub fn sync_bits(&self) -> SyncBits {
+ SyncBits::from(self.0 & SYNC_MASK)
+ }
+
+ pub fn with_sync_bits(self, sb: SyncBits) -> Self {
+ Self(self.0 & !SYNC_MASK | <SyncBits as Into<u8>>::into(sb))
+ }
+
+ pub const fn is_ack(&self) -> bool {
+ self.0 & ACK_MASK == ACK_MESSAGE
+ }
+
+ pub const fn is_continuation(&self) -> bool {
+ self.0 & CONTINUATION_PACKET_MASK == CONTINUATION_PACKET
+ }
+
+ pub const fn is_encrypted_transport(&self) -> bool {
+ self.0 & DATA_MASK == ENCRYPTED_TRANSPORT
+ }
+
+ pub const fn is_codec_v1(&self) -> bool {
+ self.0 == CODEC_V1
+ }
+
+ pub const fn is_ping(&self) -> bool {
+ self.0 == PING
+ }
+
+ pub const fn is_pong(&self) -> bool {
+ self.0 == PONG
+ }
+
+ pub const fn is_error(&self) -> bool {
+ self.0 == ERROR
+ }
+
+ pub const fn is_channel_allocation_request(&self) -> bool {
+ self.0 == CHANNEL_ALLOCATION_REQ
+ }
+
+ pub const fn is_channel_allocation_response(&self) -> bool {
+ self.0 == CHANNEL_ALLOCATION_RES
+ }
+}
+
+// Note consider TryFrom + validation
+impl From<u8> for ControlByte {
+ fn from(byte: u8) -> Self {
+ Self(byte)
+ }
+}
+
+impl From<ControlByte> for u8 {
+ fn from(cb: ControlByte) -> Self {
+ cb.0
+ }
+}
diff --git a/rust/trezor-thp/src/crc32.rs b/rust/trezor-thp/src/crc32.rs
new file mode 100644
index 00000000..27219a40
--- /dev/null
+++ b/rust/trezor-thp/src/crc32.rs
@@ -0,0 +1,83 @@
+#![allow(dead_code)]
+
+pub struct Crc32 {
+ value: u32,
+}
+
+static CRC32TAB: [u32; 16] = [
+ 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
+ 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c,
+];
+
+pub const CHECKSUM_LEN: usize = 4;
+
+impl Crc32 {
+ pub fn new() -> Self {
+ Self { value: u32::MAX }
+ }
+
+ pub fn update(&mut self, data: &[u8]) {
+ for b in data {
+ self.value ^= *b as u32;
+ self.value = CRC32TAB[(self.value & 0x0f) as usize] ^ (self.value >> 4);
+ self.value = CRC32TAB[(self.value & 0x0f) as usize] ^ (self.value >> 4);
+ }
+ }
+
+ pub fn finalize(&self) -> [u8; CHECKSUM_LEN] {
+ let inverted = self.value ^ u32::MAX;
+ inverted.to_be_bytes()
+ }
+}
+
+pub fn digest(data: &[u8]) -> [u8; CHECKSUM_LEN] {
+ let mut crc = Crc32::new();
+ crc.update(data);
+ crc.finalize()
+}
+
+pub fn verify_payload(buffer: &[u8]) -> Option<&[u8]> {
+ let (payload, digest_in) = buffer.split_last_chunk::<CHECKSUM_LEN>()?;
+ let digest_computed = digest(payload);
+ (digest_computed == *digest_in).then_some(payload)
+}
+
+pub fn verify(data: &[u8]) -> bool {
+ verify_payload(data).is_some()
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const CRC32_VECTORS: &[(&[u8], &str)] = &[
+ (b"", "00000000"),
+ (b"a", "e8b7be43"),
+ (b"abc", "352441c2"),
+ (b"message digest", "20159d7f"),
+ (b"abcdefghijklmnopqrstuvwxyz", "4c2750bd"),
+ (
+ b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+ "1fc2e6d2",
+ ),
+ (
+ b"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
+ "7ca94a72",
+ ),
+ ];
+
+ #[test]
+ fn test_no_update() {
+ let out = Crc32::new().finalize();
+ let out_hex = hex::encode(out);
+ assert_eq!(out_hex, "00000000");
+ }
+
+ #[test]
+ fn test_vectors() {
+ for (data, expected) in CRC32_VECTORS {
+ let out_hex = hex::encode(digest(data));
+ assert_eq!(out_hex, *expected);
+ }
+ }
+}
diff --git a/rust/trezor-thp/src/error.rs b/rust/trezor-thp/src/error.rs
new file mode 100644
index 00000000..ef2864b1
--- /dev/null
+++ b/rust/trezor-thp/src/error.rs
@@ -0,0 +1,39 @@
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(Clone, Copy, PartialEq)]
+#[repr(u8)]
+pub enum TransportError {
+ TransportBusy = 1,
+ UnallocatedChannel = 2,
+ DecryptionFailed = 3,
+ DeviceLocked = 5,
+}
+
+impl TryFrom<u8> for TransportError {
+ type Error = Error;
+
+ fn try_from(val: u8) -> Result<Self> {
+ Ok(match val {
+ 1 => Self::TransportBusy,
+ 2 => Self::UnallocatedChannel,
+ 3 => Self::DecryptionFailed,
+ 5 => Self::DeviceLocked,
+ _ => return Err(Error::OutOfBounds),
+ })
+ }
+}
+
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+pub enum Error {
+ /// Numeric field has forbidden value.
+ OutOfBounds,
+ /// Invalid data/operation from crate user.
+ UnexpectedInput,
+ /// Invalid data from the wire.
+ MalformedData,
+ /// Checksum doesn't match.
+ InvalidDigest,
+ /// Provided buffer is too small.
+ InsufficientBuffer,
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
diff --git a/rust/trezor-thp/src/fragment.rs b/rust/trezor-thp/src/fragment.rs
new file mode 100644
index 00000000..37ee8a22
--- /dev/null
+++ b/rust/trezor-thp/src/fragment.rs
@@ -0,0 +1,354 @@
+use crate::Role;
+use crate::alternating_bit::SyncBits;
+use crate::header::Header;
+use crate::{
+ crc32::{CHECKSUM_LEN, Crc32},
+ error::{Error, Result},
+};
+
+pub struct Fragmenter<R: Role> {
+ header: Header<R>,
+ sync_bits: SyncBits,
+ offset: usize,
+ checksum: Crc32,
+ crc_offset: usize,
+}
+
+impl<R: Role> Fragmenter<R> {
+ pub fn new(header: Header<R>, sync_bits: SyncBits, payload: &[u8]) -> Result<Self> {
+ if payload.len() + CHECKSUM_LEN != header.payload_len().into() {
+ return Err(Error::UnexpectedInput);
+ }
+ Ok(Self {
+ header,
+ sync_bits,
+ offset: 0,
+ checksum: Crc32::new(),
+ crc_offset: 0,
+ })
+ }
+
+ pub fn next(&mut self, payload: &[u8], dest: &mut [u8]) -> Result<bool> {
+ const MIN_PACKET_SIZE: usize = Header::<crate::Device>::new_ack(0).header_len() + 1;
+ if dest.len() < MIN_PACKET_SIZE {
+ return Err(Error::InsufficientBuffer);
+ }
+ if payload.len() + CHECKSUM_LEN != self.header.payload_len().into() {
+ // buffer changed since new
+ return Err(Error::UnexpectedInput);
+ }
+ if self.is_done() {
+ return Ok(false);
+ }
+
+ let header_len = if self.offset == 0 {
+ let header_len = self
+ .header
+ .to_bytes(self.sync_bits, dest)
+ .ok_or(Error::UnexpectedInput)?;
+ self.checksum.update(&dest[..header_len]);
+ header_len
+ } else {
+ let cont_header = Header::<R>::new_continuation(self.header.channel_id());
+ cont_header
+ .to_bytes(SyncBits::new(), dest)
+ .ok_or(Error::UnexpectedInput)?
+ };
+ let mut rest = &mut dest[header_len..];
+
+ if self.offset < payload.len() {
+ let source = &payload[self.offset..];
+ let nbytes = source.len().min(rest.len());
+ rest[..nbytes].copy_from_slice(&source[..nbytes]);
+ self.checksum.update(&source[..nbytes]);
+ self.offset += nbytes;
+ rest = &mut rest[nbytes..];
+ }
+
+ if self.offset >= payload.len() && self.crc_offset < CHECKSUM_LEN {
+ let crc = self.checksum.finalize();
+ let crc = crc.get(self.crc_offset..).ok_or(Error::UnexpectedInput)?;
+ let nbytes = crc.len().min(rest.len());
+ rest[..nbytes].copy_from_slice(&crc[..nbytes]);
+ self.crc_offset += nbytes;
+ rest = &mut rest[nbytes..];
+ }
+
+ rest.fill(0u8); // zero-pad last fragment
+ Ok(true)
+ }
+
+ pub fn is_done(&self) -> bool {
+ let payload_done = self.offset + CHECKSUM_LEN >= self.header.payload_len().into();
+ let crc_done = self.crc_offset >= CHECKSUM_LEN;
+ payload_done && crc_done
+ }
+
+ /// Shortcut to serialize packet into buffer known to be large enough.
+ pub fn single(header: Header<R>, sb: SyncBits, payload: &[u8], dest: &mut [u8]) -> Result<()> {
+ let mut fragmenter = Self::new(header, sb, payload)?;
+ fragmenter.next(payload, dest)?;
+ if !fragmenter.is_done() {
+ return Err(Error::InsufficientBuffer);
+ }
+ Ok(())
+ }
+}
+
+pub struct Reassembler<R: Role> {
+ header: Header<R>,
+ offset: usize,
+ checksum: Crc32,
+}
+
+impl<R: Role> Reassembler<R> {
+ pub fn new(input: &[u8], buffer: &mut [u8]) -> Result<Self> {
+ let (header, after_header) = Header::parse(input)?;
+ if header.is_continuation() {
+ return Err(Error::UnexpectedInput);
+ }
+
+ let payload_len = header.payload_len().into();
+ if buffer.len() < payload_len {
+ return Err(Error::InsufficientBuffer);
+ }
+
+ let mut checksum = Crc32::new();
+ checksum.update(&input[..header.header_len()]);
+
+ let nbytes = after_header.len(); // Header::parse strips padding
+ buffer[..nbytes].copy_from_slice(after_header);
+
+ let checksum_bytes = (payload_len - CHECKSUM_LEN).min(nbytes);
+ checksum.update(&after_header[..checksum_bytes]);
+
+ Ok(Self {
+ header,
+ offset: nbytes,
+ checksum,
+ })
+ }
+
+ pub fn update(&mut self, input: &[u8], buffer: &mut [u8]) -> Result<()> {
+ let (header, after_header) = Header::<R>::parse(input)?;
+ if !header.is_continuation() {
+ return Err(Error::UnexpectedInput);
+ }
+
+ if header.channel_id() != self.header.channel_id() {
+ return Err(Error::OutOfBounds);
+ }
+
+ let payload_len = self.header.payload_len().into();
+ if buffer.len() < payload_len {
+ return Err(Error::InsufficientBuffer); // buffer changed since new()
+ }
+ let payload_remaining = payload_len.saturating_sub(self.offset);
+
+ let nbytes = after_header.len().min(payload_remaining); // there can be padding
+ buffer[self.offset..self.offset + nbytes].copy_from_slice(&after_header[..nbytes]);
+
+ let checksum_bytes = payload_remaining.saturating_sub(CHECKSUM_LEN).min(nbytes);
+ self.checksum.update(&after_header[..checksum_bytes]);
+
+ self.offset += nbytes;
+ Ok(())
+ }
+
+ pub fn is_done(&self) -> bool {
+ self.offset >= self.header.payload_len().into()
+ }
+
+ pub fn verify(&self, buffer: &[u8]) -> Result<usize> {
+ if !self.is_done() {
+ return Err(Error::InvalidDigest);
+ }
+ let computed_checksum = self.checksum.finalize();
+ let length_no_checksum =
+ usize::from(self.header.payload_len()).saturating_sub(CHECKSUM_LEN);
+ let received_checksum = *buffer
+ .get(length_no_checksum..)
+ .ok_or(Error::InvalidDigest)?
+ .first_chunk::<CHECKSUM_LEN>()
+ .ok_or(Error::InvalidDigest)?;
+ if computed_checksum != received_checksum {
+ return Err(Error::InvalidDigest);
+ }
+ Ok(length_no_checksum)
+ }
+
+ pub fn header(&self) -> Header<R> {
+ self.header.clone()
+ }
+
+ // Shortcut to deserialize single packet message.
+ pub fn single<'a>(buffer: &[u8], dest: &'a mut [u8]) -> Result<(Header<R>, &'a [u8])> {
+ let reassembler = Self::new(buffer, dest)?;
+ if !reassembler.is_done() {
+ return Err(Error::MalformedData);
+ }
+ let reply_len = reassembler.verify(dest)?;
+ let header = reassembler.header;
+ Ok((header, &dest[..reply_len]))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::{Device, Host};
+ use heapless::Vec;
+
+ const MAX_FRAGMENTS: usize = 256;
+ const MAX_PACKET: usize = 128;
+ const MAX_MESSAGE: usize = 8096;
+
+ fn fragment<R: Role>(
+ header: Header<R>,
+ sb: SyncBits,
+ input: &[u8],
+ packet_size: usize,
+ ) -> Vec<Vec<u8, MAX_PACKET>, MAX_FRAGMENTS> {
+ let mut packets = Vec::new();
+ let mut fragmenter = Fragmenter::<R>::new(header, sb, input).expect("fragmenter");
+ while !fragmenter.is_done() {
+ let mut packet = Vec::new();
+ packet.resize(packet_size, 0u8).unwrap();
+ fragmenter.next(input, packet.as_mut_slice()).expect("next");
+ packets.push(packet).unwrap();
+ }
+ packets
+ }
+
+ fn assemble<R: Role>(packets: &[Vec<u8, MAX_PACKET>]) -> Vec<u8, MAX_MESSAGE> {
+ let mut received = Vec::new();
+ received.resize(MAX_MESSAGE, 0u8).unwrap();
+ let mut reassembler = Reassembler::<R>::new(packets[0].as_slice(), received.as_mut_slice())
+ .expect("reassembler");
+ for p in packets.iter().skip(1) {
+ assert!(!reassembler.is_done());
+ reassembler
+ .update(p.as_slice(), received.as_mut_slice())
+ .expect("update");
+ }
+ assert!(reassembler.is_done());
+ let received_len = reassembler.verify(received.as_slice()).expect("verify");
+ received.truncate(received_len);
+ received
+ }
+
+ #[test]
+ fn test_roundtrip() {
+ const DATA: &'static [u8] = b"The Quick Brown Fox Jumps Over the Lazy Dog The Quick Brown Fox Jumps Over the Lazy Dog";
+ const PACKET_SIZE: usize = 13;
+
+ for i in 0..DATA.len() {
+ let source = &DATA[..i];
+ let channel_id = i as u16;
+ let header = Header::<Host>::new_encrypted(channel_id, source);
+
+ let packets = fragment(header, SyncBits::new(), source, PACKET_SIZE);
+ // println!("message len: {}, packets: {}", i, packets.len());
+ // packets.iter().for_each(|p| println!("{}", hex::encode(&p)));
+ let assembled = assemble::<Device>(&packets);
+
+ let expected_hex = hex::encode(source);
+ let received_hex = hex::encode(assembled.as_slice());
+ assert_eq!(received_hex, expected_hex);
+ }
+ }
+
+ // follwing vectors and tests adapted from test_trezor.wire.thp.writer.py
+
+ const EMPTY_PAYLOAD_EXPECTED: &str = "0412340004edbd479c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
+ const SHORT_PAYLOAD_EXPECTED: &str = "041234000507ac292947000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
+ const LONGER_PAYLOAD_EXPECTED: &[&str] = &[
+ "0412340104000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a",
+ "8012343b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677",
+ "80123478797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4",
+ "801234b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1",
+ "801234f2f3f4f5f6f7f8f9fafbfcfdfeffc5ecc4ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ ];
+ const EVEN_LONGER_PAYLOADS_EXPECTED: &[&str] = &[
+ "0412340804000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a",
+ "8012343b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f7071727374757677",
+ "80123478797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4",
+ "801234b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1",
+ "801234f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e",
+ "8012342f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b",
+ "8012346c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8",
+ "801234a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5",
+ "801234e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122",
+ "801234232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
+ "801234606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c",
+ "8012349d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9",
+ "801234dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f10111213141516",
+ "8012341718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253",
+ "8012345455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f90",
+ "8012349192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccd",
+ "801234cecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a",
+ "8012340b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f4041424344454647",
+ "80123448494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f8081828384",
+ "80123485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1",
+ "801234c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfe",
+ "801234ff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b",
+ "8012343c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778",
+ "801234797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5",
+ "801234b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2",
+ "801234f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f",
+ "801234303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c",
+ "8012346d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9",
+ "801234aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6",
+ "801234e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
+ "8012342425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60",
+ "8012346162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d",
+ "8012349e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9da",
+ "801234dbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff13fe4cae0000000000000000000000000000000000000000",
+ ];
+
+ const PACKET_LEN: usize = 64;
+ const CHANNEL_ID: u16 = 4660;
+
+ #[test]
+ fn test_write_empty_payload() {
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, &[]);
+ let packets = fragment(header, SyncBits::new(), &[], PACKET_LEN);
+ assert_eq!(packets.len(), 1);
+ assert_eq!(hex::encode(&packets[0]), EMPTY_PAYLOAD_EXPECTED);
+ }
+
+ #[test]
+ fn test_write_short_payload() {
+ let data = &[0x07];
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, data);
+ let packets = fragment(header, SyncBits::new(), data, PACKET_LEN);
+ assert_eq!(packets.len(), 1);
+ assert_eq!(hex::encode(&packets[0]), SHORT_PAYLOAD_EXPECTED);
+ }
+
+ #[test]
+ fn test_write_longer_payload() {
+ let data: Vec<u8, 256> = (0..=255).collect();
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, &data);
+ let packets = fragment(header, SyncBits::new(), &data, PACKET_LEN);
+ assert_eq!(packets.len(), LONGER_PAYLOAD_EXPECTED.len());
+ packets
+ .iter()
+ .zip(LONGER_PAYLOAD_EXPECTED)
+ .for_each(|(got, expected)| assert_eq!(&hex::encode(got), expected));
+ }
+
+ #[test]
+ fn test_write_even_longer_payload() {
+ let data: Vec<u8, 2048> = (0..2048u16)
+ .map(|n| u8::try_from(n & 0xff).unwrap())
+ .collect();
+ let header = Header::<Device>::new_encrypted(CHANNEL_ID, &data);
+ let packets = fragment(header, SyncBits::new(), &data, PACKET_LEN);
+ assert_eq!(packets.len(), EVEN_LONGER_PAYLOADS_EXPECTED.len());
+ packets
+ .iter()
+ .zip(EVEN_LONGER_PAYLOADS_EXPECTED)
+ .for_each(|(got, expected)| assert_eq!(&hex::encode(got), expected));
+ }
+}
diff --git a/rust/trezor-thp/src/header.rs b/rust/trezor-thp/src/header.rs
new file mode 100644
index 00000000..1195d217
--- /dev/null
+++ b/rust/trezor-thp/src/header.rs
@@ -0,0 +1,584 @@
+use crate::Role;
+use crate::alternating_bit::SyncBits;
+use crate::control_byte::{self, ControlByte};
+use crate::crc32;
+use crate::error::{Error, Result};
+
+use core::marker::PhantomData;
+
+const CHECKSUM_LEN: u16 = crc32::CHECKSUM_LEN as u16;
+const NONCE_LEN: u16 = 8;
+const MAX_PAYLOAD_LEN: u16 = 60000;
+
+const MAX_CHANNEL_ID: u16 = 0xFFEF;
+const BROADCAST_CHANNEL_ID: u16 = 0xFFFF;
+
+/// Represents packet header, i.e. control byte, channel id and possibly payload length.
+/// Please note that `seq_bit` and `ack_bit` which are also part of the header are handled separately.
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(Clone, PartialEq)]
+pub enum Header<R: Role> {
+ Continuation {
+ channel_id: u16,
+ },
+ Ack {
+ channel_id: u16,
+ _phantom: PhantomData<R>,
+ },
+ CodecV1Request {
+ is_continuation: bool,
+ },
+ CodecV1Response,
+ ChannelAllocationRequest,
+ ChannelAllocationResponse {
+ payload_len: u16,
+ },
+ TransportError {
+ channel_id: u16,
+ },
+ Ping,
+ Pong,
+ Handshake {
+ phase: HandshakeMessage,
+ channel_id: u16,
+ payload_len: u16,
+ },
+ Encrypted {
+ channel_id: u16,
+ payload_len: u16,
+ },
+}
+
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(Clone, PartialEq)]
+pub enum HandshakeMessage {
+ InitiationRequest,
+ InitiationResponse,
+ CompletionRequest,
+ CompletionResponse,
+}
+
+pub const fn channel_id_valid(channel_id: u16) -> bool {
+ channel_id <= MAX_CHANNEL_ID || channel_id == BROADCAST_CHANNEL_ID
+}
+
+fn parse_u16(buffer: &[u8]) -> Result<(u16, &[u8])> {
+ let (bytes, rest) = buffer
+ .split_first_chunk::<2>()
+ .ok_or(Error::MalformedData)?;
+ Ok((u16::from_be_bytes(*bytes), rest))
+}
+
+impl<R: Role> Header<R> {
+ const INIT_LEN: usize = 5;
+ const CONT_LEN: usize = 3;
+
+ /// Parse header from a byte slice. Return remaining subslice on success.
+ /// Note: sync bits are discarded and need to be obtained from input buffer separately.
+ pub fn parse(buffer: &[u8]) -> Result<(Self, &[u8])> {
+ let (first_byte, rest) = buffer.split_first().ok_or(Error::MalformedData)?;
+ let cb = ControlByte::from(*first_byte);
+ if cb.is_codec_v1() {
+ if R::is_host() {
+ return Ok((Header::CodecV1Response, &[]));
+ } else {
+ let is_continuation = !matches!(rest.get(..2), Some(b"##"));
+ return Ok((Header::CodecV1Request { is_continuation }, &[]));
+ }
+ }
+ let (channel_id, rest) = parse_u16(rest)?;
+ if !channel_id_valid(channel_id) {
+ return Err(Error::OutOfBounds);
+ }
+ if cb.is_continuation() {
+ return Ok((Header::Continuation { channel_id }, rest));
+ }
+ let (payload_len, rest) = parse_u16(rest)?;
+ if payload_len > MAX_PAYLOAD_LEN {
+ return Err(Error::OutOfBounds);
+ }
+ // strip padding if there is any
+ let without_padding = rest.len().min(payload_len.into());
+ let rest = &rest[..without_padding];
+ if let Some(fixed) = Self::parse_fixed(cb, channel_id, payload_len)? {
+ return Ok((fixed, rest));
+ }
+ if let Some(phase) = HandshakeMessage::from_u8::<R>(cb.into()) {
+ return Ok((
+ Self::Handshake {
+ phase,
+ channel_id,
+ payload_len,
+ },
+ rest,
+ ));
+ }
+ if cb.is_encrypted_transport() {
+ return Ok((
+ Self::Encrypted {
+ channel_id,
+ payload_len,
+ },
+ rest,
+ ));
+ }
+ if R::is_host() && channel_id == BROADCAST_CHANNEL_ID && cb.is_channel_allocation_response()
+ {
+ return Ok((Self::ChannelAllocationResponse { payload_len }, rest));
+ }
+ Err(Error::MalformedData)
+ }
+
+ fn parse_fixed(cb: ControlByte, channel_id: u16, payload_len: u16) -> Result<Option<Self>> {
+ let res = if cb.is_ack() {
+ Self::Ack {
+ channel_id,
+ _phantom: PhantomData,
+ }
+ } else if cb.is_error() {
+ Self::TransportError { channel_id }
+ } else if channel_id == BROADCAST_CHANNEL_ID {
+ if cb.is_channel_allocation_request() && !R::is_host() {
+ Header::ChannelAllocationRequest
+ } else if cb.is_ping() && !R::is_host() {
+ Header::Ping
+ } else if cb.is_pong() && R::is_host() {
+ Header::Pong
+ } else {
+ return Ok(None);
+ }
+ } else {
+ return Ok(None);
+ };
+ if res.payload_len() == payload_len {
+ Ok(Some(res))
+ } else {
+ Err(Error::MalformedData)
+ }
+ }
+
+ fn control_byte(&self, sync_bits: SyncBits) -> Option<ControlByte> {
+ let cb = match self {
+ Self::Continuation { .. } => control_byte::CONTINUATION_PACKET,
+ Self::Ack { .. } => control_byte::ACK_MESSAGE,
+ Self::ChannelAllocationRequest if R::is_host() => control_byte::CHANNEL_ALLOCATION_REQ,
+ Self::ChannelAllocationResponse { .. } if !R::is_host() => {
+ control_byte::CHANNEL_ALLOCATION_RES
+ }
+ Self::TransportError { .. } => control_byte::ERROR,
+ Self::Ping if R::is_host() => control_byte::PING,
+ Self::Pong if !R::is_host() => control_byte::PONG,
+ Self::Handshake { phase, .. } => phase.to_u8::<R>()?,
+ Self::Encrypted { .. } => control_byte::ENCRYPTED_TRANSPORT,
+ _ => return None,
+ };
+ let mut cb = ControlByte::from(cb);
+ if self.is_ack() && sync_bits.seq_bit() {
+ // ACK must have seq_bit=0
+ return None;
+ } else if self.is_ack() || self.is_encrypted() || self.is_handshake() {
+ cb = cb.with_sync_bits(sync_bits)
+ }
+ Some(cb)
+ }
+
+ /// Serialize header to a buffer. Returns length of the result on success.
+ pub fn to_bytes(&self, sync_bits: SyncBits, dest: &mut [u8]) -> Option<usize> {
+ if let Self::Continuation { channel_id } = self {
+ return if dest.len() < Self::CONT_LEN {
+ None
+ } else {
+ dest[0] = control_byte::CONTINUATION_PACKET;
+ dest[1..3].copy_from_slice(&channel_id.to_be_bytes());
+ Some(Self::CONT_LEN)
+ };
+ }
+ if dest.len() < Self::INIT_LEN {
+ return None;
+ }
+ let length = self.payload_len();
+ dest[0] = self.control_byte(sync_bits)?.into();
+ dest[1..3].copy_from_slice(&self.channel_id().to_be_bytes());
+ dest[3..5].copy_from_slice(&length.to_be_bytes());
+ Some(Self::INIT_LEN)
+ }
+
+ pub const fn channel_id(&self) -> u16 {
+ match self {
+ Self::Continuation { channel_id } => *channel_id,
+ Self::Ack { channel_id, .. } => *channel_id,
+ Self::TransportError { channel_id, .. } => *channel_id,
+ Self::Handshake {
+ phase: _,
+ channel_id,
+ ..
+ } => *channel_id,
+ Self::Encrypted { channel_id, .. } => *channel_id,
+ _ => BROADCAST_CHANNEL_ID,
+ }
+ }
+
+ /// Payload length including checksum. Messages without checksum return 0.
+ pub const fn payload_len(&self) -> u16 {
+ match self {
+ Self::Continuation { .. } => 0,
+ Self::Ack { .. } => CHECKSUM_LEN,
+ Self::CodecV1Request { .. } | Self::CodecV1Response => 0,
+ Self::ChannelAllocationRequest => NONCE_LEN + CHECKSUM_LEN,
+ Self::ChannelAllocationResponse { payload_len } => *payload_len,
+ Self::TransportError { .. } => 1 + CHECKSUM_LEN,
+ Self::Ping => NONCE_LEN + CHECKSUM_LEN,
+ Self::Pong => NONCE_LEN + CHECKSUM_LEN,
+ Self::Handshake {
+ phase: _,
+ channel_id: _,
+ payload_len,
+ } => *payload_len,
+ Self::Encrypted {
+ channel_id: _,
+ payload_len,
+ } => *payload_len,
+ }
+ }
+
+ pub const fn header_len(&self) -> usize {
+ match self {
+ Self::Continuation { .. } => Self::CONT_LEN,
+ _ => Self::INIT_LEN,
+ }
+ }
+
+ pub const fn new_continuation(channel_id: u16) -> Self {
+ Self::Continuation { channel_id }
+ }
+
+ pub const fn new_encrypted(channel_id: u16, payload: &[u8]) -> Self {
+ // FIXME validate payload length, channel id?
+ Self::Encrypted {
+ channel_id,
+ payload_len: payload.len() as u16 + CHECKSUM_LEN,
+ }
+ }
+
+ pub const fn new_error(channel_id: u16) -> Self {
+ Self::TransportError { channel_id }
+ }
+
+ pub const fn new_ack(channel_id: u16) -> Self {
+ Self::Ack {
+ channel_id,
+ _phantom: PhantomData,
+ }
+ }
+
+ pub const fn new_ping() -> Self {
+ Self::Ping
+ }
+
+ pub const fn new_pong() -> Self {
+ Self::Pong
+ }
+
+ pub const fn is_continuation(&self) -> bool {
+ matches!(self, Self::Continuation { .. })
+ }
+
+ pub const fn is_ping(&self) -> bool {
+ matches!(self, Self::Ping)
+ }
+
+ pub const fn is_pong(&self) -> bool {
+ matches!(self, Self::Pong)
+ }
+
+ pub const fn is_handshake(&self) -> bool {
+ matches!(self, Self::Handshake { .. })
+ }
+
+ pub const fn is_encrypted(&self) -> bool {
+ matches!(self, Self::Encrypted { .. })
+ }
+
+ pub const fn is_ack(&self) -> bool {
+ matches!(self, Self::Ack { .. })
+ }
+}
+
+impl HandshakeMessage {
+ pub fn from_u8<R: Role>(val: u8) -> Option<Self> {
+ let masked = val & control_byte::DATA_MASK;
+ Some(match masked {
+ control_byte::HANDSHAKE_INIT_REQ if !R::is_host() => {
+ HandshakeMessage::InitiationRequest
+ }
+ control_byte::HANDSHAKE_INIT_RES if R::is_host() => {
+ HandshakeMessage::InitiationResponse
+ }
+ control_byte::HANDSHAKE_COMP_REQ if !R::is_host() => {
+ HandshakeMessage::CompletionRequest
+ }
+ control_byte::HANDSHAKE_COMP_RES if R::is_host() => {
+ HandshakeMessage::CompletionResponse
+ }
+ _ => return None,
+ })
+ }
+
+ pub fn to_u8<R: Role>(&self) -> Option<u8> {
+ Some(match self {
+ HandshakeMessage::InitiationRequest if R::is_host() => control_byte::HANDSHAKE_INIT_REQ,
+ HandshakeMessage::InitiationResponse if !R::is_host() => {
+ control_byte::HANDSHAKE_INIT_RES
+ }
+ HandshakeMessage::CompletionRequest if R::is_host() => control_byte::HANDSHAKE_COMP_REQ,
+ HandshakeMessage::CompletionResponse if !R::is_host() => {
+ control_byte::HANDSHAKE_COMP_RES
+ }
+ _ => return None,
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::{Device, Host};
+
+ impl<R: Role> Header<R> {
+ fn transmute<S: Role>(&self) -> Header<S> {
+ let s = self.clone();
+ match s {
+ Self::Continuation { channel_id } => Header::<S>::Continuation { channel_id },
+ Self::Ack { channel_id, .. } => Header::<S>::Ack {
+ channel_id,
+ _phantom: PhantomData,
+ },
+ Self::CodecV1Request { is_continuation } => {
+ Header::<S>::CodecV1Request { is_continuation }
+ }
+ Self::CodecV1Response => Header::<S>::CodecV1Response,
+ Self::ChannelAllocationRequest => Header::<S>::ChannelAllocationRequest,
+ Self::ChannelAllocationResponse { payload_len } => {
+ Header::<S>::ChannelAllocationResponse { payload_len }
+ }
+ Self::TransportError { channel_id } => Header::<S>::TransportError { channel_id },
+ Self::Ping => Header::<S>::Ping,
+ Self::Pong => Header::<S>::Pong,
+ Self::Handshake {
+ phase,
+ channel_id,
+ payload_len,
+ } => Header::<S>::Handshake {
+ phase,
+ channel_id,
+ payload_len,
+ },
+ Self::Encrypted {
+ channel_id,
+ payload_len,
+ } => Header::<S>::Encrypted {
+ channel_id,
+ payload_len,
+ },
+ }
+ }
+ }
+
+ const VECTORS_GOOD: &[(&str, Header<Device>)] = &[
+ ("801234", Header::Continuation { channel_id: 0x1234 }),
+ (
+ "80ffff",
+ Header::Continuation {
+ channel_id: BROADCAST_CHANNEL_ID,
+ },
+ ),
+ ("8012345678", Header::Continuation { channel_id: 0x1234 }),
+ ("201337000463061764", Header::new_ack(0x1337)),
+ ("20ffff000460132e1c", Header::new_ack(BROADCAST_CHANNEL_ID)),
+ (
+ "041337000457479ea0",
+ Header::Encrypted {
+ channel_id: 0x1337,
+ payload_len: 4,
+ },
+ ),
+ (
+ "041337000457479ea0041337000457479ea0",
+ Header::Encrypted {
+ channel_id: 0x1337,
+ payload_len: 4,
+ },
+ ),
+ (
+ "42ffff000502744a5ed00000",
+ Header::TransportError {
+ channel_id: BROADCAST_CHANNEL_ID,
+ },
+ ),
+ (
+ "4213370005022a97b2e7",
+ Header::TransportError { channel_id: 0x1337 },
+ ),
+ ];
+
+ #[test]
+ fn test_parse_good() {
+ for (input, expected) in VECTORS_GOOD {
+ let input = hex::decode(input).unwrap();
+ let (header_device, _) = Header::<Device>::parse(&input).expect("parse1");
+ assert_eq!(header_device, *expected);
+ let (header_host, _) = Header::<Host>::parse(&input).expect("parse2");
+ assert_eq!(header_host, expected.transmute());
+ }
+ }
+
+ #[test]
+ fn test_serialize() {
+ for (bytes, header) in VECTORS_GOOD {
+ let bytes = hex::decode(bytes).unwrap();
+ let mut buffer = [0u8; 256];
+ let sb = SyncBits::new();
+ let len = header.to_bytes(sb, &mut buffer).expect("to_bytes1");
+ assert!(len > 0);
+ assert_eq!(&buffer[..len], &bytes[..len]);
+ let len = header
+ .transmute::<Host>()
+ .to_bytes(sb, &mut buffer)
+ .expect("to_bytes2");
+ assert!(len > 0);
+ assert_eq!(&buffer[..len], &bytes[..len]);
+ }
+ }
+
+ #[test]
+ fn test_serialize_sync() {
+ for (bytes, header) in VECTORS_GOOD {
+ if !header.is_encrypted() && !header.is_handshake() {
+ continue; // no sync bits
+ }
+ let bytes = hex::decode(bytes).unwrap();
+ let mut buffer = [0u8; 256];
+ let sb = SyncBits::new().with_seq_bit(true);
+ let len = header.to_bytes(sb, &mut buffer).expect("to_bytes");
+ assert!(len > 0);
+ assert_ne!(&buffer[..len], &bytes[..len]);
+ }
+ }
+
+ const VECTORS_GOOD_DEVICE: &[(&str, Header<Device>)] = &[
+ // ("3f2323", Header::CodecV1Request { is_continuation: false }),
+ // ("3f6666", Header::CodecV1Request { is_continuation: true }),
+ (
+ "40ffff000caaaaaaaaaaaaaaaab67b3b6b",
+ Header::ChannelAllocationRequest,
+ ),
+ ("43ffff000cbaaaaaaaaaaaaaaa770a668e", Header::Ping),
+ (
+ "00000f000cbaaaaaaaaaaaaaaac8978de2",
+ Header::Handshake {
+ phase: HandshakeMessage::InitiationRequest,
+ channel_id: 0x000f,
+ payload_len: 12,
+ },
+ ),
+ (
+ "02000f000cff00ff00aaaaaaaa4cb96673",
+ Header::Handshake {
+ phase: HandshakeMessage::CompletionRequest,
+ channel_id: 0x000f,
+ payload_len: 12,
+ },
+ ),
+ ];
+
+ #[test]
+ fn test_role_device() {
+ for (bytes, header) in VECTORS_GOOD_DEVICE {
+ let bytes = hex::decode(bytes).unwrap();
+ let (parsed_header, _) = Header::<Device>::parse(&bytes).expect("parse");
+ assert_eq!(parsed_header, *header);
+ let res = Header::<Host>::parse(&bytes);
+ assert!(res.is_err());
+
+ let mut buffer = [0u8; 256];
+ let sb = SyncBits::new();
+ let len = header
+ .transmute::<Host>()
+ .to_bytes(sb, &mut buffer)
+ .expect("to_bytes");
+ assert!(len > 0);
+ assert_eq!(&buffer[..len], &bytes[..len]);
+ let res = header.to_bytes(sb, &mut buffer);
+ assert!(res.is_none());
+ }
+ }
+
+ const VECTORS_GOOD_HOST: &[(&str, Header<Host>)] = &[
+ (
+ "41ffff029aaaaaaaaaaaaaaaaab67b3b6b",
+ Header::ChannelAllocationResponse { payload_len: 666 },
+ ),
+ ("44ffff000cbaaaaaaaaaaaaaaa770a668e", Header::Pong),
+ (
+ "01000f000cbaaaaaaaaaaaaaaac8978de2",
+ Header::Handshake {
+ phase: HandshakeMessage::InitiationResponse,
+ channel_id: 0x000f,
+ payload_len: 12,
+ },
+ ),
+ (
+ "03000f000cff00ff00aaaaaaaa4cb96673",
+ Header::Handshake {
+ phase: HandshakeMessage::CompletionResponse,
+ channel_id: 0x000f,
+ payload_len: 12,
+ },
+ ),
+ ];
+
+ #[test]
+ fn test_role_host() {
+ for (bytes, header) in VECTORS_GOOD_HOST {
+ let bytes = hex::decode(bytes).unwrap();
+ let (parsed_header, _) = Header::<Host>::parse(&bytes).expect("parse");
+ assert_eq!(parsed_header, *header);
+ let res = Header::<Device>::parse(&bytes);
+ assert!(res.is_err());
+
+ let mut buffer = [0u8; 256];
+ let sb = SyncBits::new();
+ let len = header
+ .transmute::<Device>()
+ .to_bytes(sb, &mut buffer)
+ .expect("to_bytes");
+ assert!(len > 0);
+ assert_eq!(&buffer[..len], &bytes[..len]);
+ let res = header.to_bytes(sb, &mut buffer);
+ assert!(res.is_none());
+ }
+ }
+
+ const VECTORS_BAD_SHORT: &[&str] = &[
+ "", "00", "04", "80", "39", "0000", "0400", "8000", "4f6f", "040000", "04000000",
+ "42000000", "00000000",
+ ];
+
+ const VECTORS_BAD: &[&str] = &[
+ "7f00000000", // bad control byte
+ "04fff00001", // bad channel id
+ "041111ffee", // invalid length field
+ "80fffe0000", // bad channel id
+ ];
+
+ #[test]
+ fn test_parse_bad() {
+ for v in VECTORS_BAD_SHORT.iter().chain(VECTORS_BAD) {
+ let v = hex::decode(v).unwrap();
+ let res = Header::<Device>::parse(&v);
+ assert!(res.is_err());
+ let res = Header::<Host>::parse(&v);
+ assert!(res.is_err());
+ }
+ }
+}
diff --git a/rust/trezor-thp/src/lib.rs b/rust/trezor-thp/src/lib.rs
new file mode 100644
index 00000000..77c069eb
--- /dev/null
+++ b/rust/trezor-thp/src/lib.rs
@@ -0,0 +1,35 @@
+//! Trezor Host Protocol implementation in Rust.
+
+#![no_std]
+#![forbid(unsafe_code)]
+
+pub mod alternating_bit;
+mod control_byte;
+mod crc32;
+pub mod error;
+pub mod fragment;
+pub mod header;
+
+pub trait Role: Clone + PartialEq {
+ fn is_host() -> bool;
+}
+
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(Clone, PartialEq)]
+pub struct Host;
+
+impl Role for Host {
+ fn is_host() -> bool {
+ true
+ }
+}
+
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(Clone, PartialEq)]
+pub struct Device;
+
+impl Role for Device {
+ fn is_host() -> bool {
+ false
+ }
+}
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.