diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs index 06f1ece584..92245281d1 100644 --- a/src/rust/bitbox02-rust/src/hal.rs +++ b/src/rust/bitbox02-rust/src/hal.rs @@ -38,11 +38,20 @@ pub trait Random { fn random_32_bytes(&mut self) -> Box>; } +pub trait SecureChip { + fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error>; + fn stretch_password( + &mut self, + password: &str, + ) -> Result>, bitbox02::securechip::Error>; +} + /// Hardware abstraction layer for BitBox devices. pub trait Hal { fn ui(&mut self) -> &mut impl Ui; fn sd(&mut self) -> &mut impl Sd; fn random(&mut self) -> &mut impl Random; + fn securechip(&mut self) -> &mut impl SecureChip; } pub struct BitBox02Sd; @@ -97,10 +106,26 @@ impl Random for BitBox02Random { } } +pub struct BitBox02SecureChip; + +impl SecureChip for BitBox02SecureChip { + fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error> { + bitbox02::securechip::init_new_password(password) + } + + fn stretch_password( + &mut self, + password: &str, + ) -> Result>, bitbox02::securechip::Error> { + bitbox02::securechip::stretch_password(password) + } +} + pub struct BitBox02Hal { ui: RealWorkflows, sd: BitBox02Sd, random: BitBox02Random, + securechip: BitBox02SecureChip, } impl BitBox02Hal { @@ -109,6 +134,7 @@ impl BitBox02Hal { ui: crate::workflow::RealWorkflows, sd: BitBox02Sd, random: BitBox02Random, + securechip: BitBox02SecureChip, } } } @@ -123,6 +149,9 @@ impl Hal for BitBox02Hal { fn random(&mut self) -> &mut impl Random { &mut self.random } + fn securechip(&mut self) -> &mut impl SecureChip { + &mut self.securechip + } } #[cfg(feature = "testing")] @@ -223,10 +252,62 @@ pub mod testing { } } + pub struct TestingSecureChip { + // Count how man seceurity events happen. The numbers were obtained by reading the security + // event counter slot (0xE0C5) on a real device. We can use this to assert how many events + // were used in unit tests. The number is relevant due to Optiga's throttling mechanism. + event_counter: u32, + } + + impl TestingSecureChip { + pub fn new() -> Self { + TestingSecureChip { event_counter: 0 } + } + + /// Resets the event counter. + pub fn event_counter_reset(&mut self) { + self.event_counter = 0; + // TODO: remove once all unit tests use the SecureChip HAL. + bitbox02::securechip::fake_event_counter_reset() + } + + /// Retrieves the event counter. + pub fn get_event_counter(&self) -> u32 { + // TODO: remove fake_event_counter() once all unit tests use the SecureChip HAL. + bitbox02::securechip::fake_event_counter() + self.event_counter + } + } + + impl super::SecureChip for TestingSecureChip { + fn init_new_password( + &mut self, + _password: &str, + ) -> Result<(), bitbox02::securechip::Error> { + self.event_counter += 1; + Ok(()) + } + + fn stretch_password( + &mut self, + password: &str, + ) -> Result>, bitbox02::securechip::Error> { + self.event_counter += 5; + + use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256}; + let mut engine = HmacEngine::::new(b"unit-test"); + engine.input(password.as_bytes()); + let hmac_result: Hmac = Hmac::from_engine(engine); + Ok(zeroize::Zeroizing::new( + hmac_result.to_byte_array().to_vec(), + )) + } + } + pub struct TestingHal<'a> { pub ui: crate::workflow::testing::TestingWorkflows<'a>, pub sd: TestingSd, pub random: TestingRandom, + pub securechip: TestingSecureChip, } impl TestingHal<'_> { @@ -235,6 +316,7 @@ pub mod testing { ui: crate::workflow::testing::TestingWorkflows::new(), sd: TestingSd::new(), random: TestingRandom::new(), + securechip: TestingSecureChip::new(), } } } @@ -249,6 +331,9 @@ pub mod testing { fn random(&mut self) -> &mut impl super::Random { &mut self.random } + fn securechip(&mut self) -> &mut impl super::SecureChip { + &mut self.securechip + } } #[cfg(test)] diff --git a/src/rust/bitbox02-rust/src/hww/api/backup.rs b/src/rust/bitbox02-rust/src/hww/api/backup.rs index 1e84b06dcd..a115154b2c 100644 --- a/src/rust/bitbox02-rust/src/hww/api/backup.rs +++ b/src/rust/bitbox02-rust/src/hww/api/backup.rs @@ -187,7 +187,7 @@ mod tests { let mut mock_hal = TestingHal::new(); mock_hal.sd.inserted = Some(true); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(create( &mut mock_hal, @@ -198,7 +198,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert_eq!(EXPECTED_TIMESTMAP, bitbox02::memory::get_seed_birthdate()); assert_eq!( mock_hal.ui.screens, @@ -246,7 +246,7 @@ mod tests { password_entered = true; Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(create( &mut mock_hal, @@ -257,7 +257,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); assert_eq!( mock_hal.ui.screens, vec![ diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs index ff6199156d..68945fdfa6 100644 --- a/src/rust/bitbox02-rust/src/hww/api/restore.rs +++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs @@ -189,7 +189,7 @@ mod tests { Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(from_mnemonic( &mut mock_hal, @@ -200,7 +200,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 8); + assert_eq!(mock_hal.securechip.get_event_counter(), 8); drop(mock_hal); // to remove mutable borrow of counter assert_eq!(counter, 2); assert!(!crate::keystore::is_locked()); diff --git a/src/rust/bitbox02-rust/src/hww/api/set_password.rs b/src/rust/bitbox02-rust/src/hww/api/set_password.rs index 939829196a..332c0845a8 100644 --- a/src/rust/bitbox02-rust/src/hww/api/set_password.rs +++ b/src/rust/bitbox02-rust/src/hww/api/set_password.rs @@ -70,7 +70,7 @@ mod tests { Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(process( &mut mock_hal, @@ -80,7 +80,7 @@ mod tests { )), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 9); + assert_eq!(mock_hal.securechip.get_event_counter(), 9); drop(mock_hal); // to remove mutable borrow of counter assert_eq!(counter, 2); assert!(!keystore::is_locked()); diff --git a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs index 5648fb1c57..2fe7b34be3 100644 --- a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs +++ b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs @@ -97,13 +97,13 @@ mod tests { panic!("unexpected call to enter password") })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(process(&mut mock_hal)), Ok(Response::Success(pb::Success {})) ); // 1 operation for one copy_seed() to get the seed to display it. - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert_eq!( mock_hal.ui.screens, @@ -151,12 +151,12 @@ mod tests { Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!( block_on(process(&mut mock_hal)), Ok(Response::Success(pb::Success {})) ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); assert_eq!( mock_hal.ui.screens, @@ -206,9 +206,9 @@ mod tests { .ui .set_enter_string(Box::new(|_params| Ok("wrong password".into()))); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(block_on(process(&mut mock_hal)), Err(Error::Generic)); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); assert_eq!( mock_hal.ui.screens, diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs index 4665ae720d..c1a651c646 100644 --- a/src/rust/bitbox02-rust/src/keystore.rs +++ b/src/rust/bitbox02-rust/src/keystore.rs @@ -19,7 +19,7 @@ use alloc::string::String; use alloc::vec::Vec; use crate::bip32; -use crate::hal::Random; +use crate::hal::{Random, SecureChip}; pub use bitbox02::keystore::SignResult; use bitbox02::{keystore, securechip}; @@ -211,9 +211,10 @@ pub fn encrypt_and_store_seed( bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT); - securechip::init_new_password(password)?; + hal.securechip().init_new_password(password)?; + + let secret = hal.securechip().stretch_password(password)?; - let secret = securechip::stretch_password(password)?; let iv_rand = hal.random().random_32_bytes(); let iv: &[u8; 16] = iv_rand.first_chunk::<16>().unwrap(); let encrypted = bitbox_aes::encrypt_with_hmac(iv, &secret, seed); @@ -243,13 +244,16 @@ fn check_retained_seed(seed: &[u8]) -> Result<(), ()> { Ok(()) } -fn get_and_decrypt_seed(password: &str) -> Result>, Error> { +fn get_and_decrypt_seed( + hal: &mut impl crate::hal::Hal, + password: &str, +) -> Result>, Error> { let encrypted = bitbox02::memory::get_encrypted_seed_and_hmac().map_err(|_| Error::Memory)?; // Our Optiga securechip implementation fails password stretching if the password is // wrong, so it already returns an error here. The ATECC stretches the password without checking // if the password is correct, and we determine if it is correct in the seed decryption // step below. - let secret = securechip::stretch_password(password)?; + let secret = hal.securechip().stretch_password(password)?; let seed = match bitbox_aes::decrypt_with_hmac(&secret, &encrypted) { Ok(seed) => seed, Err(()) => return Err(Error::IncorrectPassword), @@ -279,7 +283,7 @@ pub fn unlock( } bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT); bitbox02::memory::smarteeprom_increment_unlock_attempts(); - let seed = match get_and_decrypt_seed(password) { + let seed = match get_and_decrypt_seed(hal, password) { Ok(seed) => seed, err @ Err(_) => { if get_remaining_unlock_attempts() == 0 { @@ -892,17 +896,17 @@ mod tests { )); // First call: unlock. The first one does a seed rentention (1 securechip event). - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed); - assert_eq!(bitbox02::securechip::fake_event_counter(), 6); + assert_eq!(mock_hal.securechip.get_event_counter(), 6); // Loop to check that unlocking works while unlocked. for _ in 0..2 { // Further calls perform a password check.The password check does not do the retention // so it ends up needing one secure chip operation less. - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(unlock(&mut mock_hal, "password").unwrap().as_slice(), seed); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); } // Also check that the retained seed was encrypted with the expected encryption key. @@ -1510,9 +1514,11 @@ mod tests { lock(); let seed = &seed[..test.seed_len]; + let mut mock_hal = crate::hal::testing::TestingHal::new(); + assert!( block_on(unlock_bip39( - &mut crate::hal::testing::TestingRandom::new(), + &mut mock_hal.random, seed, test.mnemonic_passphrase, async || {} @@ -1520,23 +1526,23 @@ mod tests { .is_err() ); - bitbox02::securechip::fake_event_counter_reset(); - assert!(encrypt_and_store_seed(&mut TestingHal::new(), seed, "foo").is_ok()); - assert_eq!(bitbox02::securechip::fake_event_counter(), 7); + mock_hal.securechip.event_counter_reset(); + assert!(encrypt_and_store_seed(&mut mock_hal, seed, "foo").is_ok()); + assert_eq!(mock_hal.securechip.get_event_counter(), 7); assert!(is_locked()); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert!( block_on(unlock_bip39( - &mut crate::hal::testing::TestingRandom::new(), + &mut mock_hal.random, seed, test.mnemonic_passphrase, async || {} )) .is_ok() ); - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert!(!is_locked()); assert_eq!( @@ -1545,9 +1551,9 @@ mod tests { ); let keypath = &[44 + HARDENED, 0 + HARDENED, 0 + HARDENED]; - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); let xpub = get_xpub_once(keypath).unwrap(); - assert_eq!(bitbox02::securechip::fake_event_counter(), 1); + assert_eq!(mock_hal.securechip.get_event_counter(), 1); assert_eq!( xpub.serialize_str(crate::bip32::XPubType::Xpub).unwrap(), diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs index 021b0f7720..934799a96a 100644 --- a/src/rust/bitbox02-rust/src/workflow/unlock.rs +++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs @@ -218,10 +218,10 @@ mod tests { password_entered = true; Ok("password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert_eq!(block_on(unlock(&mut mock_hal)), Ok(())); // 6 for keystore unlock, 1 for keystore bip39 unlock. - assert_eq!(bitbox02::securechip::fake_event_counter(), 7); + assert_eq!(mock_hal.securechip.get_event_counter(), 7); assert!(!crate::keystore::is_locked()); @@ -261,7 +261,7 @@ mod tests { Ok("wrong password".into()) })); - bitbox02::securechip::fake_event_counter_reset(); + mock_hal.securechip.event_counter_reset(); assert!(matches!( block_on(unlock_keystore( &mut mock_hal, @@ -270,7 +270,7 @@ mod tests { )), Err(UnlockError::IncorrectPassword), )); - assert_eq!(bitbox02::securechip::fake_event_counter(), 5); + assert_eq!(mock_hal.securechip.get_event_counter(), 5); // Checks that the device is locked. assert!(crate::keystore::copy_seed().is_err());