Skip to content

Commit 8658620

Browse files
committed
Merge branch 'seed'
2 parents 7901849 + 4f04a35 commit 8658620

File tree

8 files changed

+313
-60
lines changed

8 files changed

+313
-60
lines changed

CHANGELOG.md

+1
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

+2-2
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

+187-31
Original file line numberDiff line numberDiff line change
@@ -36,31 +36,22 @@
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+
// Stores a random key after unlock which, after stretching, is used to encrypt the retained seed.
40+
static uint8_t _unstretched_retained_seed_encryption_key[32] = {0};
4241
// 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};
42+
// Stores the encrypted seed after unlock.
43+
static uint8_t _retained_seed_encrypted[KEYSTORE_MAX_SEED_LENGTH + 64] = {0};
44+
static size_t _retained_seed_encrypted_len = 0;
4445

4546
// Change this ONLY via keystore_unlock_bip39().
4647
static bool _is_unlocked_bip39 = false;
48+
// Stores a random keyy after bip39-unlock which, after stretching, is used to encrypt the retained
49+
// bip39 seed.
50+
static uint8_t _unstretched_retained_bip39_seed_encryption_key[32] = {0};
4751
// Must be defined if _is_unlocked is true. ONLY ACCESS THIS WITH _copy_bip39_seed().
48-
static uint8_t _retained_bip39_seed[64] = {0};
49-
50-
#ifdef TESTING
51-
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed)
52-
{
53-
_is_unlocked_device = seed != NULL;
54-
if (seed != NULL) {
55-
_seed_length = seed_len;
56-
memcpy(_retained_seed, seed, seed_len);
57-
}
58-
_is_unlocked_bip39 = bip39_seed != NULL;
59-
if (bip39_seed != NULL) {
60-
memcpy(_retained_bip39_seed, bip39_seed, sizeof(_retained_bip39_seed));
61-
}
62-
}
63-
#endif
52+
// Stores the encrypted BIP-39 seed after bip39-unlock.
53+
static uint8_t _retained_bip39_seed_encrypted[64 + 64] = {0};
54+
static size_t _retained_bip39_seed_encrypted_len = 0;
6455

6556
/**
6657
* We allow seeds of 16, 24 or 32 bytes.
@@ -70,13 +61,56 @@ static bool _validate_seed_length(size_t seed_len)
7061
return seed_len == 16 || seed_len == 24 || seed_len == 32;
7162
}
7263

64+
USE_RESULT static keystore_error_t _stretch_retained_seed_encryption_key(
65+
const uint8_t* encryption_key,
66+
const char* purpose_in,
67+
const char* purpose_out,
68+
uint8_t* out)
69+
{
70+
uint8_t salted_hashed[32] = {0};
71+
UTIL_CLEANUP_32(salted_hashed);
72+
if (!salt_hash_data(encryption_key, 32, purpose_in, salted_hashed)) {
73+
return KEYSTORE_ERR_SALT;
74+
}
75+
if (securechip_kdf(SECURECHIP_SLOT_KDF, salted_hashed, 32, out)) {
76+
return KEYSTORE_ERR_SECURECHIP;
77+
}
78+
if (!salt_hash_data(encryption_key, 32, purpose_out, salted_hashed)) {
79+
return KEYSTORE_ERR_SALT;
80+
}
81+
if (wally_hmac_sha256(salted_hashed, sizeof(salted_hashed), out, 32, out, 32) != WALLY_OK) {
82+
return KEYSTORE_ERR_HASH;
83+
}
84+
return KEYSTORE_OK;
85+
}
86+
7387
bool keystore_copy_seed(uint8_t* seed_out, size_t* length_out)
7488
{
7589
if (!_is_unlocked_device) {
7690
return false;
7791
}
78-
memcpy(seed_out, _retained_seed, _seed_length);
79-
*length_out = _seed_length;
92+
93+
uint8_t retained_seed_encryption_key[32] = {0};
94+
UTIL_CLEANUP_32(retained_seed_encryption_key);
95+
if (_stretch_retained_seed_encryption_key(
96+
_unstretched_retained_seed_encryption_key,
97+
"keystore_retained_seed_access_in",
98+
"keystore_retained_seed_access_out",
99+
retained_seed_encryption_key) != KEYSTORE_OK) {
100+
return false;
101+
}
102+
size_t len = _retained_seed_encrypted_len - 48;
103+
bool password_correct = cipher_aes_hmac_decrypt(
104+
_retained_seed_encrypted,
105+
_retained_seed_encrypted_len,
106+
seed_out,
107+
&len,
108+
retained_seed_encryption_key);
109+
if (!password_correct) {
110+
// Should never happen.
111+
return false;
112+
}
113+
*length_out = len;
80114
return true;
81115
}
82116

@@ -92,13 +126,37 @@ static bool _copy_bip39_seed(uint8_t* bip39_seed_out)
92126
if (!_is_unlocked_bip39) {
93127
return false;
94128
}
129+
130+
uint8_t retained_bip39_seed_encryption_key[32] = {0};
131+
UTIL_CLEANUP_32(retained_bip39_seed_encryption_key);
132+
if (_stretch_retained_seed_encryption_key(
133+
_unstretched_retained_bip39_seed_encryption_key,
134+
"keystore_retained_bip39_seed_access_in",
135+
"keystore_retained_bip39_seed_access_out",
136+
retained_bip39_seed_encryption_key) != KEYSTORE_OK) {
137+
return false;
138+
}
139+
size_t len = _retained_bip39_seed_encrypted_len - 48;
140+
bool password_correct = cipher_aes_hmac_decrypt(
141+
_retained_bip39_seed_encrypted,
142+
_retained_bip39_seed_encrypted_len,
143+
bip39_seed_out,
144+
&len,
145+
retained_bip39_seed_encryption_key);
146+
if (!password_correct) {
147+
// Should never happen.
148+
return false;
149+
}
150+
if (len != 64) {
151+
// Should never happen.
152+
return false;
153+
}
95154
// sanity check
96155
uint8_t zero[64] = {0};
97156
util_zero(zero, 64);
98-
if (MEMEQ(_retained_bip39_seed, zero, sizeof(_retained_bip39_seed))) {
157+
if (MEMEQ(bip39_seed_out, zero, 64)) {
99158
return false;
100159
}
101-
memcpy(bip39_seed_out, _retained_bip39_seed, sizeof(_retained_bip39_seed));
102160
return true;
103161
}
104162

@@ -319,6 +377,67 @@ static void _free_string(char** str)
319377
wally_free_string(*str);
320378
}
321379

380+
USE_RESULT static keystore_error_t _retain_seed(const uint8_t* seed, size_t seed_len)
381+
{
382+
random_32_bytes(_unstretched_retained_seed_encryption_key);
383+
uint8_t retained_seed_encryption_key[32] = {0};
384+
UTIL_CLEANUP_32(retained_seed_encryption_key);
385+
keystore_error_t result = _stretch_retained_seed_encryption_key(
386+
_unstretched_retained_seed_encryption_key,
387+
"keystore_retained_seed_access_in",
388+
"keystore_retained_seed_access_out",
389+
retained_seed_encryption_key);
390+
if (result != KEYSTORE_OK) {
391+
return result;
392+
}
393+
size_t len = seed_len + 64;
394+
if (!cipher_aes_hmac_encrypt(
395+
seed, seed_len, _retained_seed_encrypted, &len, retained_seed_encryption_key)) {
396+
return KEYSTORE_ERR_ENCRYPT;
397+
}
398+
_retained_seed_encrypted_len = len;
399+
return KEYSTORE_OK;
400+
}
401+
402+
USE_RESULT static bool _retain_bip39_seed(const uint8_t* bip39_seed)
403+
{
404+
random_32_bytes(_unstretched_retained_bip39_seed_encryption_key);
405+
uint8_t retained_bip39_seed_encryption_key[32] = {0};
406+
UTIL_CLEANUP_32(retained_bip39_seed_encryption_key);
407+
if (_stretch_retained_seed_encryption_key(
408+
_unstretched_retained_bip39_seed_encryption_key,
409+
"keystore_retained_bip39_seed_access_in",
410+
"keystore_retained_bip39_seed_access_out",
411+
retained_bip39_seed_encryption_key) != KEYSTORE_OK) {
412+
return false;
413+
}
414+
size_t len = sizeof(_retained_bip39_seed_encrypted);
415+
if (!cipher_aes_hmac_encrypt(
416+
bip39_seed,
417+
64,
418+
_retained_bip39_seed_encrypted,
419+
&len,
420+
retained_bip39_seed_encryption_key)) {
421+
return false;
422+
}
423+
_retained_bip39_seed_encrypted_len = len;
424+
return true;
425+
}
426+
427+
static void _delete_retained_seeds(void)
428+
{
429+
util_zero(
430+
_unstretched_retained_seed_encryption_key,
431+
sizeof(_unstretched_retained_seed_encryption_key));
432+
util_zero(_retained_seed_encrypted, sizeof(_retained_seed_encrypted));
433+
_retained_seed_encrypted_len = 0;
434+
util_zero(
435+
_unstretched_retained_bip39_seed_encryption_key,
436+
sizeof(_unstretched_retained_seed_encryption_key));
437+
util_zero(_retained_bip39_seed_encrypted, sizeof(_retained_bip39_seed_encrypted));
438+
_retained_bip39_seed_encrypted_len = 0;
439+
}
440+
322441
keystore_error_t keystore_unlock(
323442
const char* password,
324443
uint8_t* remaining_attempts_out,
@@ -350,12 +469,19 @@ keystore_error_t keystore_unlock(
350469
if (result == KEYSTORE_OK) {
351470
if (_is_unlocked_device) {
352471
// Already unlocked. Fail if the seed changed under our feet (should never happen).
353-
if (seed_len != _seed_length || !MEMEQ(_retained_seed, seed, _seed_length)) {
472+
uint8_t current_seed[KEYSTORE_MAX_SEED_LENGTH] = {0};
473+
size_t current_seed_len = 0;
474+
if (!keystore_copy_seed(current_seed, &current_seed_len)) {
475+
return KEYSTORE_ERR_DECRYPT;
476+
}
477+
if (seed_len != current_seed_len || !MEMEQ(current_seed, seed, current_seed_len)) {
354478
Abort("Seed has suddenly changed. This should never happen.");
355479
}
356480
} else {
357-
memcpy(_retained_seed, seed, seed_len);
358-
_seed_length = seed_len;
481+
keystore_error_t retain_seed_result = _retain_seed(seed, seed_len);
482+
if (retain_seed_result != KEYSTORE_OK) {
483+
return retain_seed_result;
484+
}
359485
_is_unlocked_device = true;
360486
}
361487
bitbox02_smarteeprom_reset_unlock_attempts();
@@ -396,7 +522,9 @@ bool keystore_unlock_bip39(const char* mnemonic_passphrase)
396522
mnemonic, mnemonic_passphrase, bip39_seed, sizeof(bip39_seed), NULL) != WALLY_OK) {
397523
return false;
398524
}
399-
memcpy(_retained_bip39_seed, bip39_seed, sizeof(bip39_seed));
525+
if (!_retain_bip39_seed(bip39_seed)) {
526+
return false;
527+
}
400528
_is_unlocked_bip39 = true;
401529
return true;
402530
}
@@ -405,9 +533,7 @@ void keystore_lock(void)
405533
{
406534
_is_unlocked_device = false;
407535
_is_unlocked_bip39 = false;
408-
_seed_length = 0;
409-
util_zero(_retained_seed, sizeof(_retained_seed));
410-
util_zero(_retained_bip39_seed, sizeof(_retained_bip39_seed));
536+
_delete_retained_seeds();
411537
}
412538

413539
bool keystore_is_locked(void)
@@ -789,3 +915,33 @@ bool keystore_secp256k1_schnorr_bip86_sign(
789915
}
790916
return secp256k1_schnorrsig_verify(ctx, sig64_out, msg32, 32, &pubkey) == 1;
791917
}
918+
919+
#ifdef TESTING
920+
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed)
921+
{
922+
_is_unlocked_device = seed != NULL;
923+
if (seed != NULL) {
924+
if (_retain_seed(seed, seed_len) != KEYSTORE_OK) {
925+
Abort("couldn't retain seed");
926+
}
927+
}
928+
_is_unlocked_bip39 = bip39_seed != NULL;
929+
if (bip39_seed != NULL) {
930+
if (!_retain_bip39_seed(bip39_seed)) {
931+
Abort("couldn't retain bip39 seed");
932+
}
933+
}
934+
}
935+
936+
const uint8_t* keystore_test_get_retained_seed_encrypted(size_t* len_out)
937+
{
938+
*len_out = _retained_seed_encrypted_len;
939+
return _retained_seed_encrypted;
940+
}
941+
942+
const uint8_t* keystore_test_get_retained_bip39_seed_encrypted(size_t* len_out)
943+
{
944+
*len_out = _retained_bip39_seed_encrypted_len;
945+
return _retained_bip39_seed_encrypted;
946+
}
947+
#endif

src/keystore.h

+11-7
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,9 @@ typedef enum {
4343
KEYSTORE_ERR_SALT,
4444
KEYSTORE_ERR_HASH,
4545
KEYSTORE_ERR_ENCRYPT,
46+
KEYSTORE_ERR_DECRYPT,
4647
} keystore_error_t;
4748

48-
#ifdef TESTING
49-
/**
50-
* convenience to mock the keystore state (locked, seed) in tests.
51-
*/
52-
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed);
53-
#endif
54-
5549
/**
5650
* Copies the retained seed into the given buffer. The caller must
5751
* zero the seed with util_zero once it is no longer needed.
@@ -284,4 +278,14 @@ USE_RESULT bool keystore_secp256k1_schnorr_bip86_sign(
284278
const uint8_t* msg32,
285279
uint8_t* sig64_out);
286280

281+
#ifdef TESTING
282+
/**
283+
* convenience to mock the keystore state (locked, seed) in tests.
284+
*/
285+
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);
288+
const uint8_t* keystore_test_get_retained_bip39_seed_encrypted(size_t* len_out);
289+
#endif
290+
287291
#endif

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

+5-4
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,

0 commit comments

Comments
 (0)