rust/keystore: move bip39_unlock and ROOT_FINGERPRINT
What changed, and why it matters
This commit is a code reorganization: it moves the BIP39 seed derivation and root fingerprint storage from one Rust module to another. The same logic is preserved, including a safety check that derives the seed twice to detect memory corruption. There is no visible security fix or vulnerability being introduced.
No action required; treat as routine refactoring. Standard regression testing of unlock/lock/root-fingerprint flows is sufficient.
Security signals we found
Refactor only: logic moved between modules with no functional change
Retained double-derivation memory-corruption check
Root fingerprint still cached in a SyncUnsafeCell and cleared on lock
Evidence from the diff
The change relocates derive_bip39_seed from bitbox02::keystore to bitbox02-rust::bip39 (renamed derive_seed) and moves ROOT_FINGERPRINT storage plus unlock_bip39 orchestration from the lower-level bitbox02 crate to the higher-level bitbox02-rust::keystore. The C FFI functions keystore_unlock_bip39_check and keystore_unlock_bip39_finalize are now called directly from bitbox02-rust::keystore. The double-derivation integrity check and root-fingerprint caching behavior remain identical. No new unsafe patterns are introduced beyond the pre-existing SyncUnsafeCell usage.
Changed components
src/rust/bitbox02-rust/src/bip39.rssrc/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02/src/keystore.rsInspect captured patch +116 / −144
diff --git a/src/rust/bitbox02-rust/src/bip39.rs b/src/rust/bitbox02-rust/src/bip39.rs
index ab952e8..e3bcb0c 100644
--- a/src/rust/bitbox02-rust/src/bip39.rs
+++ b/src/rust/bitbox02-rust/src/bip39.rs
@@ -40,6 +40,29 @@ pub fn mnemonic_to_seed(mnemonic: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, (
Ok(zeroize::Zeroizing::new(seed[..seed_len].to_vec()))
}
+/// Derives the bip39 seed and returns it and the bip32 root fingerprint.
+/// `mnemonic_passphrase` is the bip39 passphrase used in the derivation.
+/// `yield_now` is called in each of the 2048 bip39 pbkdf2 iterations.
+pub async fn derive_seed(
+ seed: &[u8],
+ mnemonic_passphrase: &str,
+ yield_now: impl AsyncFn(),
+) -> (zeroize::Zeroizing<[u8; 64]>, [u8; 4]) {
+ let mnemonic = bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed).unwrap();
+ let bip39_seed: zeroize::Zeroizing<[u8; 64]> = zeroize::Zeroizing::new(
+ mnemonic
+ .to_seed_normalized_async(mnemonic_passphrase, yield_now)
+ .await,
+ );
+ let root_fingerprint: [u8; 4] =
+ bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
+ .unwrap()
+ .fingerprint(crate::secp256k1::SECP256K1)
+ .to_bytes();
+
+ (bip39_seed, root_fingerprint)
+}
+
// C API
#[unsafe(no_mangle)]
@@ -61,6 +84,7 @@ pub extern "C" fn rust_get_bip39_word(idx: u16, mut out: util::bytes::BytesMut)
#[cfg(test)]
mod tests {
use super::*;
+ use util::bb02_async::block_on;
#[test]
fn test_rust_get_bip39_word() {
@@ -168,4 +192,67 @@ mod tests {
b"\xae\x45\xd4\x02\x3a\xfa\x4a\x48\x68\x77\x51\x69\xfe\xa5\xf5\xe4\x97\xf7\xa1\xa4\xd6\x22\x9a\xd0\x23\x9e\x68\x9b\x48\x2e\xd3\x5e",
);
}
+
+ #[test]
+ fn test_derive_bip39_seed() {
+ struct Test {
+ seed: &'static str,
+ passphrase: &'static str,
+ expected_bip39_seed: &'static str,
+ expected_root_fingerprint: &'static str,
+ }
+
+ let tests = &[
+ // 16 byte seed
+ Test {
+ seed: "fb5cf00d5ea61059fa066e25a6be9544",
+ passphrase: "",
+ expected_bip39_seed: "f4577e463be595868060e5a763328153155b4167cd284998c8c6096d044742372020f5b052d0c41c1c5e6a6a7da2cb8a367aaaa074fab7773e8d5b2f684257ed",
+ expected_root_fingerprint: "0b2fa4e5",
+ },
+ Test {
+ seed: "fb5cf00d5ea61059fa066e25a6be9544",
+ passphrase: "password",
+ expected_bip39_seed: "5922fb7630bc7cb871af102f733b6bdb8f05945147cd4646a89056fde0bdad5c3a4ff5be3f9e7af535f570e7053b5b22472555b331bc89cb797c306f7eb6a5a1",
+ expected_root_fingerprint: "c4062d44",
+ },
+ // 24 byte seed
+ Test {
+ seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
+ passphrase: "",
+ expected_bip39_seed: "4a2a016a6d90eb3a79b7931ca0a172df5c5bfee3e5b47f0fd84bc0791ea3bbc9476c3d5de71cdb12c37e93c2aa3d5c303257f1992aed400fc5bbfc7da787bfa7",
+ expected_root_fingerprint: "62fd19e0",
+ },
+ Test {
+ seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
+ passphrase: "password",
+ expected_bip39_seed: "bc317ee0f88870254be32274d63ec2b0e962bf09f3ca04287912bfc843f2fab7c556f8657cadc924f99a217b0daa91898303a8414102031a125c50023e45a80b",
+ expected_root_fingerprint: "c745266d",
+ },
+ // 32 byte seed
+ Test {
+ seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
+ passphrase: "",
+ expected_bip39_seed: "63f844e2c61ecfb20f9100de381a7a9ec875b085f5ac7735a2ba4d615a0f4147b87be402f65651969130683deeef752760c09e291604fe4b89d61ffee2630be8",
+ expected_root_fingerprint: "93ba3a7b",
+ },
+ Test {
+ seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
+ passphrase: "password",
+ expected_bip39_seed: "42e90dacd61f3373542d212f0fb9c291dcea84a6d85034272372dde7188638a98527280d65e41599f30d3434d8ee3d4747dbb84801ff1a851d2306c7d1648374",
+ expected_root_fingerprint: "b95c9318",
+ },
+ ];
+
+ for test in tests {
+ let seed = hex::decode(test.seed).unwrap();
+ let (bip39_seed, root_fingerprint) =
+ block_on(derive_seed(&seed, test.passphrase, async || {}));
+ assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
+ assert_eq!(
+ hex::encode(root_fingerprint).as_str(),
+ test.expected_root_fingerprint
+ );
+ }
+ }
}
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index 078d3bd..5259a12 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -24,6 +24,7 @@ pub use bitbox02::keystore::SignResult;
use bitbox02::{keystore, securechip};
use util::bip32::HARDENED;
+use util::cell::SyncUnsafeCell;
use crate::secp256k1::SECP256K1;
@@ -32,9 +33,12 @@ use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine, sha256, sha512};
/// Length of a compressed secp256k1 pubkey.
const EC_PUBLIC_KEY_LEN: usize = 33;
+static ROOT_FINGERPRINT: SyncUnsafeCell<Option<[u8; 4]>> = SyncUnsafeCell::new(None);
+
/// Locks the keystore (resets to state before `unlock()`).
pub fn lock() {
keystore::_lock();
+ unsafe { ROOT_FINGERPRINT.write(None) }
}
/// Returns false if the keystore is unlocked (unlock() followed by unlock_bip39()), true otherwise.
@@ -55,7 +59,25 @@ pub async fn unlock_bip39(
mnemonic_passphrase: &str,
yield_now: impl AsyncFn(),
) -> Result<(), Error> {
- keystore::_unlock_bip39(SECP256K1, seed, mnemonic_passphrase, yield_now).await
+ keystore::unlock_bip39_check(seed)?;
+
+ let (bip39_seed, root_fingerprint) =
+ crate::bip39::derive_seed(seed, mnemonic_passphrase, &yield_now).await;
+
+ let (bip39_seed_2, root_fingerprint_2) =
+ crate::bip39::derive_seed(seed, mnemonic_passphrase, &yield_now).await;
+
+ if bip39_seed != bip39_seed_2 || root_fingerprint != root_fingerprint_2 {
+ return Err(Error::Memory);
+ }
+
+ keystore::unlock_bip39_finalize(bip39_seed.as_slice().try_into().unwrap())?;
+
+ // Store root fingerprint.
+ unsafe {
+ ROOT_FINGERPRINT.write(Some(root_fingerprint));
+ }
+ Ok(())
}
/// Returns a copy of the retained seed. Errors if the keystore is locked.
@@ -183,7 +205,10 @@ pub fn get_xpubs_twice(keypaths: &[&[u32]]) -> Result<Vec<bip32::Xpub>, ()> {
/// according to:
/// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
- keystore::root_fingerprint()
+ if is_locked() {
+ return Err(());
+ }
+ unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
}
/// Stretches the given encryption_key using the securechip. The resulting key is used to encrypt
diff --git a/src/rust/bitbox02/src/keystore.rs b/src/rust/bitbox02/src/keystore.rs
index 5064586..5413b6a 100644
--- a/src/rust/bitbox02/src/keystore.rs
+++ b/src/rust/bitbox02/src/keystore.rs
@@ -17,8 +17,6 @@ extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
-use util::cell::SyncUnsafeCell;
-
use bitcoin::secp256k1::{All, Secp256k1};
use core::convert::TryInto;
@@ -29,8 +27,6 @@ use bitbox02_sys::keystore_error_t;
const EC_PUBLIC_KEY_LEN: usize = 33;
pub const MAX_SEED_LENGTH: usize = bitbox02_sys::KEYSTORE_MAX_SEED_LENGTH as usize;
-static ROOT_FINGERPRINT: SyncUnsafeCell<Option<[u8; 4]>> = SyncUnsafeCell::new(None);
-
pub fn _is_locked() -> bool {
unsafe { bitbox02_sys::keystore_is_locked() }
}
@@ -97,11 +93,9 @@ pub fn _unlock(password: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
pub fn _lock() {
unsafe { bitbox02_sys::keystore_lock() }
-
- unsafe { ROOT_FINGERPRINT.write(None) }
}
-fn unlock_bip39_check(seed: &[u8]) -> Result<(), Error> {
+pub fn unlock_bip39_check(seed: &[u8]) -> Result<(), Error> {
if unsafe { bitbox02_sys::keystore_unlock_bip39_check(seed.as_ptr(), seed.len()) } {
Ok(())
} else {
@@ -109,7 +103,7 @@ fn unlock_bip39_check(seed: &[u8]) -> Result<(), Error> {
}
}
-fn unlock_bip39_finalize(bip39_seed: &[u8; 64]) -> Result<(), Error> {
+pub fn unlock_bip39_finalize(bip39_seed: &[u8; 64]) -> Result<(), Error> {
if unsafe { bitbox02_sys::keystore_unlock_bip39_finalize(bip39_seed.as_ptr()) } {
Ok(())
} else {
@@ -117,27 +111,6 @@ fn unlock_bip39_finalize(bip39_seed: &[u8; 64]) -> Result<(), Error> {
}
}
-async fn derive_bip39_seed(
- secp: &Secp256k1<All>,
- seed: &[u8],
- mnemonic_passphrase: &str,
- yield_now: impl AsyncFn(),
-) -> (zeroize::Zeroizing<[u8; 64]>, [u8; 4]) {
- let mnemonic = bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed).unwrap();
- let bip39_seed: zeroize::Zeroizing<[u8; 64]> = zeroize::Zeroizing::new(
- mnemonic
- .to_seed_normalized_async(mnemonic_passphrase, yield_now)
- .await,
- );
- let root_fingerprint: [u8; 4] =
- bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
- .unwrap()
- .fingerprint(secp)
- .to_bytes();
-
- (bip39_seed, root_fingerprint)
-}
-
#[cfg(feature = "testing")]
pub fn test_get_retained_seed_encrypted() -> &'static [u8] {
unsafe {
@@ -156,44 +129,6 @@ pub fn test_get_retained_bip39_seed_encrypted() -> &'static [u8] {
}
}
-/// 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
-/// passphrase is needed or provided.
-pub async fn _unlock_bip39(
- secp: &Secp256k1<All>,
- seed: &[u8],
- mnemonic_passphrase: &str,
- yield_now: impl AsyncFn(),
-) -> Result<(), Error> {
- unlock_bip39_check(seed)?;
-
- let (bip39_seed, root_fingerprint) =
- derive_bip39_seed(secp, seed, mnemonic_passphrase, &yield_now).await;
-
- let (bip39_seed_2, root_fingerprint_2) =
- derive_bip39_seed(secp, seed, mnemonic_passphrase, &yield_now).await;
-
- if bip39_seed != bip39_seed_2 || root_fingerprint != root_fingerprint_2 {
- return Err(Error::Memory);
- }
-
- unlock_bip39_finalize(bip39_seed.as_slice().try_into().unwrap())?;
-
- // Store root fingerprint.
- unsafe {
- ROOT_FINGERPRINT.write(Some(root_fingerprint));
- }
- Ok(())
-}
-
-pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
- if _is_locked() {
- return Err(());
- }
- unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
-}
-
pub fn _create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
match unsafe {
bitbox02_sys::keystore_create_and_store_seed(
@@ -304,78 +239,3 @@ pub fn mock_unlocked(seed: &[u8]) {
bitbox02_sys::keystore_mock_unlocked(seed.as_ptr(), seed.len() as _, core::ptr::null())
}
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use bitcoin::secp256k1;
- use util::bb02_async::block_on;
-
- #[test]
- fn test_derive_bip39_seed() {
- struct Test {
- seed: &'static str,
- passphrase: &'static str,
- expected_bip39_seed: &'static str,
- expected_root_fingerprint: &'static str,
- }
-
- let tests = &[
- // 16 byte seed
- Test {
- seed: "fb5cf00d5ea61059fa066e25a6be9544",
- passphrase: "",
- expected_bip39_seed: "f4577e463be595868060e5a763328153155b4167cd284998c8c6096d044742372020f5b052d0c41c1c5e6a6a7da2cb8a367aaaa074fab7773e8d5b2f684257ed",
- expected_root_fingerprint: "0b2fa4e5",
- },
- Test {
- seed: "fb5cf00d5ea61059fa066e25a6be9544",
- passphrase: "password",
- expected_bip39_seed: "5922fb7630bc7cb871af102f733b6bdb8f05945147cd4646a89056fde0bdad5c3a4ff5be3f9e7af535f570e7053b5b22472555b331bc89cb797c306f7eb6a5a1",
- expected_root_fingerprint: "c4062d44",
- },
- // 24 byte seed
- Test {
- seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
- passphrase: "",
- expected_bip39_seed: "4a2a016a6d90eb3a79b7931ca0a172df5c5bfee3e5b47f0fd84bc0791ea3bbc9476c3d5de71cdb12c37e93c2aa3d5c303257f1992aed400fc5bbfc7da787bfa7",
- expected_root_fingerprint: "62fd19e0",
- },
- Test {
- seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
- passphrase: "password",
- expected_bip39_seed: "bc317ee0f88870254be32274d63ec2b0e962bf09f3ca04287912bfc843f2fab7c556f8657cadc924f99a217b0daa91898303a8414102031a125c50023e45a80b",
- expected_root_fingerprint: "c745266d",
- },
- // 32 byte seed
- Test {
- seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
- passphrase: "",
- expected_bip39_seed: "63f844e2c61ecfb20f9100de381a7a9ec875b085f5ac7735a2ba4d615a0f4147b87be402f65651969130683deeef752760c09e291604fe4b89d61ffee2630be8",
- expected_root_fingerprint: "93ba3a7b",
- },
- Test {
- seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
- passphrase: "password",
- expected_bip39_seed: "42e90dacd61f3373542d212f0fb9c291dcea84a6d85034272372dde7188638a98527280d65e41599f30d3434d8ee3d4747dbb84801ff1a851d2306c7d1648374",
- expected_root_fingerprint: "b95c9318",
- },
- ];
-
- let secp = secp256k1::Secp256k1::new();
- for test in tests {
- let seed = hex::decode(test.seed).unwrap();
- let (bip39_seed, root_fingerprint) = block_on(derive_bip39_seed(
- &secp,
- &seed,
- test.passphrase,
- async || {},
- ));
- assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
- assert_eq!(
- hex::encode(root_fingerprint).as_str(),
- test.expected_root_fingerprint
- );
- }
- }
-}
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.