Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/rust/bitbox02-rust/src/hal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,20 @@ pub trait Random {
fn random_32_bytes(&mut self) -> Box<zeroize::Zeroizing<[u8; 32]>>;
}

pub trait SecureChip {
fn init_new_password(&mut self, password: &str) -> Result<(), bitbox02::securechip::Error>;
fn stretch_password(
&mut self,
password: &str,
) -> Result<zeroize::Zeroizing<Vec<u8>>, 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;
Expand Down Expand Up @@ -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<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
bitbox02::securechip::stretch_password(password)
}
}

pub struct BitBox02Hal {
ui: RealWorkflows,
sd: BitBox02Sd,
random: BitBox02Random,
securechip: BitBox02SecureChip,
}

impl BitBox02Hal {
Expand All @@ -109,6 +134,7 @@ impl BitBox02Hal {
ui: crate::workflow::RealWorkflows,
sd: BitBox02Sd,
random: BitBox02Random,
securechip: BitBox02SecureChip,
}
}
}
Expand All @@ -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")]
Expand Down Expand Up @@ -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<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
self.event_counter += 5;

use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
let mut engine = HmacEngine::<sha256::Hash>::new(b"unit-test");
engine.input(password.as_bytes());
let hmac_result: Hmac<sha256::Hash> = 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<'_> {
Expand All @@ -235,6 +316,7 @@ pub mod testing {
ui: crate::workflow::testing::TestingWorkflows::new(),
sd: TestingSd::new(),
random: TestingRandom::new(),
securechip: TestingSecureChip::new(),
}
}
}
Expand All @@ -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)]
Expand Down
8 changes: 4 additions & 4 deletions src/rust/bitbox02-rust/src/hww/api/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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![
Expand Down
4 changes: 2 additions & 2 deletions src/rust/bitbox02-rust/src/hww/api/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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());
Expand Down
4 changes: 2 additions & 2 deletions src/rust/bitbox02-rust/src/hww/api/set_password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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());
Expand Down
12 changes: 6 additions & 6 deletions src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
44 changes: 25 additions & 19 deletions src/rust/bitbox02-rust/src/keystore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -243,13 +244,16 @@ fn check_retained_seed(seed: &[u8]) -> Result<(), ()> {
Ok(())
}

fn get_and_decrypt_seed(password: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
fn get_and_decrypt_seed(
hal: &mut impl crate::hal::Hal,
password: &str,
) -> Result<zeroize::Zeroizing<Vec<u8>>, 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),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1510,33 +1514,35 @@ 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 || {}
))
.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!(
Expand All @@ -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(),
Expand Down
Loading