Skip to content

Commit 031f463

Browse files
committed
keystore: pass seed to unlock_bip39() to reduce secure chip events
When restoring a wallet, the seed is already known, so no need to do another `copy_seed()` which is a secure chip security event. This reduces the number of secure chip operations when restoring. The hashed seed is retained so it can be compared without storing it in plaintext. This effort is part of mitigating Optiga's throttling mechanism that kicks in after 133 events - users can run into this by repeatedly resetting/restoring).
1 parent 9ceccb0 commit 031f463

File tree

8 files changed

+55
-23
lines changed

8 files changed

+55
-23
lines changed

src/keystore.c

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ static uint8_t _unstretched_retained_seed_encryption_key[32] = {0};
3737
// Stores the encrypted seed after unlock.
3838
static uint8_t _retained_seed_encrypted[KEYSTORE_MAX_SEED_LENGTH + 64] = {0};
3939
static size_t _retained_seed_encrypted_len = 0;
40+
// A hash of the unencrypted retained seed, used for comparing seeds without knowing their
41+
// plaintext.
42+
static uint8_t _retained_seed_hash[32] = {0};
4043

4144
// Change this ONLY via keystore_unlock_bip39().
4245
static bool _is_unlocked_bip39 = false;
@@ -224,6 +227,17 @@ static bool _verify_seed(
224227
return true;
225228
}
226229

230+
static keystore_error_t _hash_seed(const uint8_t* seed, size_t seed_len, uint8_t* out)
231+
{
232+
uint8_t salted_key[32] = {0};
233+
if (!salt_hash_data(NULL, 0, "keystore_retain_seed_hash", salted_key)) {
234+
return KEYSTORE_ERR_SALT;
235+
}
236+
237+
rust_hmac_sha256(salted_key, sizeof(salted_key), seed, seed_len, out);
238+
return KEYSTORE_OK;
239+
}
240+
227241
USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed_len)
228242
{
229243
#ifdef TESTING
@@ -253,7 +267,8 @@ USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed
253267
return KEYSTORE_ERR_ENCRYPT;
254268
}
255269
_retained_seed_encrypted_len = len;
256-
return KEYSTORE_OK;
270+
271+
return _hash_seed(seed, seed_len, _retained_seed_hash);
257272
}
258273

259274
USE_RESULT static bool _retain_bip39_seed(const uint8_t* bip39_seed)
@@ -298,6 +313,8 @@ static void _delete_retained_seeds(void)
298313
sizeof(_unstretched_retained_seed_encryption_key));
299314
util_zero(_retained_seed_encrypted, sizeof(_retained_seed_encrypted));
300315
_retained_seed_encrypted_len = 0;
316+
util_zero(_retained_seed_hash, sizeof(_retained_seed_hash));
317+
301318
util_zero(
302319
_unstretched_retained_bip39_seed_encryption_key,
303320
sizeof(_unstretched_retained_seed_encryption_key));
@@ -455,17 +472,19 @@ keystore_error_t keystore_unlock(
455472
return result;
456473
}
457474

458-
bool keystore_unlock_bip39(const char* mnemonic_passphrase)
475+
bool keystore_unlock_bip39(const uint8_t* seed, size_t seed_length, const char* mnemonic_passphrase)
459476
{
460477
if (!_is_unlocked_device) {
461478
return false;
462479
}
463480
usb_processing_timeout_reset(LONG_TIMEOUT);
464481

465-
uint8_t seed[KEYSTORE_MAX_SEED_LENGTH] = {0};
466-
UTIL_CLEANUP_32(seed);
467-
size_t seed_length = 0;
468-
if (!keystore_copy_seed(seed, &seed_length)) {
482+
uint8_t seed_hashed[32] = {0};
483+
UTIL_CLEANUP_32(seed_hashed);
484+
if (_hash_seed(seed, seed_length, seed_hashed) != KEYSTORE_OK) {
485+
return false;
486+
}
487+
if (!MEMEQ(seed_hashed, _retained_seed_hash, sizeof(_retained_seed_hash))) {
469488
return false;
470489
}
471490

src/keystore.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,18 @@ USE_RESULT keystore_error_t keystore_create_and_store_seed(
104104
USE_RESULT keystore_error_t
105105
keystore_unlock(const char* password, uint8_t* remaining_attempts_out, int* securechip_result_out);
106106

107-
/** Unlocks the bip39 seed.
107+
/** Unlocks the bip39 seed. The input seed must be the keystore seed (i.e. must match the output
108+
* of `keystore_copy_seed()`).
109+
* @param[in] seed the input seed to BIP39.
110+
* @param[in] seed_length the size of the seed
108111
* @param[in] mnemonic_passphrase bip39 passphrase used in the derivation. Use the
109112
* empty string if no passphrase is needed or provided.
110113
* @return returns false if there was a critital memory error, otherwise true.
111114
*/
112-
USE_RESULT bool keystore_unlock_bip39(const char* mnemonic_passphrase);
115+
USE_RESULT bool keystore_unlock_bip39(
116+
const uint8_t* seed,
117+
size_t seed_length,
118+
const char* mnemonic_passphrase);
113119

114120
/**
115121
* Locks the keystore (resets to state before `keystore_unlock()`).

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ pub async fn from_file(
6565
}
6666

6767
let password = password::enter_twice(hal).await?;
68-
if let Err(err) = bitbox02::keystore::encrypt_and_store_seed(data.get_seed(), &password) {
68+
let seed = data.get_seed();
69+
if let Err(err) = bitbox02::keystore::encrypt_and_store_seed(seed, &password) {
6970
hal.ui()
7071
.status(&format!("Could not\nrestore backup\n{:?}", err), false)
7172
.await;
@@ -87,7 +88,7 @@ pub async fn from_file(
8788
// Ignore non-critical error.
8889
let _ = bitbox02::memory::set_device_name(&metadata.name);
8990

90-
unlock::unlock_bip39(hal).await;
91+
unlock::unlock_bip39(hal, seed).await;
9192
Ok(Response::Success(pb::Success {}))
9293
}
9394

@@ -157,7 +158,7 @@ pub async fn from_mnemonic(
157158

158159
bitbox02::memory::set_initialized().or(Err(Error::Memory))?;
159160

160-
unlock::unlock_bip39(hal).await;
161+
unlock::unlock_bip39(hal, &seed).await;
161162
Ok(Response::Success(pb::Success {}))
162163
}
163164

@@ -199,7 +200,7 @@ mod tests {
199200
)),
200201
Ok(Response::Success(pb::Success {}))
201202
);
202-
assert_eq!(bitbox02::securechip::fake_event_counter(), 14);
203+
assert_eq!(bitbox02::securechip::fake_event_counter(), 13);
203204
drop(mock_hal); // to remove mutable borrow of counter
204205
assert_eq!(counter, 2);
205206
assert!(!keystore::is_locked());

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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).await;
43+
unlock::unlock_bip39(hal, &keystore::copy_seed()?).await;
4444
Ok(Response::Success(pb::Success {}))
4545
}
4646

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ mod tests {
453453
keystore::lock();
454454
let seed = &seed[..test.seed_len];
455455

456-
assert!(keystore::unlock_bip39(test.mnemonic_passphrase).is_err());
456+
assert!(keystore::unlock_bip39(seed, test.mnemonic_passphrase).is_err());
457457

458458
bitbox02::securechip::fake_event_counter_reset();
459459
assert!(keystore::encrypt_and_store_seed(seed, "foo").is_ok());
@@ -462,8 +462,8 @@ mod tests {
462462
assert!(keystore::is_locked());
463463

464464
bitbox02::securechip::fake_event_counter_reset();
465-
assert!(keystore::unlock_bip39(test.mnemonic_passphrase).is_ok());
466-
assert_eq!(bitbox02::securechip::fake_event_counter(), 2);
465+
assert!(keystore::unlock_bip39(seed, test.mnemonic_passphrase).is_ok());
466+
assert_eq!(bitbox02::securechip::fake_event_counter(), 1);
467467

468468
assert!(!keystore::is_locked());
469469
assert_eq!(

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub async fn unlock_keystore(
108108

109109
/// Performs the BIP39 keystore unlock, including unlock animation. If the optional passphrase
110110
/// feature is enabled, the user will be asked for the passphrase.
111-
pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal) {
111+
pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal, seed: &[u8]) {
112112
// Empty passphrase by default.
113113
let mut mnemonic_passphrase = zeroize::Zeroizing::new("".into());
114114

@@ -133,7 +133,8 @@ pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal) {
133133
}
134134
}
135135

136-
let result = bitbox02::ui::with_lock_animation(|| keystore::unlock_bip39(&mnemonic_passphrase));
136+
let result =
137+
bitbox02::ui::with_lock_animation(|| keystore::unlock_bip39(seed, &mnemonic_passphrase));
137138
if result.is_err() {
138139
abort("bip39 unlock failed");
139140
}
@@ -160,6 +161,6 @@ pub async fn unlock(hal: &mut impl crate::hal::Hal) -> Result<(), ()> {
160161
.is_err()
161162
{}
162163

163-
unlock_bip39(hal).await;
164+
unlock_bip39(hal, &bitbox02::keystore::copy_seed()?).await;
164165
Ok(())
165166
}

src/rust/bitbox02/src/keystore.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,11 @@ pub fn lock() {
8989
unsafe { bitbox02_sys::keystore_lock() }
9090
}
9191

92-
pub fn unlock_bip39(mnemonic_passphrase: &str) -> Result<(), Error> {
92+
pub fn unlock_bip39(seed: &[u8], mnemonic_passphrase: &str) -> Result<(), Error> {
9393
if unsafe {
9494
bitbox02_sys::keystore_unlock_bip39(
95+
seed.as_ptr(),
96+
seed.len(),
9597
crate::util::str_to_cstr_vec(mnemonic_passphrase)
9698
.unwrap()
9799
.as_ptr()
@@ -399,7 +401,7 @@ mod tests {
399401
.unwrap();
400402
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
401403
assert!(is_locked()); // still locked, it is only unlocked after unlock_bip39.
402-
assert!(unlock_bip39("foo").is_ok());
404+
assert!(unlock_bip39(&seed, "foo").is_ok());
403405
assert!(!is_locked());
404406
lock();
405407
assert!(is_locked());
@@ -486,7 +488,10 @@ mod tests {
486488
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
487489

488490
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
489-
assert!(unlock_bip39("foo").is_ok());
491+
// Incorrect seed passed
492+
assert!(unlock_bip39(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "foo").is_err());
493+
// Correct seed passed.
494+
assert!(unlock_bip39(&seed, "foo").is_ok());
490495

491496
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
492497

src/rust/bitbox02/src/testing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub fn mock_unlocked_using_mnemonic(mnemonic: &str, passphrase: &str) {
2222
unsafe {
2323
bitbox02_sys::keystore_mock_unlocked(seed.as_ptr(), seed.len() as _, core::ptr::null())
2424
}
25-
keystore::unlock_bip39(passphrase).unwrap();
25+
keystore::unlock_bip39(&seed, passphrase).unwrap();
2626
}
2727

2828
pub const TEST_MNEMONIC: &str = "purity concert above invest pigeon category peace tuition hazard vivid latin since legal speak nation session onion library travel spell region blast estate stay";

0 commit comments

Comments
 (0)