What changed, and why it matters
This commit adds a new 'Change Password' feature to the BitBox02 hardware wallet. Instead of resetting the device and restoring from a backup, users can now change their device password directly. The wallet re-encrypts the stored seed with the new password while keeping the same accounts and identity. The change is a normal product feature, not a security fix, but it touches sensitive crypto code that protects the user's funds.
Treat as a feature commit, not a vulnerability patch. If auditing, focus review on keystore::re_encrypt_seed to ensure the BIP39 seed and root fingerprint are safely retained and that no plaintext seed material is exposed during the rotation. Also verify that the new endpoint cannot be reached in a locked or uninitialized state and that the secure chip event-counter assumptions in tests match production behavior.
Security signals we found
New privileged API endpoint added to an existing oneof (protobuf tag 30)
Workflow requires old-password re-authentication before password rotation
Re-encryption of seed material via secure chip key stretching
BIP39 seed and root fingerprint are copied and restored across re-encryption to preserve wallet state
Endpoint is restricted to InitializedAndUnlocked device state
No input parameters in ChangePasswordRequest, so no attacker-controlled data is parsed
Extensive unit tests included for success, failure, and state-preservation cases
Evidence from the diff
The patch introduces a new HWW API endpoint ChangePasswordRequest (protobuf tag 30) and a Rust workflow in src/rust/bitbox02-rust/src/hww/api/change_password.rs. The workflow: (1) asks the user to confirm, (2) forces re-unlock with the old password via unlock::unlock_keystore, (3) collects the new password twice via password::enter_twice, and (4) calls keystore::re_encrypt_seed to re-encrypt the retained seed with the new password. re_encrypt_seed preserves the BIP39 seed and root fingerprint across the re-encryption so passphrases and pairings remain valid. The endpoint is gated behind the InitializedAndUnlocked state. Tests cover success, wrong old password, and password mismatch paths.
Changed components
src/rust/bitbox02-rust/src/hww/api.rssrc/rust/bitbox02-rust/src/hww/api/change_password.rssrc/rust/bitbox02-rust/src/keystore.rsmessages/bitbox02_system.protomessages/hww.protopy/bitbox02/bitbox02/bitbox02.pypy/send_message.pyInspect captured patch +494 / −23
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bacec07..087710c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
### [Unreleased]
- Fix bug that BLE was turned off when iOS device is unlocked
- simulator-graphical: a new simulator with a graphical user interface
+- Add ability to change the device password after initial setup
### v9.24.0
- Change title when entering recovery words to `1 of 24`, `2 of 24`, etc.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 341c015..7c46728 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -96,7 +96,7 @@ endif()
#
# Versions MUST contain three parts and start with lowercase 'v'.
# Example 'v1.0.0'. They MUST not contain a pre-release label such as '-beta'.
-set(FIRMWARE_VERSION "v9.24.0")
+set(FIRMWARE_VERSION "v9.25.0")
set(BOOTLOADER_VERSION "v1.1.2")
find_package(PythonInterp 3.6 REQUIRED)
diff --git a/messages/bitbox02_system.proto b/messages/bitbox02_system.proto
index a5e8e55..aef431f 100644
--- a/messages/bitbox02_system.proto
+++ b/messages/bitbox02_system.proto
@@ -66,3 +66,6 @@ message SetDeviceNameRequest {
message SetPasswordRequest {
bytes entropy = 1;
}
+
+message ChangePasswordRequest{
+}
\ No newline at end of file
diff --git a/messages/hww.proto b/messages/hww.proto
index 4088ae3..ea7177e 100644
--- a/messages/hww.proto
+++ b/messages/hww.proto
@@ -69,6 +69,7 @@ message Request {
CardanoRequest cardano = 27;
BIP85Request bip85 = 28;
BluetoothRequest bluetooth = 29;
+ ChangePasswordRequest change_password = 30;
}
}
diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py
index 5a0c20a..234e80f 100644
--- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py
+++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py
@@ -212,6 +212,17 @@ class BitBox02(BitBoxCommonAPI):
raise
return True
+ def change_password(self) -> None:
+ """
+ Changes the device password. The user must unlock with the old password
+ and enter and confirm the new password.
+ Raises a Bitbox02Exception on failure.
+ """
+ # pylint: disable=no-member
+ request = hww.Request()
+ request.change_password.CopyFrom(bitbox02_system.ChangePasswordRequest())
+ self._msg_query(request, expected_response="success")
+
def create_backup(self) -> bool:
"""
Returns True if the backup was created successfully.
diff --git a/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.py b/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.py
index 1b519c9..4e07d70 100644
--- a/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.py
+++ b/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.py
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x62itbox02_system.proto\x12\x14shiftcrypto.bitbox02\"\x14\n\x12\x43heckSDCardRequest\"\'\n\x13\x43heckSDCardResponse\x12\x10\n\x08inserted\x18\x01 \x01(\x08\"\x13\n\x11\x44\x65viceInfoRequest\"\xd8\x02\n\x12\x44\x65viceInfoResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0binitialized\x18\x02 \x01(\x08\x12\x0f\n\x07version\x18\x03 \x01(\t\x12#\n\x1bmnemonic_passphrase_enabled\x18\x04 \x01(\x08\x12&\n\x1emonotonic_increments_remaining\x18\x05 \x01(\r\x12\x18\n\x10securechip_model\x18\x06 \x01(\t\x12J\n\tbluetooth\x18\x07 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.DeviceInfoResponse.BluetoothH\x00\x88\x01\x01\x1aM\n\tBluetooth\x12\x15\n\rfirmware_hash\x18\x01 \x01(\x0c\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x42\x0c\n\n_bluetooth\"\x9b\x01\n\x19InsertRemoveSDCardRequest\x12L\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32<.shiftcrypto.bitbox02.InsertRemoveSDCardRequest.SDCardAction\"0\n\x0cSDCardAction\x12\x0f\n\x0bREMOVE_CARD\x10\x00\x12\x0f\n\x0bINSERT_CARD\x10\x01\"\x0e\n\x0cResetRequest\",\n\x18SetDeviceLanguageRequest\x12\x10\n\x08language\x18\x01 \x01(\t\"$\n\x14SetDeviceNameRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"%\n\x12SetPasswordRequest\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x62itbox02_system.proto\x12\x14shiftcrypto.bitbox02\"\x14\n\x12\x43heckSDCardRequest\"\'\n\x13\x43heckSDCardResponse\x12\x10\n\x08inserted\x18\x01 \x01(\x08\"\x13\n\x11\x44\x65viceInfoRequest\"\xd8\x02\n\x12\x44\x65viceInfoResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0binitialized\x18\x02 \x01(\x08\x12\x0f\n\x07version\x18\x03 \x01(\t\x12#\n\x1bmnemonic_passphrase_enabled\x18\x04 \x01(\x08\x12&\n\x1emonotonic_increments_remaining\x18\x05 \x01(\r\x12\x18\n\x10securechip_model\x18\x06 \x01(\t\x12J\n\tbluetooth\x18\x07 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.DeviceInfoResponse.BluetoothH\x00\x88\x01\x01\x1aM\n\tBluetooth\x12\x15\n\rfirmware_hash\x18\x01 \x01(\x0c\x12\x18\n\x10\x66irmware_version\x18\x02 \x01(\t\x12\x0f\n\x07\x65nabled\x18\x03 \x01(\x08\x42\x0c\n\n_bluetooth\"\x9b\x01\n\x19InsertRemoveSDCardRequest\x12L\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32<.shiftcrypto.bitbox02.InsertRemoveSDCardRequest.SDCardAction\"0\n\x0cSDCardAction\x12\x0f\n\x0bREMOVE_CARD\x10\x00\x12\x0f\n\x0bINSERT_CARD\x10\x01\"\x0e\n\x0cResetRequest\",\n\x18SetDeviceLanguageRequest\x12\x10\n\x08language\x18\x01 \x01(\t\"$\n\x14SetDeviceNameRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"%\n\x12SetPasswordRequest\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\"\x17\n\x15\x43hangePasswordRequestb\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'bitbox02_system_pb2', globals())
@@ -42,4 +42,6 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_SETDEVICENAMEREQUEST._serialized_end=734
_SETPASSWORDREQUEST._serialized_start=736
_SETPASSWORDREQUEST._serialized_end=773
+ _CHANGEPASSWORDREQUEST._serialized_start=775
+ _CHANGEPASSWORDREQUEST._serialized_end=798
# @@protoc_insertion_point(module_scope)
diff --git a/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
index b928e85..ac163ab 100644
--- a/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
@@ -208,3 +208,13 @@ class SetPasswordRequest(google.protobuf.message.Message):
def ClearField(self, field_name: typing.Literal["entropy", b"entropy"]) -> None: ...
global___SetPasswordRequest = SetPasswordRequest
+
+@typing.final
+class ChangePasswordRequest(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ def __init__(
+ self,
+ ) -> None: ...
+
+global___ChangePasswordRequest = ChangePasswordRequest
diff --git a/py/bitbox02/bitbox02/communication/generated/hww_pb2.py b/py/bitbox02/bitbox02/communication/generated/hww_pb2.py
index 15b3524..1ea4168 100644
--- a/py/bitbox02/bitbox02/communication/generated/hww_pb2.py
+++ b/py/bitbox02/bitbox02/communication/generated/hww_pb2.py
@@ -24,7 +24,7 @@ from . import system_pb2 as system__pb2
from . import perform_attestation_pb2 as perform__attestation__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\thww.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x15\x62\x61\x63kup_commands.proto\x1a\x15\x62itbox02_system.proto\x1a\x0f\x62luetooth.proto\x1a\tbtc.proto\x1a\rcardano.proto\x1a\teth.proto\x1a\x0ekeystore.proto\x1a\x0emnemonic.proto\x1a\x0csystem.proto\x1a\x19perform_attestation.proto\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"\t\n\x07Success\"\xba\x0e\n\x07Request\x12\x41\n\x0b\x64\x65vice_name\x18\x02 \x01(\x0b\x32*.shiftcrypto.bitbox02.SetDeviceNameRequestH\x00\x12I\n\x0f\x64\x65vice_language\x18\x03 \x01(\x0b\x32..shiftcrypto.bitbox02.SetDeviceLanguageRequestH\x00\x12>\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32\'.shiftcrypto.bitbox02.DeviceInfoRequestH\x00\x12@\n\x0cset_password\x18\x05 \x01(\x0b\x32(.shiftcrypto.bitbox02.SetPasswordRequestH\x00\x12\x42\n\rcreate_backup\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.CreateBackupRequestH\x00\x12\x42\n\rshow_mnemonic\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ShowMnemonicRequestH\x00\x12\x36\n\x07\x62tc_pub\x18\x08 \x01(\x0b\x32#.shiftcrypto.bitbox02.BTCPubRequestH\x00\x12\x41\n\rbtc_sign_init\x18\t \x01(\x0b\x32(.shiftcrypto.bitbox02.BTCSignInitRequestH\x00\x12\x43\n\x0e\x62tc_sign_input\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignInputRequestH\x00\x12\x45\n\x0f\x62tc_sign_output\x18\x0b \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCSignOutputRequestH\x00\x12O\n\x14insert_remove_sdcard\x18\x0c \x01(\x0b\x32/.shiftcrypto.bitbox02.InsertRemoveSDCardRequestH\x00\x12@\n\x0c\x63heck_sdcard\x18\r \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckSDCardRequestH\x00\x12\x64\n\x1fset_mnemonic_passphrase_enabled\x18\x0e \x01(\x0b\x32\x39.shiftcrypto.bitbox02.SetMnemonicPassphraseEnabledRequestH\x00\x12@\n\x0clist_backups\x18\x0f \x01(\x0b\x32(.shiftcrypto.bitbox02.ListBackupsRequestH\x00\x12\x44\n\x0erestore_backup\x18\x10 \x01(\x0b\x32*.shiftcrypto.bitbox02.RestoreBackupRequestH\x00\x12N\n\x13perform_attestation\x18\x11 \x01(\x0b\x32/.shiftcrypto.bitbox02.PerformAttestationRequestH\x00\x12\x35\n\x06reboot\x18\x12 \x01(\x0b\x32#.shiftcrypto.bitbox02.RebootRequestH\x00\x12@\n\x0c\x63heck_backup\x18\x13 \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckBackupRequestH\x00\x12/\n\x03\x65th\x18\x14 \x01(\x0b\x32 .shiftcrypto.bitbox02.ETHRequestH\x00\x12\x33\n\x05reset\x18\x15 \x01(\x0b\x32\".shiftcrypto.bitbox02.ResetRequestH\x00\x12Q\n\x15restore_from_mnemonic\x18\x16 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.RestoreFromMnemonicRequestH\x00\x12\x43\n\x0b\x66ingerprint\x18\x18 \x01(\x0b\x32,.shiftcrypto.bitbox02.RootFingerprintRequestH\x00\x12/\n\x03\x62tc\x18\x19 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCRequestH\x00\x12U\n\x17\x65lectrum_encryption_key\x18\x1a \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ElectrumEncryptionKeyRequestH\x00\x12\x37\n\x07\x63\x61rdano\x18\x1b \x01(\x0b\x32$.shiftcrypto.bitbox02.CardanoRequestH\x00\x12\x33\n\x05\x62ip85\x18\x1c \x01(\x0b\x32\".shiftcrypto.bitbox02.BIP85RequestH\x00\x12;\n\tbluetooth\x18\x1d \x01(\x0b\x32&.shiftcrypto.bitbox02.BluetoothRequestH\x00\x42\t\n\x07requestJ\x04\x08\x01\x10\x02J\x04\x08\x17\x10\x18\"\xfd\x07\n\x08Response\x12\x30\n\x07success\x18\x01 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.SuccessH\x00\x12,\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x1b.shiftcrypto.bitbox02.ErrorH\x00\x12?\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32(.shiftcrypto.bitbox02.DeviceInfoResponseH\x00\x12\x30\n\x03pub\x18\x05 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x42\n\rbtc_sign_next\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x41\n\x0clist_backups\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ListBackupsResponseH\x00\x12\x41\n\x0c\x63heck_backup\x18\x08 \x01(\x0b\x32).shiftcrypto.bitbox02.CheckBackupResponseH\x00\x12O\n\x13perform_attestation\x18\t \x01(\x0b\x32\x30.shiftcrypto.bitbox02.PerformAttestationResponseH\x00\x12\x41\n\x0c\x63heck_sdcard\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.CheckSDCardResponseH\x00\x12\x30\n\x03\x65th\x18\x0b \x01(\x0b\x32!.shiftcrypto.bitbox02.ETHResponseH\x00\x12\x44\n\x0b\x66ingerprint\x18\x0c \x01(\x0b\x32-.shiftcrypto.bitbox02.RootFingerprintResponseH\x00\x12\x30\n\x03\x62tc\x18\r \x01(\x0b\x32!.shiftcrypto.bitbox02.BTCResponseH\x00\x12V\n\x17\x65lectrum_encryption_key\x18\x0e \x01(\x0b\x32\x33.shiftcrypto.bitbox02.ElectrumEncryptionKeyResponseH\x00\x12\x38\n\x07\x63\x61rdano\x18\x0f \x01(\x0b\x32%.shiftcrypto.bitbox02.CardanoResponseH\x00\x12\x34\n\x05\x62ip85\x18\x10 \x01(\x0b\x32#.shiftcrypto.bitbox02.BIP85ResponseH\x00\x12<\n\tbluetooth\x18\x11 \x01(\x0b\x32\'.shiftcrypto.bitbox02.BluetoothResponseH\x00\x42\n\n\x08responseJ\x04\x08\x03\x10\x04\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\thww.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x15\x62\x61\x63kup_commands.proto\x1a\x15\x62itbox02_system.proto\x1a\x0f\x62luetooth.proto\x1a\tbtc.proto\x1a\rcardano.proto\x1a\teth.proto\x1a\x0ekeystore.proto\x1a\x0emnemonic.proto\x1a\x0csystem.proto\x1a\x19perform_attestation.proto\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"\t\n\x07Success\"\x82\x0f\n\x07Request\x12\x41\n\x0b\x64\x65vice_name\x18\x02 \x01(\x0b\x32*.shiftcrypto.bitbox02.SetDeviceNameRequestH\x00\x12I\n\x0f\x64\x65vice_language\x18\x03 \x01(\x0b\x32..shiftcrypto.bitbox02.SetDeviceLanguageRequestH\x00\x12>\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32\'.shiftcrypto.bitbox02.DeviceInfoRequestH\x00\x12@\n\x0cset_password\x18\x05 \x01(\x0b\x32(.shiftcrypto.bitbox02.SetPasswordRequestH\x00\x12\x42\n\rcreate_backup\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.CreateBackupRequestH\x00\x12\x42\n\rshow_mnemonic\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ShowMnemonicRequestH\x00\x12\x36\n\x07\x62tc_pub\x18\x08 \x01(\x0b\x32#.shiftcrypto.bitbox02.BTCPubRequestH\x00\x12\x41\n\rbtc_sign_init\x18\t \x01(\x0b\x32(.shiftcrypto.bitbox02.BTCSignInitRequestH\x00\x12\x43\n\x0e\x62tc_sign_input\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignInputRequestH\x00\x12\x45\n\x0f\x62tc_sign_output\x18\x0b \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCSignOutputRequestH\x00\x12O\n\x14insert_remove_sdcard\x18\x0c \x01(\x0b\x32/.shiftcrypto.bitbox02.InsertRemoveSDCardRequestH\x00\x12@\n\x0c\x63heck_sdcard\x18\r \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckSDCardRequestH\x00\x12\x64\n\x1fset_mnemonic_passphrase_enabled\x18\x0e \x01(\x0b\x32\x39.shiftcrypto.bitbox02.SetMnemonicPassphraseEnabledRequestH\x00\x12@\n\x0clist_backups\x18\x0f \x01(\x0b\x32(.shiftcrypto.bitbox02.ListBackupsRequestH\x00\x12\x44\n\x0erestore_backup\x18\x10 \x01(\x0b\x32*.shiftcrypto.bitbox02.RestoreBackupRequestH\x00\x12N\n\x13perform_attestation\x18\x11 \x01(\x0b\x32/.shiftcrypto.bitbox02.PerformAttestationRequestH\x00\x12\x35\n\x06reboot\x18\x12 \x01(\x0b\x32#.shiftcrypto.bitbox02.RebootRequestH\x00\x12@\n\x0c\x63heck_backup\x18\x13 \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckBackupRequestH\x00\x12/\n\x03\x65th\x18\x14 \x01(\x0b\x32 .shiftcrypto.bitbox02.ETHRequestH\x00\x12\x33\n\x05reset\x18\x15 \x01(\x0b\x32\".shiftcrypto.bitbox02.ResetRequestH\x00\x12Q\n\x15restore_from_mnemonic\x18\x16 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.RestoreFromMnemonicRequestH\x00\x12\x43\n\x0b\x66ingerprint\x18\x18 \x01(\x0b\x32,.shiftcrypto.bitbox02.RootFingerprintRequestH\x00\x12/\n\x03\x62tc\x18\x19 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCRequestH\x00\x12U\n\x17\x65lectrum_encryption_key\x18\x1a \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ElectrumEncryptionKeyRequestH\x00\x12\x37\n\x07\x63\x61rdano\x18\x1b \x01(\x0b\x32$.shiftcrypto.bitbox02.CardanoRequestH\x00\x12\x33\n\x05\x62ip85\x18\x1c \x01(\x0b\x32\".shiftcrypto.bitbox02.BIP85RequestH\x00\x12;\n\tbluetooth\x18\x1d \x01(\x0b\x32&.shiftcrypto.bitbox02.BluetoothRequestH\x00\x12\x46\n\x0f\x63hange_password\x18\x1e \x01(\x0b\x32+.shiftcrypto.bitbox02.ChangePasswordRequestH\x00\x42\t\n\x07requestJ\x04\x08\x01\x10\x02J\x04\x08\x17\x10\x18\"\xfd\x07\n\x08Response\x12\x30\n\x07success\x18\x01 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.SuccessH\x00\x12,\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x1b.shiftcrypto.bitbox02.ErrorH\x00\x12?\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32(.shiftcrypto.bitbox02.DeviceInfoResponseH\x00\x12\x30\n\x03pub\x18\x05 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x42\n\rbtc_sign_next\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x41\n\x0clist_backups\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ListBackupsResponseH\x00\x12\x41\n\x0c\x63heck_backup\x18\x08 \x01(\x0b\x32).shiftcrypto.bitbox02.CheckBackupResponseH\x00\x12O\n\x13perform_attestation\x18\t \x01(\x0b\x32\x30.shiftcrypto.bitbox02.PerformAttestationResponseH\x00\x12\x41\n\x0c\x63heck_sdcard\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.CheckSDCardResponseH\x00\x12\x30\n\x03\x65th\x18\x0b \x01(\x0b\x32!.shiftcrypto.bitbox02.ETHResponseH\x00\x12\x44\n\x0b\x66ingerprint\x18\x0c \x01(\x0b\x32-.shiftcrypto.bitbox02.RootFingerprintResponseH\x00\x12\x30\n\x03\x62tc\x18\r \x01(\x0b\x32!.shiftcrypto.bitbox02.BTCResponseH\x00\x12V\n\x17\x65lectrum_encryption_key\x18\x0e \x01(\x0b\x32\x33.shiftcrypto.bitbox02.ElectrumEncryptionKeyResponseH\x00\x12\x38\n\x07\x63\x61rdano\x18\x0f \x01(\x0b\x32%.shiftcrypto.bitbox02.CardanoResponseH\x00\x12\x34\n\x05\x62ip85\x18\x10 \x01(\x0b\x32#.shiftcrypto.bitbox02.BIP85ResponseH\x00\x12<\n\tbluetooth\x18\x11 \x01(\x0b\x32\'.shiftcrypto.bitbox02.BluetoothResponseH\x00\x42\n\n\x08responseJ\x04\x08\x03\x10\x04\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hww_pb2', globals())
@@ -36,7 +36,7 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_SUCCESS._serialized_start=262
_SUCCESS._serialized_end=271
_REQUEST._serialized_start=274
- _REQUEST._serialized_end=2124
- _RESPONSE._serialized_start=2127
- _RESPONSE._serialized_end=3148
+ _REQUEST._serialized_end=2196
+ _RESPONSE._serialized_start=2199
+ _RESPONSE._serialized_end=3220
# @@protoc_insertion_point(module_scope)
diff --git a/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi
index b5b008f..3eb58cb 100644
--- a/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/hww_pb2.pyi
@@ -93,6 +93,7 @@ class Request(google.protobuf.message.Message):
CARDANO_FIELD_NUMBER: builtins.int
BIP85_FIELD_NUMBER: builtins.int
BLUETOOTH_FIELD_NUMBER: builtins.int
+ CHANGE_PASSWORD_FIELD_NUMBER: builtins.int
@property
def device_name(self) -> bitbox02_system_pb2.SetDeviceNameRequest:
"""removed: RandomNumberRequest random_number = 1;"""
@@ -151,6 +152,8 @@ class Request(google.protobuf.message.Message):
def bip85(self) -> keystore_pb2.BIP85Request: ...
@property
def bluetooth(self) -> bluetooth_pb2.BluetoothRequest: ...
+ @property
+ def change_password(self) -> bitbox02_system_pb2.ChangePasswordRequest: ...
def __init__(
self,
*,
@@ -181,10 +184,11 @@ class Request(google.protobuf.message.Message):
cardano: cardano_pb2.CardanoRequest | None = ...,
bip85: keystore_pb2.BIP85Request | None = ...,
bluetooth: bluetooth_pb2.BluetoothRequest | None = ...,
+ change_password: bitbox02_system_pb2.ChangePasswordRequest | None = ...,
) -> None: ...
- def HasField(self, field_name: typing.Literal["bip85", b"bip85", "bluetooth", b"bluetooth", "btc", b"btc", "btc_pub", b"btc_pub", "btc_sign_init", b"btc_sign_init", "btc_sign_input", b"btc_sign_input", "btc_sign_output", b"btc_sign_output", "cardano", b"cardano", "check_backup", b"check_backup", "check_sdcard", b"check_sdcard", "create_backup", b"create_backup", "device_info", b"device_info", "device_language", b"device_language", "device_name", b"device_name", "electrum_encryption_key", b"electrum_encryption_key", "eth", b"eth", "fingerprint", b"fingerprint", "insert_remove_sdcard", b"insert_remove_sdcard", "list_backups", b"list_backups", "perform_attestation", b"perform_attestation", "reboot", b"reboot", "request", b"request", "reset", b"reset", "restore_backup", b"restore_backup", "restore_from_mnemonic", b"restore_from_mnemonic", "set_mnemonic_passphrase_enabled", b"set_mnemonic_passphrase_enabled", "set_password", b"set_password", "show_mnemonic", b"show_mnemonic"]) -> builtins.bool: ...
- def ClearField(self, field_name: typing.Literal["bip85", b"bip85", "bluetooth", b"bluetooth", "btc", b"btc", "btc_pub", b"btc_pub", "btc_sign_init", b"btc_sign_init", "btc_sign_input", b"btc_sign_input", "btc_sign_output", b"btc_sign_output", "cardano", b"cardano", "check_backup", b"check_backup", "check_sdcard", b"check_sdcard", "create_backup", b"create_backup", "device_info", b"device_info", "device_language", b"device_language", "device_name", b"device_name", "electrum_encryption_key", b"electrum_encryption_key", "eth", b"eth", "fingerprint", b"fingerprint", "insert_remove_sdcard", b"insert_remove_sdcard", "list_backups", b"list_backups", "perform_attestation", b"perform_attestation", "reboot", b"reboot", "request", b"request", "reset", b"reset", "restore_backup", b"restore_backup", "restore_from_mnemonic", b"restore_from_mnemonic", "set_mnemonic_passphrase_enabled", b"set_mnemonic_passphrase_enabled", "set_password", b"set_password", "show_mnemonic", b"show_mnemonic"]) -> None: ...
- def WhichOneof(self, oneof_group: typing.Literal["request", b"request"]) -> typing.Literal["device_name", "device_language", "device_info", "set_password", "create_backup", "show_mnemonic", "btc_pub", "btc_sign_init", "btc_sign_input", "btc_sign_output", "insert_remove_sdcard", "check_sdcard", "set_mnemonic_passphrase_enabled", "list_backups", "restore_backup", "perform_attestation", "reboot", "check_backup", "eth", "reset", "restore_from_mnemonic", "fingerprint", "btc", "electrum_encryption_key", "cardano", "bip85", "bluetooth"] | None: ...
+ def HasField(self, field_name: typing.Literal["bip85", b"bip85", "bluetooth", b"bluetooth", "btc", b"btc", "btc_pub", b"btc_pub", "btc_sign_init", b"btc_sign_init", "btc_sign_input", b"btc_sign_input", "btc_sign_output", b"btc_sign_output", "cardano", b"cardano", "change_password", b"change_password", "check_backup", b"check_backup", "check_sdcard", b"check_sdcard", "create_backup", b"create_backup", "device_info", b"device_info", "device_language", b"device_language", "device_name", b"device_name", "electrum_encryption_key", b"electrum_encryption_key", "eth", b"eth", "fingerprint", b"fingerprint", "insert_remove_sdcard", b"insert_remove_sdcard", "list_backups", b"list_backups", "perform_attestation", b"perform_attestation", "reboot", b"reboot", "request", b"request", "reset", b"reset", "restore_backup", b"restore_backup", "restore_from_mnemonic", b"restore_from_mnemonic", "set_mnemonic_passphrase_enabled", b"set_mnemonic_passphrase_enabled", "set_password", b"set_password", "show_mnemonic", b"show_mnemonic"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["bip85", b"bip85", "bluetooth", b"bluetooth", "btc", b"btc", "btc_pub", b"btc_pub", "btc_sign_init", b"btc_sign_init", "btc_sign_input", b"btc_sign_input", "btc_sign_output", b"btc_sign_output", "cardano", b"cardano", "change_password", b"change_password", "check_backup", b"check_backup", "check_sdcard", b"check_sdcard", "create_backup", b"create_backup", "device_info", b"device_info", "device_language", b"device_language", "device_name", b"device_name", "electrum_encryption_key", b"electrum_encryption_key", "eth", b"eth", "fingerprint", b"fingerprint", "insert_remove_sdcard", b"insert_remove_sdcard", "list_backups", b"list_backups", "perform_attestation", b"perform_attestation", "reboot", b"reboot", "request", b"request", "reset", b"reset", "restore_backup", b"restore_backup", "restore_from_mnemonic", b"restore_from_mnemonic", "set_mnemonic_passphrase_enabled", b"set_mnemonic_passphrase_enabled", "set_password", b"set_password", "show_mnemonic", b"show_mnemonic"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["request", b"request"]) -> typing.Literal["device_name", "device_language", "device_info", "set_password", "create_backup", "show_mnemonic", "btc_pub", "btc_sign_init", "btc_sign_input", "btc_sign_output", "insert_remove_sdcard", "check_sdcard", "set_mnemonic_passphrase_enabled", "list_backups", "restore_backup", "perform_attestation", "reboot", "check_backup", "eth", "reset", "restore_from_mnemonic", "fingerprint", "btc", "electrum_encryption_key", "cardano", "bip85", "bluetooth", "change_password"] | None: ...
global___Request = Request
diff --git a/py/send_message.py b/py/send_message.py
index dfb74ae..784dc22 100755
--- a/py/send_message.py
+++ b/py/send_message.py
@@ -1519,6 +1519,14 @@ class SendMessage:
else:
print("Device NOT reset")
+ def _change_password_workflow(self) -> None:
+ """Initiate the change password workflow."""
+ try:
+ self._device.change_password()
+ print("Change password workflow completed")
+ except UserAbortException:
+ eprint("Aborted by user")
+
def _menu_notinit(self) -> None:
"""TODO: Document
@@ -1581,6 +1589,7 @@ class SendMessage:
("Upgrade Bluetooth firmware", self._bluetooth_upgrade),
("Toggle bluetooth", self._bluetooth_toggle_enabled),
("Reset Device", self._reset_device),
+ ("Change Password", self._change_password_workflow),
)
choice = ask_user(choices)
if isinstance(choice, bool):
diff --git a/src/rust/bitbox02-rust/src/hww/api.rs b/src/rust/bitbox02-rust/src/hww/api.rs
index 30b5741..d6a5810 100644
--- a/src/rust/bitbox02-rust/src/hww/api.rs
+++ b/src/rust/bitbox02-rust/src/hww/api.rs
@@ -28,6 +28,7 @@ mod cardano;
mod backup;
mod bip85;
mod bluetooth;
+mod change_password;
mod device_info;
mod electrum;
mod reset;
@@ -151,7 +152,8 @@ fn can_call(request: &Request) -> bool {
| Request::Eth(_)
| Request::Reset(_)
| Request::Cardano(_)
- | Request::Bip85(_) => {
+ | Request::Bip85(_)
+ | Request::ChangePassword(_) => {
matches!(state, State::InitializedAndUnlocked)
}
// These are streamed asynchronously using the `next_request()` primitive in
@@ -167,6 +169,7 @@ async fn process_api(hal: &mut impl crate::hal::Hal, request: &Request) -> Resul
Request::DeviceInfo(_) => device_info::process(),
Request::DeviceName(request) => set_device_name::process(hal, request).await,
Request::SetPassword(request) => set_password::process(hal, request).await,
+ Request::ChangePassword(_) => change_password::process(hal).await,
Request::Reset(_) => reset::process(hal).await,
Request::SetMnemonicPassphraseEnabled(request) => {
set_mnemonic_passphrase_enabled::process(hal, request).await
diff --git a/src/rust/bitbox02-rust/src/hww/api/change_password.rs b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
new file mode 100644
index 0000000..022570b
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
@@ -0,0 +1,229 @@
+use super::Error;
+use crate::pb;
+
+use pb::response::Response;
+
+use crate::hal::Ui;
+use crate::keystore;
+use crate::workflow::{confirm, password, unlock};
+
+pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
+ // Process confirmation and instruction for user
+ hal.ui()
+ .confirm(&confirm::Params {
+ title: "",
+ body: "Proceed to\nchange password?",
+ accept_is_nextarrow: true,
+ ..Default::default()
+ })
+ .await?;
+ // Unlock with old password
+ let seed = unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?;
+ // Enter and confirm new password
+ let new_password = password::enter_twice(hal).await?;
+
+ // Re-encrypt seed with new password
+ if let Err(err) = keystore::re_encrypt_seed(hal, &seed, &new_password) {
+ hal.ui().status(&format!("Error\n{:?}", err), false).await;
+ return Err(Error::Generic);
+ }
+
+ Ok(Response::Success(pb::Success {}))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::hal::testing::TestingHal;
+ use crate::workflow::{testing::Screen, unlock};
+ use alloc::boxed::Box;
+ use bitbox02::testing::mock_memory;
+ use hex_lit::hex;
+ use util::bb02_async::block_on;
+
+ // Test the intended success path
+ #[test]
+ fn test_process_success() {
+ //set up dummy (initialized, retained seed and bip39-seed)
+ mock_memory();
+ let seed = hex!("c7940c13479b8d9a6498f4e50d5a42e0d617bc8e8ac9f2b8cecf97e94c2b035c");
+ let old_password = "old_password";
+ let new_password = "new_password";
+
+ let mut hal = TestingHal::new();
+ keystore::encrypt_and_store_seed(&mut hal, &seed, old_password).unwrap();
+ block_on(unlock::unlock_bip39(&mut hal, &seed));
+ bitbox02::memory::set_initialized().unwrap();
+
+ // Allow exactly 3 prompts
+ let mut prompt_counter = 0u32;
+ hal.ui.set_enter_string(Box::new(|params| {
+ prompt_counter += 1;
+ match prompt_counter {
+ 1 => {
+ assert_eq!(params.title, "Unlock device");
+ Ok(old_password.into())
+ }
+ 2 => {
+ assert_eq!(params.title, "Set password");
+ Ok(new_password.into())
+ }
+ 3 => {
+ assert_eq!(params.title, "Repeat password");
+ Ok(new_password.into())
+ }
+ _ => panic!("unexpected password prompt"),
+ }
+ }));
+ // reset the chip counter
+ hal.securechip.event_counter_reset();
+ // call process
+ let result = block_on(process(&mut hal));
+ // assert success
+ assert_eq!(result, Ok(Response::Success(pb::Success {})));
+ // assert correct screens
+ let screens = hal.ui.screens.clone();
+ assert_eq!(
+ screens,
+ vec![
+ Screen::Confirm {
+ title: "".into(),
+ body: "Proceed to\nchange password?".into(),
+ longtouch: false,
+ },
+ Screen::Status {
+ title: "Success".into(),
+ success: true,
+ }
+ ]
+ );
+
+ let securechip_events = hal.securechip.get_event_counter();
+ drop(hal);
+ // We expect 14 secure chip events. This is intentionally brittle to catch
+ // unintended changes in the number of securechip operations during password change.
+ // If this fails after a legitimate change, update the expected count.
+ assert_eq!(securechip_events, 14);
+ assert_eq!(prompt_counter, 3);
+
+ // check that the old password is no longer valid
+ keystore::lock();
+ // create new hal instance to call unlock
+ let mut hal_verify = TestingHal::new();
+ assert!(matches!(
+ keystore::unlock(&mut hal_verify, old_password),
+ Err(keystore::Error::IncorrectPassword)
+ ));
+ // check that the new password is valid
+ assert_eq!(
+ keystore::unlock(&mut hal_verify, new_password)
+ .unwrap()
+ .as_slice(),
+ seed.as_slice()
+ );
+ }
+
+ // Test that we fail if the unlock fails
+ #[test]
+ fn test_process_unlock_failure() {
+ mock_memory();
+
+ let seed = hex!("c7940c13479b8d9a6498f4e50d5a42e0d617bc8e8ac9f2b8cecf97e94c2b035c");
+ let correct_password = "correct_password";
+
+ let mut hal = TestingHal::new();
+ keystore::encrypt_and_store_seed(&mut hal, &seed, correct_password).unwrap();
+ block_on(unlock::unlock_bip39(&mut hal, &seed));
+ bitbox02::memory::set_initialized().unwrap();
+ keystore::lock();
+
+ let mut prompt_counter = 0u32;
+ hal.ui.set_enter_string(Box::new(|params| {
+ prompt_counter += 1;
+ assert_eq!(params.title, "Unlock device");
+ Ok("wrong_password".into())
+ }));
+
+ hal.securechip.event_counter_reset();
+ let result = block_on(process(&mut hal));
+
+ assert_eq!(result, Err(Error::Generic));
+ assert_eq!(
+ hal.ui.screens,
+ vec![
+ Screen::Confirm {
+ title: "".into(),
+ body: "Proceed to\nchange password?".into(),
+ longtouch: false,
+ },
+ Screen::Status {
+ title: "Wrong password\n9 tries remain".into(),
+ success: false,
+ }
+ ]
+ );
+ // We expect 5 secure chip events (sensitive to code changes)
+ assert_eq!(hal.securechip.get_event_counter(), 5);
+
+ drop(hal);
+ assert_eq!(prompt_counter, 1);
+ // check that the old password is still valid
+ let mut hal_verify = TestingHal::new();
+ assert_eq!(
+ keystore::unlock(&mut hal_verify, correct_password)
+ .unwrap()
+ .as_slice(),
+ seed.as_slice()
+ );
+ }
+
+ // Test that we fail if the confirm password mismatch
+ #[test]
+ fn test_process_confirm_password_mismatch() {
+ mock_memory();
+
+ let seed = hex!("c7940c13479b8d9a6498f4e50d5a42e0d617bc8e8ac9f2b8cecf97e94c2b035c");
+ let old_password = "old_password";
+ let first_password = "first_password";
+ let second_password = "mismatch";
+
+ let mut hal = TestingHal::new();
+ keystore::encrypt_and_store_seed(&mut hal, &seed, old_password).unwrap();
+ block_on(unlock::unlock_bip39(&mut hal, &seed));
+ bitbox02::memory::set_initialized().unwrap();
+ keystore::lock();
+
+ let mut prompt_counter = 0u32;
+ hal.ui.set_enter_string(Box::new(|params| {
+ prompt_counter += 1;
+ match prompt_counter {
+ 1 => {
+ assert_eq!(params.title, "Unlock device");
+ Ok(old_password.into())
+ }
+ 2 => {
+ assert_eq!(params.title, "Set password");
+ Ok(first_password.into())
+ }
+ 3 => {
+ assert_eq!(params.title, "Repeat password");
+ Ok(second_password.into())
+ }
+ _ => panic!("unexpected password prompt"),
+ }
+ }));
+ let result = block_on(process(&mut hal));
+ drop(hal);
+
+ assert_eq!(result, Err(Error::Generic));
+ // check that the old password is still valid
+ let mut hal_verify = TestingHal::new();
+ assert_eq!(
+ keystore::unlock(&mut hal_verify, old_password)
+ .unwrap()
+ .as_slice(),
+ seed.as_slice()
+ );
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index c1a651c..854cfed 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -42,6 +42,7 @@ const LONG_TIMEOUT: i16 = -70;
#[derive(Debug)]
pub enum Error {
+ InvalidState,
CannotUnlockBIP39,
IncorrectPassword,
MaxAttemptsExceeded,
@@ -192,21 +193,27 @@ fn retain_seed(random: &mut impl crate::hal::Random, seed: &[u8]) -> Result<(),
Ok(())
}
-/// Restores a seed. This also unlocks the keystore with this seed.
-/// `password` is the password with which we encrypt the seed.
-pub fn encrypt_and_store_seed(
+fn retain_bip39_seed(random: &mut impl crate::hal::Random, bip39_seed: &[u8]) -> Result<(), Error> {
+ RETAINED_BIP39_SEED.write(Some(RetainedEncryptedBuffer::from_buffer(
+ random,
+ bip39_seed,
+ "keystore_retained_bip39_seed_access",
+ )?));
+ Ok(())
+}
+
+/// Internal helper to encrypt a seed with a password and store it on flash
+fn encrypt_and_store_seed_internal(
hal: &mut impl crate::hal::Hal,
seed: &[u8],
password: &str,
) -> Result<(), Error> {
- if bitbox02::memory::is_initialized() {
- return Err(Error::Memory);
- }
-
+ // Check that the seed is valid
if !matches!(seed.len(), 16 | 24 | 32) {
return Err(Error::SeedSize);
}
+ // Lock to ensure clean RAM
lock();
bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT);
@@ -233,6 +240,45 @@ pub fn encrypt_and_store_seed(
retain_seed(hal.random(), seed)
}
+/// Restores a seed. This also unlocks the keystore with this seed.
+/// `password` is the password with which we encrypt the seed.
+pub fn encrypt_and_store_seed(
+ hal: &mut impl crate::hal::Hal,
+ seed: &[u8],
+ password: &str,
+) -> Result<(), Error> {
+ if bitbox02::memory::is_initialized() {
+ return Err(Error::Memory);
+ }
+ encrypt_and_store_seed_internal(hal, seed, password)
+}
+
+/// Re-encrypts the seed with a (new) password
+pub fn re_encrypt_seed(
+ hal: &mut impl crate::hal::Hal,
+ seed: &[u8],
+ new_password: &str,
+) -> Result<(), Error> {
+ if !bitbox02::memory::is_seeded() {
+ return Err(Error::Unseeded);
+ }
+
+ // Store BIP39 seed and root fingerprint before re-encryption because:
+ // 1. The secure chip's internal keys are regenerated with the new password
+ // 2. encrypt_and_store_seed_internal calls lock() which clears BIP39 seed and root fingerprint
+ // 3. We want to avoid forcing the user to re-enter their BIP39 passphrase
+ let bip39_seed = copy_bip39_seed().map_err(|_| Error::InvalidState)?;
+ let root_fingerprint = ROOT_FINGERPRINT.read().ok_or(Error::InvalidState)?;
+
+ encrypt_and_store_seed_internal(hal, seed, new_password)?;
+
+ // Re-retain the bip39 seed and root fingerprint
+ retain_bip39_seed(hal.random(), bip39_seed.as_slice())?;
+ ROOT_FINGERPRINT.write(Some(root_fingerprint));
+
+ Ok(())
+}
+
// Checks if the retained seed matches the passed seed.
fn check_retained_seed(seed: &[u8]) -> Result<(), ()> {
if RETAINED_SEED.read().is_none() {
@@ -335,11 +381,7 @@ pub async fn unlock_bip39(
return Err(Error::Memory);
}
- RETAINED_BIP39_SEED.write(Some(RetainedEncryptedBuffer::from_buffer(
- random,
- bip39_seed.as_slice(),
- "keystore_retained_bip39_seed_access",
- )?));
+ retain_bip39_seed(random, bip39_seed.as_slice())?;
// Store root fingerprint.
ROOT_FINGERPRINT.write(Some(root_fingerprint));
@@ -838,6 +880,157 @@ mod tests {
}
}
+ #[test]
+ fn test_re_encrypt_seed() {
+ mock_memory();
+ lock();
+
+ let mut mock_hal = TestingHal::new();
+ let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
+
+ // Try to re-encrypt without seeding first
+ assert!(matches!(
+ re_encrypt_seed(&mut mock_hal, &seed, "new_password"),
+ Err(Error::Unseeded)
+ ));
+ }
+
+ #[test]
+ fn test_re_encrypt_seed_changes_password() {
+ mock_memory();
+ lock();
+
+ let mut mock_hal = TestingHal::new();
+ let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
+
+ // Step 1: Set up device with initial password
+ assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "old_password").is_ok());
+
+ // Step 2: Unlock with initial password and set up BIP39
+ let unlocked_seed = unlock(&mut mock_hal, "old_password").unwrap();
+ assert_eq!(unlocked_seed.as_slice(), seed.as_slice());
+
+ let mut random = crate::hal::testing::TestingRandom::new();
+ assert!(block_on(unlock_bip39(&mut random, &seed, "", async || {})).is_ok());
+
+ // Step 3: Re-encrypt with new password
+ assert!(re_encrypt_seed(&mut mock_hal, &seed, "new_password").is_ok());
+
+ // Step 4: Lock and verify old password no longer works
+ lock();
+ assert!(matches!(
+ unlock(&mut mock_hal, "old_password"),
+ Err(Error::IncorrectPassword)
+ ));
+
+ // Step 5: Verify new password works
+ let unlocked_seed_new = unlock(&mut mock_hal, "new_password").unwrap();
+ assert_eq!(unlocked_seed_new.as_slice(), seed.as_slice());
+ }
+
+ #[test]
+ fn test_re_encrypt_seed_preserves_seeds_and_fingerprint() {
+ mock_memory();
+ lock();
+
+ let mut mock_hal = TestingHal::new();
+ let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
+
+ // Initial setup
+ assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "password1").is_ok());
+
+ let mut random = crate::hal::testing::TestingRandom::new();
+ assert!(block_on(unlock_bip39(&mut random, &seed, "", async || {})).is_ok());
+
+ let seed_reference = copy_seed().unwrap();
+ let bip39_seed_reference = copy_bip39_seed().unwrap();
+ let root_fingerprint_reference = root_fingerprint().unwrap();
+
+ // Re-encrypt multiple times
+ for new_password in ["password2", "password3", "password4"] {
+ // re-encrypt
+ assert!(re_encrypt_seed(&mut mock_hal, &seed_reference, new_password).is_ok());
+
+ // Verify everything is still there and correct
+ assert_eq!(copy_seed().unwrap().as_slice(), seed_reference.as_slice());
+ assert_eq!(
+ copy_bip39_seed().unwrap().as_slice(),
+ bip39_seed_reference.as_slice()
+ );
+ assert_eq!(root_fingerprint().unwrap(), root_fingerprint_reference);
+ }
+ }
+
+ #[test]
+ fn test_re_encrypt_seed_invalid_seed_size() {
+ mock_memory();
+ lock();
+
+ let mut mock_hal = TestingHal::new();
+ let seed = hex!("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044");
+
+ // Initial setup
+ assert!(encrypt_and_store_seed(&mut mock_hal, &seed, "password").is_ok());
+ unlock(&mut mock_hal, "password").unwrap();
+
+ let mut random = crate::hal::testing::TestingRandom::new();
+ assert!(block_on(unlock_bip39(&mut random, &seed, "", async || {})).is_ok());
+
+ // Try to re-encrypt with invalid seed size
+ assert!(matches!(
+ re_encrypt_seed(&mut mock_hal, &[0u8; 31], "new_password"),
+ Err(Error::SeedSize)
+ ));
+ }
+
+ #[test]
+ fn test_retain_bip39_seed() {
+ mock_memory();
+ lock();
+
+ let mut random = crate::hal::testing::TestingRandom::new();
+ let bip39_seed = hex!(
+ "2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a"
+ );
+
+ // Before retention, should not be available
+ assert!(copy_bip39_seed().is_err());
+
+ // Retain the BIP39 seed
+ assert!(retain_bip39_seed(&mut random, &bip39_seed).is_ok());
+
+ // Should now be available
+ let retrieved = copy_bip39_seed().unwrap();
+ assert_eq!(retrieved.as_slice(), bip39_seed.as_slice());
+ }
+
+ #[test]
+ fn test_retain_bip39_seed_overwrites_previous() {
+ mock_memory();
+ lock();
+ let mut random = crate::hal::testing::TestingRandom::new();
+ let bip39_seed1 = hex!(
+ "1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
+ );
+ let bip39_seed2 = hex!(
+ "2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222"
+ );
+
+ // Retain first seed
+ assert!(retain_bip39_seed(&mut random, &bip39_seed1).is_ok());
+ assert_eq!(
+ copy_bip39_seed().unwrap().as_slice(),
+ bip39_seed1.as_slice()
+ );
+
+ // Retain second seed (should overwrite)
+ assert!(retain_bip39_seed(&mut random, &bip39_seed2).is_ok());
+ assert_eq!(
+ copy_bip39_seed().unwrap().as_slice(),
+ bip39_seed2.as_slice()
+ );
+ }
+
// This tests that you can create a keystore, unlock it, and then do this again. This is an
// expected workflow for when the wallet setup process is restarted after seeding and unlocking,
// but before creating a backup, in which case a new seed is created.
diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
index 3a5f532..88c0efa 100644
--- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
+++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
@@ -220,6 +220,9 @@ pub struct SetPasswordRequest {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
+pub struct ChangePasswordRequest {}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct BluetoothToggleEnabledRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
@@ -1975,7 +1978,7 @@ pub struct Success {}
pub struct Request {
#[prost(
oneof = "request::Request",
- tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29"
+ tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30"
)]
pub request: ::core::option::Option<request::Request>,
}
@@ -2040,6 +2043,8 @@ pub mod request {
Bip85(super::Bip85Request),
#[prost(message, tag = "29")]
Bluetooth(super::BluetoothRequest),
+ #[prost(message, tag = "30")]
+ ChangePassword(super::ChangePasswordRequest),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
Why this scored 36/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.