What changed, and why it matters
This commit is a pure internal code reorganization (refactor) of the BitBox02 firmware's Rust code. It moves the user-interface trait and its real/testing implementations into a new 'hal' (hardware abstraction layer) module structure and renames some types (e.g., Workflows -> Ui, RealWorkflows -> BitBox02Ui, TestingWorkflows -> TestingUi). No security-sensitive behavior appears to change; it is a maintainability/clean-up change.
No security action required. Review as normal code-quality refactor if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change relocates the Workflows trait from crate::workflow to crate::hal::ui as Ui, and RealWorkflows to crate::hal::BitBox02Ui. The test-only inline module hal::testing is split into separate files (memory.rs, random.rs, sd.rs, securechip.rs, system.rs, ui.rs). Call sites are updated to use the new paths and names. Function bodies, signatures, and semantics are preserved; only module paths and type names change. The diff shows large deletions and additions because code is moved between files, not because logic changed.
Changed components
src/rust/bitbox02-rust/src/hal.rssrc/rust/bitbox02-rust/src/hal/ui.rssrc/rust/bitbox02-rust/src/hal/testing.rssrc/rust/bitbox02-rust/src/hal/testing/ui.rssrc/rust/bitbox02-rust/src/workflow.rssrc/rust/bitbox02-rust/src/workflow/testing.rssrc/rust/bitbox02-rust/src/hww.rssrc/rust/bitbox02-rust/src/hww/api/reset.rssrc/rust/bitbox02-rust/src/workflow/password.rssrc/rust/bitbox02-rust/src/workflow/verify_message.rsInspect captured patch +1054 / −1000
diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs
index 7ebce40..ae1b9ec 100644
--- a/src/rust/bitbox02-rust/src/hal.rs
+++ b/src/rust/bitbox02-rust/src/hal.rs
@@ -1,7 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
-use crate::workflow::RealWorkflows;
-pub use crate::workflow::Workflows as Ui;
+pub mod ui;
+pub use ui::Ui;
+
+#[cfg(feature = "testing")]
+pub mod testing;
use alloc::boxed::Box;
use alloc::string::String;
@@ -9,6 +12,11 @@ use alloc::vec::Vec;
use futures_lite::future::yield_now;
+use crate::workflow::{
+ cancel, confirm, menu, mnemonic, sdcard, status, transaction, trinary_choice,
+ trinary_input_string,
+};
+
#[allow(async_fn_in_trait)]
pub trait Sd {
async fn sdcard_inserted(&mut self) -> bool;
@@ -107,6 +115,82 @@ pub trait Hal {
fn system(&mut self) -> &mut impl System;
}
+pub struct BitBox02Ui;
+
+impl Ui for BitBox02Ui {
+ #[inline(always)]
+ async fn confirm(&mut self, params: &confirm::Params<'_>) -> Result<(), confirm::UserAbort> {
+ confirm::confirm(params).await
+ }
+
+ #[inline(always)]
+ async fn verify_recipient(
+ &mut self,
+ recipient: &str,
+ amount: &str,
+ ) -> Result<(), transaction::UserAbort> {
+ transaction::verify_recipient(recipient, amount).await
+ }
+
+ #[inline(always)]
+ async fn verify_total_fee(
+ &mut self,
+ total: &str,
+ fee: &str,
+ longtouch: bool,
+ ) -> Result<(), transaction::UserAbort> {
+ transaction::verify_total_fee(total, fee, longtouch).await
+ }
+
+ #[inline(always)]
+ async fn status(&mut self, title: &str, status_success: bool) {
+ status::status(title, status_success).await
+ }
+
+ #[inline(always)]
+ async fn enter_string(
+ &mut self,
+ params: &trinary_input_string::Params<'_>,
+ can_cancel: trinary_input_string::CanCancel,
+ preset: &str,
+ ) -> Result<zeroize::Zeroizing<String>, trinary_input_string::Error> {
+ trinary_input_string::enter(params, can_cancel, preset).await
+ }
+
+ #[inline(always)]
+ async fn insert_sdcard(&mut self) -> Result<(), sdcard::UserAbort> {
+ sdcard::sdcard().await
+ }
+
+ #[inline(always)]
+ async fn menu(&mut self, words: &[&str], title: Option<&str>) -> Result<u8, menu::CancelError> {
+ menu::pick(words, title).await
+ }
+
+ #[inline(always)]
+ async fn trinary_choice(
+ &mut self,
+ message: &str,
+ label_left: Option<&str>,
+ label_middle: Option<&str>,
+ label_right: Option<&str>,
+ ) -> trinary_choice::TrinaryChoice {
+ trinary_choice::choose(message, label_left, label_middle, label_right).await
+ }
+
+ async fn show_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error> {
+ mnemonic::show_mnemonic(words).await
+ }
+
+ async fn quiz_mnemonic_word(
+ &mut self,
+ choices: &[&str],
+ title: &str,
+ ) -> Result<u8, cancel::Error> {
+ mnemonic::confirm_word(choices, title).await
+ }
+}
+
pub struct BitBox02Sd;
impl Sd for BitBox02Sd {
@@ -329,7 +413,7 @@ impl System for BitBox02System {
}
pub struct BitBox02Hal {
- ui: RealWorkflows,
+ ui: BitBox02Ui,
sd: BitBox02Sd,
random: BitBox02Random,
securechip: BitBox02SecureChip,
@@ -340,7 +424,7 @@ pub struct BitBox02Hal {
impl BitBox02Hal {
pub const fn new() -> Self {
Self {
- ui: crate::workflow::RealWorkflows,
+ ui: BitBox02Ui,
sd: BitBox02Sd,
random: BitBox02Random,
securechip: BitBox02SecureChip,
@@ -370,625 +454,3 @@ impl Hal for BitBox02Hal {
&mut self.system
}
}
-
-#[cfg(feature = "testing")]
-pub mod testing {
- use alloc::boxed::Box;
- use alloc::collections::{BTreeMap, VecDeque};
- use alloc::string::String;
- use alloc::vec::Vec;
-
- use bitcoin::hashes::{Hash, sha256};
-
- use bitbox02::memory::SecurechipType;
- use hex_lit::hex;
-
- pub struct TestingRandom {
- mock_next_values: VecDeque<[u8; 32]>,
- counter: u32,
- }
-
- impl TestingRandom {
- pub fn new() -> Self {
- Self {
- mock_next_values: VecDeque::new(),
- counter: 0,
- }
- }
-
- pub fn mock_next(&mut self, value: [u8; 32]) {
- self.mock_next_values.push_back(value)
- }
- }
-
- impl super::Random for TestingRandom {
- fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>> {
- self.counter += 1;
- let value = if let Some(value) = self.mock_next_values.pop_front() {
- value
- } else {
- let hash = sha256::Hash::hash(&self.counter.to_be_bytes());
- hash.to_byte_array()
- };
- Box::new(zeroize::Zeroizing::new(value))
- }
- }
-
- pub struct TestingSd {
- pub inserted: Option<bool>,
- files: BTreeMap<String, BTreeMap<String, Vec<u8>>>,
- }
-
- impl TestingSd {
- pub fn new() -> Self {
- Self {
- inserted: None,
- files: BTreeMap::new(),
- }
- }
- }
-
- impl super::Sd for TestingSd {
- async fn sdcard_inserted(&mut self) -> bool {
- self.inserted.unwrap()
- }
-
- async fn list_subdir(&mut self, subdir: Option<&str>) -> Result<Vec<String>, ()> {
- match subdir {
- Some(key) => Ok(self
- .files
- .get(key)
- .map(|files| files.keys().cloned().collect())
- .unwrap_or_default()),
- None => Ok(self.files.keys().cloned().collect()),
- }
- }
-
- async fn erase_file_in_subdir(&mut self, filename: &str, dir: &str) -> Result<(), ()> {
- self.files
- .get_mut(dir)
- .and_then(|files| files.remove(filename).map(|_| ()))
- .ok_or(())
- }
-
- async fn load_bin(
- &mut self,
- filename: &str,
- dir: &str,
- ) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
- self.files
- .get(dir)
- .and_then(|files| files.get(filename))
- .map(|data| zeroize::Zeroizing::new(data.clone()))
- .ok_or(())
- }
-
- async fn write_bin(&mut self, filename: &str, dir: &str, data: &[u8]) -> Result<(), ()> {
- self.files
- .entry(dir.into())
- .or_default()
- .insert(filename.into(), data.to_vec());
- Ok(())
- }
- }
-
- pub struct TestingSecureChip {
- // Count how man security events happen. The numbers were obtained by reading the security
- // event counter slot (0xE0C5) on a real device. We can use this to assert how many events
- // were used in unit tests. The number is relevant due to Optiga's throttling mechanism.
- event_counter: u32,
- reset_keys_fail_once: bool,
- #[cfg(feature = "app-u2f")]
- u2f_counter: u32,
- mock_attestation_signature: [u8; 64],
- last_attestation_challenge: Option<[u8; 32]>,
- }
-
- pub struct TestingMemory {
- securechip_type: SecurechipType,
- platform: bitbox02::memory::Platform,
- initialized: bool,
- is_seeded: bool,
- mnemonic_passphrase_enabled: bool,
- seed_birthdate: u32,
- encrypted_seed_and_hmac: Option<(Vec<u8>, bitbox02::memory::PasswordStretchAlgo)>,
- device_name: Option<String>,
- unlock_attempts: u8,
- salt_root: [u8; 32],
- attestation_device_pubkey: Option<[u8; 64]>,
- attestation_certificate: Option<[u8; 64]>,
- attestation_root_pubkey_identifier: Option<[u8; 32]>,
- attestation_bootloader_hash: [u8; 32],
- multisig_entries: Vec<([u8; 32], String)>,
- }
-
- impl TestingSecureChip {
- pub fn new() -> Self {
- TestingSecureChip {
- event_counter: 0,
- reset_keys_fail_once: false,
- #[cfg(feature = "app-u2f")]
- u2f_counter: 0,
- mock_attestation_signature: [0u8; 64],
- last_attestation_challenge: None,
- }
- }
-
- /// Resets the event counter.
- pub fn event_counter_reset(&mut self) {
- self.event_counter = 0;
- }
-
- /// Retrieves the event counter.
- pub fn get_event_counter(&self) -> u32 {
- self.event_counter
- }
-
- /// Make the next `reset_keys()` call return an error once. Subsequent calls succeed.
- pub fn mock_reset_keys_fails(&mut self) {
- self.reset_keys_fail_once = true;
- }
-
- #[cfg(feature = "app-u2f")]
- pub fn get_u2f_counter(&self) -> u32 {
- self.u2f_counter
- }
-
- pub fn set_mock_attestation_signature(&mut self, sig: &[u8; 64]) {
- self.mock_attestation_signature = *sig;
- }
-
- pub fn last_attestation_challenge(&self) -> Option<[u8; 32]> {
- self.last_attestation_challenge
- }
- }
-
- impl super::SecureChip for TestingSecureChip {
- fn init_new_password(
- &mut self,
- password: &str,
- password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
- ) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
- self.event_counter += 3;
-
- let key: &'static [u8] = match password_stretch_algo {
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => {
- b"unit-test-v0"
- }
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => {
- b"unit-test"
- }
- };
- use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
- let mut engine = HmacEngine::<sha256::Hash>::new(key);
- engine.input(password.as_bytes());
- let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
- Ok(zeroize::Zeroizing::new(
- hmac_result.to_byte_array().to_vec(),
- ))
- }
-
- fn stretch_password(
- &mut self,
- password: &str,
- password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
- ) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
- self.event_counter += match password_stretch_algo {
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => 5,
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => 4,
- };
-
- let key: &'static [u8] = match password_stretch_algo {
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => {
- b"unit-test-v0"
- }
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => {
- b"unit-test"
- }
- };
-
- use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
- let mut engine = HmacEngine::<sha256::Hash>::new(key);
- engine.input(password.as_bytes());
- let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
- Ok(zeroize::Zeroizing::new(
- hmac_result.to_byte_array().to_vec(),
- ))
- }
-
- fn kdf(
- &mut self,
- msg: &[u8],
- ) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
- self.event_counter += 1;
-
- use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
- let mut engine = HmacEngine::<sha256::Hash>::new(&hex!(
- "d2e1e6b18b6c6b08433edbc1d168c1a0043774a4221877e79ed56684be5ac01b"
- ));
- engine.input(msg);
- let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
- Ok(zeroize::Zeroizing::new(
- hmac_result.to_byte_array().to_vec(),
- ))
- }
-
- fn attestation_sign(
- &mut self,
- challenge: &[u8; 32],
- signature: &mut [u8; 64],
- ) -> Result<(), ()> {
- self.event_counter += 1;
- self.last_attestation_challenge = Some(*challenge);
- *signature = self.mock_attestation_signature;
- Ok(())
- }
-
- fn monotonic_increments_remaining(&mut self) -> Result<u32, ()> {
- Ok(1)
- }
-
- fn model(&mut self) -> Result<bitbox02::securechip::Model, ()> {
- Ok(bitbox02::securechip::Model::ATECC_ATECC608B)
- }
-
- fn reset_keys(&mut self) -> Result<(), ()> {
- if self.reset_keys_fail_once {
- self.reset_keys_fail_once = false;
- Err(())
- } else {
- self.event_counter += 3;
- Ok(())
- }
- }
-
- #[cfg(feature = "app-u2f")]
- fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()> {
- self.u2f_counter = counter;
- Ok(())
- }
- }
-
- // Same as MEMORY_MULTISIG_NUM_ENTRIES in memory.h.
- const MULTISIG_LIMIT: usize = 25;
- impl TestingMemory {
- pub fn new() -> Self {
- Self {
- securechip_type: SecurechipType::Optiga,
- platform: bitbox02::memory::Platform::BitBox02,
- initialized: false,
- is_seeded: false,
- mnemonic_passphrase_enabled: false,
- seed_birthdate: 0,
- encrypted_seed_and_hmac: None,
- device_name: None,
- unlock_attempts: 0,
- salt_root: *b"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- attestation_device_pubkey: None,
- attestation_certificate: None,
- attestation_root_pubkey_identifier: None,
- attestation_bootloader_hash: [0; 32],
- multisig_entries: Vec::new(),
- }
- }
-
- pub fn set_securechip_type(&mut self, securechip_type: SecurechipType) {
- self.securechip_type = securechip_type;
- }
-
- pub fn set_platform(&mut self, platform: bitbox02::memory::Platform) {
- self.platform = platform;
- }
-
- pub fn set_unlock_attempts_for_testing(&mut self, attempts: u8) {
- self.unlock_attempts = attempts;
- }
-
- pub fn set_salt_root(&mut self, salt_root: &[u8; 32]) {
- self.salt_root = *salt_root;
- }
-
- pub fn set_attestation_certificate(
- &mut self,
- pubkey: &[u8; 64],
- certificate: &[u8; 64],
- root_pubkey_identifier: &[u8; 32],
- ) {
- self.attestation_device_pubkey = Some(*pubkey);
- self.attestation_certificate = Some(*certificate);
- self.attestation_root_pubkey_identifier = Some(*root_pubkey_identifier);
- }
-
- pub fn set_attestation_bootloader_hash(&mut self, hash: &[u8; 32]) {
- self.attestation_bootloader_hash = *hash;
- }
- }
-
- impl super::Memory for TestingMemory {
- fn get_securechip_type(&mut self) -> Result<SecurechipType, ()> {
- Ok(self.securechip_type)
- }
-
- fn get_platform(&mut self) -> Result<bitbox02::memory::Platform, ()> {
- Ok(self.platform)
- }
-
- fn get_device_name(&mut self) -> String {
- self.device_name
- .clone()
- .unwrap_or_else(|| "My BitBox".into())
- }
-
- fn set_device_name(&mut self, name: &str) -> Result<(), bitbox02::memory::Error> {
- self.device_name = Some(name.into());
- Ok(())
- }
-
- fn is_mnemonic_passphrase_enabled(&mut self) -> bool {
- self.mnemonic_passphrase_enabled
- }
-
- fn set_mnemonic_passphrase_enabled(&mut self, enabled: bool) -> Result<(), ()> {
- self.mnemonic_passphrase_enabled = enabled;
- Ok(())
- }
-
- fn set_seed_birthdate(&mut self, timestamp: u32) -> Result<(), ()> {
- self.seed_birthdate = timestamp;
- Ok(())
- }
-
- fn get_seed_birthdate(&mut self) -> u32 {
- self.seed_birthdate
- }
-
- fn is_seeded(&mut self) -> bool {
- self.is_seeded
- }
-
- fn is_initialized(&mut self) -> bool {
- self.initialized
- }
-
- fn set_initialized(&mut self) -> Result<(), ()> {
- self.initialized = true;
- Ok(())
- }
-
- fn get_encrypted_seed_and_hmac(
- &mut self,
- ) -> Result<(alloc::vec::Vec<u8>, bitbox02::memory::PasswordStretchAlgo), ()> {
- self.encrypted_seed_and_hmac.clone().ok_or(())
- }
-
- fn set_encrypted_seed_and_hmac(
- &mut self,
- data: &[u8],
- password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
- ) -> Result<(), ()> {
- // 96 is the max space allocated in BitBox02's memory for this.
- if data.len() > 96 {
- return Err(());
- }
- self.encrypted_seed_and_hmac = Some((data.to_vec(), password_stretch_algo));
- self.is_seeded = true;
- Ok(())
- }
-
- fn reset_hww(&mut self) -> Result<(), ()> {
- self.initialized = false;
- self.is_seeded = false;
- self.mnemonic_passphrase_enabled = false;
- self.seed_birthdate = 0;
- self.encrypted_seed_and_hmac = None;
- self.device_name = None;
- self.multisig_entries = Vec::new();
- Ok(())
- }
-
- fn get_unlock_attempts(&mut self) -> u8 {
- self.unlock_attempts
- }
-
- fn increment_unlock_attempts(&mut self) {
- self.unlock_attempts += 1;
- }
-
- fn reset_unlock_attempts(&mut self) {
- self.unlock_attempts = 0;
- }
-
- fn get_salt_root(&mut self) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
- if self.salt_root.iter().all(|&b| b == 0xff) {
- Err(())
- } else {
- Ok(zeroize::Zeroizing::new(self.salt_root.to_vec()))
- }
- }
-
- fn get_attestation_pubkey_and_certificate(
- &mut self,
- pubkey_out: &mut [u8; 64],
- certificate_out: &mut [u8; 64],
- root_pubkey_identifier_out: &mut [u8; 32],
- ) -> Result<(), ()> {
- match (
- self.attestation_device_pubkey,
- self.attestation_certificate,
- self.attestation_root_pubkey_identifier,
- ) {
- (Some(pubkey), Some(certificate), Some(root_id)) => {
- *pubkey_out = pubkey;
- *certificate_out = certificate;
- *root_pubkey_identifier_out = root_id;
- Ok(())
- }
- _ => Err(()),
- }
- }
-
- fn get_attestation_bootloader_hash(&mut self) -> [u8; 32] {
- self.attestation_bootloader_hash
- }
-
- fn multisig_set_by_hash(
- &mut self,
- hash: &[u8; 32],
- name: &str,
- ) -> Result<(), bitbox02::memory::MemoryError> {
- // Validate input
- if name.is_empty() {
- return Err(bitbox02::memory::MemoryError::MEMORY_ERR_INVALID_INPUT);
- }
- // Check for duplicate name with different hash
- for (existing_hash, existing_name) in &self.multisig_entries {
- if existing_name == name {
- if existing_hash != hash {
- // Mirror bitbox02::memory multisig_set_by_hash semantics (duplicate-name / full-table),
- // even if these branches are not currently exercised in bitbox02-rust tests.
- return Err(bitbox02::memory::MemoryError::MEMORY_ERR_DUPLICATE_NAME);
- }
- // same name, same hash (already stored)
- return Ok(());
- }
- }
- // Try to find existing entry with same hash
- if let Some((_, existing_name)) = self
- .multisig_entries
- .iter_mut()
- .find(|(existing_hash, _)| existing_hash == hash)
- {
- // rename: same hash, new name
- *existing_name = String::from(name);
- return Ok(());
- }
- if self.multisig_entries.len() >= MULTISIG_LIMIT {
- // See comment above about mirroring bitbox02::memory semantics.
- return Err(bitbox02::memory::MemoryError::MEMORY_ERR_FULL);
- }
- // Insert new entry
- self.multisig_entries.push((*hash, String::from(name)));
- Ok(())
- }
-
- fn multisig_get_by_hash(&self, hash: &[u8; 32]) -> Option<String> {
- self.multisig_entries
- .iter()
- .find(|(existing_hash, _)| existing_hash == hash)
- .map(|(_, name)| name.clone())
- }
- }
-
- pub struct TestingSystem;
-
- impl TestingSystem {
- pub fn new() -> Self {
- Self
- }
- }
-
- impl super::System for TestingSystem {
- fn reboot_to_bootloader(&mut self) -> ! {
- panic!("reboot_to_bootloader called")
- }
- }
-
- pub struct TestingHal<'a> {
- pub ui: crate::workflow::testing::TestingWorkflows<'a>,
- pub sd: TestingSd,
- pub random: TestingRandom,
- pub securechip: TestingSecureChip,
- pub memory: TestingMemory,
- pub system: TestingSystem,
- }
-
- impl TestingHal<'_> {
- pub fn new() -> Self {
- Self {
- ui: crate::workflow::testing::TestingWorkflows::new(),
- sd: TestingSd::new(),
- random: TestingRandom::new(),
- securechip: TestingSecureChip::new(),
- memory: TestingMemory::new(),
- system: TestingSystem::new(),
- }
- }
- }
-
- impl super::Hal for TestingHal<'_> {
- fn ui(&mut self) -> &mut impl super::Ui {
- &mut self.ui
- }
- fn sd(&mut self) -> &mut impl super::Sd {
- &mut self.sd
- }
- fn random(&mut self) -> &mut impl super::Random {
- &mut self.random
- }
- fn securechip(&mut self) -> &mut impl super::SecureChip {
- &mut self.securechip
- }
- fn memory(&mut self) -> &mut impl super::Memory {
- &mut self.memory
- }
- fn system(&mut self) -> &mut impl super::System {
- &mut self.system
- }
- }
-
- #[cfg(test)]
- mod tests {
- use super::*;
- use crate::hal::{Random, Sd};
- use hex_lit::hex;
-
- use util::bb02_async::block_on;
-
- // Quick check if our mock TestingSd implementation makes sense.
- #[test]
- fn test_sd_list_write_read_erase() {
- let mut sd = TestingSd::new();
- assert_eq!(block_on(sd.list_subdir(None)), Ok(vec![]));
- assert_eq!(block_on(sd.list_subdir(Some("dir1"))), Ok(vec![]));
-
- assert!(block_on(sd.load_bin("file1.txt", "dir1")).is_err());
- assert!(block_on(sd.write_bin("file1.txt", "dir1", b"data")).is_ok());
- assert_eq!(block_on(sd.list_subdir(None)), Ok(vec!["dir1".into()]));
- assert_eq!(
- block_on(sd.list_subdir(Some("dir1"))),
- Ok(vec!["file1.txt".into()])
- );
- assert_eq!(
- block_on(sd.load_bin("file1.txt", "dir1"))
- .unwrap()
- .as_slice(),
- b"data"
- );
- assert!(block_on(sd.write_bin("file1.txt", "dir1", b"replaced data")).is_ok());
- assert_eq!(
- block_on(sd.load_bin("file1.txt", "dir1"))
- .unwrap()
- .as_slice(),
- b"replaced data"
- );
- assert!(block_on(sd.erase_file_in_subdir("doesnt-exist.txt", "dir1")).is_err());
- assert!(block_on(sd.erase_file_in_subdir("file1.txt", "dir1")).is_ok());
- assert_eq!(block_on(sd.list_subdir(Some("dir1"))), Ok(vec![]));
- }
-
- #[test]
- fn test_random() {
- let mut random = TestingRandom::new();
- let first = random.random_32_bytes();
- let second = random.random_32_bytes();
- assert_eq!(
- first.as_slice(),
- &hex!("b40711a88c7039756fb8a73827eabe2c0fe5a0346ca7e0a104adc0fc764f528d"),
- );
- assert_eq!(
- second.as_slice(),
- &hex!("433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a"),
- );
- }
- }
-}
diff --git a/src/rust/bitbox02-rust/src/hal/testing.rs b/src/rust/bitbox02-rust/src/hal/testing.rs
new file mode 100644
index 0000000..c5ac3db
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing.rs
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: Apache-2.0
+
+pub mod memory;
+pub mod random;
+pub mod sd;
+pub mod securechip;
+pub mod system;
+pub mod ui;
+
+pub use memory::TestingMemory;
+pub use random::TestingRandom;
+pub use sd::TestingSd;
+pub use securechip::TestingSecureChip;
+pub use system::TestingSystem;
+pub use ui::{Screen, TestingUi};
+
+pub struct TestingHal<'a> {
+ pub ui: TestingUi<'a>,
+ pub sd: TestingSd,
+ pub random: TestingRandom,
+ pub securechip: TestingSecureChip,
+ pub memory: TestingMemory,
+ pub system: TestingSystem,
+}
+
+impl TestingHal<'_> {
+ pub fn new() -> Self {
+ Self {
+ ui: TestingUi::new(),
+ sd: TestingSd::new(),
+ random: TestingRandom::new(),
+ securechip: TestingSecureChip::new(),
+ memory: TestingMemory::new(),
+ system: TestingSystem::new(),
+ }
+ }
+}
+
+impl crate::hal::Hal for TestingHal<'_> {
+ fn ui(&mut self) -> &mut impl crate::hal::Ui {
+ &mut self.ui
+ }
+
+ fn sd(&mut self) -> &mut impl crate::hal::Sd {
+ &mut self.sd
+ }
+
+ fn random(&mut self) -> &mut impl crate::hal::Random {
+ &mut self.random
+ }
+
+ fn securechip(&mut self) -> &mut impl crate::hal::SecureChip {
+ &mut self.securechip
+ }
+
+ fn memory(&mut self) -> &mut impl crate::hal::Memory {
+ &mut self.memory
+ }
+
+ fn system(&mut self) -> &mut impl crate::hal::System {
+ &mut self.system
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/testing/memory.rs b/src/rust/bitbox02-rust/src/hal/testing/memory.rs
new file mode 100644
index 0000000..4e788eb
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing/memory.rs
@@ -0,0 +1,255 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use alloc::string::String;
+use alloc::vec::Vec;
+
+use bitbox02::memory::SecurechipType;
+
+pub struct TestingMemory {
+ securechip_type: SecurechipType,
+ platform: bitbox02::memory::Platform,
+ initialized: bool,
+ is_seeded: bool,
+ mnemonic_passphrase_enabled: bool,
+ seed_birthdate: u32,
+ encrypted_seed_and_hmac: Option<(Vec<u8>, bitbox02::memory::PasswordStretchAlgo)>,
+ device_name: Option<String>,
+ unlock_attempts: u8,
+ salt_root: [u8; 32],
+ attestation_device_pubkey: Option<[u8; 64]>,
+ attestation_certificate: Option<[u8; 64]>,
+ attestation_root_pubkey_identifier: Option<[u8; 32]>,
+ attestation_bootloader_hash: [u8; 32],
+ multisig_entries: Vec<([u8; 32], String)>,
+}
+
+// Same as MEMORY_MULTISIG_NUM_ENTRIES in memory.h.
+const MULTISIG_LIMIT: usize = 25;
+
+impl TestingMemory {
+ pub fn new() -> Self {
+ Self {
+ securechip_type: SecurechipType::Optiga,
+ platform: bitbox02::memory::Platform::BitBox02,
+ initialized: false,
+ is_seeded: false,
+ mnemonic_passphrase_enabled: false,
+ seed_birthdate: 0,
+ encrypted_seed_and_hmac: None,
+ device_name: None,
+ unlock_attempts: 0,
+ salt_root: *b"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ attestation_device_pubkey: None,
+ attestation_certificate: None,
+ attestation_root_pubkey_identifier: None,
+ attestation_bootloader_hash: [0; 32],
+ multisig_entries: Vec::new(),
+ }
+ }
+
+ pub fn set_securechip_type(&mut self, securechip_type: SecurechipType) {
+ self.securechip_type = securechip_type;
+ }
+
+ pub fn set_platform(&mut self, platform: bitbox02::memory::Platform) {
+ self.platform = platform;
+ }
+
+ pub fn set_unlock_attempts_for_testing(&mut self, attempts: u8) {
+ self.unlock_attempts = attempts;
+ }
+
+ pub fn set_salt_root(&mut self, salt_root: &[u8; 32]) {
+ self.salt_root = *salt_root;
+ }
+
+ pub fn set_attestation_certificate(
+ &mut self,
+ pubkey: &[u8; 64],
+ certificate: &[u8; 64],
+ root_pubkey_identifier: &[u8; 32],
+ ) {
+ self.attestation_device_pubkey = Some(*pubkey);
+ self.attestation_certificate = Some(*certificate);
+ self.attestation_root_pubkey_identifier = Some(*root_pubkey_identifier);
+ }
+
+ pub fn set_attestation_bootloader_hash(&mut self, hash: &[u8; 32]) {
+ self.attestation_bootloader_hash = *hash;
+ }
+}
+
+impl crate::hal::Memory for TestingMemory {
+ fn get_securechip_type(&mut self) -> Result<SecurechipType, ()> {
+ Ok(self.securechip_type)
+ }
+
+ fn get_platform(&mut self) -> Result<bitbox02::memory::Platform, ()> {
+ Ok(self.platform)
+ }
+
+ fn get_device_name(&mut self) -> String {
+ self.device_name
+ .clone()
+ .unwrap_or_else(|| "My BitBox".into())
+ }
+
+ fn set_device_name(&mut self, name: &str) -> Result<(), bitbox02::memory::Error> {
+ self.device_name = Some(name.into());
+ Ok(())
+ }
+
+ fn is_mnemonic_passphrase_enabled(&mut self) -> bool {
+ self.mnemonic_passphrase_enabled
+ }
+
+ fn set_mnemonic_passphrase_enabled(&mut self, enabled: bool) -> Result<(), ()> {
+ self.mnemonic_passphrase_enabled = enabled;
+ Ok(())
+ }
+
+ fn set_seed_birthdate(&mut self, timestamp: u32) -> Result<(), ()> {
+ self.seed_birthdate = timestamp;
+ Ok(())
+ }
+
+ fn get_seed_birthdate(&mut self) -> u32 {
+ self.seed_birthdate
+ }
+
+ fn is_seeded(&mut self) -> bool {
+ self.is_seeded
+ }
+
+ fn is_initialized(&mut self) -> bool {
+ self.initialized
+ }
+
+ fn set_initialized(&mut self) -> Result<(), ()> {
+ self.initialized = true;
+ Ok(())
+ }
+
+ fn get_encrypted_seed_and_hmac(
+ &mut self,
+ ) -> Result<(alloc::vec::Vec<u8>, bitbox02::memory::PasswordStretchAlgo), ()> {
+ self.encrypted_seed_and_hmac.clone().ok_or(())
+ }
+
+ fn set_encrypted_seed_and_hmac(
+ &mut self,
+ data: &[u8],
+ password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
+ ) -> Result<(), ()> {
+ // 96 is the max space allocated in BitBox02's memory for this.
+ if data.len() > 96 {
+ return Err(());
+ }
+ self.encrypted_seed_and_hmac = Some((data.to_vec(), password_stretch_algo));
+ self.is_seeded = true;
+ Ok(())
+ }
+
+ fn reset_hww(&mut self) -> Result<(), ()> {
+ self.initialized = false;
+ self.is_seeded = false;
+ self.mnemonic_passphrase_enabled = false;
+ self.seed_birthdate = 0;
+ self.encrypted_seed_and_hmac = None;
+ self.device_name = None;
+ self.multisig_entries = Vec::new();
+ Ok(())
+ }
+
+ fn get_unlock_attempts(&mut self) -> u8 {
+ self.unlock_attempts
+ }
+
+ fn increment_unlock_attempts(&mut self) {
+ self.unlock_attempts += 1;
+ }
+
+ fn reset_unlock_attempts(&mut self) {
+ self.unlock_attempts = 0;
+ }
+
+ fn get_salt_root(&mut self) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
+ if self.salt_root.iter().all(|&b| b == 0xff) {
+ Err(())
+ } else {
+ Ok(zeroize::Zeroizing::new(self.salt_root.to_vec()))
+ }
+ }
+
+ fn get_attestation_pubkey_and_certificate(
+ &mut self,
+ pubkey_out: &mut [u8; 64],
+ certificate_out: &mut [u8; 64],
+ root_pubkey_identifier_out: &mut [u8; 32],
+ ) -> Result<(), ()> {
+ match (
+ self.attestation_device_pubkey,
+ self.attestation_certificate,
+ self.attestation_root_pubkey_identifier,
+ ) {
+ (Some(pubkey), Some(certificate), Some(root_id)) => {
+ *pubkey_out = pubkey;
+ *certificate_out = certificate;
+ *root_pubkey_identifier_out = root_id;
+ Ok(())
+ }
+ _ => Err(()),
+ }
+ }
+
+ fn get_attestation_bootloader_hash(&mut self) -> [u8; 32] {
+ self.attestation_bootloader_hash
+ }
+
+ fn multisig_set_by_hash(
+ &mut self,
+ hash: &[u8; 32],
+ name: &str,
+ ) -> Result<(), bitbox02::memory::MemoryError> {
+ // Validate input
+ if name.is_empty() {
+ return Err(bitbox02::memory::MemoryError::MEMORY_ERR_INVALID_INPUT);
+ }
+ // Check for duplicate name with different hash
+ for (existing_hash, existing_name) in &self.multisig_entries {
+ if existing_name == name {
+ if existing_hash != hash {
+ // Mirror bitbox02::memory multisig_set_by_hash semantics (duplicate-name / full-table),
+ // even if these branches are not currently exercised in bitbox02-rust tests.
+ return Err(bitbox02::memory::MemoryError::MEMORY_ERR_DUPLICATE_NAME);
+ }
+ // same name, same hash (already stored)
+ return Ok(());
+ }
+ }
+ // Try to find existing entry with same hash
+ if let Some((_, existing_name)) = self
+ .multisig_entries
+ .iter_mut()
+ .find(|(existing_hash, _)| existing_hash == hash)
+ {
+ // rename: same hash, new name
+ *existing_name = String::from(name);
+ return Ok(());
+ }
+ if self.multisig_entries.len() >= MULTISIG_LIMIT {
+ // See comment above about mirroring bitbox02::memory semantics.
+ return Err(bitbox02::memory::MemoryError::MEMORY_ERR_FULL);
+ }
+ // Insert new entry
+ self.multisig_entries.push((*hash, String::from(name)));
+ Ok(())
+ }
+
+ fn multisig_get_by_hash(&self, hash: &[u8; 32]) -> Option<String> {
+ self.multisig_entries
+ .iter()
+ .find(|(existing_hash, _)| existing_hash == hash)
+ .map(|(_, name)| name.clone())
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/testing/random.rs b/src/rust/bitbox02-rust/src/hal/testing/random.rs
new file mode 100644
index 0000000..2a3b5a1
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing/random.rs
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use alloc::boxed::Box;
+use alloc::collections::VecDeque;
+
+use bitcoin::hashes::{Hash, sha256};
+
+pub struct TestingRandom {
+ mock_next_values: VecDeque<[u8; 32]>,
+ counter: u32,
+}
+
+impl TestingRandom {
+ pub fn new() -> Self {
+ Self {
+ mock_next_values: VecDeque::new(),
+ counter: 0,
+ }
+ }
+
+ pub fn mock_next(&mut self, value: [u8; 32]) {
+ self.mock_next_values.push_back(value)
+ }
+}
+
+impl crate::hal::Random for TestingRandom {
+ fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>> {
+ self.counter += 1;
+ let value = if let Some(value) = self.mock_next_values.pop_front() {
+ value
+ } else {
+ let hash = sha256::Hash::hash(&self.counter.to_be_bytes());
+ hash.to_byte_array()
+ };
+ Box::new(zeroize::Zeroizing::new(value))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::hal::Random;
+ use hex_lit::hex;
+
+ #[test]
+ fn test_random() {
+ let mut random = TestingRandom::new();
+ let first = random.random_32_bytes();
+ let second = random.random_32_bytes();
+ assert_eq!(
+ first.as_slice(),
+ &hex!("b40711a88c7039756fb8a73827eabe2c0fe5a0346ca7e0a104adc0fc764f528d"),
+ );
+ assert_eq!(
+ second.as_slice(),
+ &hex!("433ebf5bc03dffa38536673207a21281612cef5faa9bc7a4d5b9be2fdb12cf1a"),
+ );
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/testing/sd.rs b/src/rust/bitbox02-rust/src/hal/testing/sd.rs
new file mode 100644
index 0000000..3027c1b
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing/sd.rs
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use alloc::collections::BTreeMap;
+use alloc::string::String;
+use alloc::vec::Vec;
+
+pub struct TestingSd {
+ pub inserted: Option<bool>,
+ files: BTreeMap<String, BTreeMap<String, Vec<u8>>>,
+}
+
+impl TestingSd {
+ pub fn new() -> Self {
+ Self {
+ inserted: None,
+ files: BTreeMap::new(),
+ }
+ }
+}
+
+impl crate::hal::Sd for TestingSd {
+ async fn sdcard_inserted(&mut self) -> bool {
+ self.inserted.unwrap()
+ }
+
+ async fn list_subdir(&mut self, subdir: Option<&str>) -> Result<Vec<String>, ()> {
+ match subdir {
+ Some(key) => Ok(self
+ .files
+ .get(key)
+ .map(|files| files.keys().cloned().collect())
+ .unwrap_or_default()),
+ None => Ok(self.files.keys().cloned().collect()),
+ }
+ }
+
+ async fn erase_file_in_subdir(&mut self, filename: &str, dir: &str) -> Result<(), ()> {
+ self.files
+ .get_mut(dir)
+ .and_then(|files| files.remove(filename).map(|_| ()))
+ .ok_or(())
+ }
+
+ async fn load_bin(
+ &mut self,
+ filename: &str,
+ dir: &str,
+ ) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
+ self.files
+ .get(dir)
+ .and_then(|files| files.get(filename))
+ .map(|data| zeroize::Zeroizing::new(data.clone()))
+ .ok_or(())
+ }
+
+ async fn write_bin(&mut self, filename: &str, dir: &str, data: &[u8]) -> Result<(), ()> {
+ self.files
+ .entry(dir.into())
+ .or_default()
+ .insert(filename.into(), data.to_vec());
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::hal::Sd;
+ use util::bb02_async::block_on;
+
+ // Quick check if our mock TestingSd implementation makes sense.
+ #[test]
+ fn test_sd_list_write_read_erase() {
+ let mut sd = TestingSd::new();
+ assert_eq!(block_on(sd.list_subdir(None)), Ok(vec![]));
+ assert_eq!(block_on(sd.list_subdir(Some("dir1"))), Ok(vec![]));
+
+ assert!(block_on(sd.load_bin("file1.txt", "dir1")).is_err());
+ assert!(block_on(sd.write_bin("file1.txt", "dir1", b"data")).is_ok());
+ assert_eq!(block_on(sd.list_subdir(None)), Ok(vec!["dir1".into()]));
+ assert_eq!(
+ block_on(sd.list_subdir(Some("dir1"))),
+ Ok(vec!["file1.txt".into()])
+ );
+ assert_eq!(
+ block_on(sd.load_bin("file1.txt", "dir1"))
+ .unwrap()
+ .as_slice(),
+ b"data"
+ );
+ assert!(block_on(sd.write_bin("file1.txt", "dir1", b"replaced data")).is_ok());
+ assert_eq!(
+ block_on(sd.load_bin("file1.txt", "dir1"))
+ .unwrap()
+ .as_slice(),
+ b"replaced data"
+ );
+ assert!(block_on(sd.erase_file_in_subdir("doesnt-exist.txt", "dir1")).is_err());
+ assert!(block_on(sd.erase_file_in_subdir("file1.txt", "dir1")).is_ok());
+ assert_eq!(block_on(sd.list_subdir(Some("dir1"))), Ok(vec![]));
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/testing/securechip.rs b/src/rust/bitbox02-rust/src/hal/testing/securechip.rs
new file mode 100644
index 0000000..a5eeab6
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing/securechip.rs
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use alloc::vec::Vec;
+
+use bitcoin::hashes::Hash;
+use hex_lit::hex;
+
+pub struct TestingSecureChip {
+ // Count how man security events happen. The numbers were obtained by reading the security
+ // event counter slot (0xE0C5) on a real device. We can use this to assert how many events
+ // were used in unit tests. The number is relevant due to Optiga's throttling mechanism.
+ event_counter: u32,
+ reset_keys_fail_once: bool,
+ #[cfg(feature = "app-u2f")]
+ u2f_counter: u32,
+ mock_attestation_signature: [u8; 64],
+ last_attestation_challenge: Option<[u8; 32]>,
+}
+
+impl TestingSecureChip {
+ pub fn new() -> Self {
+ TestingSecureChip {
+ event_counter: 0,
+ reset_keys_fail_once: false,
+ #[cfg(feature = "app-u2f")]
+ u2f_counter: 0,
+ mock_attestation_signature: [0u8; 64],
+ last_attestation_challenge: None,
+ }
+ }
+
+ /// Resets the event counter.
+ pub fn event_counter_reset(&mut self) {
+ self.event_counter = 0;
+ }
+
+ /// Retrieves the event counter.
+ pub fn get_event_counter(&self) -> u32 {
+ self.event_counter
+ }
+
+ /// Make the next `reset_keys()` call return an error once. Subsequent calls succeed.
+ pub fn mock_reset_keys_fails(&mut self) {
+ self.reset_keys_fail_once = true;
+ }
+
+ #[cfg(feature = "app-u2f")]
+ pub fn get_u2f_counter(&self) -> u32 {
+ self.u2f_counter
+ }
+
+ pub fn set_mock_attestation_signature(&mut self, sig: &[u8; 64]) {
+ self.mock_attestation_signature = *sig;
+ }
+
+ pub fn last_attestation_challenge(&self) -> Option<[u8; 32]> {
+ self.last_attestation_challenge
+ }
+}
+
+impl crate::hal::SecureChip for TestingSecureChip {
+ fn init_new_password(
+ &mut self,
+ password: &str,
+ password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
+ ) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
+ self.event_counter += 3;
+
+ let key: &'static [u8] = match password_stretch_algo {
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => {
+ b"unit-test-v0"
+ }
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => b"unit-test",
+ };
+ use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
+ let mut engine = HmacEngine::<sha256::Hash>::new(key);
+ engine.input(password.as_bytes());
+ let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
+ Ok(zeroize::Zeroizing::new(
+ hmac_result.to_byte_array().to_vec(),
+ ))
+ }
+
+ fn stretch_password(
+ &mut self,
+ password: &str,
+ password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
+ ) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
+ self.event_counter += match password_stretch_algo {
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => 5,
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => 4,
+ };
+
+ let key: &'static [u8] = match password_stretch_algo {
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => {
+ b"unit-test-v0"
+ }
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => b"unit-test",
+ };
+
+ use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
+ let mut engine = HmacEngine::<sha256::Hash>::new(key);
+ engine.input(password.as_bytes());
+ let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
+ Ok(zeroize::Zeroizing::new(
+ hmac_result.to_byte_array().to_vec(),
+ ))
+ }
+
+ fn kdf(
+ &mut self,
+ msg: &[u8],
+ ) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
+ self.event_counter += 1;
+
+ use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
+ let mut engine = HmacEngine::<sha256::Hash>::new(&hex!(
+ "d2e1e6b18b6c6b08433edbc1d168c1a0043774a4221877e79ed56684be5ac01b"
+ ));
+ engine.input(msg);
+ let hmac_result: Hmac<sha256::Hash> = Hmac::from_engine(engine);
+ Ok(zeroize::Zeroizing::new(
+ hmac_result.to_byte_array().to_vec(),
+ ))
+ }
+
+ fn attestation_sign(
+ &mut self,
+ challenge: &[u8; 32],
+ signature: &mut [u8; 64],
+ ) -> Result<(), ()> {
+ self.event_counter += 1;
+ self.last_attestation_challenge = Some(*challenge);
+ *signature = self.mock_attestation_signature;
+ Ok(())
+ }
+
+ fn monotonic_increments_remaining(&mut self) -> Result<u32, ()> {
+ Ok(1)
+ }
+
+ fn model(&mut self) -> Result<bitbox02::securechip::Model, ()> {
+ Ok(bitbox02::securechip::Model::ATECC_ATECC608B)
+ }
+
+ fn reset_keys(&mut self) -> Result<(), ()> {
+ if self.reset_keys_fail_once {
+ self.reset_keys_fail_once = false;
+ Err(())
+ } else {
+ self.event_counter += 3;
+ Ok(())
+ }
+ }
+
+ #[cfg(feature = "app-u2f")]
+ fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()> {
+ self.u2f_counter = counter;
+ Ok(())
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/testing/system.rs b/src/rust/bitbox02-rust/src/hal/testing/system.rs
new file mode 100644
index 0000000..e68e2db
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing/system.rs
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Apache-2.0
+
+pub struct TestingSystem;
+
+impl TestingSystem {
+ pub fn new() -> Self {
+ Self
+ }
+}
+
+impl crate::hal::System for TestingSystem {
+ fn reboot_to_bootloader(&mut self) -> ! {
+ panic!("reboot_to_bootloader called")
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/testing/ui.rs b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
new file mode 100644
index 0000000..424aa1a
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::hal::Ui;
+use crate::workflow::{
+ cancel, confirm, menu, sdcard, transaction, trinary_choice, trinary_input_string,
+};
+
+use alloc::boxed::Box;
+use alloc::string::String;
+use alloc::vec::Vec;
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum Screen {
+ Confirm {
+ title: String,
+ body: String,
+ longtouch: bool,
+ },
+ TotalFee {
+ total: String,
+ fee: String,
+ longtouch: bool,
+ },
+ Recipient {
+ recipient: String,
+ amount: String,
+ },
+ Status {
+ title: String,
+ success: bool,
+ },
+ ShowAndConfirmMnemonic {
+ mnemonic: String,
+ },
+ More,
+}
+
+type EnterStringCb<'a> = Box<
+ dyn FnMut(&trinary_input_string::Params<'_>) -> Result<String, trinary_input_string::Error>
+ + 'a,
+>;
+
+/// A Ui implementation for unit tests. Collects all screens and provides helper functions
+/// to verify them.
+pub struct TestingUi<'a> {
+ _abort_nth: Option<usize>,
+ pub screens: Vec<Screen>,
+ _enter_string: Option<EnterStringCb<'a>>,
+}
+
+impl Ui for TestingUi<'_> {
+ async fn confirm(&mut self, params: &confirm::Params<'_>) -> Result<(), confirm::UserAbort> {
+ self.screens.push(Screen::Confirm {
+ title: params.title.into(),
+ body: params.body.into(),
+ longtouch: params.longtouch,
+ });
+ if self
+ ._abort_nth
+ .as_ref()
+ .is_some_and(|&n| self.screens.len() - 1 == n)
+ {
+ return Err(confirm::UserAbort);
+ }
+ Ok(())
+ }
+
+ async fn verify_recipient(
+ &mut self,
+ recipient: &str,
+ amount: &str,
+ ) -> Result<(), transaction::UserAbort> {
+ self.screens.push(Screen::Recipient {
+ recipient: recipient.into(),
+ amount: amount.into(),
+ });
+ if self
+ ._abort_nth
+ .as_ref()
+ .is_some_and(|&n| self.screens.len() - 1 == n)
+ {
+ return Err(transaction::UserAbort);
+ }
+ Ok(())
+ }
+
+ async fn verify_total_fee(
+ &mut self,
+ total: &str,
+ fee: &str,
+ longtouch: bool,
+ ) -> Result<(), transaction::UserAbort> {
+ self.screens.push(Screen::TotalFee {
+ total: total.into(),
+ fee: fee.into(),
+ longtouch,
+ });
+ if self
+ ._abort_nth
+ .as_ref()
+ .is_some_and(|&n| self.screens.len() - 1 == n)
+ {
+ return Err(transaction::UserAbort);
+ }
+ Ok(())
+ }
+
+ async fn status(&mut self, title: &str, status_success: bool) {
+ self.screens.push(Screen::Status {
+ title: title.into(),
+ success: status_success,
+ });
+ if self
+ ._abort_nth
+ .as_ref()
+ .is_some_and(|&n| self.screens.len() - 1 == n)
+ {
+ panic!("canot abort status screen");
+ }
+ }
+
+ async fn enter_string(
+ &mut self,
+ params: &trinary_input_string::Params<'_>,
+ _can_cancel: trinary_input_string::CanCancel,
+ _preset: &str,
+ ) -> Result<zeroize::Zeroizing<String>, trinary_input_string::Error> {
+ self._enter_string.as_mut().unwrap()(params).map(zeroize::Zeroizing::new)
+ }
+
+ async fn insert_sdcard(&mut self) -> Result<(), sdcard::UserAbort> {
+ Ok(())
+ }
+
+ async fn menu(
+ &mut self,
+ _words: &[&str],
+ _title: Option<&str>,
+ ) -> Result<u8, menu::CancelError> {
+ todo!("not used in unit tests yet");
+ }
+
+ async fn trinary_choice(
+ &mut self,
+ _message: &str,
+ _label_left: Option<&str>,
+ _label_middle: Option<&str>,
+ _label_right: Option<&str>,
+ ) -> trinary_choice::TrinaryChoice {
+ todo!("not used in unit tests yet");
+ }
+
+ async fn show_mnemonic(&mut self, _words: &[&str]) -> Result<(), cancel::Error> {
+ todo!("not used in unit tests yet");
+ }
+
+ async fn quiz_mnemonic_word(
+ &mut self,
+ _choices: &[&str],
+ _title: &str,
+ ) -> Result<u8, cancel::Error> {
+ todo!("not used in unit tests yet");
+ }
+
+ async fn show_and_confirm_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error> {
+ self.screens.push(Screen::ShowAndConfirmMnemonic {
+ mnemonic: words.join(" "),
+ });
+ Ok(())
+ }
+
+ async fn get_mnemonic(&mut self) -> Result<zeroize::Zeroizing<String>, cancel::Error>
+ where
+ Self: Sized,
+ {
+ let words = "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide";
+
+ Ok(zeroize::Zeroizing::new(words.into()))
+ }
+}
+
+impl<'a> TestingUi<'a> {
+ pub fn new() -> Self {
+ Self {
+ screens: vec![],
+ _abort_nth: None,
+ _enter_string: None,
+ }
+ }
+
+ /// Make the `n`-th workflow (0-indexed) fail with a user abort. If that workflow cannot be
+ /// aborted, there will be panic.
+ pub fn abort_nth(&mut self, n: usize) {
+ self._abort_nth = Some(n);
+ }
+
+ pub fn contains_confirm(&self, confirm_title: &str, confirm_body: &str) -> bool {
+ self.screens.iter().any(|screen| match screen {
+ Screen::Confirm { title, body, .. } => title == confirm_title && body == confirm_body,
+ _ => false,
+ })
+ }
+
+ pub fn set_enter_string(&mut self, cb: EnterStringCb<'a>) {
+ self._enter_string = Some(cb);
+ }
+
+ pub fn remove_enter_string(&mut self) {
+ self._enter_string = None;
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hal/ui.rs b/src/rust/bitbox02-rust/src/hal/ui.rs
new file mode 100644
index 0000000..5f26e25
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hal/ui.rs
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::workflow::{
+ cancel, confirm, menu, mnemonic, sdcard, transaction, trinary_choice, trinary_input_string,
+};
+
+use alloc::string::String;
+
+#[allow(async_fn_in_trait)]
+pub trait Ui {
+ async fn confirm(&mut self, params: &confirm::Params<'_>) -> Result<(), confirm::UserAbort>;
+
+ async fn verify_recipient(
+ &mut self,
+ recipient: &str,
+ amount: &str,
+ ) -> Result<(), transaction::UserAbort>;
+
+ async fn verify_total_fee(
+ &mut self,
+ total: &str,
+ fee: &str,
+ longtouch: bool,
+ ) -> Result<(), transaction::UserAbort>;
+
+ async fn status(&mut self, title: &str, status_success: bool);
+
+ async fn enter_string(
+ &mut self,
+ params: &trinary_input_string::Params<'_>,
+ can_cancel: trinary_input_string::CanCancel,
+ preset: &str,
+ ) -> Result<zeroize::Zeroizing<String>, trinary_input_string::Error>;
+
+ async fn insert_sdcard(&mut self) -> Result<(), sdcard::UserAbort>;
+
+ async fn menu(&mut self, words: &[&str], title: Option<&str>) -> Result<u8, menu::CancelError>;
+
+ async fn trinary_choice(
+ &mut self,
+ message: &str,
+ label_left: Option<&str>,
+ label_middle: Option<&str>,
+ label_right: Option<&str>,
+ ) -> trinary_choice::TrinaryChoice;
+
+ /// Display the BIP39 mnemonic to the user.
+ async fn show_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error>;
+
+ /// Display these BIP39 mnemonic word choices to the user as part of the quiz to confirm the
+ /// user backuped up the mnemonic correctly.
+ async fn quiz_mnemonic_word(
+ &mut self,
+ choices: &[&str],
+ title: &str,
+ ) -> Result<u8, cancel::Error>;
+
+ /// Display the mnemonic words and have the user confirm them in a multiple-choice quiz.
+ ///
+ /// The default implementation is implemented in terms of `self.show_mnemonic()`,
+ /// `self.quiz_mnemonic_word()`, etc.
+ ///
+ /// This function is defined in the HAL so unit tests can easily mock it. Real implementations
+ /// should leave the default implementation.
+ async fn show_and_confirm_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error>
+ where
+ Self: Sized,
+ {
+ mnemonic::show_and_confirm_mnemonic(self, words).await
+ }
+
+ /// Retrieve a BIP39 mnemonic sentence of 12 or 24 words from the user.
+ ///
+ /// This function is defined in the HAL so unit tests can easily mock it. Real implementations
+ /// should leave the default implementation.
+ async fn get_mnemonic(&mut self) -> Result<zeroize::Zeroizing<String>, cancel::Error>
+ where
+ Self: Sized,
+ {
+ mnemonic::get(self).await
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs
index 2d4f126..653b193 100644
--- a/src/rust/bitbox02-rust/src/hww.rs
+++ b/src/rust/bitbox02-rust/src/hww.rs
@@ -382,7 +382,7 @@ mod tests {
.unwrap();
assert!(!crate::keystore::is_locked());
assert!(!mock_hal.memory.is_initialized());
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
make_request(
&mut mock_hal,
(crate::pb::Request {
@@ -423,7 +423,7 @@ mod tests {
// Can't reboot when initialized but locked.
crate::keystore::lock();
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
let response_encoded =
make_request(&mut mock_hal, &reboot_request.encode_to_vec()).unwrap();
let response = crate::pb::Response::decode(&response_encoded[..]).unwrap();
@@ -436,7 +436,7 @@ mod tests {
);
// Unlock.
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
mock_hal
.ui
.set_enter_string(Box::new(|_params| Ok("password".into())));
@@ -448,7 +448,7 @@ mod tests {
// Since in the previous request the msg was encrypted but not decrypted (query was
// rejected), the noise states are out of sync and we need to make a new channel.
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
let mut make_request = init_noise();
let reboot_called = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
make_request(&mut mock_hal, reboot_request.encode_to_vec().as_ref()).unwrap();
@@ -509,7 +509,7 @@ mod tests {
}]
);
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
make_request(
&mut mock_hal,
(crate::pb::Request {
@@ -541,7 +541,7 @@ mod tests {
let seed = crate::keystore::copy_seed(&mut mock_hal).unwrap();
assert_eq!(seed.len(), host_entropy.len());
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
assert!(matches!(
crate::pb::Response::decode(
make_request(
@@ -566,7 +566,7 @@ mod tests {
));
assert_eq!(mock_hal.ui.screens, vec![]);
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
make_request(
&mut mock_hal,
(crate::pb::Request {
@@ -593,7 +593,7 @@ mod tests {
]
);
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
assert!(matches!(
crate::pb::Response::decode(
make_request(
@@ -618,7 +618,7 @@ mod tests {
));
assert_eq!(mock_hal.ui.screens, vec![]);
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
let backup_id = match crate::pb::Response::decode(
make_request(
&mut mock_hal,
@@ -655,7 +655,7 @@ mod tests {
};
assert_eq!(mock_hal.ui.screens, vec![]);
- mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
+ mock_hal.ui = crate::workflow::testing::TestingUi::new();
mock_hal
.ui
.set_enter_string(Box::new(|_params| Ok("password".into())));
diff --git a/src/rust/bitbox02-rust/src/hww/api/reset.rs b/src/rust/bitbox02-rust/src/hww/api/reset.rs
index 4c7e427..349e0d6 100644
--- a/src/rust/bitbox02-rust/src/hww/api/reset.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/reset.rs
@@ -5,7 +5,8 @@ use crate::pb;
use pb::response::Response;
-use crate::workflow::{Workflows, confirm};
+use crate::hal::Ui;
+use crate::workflow::confirm;
pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
let params = confirm::Params {
diff --git a/src/rust/bitbox02-rust/src/workflow.rs b/src/rust/bitbox02-rust/src/workflow.rs
index 2c3777c..1f2f84e 100644
--- a/src/rust/bitbox02-rust/src/workflow.rs
+++ b/src/rust/bitbox02-rust/src/workflow.rs
@@ -23,156 +23,3 @@ pub mod u2f_c_api;
pub mod unlock;
pub mod unlock_animation;
pub mod verify_message;
-
-use alloc::string::String;
-
-#[allow(async_fn_in_trait)]
-pub trait Workflows {
- async fn confirm(&mut self, params: &confirm::Params<'_>) -> Result<(), confirm::UserAbort>;
-
- async fn verify_recipient(
- &mut self,
- recipient: &str,
- amount: &str,
- ) -> Result<(), transaction::UserAbort>;
-
- async fn verify_total_fee(
- &mut self,
- total: &str,
- fee: &str,
- longtouch: bool,
- ) -> Result<(), transaction::UserAbort>;
-
- async fn status(&mut self, title: &str, status_success: bool);
-
- async fn enter_string(
- &mut self,
- params: &trinary_input_string::Params<'_>,
- can_cancel: trinary_input_string::CanCancel,
- preset: &str,
- ) -> Result<zeroize::Zeroizing<String>, trinary_input_string::Error>;
-
- async fn insert_sdcard(&mut self) -> Result<(), sdcard::UserAbort>;
-
- async fn menu(&mut self, words: &[&str], title: Option<&str>) -> Result<u8, menu::CancelError>;
-
- async fn trinary_choice(
- &mut self,
- message: &str,
- label_left: Option<&str>,
- label_middle: Option<&str>,
- label_right: Option<&str>,
- ) -> trinary_choice::TrinaryChoice;
-
- /// Display the BIP39 mnemonic to the user.
- async fn show_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error>;
-
- /// Display these BIP39 mnemonic word choices to the user as part of the quiz to confirm the
- /// user backuped up the mnemonic correctly.
- async fn quiz_mnemonic_word(
- &mut self,
- choices: &[&str],
- title: &str,
- ) -> Result<u8, cancel::Error>;
-
- /// Display the mnemonic words and have the user confirm them in a multiple-choice quiz.
- ///
- /// The default implementation is implemented in terms of `self.show_mnemonic()`,
- /// `self.quiz_mnemonic_word()`, etc.
- ///
- /// This function is defined in the HAL so unit tests can easily mock it. Real implementations
- /// should leave the default implementation.
- async fn show_and_confirm_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error>
- where
- Self: Sized,
- {
- mnemonic::show_and_confirm_mnemonic(self, words).await
- }
-
- /// Retrieve a BIP39 mnemonic sentence of 12 or 24 words from the user.
- ///
- /// This function is defined in the HAL so unit tests can easily mock it. Real implementations
- /// should leave the default implementation.
- async fn get_mnemonic(&mut self) -> Result<zeroize::Zeroizing<String>, cancel::Error>
- where
- Self: Sized,
- {
- mnemonic::get(self).await
- }
-}
-
-pub struct RealWorkflows;
-
-impl Workflows for RealWorkflows {
- #[inline(always)]
- async fn confirm(&mut self, params: &confirm::Params<'_>) -> Result<(), confirm::UserAbort> {
- confirm::confirm(params).await
- }
-
- #[inline(always)]
- async fn verify_recipient(
- &mut self,
- recipient: &str,
- amount: &str,
- ) -> Result<(), transaction::UserAbort> {
- transaction::verify_recipient(recipient, amount).await
- }
-
- #[inline(always)]
- async fn verify_total_fee(
- &mut self,
- total: &str,
- fee: &str,
- longtouch: bool,
- ) -> Result<(), transaction::UserAbort> {
- transaction::verify_total_fee(total, fee, longtouch).await
- }
-
- #[inline(always)]
- async fn status(&mut self, title: &str, status_success: bool) {
- status::status(title, status_success).await
- }
-
- #[inline(always)]
- async fn enter_string(
- &mut self,
- params: &trinary_input_string::Params<'_>,
- can_cancel: trinary_input_string::CanCancel,
- preset: &str,
- ) -> Result<zeroize::Zeroizing<String>, trinary_input_string::Error> {
- trinary_input_string::enter(params, can_cancel, preset).await
- }
-
- #[inline(always)]
- async fn insert_sdcard(&mut self) -> Result<(), sdcard::UserAbort> {
- sdcard::sdcard().await
- }
-
- #[inline(always)]
- async fn menu(&mut self, words: &[&str], title: Option<&str>) -> Result<u8, menu::CancelError> {
- menu::pick(words, title).await
- }
-
- #[inline(always)]
- async fn trinary_choice(
- &mut self,
- message: &str,
- label_left: Option<&str>,
- label_middle: Option<&str>,
- label_right: Option<&str>,
- ) -> trinary_choice::TrinaryChoice {
- trinary_choice::choose(message, label_left, label_middle, label_right).await
- }
-
- async fn show_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error> {
- mnemonic::show_mnemonic(words).await
- }
-
- async fn quiz_mnemonic_word(
- &mut self,
- choices: &[&str],
- title: &str,
- ) -> Result<u8, cancel::Error> {
- mnemonic::confirm_word(choices, title).await
- }
-}
diff --git a/src/rust/bitbox02-rust/src/workflow/password.rs b/src/rust/bitbox02-rust/src/workflow/password.rs
index 339771c..0b2a64b 100644
--- a/src/rust/bitbox02-rust/src/workflow/password.rs
+++ b/src/rust/bitbox02-rust/src/workflow/password.rs
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
-use super::{Workflows, confirm, trinary_input_string};
+use super::{confirm, trinary_input_string};
+use crate::hal::Ui;
use crate::hal::Memory;
use bitbox02::memory::SecurechipType;
diff --git a/src/rust/bitbox02-rust/src/workflow/testing.rs b/src/rust/bitbox02-rust/src/workflow/testing.rs
index 58d7c10..a590b14 100644
--- a/src/rust/bitbox02-rust/src/workflow/testing.rs
+++ b/src/rust/bitbox02-rust/src/workflow/testing.rs
@@ -1,210 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
-use super::{
- Workflows, cancel, confirm, menu, sdcard, transaction, trinary_choice, trinary_input_string,
-};
-
-use alloc::boxed::Box;
-use alloc::string::String;
-use alloc::vec::Vec;
-
-#[derive(Debug, Eq, PartialEq, Clone)]
-pub enum Screen {
- Confirm {
- title: String,
- body: String,
- longtouch: bool,
- },
- TotalFee {
- total: String,
- fee: String,
- longtouch: bool,
- },
- Recipient {
- recipient: String,
- amount: String,
- },
- Status {
- title: String,
- success: bool,
- },
- ShowAndConfirmMnemonic {
- mnemonic: String,
- },
- More,
-}
-
-type EnterStringCb<'a> = Box<
- dyn FnMut(&trinary_input_string::Params<'_>) -> Result<String, trinary_input_string::Error>
- + 'a,
->;
-
-/// An Workflows implementation for unit tests. Collects all screens and provides helper functions
-/// to verify them.
-pub struct TestingWorkflows<'a> {
- _abort_nth: Option<usize>,
- pub screens: Vec<Screen>,
- _enter_string: Option<EnterStringCb<'a>>,
-}
-
-impl Workflows for TestingWorkflows<'_> {
- async fn confirm(&mut self, params: &confirm::Params<'_>) -> Result<(), confirm::UserAbort> {
- self.screens.push(Screen::Confirm {
- title: params.title.into(),
- body: params.body.into(),
- longtouch: params.longtouch,
- });
- if self
- ._abort_nth
- .as_ref()
- .is_some_and(|&n| self.screens.len() - 1 == n)
- {
- return Err(confirm::UserAbort);
- }
- Ok(())
- }
-
- async fn verify_recipient(
- &mut self,
- recipient: &str,
- amount: &str,
- ) -> Result<(), transaction::UserAbort> {
- self.screens.push(Screen::Recipient {
- recipient: recipient.into(),
- amount: amount.into(),
- });
- if self
- ._abort_nth
- .as_ref()
- .is_some_and(|&n| self.screens.len() - 1 == n)
- {
- return Err(transaction::UserAbort);
- }
- Ok(())
- }
-
- async fn verify_total_fee(
- &mut self,
- total: &str,
- fee: &str,
- longtouch: bool,
- ) -> Result<(), transaction::UserAbort> {
- self.screens.push(Screen::TotalFee {
- total: total.into(),
- fee: fee.into(),
- longtouch,
- });
- if self
- ._abort_nth
- .as_ref()
- .is_some_and(|&n| self.screens.len() - 1 == n)
- {
- return Err(transaction::UserAbort);
- }
- Ok(())
- }
-
- async fn status(&mut self, title: &str, status_success: bool) {
- self.screens.push(Screen::Status {
- title: title.into(),
- success: status_success,
- });
- if self
- ._abort_nth
- .as_ref()
- .is_some_and(|&n| self.screens.len() - 1 == n)
- {
- panic!("canot abort status screen");
- }
- }
-
- async fn enter_string(
- &mut self,
- params: &trinary_input_string::Params<'_>,
- _can_cancel: trinary_input_string::CanCancel,
- _preset: &str,
- ) -> Result<zeroize::Zeroizing<String>, trinary_input_string::Error> {
- self._enter_string.as_mut().unwrap()(params).map(zeroize::Zeroizing::new)
- }
-
- async fn insert_sdcard(&mut self) -> Result<(), sdcard::UserAbort> {
- Ok(())
- }
-
- async fn menu(
- &mut self,
- _words: &[&str],
- _title: Option<&str>,
- ) -> Result<u8, menu::CancelError> {
- todo!("not used in unit tests yet");
- }
-
- async fn trinary_choice(
- &mut self,
- _message: &str,
- _label_left: Option<&str>,
- _label_middle: Option<&str>,
- _label_right: Option<&str>,
- ) -> trinary_choice::TrinaryChoice {
- todo!("not used in unit tests yet");
- }
-
- async fn show_mnemonic(&mut self, _words: &[&str]) -> Result<(), cancel::Error> {
- todo!("not used in unit tests yet");
- }
-
- async fn quiz_mnemonic_word(
- &mut self,
- _choices: &[&str],
- _title: &str,
- ) -> Result<u8, cancel::Error> {
- todo!("not used in unit tests yet");
- }
-
- async fn show_and_confirm_mnemonic(&mut self, words: &[&str]) -> Result<(), cancel::Error> {
- self.screens.push(Screen::ShowAndConfirmMnemonic {
- mnemonic: words.join(" "),
- });
- Ok(())
- }
-
- async fn get_mnemonic(&mut self) -> Result<zeroize::Zeroizing<String>, cancel::Error>
- where
- Self: Sized,
- {
- let words = "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide";
-
- Ok(zeroize::Zeroizing::new(words.into()))
- }
-}
-
-impl<'a> TestingWorkflows<'a> {
- pub fn new() -> Self {
- Self {
- screens: vec![],
- _abort_nth: None,
- _enter_string: None,
- }
- }
-
- /// Make the `n`-th workflow (0-indexed) fail with a user abort. If that workflow cannot be
- /// aborted, there will be panic.
- pub fn abort_nth(&mut self, n: usize) {
- self._abort_nth = Some(n);
- }
-
- pub fn contains_confirm(&self, confirm_title: &str, confirm_body: &str) -> bool {
- self.screens.iter().any(|screen| match screen {
- Screen::Confirm { title, body, .. } => title == confirm_title && body == confirm_body,
- _ => false,
- })
- }
-
- pub fn set_enter_string(&mut self, cb: EnterStringCb<'a>) {
- self._enter_string = Some(cb);
- }
-
- pub fn remove_enter_string(&mut self) {
- self._enter_string = None;
- }
-}
+pub use crate::hal::testing::ui::TestingUi as Ui;
+pub use crate::hal::testing::ui::*;
diff --git a/src/rust/bitbox02-rust/src/workflow/verify_message.rs b/src/rust/bitbox02-rust/src/workflow/verify_message.rs
index 67f730a..7b3ba89 100644
--- a/src/rust/bitbox02-rust/src/workflow/verify_message.rs
+++ b/src/rust/bitbox02-rust/src/workflow/verify_message.rs
@@ -2,7 +2,8 @@
use alloc::vec::Vec;
-use super::{Workflows, confirm};
+use super::confirm;
+use crate::hal::Ui;
use util::ascii;
Why this scored 15/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.