What changed, and why it matters
This commit only adds a new automated test to the BitBox02 firmware. It checks that entering the wrong password repeatedly while the device is already locked correctly triggers the lockout mechanism and eventually wipes the seed. There is no change to production code, no new feature, and no fix to an existing bug.
No action required. Review the test for correctness during normal code review; it is a defensive test that increases coverage for an existing anti-brute-force lockout behavior.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff adds a Rust unit test test_unlock_lockout_while_locked in src/rust/bitbox02-rust/src/keystore.rs. The test seeds a mock device, locks it, then attempts to unlock with an invalid password until MAX_UNLOCK_ATTEMPTS is reached, verifying that remaining attempts decrement correctly, the keystore stays locked, and after the final failed attempt the seed is wiped (is_seeded() returns false) and subsequent unlocks return Error::Unseeded. No implementation code is modified.
Changed components
src/rust/bitbox02-rust/src/keystore.rsInspect captured patch +36 / −0
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index f200bff..bb08c28 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -889,6 +889,42 @@ mod tests {
assert!(matches!(unlock("password"), Err(Error::Unseeded)));
}
+ #[test]
+ fn test_unlock_lockout_while_locked() {
+ mock_memory();
+ lock();
+
+ let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
+ let mock_salt_root =
+ hex!("3333333333333333444444444444444411111111111111112222222222222222");
+ bitbox02::memory::set_salt_root(&mock_salt_root).unwrap();
+
+ assert!(encrypt_and_store_seed(&mut TestingHal::new(), &seed, "password").is_ok());
+ lock();
+ assert!(is_locked());
+ assert!(copy_seed().is_err());
+
+ for attempt in 1..bitbox02::memory::MAX_UNLOCK_ATTEMPTS {
+ assert!(matches!(
+ unlock("invalid password"),
+ Err(Error::IncorrectPassword { remaining_attempts })
+ if remaining_attempts == bitbox02::memory::MAX_UNLOCK_ATTEMPTS - attempt
+ ));
+ assert!(is_locked());
+ assert!(copy_seed().is_err());
+ assert!(bitbox02::memory::is_seeded());
+ }
+
+ assert!(matches!(
+ unlock("invalid password"),
+ Err(Error::MaxAttemptsExceeded)
+ ));
+ assert!(is_locked());
+ assert!(copy_seed().is_err());
+ assert!(!bitbox02::memory::is_seeded());
+ assert!(matches!(unlock("password"), Err(Error::Unseeded)));
+ }
+
#[test]
fn test_unlock_bip39() {
mock_memory();
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.