diff --git a/ports/stm32/boards/Passport/modules/tasks/restore_backup_task.py b/ports/stm32/boards/Passport/modules/tasks/restore_backup_task.py index 5c170db47..5f71c9629 100644 --- a/ports/stm32/boards/Passport/modules/tasks/restore_backup_task.py +++ b/ports/stm32/boards/Passport/modules/tasks/restore_backup_task.py @@ -20,6 +20,20 @@ from constants import MAX_BACKUP_FILE_SIZE from pincodes import SE_SECRET_LEN +NON_RESTORABLE_BACKUP_SETTINGS = ('bip39_passphrase', 'root_xfp', 'xfp', 'xpub') + + +def restore_settings_from_backup(vals, settings): + for k in vals: + if not k.startswith('setting.'): + continue + + setting_key = k[8:] + if setting_key in NON_RESTORABLE_BACKUP_SETTINGS: + continue + + settings.set(setting_key, vals[k]) + async def restore_backup_task(on_done, decryption_password, backup_file_path): from common import pa, settings @@ -114,17 +128,15 @@ async def restore_backup_task(on_done, decryption_password, backup_file_path): await pa.new_main_secret(raw, chain) # Finally, restore the settings - for idx, k in enumerate(vals): - if not k.startswith('setting.'): - continue - - if k == 'xfp' or k == 'xpub': - continue - - settings.set(k[8:], vals[k]) + restore_settings_from_backup(vals, settings) # This would be true in the old backup, but false for this new device settings.set('backup_quiz', False) + # Wallet identity metadata is derived from the restored secret, never trusted from the backup. + with stash.SensitiveValues(raw) as sv: + sv.chain = chain + sv.capture_xpub(save=True) + # Success! await on_done(None) diff --git a/ports/stm32/boards/Passport/modules/tests/test_unit.py b/ports/stm32/boards/Passport/modules/tests/test_unit.py index a088f75fb..3acf2050d 100644 --- a/ports/stm32/boards/Passport/modules/tests/test_unit.py +++ b/ports/stm32/boards/Passport/modules/tests/test_unit.py @@ -26,3 +26,7 @@ def test_ui(test): def test_foundation(test): assert test('foundation.py') == b'OK' + + +def test_restore_backup(test): + assert test('restore_backup.py') == b'OK' diff --git a/ports/stm32/boards/Passport/modules/tests/unit/restore_backup.py b/ports/stm32/boards/Passport/modules/tests/unit/restore_backup.py new file mode 100644 index 000000000..5e03a6084 --- /dev/null +++ b/ports/stm32/boards/Passport/modules/tests/unit/restore_backup.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 Foundation Devices, Inc. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Test backup restore settings filtering. + +from tasks.restore_backup_task import restore_settings_from_backup + + +class FakeSettings: + def __init__(self): + self.values = {} + + def set(self, key, value): + self.values[key] = value + + +vals = { + 'chain': 'BTC', + 'xfp': 'top-level metadata is ignored here', + 'setting.xfp': 0x11111111, + 'setting.xpub': 'attacker-xpub', + 'setting.root_xfp': 0x22222222, + 'setting.bip39_passphrase': 'runtime-only', + 'setting.units': 'sats', + 'setting.backup_quiz': True, +} + +settings = FakeSettings() +restore_settings_from_backup(vals, settings) + +assert settings.values == { + 'units': 'sats', + 'backup_quiz': True, +} + +return_value.write(b'OK')