Skip to content

Commit e6d8560

Browse files
committed
keystore: reduce secure chip operations when unlocking
Every call to `keystore::unlock` is followed by `copy_seed()`. The latter uses a secure chip operation. We can remove that by returning the seed from unlock and re-using it. This reduces the number of securechip operations by 1 when: - when unlocking the device - showing mnemonic on initialized device - creating backu on initialized device This is to reduce the risk of running into Optiga's throttling security mechanism.
1 parent 17ceae1 commit e6d8560

File tree

6 files changed

+63
-35
lines changed

6 files changed

+63
-35
lines changed

src/keystore.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,9 @@ static bool _check_retained_seed(const uint8_t* seed, size_t seed_length)
445445
keystore_error_t keystore_unlock(
446446
const char* password,
447447
uint8_t* remaining_attempts_out,
448-
int* securechip_result_out)
448+
int* securechip_result_out,
449+
uint8_t* seed_out,
450+
size_t* seed_len_out)
449451
{
450452
if (!memory_is_seeded()) {
451453
return KEYSTORE_ERR_UNSEEDED;
@@ -486,6 +488,11 @@ keystore_error_t keystore_unlock(
486488
_is_unlocked_device = true;
487489
}
488490
bitbox02_smarteeprom_reset_unlock_attempts();
491+
492+
if (seed_out != NULL && seed_len_out != NULL) {
493+
memcpy(seed_out, seed, seed_len);
494+
*seed_len_out = seed_len;
495+
}
489496
}
490497
// Compute remaining attempts
491498
failed_attempts = bitbox02_smarteeprom_get_unlock_attempts();

src/keystore.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,22 @@ USE_RESULT keystore_error_t keystore_create_and_store_seed(
9595
* @param[out] remaining_attempts_out will have the number of remaining attempts.
9696
* If zero, the keystore is locked until the device is reset.
9797
* @param[out] securechip_result_out, if not NULL, will contain the error code from
98+
* @param[out] seed_out The seed bytes copied from the retained seed.
99+
* The buffer should be KEYSTORE_MAX_SEED_LENGTH bytes long. The caller must
100+
* zero the seed once it is no longer needed.
101+
* @param[out] seed_len_out The seed length.
98102
* `securechip_kdf()` if there was a secure chip error, and 0 otherwise.
99103
* @return
100104
* - KEYSTORE_OK if they keystore was successfully unlocked
101105
* - KEYSTORE_ERR_* if unsuccessful.
102106
* Only call this if memory_is_seeded() returns true.
103107
*/
104-
USE_RESULT keystore_error_t
105-
keystore_unlock(const char* password, uint8_t* remaining_attempts_out, int* securechip_result_out);
108+
USE_RESULT keystore_error_t keystore_unlock(
109+
const char* password,
110+
uint8_t* remaining_attempts_out,
111+
int* securechip_result_out,
112+
uint8_t* seed_out,
113+
size_t* seed_len_out);
106114

107115
/**
108116
* Checks if bip39 unlocking can be performed. It can be performed if `keystore_unlock()`

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,15 @@ pub async fn create(
9999

100100
let is_initialized = bitbox02::memory::is_initialized();
101101

102-
if is_initialized {
103-
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?;
104-
}
105-
106-
let seed = bitbox02::keystore::copy_seed()?;
107-
108-
// Yield now to give executor a chance to process USB/BLE communication, as copy_seed() causes
109-
// some delay.
110-
futures_lite::future::yield_now().await;
102+
let seed = if is_initialized {
103+
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?
104+
} else {
105+
let seed = bitbox02::keystore::copy_seed()?;
106+
// Yield now to give executor a chance to process USB/BLE communication, as copy_seed() causes
107+
// some delay.
108+
futures_lite::future::yield_now().await;
109+
seed
110+
};
111111

112112
let seed_birthdate = if !is_initialized {
113113
if bitbox02::memory::set_seed_birthdate(timestamp).is_err() {
@@ -256,7 +256,7 @@ mod tests {
256256
)),
257257
Ok(Response::Success(pb::Success {}))
258258
);
259-
assert_eq!(bitbox02::securechip::fake_event_counter(), 6);
259+
assert_eq!(bitbox02::securechip::fake_event_counter(), 5);
260260
assert_eq!(
261261
mock_hal.ui.screens,
262262
vec![

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ use pb::response::Response;
2222
use crate::hal::Ui;
2323
use crate::workflow::{confirm, unlock};
2424

25-
use crate::keystore;
26-
2725
/// Handle the ShowMnemonic API call. This shows the seed encoded as
2826
/// 12/18/24 BIP39 English words. Afterwards, for each word, the user
2927
/// is asked to pick the right word among 5 words, to check if they
3028
/// wrote it down correctly.
3129
pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
32-
if bitbox02::memory::is_initialized() {
33-
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?;
34-
}
30+
let mnemonic_sentence = {
31+
let seed = if bitbox02::memory::is_initialized() {
32+
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?
33+
} else {
34+
bitbox02::keystore::copy_seed()?
35+
};
3536

36-
let mnemonic_sentence = keystore::get_bip39_mnemonic()?;
37+
bitbox02::keystore::bip39_mnemonic_from_seed(&seed)?
38+
};
3739

3840
hal.ui()
3941
.confirm(&confirm::Params {
@@ -152,7 +154,7 @@ mod tests {
152154
block_on(process(&mut mock_hal)),
153155
Ok(Response::Success(pb::Success {}))
154156
);
155-
assert_eq!(bitbox02::securechip::fake_event_counter(), 6);
157+
assert_eq!(bitbox02::securechip::fake_event_counter(), 5);
156158

157159
assert_eq!(
158160
mock_hal.ui.screens,

src/rust/bitbox02-rust/src/workflow/unlock.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use bitbox02::keystore;
1919

2020
pub use password::CanCancel;
2121

22+
use alloc::vec::Vec;
23+
2224
/// Confirm the entered mnemonic passphrase with the user. Returns true if the user confirmed it,
2325
/// false if the user rejected it.
2426
async fn confirm_mnemonic_passphrase(
@@ -79,7 +81,7 @@ pub async fn unlock_keystore(
7981
hal: &mut impl crate::hal::Hal,
8082
title: &str,
8183
can_cancel: password::CanCancel,
82-
) -> Result<(), UnlockError> {
84+
) -> Result<zeroize::Zeroizing<Vec<u8>>, UnlockError> {
8385
let password = password::enter(
8486
hal,
8587
title,
@@ -89,7 +91,7 @@ pub async fn unlock_keystore(
8991
.await?;
9092

9193
match keystore::unlock(&password) {
92-
Ok(()) => Ok(()),
94+
Ok(seed) => Ok(seed),
9395
Err(keystore::Error::IncorrectPassword { remaining_attempts }) => {
9496
let msg = match remaining_attempts {
9597
1 => "Wrong password\n1 try remains".into(),
@@ -171,13 +173,12 @@ pub async fn unlock(hal: &mut impl crate::hal::Hal) -> Result<(), ()> {
171173
}
172174

173175
// Loop unlock until the password is correct or the device resets.
174-
while unlock_keystore(hal, "Enter password", password::CanCancel::No)
175-
.await
176-
.is_err()
177-
{}
178-
179-
unlock_bip39(hal, &bitbox02::keystore::copy_seed()?).await;
180-
Ok(())
176+
loop {
177+
if let Ok(seed) = unlock_keystore(hal, "Enter password", password::CanCancel::No).await {
178+
unlock_bip39(hal, &seed).await;
179+
return Ok(());
180+
}
181+
}
181182
}
182183

183184
#[cfg(test)]
@@ -217,7 +218,8 @@ mod tests {
217218
}));
218219
bitbox02::securechip::fake_event_counter_reset();
219220
assert_eq!(block_on(unlock(&mut mock_hal)), Ok(()));
220-
assert_eq!(bitbox02::securechip::fake_event_counter(), 8);
221+
// 6 for keystore unlock, 1 for keystore bip39 unlock.
222+
assert_eq!(bitbox02::securechip::fake_event_counter(), 7);
221223

222224
assert!(!bitbox02::keystore::is_locked());
223225

src/rust/bitbox02/src/keystore.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ impl core::convert::From<keystore_error_t> for Error {
6767
}
6868
}
6969

70-
pub fn unlock(password: &str) -> Result<(), Error> {
70+
pub fn unlock(password: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
7171
let mut remaining_attempts: u8 = 0;
7272
let mut securechip_result: i32 = 0;
73+
let mut seed = zeroize::Zeroizing::new([0u8; MAX_SEED_LENGTH].to_vec());
74+
let mut seed_len: usize = 0;
7375
match unsafe {
7476
bitbox02_sys::keystore_unlock(
7577
crate::util::str_to_cstr_vec(password)
@@ -78,9 +80,14 @@ pub fn unlock(password: &str) -> Result<(), Error> {
7880
.cast(),
7981
&mut remaining_attempts,
8082
&mut securechip_result,
83+
seed.as_mut_ptr(),
84+
&mut seed_len,
8185
)
8286
} {
83-
keystore_error_t::KEYSTORE_OK => Ok(()),
87+
keystore_error_t::KEYSTORE_OK => {
88+
seed.truncate(seed_len);
89+
Ok(seed)
90+
}
8491
keystore_error_t::KEYSTORE_ERR_INCORRECT_PASSWORD => {
8592
Err(Error::IncorrectPassword { remaining_attempts })
8693
}
@@ -483,15 +490,15 @@ mod tests {
483490

484491
// First call: unlock. The first one does a seed rentention (1 securechip event).
485492
crate::securechip::fake_event_counter_reset();
486-
assert!(unlock("password").is_ok());
493+
assert_eq!(unlock("password").unwrap().as_slice(), seed);
487494
assert_eq!(crate::securechip::fake_event_counter(), 6);
488495

489496
// Loop to check that unlocking works while unlocked.
490497
for _ in 0..2 {
491498
// Further calls perform a password check.The password check does not do the retention
492499
// so it ends up needing one secure chip operation less.
493500
crate::securechip::fake_event_counter_reset();
494-
assert!(unlock("password").is_ok());
501+
assert_eq!(unlock("password").unwrap().as_slice(), seed);
495502
assert_eq!(crate::securechip::fake_event_counter(), 5);
496503
}
497504

@@ -630,7 +637,9 @@ mod tests {
630637
.is_err()
631638
);
632639
// Correct seed passed.
640+
crate::securechip::fake_event_counter_reset();
633641
assert!(block_on(unlock_bip39(&secp, &seed, "foo", async || {})).is_ok());
642+
assert_eq!(crate::securechip::fake_event_counter(), 1);
634643
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
635644

636645
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
@@ -769,7 +778,7 @@ mod tests {
769778

770779
// Correct password. First time: unlock. After unlock, it becomes a password check.
771780
for _ in 0..3 {
772-
assert!(unlock("foo").is_ok());
781+
assert_eq!(unlock("foo").unwrap().as_slice(), &seed[..seed_size]);
773782
}
774783
assert_eq!(copy_seed().unwrap().as_slice(), &seed[..seed_size]);
775784

0 commit comments

Comments
 (0)