What changed, and why it matters
This commit is a routine code cleanup in the BitBox02 firmware's user-interface layer. It removes a convenience method called get_mnemonic from the UI hardware-abstraction trait and makes callers use the underlying workflow function directly. The change also expands unit-test helpers so the existing restore-from-mnemonic test can simulate a 24-word recovery phrase. There is no indication this fixes a security bug or changes user-visible behavior.
No security action required. Treat as normal refactoring/test-coverage improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch removes Ui::get_mnemonic(), which had a default implementation that delegated to workflow::mnemonic::get(self). Call sites (restore.rs) now call workflow::mnemonic::get(hal.ui()). The testing UI mock gains settable menu and trinary_choice callbacks and a helper prepare_get_mnemonic_24_words() so tests can drive the 12/24-word selection and 24-word entry flow. A new unit test verifies get() returns the expected mnemonic. No cryptographic, memory-safety, or access-control changes are present.
Changed components
src/rust/bitbox02-rust/src/hal/ui.rssrc/rust/bitbox02-rust/src/hal/testing/ui.rssrc/rust/bitbox02-rust/src/hww/api/restore.rssrc/rust/bitbox02-rust/src/workflow/mnemonic.rsInspect captured patch +102 / −27
diff --git a/src/rust/bitbox02-rust/src/hal/testing/ui.rs b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
index 5eb3a89..276d2fc 100644
--- a/src/rust/bitbox02-rust/src/hal/testing/ui.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
@@ -40,6 +40,9 @@ pub enum Screen {
}
type EnterStringCb<'a> = Box<dyn FnMut(&EnterStringParams<'_>) -> Result<String, UserAbort> + 'a>;
+type MenuCb<'a> = Box<dyn FnMut(&[&str], Option<&str>) -> Result<u8, UserAbort> + 'a>;
+type TrinaryChoiceCb<'a> =
+ Box<dyn FnMut(&str, Option<&str>, Option<&str>, Option<&str>) -> TrinaryChoice + 'a>;
/// A Ui implementation for unit tests. Collects all screens and provides helper functions
/// to verify them.
@@ -47,6 +50,8 @@ pub struct TestingUi<'a> {
_abort_nth: Option<usize>,
pub screens: Vec<Screen>,
_enter_string: Option<EnterStringCb<'a>>,
+ _menu: Option<MenuCb<'a>>,
+ _trinary_choice: Option<TrinaryChoiceCb<'a>>,
_quiz_choices: VecDeque<u8>,
}
@@ -130,18 +135,18 @@ impl Ui for TestingUi<'_> {
Ok(())
}
- async fn menu(&mut self, _words: &[&str], _title: Option<&str>) -> Result<u8, UserAbort> {
- todo!("not used in unit tests yet");
+ async fn menu(&mut self, words: &[&str], title: Option<&str>) -> Result<u8, UserAbort> {
+ self._menu.as_mut().unwrap()(words, title)
}
async fn trinary_choice(
&mut self,
- _message: &str,
- _label_left: Option<&str>,
- _label_middle: Option<&str>,
- _label_right: Option<&str>,
+ message: &str,
+ label_left: Option<&str>,
+ label_middle: Option<&str>,
+ label_right: Option<&str>,
) -> TrinaryChoice {
- todo!("not used in unit tests yet");
+ self._trinary_choice.as_mut().unwrap()(message, label_left, label_middle, label_right)
}
async fn show_mnemonic(&mut self, words: &[&str]) -> Result<(), UserAbort> {
@@ -194,6 +199,8 @@ impl<'a> TestingUi<'a> {
screens: vec![],
_abort_nth: None,
_enter_string: None,
+ _menu: None,
+ _trinary_choice: None,
_quiz_choices: VecDeque::new(),
}
}
@@ -219,6 +226,22 @@ impl<'a> TestingUi<'a> {
self._enter_string = None;
}
+ pub fn set_menu(&mut self, cb: MenuCb<'a>) {
+ self._menu = Some(cb);
+ }
+
+ pub fn remove_menu(&mut self) {
+ self._menu = None;
+ }
+
+ pub fn set_trinary_choice(&mut self, cb: TrinaryChoiceCb<'a>) {
+ self._trinary_choice = Some(cb);
+ }
+
+ pub fn remove_trinary_choice(&mut self) {
+ self._trinary_choice = None;
+ }
+
pub fn push_quiz_choice(&mut self, selected: u8) {
self._quiz_choices.push_back(selected);
}
@@ -265,6 +288,49 @@ impl<'a> TestingUi<'a> {
}
}
+ /// Configure inputs for `workflow::mnemonic::get()` with a 24-word mnemonic.
+ /// This also wraps an existing `enter_string` callback for non-mnemonic prompts,
+ /// e.g. password entry in higher-level workflows.
+ pub fn prepare_get_mnemonic_24_words(&mut self, words: &[&str]) {
+ assert_eq!(words.len(), 24, "expected exactly 24 words");
+ let words: Vec<String> = words.iter().map(|word| (*word).into()).collect();
+ let mut first_words: VecDeque<String> = words[..23].iter().cloned().collect();
+ let last_word = words[23].clone();
+ let mut fallback_enter_string = self._enter_string.take();
+
+ self.set_trinary_choice(Box::new(
+ |message, label_left, label_middle, label_right| {
+ assert_eq!(message, "How many words?");
+ assert_eq!(label_left, Some("12"));
+ assert_eq!(label_middle, None);
+ assert_eq!(label_right, Some("24"));
+ TrinaryChoice::Right
+ },
+ ));
+
+ self.set_menu(Box::new(move |menu_words, title| {
+ assert_eq!(title, Some("24 of 24"));
+ Ok(menu_words
+ .iter()
+ .position(|word| *word == last_word.as_str())
+ .unwrap()
+ .try_into()
+ .unwrap())
+ }));
+
+ self.set_enter_string(Box::new(move |params| {
+ if params.wordlist.is_some() && params.title.ends_with(" of 24") {
+ return Ok(first_words
+ .pop_front()
+ .expect("too many mnemonic word entries"));
+ }
+ if let Some(ref mut fallback) = fallback_enter_string {
+ return fallback(params);
+ }
+ panic!("unexpected enter_string call: {}", params.title);
+ }));
+ }
+
/// Assert screens emitted by `workflow::mnemonic::show_and_confirm_mnemonic()`.
pub fn assert_show_and_confirm_mnemonic_screens(screens: &[Screen], words: &[&str]) {
assert_eq!(
diff --git a/src/rust/bitbox02-rust/src/hal/ui.rs b/src/rust/bitbox02-rust/src/hal/ui.rs
index e6620cb..c41baa8 100644
--- a/src/rust/bitbox02-rust/src/hal/ui.rs
+++ b/src/rust/bitbox02-rust/src/hal/ui.rs
@@ -1,7 +1,5 @@
// SPDX-License-Identifier: Apache-2.0
-use crate::workflow::mnemonic;
-
use alloc::string::String;
pub struct UserAbort;
@@ -107,15 +105,4 @@ pub trait Ui {
/// 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, UserAbort>;
-
- /// 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>, UserAbort>
- where
- Self: Sized,
- {
- mnemonic::get(self).await
- }
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs
index 96678f8..9542506 100644
--- a/src/rust/bitbox02-rust/src/hww/api/restore.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs
@@ -105,7 +105,7 @@ pub async fn from_mnemonic(
.await?;
}
- let mnemonic = hal.ui().get_mnemonic().await?;
+ let mnemonic = crate::workflow::mnemonic::get(hal.ui()).await?;
let seed = match crate::bip39::mnemonic_to_seed(&mnemonic) {
Ok(seed) => seed,
Err(()) => {
@@ -163,22 +163,27 @@ mod tests {
use util::bb02_async::block_on;
use alloc::boxed::Box;
+ use alloc::vec::Vec;
#[test]
fn test_from_mnemonic() {
mock_memory();
crate::keystore::lock();
- let mut counter = 0u32;
+ let mnemonic_words: Vec<&str> = "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide"
+ .split(' ')
+ .collect();
+ let mut password_entries = 0usize;
let mut mock_hal = TestingHal::new();
mock_hal.ui.set_enter_string(Box::new(|params| {
- counter += 1;
- match counter {
+ password_entries += 1;
+ match password_entries {
1 => assert_eq!(params.title, "Set password"),
2 => assert_eq!(params.title, "Repeat password"),
- _ => panic!("too many user inputs"),
+ _ => panic!("too many password user inputs"),
}
Ok("password".into())
}));
+ mock_hal.ui.prepare_get_mnemonic_24_words(&mnemonic_words);
mock_hal.securechip.event_counter_reset();
assert_eq!(
@@ -206,7 +211,7 @@ mod tests {
"257724bccc8858cfe565b456b01263a4a6a45184fab4531f5c199649207a74e74c399a01d4f957258c05cee818369b31404c884a4b7a29ff6886bae6700fb56a"
);
- drop(mock_hal); // to remove mutable borrow of counter
- assert_eq!(counter, 2);
+ drop(mock_hal); // to remove mutable borrow of password_entries
+ assert_eq!(password_entries, 2);
}
}
diff --git a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs
index f5860ae..cec4770 100644
--- a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs
+++ b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs
@@ -430,6 +430,23 @@ mod tests {
TestingUi::assert_show_and_confirm_mnemonic_screens(&ui.screens, &words);
}
+ #[test]
+ fn test_get() {
+ let words: Vec<&str> = "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide"
+ .split(' ')
+ .collect();
+ let mut ui = TestingUi::new();
+ ui.prepare_get_mnemonic_24_words(&words);
+
+ let result = block_on(get(&mut ui));
+ assert!(result.is_ok());
+ let mnemonic = match result {
+ Ok(mnemonic) => mnemonic,
+ Err(_) => panic!("unexpected user abort"),
+ };
+ assert_eq!(mnemonic.as_str(), words.join(" "));
+ }
+
#[test]
fn test_lastword_choices() {
// 23 words
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.