Skip to content

Commit a670560

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 d96c98c commit a670560

File tree

6 files changed

+59
-30
lines changed

6 files changed

+59
-30
lines changed

src/keystore.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,9 @@ static bool _check_retained_seed(const uint8_t* seed, size_t seed_length)
442442
keystore_error_t keystore_unlock(
443443
const char* password,
444444
uint8_t* remaining_attempts_out,
445-
int* securechip_result_out)
445+
int* securechip_result_out,
446+
uint8_t* seed_out,
447+
size_t* seed_len_out)
446448
{
447449
if (!memory_is_seeded()) {
448450
return KEYSTORE_ERR_UNSEEDED;
@@ -483,6 +485,11 @@ keystore_error_t keystore_unlock(
483485
_is_unlocked_device = true;
484486
}
485487
bitbox02_smarteeprom_reset_unlock_attempts();
488+
489+
if (seed_out != NULL && seed_len_out != NULL) {
490+
memcpy(seed_out, seed, seed_len);
491+
*seed_len_out = seed_len;
492+
}
486493
}
487494
// Compute remaining attempts
488495
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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ 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-
}
102+
let seed = if is_initialized {
103+
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?
104+
} else {
105+
bitbox02::keystore::copy_seed()?
106+
};
105107

106-
let seed = bitbox02::keystore::copy_seed()?;
107108
let seed_birthdate = if !is_initialized {
108109
if bitbox02::memory::set_seed_birthdate(timestamp).is_err() {
109110
return Err(Error::Memory);
@@ -249,7 +250,7 @@ mod tests {
249250
)),
250251
Ok(Response::Success(pb::Success {}))
251252
);
252-
assert_eq!(bitbox02::securechip::fake_event_counter(), 6);
253+
assert_eq!(bitbox02::securechip::fake_event_counter(), 5);
253254
assert_eq!(
254255
mock_hal.ui.screens,
255256
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(),
@@ -157,13 +159,12 @@ pub async fn unlock(hal: &mut impl crate::hal::Hal) -> Result<(), ()> {
157159
}
158160

159161
// Loop unlock until the password is correct or the device resets.
160-
while unlock_keystore(hal, "Enter password", password::CanCancel::No)
161-
.await
162-
.is_err()
163-
{}
164-
165-
unlock_bip39(hal, &bitbox02::keystore::copy_seed()?).await;
166-
Ok(())
162+
loop {
163+
if let Ok(seed) = unlock_keystore(hal, "Enter password", password::CanCancel::No).await {
164+
unlock_bip39(hal, &seed).await;
165+
return Ok(());
166+
}
167+
}
167168
}
168169

169170
#[cfg(test)]
@@ -203,7 +204,8 @@ mod tests {
203204
}));
204205
bitbox02::securechip::fake_event_counter_reset();
205206
assert_eq!(block_on(unlock(&mut mock_hal)), Ok(()));
206-
assert_eq!(bitbox02::securechip::fake_event_counter(), 8);
207+
// 6 for keystore unlock, 1 for keystore bip39 unlock.
208+
assert_eq!(bitbox02::securechip::fake_event_counter(), 7);
207209

208210
assert!(!bitbox02::keystore::is_locked());
209211

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
}
@@ -468,15 +475,15 @@ mod tests {
468475

469476
// First call: unlock. The first one does a seed rentention (1 securechip event).
470477
crate::securechip::fake_event_counter_reset();
471-
assert!(unlock("password").is_ok());
478+
assert_eq!(unlock("password").unwrap().as_slice(), seed);
472479
assert_eq!(crate::securechip::fake_event_counter(), 6);
473480

474481
// Loop to check that unlocking works while unlocked.
475482
for _ in 0..2 {
476483
// Further calls perform a password check.The password check does not do the retention
477484
// so it ends up needing one secure chip operation less.
478485
crate::securechip::fake_event_counter_reset();
479-
assert!(unlock("password").is_ok());
486+
assert_eq!(unlock("password").unwrap().as_slice(), seed);
480487
assert_eq!(crate::securechip::fake_event_counter(), 5);
481488
}
482489

@@ -602,7 +609,9 @@ mod tests {
602609
// Incorrect seed passed
603610
assert!(unlock_bip39(&secp, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "foo").is_err());
604611
// Correct seed passed.
612+
crate::securechip::fake_event_counter_reset();
605613
assert!(unlock_bip39(&secp, &seed, "foo").is_ok());
614+
assert_eq!(crate::securechip::fake_event_counter(), 1);
606615
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
607616

608617
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
@@ -741,7 +750,7 @@ mod tests {
741750

742751
// Correct password. First time: unlock. After unlock, it becomes a password check.
743752
for _ in 0..3 {
744-
assert!(unlock("foo").is_ok());
753+
assert_eq!(unlock("foo").unwrap().as_slice(), &seed[..seed_size]);
745754
}
746755
assert_eq!(copy_seed().unwrap().as_slice(), &seed[..seed_size]);
747756

0 commit comments

Comments
 (0)