Skip to content

Commit 05c21c6

Browse files
committed
rust/keystore: wrap create_and_store_seed() in bitbox02-rust
Original is renamed so the compiler can error if we missed one. By moving all keystore calls to bitbox02_rust::keystore, we can more easily replace the function bodies with native Rust implementations (without wrapping C).
1 parent 4651417 commit 05c21c6

File tree

3 files changed

+83
-76
lines changed

3 files changed

+83
-76
lines changed

src/rust/bitbox02-rust/src/hww/api/set_password.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::pb;
1818
use crate::hal::Ui;
1919
use crate::workflow::{password, unlock};
2020

21-
use bitbox02::keystore;
21+
use crate::keystore;
2222
use pb::response::Response;
2323

2424
/// Handles the SetPassword api call. This has the user enter a password twice and creates the
@@ -40,7 +40,7 @@ pub async fn process(
4040
hal.ui().status(&format!("Error\n{:?}", err), false).await;
4141
return Err(Error::Generic);
4242
}
43-
unlock::unlock_bip39(hal, &crate::keystore::copy_seed()?).await;
43+
unlock::unlock_bip39(hal, &keystore::copy_seed()?).await;
4444
Ok(Response::Success(pb::Success {}))
4545
}
4646

@@ -57,7 +57,7 @@ mod tests {
5757
#[test]
5858
fn test_process() {
5959
mock_memory();
60-
crate::keystore::lock();
60+
keystore::lock();
6161
let mut counter = 0u32;
6262
let mut mock_hal = TestingHal::new();
6363
mock_hal.ui.set_enter_string(Box::new(|params| {
@@ -83,15 +83,15 @@ mod tests {
8383
assert_eq!(bitbox02::securechip::fake_event_counter(), 9);
8484
drop(mock_hal); // to remove mutable borrow of counter
8585
assert_eq!(counter, 2);
86-
assert!(!crate::keystore::is_locked());
87-
assert!(crate::keystore::copy_seed().unwrap().len() == 32);
86+
assert!(!keystore::is_locked());
87+
assert!(keystore::copy_seed().unwrap().len() == 32);
8888
}
8989

9090
/// Shorter host entropy results in shorter seed.
9191
#[test]
9292
fn test_process_16_bytes() {
9393
mock_memory();
94-
crate::keystore::lock();
94+
keystore::lock();
9595
let mut mock_hal = TestingHal::new();
9696
mock_hal
9797
.ui
@@ -105,20 +105,20 @@ mod tests {
105105
)),
106106
Ok(Response::Success(pb::Success {}))
107107
);
108-
assert!(!crate::keystore::is_locked());
109-
assert!(crate::keystore::copy_seed().unwrap().len() == 16);
108+
assert!(!keystore::is_locked());
109+
assert!(keystore::copy_seed().unwrap().len() == 16);
110110
}
111111

112112
/// Invalid host entropy size.
113113
#[test]
114114
fn test_process_invalid_host_entropy() {
115115
mock_memory();
116-
crate::keystore::lock();
116+
keystore::lock();
117117
let mut mock_hal = TestingHal::new();
118118
mock_hal
119119
.ui
120120
.set_enter_string(Box::new(|_params| Ok("password".into())));
121-
assert!(crate::keystore::is_locked());
121+
assert!(keystore::is_locked());
122122
assert_eq!(
123123
block_on(process(
124124
&mut mock_hal,
@@ -128,13 +128,13 @@ mod tests {
128128
)),
129129
Err(Error::InvalidInput),
130130
);
131-
assert!(crate::keystore::is_locked());
131+
assert!(keystore::is_locked());
132132
}
133133

134134
#[test]
135135
fn test_process_2nd_password_doesnt_match() {
136136
mock_memory();
137-
crate::keystore::lock();
137+
keystore::lock();
138138
let mut counter = 0u32;
139139
let mut mock_hal = TestingHal::new();
140140
mock_hal.ui.set_enter_string(Box::new(|_params| {
@@ -154,6 +154,6 @@ mod tests {
154154
)),
155155
Err(Error::Generic),
156156
);
157-
assert!(crate::keystore::is_locked());
157+
assert!(keystore::is_locked());
158158
}
159159
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ pub fn encrypt_and_store_seed(seed: &[u8], password: &str) -> Result<(), Error>
7474
keystore::_encrypt_and_store_seed(seed, password)
7575
}
7676

77+
/// Generates the seed, mixes it with host_entropy, and stores it encrypted with the
78+
/// password. The size of the host entropy determines the size of the seed. Can be either 16 or 32
79+
/// bytes, resulting in 12 or 24 BIP39 recovery words.
80+
/// This also unlocks the keystore with the new seed.
81+
pub fn create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
82+
keystore::_create_and_store_seed(password, host_entropy)
83+
}
84+
7785
/// Returns the keystore's seed encoded as a BIP-39 mnemonic.
7886
pub fn get_bip39_mnemonic() -> Result<zeroize::Zeroizing<String>, ()> {
7987
keystore::bip39_mnemonic_from_seed(&copy_seed()?)
@@ -462,6 +470,67 @@ mod tests {
462470
);
463471
}
464472

473+
#[test]
474+
fn test_create_and_store_seed() {
475+
let mock_salt_root =
476+
hex::decode("3333333333333333444444444444444411111111111111112222222222222222")
477+
.unwrap();
478+
479+
let host_entropy =
480+
hex::decode("25569b9a11f9db6560459e8e48b4727a4c935300143d978989ed55db1d1b9cbe25569b9a11f9db6560459e8e48b4727a4c935300143d978989ed55db1d1b9cbe")
481+
.unwrap();
482+
483+
// Invalid seed lengths
484+
for size in [8, 24, 40] {
485+
assert!(matches!(
486+
create_and_store_seed("password", &host_entropy[..size]),
487+
Err(Error::SeedSize)
488+
));
489+
}
490+
491+
// Hack to get the random bytes that will be used.
492+
let seed_random = {
493+
bitbox02::random::fake_reset();
494+
bitbox02::random::random_32_bytes()
495+
};
496+
497+
// Derived from mock_salt_root and "password".
498+
let password_salted_hashed =
499+
hex::decode("e8c70a20d9108fbb9454b1b8e2d7373e78cbaf9de025ab2d4f4d3c7a6711694c")
500+
.unwrap();
501+
502+
// expected_seed = seed_random ^ host_entropy ^ password_salted_hashed
503+
let expected_seed: Vec<u8> = seed_random
504+
.into_iter()
505+
.zip(host_entropy.iter())
506+
.zip(password_salted_hashed)
507+
.map(|((a, &b), c)| a ^ b ^ c)
508+
.collect();
509+
510+
for size in [16, 32] {
511+
mock_memory();
512+
bitbox02::random::fake_reset();
513+
bitbox02::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
514+
lock();
515+
516+
assert!(create_and_store_seed("password", &host_entropy[..size]).is_ok());
517+
assert_eq!(copy_seed().unwrap().as_slice(), &expected_seed[..size]);
518+
// Check the seed has been stored encrypted with the expected encryption key.
519+
// Decrypt and check seed.
520+
let cipher = bitbox02::memory::get_encrypted_seed_and_hmac().unwrap();
521+
522+
// Same as Python:
523+
// import hmac, hashlib; hmac.digest(b"unit-test", b"password", hashlib.sha256).hex()
524+
// See also: mock_securechip.c
525+
let expected_encryption_key =
526+
hex::decode("e56de448f5f1d29cdcc0e0099007309afe4d5a3ef2349e99dcc41840ad98409e")
527+
.unwrap();
528+
let decrypted =
529+
bitbox_aes::decrypt_with_hmac(&expected_encryption_key, &cipher).unwrap();
530+
assert_eq!(decrypted.as_slice(), &expected_seed[..size]);
531+
}
532+
}
533+
465534
// This tests that you can create a keystore, unlock it, and then do this again. This is an
466535
// expected workflow for when the wallet setup process is restarted after seeding and unlocking,
467536
// but before creating a backup, in which case a new seed is created.

src/rust/bitbox02/src/keystore.rs

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
195195
unsafe { ROOT_FINGERPRINT.read().ok_or(()).map(|fp| fp.to_vec()) }
196196
}
197197

198-
pub fn create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
198+
pub fn _create_and_store_seed(password: &str, host_entropy: &[u8]) -> Result<(), Error> {
199199
match unsafe {
200200
bitbox02_sys::keystore_create_and_store_seed(
201201
crate::util::str_to_cstr_vec(password)
@@ -316,7 +316,6 @@ mod tests {
316316
use super::*;
317317
use bitcoin::secp256k1;
318318

319-
use crate::testing::mock_memory;
320319
use util::bb02_async::block_on;
321320

322321
#[test]
@@ -446,65 +445,4 @@ mod tests {
446445
);
447446
}
448447
}
449-
450-
#[test]
451-
fn test_create_and_store_seed() {
452-
let mock_salt_root =
453-
hex::decode("3333333333333333444444444444444411111111111111112222222222222222")
454-
.unwrap();
455-
456-
let host_entropy =
457-
hex::decode("25569b9a11f9db6560459e8e48b4727a4c935300143d978989ed55db1d1b9cbe25569b9a11f9db6560459e8e48b4727a4c935300143d978989ed55db1d1b9cbe")
458-
.unwrap();
459-
460-
// Invalid seed lengths
461-
for size in [8, 24, 40] {
462-
assert!(matches!(
463-
create_and_store_seed("password", &host_entropy[..size]),
464-
Err(Error::SeedSize)
465-
));
466-
}
467-
468-
// Hack to get the random bytes that will be used.
469-
let seed_random = {
470-
crate::random::fake_reset();
471-
crate::random::random_32_bytes()
472-
};
473-
474-
// Derived from mock_salt_root and "password".
475-
let password_salted_hashed =
476-
hex::decode("e8c70a20d9108fbb9454b1b8e2d7373e78cbaf9de025ab2d4f4d3c7a6711694c")
477-
.unwrap();
478-
479-
// expected_seed = seed_random ^ host_entropy ^ password_salted_hashed
480-
let expected_seed: Vec<u8> = seed_random
481-
.into_iter()
482-
.zip(host_entropy.iter())
483-
.zip(password_salted_hashed)
484-
.map(|((a, &b), c)| a ^ b ^ c)
485-
.collect();
486-
487-
for size in [16, 32] {
488-
mock_memory();
489-
crate::random::fake_reset();
490-
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
491-
_lock();
492-
493-
assert!(create_and_store_seed("password", &host_entropy[..size]).is_ok());
494-
assert_eq!(_copy_seed().unwrap().as_slice(), &expected_seed[..size]);
495-
// Check the seed has been stored encrypted with the expected encryption key.
496-
// Decrypt and check seed.
497-
let cipher = crate::memory::get_encrypted_seed_and_hmac().unwrap();
498-
499-
// Same as Python:
500-
// import hmac, hashlib; hmac.digest(b"unit-test", b"password", hashlib.sha256).hex()
501-
// See also: mock_securechip.c
502-
let expected_encryption_key =
503-
hex::decode("e56de448f5f1d29cdcc0e0099007309afe4d5a3ef2349e99dcc41840ad98409e")
504-
.unwrap();
505-
let decrypted =
506-
bitbox_aes::decrypt_with_hmac(&expected_encryption_key, &cipher).unwrap();
507-
assert_eq!(decrypted.as_slice(), &expected_seed[..size]);
508-
}
509-
}
510448
}

0 commit comments

Comments
 (0)