What changed, and why it matters
This commit rewrites the device factory-reset routine from C to Rust. It is a straightforward language port: the same steps (lock keystore, reset secure-chip keys, reset U2F counter, wipe memory, disable SmartEEPROM, show a status screen, reboot) are preserved. The change also adds unit tests and makes the unlock function asynchronous so it can call the new async reset. Nothing in the diff introduces a new vulnerability or changes security-critical behavior in a suspicious way.
No security action required. Treat as a normal refactoring commit. Reviewers may optionally verify that the Rust abort() helper behaves identically to the C Abort() for the three fatal error paths (secure chip reset failure, U2F counter init failure, memory reset failure).
Security signals we found
No new security-relevant behavior introduced
Existing security operations preserved during C-to-Rust port
Added unit-test coverage for reset retry and status display
Made unlock() async to accommodate async reset()
Removed direct C abort paths from reset_reset; Rust uses abort() helper for the same fatal errors
Evidence from the diff
The C function reset_reset() is removed and replaced by a Rust async function reset::reset() in bitbox02-rust. The Rust implementation mirrors the original logic: keystore lock, USB watchdog timeout extension, securechip_reset_keys() with up to 5 retries, securechip_u2f_counter_set(0) with up to 5 retries (under app-u2f feature), memory_reset_hww(), smarteeprom_disable(), UI status screen, BLE reset on BitBox02Plus, and reboot. FFI bindings are updated accordingly. Tests are added for success and failure status screens, including retry behavior for reset_keys(). keystore::unlock() is made async so it can await reset() after max password attempts.
Changed components
src/reset.csrc/reset.hsrc/rust/bitbox02-rust/src/reset.rssrc/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02-rust/src/hal.rssrc/rust/bitbox02-rust/src/hww/api/reset.rssrc/rust/bitbox02-rust/src/workflow/unlock.rssrc/rust/bitbox02/src/lib.rssrc/rust/bitbox02/src/securechip.rssrc/rust/bitbox02/src/smarteeprom.rssrc/rust/bitbox02-sys/build.rsInspect captured patch +314 / −131
diff --git a/src/reset.c b/src/reset.c
index 16edbd1..1535b16 100644
--- a/src/reset.c
+++ b/src/reset.c
@@ -19,35 +19,13 @@
#include "keystore.h"
#include "memory/memory.h"
#include "memory/memory_shared.h"
-#include "memory/smarteeprom.h"
#include "system.h"
#include "uart.h"
#include <rust/rust.h>
#include <screen.h>
#ifndef TESTING
- #include "securechip/securechip.h"
#include <driver_init.h>
- #include <hal_delay.h>
- #include <ui/components/status.h>
- #include <ui/ugui/ugui.h>
-#endif
-
-#if !defined(TESTING)
-/*
- * Shows a centered "Device reset" label.
- * Waits for 3000ms, then exit.
- */
-static void _show_reset_label(bool status)
-{
- const char* msg = "Device reset";
- component_t* comp = status_create(msg, status, NULL, NULL);
- screen_clear();
- comp->f->render(comp);
- UG_SendBuffer();
- comp->f->cleanup(comp);
- delay_ms(3000);
-}
#endif
void reset_ble(void)
@@ -62,49 +40,3 @@ void reset_ble(void)
}
#endif
}
-
-void reset_reset(bool status)
-{
- rust_keystore_lock();
-#if !defined(TESTING)
- bool sc_result_reset_keys = false;
- for (int retries = 0; retries < 5; retries++) {
- sc_result_reset_keys = securechip_reset_keys();
- if (sc_result_reset_keys) {
- break;
- }
- }
- if (!sc_result_reset_keys) {
- Abort("Could not reset secure chip.");
- }
- #if APP_U2F == 1
- bool sc_result_u2f_counter_set = false;
- for (int retries = 0; retries < 5; retries++) {
- sc_result_u2f_counter_set = securechip_u2f_counter_set(0);
- if (sc_result_u2f_counter_set) {
- break;
- }
- }
- if (!sc_result_u2f_counter_set) {
- Abort("Could not initialize U2F counter.");
- }
- #endif
-#endif
- if (!memory_reset_hww()) {
- Abort("Could not reset memory.");
- }
-#if !defined(TESTING)
- /* Disable SmartEEPROM, so it will be erased on next reboot. */
- smarteeprom_disable();
- _show_reset_label(status);
-
- // The ble chip needs to be restarted to load the new secrets.
- if (memory_get_platform() == MEMORY_PLATFORM_BITBOX02_PLUS) {
- reset_ble();
- }
-
- reboot();
-#else
- (void)status;
-#endif
-}
diff --git a/src/reset.h b/src/reset.h
index 3121f6e..ee19d92 100644
--- a/src/reset.h
+++ b/src/reset.h
@@ -22,16 +22,4 @@
* memory.
*/
void reset_ble(void);
-
-/**
- * Resets the device:
- * - Updates secure chip KDF keys.
- * - Resets the securechip eeprom (u2f counter).
- * - Resets MCU flash app memory.
- * - Resets smart eeprom memory.
- * - Shows a "Device reset" status message.
- * @param[in] status If the status message should indicate success or failure
- * (the reset was user invoked or forced).
- */
-void reset_reset(bool status);
#endif
diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs
index 5da76bc..7d74c7d 100644
--- a/src/rust/bitbox02-rust/src/hal.rs
+++ b/src/rust/bitbox02-rust/src/hal.rs
@@ -55,6 +55,9 @@ pub trait SecureChip {
) -> Result<(), ()>;
fn monotonic_increments_remaining(&mut self) -> Result<u32, ()>;
fn model(&mut self) -> Result<bitbox02::securechip::Model, ()>;
+ fn reset_keys(&mut self) -> Result<(), ()>;
+ #[cfg(feature = "app-u2f")]
+ fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()>;
}
/// Hardware abstraction layer for BitBox devices.
@@ -153,6 +156,15 @@ impl SecureChip for BitBox02SecureChip {
fn model(&mut self) -> Result<bitbox02::securechip::Model, ()> {
bitbox02::securechip::model()
}
+
+ fn reset_keys(&mut self) -> Result<(), ()> {
+ bitbox02::securechip::reset_keys()
+ }
+
+ #[cfg(feature = "app-u2f")]
+ fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()> {
+ bitbox02::securechip::u2f_counter_set(counter)
+ }
}
pub struct BitBox02Hal {
@@ -289,15 +301,23 @@ pub mod testing {
}
pub struct TestingSecureChip {
- // Count how man seceurity events happen. The numbers were obtained by reading the security
+ // 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,
}
impl TestingSecureChip {
pub fn new() -> Self {
- TestingSecureChip { event_counter: 0 }
+ TestingSecureChip {
+ event_counter: 0,
+ reset_keys_fail_once: false,
+ #[cfg(feature = "app-u2f")]
+ u2f_counter: 0,
+ }
}
/// Resets the event counter.
@@ -312,6 +332,16 @@ pub mod testing {
// TODO: remove fake_event_counter() once all unit tests use the SecureChip HAL.
bitbox02::securechip::fake_event_counter() + 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
+ }
}
impl super::SecureChip for TestingSecureChip {
@@ -371,6 +401,22 @@ pub mod testing {
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 += 1;
+ Ok(())
+ }
+ }
+
+ #[cfg(feature = "app-u2f")]
+ fn u2f_counter_set(&mut self, counter: u32) -> Result<(), ()> {
+ self.u2f_counter = counter;
+ Ok(())
+ }
}
pub struct TestingHal<'a> {
diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs
index e02e42f..b8fd8ab 100644
--- a/src/rust/bitbox02-rust/src/hww.rs
+++ b/src/rust/bitbox02-rust/src/hww.rs
@@ -593,11 +593,17 @@ mod tests {
.unwrap();
assert_eq!(
mock_hal.ui.screens,
- vec![Screen::Confirm {
- title: "RESET".into(),
- body: "Proceed to\nfactory reset?".into(),
- longtouch: true,
- }]
+ vec![
+ Screen::Confirm {
+ title: "RESET".into(),
+ body: "Proceed to\nfactory reset?".into(),
+ longtouch: true,
+ },
+ Screen::Status {
+ title: "Device reset".into(),
+ success: true,
+ }
+ ]
);
mock_hal.ui = crate::workflow::testing::TestingWorkflows::new();
diff --git a/src/rust/bitbox02-rust/src/hww/api/change_password.rs b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
index 022570b..8328425 100644
--- a/src/rust/bitbox02-rust/src/hww/api/change_password.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
@@ -112,12 +112,12 @@ mod tests {
// create new hal instance to call unlock
let mut hal_verify = TestingHal::new();
assert!(matches!(
- keystore::unlock(&mut hal_verify, old_password),
+ block_on(keystore::unlock(&mut hal_verify, old_password)),
Err(keystore::Error::IncorrectPassword)
));
// check that the new password is valid
assert_eq!(
- keystore::unlock(&mut hal_verify, new_password)
+ block_on(keystore::unlock(&mut hal_verify, new_password))
.unwrap()
.as_slice(),
seed.as_slice()
@@ -171,7 +171,7 @@ mod tests {
// check that the old password is still valid
let mut hal_verify = TestingHal::new();
assert_eq!(
- keystore::unlock(&mut hal_verify, correct_password)
+ block_on(keystore::unlock(&mut hal_verify, correct_password))
.unwrap()
.as_slice(),
seed.as_slice()
@@ -220,7 +220,7 @@ mod tests {
// check that the old password is still valid
let mut hal_verify = TestingHal::new();
assert_eq!(
- keystore::unlock(&mut hal_verify, old_password)
+ block_on(keystore::unlock(&mut hal_verify, old_password))
.unwrap()
.as_slice(),
seed.as_slice()
diff --git a/src/rust/bitbox02-rust/src/hww/api/reset.rs b/src/rust/bitbox02-rust/src/hww/api/reset.rs
index d70aaf1..a2db511 100644
--- a/src/rust/bitbox02-rust/src/hww/api/reset.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/reset.rs
@@ -17,8 +17,7 @@ use crate::pb;
use pb::response::Response;
-use crate::hal::Ui;
-use crate::workflow::confirm;
+use crate::workflow::{Workflows, confirm};
pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
let params = confirm::Params {
@@ -30,7 +29,7 @@ pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error>
hal.ui().confirm(¶ms).await.or(Err(Error::Generic))?;
- bitbox02::reset(true);
+ crate::reset::reset(hal, true).await;
Ok(Response::Success(pb::Success {}))
}
@@ -75,11 +74,17 @@ mod tests {
);
assert_eq!(
mock_hal.ui.screens,
- vec![Screen::Confirm {
- title: "RESET".into(),
- body: "Proceed to\nfactory reset?".into(),
- longtouch: true,
- }],
+ vec![
+ Screen::Confirm {
+ title: "RESET".into(),
+ body: "Proceed to\nfactory reset?".into(),
+ longtouch: true,
+ },
+ Screen::Status {
+ title: "Device reset".into(),
+ success: true,
+ }
+ ],
);
assert_eq!(bitbox02::memory::get_device_name().as_str(), "My BitBox");
}
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index 2592e1d..6b59680 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -321,7 +321,7 @@ fn get_and_decrypt_seed(
Ok(seed)
}
-pub fn unlock(
+pub async fn unlock(
hal: &mut impl crate::hal::Hal,
password: &str,
) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
@@ -334,7 +334,7 @@ pub fn unlock(
// is made. So we should never enter this branch...
// This is just an extraordinary measure for added resilience.
//
- bitbox02::reset(false);
+ crate::reset::reset(hal, false).await;
return Err(Error::MaxAttemptsExceeded);
}
bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT);
@@ -343,7 +343,7 @@ pub fn unlock(
Ok(seed) => seed,
err @ Err(_) => {
if get_remaining_unlock_attempts() == 0 {
- bitbox02::reset(false);
+ crate::reset::reset(hal, false).await;
return Err(Error::MaxAttemptsExceeded);
}
return err;
@@ -943,7 +943,7 @@ mod tests {
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "old_password").is_ok());
// Step 2: Unlock with initial password and set up BIP39
- let unlocked_seed = unlock(&mut mock_hal, "old_password").unwrap();
+ let unlocked_seed = block_on(unlock(&mut mock_hal, "old_password")).unwrap();
assert_eq!(unlocked_seed.as_slice(), seed.as_slice());
assert!(block_on(unlock_bip39(&mut mock_hal, &seed, "", async || {})).is_ok());
@@ -954,12 +954,12 @@ mod tests {
// Step 4: Lock and verify old password no longer works
lock();
assert!(matches!(
- unlock(&mut mock_hal, "old_password"),
+ block_on(unlock(&mut mock_hal, "old_password")),
Err(Error::IncorrectPassword)
));
// Step 5: Verify new password works
- let unlocked_seed_new = unlock(&mut mock_hal, "new_password").unwrap();
+ let unlocked_seed_new = block_on(unlock(&mut mock_hal, "new_password")).unwrap();
assert_eq!(unlocked_seed_new.as_slice(), seed.as_slice());
}
@@ -1008,7 +1008,7 @@ mod tests {
// Initial setup
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "password").is_ok());
- unlock(&mut mock_hal, "password").unwrap();
+ block_on(unlock(&mut mock_hal, "password")).unwrap();
assert!(block_on(unlock_bip39(&mut mock_hal, &seed, "", async || {})).is_ok());
@@ -1109,7 +1109,7 @@ mod tests {
let mut mock_hal = TestingHal::new();
assert!(matches!(
- unlock(&mut mock_hal, "password"),
+ block_on(unlock(&mut mock_hal, "password")),
Err(Error::Unseeded)
));
@@ -1129,7 +1129,12 @@ mod tests {
// First call: unlock. The first one does a seed rentention (1 securechip event).
mock_hal.securechip.event_counter_reset();
- assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed);
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
assert_eq!(mock_hal.securechip.get_event_counter(), 6);
// Loop to check that unlocking works while unlocked.
@@ -1137,7 +1142,12 @@ mod tests {
// Further calls perform a password check.The password check does not do the retention
// so it ends up needing one secure chip operation less.
mock_hal.securechip.event_counter_reset();
- assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed);
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
assert_eq!(mock_hal.securechip.get_event_counter(), 5);
}
@@ -1156,7 +1166,7 @@ mod tests {
// First 9 wrong attempts.
for i in 1..bitbox02::memory::MAX_UNLOCK_ATTEMPTS {
assert!(matches!(
- unlock(&mut mock_hal, "invalid password"),
+ block_on(unlock(&mut mock_hal, "invalid password")),
Err(Error::IncorrectPassword)
));
assert_eq!(
@@ -1170,14 +1180,14 @@ mod tests {
}
// Last attempt, triggers reset.
assert!(matches!(
- unlock(&mut mock_hal, "invalid password"),
+ block_on(unlock(&mut mock_hal, "invalid password")),
Err(Error::MaxAttemptsExceeded),
));
// Last wrong attempt locks & resets. There is no more seed.
assert!(!bitbox02::memory::is_seeded());
assert!(copy_seed(&mut mock_hal).is_err());
assert!(matches!(
- unlock(&mut mock_hal, "password"),
+ block_on(unlock(&mut mock_hal, "password")),
Err(Error::Unseeded)
));
}
@@ -1201,7 +1211,7 @@ mod tests {
for attempt in 1..bitbox02::memory::MAX_UNLOCK_ATTEMPTS {
assert!(matches!(
- unlock(&mut mock_hal, "invalid password"),
+ block_on(unlock(&mut mock_hal, "invalid password")),
Err(Error::IncorrectPassword),
));
@@ -1215,14 +1225,14 @@ mod tests {
}
assert!(matches!(
- unlock(&mut mock_hal, "invalid password"),
+ block_on(unlock(&mut mock_hal, "invalid password")),
Err(Error::MaxAttemptsExceeded)
));
assert!(is_locked());
assert!(copy_seed(&mut mock_hal).is_err());
assert!(!bitbox02::memory::is_seeded());
assert!(matches!(
- unlock(&mut mock_hal, "password"),
+ block_on(unlock(&mut mock_hal, "password")),
Err(Error::Unseeded)
));
}
@@ -1254,7 +1264,7 @@ mod tests {
);
assert!(matches!(
- unlock(&mut mock_hal, "password"),
+ block_on(unlock(&mut mock_hal, "password")),
Err(Error::MaxAttemptsExceeded)
));
assert!(is_locked());
@@ -1281,7 +1291,7 @@ mod tests {
fn wrong_attempt(hal: &mut impl crate::hal::Hal) {
assert!(matches!(
- unlock(hal, "wrong"),
+ block_on(unlock(hal, "wrong")),
Err(Error::IncorrectPassword)
));
assert_eq!(
@@ -1293,7 +1303,12 @@ mod tests {
wrong_attempt(&mut mock_hal);
assert!(copy_seed(&mut mock_hal).is_err());
- assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed);
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
assert!(copy_seed(&mut mock_hal).is_ok());
lock();
@@ -1321,12 +1336,17 @@ mod tests {
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "password").is_ok());
lock();
- assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed);
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
assert!(copy_seed(&mut mock_hal).is_ok());
fn wrong_attempt(hal: &mut impl crate::hal::Hal) {
assert!(matches!(
- unlock(hal, "wrong"),
+ block_on(unlock(hal, "wrong")),
Err(Error::IncorrectPassword)
));
assert_eq!(
@@ -1338,7 +1358,12 @@ mod tests {
wrong_attempt(&mut mock_hal);
assert!(copy_seed(&mut mock_hal).is_ok());
- assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed);
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
assert!(copy_seed(&mut mock_hal).is_ok());
wrong_attempt(&mut mock_hal);
@@ -2015,7 +2040,7 @@ mod tests {
// Wrong password.
assert!(matches!(
- unlock(&mut mock_hal, "bar"),
+ block_on(unlock(&mut mock_hal, "bar")),
Err(Error::IncorrectPassword)
));
assert_eq!(get_remaining_unlock_attempts(), 9);
@@ -2023,7 +2048,7 @@ mod tests {
// Correct password. First time: unlock. After unlock, it becomes a password check.
for _ in 0..3 {
assert_eq!(
- unlock(&mut mock_hal, "foo").unwrap().as_slice(),
+ block_on(unlock(&mut mock_hal, "foo")).unwrap().as_slice(),
&seed[..seed_size]
);
}
diff --git a/src/rust/bitbox02-rust/src/lib.rs b/src/rust/bitbox02-rust/src/lib.rs
index 2a68597..541a367 100644
--- a/src/rust/bitbox02-rust/src/lib.rs
+++ b/src/rust/bitbox02-rust/src/lib.rs
@@ -36,6 +36,7 @@ pub mod hal;
pub mod hash;
pub mod hww;
pub mod keystore;
+pub mod reset;
pub mod salt;
pub mod secp256k1;
#[cfg(feature = "app-u2f")]
diff --git a/src/rust/bitbox02-rust/src/reset.rs b/src/rust/bitbox02-rust/src/reset.rs
new file mode 100644
index 0000000..4cb34c4
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/reset.rs
@@ -0,0 +1,153 @@
+// Copyright 2025 Shift Crypto AG
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::general::abort;
+use crate::hal::{SecureChip, Ui};
+
+/// Resets the device:
+/// - Updates secure chip KDF keys.
+/// - Resets the securechip EEPROM (U2F counter).
+/// - Resets MCU flash app memory.
+/// - Disables SmartEEPROM memory (will be erased/setup on next boot).
+/// - Shows a "Device reset" status message.
+///
+/// `status` selects whether the status message indicates success or failure (user invoked vs forced).
+pub(crate) async fn reset(hal: &mut impl crate::hal::Hal, status: bool) {
+ crate::keystore::lock();
+ // Resetting takes longer than the default 500 ms watchdog. Bump the watchdog timeout to roughly
+ // 7 seconds (longer than needed) so we don't assume communication was lost and this task gets
+ // dropped at an await point.
+ const LONG_TIMEOUT: i16 = -70;
+ bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT);
+
+ // Reset secure chip keys and U2F counter with retries. We retry in case there are transient
+ // errors.
+ let mut reset_ok = false;
+ for _ in 0..5 {
+ if hal.securechip().reset_keys().is_ok() {
+ reset_ok = true;
+ break;
+ }
+ }
+ if !reset_ok {
+ abort("Could not reset secure chip.");
+ }
+
+ #[cfg(feature = "app-u2f")]
+ {
+ let mut u2f_ok = false;
+ for _ in 0..5 {
+ if hal.securechip().u2f_counter_set(0).is_ok() {
+ u2f_ok = true;
+ break;
+ }
+ }
+ if !u2f_ok {
+ abort("Could not initialize U2F counter.");
+ }
+ }
+
+ if bitbox02::memory::reset_hww().is_err() {
+ abort("Could not reset memory.");
+ }
+
+ // Disable SmartEEPROM so it will be erased on next reboot.
+ bitbox02::smarteeprom::disable();
+
+ // Show "Device reset" status using the UI workflow.
+ hal.ui().status("Device reset", status).await;
+
+ // The ble chip needs to be restarted to load the new secrets.
+ if matches!(
+ bitbox02::memory::get_platform(),
+ Ok(bitbox02::memory::Platform::BitBox02Plus)
+ ) {
+ bitbox02::reset_ble();
+ }
+
+ #[cfg(not(feature = "testing"))]
+ bitbox02::reboot();
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::hal::SecureChip;
+ use crate::hal::testing::TestingHal;
+ use crate::keystore;
+ use crate::keystore::testing::mock_unlocked;
+ use crate::workflow::testing::Screen;
+ use bitbox02::testing::mock_memory;
+ use util::bb02_async::block_on;
+
+ #[test]
+ fn test_reset_success() {
+ mock_memory();
+
+ keystore::lock();
+ mock_unlocked();
+ bitbox02::memory::set_device_name("Custom name").unwrap();
+ assert!(!keystore::is_locked());
+ assert!(bitbox02::smarteeprom::is_enabled());
+
+ let mut hal = TestingHal::new();
+ // Make the reset keys call fail once, to test that it is retried.
+ hal.securechip.mock_reset_keys_fails();
+
+ // Simulate a non-zero U2F counter before reset.
+ SecureChip::u2f_counter_set(&mut hal.securechip, 42).unwrap();
+
+ hal.securechip.event_counter_reset();
+ block_on(reset(&mut hal, true));
+ // Secure chip operations happened as expected: reset_keys() was retried once, but only the
+ // successful call increments the event counter.
+ assert_eq!(hal.securechip.get_event_counter(), 1);
+
+ // Keystore is locked again.
+ assert!(keystore::is_locked());
+
+ // Memory has been reset to factory defaults.
+ assert_eq!(bitbox02::memory::get_device_name().as_str(), "My BitBox");
+
+ // SmartEEPROM was disabled as part of the reset.
+ assert!(!bitbox02::smarteeprom::is_enabled());
+
+ assert_eq!(hal.securechip.get_u2f_counter(), 0);
+
+ assert_eq!(
+ hal.ui.screens,
+ vec![Screen::Status {
+ title: "Device reset".into(),
+ success: true,
+ }],
+ );
+ }
+
+ #[test]
+ fn test_reset_status_failure() {
+ mock_memory();
+
+ let mut hal = TestingHal::new();
+ block_on(reset(&mut hal, false));
+
+ assert_eq!(
+ hal.ui.screens,
+ vec![Screen::Status {
+ title: "Device reset".into(),
+ success: false,
+ }],
+ );
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs
index c654793..8b3aeeb 100644
--- a/src/rust/bitbox02-rust/src/workflow/unlock.rs
+++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs
@@ -89,7 +89,7 @@ pub async fn unlock_keystore(
)
.await?;
- match crate::keystore::unlock(hal, &password) {
+ match crate::keystore::unlock(hal, &password).await {
Ok(seed) => Ok(seed),
Err(crate::keystore::Error::IncorrectPassword) => {
let msg = match crate::keystore::get_remaining_unlock_attempts() {
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index 1f08359..865a09a 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -145,8 +145,8 @@ const ALLOWLIST_FNS: &[&str] = &[
"random_32_bytes",
"random_fake_reset",
"reboot_to_bootloader",
+ "reboot",
"reset_ble",
- "reset_reset",
"screen_clear",
"screen_init",
"screen_print_debug",
@@ -171,6 +171,7 @@ const ALLOWLIST_FNS: &[&str] = &[
"securechip_attestation_sign",
"securechip_init_new_password",
"securechip_kdf",
+ "securechip_reset_keys",
"securechip_model",
"securechip_monotonic_increments_remaining",
"securechip_stretch_password",
diff --git a/src/rust/bitbox02/src/lib.rs b/src/rust/bitbox02/src/lib.rs
index 56efecc..bed8a1c 100644
--- a/src/rust/bitbox02/src/lib.rs
+++ b/src/rust/bitbox02/src/lib.rs
@@ -50,7 +50,6 @@ pub mod screen_saver;
pub mod sd;
pub mod secp256k1;
pub mod securechip;
-#[cfg(feature = "simulator-graphical")]
pub mod smarteeprom;
pub mod spi_mem;
pub mod ui;
@@ -127,14 +126,15 @@ pub fn screen_print_debug(msg: &str, duration: i32) {
}
}
-pub fn reset(status: bool) {
- unsafe { bitbox02_sys::reset_reset(status) }
-}
-
pub fn reset_ble() {
unsafe { bitbox02_sys::reset_ble() }
}
+#[cfg(not(feature = "testing"))]
+pub fn reboot() {
+ unsafe { bitbox02_sys::reboot() }
+}
+
pub struct Tm {
tm: bitbox02_sys::tm,
}
diff --git a/src/rust/bitbox02/src/securechip.rs b/src/rust/bitbox02/src/securechip.rs
index fca0e43..aab7eb4 100644
--- a/src/rust/bitbox02/src/securechip.rs
+++ b/src/rust/bitbox02/src/securechip.rs
@@ -85,6 +85,13 @@ pub fn monotonic_increments_remaining() -> Result<u32, ()> {
}
}
+pub fn reset_keys() -> Result<(), ()> {
+ match unsafe { bitbox02_sys::securechip_reset_keys() } {
+ true => Ok(()),
+ false => Err(()),
+ }
+}
+
pub fn init_new_password(password: &str) -> Result<(), Error> {
let password = crate::util::str_to_cstr_vec_zeroizing(password)
.map_err(|_| Error::SecureChip(SecureChipError::SC_ERR_INVALID_ARGS))?;
diff --git a/src/rust/bitbox02/src/smarteeprom.rs b/src/rust/bitbox02/src/smarteeprom.rs
index cc959ee..795cd94 100644
--- a/src/rust/bitbox02/src/smarteeprom.rs
+++ b/src/rust/bitbox02/src/smarteeprom.rs
@@ -11,10 +11,22 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+
+#[cfg(feature = "simulator-graphical")]
pub fn bb02_config() {
unsafe { bitbox02_sys::smarteeprom_bb02_config() };
}
+#[cfg(feature = "simulator-graphical")]
pub fn init() {
unsafe { bitbox02_sys::bitbox02_smarteeprom_init() };
}
+
+pub fn disable() {
+ unsafe { bitbox02_sys::smarteeprom_disable() };
+}
+
+#[cfg(feature = "testing")]
+pub fn is_enabled() -> bool {
+ unsafe { bitbox02_sys::smarteeprom_is_enabled() }
+}
diff --git a/test/hardware-fakes/src/fake_securechip.c b/test/hardware-fakes/src/fake_securechip.c
index 1ddad25..f388ceb 100644
--- a/test/hardware-fakes/src/fake_securechip.c
+++ b/test/hardware-fakes/src/fake_securechip.c
@@ -25,7 +25,7 @@ static const uint8_t _kdfkey[32] =
"\xd2\xe1\xe6\xb1\x8b\x6c\x6b\x08\x43\x3e\xdb\xc1\xd1\x68\xc1\xa0\x04\x37\x74\xa4\x22\x18\x77"
"\xe7\x9e\xd5\x66\x84\xbe\x5a\xc0\x1b";
-// Count how man seceurity events happen. The numbers were obtained by reading the security event
+// 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.
static uint32_t _event_counter = 0;
@@ -50,6 +50,13 @@ int securechip_stretch_password(const char* password, uint8_t* stretched_out)
rust_hmac_sha256(key, sizeof(key), (const uint8_t*)password, strlen(password), stretched_out);
return 0;
}
+
+bool securechip_reset_keys(void)
+{
+ _event_counter += 1;
+ return true;
+}
+
bool securechip_u2f_counter_set(uint32_t counter)
{
_event_counter += 0;
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.