Skip to content

Commit 256a514

Browse files
committed
Adress review comments
- Modified change_password in bitbox02.py to raise BitBox02Exception on failure - The api call is now only allowed if the device is initialized and unlocked - Extra check in removed in process(...) in change_password.rs - Changed ui status to show more info when re_encrypt_seed fails - corrected error type in re_encrypt_seed(...) in keystore.rs - Added retain_bip39_seed(...) to re-use code - Added unit tests for re_encrypt_seed(...) and retain_bip39_seed(...) - Removed outdated comment - Changed check in re_encrypt_seed(...) to is_seeded() instead of is_initialzed() - Adapted change_password.rs due to merge conflict
1 parent 8ade527 commit 256a514

File tree

4 files changed

+213
-48
lines changed

4 files changed

+213
-48
lines changed

py/bitbox02/bitbox02/bitbox02/bitbox02.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,22 +212,16 @@ def set_password(self, entropy_size: int = 32) -> bool:
212212
raise
213213
return True
214214

215-
def change_password(self) -> bool:
215+
def change_password(self) -> None:
216216
"""
217217
Returns True if the user changes password successfully by
218218
unlocking with old password and entering and confirming new password.
219-
Returns False otherwise.
219+
Raises a Bitbox02Exception on failure.
220220
"""
221221
# pylint: disable=no-member
222222
request = hww.Request()
223223
request.change_password.CopyFrom(bitbox02_system.ChangePasswordRequest())
224-
try:
225-
self._msg_query(request, expected_response="success")
226-
except Bitbox02Exception as err:
227-
if err.code == ERR_GENERIC:
228-
return False
229-
raise
230-
return True
224+
self._msg_query(request, expected_response="success")
231225

232226
def create_backup(self) -> bool:
233227
"""

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ fn can_call(request: &Request) -> bool {
139139
Request::SetPassword(_) | Request::RestoreBackup(_) | Request::RestoreFromMnemonic(_) => {
140140
matches!(state, State::Uninitialized | State::Seeded)
141141
}
142-
Request::CreateBackup(_) | Request::ShowMnemonic(_) | Request::ChangePassword(_) => {
142+
Request::CreateBackup(_) | Request::ShowMnemonic(_) => {
143143
matches!(state, State::Seeded | State::InitializedAndUnlocked)
144144
}
145145
Request::Fingerprint(_)
@@ -152,7 +152,8 @@ fn can_call(request: &Request) -> bool {
152152
| Request::Eth(_)
153153
| Request::Reset(_)
154154
| Request::Cardano(_)
155-
| Request::Bip85(_) => {
155+
| Request::Bip85(_)
156+
| Request::ChangePassword(_) => {
156157
matches!(state, State::InitializedAndUnlocked)
157158
}
158159
// These are streamed asynchronously using the `next_request()` primitive in

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

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,14 @@ use crate::keystore;
88
use crate::workflow::{password, unlock};
99

1010
pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
11-
// Must be initialized
12-
if !bitbox02::memory::is_initialized() {
13-
return Err(Error::Generic);
14-
}
15-
1611
// Unlock with old password
1712
let seed = unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?;
1813
// Enter and confirm new password
1914
let new_password = password::enter_twice(hal).await?;
2015

2116
// Re-encrypt seed with new password
22-
if keystore::re_encrypt_seed(hal, &seed, &new_password).is_err() {
23-
hal.ui().status("Could not change\npassword", false).await;
17+
if let Err(err) = keystore::re_encrypt_seed(hal, &seed, &new_password) {
18+
hal.ui().status(&format!("Error\n{:?}", err), false).await;
2419
return Err(Error::Generic);
2520
}
2621

@@ -73,7 +68,7 @@ mod tests {
7368
}
7469
}));
7570
// reset the chip counter
76-
bitbox02::securechip::fake_event_counter_reset();
71+
hal.securechip.event_counter_reset();
7772
// call process
7873
let result = block_on(process(&mut hal));
7974
// assert success
@@ -87,9 +82,9 @@ mod tests {
8782
success: true,
8883
}]
8984
);
90-
drop(hal);
9185

92-
let securechip_events = bitbox02::securechip::fake_event_counter();
86+
let securechip_events = hal.securechip.get_event_counter();
87+
drop(hal);
9388
// We expect 14 secure chip events. This is intentionally brittle to catch
9489
// unintended changes in the number of securechip operations during password change.
9590
// If this fails after a legitimate change, update the expected count.
@@ -113,18 +108,6 @@ mod tests {
113108
);
114109
}
115110

116-
// Test that we fail if the device is not initialized
117-
#[test]
118-
fn test_process_device_not_initialized() {
119-
mock_memory();
120-
let mut hal = TestingHal::new();
121-
assert!(!bitbox02::memory::is_initialized());
122-
bitbox02::securechip::fake_event_counter_reset();
123-
assert_eq!(block_on(process(&mut hal)), Err(Error::Generic));
124-
assert!(hal.ui.screens.is_empty());
125-
assert_eq!(bitbox02::securechip::fake_event_counter(), 0);
126-
}
127-
128111
// Test that we fail if the unlock fails
129112
#[test]
130113
fn test_process_unlock_failure() {
@@ -146,7 +129,7 @@ mod tests {
146129
Ok("wrong_password".into())
147130
}));
148131

149-
bitbox02::securechip::fake_event_counter_reset();
132+
hal.securechip.event_counter_reset();
150133
let result = block_on(process(&mut hal));
151134

152135
assert_eq!(result, Err(Error::Generic));
@@ -158,7 +141,7 @@ mod tests {
158141
}]
159142
);
160143
// We expect 5 secure chip events (sensitive to code changes)
161-
assert_eq!(bitbox02::securechip::fake_event_counter(), 5);
144+
assert_eq!(hal.securechip.get_event_counter(), 5);
162145

163146
drop(hal);
164147
assert_eq!(prompt_counter, 1);

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

Lines changed: 200 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ fn retain_seed(random: &mut impl crate::hal::Random, seed: &[u8]) -> Result<(),
192192
Ok(())
193193
}
194194

195+
fn retain_bip39_seed(random: &mut impl crate::hal::Random, bip39_seed: &[u8]) -> Result<(), Error> {
196+
RETAINED_BIP39_SEED.write(Some(RetainedEncryptedBuffer::from_buffer(
197+
random,
198+
bip39_seed,
199+
"keystore_retained_bip39_seed_access",
200+
)?));
201+
Ok(())
202+
}
203+
195204
// Internal helper to encrypt a seed with a password and store it on flash
196205
// For setting password (initilization) we clear memory first, to guarantee clean RAM
197206
// For changing password we do not clear memory to retain the BIP39 seed
@@ -243,7 +252,6 @@ pub fn encrypt_and_store_seed(
243252
if bitbox02::memory::is_initialized() {
244253
return Err(Error::Memory);
245254
}
246-
// We are in setup phase, so we clear memory first to guarantee clean RAM
247255
encrypt_and_store_seed_internal(hal, seed, password)
248256
}
249257

@@ -253,24 +261,20 @@ pub fn re_encrypt_seed(
253261
seed: &[u8],
254262
new_password: &str,
255263
) -> Result<(), Error> {
256-
if !bitbox02::memory::is_initialized() {
264+
if !bitbox02::memory::is_seeded() {
257265
return Err(Error::Unseeded);
258266
}
259267

260268
// Store the bip39 seed before re-encryption because:
261269
// 1. The secure chip's internal keys are regenerated with the new password
262270
// 2. encrypt_and_store_seed_internal calls lock() which clears RAM including BIP39 seed
263271
// 3. We want to avoid forcing the user to re-enter their BIP39 passphrase
264-
let bip39_seed = copy_bip39_seed().map_err(|_| Error::CannotUnlockBIP39)?;
272+
let bip39_seed = copy_bip39_seed().map_err(|_| Error::Memory)?;
265273

266274
encrypt_and_store_seed_internal(hal, seed, new_password)?;
267275

268276
// Re-retain the bip39 seed
269-
RETAINED_BIP39_SEED.write(Some(RetainedEncryptedBuffer::from_buffer(
270-
hal.random(),
271-
bip39_seed.as_slice(),
272-
"keystore_retained_bip39_seed_access",
273-
)?));
277+
retain_bip39_seed(hal.random(), bip39_seed.as_slice())?;
274278

275279
Ok(())
276280
}
@@ -377,11 +381,7 @@ pub async fn unlock_bip39(
377381
return Err(Error::Memory);
378382
}
379383

380-
RETAINED_BIP39_SEED.write(Some(RetainedEncryptedBuffer::from_buffer(
381-
random,
382-
bip39_seed.as_slice(),
383-
"keystore_retained_bip39_seed_access",
384-
)?));
384+
retain_bip39_seed(random, bip39_seed.as_slice())?;
385385

386386
// Store root fingerprint.
387387
ROOT_FINGERPRINT.write(Some(root_fingerprint));
@@ -880,6 +880,193 @@ mod tests {
880880
}
881881
}
882882

883+
#[test]
884+
fn test_re_encrypt_seed() {
885+
mock_memory();
886+
lock();
887+
888+
let mut mock_hal = TestingHal::new();
889+
let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
890+
891+
// Try to re-encrypt without seeding first
892+
assert!(matches!(
893+
re_encrypt_seed(&mut mock_hal, &seed, "new_password"),
894+
Err(Error::Unseeded)
895+
));
896+
}
897+
898+
#[test]
899+
fn test_re_encrypt_seed_changes_password() {
900+
mock_memory();
901+
lock();
902+
903+
let mut mock_hal = TestingHal::new();
904+
let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
905+
906+
// Step 1: Set up device with initial password
907+
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "old_password").is_ok());
908+
909+
// Step 2: Unlock with initial password and set up BIP39
910+
let unlocked_seed = unlock(&mut mock_hal, "old_password").unwrap();
911+
assert_eq!(unlocked_seed.as_slice(), seed.as_slice());
912+
913+
let mut random = crate::hal::testing::TestingRandom::new();
914+
assert!(block_on(unlock_bip39(&mut random, &seed, "", async || {})).is_ok());
915+
916+
// Step 3: Re-encrypt with new password
917+
assert!(re_encrypt_seed(&mut mock_hal, &seed, "new_password").is_ok());
918+
919+
// Step 4: Lock and verify old password no longer works
920+
lock();
921+
assert!(matches!(
922+
unlock(&mut mock_hal, "old_password"),
923+
Err(Error::IncorrectPassword)
924+
));
925+
926+
// Step 5: Verify new password works
927+
let unlocked_seed_new = unlock(&mut mock_hal, "new_password").unwrap();
928+
assert_eq!(unlocked_seed_new.as_slice(), seed.as_slice());
929+
}
930+
931+
#[test]
932+
fn test_re_encrypt_seed_preserves_seed() {
933+
mock_memory();
934+
lock();
935+
936+
let mut mock_hal = TestingHal::new();
937+
let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
938+
939+
// Initial setup
940+
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "password1").is_ok());
941+
let seed_copy1 = copy_seed().unwrap();
942+
943+
// Re-encrypt multiple times
944+
for (i, new_password) in ["password2", "password3", "password4"].iter().enumerate() {
945+
// Need to be unlocked with BIP39 before re-encrypting
946+
if i > 0 {
947+
let prev_password = match i {
948+
1 => "password2",
949+
2 => "password3",
950+
_ => unreachable!(),
951+
};
952+
lock();
953+
unlock(&mut mock_hal, prev_password).unwrap();
954+
}
955+
956+
let mut random = crate::hal::testing::TestingRandom::new();
957+
assert!(block_on(unlock_bip39(&mut random, &seed, "", async || {})).is_ok());
958+
assert!(re_encrypt_seed(&mut mock_hal, &seed, new_password).is_ok());
959+
960+
// Seed should remain the same
961+
assert_eq!(copy_seed().unwrap().as_slice(), seed_copy1.as_slice());
962+
}
963+
}
964+
965+
#[test]
966+
fn test_re_encrypt_seed_preserves_bip39_seed() {
967+
mock_memory();
968+
lock();
969+
970+
let mut mock_hal = TestingHal::new();
971+
let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
972+
let passphrase = "my_secret_passphrase";
973+
974+
// Initial setup with password
975+
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "old_password").is_ok());
976+
unlock(&mut mock_hal, "old_password").unwrap();
977+
978+
// Unlock BIP39 with passphrase
979+
let mut random = crate::hal::testing::TestingRandom::new();
980+
assert!(block_on(unlock_bip39(&mut random, &seed, passphrase, async || {})).is_ok());
981+
982+
// Store the BIP39 seed before password change
983+
let bip39_seed_before = copy_bip39_seed().unwrap();
984+
985+
// Re-encrypt with new password
986+
assert!(re_encrypt_seed(&mut mock_hal, &seed, "new_password").is_ok());
987+
988+
// BIP39 seed should still be available (not cleared by lock() inside re_encrypt_seed)
989+
let bip39_seed_after = copy_bip39_seed().unwrap();
990+
assert_eq!(bip39_seed_before.as_slice(), bip39_seed_after.as_slice());
991+
992+
// Lock and unlock with new password
993+
lock();
994+
unlock(&mut mock_hal, "new_password").unwrap();
995+
996+
// BIP39 should be locked now (we'd need to call unlock_bip39 again)
997+
assert!(copy_bip39_seed().is_err());
998+
}
999+
1000+
#[test]
1001+
fn test_re_encrypt_seed_invalid_seed_size() {
1002+
mock_memory();
1003+
lock();
1004+
1005+
let mut mock_hal = TestingHal::new();
1006+
let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
1007+
1008+
// Initial setup
1009+
assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "password").is_ok());
1010+
unlock(&mut mock_hal, "password").unwrap();
1011+
1012+
let mut random = crate::hal::testing::TestingRandom::new();
1013+
assert!(block_on(unlock_bip39(&mut random, &seed, "", async || {})).is_ok());
1014+
1015+
// Try to re-encrypt with invalid seed size
1016+
assert!(matches!(
1017+
re_encrypt_seed(&mut mock_hal, &[0u8; 31], "new_password"),
1018+
Err(Error::SeedSize)
1019+
));
1020+
}
1021+
1022+
#[test]
1023+
fn test_retain_bip39_seed() {
1024+
mock_memory();
1025+
lock();
1026+
1027+
let mut random = crate::hal::testing::TestingRandom::new();
1028+
let bip39_seed = hex!(
1029+
"2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a"
1030+
);
1031+
1032+
// Before retention, should not be available
1033+
assert!(copy_bip39_seed().is_err());
1034+
1035+
// Retain the BIP39 seed
1036+
assert!(retain_bip39_seed(&mut random, &bip39_seed).is_ok());
1037+
1038+
// Should now be available
1039+
let retrieved = copy_bip39_seed().unwrap();
1040+
assert_eq!(retrieved.as_slice(), bip39_seed.as_slice());
1041+
}
1042+
1043+
#[test]
1044+
fn test_retain_bip39_seed_overwrites_previous() {
1045+
mock_memory();
1046+
lock();
1047+
let mut random = crate::hal::testing::TestingRandom::new();
1048+
let bip39_seed1 = hex!(
1049+
"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
1050+
);
1051+
let bip39_seed2 = hex!(
1052+
"2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222"
1053+
);
1054+
1055+
// Retain first seed
1056+
assert!(retain_bip39_seed(&mut random, &bip39_seed1).is_ok());
1057+
assert_eq!(
1058+
copy_bip39_seed().unwrap().as_slice(),
1059+
bip39_seed1.as_slice()
1060+
);
1061+
1062+
// Retain second seed (should overwrite)
1063+
assert!(retain_bip39_seed(&mut random, &bip39_seed2).is_ok());
1064+
assert_eq!(
1065+
copy_bip39_seed().unwrap().as_slice(),
1066+
bip39_seed2.as_slice()
1067+
);
1068+
}
1069+
8831070
// This tests that you can create a keystore, unlock it, and then do this again. This is an
8841071
// expected workflow for when the wallet setup process is restarted after seeding and unlocking,
8851072
// but before creating a backup, in which case a new seed is created.

0 commit comments

Comments
 (0)