Skip to content

Commit 9ceccb0

Browse files
committed
reduce number of secure chip security events when creating/restoring
Before, `keystore_encrypt_and_store_seed()` (called when creating/restoring a seed) would always be followed by `keystore_unlock(<password>)` with the password the user just chose, so unlock could never fail. The unlocking part costs many secure chip operations (for stretching the password). By making the first function already unlock the keystore, we can avoid calling `keystore_unlock()`, reducing the number of secure chip events by 5. 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 5e7381e commit 9ceccb0

File tree

6 files changed

+100
-110
lines changed

6 files changed

+100
-110
lines changed

src/keystore.c

Lines changed: 87 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -224,86 +224,6 @@ static bool _verify_seed(
224224
return true;
225225
}
226226

227-
keystore_error_t keystore_encrypt_and_store_seed(
228-
const uint8_t* seed,
229-
size_t seed_length,
230-
const char* password)
231-
{
232-
if (memory_is_initialized()) {
233-
return KEYSTORE_ERR_MEMORY;
234-
}
235-
keystore_lock();
236-
if (!_validate_seed_length(seed_length)) {
237-
return KEYSTORE_ERR_SEED_SIZE;
238-
}
239-
if (securechip_init_new_password(password)) {
240-
return KEYSTORE_ERR_SECURECHIP;
241-
}
242-
uint8_t secret[32] = {0};
243-
UTIL_CLEANUP_32(secret);
244-
if (securechip_stretch_password(password, secret)) {
245-
return KEYSTORE_ERR_SECURECHIP;
246-
}
247-
248-
size_t encrypted_seed_len = seed_length + 64;
249-
uint8_t encrypted_seed[encrypted_seed_len];
250-
UTIL_CLEANUP_32(encrypted_seed);
251-
if (!cipher_aes_hmac_encrypt(seed, seed_length, encrypted_seed, &encrypted_seed_len, secret)) {
252-
return KEYSTORE_ERR_ENCRYPT;
253-
}
254-
if (encrypted_seed_len > 255) { // sanity check, can't happen
255-
Abort("keystore_encrypt_and_store_seed");
256-
}
257-
uint8_t encrypted_seed_len_u8 = (uint8_t)encrypted_seed_len;
258-
if (!memory_set_encrypted_seed_and_hmac(encrypted_seed, encrypted_seed_len_u8)) {
259-
return KEYSTORE_ERR_MEMORY;
260-
}
261-
if (!_verify_seed(password, seed, seed_length)) {
262-
if (!memory_reset_hww()) {
263-
return KEYSTORE_ERR_MEMORY;
264-
}
265-
return KEYSTORE_ERR_MEMORY;
266-
}
267-
return KEYSTORE_OK;
268-
}
269-
270-
keystore_error_t keystore_create_and_store_seed(
271-
const char* password,
272-
const uint8_t* host_entropy,
273-
size_t host_entropy_size)
274-
{
275-
if (host_entropy_size != 16 && host_entropy_size != 32) {
276-
return KEYSTORE_ERR_SEED_SIZE;
277-
}
278-
if (KEYSTORE_MAX_SEED_LENGTH != RANDOM_NUM_SIZE) {
279-
Abort("keystore create: size mismatch");
280-
}
281-
uint8_t seed[KEYSTORE_MAX_SEED_LENGTH];
282-
UTIL_CLEANUP_32(seed);
283-
random_32_bytes(seed);
284-
285-
// Mix in Host entropy.
286-
for (size_t i = 0; i < host_entropy_size; i++) {
287-
seed[i] ^= host_entropy[i];
288-
}
289-
290-
// Mix in entropy derived from the user password.
291-
uint8_t password_salted_hashed[KEYSTORE_MAX_SEED_LENGTH] = {0};
292-
UTIL_CLEANUP_32(password_salted_hashed);
293-
if (!salt_hash_data(
294-
(const uint8_t*)password,
295-
strlen(password),
296-
"keystore_seed_generation",
297-
password_salted_hashed)) {
298-
return KEYSTORE_ERR_SALT;
299-
}
300-
301-
for (size_t i = 0; i < host_entropy_size; i++) {
302-
seed[i] ^= password_salted_hashed[i];
303-
}
304-
return keystore_encrypt_and_store_seed(seed, host_entropy_size, password);
305-
}
306-
307227
USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed_len)
308228
{
309229
#ifdef TESTING
@@ -385,6 +305,93 @@ static void _delete_retained_seeds(void)
385305
_retained_bip39_seed_encrypted_len = 0;
386306
}
387307

308+
keystore_error_t keystore_encrypt_and_store_seed(
309+
const uint8_t* seed,
310+
size_t seed_length,
311+
const char* password)
312+
{
313+
if (memory_is_initialized()) {
314+
return KEYSTORE_ERR_MEMORY;
315+
}
316+
keystore_lock();
317+
if (!_validate_seed_length(seed_length)) {
318+
return KEYSTORE_ERR_SEED_SIZE;
319+
}
320+
if (securechip_init_new_password(password)) {
321+
return KEYSTORE_ERR_SECURECHIP;
322+
}
323+
uint8_t secret[32] = {0};
324+
UTIL_CLEANUP_32(secret);
325+
if (securechip_stretch_password(password, secret)) {
326+
return KEYSTORE_ERR_SECURECHIP;
327+
}
328+
329+
size_t encrypted_seed_len = seed_length + 64;
330+
uint8_t encrypted_seed[encrypted_seed_len];
331+
UTIL_CLEANUP_32(encrypted_seed);
332+
if (!cipher_aes_hmac_encrypt(seed, seed_length, encrypted_seed, &encrypted_seed_len, secret)) {
333+
return KEYSTORE_ERR_ENCRYPT;
334+
}
335+
if (encrypted_seed_len > 255) { // sanity check, can't happen
336+
Abort("keystore_encrypt_and_store_seed");
337+
}
338+
uint8_t encrypted_seed_len_u8 = (uint8_t)encrypted_seed_len;
339+
if (!memory_set_encrypted_seed_and_hmac(encrypted_seed, encrypted_seed_len_u8)) {
340+
return KEYSTORE_ERR_MEMORY;
341+
}
342+
if (!_verify_seed(password, seed, seed_length)) {
343+
if (!memory_reset_hww()) {
344+
return KEYSTORE_ERR_MEMORY;
345+
}
346+
return KEYSTORE_ERR_MEMORY;
347+
}
348+
349+
keystore_error_t retain_seed_result = _retain_seed(seed, seed_length);
350+
if (retain_seed_result != KEYSTORE_OK) {
351+
return retain_seed_result;
352+
}
353+
_is_unlocked_device = true;
354+
355+
return KEYSTORE_OK;
356+
}
357+
358+
keystore_error_t keystore_create_and_store_seed(
359+
const char* password,
360+
const uint8_t* host_entropy,
361+
size_t host_entropy_size)
362+
{
363+
if (host_entropy_size != 16 && host_entropy_size != 32) {
364+
return KEYSTORE_ERR_SEED_SIZE;
365+
}
366+
if (KEYSTORE_MAX_SEED_LENGTH != RANDOM_NUM_SIZE) {
367+
Abort("keystore create: size mismatch");
368+
}
369+
uint8_t seed[KEYSTORE_MAX_SEED_LENGTH];
370+
UTIL_CLEANUP_32(seed);
371+
random_32_bytes(seed);
372+
373+
// Mix in Host entropy.
374+
for (size_t i = 0; i < host_entropy_size; i++) {
375+
seed[i] ^= host_entropy[i];
376+
}
377+
378+
// Mix in entropy derived from the user password.
379+
uint8_t password_salted_hashed[KEYSTORE_MAX_SEED_LENGTH] = {0};
380+
UTIL_CLEANUP_32(password_salted_hashed);
381+
if (!salt_hash_data(
382+
(const uint8_t*)password,
383+
strlen(password),
384+
"keystore_seed_generation",
385+
password_salted_hashed)) {
386+
return KEYSTORE_ERR_SALT;
387+
}
388+
389+
for (size_t i = 0; i < host_entropy_size; i++) {
390+
seed[i] ^= password_salted_hashed[i];
391+
}
392+
return keystore_encrypt_and_store_seed(seed, host_entropy_size, password);
393+
}
394+
388395
keystore_error_t keystore_unlock(
389396
const char* password,
390397
uint8_t* remaining_attempts_out,

src/keystore.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ USE_RESULT bool keystore_copy_seed(uint8_t* seed_out, size_t* length_out);
6363
USE_RESULT bool keystore_copy_bip39_seed(uint8_t* bip32_seed_out);
6464

6565
/**
66-
* Restores a seed.
66+
* Restores a seed. This also unlocks the keystore with this seed.
6767
* @param[in] seed The seed that is to be restored.
6868
* @param[in] seed_length The length of the seed (max. 32 bytes).
6969
* @param[in] password The password with which we encrypt the seed.
@@ -75,6 +75,7 @@ keystore_encrypt_and_store_seed(const uint8_t* seed, size_t seed_length, const c
7575
Generates the seed, mixes it with host_entropy, and stores it encrypted with the
7676
password. The size of the host entropy determines the size of the seed. Can be either 16 or 32
7777
bytes, resulting in 12 or 24 BIP39 recovery words.
78+
This also unlocks the keystore with the new seed.
7879
@param[in] host_entropy bytes of entropy to be mixed in.
7980
@param[in] host_entropy_size must be 16 or 32.
8081
*/

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use crate::pb;
1717

1818
use pb::response::Response;
1919

20-
use crate::general::abort;
2120
use crate::hal::Ui;
2221
use crate::workflow::{confirm, mnemonic, password, unlock};
2322

@@ -84,9 +83,6 @@ pub async fn from_file(
8483
}
8584

8685
bitbox02::memory::set_initialized().or(Err(Error::Memory))?;
87-
if bitbox02::keystore::unlock(&password).is_err() {
88-
abort("restore_from_file: unlock failed");
89-
};
9086

9187
// Ignore non-critical error.
9288
let _ = bitbox02::memory::set_device_name(&metadata.name);
@@ -160,10 +156,6 @@ pub async fn from_mnemonic(
160156
}
161157

162158
bitbox02::memory::set_initialized().or(Err(Error::Memory))?;
163-
// This should never fail.
164-
if bitbox02::keystore::unlock(&password).is_err() {
165-
abort("restore_from_mnemonic: unlock failed");
166-
};
167159

168160
unlock::unlock_bip39(hal).await;
169161
Ok(Response::Success(pb::Success {}))
@@ -207,7 +199,7 @@ mod tests {
207199
)),
208200
Ok(Response::Success(pb::Success {}))
209201
);
210-
assert_eq!(bitbox02::securechip::fake_event_counter(), 19);
202+
assert_eq!(bitbox02::securechip::fake_event_counter(), 14);
211203
drop(mock_hal); // to remove mutable borrow of counter
212204
assert_eq!(counter, 2);
213205
assert!(!keystore::is_locked());

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ pub async fn process(
4040
hal.ui().status(&format!("Error\n{:?}", err), false).await;
4141
return Err(Error::Generic);
4242
}
43-
if keystore::unlock(&password).is_err() {
44-
panic!("Unexpected error during restore: unlock failed.");
45-
}
4643
unlock::unlock_bip39(hal).await;
4744
Ok(Response::Success(pb::Success {}))
4845
}
@@ -83,7 +80,7 @@ mod tests {
8380
)),
8481
Ok(Response::Success(pb::Success {}))
8582
);
86-
assert_eq!(bitbox02::securechip::fake_event_counter(), 19);
83+
assert_eq!(bitbox02::securechip::fake_event_counter(), 14);
8784
drop(mock_hal); // to remove mutable borrow of counter
8885
assert_eq!(counter, 2);
8986
assert!(!keystore::is_locked());

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -457,14 +457,7 @@ mod tests {
457457

458458
bitbox02::securechip::fake_event_counter_reset();
459459
assert!(keystore::encrypt_and_store_seed(seed, "foo").is_ok());
460-
assert_eq!(bitbox02::securechip::fake_event_counter(), 11);
461-
462-
assert!(keystore::unlock_bip39(test.mnemonic_passphrase).is_err());
463-
assert!(keystore::is_locked());
464-
465-
bitbox02::securechip::fake_event_counter_reset();
466-
assert!(keystore::unlock("foo").is_ok());
467-
assert_eq!(bitbox02::securechip::fake_event_counter(), 6);
460+
assert_eq!(bitbox02::securechip::fake_event_counter(), 12);
468461

469462
assert!(keystore::is_locked());
470463

src/rust/bitbox02/src/keystore.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,6 @@ mod tests {
398398
let seed = hex::decode("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044")
399399
.unwrap();
400400
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
401-
assert!(unlock("password").is_ok());
402401
assert!(is_locked()); // still locked, it is only unlocked after unlock_bip39.
403402
assert!(unlock_bip39("foo").is_ok());
404403
assert!(!is_locked());
@@ -422,6 +421,7 @@ mod tests {
422421
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
423422

424423
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
424+
lock();
425425

426426
// Loop to check that unlocking works while unlocked.
427427
for _ in 0..3 {
@@ -486,7 +486,6 @@ mod tests {
486486
crate::memory::set_salt_root(mock_salt_root.as_slice().try_into().unwrap()).unwrap();
487487

488488
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
489-
assert!(unlock("password").is_ok());
490489
assert!(unlock_bip39("foo").is_ok());
491490

492491
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
@@ -559,7 +558,6 @@ mod tests {
559558
lock();
560559

561560
assert!(create_and_store_seed("password", &host_entropy[..size]).is_ok());
562-
assert!(unlock("password").is_ok());
563561
assert_eq!(copy_seed().unwrap().as_slice(), &expected_seed[..size]);
564562
// Check the seed has been stored encrypted with the expected encryption key.
565563
// Decrypt and check seed.
@@ -590,14 +588,12 @@ mod tests {
590588
let seed2 = hex::decode("c28135734876aff9ccf4f1d60df8d19a0a38fd02085883f65fc608eb769a635d")
591589
.unwrap();
592590
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
593-
assert!(unlock("password").is_ok());
594591
// Create new (different) seed.
595592
assert!(encrypt_and_store_seed(&seed2, "password").is_ok());
596-
assert!(unlock("password").is_ok());
597593
assert_eq!(copy_seed().unwrap().as_slice(), &seed2);
598594
}
599595

600-
// Functional test to store seeds, unlock, retrieve seed.
596+
// Functional test to store seeds, lock/unlock, retrieve seed.
601597
#[test]
602598
fn test_seeds() {
603599
let seed = hex::decode("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044")
@@ -611,6 +607,12 @@ mod tests {
611607
for _ in 0..2 {
612608
assert!(encrypt_and_store_seed(&seed[..seed_size], "foo").is_ok());
613609
}
610+
// Also unlocks, so we can get the retained seed.
611+
assert_eq!(copy_seed().unwrap().as_slice(), &seed[..seed_size]);
612+
613+
lock();
614+
// Can't get seed before unlock.
615+
assert!(copy_seed().is_err());
614616

615617
// Wrong password.
616618
assert!(matches!(
@@ -620,8 +622,6 @@ mod tests {
620622
})
621623
));
622624

623-
// Can't get seed before unlock.
624-
assert!(copy_seed().is_err());
625625
// Correct password. First time: unlock. After unlock, it becomes a password check.
626626
for _ in 0..3 {
627627
assert!(unlock("foo").is_ok());

0 commit comments

Comments
 (0)