@@ -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