keystore: add get_remaining_unlock_attempts()
What changed, and why it matters
This commit adds a small helper function that reports how many more wrong password attempts are allowed before the device wipes itself. It also removes a testing-only restriction so the underlying counter can be read in normal builds. There is no security fix or vulnerability here—just a code cleanup to simplify a later change.
No security action required; review as normal refactoring.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces keystore::get_remaining_unlock_attempts(), which computes MAX_UNLOCK_ATTEMPTS - smarteeprom_get_unlock_attempts() using saturating subtraction. It removes the #[cfg(feature = "testing")] guard from memory::smarteeprom_get_unlock_attempts() so the new helper can call it in production code. The diff is entirely additive and consists of the new function plus unit-test assertions that its return value matches the remaining_attempts already reported by unlock().
Changed components
src/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02/src/memory.rsInspect captured patch +26 / −1
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index b5798a7..edab318 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -235,6 +235,13 @@ pub fn unlock(password: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
keystore::_unlock(password)
}
+/// Returns the number of remaining unlock attempts (calls to `unlock()`) that are allowed before
+/// the device resets itself.
+pub fn get_remaining_unlock_attempts() -> u8 {
+ let failed_attempts: u8 = bitbox02::memory::smarteeprom_get_unlock_attempts();
+ bitbox02::memory::MAX_UNLOCK_ATTEMPTS.saturating_sub(failed_attempts)
+}
+
/// Unlocks the bip39 seed. The input seed must be the keystore seed (i.e. must match the output
/// of `keystore_copy_seed()`).
/// `mnemonic_passphrase` is the bip39 passphrase used in the derivation. Use the empty string if no
@@ -873,6 +880,10 @@ mod tests {
Err(Error::IncorrectPassword { remaining_attempts }) if remaining_attempts
== bitbox02::memory::MAX_UNLOCK_ATTEMPTS - i
));
+ assert_eq!(
+ get_remaining_unlock_attempts(),
+ bitbox02::memory::MAX_UNLOCK_ATTEMPTS - i
+ );
// Still seeded.
assert!(bitbox02::memory::is_seeded());
// Wrong password does not lock the keystore again if already unlocked.
@@ -910,6 +921,11 @@ mod tests {
Err(Error::IncorrectPassword { remaining_attempts })
if remaining_attempts == bitbox02::memory::MAX_UNLOCK_ATTEMPTS - attempt
));
+
+ assert_eq!(
+ get_remaining_unlock_attempts(),
+ bitbox02::memory::MAX_UNLOCK_ATTEMPTS - attempt
+ );
assert!(is_locked());
assert!(copy_seed().is_err());
assert!(bitbox02::memory::is_seeded());
@@ -943,6 +959,7 @@ mod tests {
assert!(is_locked());
bitbox02::memory::set_unlock_attempts_for_testing(bitbox02::memory::MAX_UNLOCK_ATTEMPTS);
+ assert_eq!(get_remaining_unlock_attempts(), 0);
assert_eq!(
bitbox02::memory::smarteeprom_get_unlock_attempts(),
bitbox02::memory::MAX_UNLOCK_ATTEMPTS
@@ -978,6 +995,10 @@ mod tests {
Err(Error::IncorrectPassword { remaining_attempts })
if remaining_attempts == bitbox02::memory::MAX_UNLOCK_ATTEMPTS - 1
));
+ assert_eq!(
+ get_remaining_unlock_attempts(),
+ bitbox02::memory::MAX_UNLOCK_ATTEMPTS - 1
+ );
};
wrong_attempt();
@@ -1018,6 +1039,10 @@ mod tests {
Err(Error::IncorrectPassword { remaining_attempts })
if remaining_attempts == bitbox02::memory::MAX_UNLOCK_ATTEMPTS - 1
));
+ assert_eq!(
+ get_remaining_unlock_attempts(),
+ bitbox02::memory::MAX_UNLOCK_ATTEMPTS - 1
+ );
};
wrong_attempt();
@@ -1668,6 +1693,7 @@ mod tests {
remaining_attempts: 9
})
));
+ assert_eq!(get_remaining_unlock_attempts(), 9);
// Correct password. First time: unlock. After unlock, it becomes a password check.
for _ in 0..3 {
diff --git a/src/rust/bitbox02/src/memory.rs b/src/rust/bitbox02/src/memory.rs
index e173b47..38c7cb4 100644
--- a/src/rust/bitbox02/src/memory.rs
+++ b/src/rust/bitbox02/src/memory.rs
@@ -170,7 +170,6 @@ pub fn reset_hww() -> Result<(), ()> {
}
}
-#[cfg(feature = "testing")]
pub fn smarteeprom_get_unlock_attempts() -> u8 {
unsafe { bitbox02_sys::bitbox02_smarteeprom_get_unlock_attempts() }
}
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.