Skip to content

Commit be929a5

Browse files
committed
keystore: encrypt seed in RAM
Until now, after keystore_unlock(), the seed was present in RAM in plaintext. We instead keep the seed in RAM in encrypted form. The enryption key is stretched using the secure chip, much like the device unlock password itself is stretched to encrypt the seed for storage in flash memory. The idea is to keep the decrypted seed in memory for the least amount of time possible, so: decrypt, use, discard. This reduces the attack surface and mitigates potential attacks where the RAM could be accessed by an attacker. The unstretched key in this case is not the device password, but a randomly generated key stored in RAM. We cannot use the device password as we want to be able to decrypt the seed on demand without prompting the user for the password. We use the KDF secure chip slot. It is not attached to the monotonic counter, so it can be used without limitations. One KDF key stretch (used in encryption and decryption) takes around 100ms. Alternatives discarded: - Use a data slot on the securechip to store the encryption key. Surprisingly, an encrypted read of this slot is slower than KDF, likely because an encrypted read, in addition to an auth call (consisting of NONCE an CHECKMAC commands), do a READ, NONCE, GENDIG and again READ, while KDF after auth only is one KDF call. Possibly the many roundtrips to the chip make it slower overall. - Use SHA instead of KDF: our secure chip configuration does not permit SHA operations. This commit performs encryption and on-demand decryption for the seed. The next commit will do the same for the bip39 seed. The changed backup.rs unit test is to move mock_unlocked() before mock_memory(), as mock_memory() overwrites the salt root, which is needed in the key stretching. The changed signtx.rs unit test is to move random::mock_reset() after mock_unlocked(), as the P2TR signature uses random_32_bytes(), which in unit tests is a deterministic PRNG, which was influenced by calling random_32_bytes() when creating the seed encryption key in mock_unlocked().
1 parent 4aea522 commit be929a5

File tree

8 files changed

+160
-36
lines changed

8 files changed

+160
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
77
## Firmware
88

99
### [Unreleased]
10+
- Improved security: keep seed encrypted in RAM
1011

1112
### 9.14.0
1213
- Improved touch button positional accuracy in noisy environments

CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ endif()
8888
#
8989
# Versions MUST contain three parts and start with lowercase 'v'.
9090
# Example 'v1.0.0'. They MUST not contain a pre-release label such as '-beta'.
91-
set(FIRMWARE_VERSION "v9.14.0")
92-
set(FIRMWARE_BTC_ONLY_VERSION "v9.14.0")
91+
set(FIRMWARE_VERSION "v9.14.1")
92+
set(FIRMWARE_BTC_ONLY_VERSION "v9.14.1")
9393
set(BOOTLOADER_VERSION "v1.0.5")
9494

9595
find_package(PythonInterp 3.6 REQUIRED)

src/keystore.c

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,10 @@
3636

3737
// Change this ONLY via keystore_unlock() or keystore_lock()
3838
static bool _is_unlocked_device = false;
39-
// Must be defined if is_unlocked is true. Length of the seed store in `_retained_seed`. See also:
40-
// `_validate_seed_length()`.
41-
static size_t _seed_length = 0;
39+
static uint8_t _unstretched_retained_seed_encryption_key[32] = {0};
4240
// Must be defined if is_unlocked is true. ONLY ACCESS THIS WITH keystore_copy_seed().
43-
static uint8_t _retained_seed[KEYSTORE_MAX_SEED_LENGTH] = {0};
41+
static uint8_t _retained_seed_encrypted[KEYSTORE_MAX_SEED_LENGTH + 64] = {0};
42+
static size_t _retained_seed_encrypted_len = 0;
4443

4544
// Change this ONLY via keystore_unlock_bip39().
4645
static bool _is_unlocked_bip39 = false;
@@ -55,13 +54,56 @@ static bool _validate_seed_length(size_t seed_len)
5554
return seed_len == 16 || seed_len == 24 || seed_len == 32;
5655
}
5756

57+
USE_RESULT static keystore_error_t _stretch_retained_seed_encryption_key(
58+
const uint8_t* encryption_key,
59+
const char* purpose_in,
60+
const char* purpose_out,
61+
uint8_t* out)
62+
{
63+
uint8_t salted_hashed[32] = {0};
64+
UTIL_CLEANUP_32(salted_hashed);
65+
if (!salt_hash_data(encryption_key, 32, purpose_in, salted_hashed)) {
66+
return KEYSTORE_ERR_SALT;
67+
}
68+
if (securechip_kdf(SECURECHIP_SLOT_KDF, salted_hashed, 32, out)) {
69+
return KEYSTORE_ERR_SECURECHIP;
70+
}
71+
if (!salt_hash_data(encryption_key, 32, purpose_out, salted_hashed)) {
72+
return KEYSTORE_ERR_SALT;
73+
}
74+
if (wally_hmac_sha256(salted_hashed, sizeof(salted_hashed), out, 32, out, 32) != WALLY_OK) {
75+
return KEYSTORE_ERR_HASH;
76+
}
77+
return KEYSTORE_OK;
78+
}
79+
5880
bool keystore_copy_seed(uint8_t* seed_out, size_t* length_out)
5981
{
6082
if (!_is_unlocked_device) {
6183
return false;
6284
}
63-
memcpy(seed_out, _retained_seed, _seed_length);
64-
*length_out = _seed_length;
85+
86+
uint8_t retained_seed_encryption_key[32] = {0};
87+
UTIL_CLEANUP_32(retained_seed_encryption_key);
88+
if (_stretch_retained_seed_encryption_key(
89+
_unstretched_retained_seed_encryption_key,
90+
"keystore_retained_seed_access_in",
91+
"keystore_retained_seed_access_out",
92+
retained_seed_encryption_key) != KEYSTORE_OK) {
93+
return false;
94+
}
95+
size_t len = _retained_seed_encrypted_len - 48;
96+
bool password_correct = cipher_aes_hmac_decrypt(
97+
_retained_seed_encrypted,
98+
_retained_seed_encrypted_len,
99+
seed_out,
100+
&len,
101+
retained_seed_encryption_key);
102+
if (!password_correct) {
103+
// Should never happen.
104+
return false;
105+
}
106+
*length_out = len;
65107
return true;
66108
}
67109

@@ -304,10 +346,26 @@ static void _free_string(char** str)
304346
wally_free_string(*str);
305347
}
306348

307-
static void _retain_seed(const uint8_t* seed, size_t seed_len)
349+
USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed_len)
308350
{
309-
memcpy(_retained_seed, seed, seed_len);
310-
_seed_length = seed_len;
351+
random_32_bytes(_unstretched_retained_seed_encryption_key);
352+
uint8_t retained_seed_encryption_key[32] = {0};
353+
UTIL_CLEANUP_32(retained_seed_encryption_key);
354+
keystore_error_t result = _stretch_retained_seed_encryption_key(
355+
_unstretched_retained_seed_encryption_key,
356+
"keystore_retained_seed_access_in",
357+
"keystore_retained_seed_access_out",
358+
retained_seed_encryption_key);
359+
if (result != KEYSTORE_OK) {
360+
return result;
361+
}
362+
size_t len = seed_len + 64;
363+
if (!cipher_aes_hmac_encrypt(
364+
seed, seed_len, _retained_seed_encrypted, &len, retained_seed_encryption_key)) {
365+
return KEYSTORE_ERR_ENCRYPT;
366+
}
367+
_retained_seed_encrypted_len = len;
368+
return KEYSTORE_OK;
311369
}
312370

313371
USE_RESULT static bool _retain_bip39_seed(const uint8_t* bip39_seed)
@@ -318,8 +376,10 @@ USE_RESULT static bool _retain_bip39_seed(const uint8_t* bip39_seed)
318376

319377
static void _delete_retained_seeds(void)
320378
{
321-
_seed_length = 0;
322-
util_zero(_retained_seed, sizeof(_retained_seed));
379+
util_zero(
380+
_unstretched_retained_seed_encryption_key,
381+
sizeof(_unstretched_retained_seed_encryption_key));
382+
util_zero(_retained_seed_encrypted, sizeof(_retained_seed_encrypted));
323383
util_zero(_retained_bip39_seed, sizeof(_retained_bip39_seed));
324384
}
325385

@@ -354,11 +414,19 @@ keystore_error_t keystore_unlock(
354414
if (result == KEYSTORE_OK) {
355415
if (_is_unlocked_device) {
356416
// Already unlocked. Fail if the seed changed under our feet (should never happen).
357-
if (seed_len != _seed_length || !MEMEQ(_retained_seed, seed, _seed_length)) {
417+
uint8_t current_seed[KEYSTORE_MAX_SEED_LENGTH] = {0};
418+
size_t current_seed_len = 0;
419+
if (!keystore_copy_seed(current_seed, &current_seed_len)) {
420+
return KEYSTORE_ERR_DECRYPT;
421+
}
422+
if (seed_len != current_seed_len || !MEMEQ(current_seed, seed, current_seed_len)) {
358423
Abort("Seed has suddenly changed. This should never happen.");
359424
}
360425
} else {
361-
_retain_seed(seed, seed_len);
426+
keystore_error_t retain_seed_result = _retain_seed(seed, seed_len);
427+
if (retain_seed_result != KEYSTORE_OK) {
428+
return retain_seed_result;
429+
}
362430
_is_unlocked_device = true;
363431
}
364432
bitbox02_smarteeprom_reset_unlock_attempts();
@@ -798,11 +866,19 @@ void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t*
798866
{
799867
_is_unlocked_device = seed != NULL;
800868
if (seed != NULL) {
801-
_retain_seed(seed, seed_len);
869+
if (_retain_seed(seed, seed_len) != KEYSTORE_OK) {
870+
Abort("couldn't retain seed");
871+
}
802872
}
803873
_is_unlocked_bip39 = bip39_seed != NULL;
804874
if (bip39_seed != NULL) {
805875
memcpy(_retained_bip39_seed, bip39_seed, sizeof(_retained_bip39_seed));
806876
}
807877
}
878+
879+
const uint8_t* keystore_test_get_retained_seed_encrypted(size_t* len_out)
880+
{
881+
*len_out = _retained_seed_encrypted_len;
882+
return _retained_seed_encrypted;
883+
}
808884
#endif

src/keystore.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ typedef enum {
4343
KEYSTORE_ERR_SALT,
4444
KEYSTORE_ERR_HASH,
4545
KEYSTORE_ERR_ENCRYPT,
46+
KEYSTORE_ERR_DECRYPT,
4647
} keystore_error_t;
4748

4849
/**
@@ -282,6 +283,8 @@ USE_RESULT bool keystore_secp256k1_schnorr_bip86_sign(
282283
* convenience to mock the keystore state (locked, seed) in tests.
283284
*/
284285
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed);
286+
287+
const uint8_t* keystore_test_get_retained_seed_encrypted(size_t* len_out);
285288
#endif
286289

287290
#endif

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,8 @@ mod tests {
180180
..Default::default()
181181
});
182182
mock_sd();
183-
mock_unlocked();
184183
mock_memory();
184+
mock_unlocked();
185185
assert_eq!(
186186
block_on(create(&pb::CreateBackupRequest {
187187
timestamp: EXPECTED_TIMESTMAP,
@@ -229,11 +229,11 @@ mod tests {
229229
..Default::default()
230230
});
231231
mock_sd();
232+
mock_memory();
232233
mock_unlocked_using_mnemonic(
233234
"memory raven era cave phone system dice come mechanic split moon repeat",
234235
"",
235236
);
236-
mock_memory();
237237

238238
// Create the three files using the a fixture in the directory with the backup ID of the
239239
// above seed.
@@ -279,8 +279,9 @@ mod tests {
279279
ui_confirm_create: Some(Box::new(|_params| true)),
280280
..Default::default()
281281
});
282-
mock_unlocked_using_mnemonic("purity concert above invest pigeon category peace tuition hazard vivid latin since legal speak nation session onion library travel spell region blast estate stay", "");
283282
mock_memory();
283+
mock_unlocked_using_mnemonic("purity concert above invest pigeon category peace tuition hazard vivid latin since legal speak nation session onion library travel spell region blast estate stay", "");
284+
284285
bitbox02::memory::set_device_name(DEVICE_NAME_1).unwrap();
285286
assert!(block_on(create(&pb::CreateBackupRequest {
286287
timestamp: EXPECTED_TIMESTAMP,
@@ -305,8 +306,8 @@ mod tests {
305306
ui_confirm_create: Some(Box::new(|_params| true)),
306307
..Default::default()
307308
});
308-
mock_unlocked_using_mnemonic("goddess item rack improve shaft occur actress rib emerge salad rich blame model glare lounge stable electric height scrub scrub oyster now dinner oven", "");
309309
mock_memory();
310+
mock_unlocked_using_mnemonic("goddess item rack improve shaft occur actress rib emerge salad rich blame model glare lounge stable electric height scrub scrub oyster now dinner oven", "");
310311
bitbox02::memory::set_device_name(DEVICE_NAME_2).unwrap();
311312
assert!(block_on(create(&pb::CreateBackupRequest {
312313
timestamp: EXPECTED_TIMESTAMP,

src/rust/bitbox02-rust/src/hww/api/bitcoin/signtx.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1736,8 +1736,6 @@ mod tests {
17361736
/// Test signing if all inputs are of type P2TR.
17371737
#[test]
17381738
pub fn test_script_type_p2tr() {
1739-
bitbox02::random::mock_reset();
1740-
17411739
let transaction =
17421740
alloc::rc::Rc::new(core::cell::RefCell::new(Transaction::new(pb::BtcCoin::Btc)));
17431741
for input in transaction.borrow_mut().inputs.iter_mut() {
@@ -1763,6 +1761,7 @@ mod tests {
17631761

17641762
mock_default_ui();
17651763
mock_unlocked();
1764+
bitbox02::random::mock_reset();
17661765
let mut init_request = transaction.borrow().init_request();
17671766
init_request.script_configs[0] = pb::BtcScriptConfigWithKeypath {
17681767
script_config: Some(pb::BtcScriptConfig {

0 commit comments

Comments
 (0)