Skip to content

Commit bbcdabc

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 f8c1614 commit bbcdabc

File tree

8 files changed

+57
-22
lines changed

8 files changed

+57
-22
lines changed

src/keystore.c

Lines changed: 29 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,23 @@ keystore_error_t keystore_unlock(
455472
return result;
456473
}
457474

458-
bool keystore_unlock_bip39(const char* mnemonic_passphrase, uint8_t* root_fingerprint_out)
475+
bool keystore_unlock_bip39(
476+
const uint8_t* seed,
477+
size_t seed_length,
478+
const char* mnemonic_passphrase,
479+
uint8_t* root_fingerprint_out)
459480
{
460481
if (!_is_unlocked_device) {
461482
return false;
462483
}
463484
usb_processing_timeout_reset(LONG_TIMEOUT);
464485

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)) {
486+
uint8_t seed_hashed[32] = {0};
487+
UTIL_CLEANUP_32(seed_hashed);
488+
if (_hash_seed(seed, seed_length, seed_hashed) != KEYSTORE_OK) {
489+
return false;
490+
}
491+
if (!MEMEQ(seed_hashed, _retained_seed_hash, sizeof(_retained_seed_hash))) {
469492
return false;
470493
}
471494

src/keystore.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,19 @@ 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
* @param[out] root_fingerprint_out must be 4 bytes long and will contain the root fingerprint of
111114
* the wallet.
112115
* @return returns false if there was a critital memory error, otherwise true.
113116
*/
114117
USE_RESULT bool keystore_unlock_bip39(
118+
const uint8_t* seed,
119+
size_t seed_length,
115120
const char* mnemonic_passphrase,
116121
uint8_t* root_fingerprint_out);
117122

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
@@ -573,7 +573,7 @@ mod tests {
573573
keystore::lock();
574574
let seed = &seed[..test.seed_len];
575575

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

578578
bitbox02::securechip::fake_event_counter_reset();
579579
assert!(keystore::encrypt_and_store_seed(seed, "foo").is_ok());
@@ -582,8 +582,8 @@ mod tests {
582582
assert!(keystore::is_locked());
583583

584584
bitbox02::securechip::fake_event_counter_reset();
585-
assert!(keystore::unlock_bip39(test.mnemonic_passphrase).is_ok());
586-
assert_eq!(bitbox02::securechip::fake_event_counter(), 2);
585+
assert!(keystore::unlock_bip39(seed, test.mnemonic_passphrase).is_ok());
586+
assert_eq!(bitbox02::securechip::fake_event_counter(), 1);
587587

588588
assert!(!keystore::is_locked());
589589
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
@@ -95,10 +95,12 @@ pub fn lock() {
9595
unsafe { ROOT_FINGERPRINT.write(None) }
9696
}
9797

98-
pub fn unlock_bip39(mnemonic_passphrase: &str) -> Result<(), Error> {
98+
pub fn unlock_bip39(seed: &[u8], mnemonic_passphrase: &str) -> Result<(), Error> {
9999
let mut root_fingerprint = [0u8; 4];
100100
if unsafe {
101101
bitbox02_sys::keystore_unlock_bip39(
102+
seed.as_ptr(),
103+
seed.len(),
102104
crate::util::str_to_cstr_vec(mnemonic_passphrase)
103105
.unwrap()
104106
.as_ptr()
@@ -409,7 +411,7 @@ mod tests {
409411
.unwrap();
410412
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
411413
assert!(is_locked()); // still locked, it is only unlocked after unlock_bip39.
412-
assert!(unlock_bip39("foo").is_ok());
414+
assert!(unlock_bip39(&seed, "foo").is_ok());
413415
assert!(!is_locked());
414416
lock();
415417
assert!(is_locked());
@@ -498,7 +500,10 @@ mod tests {
498500
assert!(root_fingerprint().is_err());
499501
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
500502
assert!(root_fingerprint().is_err());
501-
assert!(unlock_bip39("foo").is_ok());
503+
// Incorrect seed passed
504+
assert!(unlock_bip39(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "foo").is_err());
505+
// Correct seed passed.
506+
assert!(unlock_bip39(&seed, "foo").is_ok());
502507
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
503508

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

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)