Skip to content

Commit 0ff1f81

Browse files
committed
Merge branch 'cache_fingerprint'
2 parents 2e70026 + e3b21df commit 0ff1f81

File tree

8 files changed

+130
-15
lines changed

8 files changed

+130
-15
lines changed

src/keystore.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ keystore_error_t keystore_unlock(
448448
return result;
449449
}
450450

451-
bool keystore_unlock_bip39(const char* mnemonic_passphrase)
451+
bool keystore_unlock_bip39(const char* mnemonic_passphrase, uint8_t* root_fingerprint_out)
452452
{
453453
if (!_is_unlocked_device) {
454454
return false;
@@ -467,7 +467,8 @@ bool keystore_unlock_bip39(const char* mnemonic_passphrase)
467467
rust_derive_bip39_seed(
468468
rust_util_bytes(seed, seed_length),
469469
mnemonic_passphrase,
470-
rust_util_bytes_mut(bip39_seed, sizeof(bip39_seed)));
470+
rust_util_bytes_mut(bip39_seed, sizeof(bip39_seed)),
471+
rust_util_bytes_mut(root_fingerprint_out, 4));
471472

472473
if (!_retain_bip39_seed(bip39_seed)) {
473474
return false;

src/keystore.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,13 @@ keystore_unlock(const char* password, uint8_t* remaining_attempts_out, int* secu
106106
/** Unlocks the bip39 seed.
107107
* @param[in] mnemonic_passphrase bip39 passphrase used in the derivation. Use the
108108
* empty string if no passphrase is needed or provided.
109+
* @param[out] root_fingerprint_out must be 4 bytes long and will contain the root fingerprint of
110+
* the wallet.
109111
* @return returns false if there was a critital memory error, otherwise true.
110112
*/
111-
USE_RESULT bool keystore_unlock_bip39(const char* mnemonic_passphrase);
113+
USE_RESULT bool keystore_unlock_bip39(
114+
const char* mnemonic_passphrase,
115+
uint8_t* root_fingerprint_out);
112116

113117
/**
114118
* Locks the keystore (resets to state before `keystore_unlock()`).

src/rust/bitbox02-rust/src/bip39.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,30 @@ pub fn get_word(idx: u16) -> Result<zeroize::Zeroizing<String>, ()> {
3232
/// The passphrase must be not NULL and null-terminated.
3333
///
3434
/// `seed` must be 16, 24 or 32 bytes long.
35-
/// `out` must be exactly 64 bytes long.
35+
/// `bip39_seed_out` must be exactly 64 bytes long.
36+
/// `root_fingerprint_out` must be exactly 4 bytes long.
3637
#[unsafe(no_mangle)]
3738
pub unsafe extern "C" fn rust_derive_bip39_seed(
3839
seed: util::bytes::Bytes,
3940
passphrase: *const core::ffi::c_char,
40-
mut out: util::bytes::BytesMut,
41+
mut bip39_seed_out: util::bytes::BytesMut,
42+
mut root_fingerprint_out: util::bytes::BytesMut,
4143
) {
4244
let mnemonic =
4345
bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed.as_ref()).unwrap();
4446
let passphrase = unsafe { core::ffi::CStr::from_ptr(passphrase) };
45-
let bip39_seed =
47+
let bip39_seed: zeroize::Zeroizing<[u8; 64]> =
4648
zeroize::Zeroizing::new(mnemonic.to_seed_normalized(passphrase.to_str().unwrap()));
47-
out.as_mut().clone_from_slice(&bip39_seed[..]);
49+
bip39_seed_out.as_mut().clone_from_slice(&bip39_seed[..]);
50+
51+
let root_fingerprint: [u8; 4] =
52+
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
53+
.unwrap()
54+
.fingerprint(crate::secp256k1::SECP256K1)
55+
.to_bytes();
56+
root_fingerprint_out
57+
.as_mut()
58+
.clone_from_slice(&root_fingerprint);
4859
}
4960

5061
#[unsafe(no_mangle)]
@@ -73,6 +84,7 @@ mod tests {
7384
seed: &'static str,
7485
passphrase: &'static core::ffi::CStr,
7586
expected_bip39_seed: &'static str,
87+
expected_root_fingerprint: &'static str,
7688
}
7789

7890
let tests = &[
@@ -81,47 +93,62 @@ mod tests {
8193
seed: "fb5cf00d5ea61059fa066e25a6be9544",
8294
passphrase: c"",
8395
expected_bip39_seed: "f4577e463be595868060e5a763328153155b4167cd284998c8c6096d044742372020f5b052d0c41c1c5e6a6a7da2cb8a367aaaa074fab7773e8d5b2f684257ed",
96+
expected_root_fingerprint: "0b2fa4e5",
8497
},
8598
Test {
8699
seed: "fb5cf00d5ea61059fa066e25a6be9544",
87100
passphrase: c"password",
88101
expected_bip39_seed: "5922fb7630bc7cb871af102f733b6bdb8f05945147cd4646a89056fde0bdad5c3a4ff5be3f9e7af535f570e7053b5b22472555b331bc89cb797c306f7eb6a5a1",
102+
expected_root_fingerprint: "c4062d44",
89103
},
90104
// 24 byte seed
91105
Test {
92106
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
93107
passphrase: c"",
94108
expected_bip39_seed: "4a2a016a6d90eb3a79b7931ca0a172df5c5bfee3e5b47f0fd84bc0791ea3bbc9476c3d5de71cdb12c37e93c2aa3d5c303257f1992aed400fc5bbfc7da787bfa7",
109+
expected_root_fingerprint: "62fd19e0",
95110
},
96111
Test {
97112
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
98113
passphrase: c"password",
99114
expected_bip39_seed: "bc317ee0f88870254be32274d63ec2b0e962bf09f3ca04287912bfc843f2fab7c556f8657cadc924f99a217b0daa91898303a8414102031a125c50023e45a80b",
115+
expected_root_fingerprint: "c745266d",
100116
},
101117
// 32 byte seed
102118
Test {
103119
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
104120
passphrase: c"",
105121
expected_bip39_seed: "63f844e2c61ecfb20f9100de381a7a9ec875b085f5ac7735a2ba4d615a0f4147b87be402f65651969130683deeef752760c09e291604fe4b89d61ffee2630be8",
122+
expected_root_fingerprint: "93ba3a7b",
106123
},
107124
Test {
108125
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
109126
passphrase: c"password",
110127
expected_bip39_seed: "42e90dacd61f3373542d212f0fb9c291dcea84a6d85034272372dde7188638a98527280d65e41599f30d3434d8ee3d4747dbb84801ff1a851d2306c7d1648374",
128+
expected_root_fingerprint: "b95c9318",
111129
},
112130
];
113131

114132
for test in tests {
115133
let seed = hex::decode(test.seed).unwrap();
116134
let mut bip39_seed = [0u8; 64];
135+
let mut root_fingerprint = [0u8; 4];
117136
unsafe {
118137
rust_derive_bip39_seed(
119138
util::bytes::rust_util_bytes(seed.as_ptr(), seed.len()),
120139
test.passphrase.as_ptr(),
121140
util::bytes::rust_util_bytes_mut(bip39_seed.as_mut_ptr(), bip39_seed.len()),
141+
util::bytes::rust_util_bytes_mut(
142+
root_fingerprint.as_mut_ptr(),
143+
root_fingerprint.len(),
144+
),
122145
);
123146
}
124147
assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
148+
assert_eq!(
149+
hex::encode(root_fingerprint).as_str(),
150+
test.expected_root_fingerprint
151+
);
125152
}
126153
}
127154

src/rust/bitbox02-rust/src/keystore.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,7 @@ pub fn get_xpubs_twice(keypaths: &[&[u32]]) -> Result<Vec<bip32::Xpub>, ()> {
128128
/// according to:
129129
/// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
130130
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
131-
Ok(get_xpub_twice(&[])?
132-
.pubkey_hash160()
133-
.get(..4)
134-
.ok_or(())?
135-
.to_vec())
131+
keystore::root_fingerprint()
136132
}
137133

138134
fn bip85_entropy(keypath: &[u32]) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
@@ -443,13 +439,17 @@ mod tests {
443439

444440
bitbox02::securechip::fake_event_counter_reset();
445441
assert_eq!(root_fingerprint(), Ok(vec![0x02, 0x40, 0xe9, 0x2a]));
446-
assert_eq!(bitbox02::securechip::fake_event_counter(), 2);
442+
// fingerprint is precomputed during bip39 unlock, so takes no securechip events.
443+
assert_eq!(bitbox02::securechip::fake_event_counter(), 0);
447444

448445
mock_unlocked_using_mnemonic(
449446
"small agent wife animal marine cloth exit thank stool idea steel frame",
450447
"",
451448
);
452449
assert_eq!(root_fingerprint(), Ok(vec![0xf4, 0x0b, 0x46, 0x9a]));
450+
451+
keystore::lock();
452+
assert_eq!(root_fingerprint(), Err(()));
453453
}
454454

455455
#[test]

src/rust/bitbox02-rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub mod hal;
3636
pub mod hash;
3737
pub mod hww;
3838
pub mod keystore;
39-
mod secp256k1;
39+
pub mod secp256k1;
4040
#[cfg(feature = "app-u2f")]
4141
mod u2f;
4242
mod version;

src/rust/bitbox02/src/keystore.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
// limitations under the License.
1414

1515
extern crate alloc;
16-
use alloc::string::{String, ToString};
1716

17+
use alloc::string::{String, ToString};
1818
use alloc::vec;
1919
use alloc::vec::Vec;
2020

21+
use util::cell::SyncUnsafeCell;
22+
2123
use bitcoin::secp256k1::{All, Secp256k1};
2224

2325
use core::convert::TryInto;
@@ -28,6 +30,8 @@ use bitbox02_sys::keystore_error_t;
2830
const EC_PUBLIC_KEY_LEN: usize = 33;
2931
pub const MAX_SEED_LENGTH: usize = bitbox02_sys::KEYSTORE_MAX_SEED_LENGTH as usize;
3032

33+
static ROOT_FINGERPRINT: SyncUnsafeCell<Option<[u8; 4]>> = SyncUnsafeCell::new(None);
34+
3135
pub fn is_locked() -> bool {
3236
unsafe { bitbox02_sys::keystore_is_locked() }
3337
}
@@ -87,23 +91,39 @@ pub fn unlock(password: &str) -> Result<(), Error> {
8791

8892
pub fn lock() {
8993
unsafe { bitbox02_sys::keystore_lock() }
94+
95+
unsafe { ROOT_FINGERPRINT.write(None) }
9096
}
9197

9298
pub fn unlock_bip39(mnemonic_passphrase: &str) -> Result<(), Error> {
99+
let mut root_fingerprint = [0u8; 4];
93100
if unsafe {
94101
bitbox02_sys::keystore_unlock_bip39(
95102
crate::util::str_to_cstr_vec(mnemonic_passphrase)
96103
.unwrap()
97104
.as_ptr()
98105
.cast(),
106+
root_fingerprint.as_mut_ptr(),
99107
)
100108
} {
109+
// Store root fingerprint.
110+
unsafe {
111+
ROOT_FINGERPRINT.write(Some(root_fingerprint));
112+
}
113+
101114
Ok(())
102115
} else {
103116
Err(Error::CannotUnlockBIP39)
104117
}
105118
}
106119

120+
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
121+
if is_locked() {
122+
return Err(());
123+
}
124+
unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
125+
}
126+
107127
pub fn create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
108128
match unsafe {
109129
bitbox02_sys::keystore_create_and_store_seed(
@@ -475,9 +495,12 @@ mod tests {
475495
.unwrap();
476496
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
477497

498+
assert!(root_fingerprint().is_err());
478499
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
479500
assert!(unlock("password").is_ok());
501+
assert!(root_fingerprint().is_err());
480502
assert!(unlock_bip39("foo").is_ok());
503+
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
481504

482505
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
483506

src/rust/util/src/cell.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2025 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Same as `core::sync::SyncUnsafeCell`, which is still in nightly. Can remove once it is stable.
16+
pub struct SyncUnsafeCell<T: ?Sized> {
17+
value: core::cell::UnsafeCell<T>,
18+
}
19+
20+
// Implement Sync if the wrapped type is Sync.
21+
unsafe impl<T: ?Sized + Sync> Sync for SyncUnsafeCell<T> {}
22+
23+
impl<T> SyncUnsafeCell<T> {
24+
pub const fn new(val: T) -> Self {
25+
SyncUnsafeCell {
26+
value: core::cell::UnsafeCell::new(val),
27+
}
28+
}
29+
30+
/// Reads the value from `self` without moving it. This leaves the
31+
/// memory in `self` unchanged.
32+
///
33+
/// # Safety
34+
///
35+
/// This is unsafe because it allows accessing the interior value without
36+
/// synchronization. The caller must ensure no other code is currently
37+
/// writing to this cell during the read operation.
38+
pub unsafe fn read(&self) -> T
39+
where
40+
T: Sized,
41+
{
42+
unsafe { self.value.get().read() }
43+
}
44+
45+
/// Overwrites a memory location with the given value without reading or
46+
/// dropping the old value.
47+
///
48+
/// # Safety
49+
///
50+
/// This is unsafe because it allows accessing the interior value without
51+
/// synchronization. The caller must ensure no other code is currently
52+
/// accessing this cell (either reading or writing) during the write operation.
53+
pub unsafe fn write(&self, val: T)
54+
where
55+
T: Sized,
56+
{
57+
unsafe { self.value.get().write(val) }
58+
}
59+
}

src/rust/util/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
pub mod ascii;
1717
pub mod bip32;
1818
pub mod bytes;
19+
pub mod cell;
1920
pub mod decimal;
2021
pub mod log;
2122
pub mod name;

0 commit comments

Comments
 (0)