What changed, and why it matters
This commit adds a new Rust-based USB transport layer for BitBox hardware wallets. It reassembles U2F HID messages and routes vendor commands to the existing async USB task system. The change also hardens the async executor by handing out owned responses instead of copying into caller buffers, and by resetting stale state when a task is cancelled or times out. There is no vendor statement that this fixes a security bug; it reads as a feature/refactoring commit with defensive hardening.
Review the new U2FHID parser for off-by-one and state-machine issues, especially around reassembly timeouts, sequence-number wrapping, and concurrent-CID handling. Verify that the async executor's `cancel()` and timeout paths cannot drop a response that a host is still expecting. Confirm the report-queue capacity change does not exhaust embedded RAM. Treat as a normal feature/hardening commit unless additional disclosure material emerges.
Security signals we found
New USB transport parsing code handles length, sequence, timeout, and channel-busy checks defensively
Async executor now returns owned responses, removing a potential buffer-size mismatch / out-of-bounds copy path
Cancel/timeout now resets pending next_request state and unread results, reducing risk of state confusion
Queue sizing is explicitly tied to worst-case U2FHID message length
No explicit security claim, CVE, or attribution in commit message or diff
Evidence from the diff
The patch introduces the bitbox-u2fhid crate implementing U2FHID report reassembly for vendor commands, plus a hww::transport module that maps HWW request types (NEW/RETRY/CANCEL/INFO) onto the async USB executor. The executor’s copy_response is replaced by take_response, returning an owned Vec<u8> and eliminating a caller-supplied buffer. cancel() now also clears any pending next_request input and unread final results, preventing stale executor state from blocking new tasks. The USB report queue capacity is raised to 129 reports to fit one worst-case U2FHID message. Simulators are updated to use the new transport. The commit also stubs out BitBox03 platform support.
Changed components
src/rust/bitbox-u2fhid/src/lib.rssrc/rust/bitbox02-rust/src/hww/transport.rssrc/rust/bitbox02-rust/src/async_usb.rssrc/rust/bitbox02-rust-c/src/async_usb.rssrc/rust/bitbox-usb-report-queue/src/lib.rstest/simulator-graphical/src/main.rstest/simulator-graphical-bb03/src/main.rsInspect captured patch +1329 / −102
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index 4a47aa6..edf1dbf 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -174,6 +174,13 @@ dependencies = [
"hex_lit",
]
+[[package]]
+name = "bitbox-u2fhid"
+version = "0.1.0"
+dependencies = [
+ "bitbox-usb-report-queue",
+]
+
[[package]]
name = "bitbox-usb-report-queue"
version = "0.1.0"
@@ -222,6 +229,7 @@ dependencies = [
"bitbox-executor",
"bitbox-hal",
"bitbox-secp256k1",
+ "bitbox-u2fhid",
"bitbox-usb-report-queue",
"bitbox02",
"bitbox02-noise",
diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml
index 790f28b..4492324 100644
--- a/src/rust/Cargo.toml
+++ b/src/rust/Cargo.toml
@@ -3,6 +3,7 @@
[workspace]
members = [
+ "bitbox-u2fhid",
"bitbox02-rust-c",
"bitbox02-rust",
"bitbox-usb-report-queue",
diff --git a/src/rust/bitbox-hal/src/memory.rs b/src/rust/bitbox-hal/src/memory.rs
index adb525f..8edbea8 100644
--- a/src/rust/bitbox-hal/src/memory.rs
+++ b/src/rust/bitbox-hal/src/memory.rs
@@ -33,6 +33,7 @@ pub enum SecurechipType {
pub enum Platform {
BitBox02,
BitBox02Plus,
+ BitBox03,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
diff --git a/src/rust/bitbox-hal/src/system.rs b/src/rust/bitbox-hal/src/system.rs
index 5384a1c..0434d65 100644
--- a/src/rust/bitbox-hal/src/system.rs
+++ b/src/rust/bitbox-hal/src/system.rs
@@ -18,6 +18,7 @@ pub trait System {
/// cancelled).
fn communication_timeout_reset(&mut self, value: i16);
+ fn is_btconly(&mut self) -> bool;
fn reboot(&mut self) -> !;
fn reboot_to_bootloader(&mut self) -> !;
fn reset_ble(&mut self);
diff --git a/src/rust/bitbox-u2fhid/Cargo.toml b/src/rust/bitbox-u2fhid/Cargo.toml
new file mode 100644
index 0000000..feb91e7
--- /dev/null
+++ b/src/rust/bitbox-u2fhid/Cargo.toml
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: Apache-2.0
+
+[package]
+name = "bitbox-u2fhid"
+version = "0.1.0"
+edition = "2024"
+license = "Apache-2.0"
+
+[dependencies]
+bitbox-usb-report-queue = { path = "../bitbox-usb-report-queue" }
diff --git a/src/rust/bitbox-u2fhid/src/lib.rs b/src/rust/bitbox-u2fhid/src/lib.rs
new file mode 100644
index 0000000..c07f0e5
--- /dev/null
+++ b/src/rust/bitbox-u2fhid/src/lib.rs
@@ -0,0 +1,667 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+
+extern crate alloc;
+
+use alloc::vec::Vec;
+use bitbox_usb_report_queue::UsbReportQueue;
+
+pub const REPORT_SIZE: usize = 64;
+const INIT_HEADER_SIZE: usize = 7;
+const CONT_HEADER_SIZE: usize = 5;
+const INIT_PAYLOAD_SIZE: usize = REPORT_SIZE - INIT_HEADER_SIZE;
+const CONT_PAYLOAD_SIZE: usize = REPORT_SIZE - CONT_HEADER_SIZE;
+pub const MAX_MESSAGE_SIZE: usize = INIT_PAYLOAD_SIZE + 128 * CONT_PAYLOAD_SIZE;
+const MESSAGE_TIMEOUT_MS: u64 = 500;
+
+const BROADCAST_CID: u32 = 0xffff_ffff;
+
+const TYPE_INIT: u8 = 0x80;
+// U2F-native commands such as PING/MSG/LOCK/WINK/SYNC are intentionally not implemented while the
+// Rust USB port is focused on HWW. Reintroduce them when porting the U2F transport as well.
+// const COMMAND_PING: u8 = TYPE_INIT | 0x01;
+// const COMMAND_MSG: u8 = TYPE_INIT | 0x03;
+// const COMMAND_LOCK: u8 = TYPE_INIT | 0x04;
+// const COMMAND_WINK: u8 = TYPE_INIT | 0x08;
+// const COMMAND_SYNC: u8 = TYPE_INIT | 0x3c;
+//
+// We keep the INIT command ID because `usb_frame.c` treats it specially during frame
+// reassembly/resynchronization, even though completed INIT requests are currently rejected with
+// INVALID_CMD on the HWW interface.
+const COMMAND_INIT: u8 = TYPE_INIT | 0x06;
+const COMMAND_ERROR: u8 = TYPE_INIT | 0x3f;
+pub const COMMAND_VENDOR_FIRST: u8 = TYPE_INIT | 0x40;
+const COMMAND_VENDOR_LAST: u8 = TYPE_INIT | 0x7f;
+
+// U2F's INIT response reports interface/version/capabilities via a device-info structure. The
+// Rust USB port is focused on HWW for now, so this stays commented out until the U2F transport is
+// ported as well.
+//
+// pub const CAPABILITY_WINK: u8 = 0x01;
+//
+// #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+// pub struct DeviceInfo {
+// pub interface_version: u8,
+// pub version_major: u8,
+// pub version_minor: u8,
+// pub version_build: u8,
+// pub capabilities: u8,
+// }
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[repr(u8)]
+pub enum ErrorCode {
+ InvalidCmd = 0x01,
+ InvalidPar = 0x02,
+ InvalidLen = 0x03,
+ InvalidSeq = 0x04,
+ MsgTimeout = 0x05,
+ ChannelBusy = 0x06,
+ LockRequired = 0x0a,
+ InvalidCid = 0x0b,
+ Other = 0x7f,
+}
+
+pub trait VendorCommandHandler {
+ fn handle_vendor_command(
+ &mut self,
+ cid: u32,
+ cmd: u8,
+ payload: &[u8],
+ now_ms: u64,
+ ) -> Result<Vec<u8>, ErrorCode>;
+
+ fn tick(&mut self, _now_ms: u64) {}
+}
+
+struct ReceiveState {
+ buffer: [u8; MAX_MESSAGE_SIZE],
+ cid: u32,
+ cmd: u8,
+ total_len: usize,
+ received_len: usize,
+ next_seq: u8,
+ deadline_ms: Option<u64>,
+ initialized: bool,
+}
+
+impl ReceiveState {
+ const fn new() -> Self {
+ Self {
+ buffer: [0; MAX_MESSAGE_SIZE],
+ cid: 0,
+ cmd: 0,
+ total_len: 0,
+ received_len: 0,
+ next_seq: 0,
+ deadline_ms: None,
+ initialized: false,
+ }
+ }
+
+ fn reset(&mut self) {
+ self.cid = 0;
+ self.cmd = 0;
+ self.total_len = 0;
+ self.received_len = 0;
+ self.next_seq = 0;
+ self.deadline_ms = None;
+ self.initialized = false;
+ }
+}
+
+pub struct U2fHid<V> {
+ // U2F INIT would need this device info in order to answer INIT requests.
+ // device_info: DeviceInfo,
+ vendor_handler: V,
+ out_queue: UsbReportQueue,
+ receive_state: ReceiveState,
+ // U2F INIT would also need channel allocation state.
+ // next_cid: u32,
+ sending_cid: Option<u32>,
+}
+
+impl<V: VendorCommandHandler> U2fHid<V> {
+ // U2F INIT would take `device_info` as an additional constructor argument.
+ // pub fn new(device_info: DeviceInfo, vendor_handler: V) -> Self {
+ pub fn new(vendor_handler: V) -> Self {
+ Self {
+ vendor_handler,
+ out_queue: UsbReportQueue::new(),
+ receive_state: ReceiveState::new(),
+ sending_cid: None,
+ }
+ }
+
+ pub fn handle_report(&mut self, report: &[u8; REPORT_SIZE], now_ms: u64) {
+ self.tick(now_ms);
+
+ let cid = u32::from_be_bytes(report[..4].try_into().unwrap());
+ let type_byte = report[4];
+ if self.sending_cid.is_some() {
+ self.handle_while_sending(cid, type_byte);
+ return;
+ }
+
+ if type_byte & TYPE_INIT != 0 {
+ self.handle_init_packet(report, cid, type_byte, now_ms);
+ } else {
+ self.handle_cont_packet(report, cid, type_byte, now_ms);
+ }
+ }
+
+ pub fn tick(&mut self, now_ms: u64) {
+ self.vendor_handler.tick(now_ms);
+
+ let Some(deadline_ms) = self.receive_state.deadline_ms else {
+ return;
+ };
+ if now_ms < deadline_ms {
+ return;
+ }
+ let cid = self.receive_state.cid;
+ self.receive_state.reset();
+ self.enqueue_error(cid, ErrorCode::MsgTimeout);
+ }
+
+ pub fn pull_report(&mut self) -> Option<[u8; REPORT_SIZE]> {
+ let report = self.out_queue.pull();
+ if report.is_some() && self.out_queue.peek().is_none() {
+ self.sending_cid = None;
+ }
+ report
+ }
+
+ pub fn handler(&self) -> &V {
+ &self.vendor_handler
+ }
+
+ pub fn handler_mut(&mut self) -> &mut V {
+ &mut self.vendor_handler
+ }
+
+ fn handle_while_sending(&mut self, cid: u32, type_byte: u8) {
+ if type_byte & TYPE_INIT == 0 {
+ return;
+ }
+ self.enqueue_error(cid, ErrorCode::ChannelBusy);
+ }
+
+ fn handle_init_packet(&mut self, report: &[u8; REPORT_SIZE], cid: u32, cmd: u8, now_ms: u64) {
+ if self.receive_state.initialized {
+ if cid != self.receive_state.cid && self.receive_state.cmd == COMMAND_INIT {
+ self.enqueue_error(cid, ErrorCode::ChannelBusy);
+ return;
+ }
+ if cid != self.receive_state.cid
+ && cmd != COMMAND_INIT
+ && self.receive_state.cmd != COMMAND_INIT
+ {
+ self.enqueue_error(cid, ErrorCode::ChannelBusy);
+ return;
+ }
+ if cid == self.receive_state.cid && cmd != COMMAND_INIT {
+ self.receive_state.reset();
+ self.enqueue_error(cid, ErrorCode::InvalidSeq);
+ return;
+ }
+ }
+
+ let total_len = u16::from_be_bytes([report[5], report[6]]) as usize;
+ if total_len > MAX_MESSAGE_SIZE {
+ self.receive_state.reset();
+ self.enqueue_error(cid, ErrorCode::InvalidLen);
+ return;
+ }
+
+ self.receive_state.reset();
+ self.receive_state.cid = cid;
+ self.receive_state.cmd = cmd;
+ self.receive_state.total_len = total_len;
+ self.receive_state.initialized = true;
+
+ let init_copy_len = core::cmp::min(total_len, INIT_PAYLOAD_SIZE);
+ self.receive_state.buffer[..init_copy_len]
+ .copy_from_slice(&report[INIT_HEADER_SIZE..INIT_HEADER_SIZE + init_copy_len]);
+ self.receive_state.received_len = init_copy_len;
+ self.receive_state.next_seq = 0;
+ self.receive_state.deadline_ms = if init_copy_len < total_len {
+ Some(now_ms.saturating_add(MESSAGE_TIMEOUT_MS))
+ } else {
+ None
+ };
+
+ if init_copy_len == total_len {
+ self.finish_message(now_ms);
+ }
+ }
+
+ fn handle_cont_packet(&mut self, report: &[u8; REPORT_SIZE], cid: u32, seq: u8, now_ms: u64) {
+ if !self.receive_state.initialized {
+ return;
+ }
+ if cid != self.receive_state.cid {
+ self.enqueue_error(cid, ErrorCode::ChannelBusy);
+ return;
+ }
+ if seq != self.receive_state.next_seq {
+ self.receive_state.reset();
+ self.enqueue_error(cid, ErrorCode::InvalidSeq);
+ return;
+ }
+ if self.receive_state.received_len >= self.receive_state.total_len
+ || self.receive_state.received_len + CONT_PAYLOAD_SIZE > MAX_MESSAGE_SIZE
+ {
+ self.receive_state.reset();
+ self.enqueue_error(cid, ErrorCode::InvalidLen);
+ return;
+ }
+
+ let copy_len = core::cmp::min(
+ self.receive_state.total_len - self.receive_state.received_len,
+ CONT_PAYLOAD_SIZE,
+ );
+ let start = self.receive_state.received_len;
+ let end = start + copy_len;
+ self.receive_state.buffer[start..end]
+ .copy_from_slice(&report[CONT_HEADER_SIZE..CONT_HEADER_SIZE + copy_len]);
+ self.receive_state.received_len = end;
+ self.receive_state.next_seq = self.receive_state.next_seq.wrapping_add(1);
+ self.receive_state.deadline_ms =
+ if self.receive_state.received_len < self.receive_state.total_len {
+ Some(now_ms.saturating_add(MESSAGE_TIMEOUT_MS))
+ } else {
+ None
+ };
+
+ if self.receive_state.received_len == self.receive_state.total_len {
+ self.finish_message(now_ms);
+ }
+ }
+
+ fn finish_message(&mut self, now_ms: u64) {
+ let cid = self.receive_state.cid;
+ let cmd = self.receive_state.cmd;
+ let total_len = self.receive_state.total_len;
+ let mut payload = Vec::with_capacity(total_len);
+ payload.extend_from_slice(&self.receive_state.buffer[..total_len]);
+ self.receive_state.reset();
+
+ let response = self.handle_message(cid, cmd, payload.as_slice(), now_ms);
+ match response {
+ Ok(response_payload) => {
+ if self
+ .enqueue_response(cmd, cid, response_payload.as_slice())
+ .is_err()
+ {
+ self.out_queue.clear();
+ self.enqueue_error(cid, ErrorCode::Other);
+ } else if self.out_queue.peek().is_some() {
+ self.sending_cid = Some(cid);
+ }
+ }
+ Err(err) => self.enqueue_error(cid, err),
+ }
+ }
+
+ fn handle_message(
+ &mut self,
+ cid: u32,
+ cmd: u8,
+ payload: &[u8],
+ now_ms: u64,
+ ) -> Result<Vec<u8>, ErrorCode> {
+ match cmd {
+ // The HWW-only port rejects the U2F-native commands for now.
+ // COMMAND_INIT => self.handle_init(cid, payload),
+ // COMMAND_PING => self.handle_ping(cid, payload),
+ // COMMAND_WINK => self.handle_wink(cid, payload),
+ // COMMAND_LOCK => Err(ErrorCode::InvalidCmd),
+ // COMMAND_SYNC => Err(ErrorCode::InvalidCmd),
+ // COMMAND_MSG | COMMAND_ERROR => Err(ErrorCode::InvalidCmd),
+ COMMAND_VENDOR_FIRST..=COMMAND_VENDOR_LAST => {
+ // Even in the HWW-only port we keep U2FHID's reserved CID checks intact.
+ if cid == 0 || cid == BROADCAST_CID {
+ return Err(ErrorCode::InvalidCid);
+ }
+ self.vendor_handler
+ .handle_vendor_command(cid, cmd, payload, now_ms)
+ }
+ _ => Err(ErrorCode::InvalidCmd),
+ }
+ }
+
+ // When the U2F transport is ported, restore these helpers together with `DeviceInfo`,
+ // `next_cid`, and the constructor argument above.
+ //
+ // fn handle_init(&mut self, cid: u32, payload: &[u8]) -> Result<Vec<u8>, ErrorCode> {
+ // if payload.len() != 8 {
+ // return Err(ErrorCode::InvalidLen);
+ // }
+ // if cid == 0 {
+ // return Err(ErrorCode::InvalidCid);
+ // }
+ //
+ // let allocated_cid = if cid == BROADCAST_CID {
+ // self.allocate_cid()
+ // } else {
+ // cid
+ // };
+ //
+ // let mut response = Vec::with_capacity(17);
+ // response.extend_from_slice(payload);
+ // response.extend_from_slice(&allocated_cid.to_be_bytes());
+ // response.push(self.device_info.interface_version);
+ // response.push(self.device_info.version_major);
+ // response.push(self.device_info.version_minor);
+ // response.push(self.device_info.version_build);
+ // response.push(self.device_info.capabilities);
+ // Ok(response)
+ // }
+ //
+ // fn handle_ping(&self, cid: u32, payload: &[u8]) -> Result<Vec<u8>, ErrorCode> {
+ // if cid == 0 || cid == BROADCAST_CID {
+ // return Err(ErrorCode::InvalidCid);
+ // }
+ // Ok(payload.to_vec())
+ // }
+ //
+ // fn handle_wink(&self, cid: u32, payload: &[u8]) -> Result<Vec<u8>, ErrorCode> {
+ // if cid == 0 || cid == BROADCAST_CID {
+ // return Err(ErrorCode::InvalidCid);
+ // }
+ // if !payload.is_empty() {
+ // return Err(ErrorCode::InvalidLen);
+ // }
+ // Ok(Vec::new())
+ // }
+ //
+ // fn allocate_cid(&mut self) -> u32 {
+ // loop {
+ // let cid = self.next_cid;
+ // self.next_cid = self.next_cid.wrapping_add(1);
+ // if self.next_cid == 0 || self.next_cid == BROADCAST_CID {
+ // self.next_cid = 1;
+ // }
+ // if cid != 0 && cid != BROADCAST_CID {
+ // return cid;
+ // }
+ // }
+ // }
+
+ fn enqueue_error(&mut self, cid: u32, err: ErrorCode) {
+ let _ = self.enqueue_response(COMMAND_ERROR, cid, &[err as u8]);
+ if self.out_queue.peek().is_some() {
+ self.sending_cid = Some(cid);
+ }
+ }
+
+ fn enqueue_response(&mut self, cmd: u8, cid: u32, payload: &[u8]) -> Result<(), ()> {
+ if payload.len() > MAX_MESSAGE_SIZE {
+ return Err(());
+ }
+
+ let mut report = [0u8; REPORT_SIZE];
+ report[..4].copy_from_slice(&cid.to_be_bytes());
+ report[4] = cmd;
+ report[5..7].copy_from_slice(&(payload.len() as u16).to_be_bytes());
+
+ let first_len = core::cmp::min(payload.len(), INIT_PAYLOAD_SIZE);
+ report[INIT_HEADER_SIZE..INIT_HEADER_SIZE + first_len]
+ .copy_from_slice(&payload[..first_len]);
+ if self.out_queue.push(&report)
+ != bitbox_usb_report_queue::UsbReportQueueError::USB_REPORT_QUEUE_ERR_NONE
+ {
+ return Err(());
+ }
+
+ let mut offset = first_len;
+ let mut seq = 0u8;
+ while offset < payload.len() {
+ let mut cont_report = [0u8; REPORT_SIZE];
+ cont_report[..4].copy_from_slice(&cid.to_be_bytes());
+ cont_report[4] = seq;
+ let chunk_len = core::cmp::min(payload.len() - offset, CONT_PAYLOAD_SIZE);
+ cont_report[CONT_HEADER_SIZE..CONT_HEADER_SIZE + chunk_len]
+ .copy_from_slice(&payload[offset..offset + chunk_len]);
+ if self.out_queue.push(&cont_report)
+ != bitbox_usb_report_queue::UsbReportQueueError::USB_REPORT_QUEUE_ERR_NONE
+ {
+ return Err(());
+ }
+ offset += chunk_len;
+ seq = seq.wrapping_add(1);
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate std;
+
+ use super::*;
+ use alloc::vec;
+
+ const TEST_CID: u32 = 0x0102_0304;
+ const TEST_VENDOR_CMD: u8 = COMMAND_VENDOR_FIRST + 1;
+
+ #[derive(Default)]
+ struct EchoVendorHandler {
+ seen: Vec<(u32, u8, Vec<u8>)>,
+ response: Vec<u8>,
+ error: Option<ErrorCode>,
+ }
+
+ impl VendorCommandHandler for EchoVendorHandler {
+ fn handle_vendor_command(
+ &mut self,
+ cid: u32,
+ cmd: u8,
+ payload: &[u8],
+ _now_ms: u64,
+ ) -> Result<Vec<u8>, ErrorCode> {
+ self.seen.push((cid, cmd, payload.to_vec()));
+ match self.error {
+ Some(err) => Err(err),
+ None => Ok(self.response.clone()),
+ }
+ }
+ }
+
+ fn make_transport() -> U2fHid<EchoVendorHandler> {
+ U2fHid::new(EchoVendorHandler::default())
+ }
+
+ fn request_reports(cid: u32, cmd: u8, payload: &[u8]) -> Vec<[u8; REPORT_SIZE]> {
+ let mut reports = Vec::new();
+ let mut report = [0u8; REPORT_SIZE];
+ report[..4].copy_from_slice(&cid.to_be_bytes());
+ report[4] = cmd;
+ report[5..7].copy_from_slice(&(payload.len() as u16).to_be_bytes());
+ let first_len = core::cmp::min(payload.len(), INIT_PAYLOAD_SIZE);
+ report[INIT_HEADER_SIZE..INIT_HEADER_SIZE + first_len]
+ .copy_from_slice(&payload[..first_len]);
+ reports.push(report);
+
+ let mut offset = first_len;
+ let mut seq = 0u8;
+ while offset < payload.len() {
+ let mut cont_report = [0u8; REPORT_SIZE];
+ cont_report[..4].copy_from_slice(&cid.to_be_bytes());
+ cont_report[4] = seq;
+ let chunk_len = core::cmp::min(payload.len() - offset, CONT_PAYLOAD_SIZE);
+ cont_report[CONT_HEADER_SIZE..CONT_HEADER_SIZE + chunk_len]
+ .copy_from_slice(&payload[offset..offset + chunk_len]);
+ reports.push(cont_report);
+ offset += chunk_len;
+ seq = seq.wrapping_add(1);
+ }
+ reports
+ }
+
+ fn drain_reports<V: VendorCommandHandler>(transport: &mut U2fHid<V>) -> Vec<[u8; REPORT_SIZE]> {
+ let mut reports = Vec::new();
+ while let Some(report) = transport.pull_report() {
+ reports.push(report);
+ }
+ reports
+ }
+
+ fn parse_message(reports: &[[u8; REPORT_SIZE]]) -> (u32, u8, Vec<u8>) {
+ assert!(!reports.is_empty());
+ let cid = u32::from_be_bytes(reports[0][..4].try_into().unwrap());
+ let cmd = reports[0][4];
+ let len = u16::from_be_bytes(reports[0][5..7].try_into().unwrap()) as usize;
+ let mut payload = Vec::with_capacity(len);
+ let first_len = core::cmp::min(len, INIT_PAYLOAD_SIZE);
+ payload.extend_from_slice(&reports[0][INIT_HEADER_SIZE..INIT_HEADER_SIZE + first_len]);
+ let mut offset = first_len;
+ for (expected_seq, report) in reports[1..].iter().enumerate() {
+ assert_eq!(report[4], expected_seq as u8);
+ let chunk_len = core::cmp::min(len - offset, CONT_PAYLOAD_SIZE);
+ payload.extend_from_slice(&report[CONT_HEADER_SIZE..CONT_HEADER_SIZE + chunk_len]);
+ offset += chunk_len;
+ }
+ (cid, cmd, payload)
+ }
+
+ #[test]
+ fn test_u2f_native_commands_return_invalid_cmd() {
+ const TEST_COMMAND_PING: u8 = TYPE_INIT | 0x01;
+ const TEST_COMMAND_WINK: u8 = TYPE_INIT | 0x08;
+
+ let mut transport = make_transport();
+ for (cmd, payload) in [
+ (COMMAND_INIT, b"12345678".as_slice()),
+ (TEST_COMMAND_PING, b"hello".as_slice()),
+ (TEST_COMMAND_WINK, b"".as_slice()),
+ ] {
+ transport.handle_report(&request_reports(TEST_CID, cmd, payload)[0], 0);
+ let (_, response_cmd, response_payload) = parse_message(&drain_reports(&mut transport));
+ assert_eq!(response_cmd, COMMAND_ERROR);
+ assert_eq!(response_payload, vec![ErrorCode::InvalidCmd as u8]);
+ }
+ }
+
+ #[test]
+ fn test_max_reports() {
+ const MAX_REPORTS: usize =
+ 1 + (MAX_MESSAGE_SIZE - INIT_PAYLOAD_SIZE).div_ceil(CONT_PAYLOAD_SIZE);
+
+ assert_eq!(MAX_REPORTS, 129);
+ }
+
+ #[test]
+ fn test_vendor_multi_packet_roundtrip() {
+ let mut transport = make_transport();
+ let payload = [0x42; 100];
+ transport.handler_mut().response = payload.to_vec();
+ for report in request_reports(TEST_CID, TEST_VENDOR_CMD, &payload) {
+ transport.handle_report(&report, 0);
+ }
+
+ assert_eq!(
+ transport.handler().seen,
+ vec![(TEST_CID, TEST_VENDOR_CMD, payload.to_vec())]
+ );
+ let reports = drain_reports(&mut transport);
+ let (cid, cmd, response_payload) = parse_message(&reports);
+ assert_eq!(cid, TEST_CID);
+ assert_eq!(cmd, TEST_VENDOR_CMD);
+ assert_eq!(response_payload, payload);
+ }
+
+ #[test]
+ fn test_unused_bytes_are_zeroed() {
+ let mut transport = make_transport();
+ transport.handler_mut().response = b"x".to_vec();
+ transport.handle_report(
+ &request_reports(TEST_CID, TEST_VENDOR_CMD, b"request")[0],
+ 0,
+ );
+ let report = transport.pull_report().unwrap();
+ assert!(report[INIT_HEADER_SIZE + 1..].iter().all(|&byte| byte == 0));
+ }
+
+ #[test]
+ fn test_invalid_sequence_returns_error() {
+ let mut transport = make_transport();
+ let mut reports = request_reports(TEST_CID, TEST_VENDOR_CMD, &[0x55; 100]);
+ reports[1][4] = 1;
+ transport.handle_report(&reports[0], 0);
+ transport.handle_report(&reports[1], 0);
+
+ let (_, cmd, payload) = parse_message(&drain_reports(&mut transport));
+ assert_eq!(cmd, COMMAND_ERROR);
+ assert_eq!(payload, vec![ErrorCode::InvalidSeq as u8]);
+ }
+
+ #[test]
+ fn test_invalid_length_returns_error() {
+ let mut transport = make_transport();
+ let mut report = [0u8; REPORT_SIZE];
+ report[..4].copy_from_slice(&TEST_CID.to_be_bytes());
+ report[4] = TEST_VENDOR_CMD;
+ report[5..7].copy_from_slice(&((MAX_MESSAGE_SIZE + 1) as u16).to_be_bytes());
+ transport.handle_report(&report, 0);
+
+ let (_, cmd, payload) = parse_message(&drain_reports(&mut transport));
+ assert_eq!(cmd, COMMAND_ERROR);
+ assert_eq!(payload, vec![ErrorCode::InvalidLen as u8]);
+ }
+
+ #[test]
+ fn test_unsolicited_continuation_is_ignored() {
+ let mut transport = make_transport();
+ let mut report = [0u8; REPORT_SIZE];
+ report[..4].copy_from_slice(&TEST_CID.to_be_bytes());
+ report[4] = 0;
+ transport.handle_report(&report, 0);
+ assert!(transport.pull_report().is_none());
+ }
+
+ #[test]
+ fn test_channel_busy_while_receiving() {
+ let mut transport = make_transport();
+ let first = request_reports(TEST_CID, TEST_VENDOR_CMD, &[0x11; 100]);
+ let second = request_reports(0xaabb_ccdd, TEST_VENDOR_CMD, b"busy");
+ transport.handle_report(&first[0], 0);
+ transport.handle_report(&second[0], 0);
+
+ let (cid, cmd, payload) = parse_message(&drain_reports(&mut transport));
+ assert_eq!(cid, 0xaabb_ccdd);
+ assert_eq!(cmd, COMMAND_ERROR);
+ assert_eq!(payload, vec![ErrorCode::ChannelBusy as u8]);
+ }
+
+ #[test]
+ fn test_message_timeout() {
+ let mut transport = make_transport();
+ let report = request_reports(TEST_CID, TEST_VENDOR_CMD, &[0x22; 100])[0];
+ transport.handle_report(&report, 0);
+ transport.tick(MESSAGE_TIMEOUT_MS + 1);
+
+ let (_, cmd, payload) = parse_message(&drain_reports(&mut transport));
+ assert_eq!(cmd, COMMAND_ERROR);
+ assert_eq!(payload, vec![ErrorCode::MsgTimeout as u8]);
+ }
+
+ #[test]
+ fn test_max_size_vendor_roundtrip() {
+ let mut transport = make_transport();
+ let payload = vec![0x5a; MAX_MESSAGE_SIZE];
+ transport.handler_mut().response = payload.clone();
+ for report in request_reports(TEST_CID, TEST_VENDOR_CMD, &payload) {
+ transport.handle_report(&report, 0);
+ }
+
+ let reports = drain_reports(&mut transport);
+ assert_eq!(reports.len(), 129);
+ let (_, cmd, response_payload) = parse_message(&reports);
+ assert_eq!(cmd, TEST_VENDOR_CMD);
+ assert_eq!(response_payload, payload);
+ }
+}
diff --git a/src/rust/bitbox-usb-report-queue/src/lib.rs b/src/rust/bitbox-usb-report-queue/src/lib.rs
index 1dd95d7..f3e8246 100644
--- a/src/rust/bitbox-usb-report-queue/src/lib.rs
+++ b/src/rust/bitbox-usb-report-queue/src/lib.rs
@@ -7,13 +7,11 @@ extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::VecDeque;
+// The queue must hold one full worst-case U2FHID message, which can span 129 reports.
+// Keep this value fixed here instead of deriving it from the transport layer.
+// See `bitbox_u2fhid::tests::test_max_reports`.
+const MAX_SIZE: usize = 129;
const USB_REPORT_SIZE: usize = 64;
-// Keep this in sync with USB_DATA_MAX_LEN in src/usb/usb_frame.h.
-const USB_DATA_MAX_LEN: usize = 7609;
-const USB_REPORT_QUEUE_NUM_REPORTS: usize = USB_DATA_MAX_LEN / USB_REPORT_SIZE;
-// Preserve the previous effective capacity of the manual ring buffer, which
-// kept one slot empty to distinguish full from empty.
-const USB_REPORT_QUEUE_MAX_LEN: usize = USB_REPORT_QUEUE_NUM_REPORTS - 1;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
@@ -50,7 +48,7 @@ impl UsbReportQueue {
}
pub fn push(&mut self, report: &[u8; USB_REPORT_SIZE]) -> UsbReportQueueError {
- if self.reports.len() >= USB_REPORT_QUEUE_MAX_LEN {
+ if self.reports.len() >= MAX_SIZE {
return UsbReportQueueError::USB_REPORT_QUEUE_ERR_FULL;
}
self.reports.push_back(*report);
@@ -245,7 +243,7 @@ mod tests {
fn test_overflow_returns_full() {
let mut queue = UsbReportQueue::new();
- for i in 0..USB_REPORT_QUEUE_MAX_LEN {
+ for i in 0..MAX_SIZE {
assert!(matches!(
queue.push(&report((i % 251) as u8)),
UsbReportQueueError::USB_REPORT_QUEUE_ERR_NONE
@@ -262,7 +260,7 @@ mod tests {
fn test_wraparound_fifo_order() {
let mut queue = UsbReportQueue::new();
- for i in 0..USB_REPORT_QUEUE_MAX_LEN {
+ for i in 0..MAX_SIZE {
assert!(matches!(
queue.push(&report((i % 251) as u8)),
UsbReportQueueError::USB_REPORT_QUEUE_ERR_NONE
@@ -280,7 +278,7 @@ mod tests {
));
}
- for i in 16..USB_REPORT_QUEUE_MAX_LEN {
+ for i in 16..MAX_SIZE {
assert_eq!(queue.pull().unwrap(), report((i % 251) as u8));
}
diff --git a/src/rust/bitbox02-rust-c/src/async_usb.rs b/src/rust/bitbox02-rust-c/src/async_usb.rs
index 2d78099..04b94c2 100644
--- a/src/rust/bitbox02-rust-c/src/async_usb.rs
+++ b/src/rust/bitbox02-rust-c/src/async_usb.rs
@@ -28,15 +28,17 @@ pub enum UsbResponse {
/// Polls for a result of an async usb task. If a result is available, it is copied to `out`.
///
/// Returns:
-/// `UsbResponseNack` if on ask is running.
+/// `UsbResponseNack` if no task is running.
/// `UsbResponseAck` if the result was copied.
/// `UsbResponseNotReady` if a task is running but not yet complete.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_async_usb_copy_response(out: *mut bitbox02::buffer_t) -> UsbResponse {
- use bitbox02_rust::async_usb::{CopyResponseErr, copy_response};
+ use bitbox02_rust::async_usb::{CopyResponseErr, take_response};
let dst = unsafe { core::slice::from_raw_parts_mut((*out).data, (*out).max_len) };
- match copy_response(dst) {
- Ok(len) => {
+ match take_response() {
+ Ok(response) => {
+ let len = response.len();
+ dst[..len].copy_from_slice(&response);
unsafe { (*out).len = len as _ };
UsbResponse::UsbResponseAck
}
@@ -59,6 +61,6 @@ pub extern "C" fn rust_async_usb_on_request_hww(usb_in: util::bytes::Bytes) {
}
#[unsafe(no_mangle)]
-pub extern "C" fn rust_async_usb_cancel() -> bool {
+pub extern "C" fn rust_async_usb_cancel() {
bitbox02_rust::async_usb::cancel()
}
diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml
index 4a727d4..d57c9d7 100644
--- a/src/rust/bitbox02-rust/Cargo.toml
+++ b/src/rust/bitbox02-rust/Cargo.toml
@@ -17,6 +17,7 @@ doctest = false
bitbox-hal = { path = "../bitbox-hal" }
bitbox-da14531 = { path = "../bitbox-da14531" }
bitbox02 = { path = "../bitbox02" }
+bitbox-u2fhid = { path = "../bitbox-u2fhid" }
bitbox-bytequeue = { path = "../bitbox-bytequeue" }
bitbox-usb-report-queue = { path = "../bitbox-usb-report-queue" }
bitbox-secp256k1 = { path = "../bitbox-secp256k1" }
@@ -102,7 +103,7 @@ testing = [
c-unit-testing = []
-simulator-graphical = []
+simulator-graphical = ["bitbox02/simulator-graphical"]
firmware = []
diff --git a/src/rust/bitbox02-rust/src/async_usb.rs b/src/rust/bitbox02-rust/src/async_usb.rs
index 342dd6a..26d58cd 100644
--- a/src/rust/bitbox02-rust/src/async_usb.rs
+++ b/src/rust/bitbox02-rust/src/async_usb.rs
@@ -37,7 +37,7 @@ struct SafeNextRequest(RefCell<Option<UsbIn>>);
unsafe impl Sync for SafeNextRequest {}
/// An option resolving the `next_request()` future. It is `Some(...)` once a request we've been
-/// waiting for arrives. See `next_requset()` for more details.
+/// waiting for arrives. See `next_request()` for more details.
static NEXT_REQUEST: SafeNextRequest = SafeNextRequest(RefCell::new(None));
/// Describes the global state of an api query. The documentation of
@@ -105,6 +105,10 @@ pub fn waiting_for_next_request() -> bool {
)
}
+pub fn is_idle() -> bool {
+ matches!(*USB_TASK_STATE.0.borrow(), UsbTaskState::Nothing)
+}
+
/// Resolves the `next_request()` future. `waiting_for_next_request()` must be true when calling
/// this, otherwise this function panics.
pub fn on_next_request(usb_in: &[u8]) {
@@ -173,50 +177,45 @@ pub enum CopyResponseErr {
/// To be called in response to the host asking for the result of a
/// task.
///
-/// If a result is available (state = ResultAvailable), this copies
-/// the usb response to `dst` and moves the state to `Nothing`, and
-/// returns the Ok(<number of bytes written>).
+/// If a result is available (state = `ResultAvailable`), this returns the usb response and moves
+/// the state to `Nothing`.
///
-/// If there is no task running, returns `Err(CopyResponseErr::NotReady)` if a task is pending and a
-/// response is expected in the future, or `Err(CopyResponseErr::NotRunning)` if no task is running.
-pub fn copy_response(dst: &mut [u8]) -> Result<usize, CopyResponseErr> {
+/// If a task is pending and a response is expected in the future, returns
+/// `Err(CopyResponseErr::NotReady)`. If no task is running, returns
+/// `Err(CopyResponseErr::NotRunning)`.
+pub fn take_response() -> Result<UsbOut, CopyResponseErr> {
let mut state = USB_TASK_STATE.0.borrow_mut();
- match *state {
+ match &mut *state {
UsbTaskState::Nothing => Err(CopyResponseErr::NotRunning),
- UsbTaskState::Running(Some(_), ref mut next_request_state) => {
+ UsbTaskState::Running(Some(_), next_request_state) => {
if let WaitingForNextRequestState::SendingResponse(response) = next_request_state {
- let len = response.len();
- dst[..len].copy_from_slice(response);
+ let response = core::mem::take(response);
*next_request_state = WaitingForNextRequestState::AwaitingRequest;
- Ok(len)
+ Ok(response)
} else {
Err(CopyResponseErr::NotReady)
}
}
UsbTaskState::Running(_, _) => Err(CopyResponseErr::NotReady),
- UsbTaskState::ResultAvailable(ref response) => {
- let len = response.len();
- dst[..len].copy_from_slice(response);
+ UsbTaskState::ResultAvailable(response) => {
+ let response = core::mem::take(response);
*state = UsbTaskState::Nothing;
- Ok(len)
+ Ok(response)
}
}
}
-/// Cancel and drop a running task. Returns true if a task was cancelled, false if no task was
-/// running.
+/// Reset all outstanding USB task state.
///
-/// Call this inside a running task only if you expect that the host may not be able to read the
-/// result (e.g. when resetting the BLE chip as part of a task), so another task can spawn
-/// afterwards immediately (before the timeout auto-cancles it), which currently would run into a
-/// panic until the response was read and the current task concluded. See the comment in `spawn()`
-pub fn cancel() -> bool {
+/// This drops a running task, any unread final response, and any pending `next_request()` input.
+/// It is used when the host disappears or when the transport times out waiting for the host to
+/// fetch a response. Call this inside a running task only if you expect that the host may not be
+/// able to read the result (e.g. when resetting the BLE chip as part of a task), so another task
+/// can spawn afterwards immediately instead of being blocked by stale executor state.
+pub fn cancel() {
+ let _ = NEXT_REQUEST.0.borrow_mut().take();
let mut state = USB_TASK_STATE.0.borrow_mut();
- if let UsbTaskState::Running(_, _) = *state {
- *state = UsbTaskState::Nothing;
- return true;
- }
- false
+ *state = UsbTaskState::Nothing;
}
/// Must be called during the execution of a usb task. This sends out the response to the host and
@@ -242,6 +241,15 @@ mod tests {
extern crate std;
use super::*;
use std::prelude::v1::*;
+ use std::sync::{Mutex, MutexGuard};
+
+ static TEST_LOCK: Mutex<()> = Mutex::new(());
+
+ fn test_guard() -> MutexGuard<'static, ()> {
+ let guard = TEST_LOCK.lock().unwrap();
+ cancel();
+ guard
+ }
fn assert_panics<F: FnOnce() + std::panic::UnwindSafe>(f: F) {
assert!(std::panic::catch_unwind(f).is_err());
@@ -250,6 +258,8 @@ mod tests {
/// Test spawning a task, spinning it, and getting the result.
#[test]
fn test_full_cycle() {
+ let _guard = test_guard();
+
async fn task(usb_in: UsbIn) -> UsbOut {
assert_eq!(usb_in, [1, 2, 3].to_vec());
[4, 5, 6, 7].to_vec()
@@ -259,39 +269,31 @@ mod tests {
}
// repeated task processing ok
for _ in 0..3 {
- let mut response = [0; 100];
-
- // No task running, can't copy response.
- assert_eq!(
- Err(CopyResponseErr::NotRunning),
- copy_response(&mut response)
- );
+ // No task running, can't take response.
+ assert_eq!(Err(CopyResponseErr::NotRunning), take_response());
spawn(task, &[1, 2, 3]);
// Can't spawn: task already running.
assert_spawn_fails();
- // Task not complete, can't copy response.
- assert_eq!(Err(CopyResponseErr::NotReady), copy_response(&mut response));
+ // Task not complete, can't take response.
+ assert_eq!(Err(CopyResponseErr::NotReady), take_response());
spin();
// Can't spawn: result not fetched yet
assert_spawn_fails();
- // Response buffer too short.
- assert_panics(move || {
- let _ = copy_response(&mut response[..1]);
- });
- assert_eq!(Ok(4), copy_response(&mut response));
// Response ok.
- assert_eq!(&response[..4], &[4, 5, 6, 7]);
+ assert_eq!(Ok(vec![4, 5, 6, 7]), take_response());
}
}
#[test]
fn test_next_request() {
+ let _guard = test_guard();
+
async fn task(usb_in: UsbIn) -> UsbOut {
assert_eq!(&usb_in, &[1, 2, 3]);
let next_req = next_request([4, 5, 6, 7].to_vec()).await;
@@ -302,13 +304,10 @@ mod tests {
[15, 16, 17].to_vec()
}
- let mut response = [0; 100];
-
spawn(task, &[1, 2, 3]);
spin();
// Intermediate response.
- assert_eq!(Ok(4), copy_response(&mut response));
- assert_eq!(&response[..4], &[4, 5, 6, 7]);
+ assert_eq!(Ok(vec![4, 5, 6, 7]), take_response());
// Send follow-up request.
assert!(waiting_for_next_request());
@@ -316,8 +315,7 @@ mod tests {
spin();
// Intermediate response.
- assert_eq!(Ok(2), copy_response(&mut response));
- assert_eq!(&response[..2], &[11, 12]);
+ assert_eq!(Ok(vec![11, 12]), take_response());
// Send follow-up request.
assert!(waiting_for_next_request());
@@ -325,7 +323,100 @@ mod tests {
spin();
// Final response.
- assert_eq!(Ok(3), copy_response(&mut response));
- assert_eq!(&response[..3], &[15, 16, 17]);
+ assert_eq!(Ok(vec![15, 16, 17]), take_response());
+ }
+
+ #[test]
+ fn test_take_response() {
+ let _guard = test_guard();
+
+ async fn task(_usb_in: UsbIn) -> UsbOut {
+ [4, 5, 6, 7].to_vec()
+ }
+
+ spawn(task, &[1, 2, 3]);
+ spin();
+
+ assert_eq!(Ok(vec![4, 5, 6, 7]), take_response());
+ assert!(is_idle());
+ }
+
+ #[test]
+ fn test_take_response_waiting_for_next_request() {
+ let _guard = test_guard();
+
+ async fn task(_usb_in: UsbIn) -> UsbOut {
+ let next_req = next_request([4, 5, 6, 7].to_vec()).await;
+ assert_eq!(&next_req, &[8, 9, 10]);
+ [11, 12].to_vec()
+ }
+
+ spawn(task, &[1, 2, 3]);
+ spin();
+
+ assert_eq!(Ok(vec![4, 5, 6, 7]), take_response());
+ assert!(waiting_for_next_request());
+
+ on_next_request(&[8, 9, 10]);
+ spin();
+
+ assert_eq!(Ok(vec![11, 12]), take_response());
+ assert!(is_idle());
+ }
+
+ #[test]
+ fn test_cancel_clears_result_available() {
+ let _guard = test_guard();
+
+ async fn task(_usb_in: UsbIn) -> UsbOut {
+ [4, 5, 6, 7].to_vec()
+ }
+
+ spawn(task, &[1, 2, 3]);
+ spin();
+ assert!(!is_idle());
+
+ cancel();
+ assert!(is_idle());
+ assert_eq!(Err(CopyResponseErr::NotRunning), take_response());
+
+ spawn(task, &[1, 2, 3]);
+ spin();
+ assert_eq!(Ok(vec![4, 5, 6, 7]), take_response());
+ }
+
+ #[test]
+ fn test_cancel_clears_pending_next_request() {
+ let _guard = test_guard();
+
+ async fn first_task(_usb_in: UsbIn) -> UsbOut {
+ let next_req = next_request([1, 2].to_vec()).await;
+ assert_eq!(&next_req, &[3, 4]);
+ [5, 6].to_vec()
+ }
+
+ async fn second_task(_usb_in: UsbIn) -> UsbOut {
+ let next_req = next_request([7].to_vec()).await;
+ assert_eq!(&next_req, &[9]);
+ [8].to_vec()
+ }
+
+ spawn(first_task, &[]);
+ spin();
+ assert_eq!(Ok(vec![1, 2]), take_response());
+ assert!(waiting_for_next_request());
+
+ on_next_request(&[3, 4]);
+ cancel();
+ assert!(is_idle());
+
+ spawn(second_task, &[]);
+ spin();
+ assert_eq!(Ok(vec![7]), take_response());
+ assert!(waiting_for_next_request());
+
+ on_next_request(&[9]);
+ spin();
+ assert_eq!(Ok(vec![8]), take_response());
}
}
diff --git a/src/rust/bitbox02-rust/src/hal/testing.rs b/src/rust/bitbox02-rust/src/hal/testing.rs
index 4698fe1..74f4c9c 100644
--- a/src/rust/bitbox02-rust/src/hal/testing.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing.rs
@@ -40,6 +40,12 @@ impl TestingHal<'_> {
}
}
+impl Default for TestingHal<'_> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
impl<'a> crate::hal::Hal for TestingHal<'a> {
type Ui = TestingUi<'a>;
type Random = TestingRandom;
diff --git a/src/rust/bitbox02-rust/src/hal/testing/system.rs b/src/rust/bitbox02-rust/src/hal/testing/system.rs
index 9337241..908bf5b 100644
--- a/src/rust/bitbox02-rust/src/hal/testing/system.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing/system.rs
@@ -2,16 +2,24 @@
pub struct TestingSystem {
ble_reset_count: u32,
+ btconly: bool,
}
impl TestingSystem {
pub fn new() -> Self {
- Self { ble_reset_count: 0 }
+ Self {
+ ble_reset_count: 0,
+ btconly: false,
+ }
}
pub fn ble_reset_count(&self) -> u32 {
self.ble_reset_count
}
+
+ pub fn set_btconly(&mut self, btconly: bool) {
+ self.btconly = btconly;
+ }
}
impl crate::hal::System for TestingSystem {
@@ -19,6 +27,10 @@ impl crate::hal::System for TestingSystem {
fn communication_timeout_reset(&mut self, _value: i16) {}
+ fn is_btconly(&mut self) -> bool {
+ self.btconly
+ }
+
fn reboot(&mut self) -> ! {
panic!("reboot called")
}
@@ -46,4 +58,12 @@ mod tests {
system.reset_ble();
assert_eq!(system.ble_reset_count(), 2);
}
+
+ #[test]
+ fn test_is_btconly() {
+ let mut system = TestingSystem::new();
+ assert!(!system.is_btconly());
+ system.set_btconly(true);
+ assert!(system.is_btconly());
+ }
}
diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs
index f0c55ee..f40f1e2 100644
--- a/src/rust/bitbox02-rust/src/hww.rs
+++ b/src/rust/bitbox02-rust/src/hww.rs
@@ -2,6 +2,8 @@
pub mod api;
pub mod noise;
+#[cfg(feature = "simulator-graphical")]
+pub mod transport;
use crate::hal::{Memory, Ui};
use alloc::vec::Vec;
diff --git a/src/rust/bitbox02-rust/src/hww/api/device_info.rs b/src/rust/bitbox02-rust/src/hww/api/device_info.rs
index e3cc386..c3402b5 100644
--- a/src/rust/bitbox02-rust/src/hww/api/device_info.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/device_info.rs
@@ -8,7 +8,7 @@ use pb::response::Response;
pub fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
let bluetooth = match hal.memory().get_platform().map_err(|_| Error::Memory)? {
- hal_memory::Platform::BitBox02Plus => {
+ hal_memory::Platform::BitBox02Plus | hal_memory::Platform::BitBox03 => {
let ble_metadata = hal.memory().ble_get_metadata();
Some(pb::device_info_response::Bluetooth {
firmware_hash: ble_metadata.allowed_firmware_hash.to_vec(),
diff --git a/src/rust/bitbox02-rust/src/hww/transport.rs b/src/rust/bitbox02-rust/src/hww/transport.rs
new file mode 100644
index 0000000..b6822b0
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hww/transport.rs
@@ -0,0 +1,364 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use alloc::vec::Vec;
+use bitbox_u2fhid::{COMMAND_VENDOR_FIRST, ErrorCode, U2fHid, VendorCommandHandler};
+
+use crate::hal::{Hal, Memory, System};
+
+const HWW_CMD: u8 = COMMAND_VENDOR_FIRST + 1;
+
+const HWW_REQ_NEW: u8 = 0;
+const HWW_REQ_RETRY: u8 = 1;
+const HWW_REQ_CANCEL: u8 = 2;
+const HWW_REQ_INFO: u8 = b'i';
+
+const HWW_RSP_ACK: u8 = 0;
+const HWW_RSP_NOT_READY: u8 = 1;
+const HWW_RSP_BUSY: u8 = 2;
+const HWW_RSP_NACK: u8 = 3;
+const USB_OUTSTANDING_OP_TIMEOUT_MS: u64 = 500;
+
+pub type HwwTransport<H> = U2fHid<HwwVendorHandler<H>>;
+
+pub fn hww_transport<H>() -> HwwTransport<H>
+where
+ H: Hal + Default + 'static,
+{
+ U2fHid::new(HwwVendorHandler::new(H::default()))
+}
+
+fn info_response<H: Hal>(hal: &mut H) -> Vec<u8> {
+ let version = crate::version::FIRMWARE_VERSION_SHORT.as_bytes();
+ let mut response = Vec::with_capacity(version.len() + 5);
+ response.push(version.len() as u8);
+ response.extend_from_slice(version);
+ response.push(match hal.memory().get_platform() {
+ Ok(crate::hal::memory::Platform::BitBox02Plus) => 0x02,
+ Ok(crate::hal::memory::Platform::BitBox03) => 0x03,
+ _ => 0x00,
+ });
+ response.push(if hal.system().is_btconly() {
+ 0x01
+ } else {
+ 0x00
+ });
+ response.push((!crate::keystore::is_locked()) as u8);
+ response.push(hal.memory().is_initialized() as u8);
+ response
+}
+
+async fn process_packet_with_hal<H>(usb_in: Vec<u8>) -> Vec<u8>
+where
+ H: Hal + Default,
+{
+ let mut hal = H::default();
+ crate::hww::process_packet(&mut hal, usb_in).await
+}
+
+fn encode_hww_response() -> Result<Vec<u8>, ErrorCode> {
+ match crate::async_usb::take_response() {
+ Ok(payload) => {
+ let mut response = Vec::with_capacity(payload.len() + 1);
+ response.push(HWW_RSP_ACK);
+ response.extend_from_slice(&payload);
+ Ok(response)
+ }
+ Err(crate::async_usb::CopyResponseErr::NotReady) => Ok(vec![HWW_RSP_NOT_READY]),
+ Err(crate::async_usb::CopyResponseErr::NotRunning) => Ok(vec![HWW_RSP_NACK]),
+ }
+}
+
+pub struct HwwVendorHandler<H> {
+ hal: H,
+ deadline_ms: Option<u64>,
+}
+
+impl<H> HwwVendorHandler<H> {
+ pub fn new(hal: H) -> Self {
+ Self {
+ hal,
+ deadline_ms: None,
+ }
+ }
+
+ fn refresh_timeout(&mut self, now_ms: u64) {
+ self.deadline_ms = if crate::async_usb::is_idle() {
+ None
+ } else {
+ Some(now_ms.saturating_add(USB_OUTSTANDING_OP_TIMEOUT_MS))
+ };
+ }
+}
+
+impl<H> VendorCommandHandler for HwwVendorHandler<H>
+where
+ H: Hal + Default + 'static,
+{
+ fn handle_vendor_command(
+ &mut self,
+ _cid: u32,
+ cmd: u8,
+ payload: &[u8],
+ now_ms: u64,
+ ) -> Result<Vec<u8>, ErrorCode> {
+ if cmd != HWW_CMD {
+ return Err(ErrorCode::InvalidCmd);
+ }
+ if payload.is_empty() {
+ return Ok(vec![HWW_RSP_NACK]);
+ }
+
+ let request = payload[0];
+ let body = &payload[1..];
+ match request {
+ HWW_REQ_INFO => {
+ // HWW_REQ_INFO is treated as a special case: it has a direct response without a
+ // status code, so it can be called independently of the firmware version and
+ // framing protocol. Before v7.0.0, there was no HWW framing layer, and the info
+ // call was an api-call using the same 'i' OP_INFO op code byte.
+ Ok(info_response(&mut self.hal))
+ }
+ HWW_REQ_NEW => {
+ // Spawn async task, which is polled in the main loop.
+ if crate::async_usb::waiting_for_next_request() {
+ crate::async_usb::on_next_request(body);
+ } else if !crate::async_usb::is_idle() {
+ return Ok(vec![HWW_RSP_BUSY]);
+ } else {
+ crate::async_usb::spawn(process_packet_with_hal::<H>, body);
+ }
+ self.refresh_timeout(now_ms);
+ // Some tasks have an 'early return' path that is not blocking. We spin the task
+ // once so we can return immediately in if there is an early return, so the client
+ // does not have to wait ~200ms for a response that can be made available
+ // immediately.
+ crate::async_usb::spin();
+ // Respond with NOT_READY if the async task needs more time, or ACK with the payload
+ // if the task already completed.
+ self.refresh_timeout(now_ms);
+ encode_hww_response()
+ }
+ HWW_REQ_RETRY => {
+ self.refresh_timeout(now_ms);
+ encode_hww_response()
+ }
+ HWW_REQ_CANCEL => {
+ // TODO: cancel async usb task.
+ Ok(vec![HWW_RSP_NACK])
+ }
+ _ => Ok(vec![HWW_RSP_NACK]),
+ }
+ }
+
+ fn tick(&mut self, now_ms: u64) {
+ if crate::async_usb::is_idle() {
+ self.deadline_ms = None;
+ return;
+ }
+ if let Some(deadline_ms) = self.deadline_ms
+ && now_ms > deadline_ms
+ {
+ crate::async_usb::cancel();
+ self.deadline_ms = None;
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate std;
+
+ use super::*;
+ use crate::hal::testing::TestingHal;
+ use crate::hal::{Memory, System, memory::Platform};
+ use std::sync::{Mutex, MutexGuard};
+
+ static TEST_LOCK: Mutex<()> = Mutex::new(());
+
+ async fn ready_task(_usb_in: Vec<u8>) -> Vec<u8> {
+ vec![0xaa, 0xbb]
+ }
+
+ async fn pending_task(_usb_in: Vec<u8>) -> Vec<u8> {
+ core::future::pending::<()>().await;
+ unreachable!()
+ }
+
+ async fn ready_after_second_spin_task(_usb_in: Vec<u8>) -> Vec<u8> {
+ let mut first_poll = true;
+ core::future::poll_fn(move |_| {
+ if core::mem::take(&mut first_poll) {
+ core::task::Poll::Pending
+ } else {
+ core::task::Poll::Ready(())
+ }
+ })
+ .await;
+ vec![0xaa, 0xbb]
+ }
+
+ async fn next_request_task(usb_in: Vec<u8>) -> Vec<u8> {
+ assert_eq!(usb_in, vec![0xaa]);
+ let next_request = crate::async_usb::next_request(vec![0xbb]).await;
+ assert_eq!(next_request, vec![0xcc]);
+ vec![0xdd]
+ }
+
+ fn test_guard() -> MutexGuard<'static, ()> {
+ let guard = TEST_LOCK.lock().unwrap();
+ crate::async_usb::cancel();
+ guard
+ }
+
+ fn handler() -> HwwVendorHandler<TestingHal<'static>> {
+ HwwVendorHandler::new(TestingHal::new())
+ }
+
+ #[test]
+ fn test_req_info() {
+ let _guard = test_guard();
+ let mut handler = handler();
+ handler.hal.memory.set_platform(Platform::BitBox02Plus);
+ handler.hal.system.set_btconly(true);
+ handler.hal.memory.set_initialized().unwrap();
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_INFO], 0)
+ .unwrap();
+ assert!(!response.is_empty());
+ assert_eq!(
+ response[0] as usize,
+ crate::version::FIRMWARE_VERSION_SHORT.len()
+ );
+ assert_eq!(response[response.len() - 4], 0x02);
+ assert_eq!(response[response.len() - 3], 0x01);
+ assert_eq!(response[response.len() - 1], 0x01);
+ }
+
+ #[test]
+ fn test_req_info_bitbox03() {
+ let _guard = test_guard();
+ let mut handler = handler();
+ handler.hal.memory.set_platform(Platform::BitBox03);
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_INFO], 0)
+ .unwrap();
+ assert!(!response.is_empty());
+ assert_eq!(response[response.len() - 4], 0x03);
+ }
+
+ #[test]
+ fn test_req_cancel() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(pending_task, &[]);
+ let mut handler = handler();
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_CANCEL], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_NACK]);
+ assert!(!crate::async_usb::is_idle());
+ crate::async_usb::cancel();
+ }
+
+ #[test]
+ fn test_req_retry_ack() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(ready_task, &[]);
+ crate::async_usb::spin();
+
+ let mut handler = handler();
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RETRY], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_ACK, 0xaa, 0xbb]);
+ }
+
+ #[test]
+ fn test_req_new_routes_follow_up_request() {
+ let _guard = test_guard();
+ crate::async_usb::cancel();
+ crate::async_usb::spawn(next_request_task, &[0xaa]);
+ crate::async_usb::spin();
+
+ let mut handler = HwwVendorHandler::new(TestingHal::new());
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RETRY], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_ACK, 0xbb]);
+ assert!(crate::async_usb::waiting_for_next_request());
+
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_NEW, 0xcc], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_ACK, 0xdd]);
+ assert!(crate::async_usb::is_idle());
+ }
+
+ #[test]
+ fn test_req_new_while_busy_returns_busy() {
+ let _guard = test_guard();
+ crate::async_usb::cancel();
+ crate::async_usb::spawn(pending_task, &[]);
+
+ let mut handler = HwwVendorHandler::new(TestingHal::new());
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_NEW, 0x00], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_BUSY]);
+ crate::async_usb::cancel();
+ }
+
+ #[test]
+ fn test_outstanding_request_times_out_without_retry() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(pending_task, &[]);
+ let mut handler = handler();
+
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RETRY], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_NOT_READY]);
+ assert!(!crate::async_usb::is_idle());
+
+ bitbox_u2fhid::VendorCommandHandler::tick(&mut handler, USB_OUTSTANDING_OP_TIMEOUT_MS + 1);
+ assert!(crate::async_usb::is_idle());
+ }
+
+ #[test]
+ fn test_completed_outstanding_request_times_out_without_retry() {
+ let _guard = test_guard();
+ crate::async_usb::spawn(ready_after_second_spin_task, &[]);
+ let mut handler = handler();
+
+ let response = handler
+ .handle_vendor_command(1, HWW_CMD, &[HWW_REQ_RETRY], 0)
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_NOT_READY]);
+
+ // Finish the task without letting the host read the final response yet.
+ crate::async_usb::spin();
+ assert!(!crate::async_usb::is_idle());
+
+ bitbox_u2fhid::VendorCommandHandler::tick(&mut handler, USB_OUTSTANDING_OP_TIMEOUT_MS + 1);
+ assert!(crate::async_usb::is_idle());
+
+ let response = handler
+ .handle_vendor_command(
+ 1,
+ HWW_CMD,
+ &[HWW_REQ_RETRY],
+ USB_OUTSTANDING_OP_TIMEOUT_MS + 1,
+ )
+ .unwrap();
+ assert_eq!(response, vec![HWW_RSP_NACK]);
+
+ let response = handler
+ .handle_vendor_command(
+ 1,
+ HWW_CMD,
+ &[HWW_REQ_NEW],
+ USB_OUTSTANDING_OP_TIMEOUT_MS + 1,
+ )
+ .unwrap();
+ assert_eq!(response[0], HWW_RSP_ACK);
+ }
+}
diff --git a/src/rust/bitbox02/src/hal.rs b/src/rust/bitbox02/src/hal.rs
index 745aa3a..265a72a 100644
--- a/src/rust/bitbox02/src/hal.rs
+++ b/src/rust/bitbox02/src/hal.rs
@@ -38,6 +38,12 @@ impl BitBox02Hal {
}
}
+impl Default for BitBox02Hal {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
impl Hal for BitBox02Hal {
type Ui = ui::BitBox02Ui;
type Random = random::BitBox02Random;
diff --git a/src/rust/bitbox02/src/hal/system.rs b/src/rust/bitbox02/src/hal/system.rs
index 9d91590..ecd78f0 100644
--- a/src/rust/bitbox02/src/hal/system.rs
+++ b/src/rust/bitbox02/src/hal/system.rs
@@ -23,6 +23,10 @@ impl System for BitBox02System {
crate::usb_processing::timeout_reset(value);
}
+ fn is_btconly(&mut self) -> bool {
+ crate::platform::product().contains("btconly")
+ }
+
#[allow(clippy::empty_loop)]
fn reboot(&mut self) -> ! {
unsafe { bitbox02_sys::reboot() }
diff --git a/src/rust/bitbox03/src/memory.rs b/src/rust/bitbox03/src/memory.rs
index 22f38b2..3fe2db9 100644
--- a/src/rust/bitbox03/src/memory.rs
+++ b/src/rust/bitbox03/src/memory.rs
@@ -44,7 +44,7 @@ impl hal::memory::Memory for BitBox03Memory {
}
fn get_platform(&mut self) -> Result<bitbox_hal::memory::Platform, ()> {
- todo!()
+ Ok(bitbox_hal::memory::Platform::BitBox03)
}
fn get_device_name(&mut self) -> alloc::string::String {
diff --git a/src/rust/bitbox03/src/system.rs b/src/rust/bitbox03/src/system.rs
index 150d685..758c12c 100644
--- a/src/rust/bitbox03/src/system.rs
+++ b/src/rust/bitbox03/src/system.rs
@@ -3,8 +3,10 @@ use bitbox_hal as hal;
pub struct BitBox03System;
impl hal::system::System for BitBox03System {
- async fn startup() {
- todo!()
+ async fn startup() {}
+
+ fn is_btconly(&mut self) -> bool {
+ false
}
fn reboot(&mut self) -> ! {
diff --git a/test/simulator-graphical-bb03/Cargo.lock b/test/simulator-graphical-bb03/Cargo.lock
index d02af1a..bcd6c8f 100644
--- a/test/simulator-graphical-bb03/Cargo.lock
+++ b/test/simulator-graphical-bb03/Cargo.lock
@@ -391,6 +391,13 @@ dependencies = [
"cc",
]
+[[package]]
+name = "bitbox-u2fhid"
+version = "0.1.0"
+dependencies = [
+ "bitbox-usb-report-queue",
+]
+
[[package]]
name = "bitbox-usb-report-queue"
version = "0.1.0"
@@ -437,6 +444,7 @@ dependencies = [
"bitbox-executor",
"bitbox-hal",
"bitbox-secp256k1",
+ "bitbox-u2fhid",
"bitbox-usb-report-queue",
"bitbox02",
"bitbox02-noise",
diff --git a/test/simulator-graphical-bb03/src/main.rs b/test/simulator-graphical-bb03/src/main.rs
index 336acd2..8923524 100644
--- a/test/simulator-graphical-bb03/src/main.rs
+++ b/test/simulator-graphical-bb03/src/main.rs
@@ -18,7 +18,7 @@ use std::sync::{
};
use std::task::Poll::Ready;
use std::thread;
-use std::time::Duration;
+use std::time::{Duration, Instant};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
@@ -38,9 +38,8 @@ use glutin_winit::DisplayBuilder;
use tracing::{debug, error, info};
use tracing_subscriber::{EnvFilter, filter::LevelFilter, fmt, prelude::*};
-use bitbox_hal::{Hal, Ui};
+use bitbox_hal::{Hal, Ui, system::System};
-use bitbox_usb_report_queue::UsbReportQueue;
use bitbox03::BitBox03;
use bitbox03::io::touchscreen::{TouchScreen, TouchScreenEvent};
@@ -202,9 +201,14 @@ fn my_flush_cb(display: lvgl::LvDisplay, _area: &lvgl::LvArea, _px_map: *mut u8)
}
}
-fn init_hww(_bitbox: &mut BitBox03, preseed: bool, _hww_queue: &mut UsbReportQueue) -> bool {
+fn init_hww(
+ _bitbox: &mut BitBox03,
+ preseed: bool,
+) -> Option<bitbox02_rust::hww::transport::HwwTransport<BitBox03>> {
+ //bitbox02::screen::init(pixel_fn, mirror_fn, clear_fn);
+ //bitbox02::screen::splash();
+
// BitBox02 simulation initialization
- //bitbox02::usb_processing::init(hww_queue);
info!("USB setup success");
//bitbox02::hww::setup();
@@ -212,7 +216,7 @@ fn init_hww(_bitbox: &mut BitBox03, preseed: bool, _hww_queue: &mut UsbReportQue
//if !bitbox02::sd::format() {
// error!("ERROR, sd card setup failed");
- // return false;
+ // return None;
//}
info!("Sd card setup: success");
@@ -228,7 +232,8 @@ fn init_hww(_bitbox: &mut BitBox03, preseed: bool, _hww_queue: &mut UsbReportQue
//bitbox02_rust::keystore::encrypt_and_store_seed(&mut hal, &seed, "").unwrap();
//bitbox.memory().set_initialized().unwrap();
}
- true
+
+ Some(bitbox02_rust::hww::transport::hww_transport::<BitBox03>())
}
struct App {
@@ -246,7 +251,8 @@ struct App {
inbound_out: Option<mpsc::Receiver<[u8; 64]>>,
startup_task: Option<util::bb02_async::Task<'static, ()>>,
counter: usize,
- hww_queue: UsbReportQueue,
+ transport: Option<bitbox02_rust::hww::transport::HwwTransport<BitBox03>>,
+ started_at: Instant,
}
impl App {
@@ -266,7 +272,8 @@ impl App {
inbound_out: Default::default(),
startup_task: Default::default(),
counter: 0,
- hww_queue: Default::default(),
+ transport: Default::default(),
+ started_at: Instant::now(),
}
}
}
@@ -608,14 +615,17 @@ impl ApplicationHandler<UserEvent> for App {
info!("test switch to logo (pop)");
self.bitbox.ui().switch_to_logo();
}
+ let now_ms = self.started_at.elapsed().as_millis() as u64;
// Read data from TCP client
let mut inbound_out = self.inbound_out.take();
let mut disconnected = false;
if let Some(inbound_out) = &mut inbound_out {
loop {
match inbound_out.try_recv() {
- Ok(_data) => {
- //bitbox02::usb_packet::process_from_report(&data);
+ Ok(data) => {
+ if let Some(transport) = self.transport.as_mut() {
+ transport.handle_report(&data, now_ms);
+ }
}
Err(TryRecvError::Disconnected) => {
// Drop the outbound channel
@@ -632,9 +642,16 @@ impl ApplicationHandler<UserEvent> for App {
if !disconnected {
self.inbound_out = inbound_out;
}
+ if let Some(transport) = self.transport.as_mut() {
+ transport.tick(now_ms);
+ }
// Send data to TCP Client
loop {
- if let Some(data) = self.hww_queue.pull() {
+ if let Some(data) = self
+ .transport
+ .as_mut()
+ .and_then(|transport| transport.pull_report())
+ {
if let Some(outbound_in) = &mut self.outbound_in {
if outbound_in.send(data).is_err() {
info!("writer thread died and closed channel");
@@ -647,8 +664,6 @@ impl ApplicationHandler<UserEvent> for App {
}
// Business logic
bitbox02_rust::async_usb::spin();
- //bitbox02::usb_processing::process_hww();
- //bitbox02::screen::process();
lvgl::timer::handler();
if let Some(ref mut task) = self.startup_task {
@@ -672,7 +687,7 @@ impl ApplicationHandler<UserEvent> for App {
}
self.create_window(event_loop, None)
.expect("failed to create initial window");
- //self.startup_task = Some(Box::pin(bitbox02::hal::system::BitBox02System::startup()));
+ self.startup_task = Some(Box::pin(<BitBox03 as Hal>::System::startup()));
}
}
@@ -718,7 +733,8 @@ pub fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let mut app = App::new(bitbox);
- if !init_hww(&mut bitbox, args.preseed, &mut app.hww_queue) {
+ app.transport = init_hww(&mut bitbox, args.preseed);
+ if app.transport.is_none() {
return Err(Box::new(AppError::new("Failed to init hww")));
}
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock
index e5c3134..98c8d1f 100644
--- a/test/simulator-graphical/Cargo.lock
+++ b/test/simulator-graphical/Cargo.lock
@@ -335,6 +335,13 @@ dependencies = [
"cc",
]
+[[package]]
+name = "bitbox-u2fhid"
+version = "0.1.0"
+dependencies = [
+ "bitbox-usb-report-queue",
+]
+
[[package]]
name = "bitbox-usb-report-queue"
version = "0.1.0"
@@ -381,6 +388,7 @@ dependencies = [
"bitbox-executor",
"bitbox-hal",
"bitbox-secp256k1",
+ "bitbox-u2fhid",
"bitbox-usb-report-queue",
"bitbox02",
"bitbox02-noise",
@@ -2854,7 +2862,6 @@ name = "simulator-graphical"
version = "0.1.0"
dependencies = [
"bitbox-aes",
- "bitbox-usb-report-queue",
"bitbox02",
"bitbox02-rust",
"bitbox02-rust-c",
diff --git a/test/simulator-graphical/Cargo.toml b/test/simulator-graphical/Cargo.toml
index ceabdca..8bd3a07 100644
--- a/test/simulator-graphical/Cargo.toml
+++ b/test/simulator-graphical/Cargo.toml
@@ -7,7 +7,6 @@ edition = "2024"
bitbox02-rust = { path="../../src/rust/bitbox02-rust", features=["simulator-graphical"] }
bitbox02-rust-c = { path="../../src/rust/bitbox02-rust-c", features=["simulator-graphical"] }
bitbox02 = { path="../../src/rust/bitbox02", features=["simulator-graphical"] }
-bitbox-usb-report-queue = { path = "../../src/rust/bitbox-usb-report-queue" }
bitbox-aes = { path="../../src/rust/bitbox-aes"}
winit = "0.30.12"
tracing = {version = "0.1.41", features=["log"]}
diff --git a/test/simulator-graphical/src/main.rs b/test/simulator-graphical/src/main.rs
index 55e3c58..82c3b0b 100644
--- a/test/simulator-graphical/src/main.rs
+++ b/test/simulator-graphical/src/main.rs
@@ -38,7 +38,6 @@ use glutin_winit::DisplayBuilder;
use tracing::{debug, error, info};
use tracing_subscriber::{EnvFilter, filter::LevelFilter, fmt, prelude::*};
-use bitbox_usb_report_queue::UsbReportQueue;
use bitbox02::ui::ugui::UG_COLOR;
use bitbox02_rust::hal::{Eeprom, Hal, Memory, System};
@@ -138,20 +137,20 @@ fn mirror_fn(_: bool) {
static ACCEPTING_CONNECTIONS: AtomicBool = AtomicBool::new(false);
-fn init_hww(preseed: bool, hww_queue: &mut UsbReportQueue) -> bool {
+fn init_hww(
+ preseed: bool,
+) -> Option<bitbox02_rust::hww::transport::HwwTransport<bitbox02::hal::BitBox02Hal>> {
bitbox02::screen::init(pixel_fn, mirror_fn, clear_fn);
bitbox02::screen::splash();
// BitBox02 simulation initialization
- bitbox02::usb_processing::init(hww_queue);
info!("USB setup success");
- bitbox02::hww::setup();
info!("HWW setup success");
if !bitbox02::sd::format() {
error!("ERROR, sd card setup failed");
- return false;
+ return None;
}
info!("Sd card setup: success");
@@ -172,7 +171,9 @@ fn init_hww(preseed: bool, hww_queue: &mut UsbReportQueue) -> bool {
hal.eeprom().setup();
hal.eeprom().init();
- true
+ Some(bitbox02_rust::hww::transport::hww_transport::<
+ bitbox02::hal::BitBox02Hal,
+ >())
}
#[derive(Debug)]
@@ -223,7 +224,8 @@ struct App {
outbound_in: Option<mpsc::Sender<[u8; 64]>>,
inbound_out: Option<mpsc::Receiver<[u8; 64]>>,
startup_task: Option<util::bb02_async::Task<'static, ()>>,
- hww_queue: UsbReportQueue,
+ transport: Option<bitbox02_rust::hww::transport::HwwTransport<bitbox02::hal::BitBox02Hal>>,
+ started_at: std::time::Instant,
}
impl Default for App {
@@ -243,7 +245,8 @@ impl Default for App {
outbound_in: Default::default(),
inbound_out: Default::default(),
startup_task: Default::default(),
- hww_queue: Default::default(),
+ transport: Default::default(),
+ started_at: std::time::Instant::now(),
}
}
}
@@ -621,6 +624,7 @@ impl ApplicationHandler<UserEvent> for App {
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::WakeUp => {
+ let now_ms = self.started_at.elapsed().as_millis() as u64;
// Read data from TCP client
let mut inbound_out = self.inbound_out.take();
let mut disconnected = false;
@@ -628,7 +632,9 @@ impl ApplicationHandler<UserEvent> for App {
loop {
match inbound_out.try_recv() {
Ok(data) => {
- bitbox02::usb_packet::process_from_report(&data);
+ if let Some(transport) = self.transport.as_mut() {
+ transport.handle_report(&data, now_ms);
+ }
}
Err(TryRecvError::Disconnected) => {
// Drop the outbound channel
@@ -645,9 +651,16 @@ impl ApplicationHandler<UserEvent> for App {
if !disconnected {
self.inbound_out = inbound_out;
}
+ if let Some(transport) = self.transport.as_mut() {
+ transport.tick(now_ms);
+ }
// Send data to TCP Client
loop {
- if let Some(data) = self.hww_queue.pull() {
+ if let Some(data) = self
+ .transport
+ .as_mut()
+ .and_then(|transport| transport.pull_report())
+ {
if let Some(outbound_in) = &mut self.outbound_in {
if outbound_in.send(data).is_err() {
info!("writer thread died and closed channel");
@@ -660,7 +673,6 @@ impl ApplicationHandler<UserEvent> for App {
}
// Business logic
bitbox02_rust::async_usb::spin();
- bitbox02::usb_processing::process_hww();
bitbox02::screen::process();
if let Some(ref mut task) = self.startup_task {
@@ -739,7 +751,8 @@ pub fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let mut app = App::default();
- if !init_hww(args.preseed, &mut app.hww_queue) {
+ app.transport = init_hww(args.preseed);
+ if app.transport.is_none() {
return Err(Box::new(AppError::new("Failed to init hww")));
}
let event_loop = EventLoop::<UserEvent>::with_user_event().build()?;
Why this scored 37/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.