keystore: upgrade password algo upon unlock
What changed, and why it matters
This commit changes how the BitBox02 hardware wallet handles the password-stretching algorithm used to protect the wallet seed. When a user unlocks the device, if the seed was encrypted with an older, weaker stretching method, it is now automatically re-encrypted with the newer, stronger method. The change is a defensive upgrade, not a fix for an active attack, and it only affects the unlock flow after the correct password is entered.
No immediate action required. Treat as a routine hardening change. If reviewing the broader firmware, verify that V1 stretching is indeed stronger than V0 and that the migration cannot be triggered by an incorrect password or leave the seed unretained on failure.
Security signals we found
Automatic migration from legacy password-stretching algorithm to current default on unlock
No change to password verification or seed decryption logic
Seed remains retained after migration; unlock flow behavior otherwise unchanged
Unit-test HAL updated to simulate both V0 and V1 stretching algorithms
Evidence from the diff
The patch adds migrate_password_algo_and_retain_seed() in keystore.rs. During unlock(), after the seed is successfully decrypted with the user’s password, the code checks whether the stored seed uses the current default PasswordStretchAlgo. If it still uses the legacy V0 algorithm, it re-encrypts and stores the seed with V1; otherwise it simply retains the seed as before. The test-only HAL in hal.rs is updated so unit tests can distinguish V0 from V1 stretching by using different HMAC keys. The change is purely a migration helper and does not alter how passwords are verified or how the seed is decrypted.
Changed components
src/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02-rust/src/hal.rsInspect captured patch +123 / −5
diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs
index 18f9f8b..adc9b9b 100644
--- a/src/rust/bitbox02-rust/src/hal.rs
+++ b/src/rust/bitbox02-rust/src/hal.rs
@@ -510,12 +510,20 @@ pub mod testing {
fn init_new_password(
&mut self,
password: &str,
- _password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
+ 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(b"unit-test");
+ 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(
@@ -533,8 +541,17 @@ pub mod testing {
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(b"unit-test");
+ 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(
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index 2637076..85e694a 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -207,7 +207,7 @@ fn retain_bip39_seed(hal: &mut impl crate::hal::Hal, bip39_seed: &[u8]) -> Resul
}
/// Returns the stretching algo that will be used when setting new passwords.
-fn default_password_stretch_algo(
+pub fn default_password_stretch_algo(
hal: &mut impl crate::hal::Hal,
) -> Result<bitbox02::memory::PasswordStretchAlgo, Error> {
match hal
@@ -305,6 +305,26 @@ pub fn re_encrypt_seed(
Ok(())
}
+/// Re-encrypts the seed with the newest (default) password stretching algorithm if it is not
+/// already using it. The seed is retained after this function finishes, regardless of whether a
+/// migration was performed.
+fn migrate_password_algo_and_retain_seed(
+ hal: &mut impl crate::hal::Hal,
+ seed: &[u8],
+ password: &str,
+) -> Result<(), Error> {
+ let default_algo = default_password_stretch_algo(hal)?;
+ let (_, stored_algo) = hal
+ .memory()
+ .get_encrypted_seed_and_hmac()
+ .map_err(|_| Error::Memory)?;
+ if stored_algo != default_algo {
+ encrypt_and_store_seed_internal(hal, seed, password)
+ } else {
+ retain_seed(hal, seed)
+ }
+}
+
// Checks if the retained seed matches the passed seed.
fn check_retained_seed(hal: &mut impl crate::hal::Hal, seed: &[u8]) -> Result<(), ()> {
if RETAINED_SEED.read().is_none() {
@@ -377,7 +397,7 @@ pub async fn unlock(
panic!("Seed has suddenly changed. This should never happen.");
}
} else {
- retain_seed(hal, &seed)?;
+ migrate_password_algo_and_retain_seed(hal, &seed, password)?;
}
hal.memory().reset_unlock_attempts();
Ok(seed)
@@ -1338,6 +1358,87 @@ mod tests {
assert!(mock_hal.memory.is_seeded());
}
+ #[test]
+ fn test_unlock_migrate_password_algo() {
+ mock_memory();
+ lock();
+
+ let mut mock_hal = TestingHal::new();
+
+ let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
+
+ let mock_salt_root =
+ hex!("3333333333333333444444444444444411111111111111112222222222222222");
+ bitbox02::memory::set_salt_root(&mock_salt_root).unwrap();
+
+ let password = "password";
+
+ assert!(matches!(
+ mock_hal.memory.get_securechip_type().unwrap(),
+ bitbox02::memory::SecurechipType::Optiga
+ ));
+
+ // Setup a seed encrypted with algo V0.
+ {
+ let encrypted = {
+ let secret = mock_hal
+ .securechip
+ .stretch_password(
+ password,
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0,
+ )
+ .unwrap();
+ let iv: &[u8; 16] = &[0xaau8; 16];
+
+ bitbox_aes::encrypt_with_hmac(iv, &secret, &seed)
+ };
+
+ mock_hal
+ .memory
+ .set_encrypted_seed_and_hmac(
+ &encrypted,
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0,
+ )
+ .unwrap();
+ }
+
+ // Unlock will migrate from V0 to V1.
+ mock_hal.securechip.event_counter_reset();
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, password))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
+ assert_eq!(mock_hal.securechip.get_event_counter(), 9);
+
+ // Check the seed was retained again after migration.
+ assert_eq!(copy_seed(&mut mock_hal).unwrap().as_slice(), seed);
+ // Check the seed now uses the new algo.
+ let (_, stored_algo) = mock_hal.memory.get_encrypted_seed_and_hmac().unwrap();
+ assert_eq!(
+ stored_algo,
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1
+ );
+
+ // Password check still works
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
+
+ // Unlocking from scratch still works
+ lock();
+ assert_eq!(
+ block_on(unlock(&mut mock_hal, "password"))
+ .unwrap()
+ .as_slice(),
+ seed
+ );
+ }
+
#[test]
fn test_unlock_bip39() {
mock_memory();
Why this scored 28/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.