Skip to content

Commit d02d139

Browse files
committed
rust/keystore: move bip39_unlock and ROOT_FINGERPRINT
1 parent fcf2d8b commit d02d139

File tree

3 files changed

+116
-144
lines changed

3 files changed

+116
-144
lines changed

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,29 @@ pub fn mnemonic_to_seed(mnemonic: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, (
4040
Ok(zeroize::Zeroizing::new(seed[..seed_len].to_vec()))
4141
}
4242

43+
/// Derives the bip39 seed and returns it and the bip32 root fingerprint.
44+
/// `mnemonic_passphrase` is the bip39 passphrase used in the derivation.
45+
/// `yield_now` is called in each of the 2048 bip39 pbkdf2 iterations.
46+
pub async fn derive_seed(
47+
seed: &[u8],
48+
mnemonic_passphrase: &str,
49+
yield_now: impl AsyncFn(),
50+
) -> (zeroize::Zeroizing<[u8; 64]>, [u8; 4]) {
51+
let mnemonic = bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed).unwrap();
52+
let bip39_seed: zeroize::Zeroizing<[u8; 64]> = zeroize::Zeroizing::new(
53+
mnemonic
54+
.to_seed_normalized_async(mnemonic_passphrase, yield_now)
55+
.await,
56+
);
57+
let root_fingerprint: [u8; 4] =
58+
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
59+
.unwrap()
60+
.fingerprint(crate::secp256k1::SECP256K1)
61+
.to_bytes();
62+
63+
(bip39_seed, root_fingerprint)
64+
}
65+
4366
// C API
4467

4568
#[unsafe(no_mangle)]
@@ -61,6 +84,7 @@ pub extern "C" fn rust_get_bip39_word(idx: u16, mut out: util::bytes::BytesMut)
6184
#[cfg(test)]
6285
mod tests {
6386
use super::*;
87+
use util::bb02_async::block_on;
6488

6589
#[test]
6690
fn test_rust_get_bip39_word() {
@@ -168,4 +192,67 @@ mod tests {
168192
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",
169193
);
170194
}
195+
196+
#[test]
197+
fn test_derive_bip39_seed() {
198+
struct Test {
199+
seed: &'static str,
200+
passphrase: &'static str,
201+
expected_bip39_seed: &'static str,
202+
expected_root_fingerprint: &'static str,
203+
}
204+
205+
let tests = &[
206+
// 16 byte seed
207+
Test {
208+
seed: "fb5cf00d5ea61059fa066e25a6be9544",
209+
passphrase: "",
210+
expected_bip39_seed: "f4577e463be595868060e5a763328153155b4167cd284998c8c6096d044742372020f5b052d0c41c1c5e6a6a7da2cb8a367aaaa074fab7773e8d5b2f684257ed",
211+
expected_root_fingerprint: "0b2fa4e5",
212+
},
213+
Test {
214+
seed: "fb5cf00d5ea61059fa066e25a6be9544",
215+
passphrase: "password",
216+
expected_bip39_seed: "5922fb7630bc7cb871af102f733b6bdb8f05945147cd4646a89056fde0bdad5c3a4ff5be3f9e7af535f570e7053b5b22472555b331bc89cb797c306f7eb6a5a1",
217+
expected_root_fingerprint: "c4062d44",
218+
},
219+
// 24 byte seed
220+
Test {
221+
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
222+
passphrase: "",
223+
expected_bip39_seed: "4a2a016a6d90eb3a79b7931ca0a172df5c5bfee3e5b47f0fd84bc0791ea3bbc9476c3d5de71cdb12c37e93c2aa3d5c303257f1992aed400fc5bbfc7da787bfa7",
224+
expected_root_fingerprint: "62fd19e0",
225+
},
226+
Test {
227+
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
228+
passphrase: "password",
229+
expected_bip39_seed: "bc317ee0f88870254be32274d63ec2b0e962bf09f3ca04287912bfc843f2fab7c556f8657cadc924f99a217b0daa91898303a8414102031a125c50023e45a80b",
230+
expected_root_fingerprint: "c745266d",
231+
},
232+
// 32 byte seed
233+
Test {
234+
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
235+
passphrase: "",
236+
expected_bip39_seed: "63f844e2c61ecfb20f9100de381a7a9ec875b085f5ac7735a2ba4d615a0f4147b87be402f65651969130683deeef752760c09e291604fe4b89d61ffee2630be8",
237+
expected_root_fingerprint: "93ba3a7b",
238+
},
239+
Test {
240+
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
241+
passphrase: "password",
242+
expected_bip39_seed: "42e90dacd61f3373542d212f0fb9c291dcea84a6d85034272372dde7188638a98527280d65e41599f30d3434d8ee3d4747dbb84801ff1a851d2306c7d1648374",
243+
expected_root_fingerprint: "b95c9318",
244+
},
245+
];
246+
247+
for test in tests {
248+
let seed = hex::decode(test.seed).unwrap();
249+
let (bip39_seed, root_fingerprint) =
250+
block_on(derive_seed(&seed, test.passphrase, async || {}));
251+
assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
252+
assert_eq!(
253+
hex::encode(root_fingerprint).as_str(),
254+
test.expected_root_fingerprint
255+
);
256+
}
257+
}
171258
}

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub use bitbox02::keystore::SignResult;
2424
use bitbox02::{keystore, securechip};
2525

2626
use util::bip32::HARDENED;
27+
use util::cell::SyncUnsafeCell;
2728

2829
use crate::secp256k1::SECP256K1;
2930

@@ -32,9 +33,12 @@ use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine, sha256, sha512};
3233
/// Length of a compressed secp256k1 pubkey.
3334
const EC_PUBLIC_KEY_LEN: usize = 33;
3435

36+
static ROOT_FINGERPRINT: SyncUnsafeCell<Option<[u8; 4]>> = SyncUnsafeCell::new(None);
37+
3538
/// Locks the keystore (resets to state before `unlock()`).
3639
pub fn lock() {
3740
keystore::_lock();
41+
unsafe { ROOT_FINGERPRINT.write(None) }
3842
}
3943

4044
/// Returns false if the keystore is unlocked (unlock() followed by unlock_bip39()), true otherwise.
@@ -55,7 +59,25 @@ pub async fn unlock_bip39(
5559
mnemonic_passphrase: &str,
5660
yield_now: impl AsyncFn(),
5761
) -> Result<(), Error> {
58-
keystore::_unlock_bip39(SECP256K1, seed, mnemonic_passphrase, yield_now).await
62+
keystore::unlock_bip39_check(seed)?;
63+
64+
let (bip39_seed, root_fingerprint) =
65+
crate::bip39::derive_seed(seed, mnemonic_passphrase, &yield_now).await;
66+
67+
let (bip39_seed_2, root_fingerprint_2) =
68+
crate::bip39::derive_seed(seed, mnemonic_passphrase, &yield_now).await;
69+
70+
if bip39_seed != bip39_seed_2 || root_fingerprint != root_fingerprint_2 {
71+
return Err(Error::Memory);
72+
}
73+
74+
keystore::unlock_bip39_finalize(bip39_seed.as_slice().try_into().unwrap())?;
75+
76+
// Store root fingerprint.
77+
unsafe {
78+
ROOT_FINGERPRINT.write(Some(root_fingerprint));
79+
}
80+
Ok(())
5981
}
6082

6183
/// 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>, ()> {
183205
/// according to:
184206
/// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
185207
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
186-
keystore::root_fingerprint()
208+
if is_locked() {
209+
return Err(());
210+
}
211+
unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
187212
}
188213

189214
/// Stretches the given encryption_key using the securechip. The resulting key is used to encrypt

src/rust/bitbox02/src/keystore.rs

Lines changed: 2 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ extern crate alloc;
1717
use alloc::vec;
1818
use alloc::vec::Vec;
1919

20-
use util::cell::SyncUnsafeCell;
21-
2220
use bitcoin::secp256k1::{All, Secp256k1};
2321

2422
use core::convert::TryInto;
@@ -29,8 +27,6 @@ use bitbox02_sys::keystore_error_t;
2927
const EC_PUBLIC_KEY_LEN: usize = 33;
3028
pub const MAX_SEED_LENGTH: usize = bitbox02_sys::KEYSTORE_MAX_SEED_LENGTH as usize;
3129

32-
static ROOT_FINGERPRINT: SyncUnsafeCell<Option<[u8; 4]>> = SyncUnsafeCell::new(None);
33-
3430
pub fn _is_locked() -> bool {
3531
unsafe { bitbox02_sys::keystore_is_locked() }
3632
}
@@ -97,47 +93,24 @@ pub fn _unlock(password: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
9793

9894
pub fn _lock() {
9995
unsafe { bitbox02_sys::keystore_lock() }
100-
101-
unsafe { ROOT_FINGERPRINT.write(None) }
10296
}
10397

104-
fn unlock_bip39_check(seed: &[u8]) -> Result<(), Error> {
98+
pub fn unlock_bip39_check(seed: &[u8]) -> Result<(), Error> {
10599
if unsafe { bitbox02_sys::keystore_unlock_bip39_check(seed.as_ptr(), seed.len()) } {
106100
Ok(())
107101
} else {
108102
Err(Error::CannotUnlockBIP39)
109103
}
110104
}
111105

112-
fn unlock_bip39_finalize(bip39_seed: &[u8; 64]) -> Result<(), Error> {
106+
pub fn unlock_bip39_finalize(bip39_seed: &[u8; 64]) -> Result<(), Error> {
113107
if unsafe { bitbox02_sys::keystore_unlock_bip39_finalize(bip39_seed.as_ptr()) } {
114108
Ok(())
115109
} else {
116110
Err(Error::CannotUnlockBIP39)
117111
}
118112
}
119113

120-
async fn derive_bip39_seed(
121-
secp: &Secp256k1<All>,
122-
seed: &[u8],
123-
mnemonic_passphrase: &str,
124-
yield_now: impl AsyncFn(),
125-
) -> (zeroize::Zeroizing<[u8; 64]>, [u8; 4]) {
126-
let mnemonic = bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed).unwrap();
127-
let bip39_seed: zeroize::Zeroizing<[u8; 64]> = zeroize::Zeroizing::new(
128-
mnemonic
129-
.to_seed_normalized_async(mnemonic_passphrase, yield_now)
130-
.await,
131-
);
132-
let root_fingerprint: [u8; 4] =
133-
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
134-
.unwrap()
135-
.fingerprint(secp)
136-
.to_bytes();
137-
138-
(bip39_seed, root_fingerprint)
139-
}
140-
141114
#[cfg(feature = "testing")]
142115
pub fn test_get_retained_seed_encrypted() -> &'static [u8] {
143116
unsafe {
@@ -156,44 +129,6 @@ pub fn test_get_retained_bip39_seed_encrypted() -> &'static [u8] {
156129
}
157130
}
158131

159-
/// Unlocks the bip39 seed. The input seed must be the keystore seed (i.e. must match the output
160-
/// of `keystore_copy_seed()`).
161-
/// `mnemonic_passphrase` is the bip39 passphrase used in the derivation. Use the empty string if no
162-
/// passphrase is needed or provided.
163-
pub async fn _unlock_bip39(
164-
secp: &Secp256k1<All>,
165-
seed: &[u8],
166-
mnemonic_passphrase: &str,
167-
yield_now: impl AsyncFn(),
168-
) -> Result<(), Error> {
169-
unlock_bip39_check(seed)?;
170-
171-
let (bip39_seed, root_fingerprint) =
172-
derive_bip39_seed(secp, seed, mnemonic_passphrase, &yield_now).await;
173-
174-
let (bip39_seed_2, root_fingerprint_2) =
175-
derive_bip39_seed(secp, seed, mnemonic_passphrase, &yield_now).await;
176-
177-
if bip39_seed != bip39_seed_2 || root_fingerprint != root_fingerprint_2 {
178-
return Err(Error::Memory);
179-
}
180-
181-
unlock_bip39_finalize(bip39_seed.as_slice().try_into().unwrap())?;
182-
183-
// Store root fingerprint.
184-
unsafe {
185-
ROOT_FINGERPRINT.write(Some(root_fingerprint));
186-
}
187-
Ok(())
188-
}
189-
190-
pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
191-
if _is_locked() {
192-
return Err(());
193-
}
194-
unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
195-
}
196-
197132
pub fn _create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
198133
match unsafe {
199134
bitbox02_sys::keystore_create_and_store_seed(
@@ -304,78 +239,3 @@ pub fn mock_unlocked(seed: &[u8]) {
304239
bitbox02_sys::keystore_mock_unlocked(seed.as_ptr(), seed.len() as _, core::ptr::null())
305240
}
306241
}
307-
308-
#[cfg(test)]
309-
mod tests {
310-
use super::*;
311-
use bitcoin::secp256k1;
312-
use util::bb02_async::block_on;
313-
314-
#[test]
315-
fn test_derive_bip39_seed() {
316-
struct Test {
317-
seed: &'static str,
318-
passphrase: &'static str,
319-
expected_bip39_seed: &'static str,
320-
expected_root_fingerprint: &'static str,
321-
}
322-
323-
let tests = &[
324-
// 16 byte seed
325-
Test {
326-
seed: "fb5cf00d5ea61059fa066e25a6be9544",
327-
passphrase: "",
328-
expected_bip39_seed: "f4577e463be595868060e5a763328153155b4167cd284998c8c6096d044742372020f5b052d0c41c1c5e6a6a7da2cb8a367aaaa074fab7773e8d5b2f684257ed",
329-
expected_root_fingerprint: "0b2fa4e5",
330-
},
331-
Test {
332-
seed: "fb5cf00d5ea61059fa066e25a6be9544",
333-
passphrase: "password",
334-
expected_bip39_seed: "5922fb7630bc7cb871af102f733b6bdb8f05945147cd4646a89056fde0bdad5c3a4ff5be3f9e7af535f570e7053b5b22472555b331bc89cb797c306f7eb6a5a1",
335-
expected_root_fingerprint: "c4062d44",
336-
},
337-
// 24 byte seed
338-
Test {
339-
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
340-
passphrase: "",
341-
expected_bip39_seed: "4a2a016a6d90eb3a79b7931ca0a172df5c5bfee3e5b47f0fd84bc0791ea3bbc9476c3d5de71cdb12c37e93c2aa3d5c303257f1992aed400fc5bbfc7da787bfa7",
342-
expected_root_fingerprint: "62fd19e0",
343-
},
344-
Test {
345-
seed: "23705a91b177b49822f28b3f1a60072d113fcaff4f250191",
346-
passphrase: "password",
347-
expected_bip39_seed: "bc317ee0f88870254be32274d63ec2b0e962bf09f3ca04287912bfc843f2fab7c556f8657cadc924f99a217b0daa91898303a8414102031a125c50023e45a80b",
348-
expected_root_fingerprint: "c745266d",
349-
},
350-
// 32 byte seed
351-
Test {
352-
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
353-
passphrase: "",
354-
expected_bip39_seed: "63f844e2c61ecfb20f9100de381a7a9ec875b085f5ac7735a2ba4d615a0f4147b87be402f65651969130683deeef752760c09e291604fe4b89d61ffee2630be8",
355-
expected_root_fingerprint: "93ba3a7b",
356-
},
357-
Test {
358-
seed: "bd83a008b3b78c8cc56c678d1b7bfc651cc5be8242f44b5c0db96a34ee297833",
359-
passphrase: "password",
360-
expected_bip39_seed: "42e90dacd61f3373542d212f0fb9c291dcea84a6d85034272372dde7188638a98527280d65e41599f30d3434d8ee3d4747dbb84801ff1a851d2306c7d1648374",
361-
expected_root_fingerprint: "b95c9318",
362-
},
363-
];
364-
365-
let secp = secp256k1::Secp256k1::new();
366-
for test in tests {
367-
let seed = hex::decode(test.seed).unwrap();
368-
let (bip39_seed, root_fingerprint) = block_on(derive_bip39_seed(
369-
&secp,
370-
&seed,
371-
test.passphrase,
372-
async || {},
373-
));
374-
assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
375-
assert_eq!(
376-
hex::encode(root_fingerprint).as_str(),
377-
test.expected_root_fingerprint
378-
);
379-
}
380-
}
381-
}

0 commit comments

Comments
 (0)