From 8ef7dfab0dc381c80cf56d51311e6cf06e0e81ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ioan=20Biz=C4=83u?= Date: Tue, 23 Apr 2024 12:26:46 +0200 Subject: [PATCH] feat(core): implement repeated backup --- common/protob/messages-management.proto | 80 +-- core/.changelog.d/3640.added | 1 + core/embed/rust/librust_qstr.h | 4 + .../generated/translated_string.rs | 13 +- core/embed/rust/src/ui/model_tr/layout.rs | 15 +- core/embed/rust/src/ui/model_tt/layout.rs | 19 +- core/mocks/generated/trezorui2.pyi | 4 +- core/mocks/trezortranslate_keys.pyi | 4 +- core/src/all_modules.py | 4 + core/src/apps/base.py | 58 +- core/src/apps/management/backup_device.py | 16 +- .../management/recovery_device/__init__.py | 84 +-- .../management/recovery_device/homescreen.py | 131 ++++- .../apps/management/recovery_device/layout.py | 8 +- core/src/storage/cache.py | 27 +- core/src/storage/recovery.py | 29 +- core/src/trezor/enums/RecoveryKind.py | 7 + core/src/trezor/enums/RecoveryStatus.py | 8 + core/src/trezor/enums/__init__.py | 11 + core/src/trezor/messages.py | 10 +- core/src/trezor/ui/layouts/tr/recovery.py | 8 +- core/src/trezor/ui/layouts/tt/recovery.py | 6 +- core/tools/translations/rules.json | 1 - core/translations/cs.json | 1 - core/translations/de.json | 1 - core/translations/en.json | 4 +- core/translations/es.json | 1 - core/translations/fr.json | 1 - core/translations/order.json | 5 +- core/translations/signatures.json | 6 +- docs/common/message-workflows.md | 16 +- legacy/firmware/fsm_msg_common.h | 6 +- python/src/trezorlib/cli/device.py | 13 +- python/src/trezorlib/device.py | 31 +- python/src/trezorlib/messages.py | 25 +- rust/trezor-client/src/client/mod.rs | 6 +- .../protos/generated/messages_management.rs | 501 ++++++++++++------ tests/click_tests/recovery.py | 60 ++- tests/click_tests/reset.py | 6 +- .../click_tests/test_backup_slip39_custom.py | 2 +- tests/click_tests/test_recovery.py | 2 +- tests/click_tests/test_repeated_backup.py | 218 ++++++++ tests/click_tests/test_reset_bip39.py | 2 +- .../click_tests/test_reset_slip39_advanced.py | 4 +- tests/click_tests/test_reset_slip39_basic.py | 2 +- .../test_recovery_bip39_dryrun.py | 13 +- .../reset_recovery/test_recovery_bip39_t1.py | 8 +- tests/device_tests/test_repeated_backup.py | 110 ++++ tests/input_flows.py | 14 +- tests/input_flows_helpers.py | 6 + .../test_shamir_persistence.py | 25 +- tests/translations.py | 6 + tests/ui_tests/fixtures.json | 7 + tests/upgrade_tests/test_firmware_upgrades.py | 6 +- 54 files changed, 1259 insertions(+), 397 deletions(-) create mode 100644 core/.changelog.d/3640.added create mode 100644 core/src/trezor/enums/RecoveryKind.py create mode 100644 core/src/trezor/enums/RecoveryStatus.py create mode 100644 tests/click_tests/test_repeated_backup.py create mode 100644 tests/device_tests/test_repeated_backup.py diff --git a/common/protob/messages-management.proto b/common/protob/messages-management.proto index e13857a51e..3150f517f8 100644 --- a/common/protob/messages-management.proto +++ b/common/protob/messages-management.proto @@ -91,8 +91,38 @@ message Features { // optional bytes fw_vendor_keys = 26; // obsoleted, use fw_vendor optional bool unfinished_backup = 27; // report unfinished backup (equals to Storage.unfinished_backup) optional bool no_backup = 28; // report no backup (equals to Storage.no_backup) - optional bool recovery_mode = 29; // is recovery mode in progress + optional RecoveryStatus recovery_status = 29; // whether or not we are in recovery mode and of what kind repeated Capability capabilities = 30; // list of supported capabilities + optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced) + optional bool sd_card_present = 32; // is SD card present + optional bool sd_protection = 33; // is SD Protect enabled + optional bool wipe_code_protection = 34; // is wipe code protection enabled + optional bytes session_id = 35; + optional bool passphrase_always_on_device = 36; // device enforces passphrase entry on Trezor + optional SafetyCheckLevel safety_checks = 37; // safety check level, set to Prompt to limit path namespace enforcement + optional uint32 auto_lock_delay_ms = 38; // number of milliseconds after which the device locks itself + optional uint32 display_rotation = 39; // in degrees from North + optional bool experimental_features = 40; // are experimental message types enabled? + optional bool busy = 41; // is the device busy, showing "Do not disconnect"? + optional HomescreenFormat homescreen_format = 42; // format of the homescreen, 1 = TOIf, 2 = jpg, 3 = TOIG + optional bool hide_passphrase_from_host = 43; // should we hide the passphrase when it comes from host? + optional string internal_model = 44; // internal model name + optional uint32 unit_color = 45; // color of the unit/device + optional bool unit_btconly = 46; // unit/device is intended as bitcoin only + optional uint32 homescreen_width = 47; // homescreen width in pixels + optional uint32 homescreen_height = 48; // homescreen height in pixels + optional bool bootloader_locked = 49; // bootloader is locked + optional bool language_version_matches = 50 [default=true]; // translation blob version matches firmware version + optional uint32 unit_packaging = 51; // unit/device packaging version + optional bool haptic_feedback = 52; // haptic feedback is enabled + + enum RecoveryStatus { + NoRecovery = 0; + InNormalRecovery = 1; + InDryRunRecovery = 2; + InUnlockRepeatedBackupRecovery = 3; + } + enum Capability { option (has_bitcoin_only_values) = true; @@ -118,28 +148,6 @@ message Features { Capability_Brightness = 20 [(bitcoin_only) = true]; Capability_Haptic = 21 [(bitcoin_only) = true]; } - optional BackupType backup_type = 31; // type of device backup (BIP-39 / SLIP-39 basic / SLIP-39 advanced) - optional bool sd_card_present = 32; // is SD card present - optional bool sd_protection = 33; // is SD Protect enabled - optional bool wipe_code_protection = 34; // is wipe code protection enabled - optional bytes session_id = 35; - optional bool passphrase_always_on_device = 36; // device enforces passphrase entry on Trezor - optional SafetyCheckLevel safety_checks = 37; // safety check level, set to Prompt to limit path namespace enforcement - optional uint32 auto_lock_delay_ms = 38; // number of milliseconds after which the device locks itself - optional uint32 display_rotation = 39; // in degrees from North - optional bool experimental_features = 40; // are experimental message types enabled? - optional bool busy = 41; // is the device busy, showing "Do not disconnect"? - optional HomescreenFormat homescreen_format = 42; // format of the homescreen, 1 = TOIf, 2 = jpg, 3 = TOIG - optional bool hide_passphrase_from_host = 43; // should we hide the passphrase when it comes from host? - optional string internal_model = 44; // internal model name - optional uint32 unit_color = 45; // color of the unit/device - optional bool unit_btconly = 46; // unit/device is intended as bitcoin only - optional uint32 homescreen_width = 47; // homescreen width in pixels - optional uint32 homescreen_height = 48; // homescreen height in pixels - optional bool bootloader_locked = 49; // bootloader is locked - optional bool language_version_matches = 50 [default=true]; // translation blob version matches firmware version - optional uint32 unit_packaging = 51; // unit/device packaging version - optional bool haptic_feedback = 52; // haptic feedback is enabled } /** @@ -434,16 +442,16 @@ message EntropyAck { * @next WordRequest */ message RecoveryDevice { - optional uint32 word_count = 1; // number of words in BIP-39 mnemonic - optional bool passphrase_protection = 2; // enable master node encryption using passphrase - optional bool pin_protection = 3; // enable PIN protection - optional string language = 4 [deprecated=true]; // deprecated (use ChangeLanguage) - optional string label = 5; // device label - optional bool enforce_wordlist = 6; // enforce BIP-39 wordlist during the process - // 7 reserved for unused recovery method - optional RecoveryDeviceType type = 8; // supported recovery type - optional uint32 u2f_counter = 9; // U2F counter - optional bool dry_run = 10; // perform dry-run recovery workflow (for safe mnemonic validation) + optional uint32 word_count = 1; // number of words in BIP-39 mnemonic (T1 only) + optional bool passphrase_protection = 2; // enable master node encryption using passphrase + optional bool pin_protection = 3; // enable PIN protection + optional string language = 4 [deprecated=true]; // deprecated (use ChangeLanguage) + optional string label = 5; // device label + optional bool enforce_wordlist = 6; // enforce BIP-39 wordlist during the process (T1 only) + reserved 7; // unused recovery method + optional RecoveryDeviceType type = 8; // supported recovery type (T1 only) + optional uint32 u2f_counter = 9; // U2F counter + optional RecoveryKind kind = 10 [default=NormalRecovery]; // the kind of recovery to perform /** * Type of recovery procedure. These should be used as bitmask, e.g., * `RecoveryDeviceType_ScrambledWords | RecoveryDeviceType_Matrix` @@ -457,6 +465,12 @@ message RecoveryDevice { RecoveryDeviceType_ScrambledWords = 0; // words in scrambled order RecoveryDeviceType_Matrix = 1; // matrix recovery type } + + enum RecoveryKind { + NormalRecovery = 0; // recovery from seedphrase on an uninitialized device + DryRun = 1; // mnemonic validation + UnlockRepeatedBackup = 2; // unlock SLIP-39 repeated backup + } } /** diff --git a/core/.changelog.d/3640.added b/core/.changelog.d/3640.added new file mode 100644 index 0000000000..ec047d5b0a --- /dev/null +++ b/core/.changelog.d/3640.added @@ -0,0 +1 @@ +Added support for repeated backups. diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 257254cc5d..96218e25f2 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -293,6 +293,7 @@ static void _librust_qstrs(void) { MP_QSTR_joint__title; MP_QSTR_joint__to_the_total_amount; MP_QSTR_joint__you_are_contributing; + MP_QSTR_kind; MP_QSTR_label; MP_QSTR_language; MP_QSTR_language__change_to_template; @@ -440,7 +441,10 @@ static void _librust_qstrs(void) { MP_QSTR_recovery__title_dry_run; MP_QSTR_recovery__title_recover; MP_QSTR_recovery__title_remaining_shares; + MP_QSTR_recovery__title_unlock_repeated_backup; MP_QSTR_recovery__type_word_x_of_y_template; + MP_QSTR_recovery__unlock_repeated_backup; + MP_QSTR_recovery__unlock_repeated_backup_verb; MP_QSTR_recovery__wallet_recovered; MP_QSTR_recovery__wanna_cancel_dry_run; MP_QSTR_recovery__wanna_cancel_recovery; diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index 0485a6139e..60bde8df17 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -798,7 +798,7 @@ pub enum TranslatedString { recovery__num_of_words = 511, // "Select the number of words in your backup." recovery__only_first_n_letters = 512, // "You'll only have to select the first 2-4 letters of each word." recovery__progress_will_be_lost = 513, // "All progress will be lost." - recovery__select_num_of_words = 514, // "Select the number of words in your backup." + recovery__select_num_of_words = 514, // "\"\"" recovery__share_already_entered = 515, // "Share already entered" recovery__share_from_another_multi_share_backup = 516, // "You have entered a share from a different backup." recovery__share_num_template = 517, // "Share {0}" @@ -1330,6 +1330,9 @@ pub enum TranslatedString { words__try_again = 930, // "Try again." reset__slip39_checklist_num_groups_x_template = 931, // "Number of groups: {0}" brightness__title = 932, // "Set brightness" + recovery__title_unlock_repeated_backup = 933, // "Multi-share backup" + recovery__unlock_repeated_backup = 934, // "Do you want to unlock the seed for repeated backup?" + recovery__unlock_repeated_backup_verb = 935, // "Unlock backup" } impl TranslatedString { @@ -2123,7 +2126,7 @@ impl TranslatedString { Self::recovery__num_of_words => "Select the number of words in your backup.", Self::recovery__only_first_n_letters => "You'll only have to select the first 2-4 letters of each word.", Self::recovery__progress_will_be_lost => "All progress will be lost.", - Self::recovery__select_num_of_words => "Select the number of words in your backup.", + Self::recovery__select_num_of_words => "\"\"", Self::recovery__share_already_entered => "Share already entered", Self::recovery__share_from_another_multi_share_backup => "You have entered a share from a different backup.", Self::recovery__share_num_template => "Share {0}", @@ -2655,6 +2658,9 @@ impl TranslatedString { Self::words__try_again => "Try again.", Self::reset__slip39_checklist_num_groups_x_template => "Number of groups: {0}", Self::brightness__title => "Set brightness", + Self::recovery__title_unlock_repeated_backup => "Multi-share backup", + Self::recovery__unlock_repeated_backup => "Do you want to unlock the seed for repeated backup?", + Self::recovery__unlock_repeated_backup_verb => "Unlock backup", } } @@ -3981,6 +3987,9 @@ impl TranslatedString { Qstr::MP_QSTR_words__try_again => Some(Self::words__try_again), Qstr::MP_QSTR_reset__slip39_checklist_num_groups_x_template => Some(Self::reset__slip39_checklist_num_groups_x_template), Qstr::MP_QSTR_brightness__title => Some(Self::brightness__title), + Qstr::MP_QSTR_recovery__title_unlock_repeated_backup => Some(Self::recovery__title_unlock_repeated_backup), + Qstr::MP_QSTR_recovery__unlock_repeated_backup => Some(Self::recovery__unlock_repeated_backup), + Qstr::MP_QSTR_recovery__unlock_repeated_backup_verb => Some(Self::recovery__unlock_repeated_backup_verb), _ => None, } } diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 8f4fae3882..a1cda06cbe 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -228,6 +228,9 @@ impl ComponentMsgObj for super::component::bl_confirm::Confirm<'_> { } } +const RECOVERY_KIND_DRY_RUN: u32 = 1; +const RECOVERY_KIND_UNLOCK_REPEATED_BACKUP: u32 = 2; + /// Function to create and call a `ButtonPage` dialog based on paginable content /// (e.g. `Paragraphs` or `FormattedText`). /// Has optional title (supply empty `TString` for that) and hold-to-confirm @@ -1395,7 +1398,7 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut let block = move |_args: &[Obj], kwargs: &Map| { let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let button: TString<'static> = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; - let dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?; + let kind: u32 = kwargs.get(Qstr::MP_QSTR_kind)?.try_into()?; let show_info: bool = kwargs.get(Qstr::MP_QSTR_show_info)?.try_into()?; let mut paragraphs = ParagraphVecShort::new(); @@ -1412,10 +1415,10 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut )); } - let title = if dry_run { - TR::recovery__title_dry_run - } else { - TR::recovery__title + let title = match kind { + RECOVERY_KIND_DRY_RUN => TR::recovery__title_dry_run, + RECOVERY_KIND_UNLOCK_REPEATED_BACKUP => TR::recovery__title_unlock_repeated_backup, + _ => TR::recovery__title, }; content_in_button_page( @@ -1965,7 +1968,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, # unused on TR /// description: str, /// button: str, - /// dry_run: bool, + /// kind: int, # RecoveryKind enum, passed as an int /// info_button: bool, # unused on TR /// show_info: bool, /// ) -> LayoutObj[UiResult]: diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 494d68d42d..c56fd04510 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -310,6 +310,9 @@ impl ComponentMsgObj for super::component::bl_confirm::Confirm<'_> { } } +const RECOVERY_KIND_DRY_RUN: u32 = 1; +const RECOVERY_KIND_UNLOCK_REPEATED_BACKUP: u32 = 2; + extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -1365,7 +1368,7 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let button: TString = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?; - let dry_run: bool = kwargs.get(Qstr::MP_QSTR_dry_run)?.try_into()?; + let kind: u32 = kwargs.get(Qstr::MP_QSTR_kind)?.try_into()?; let info_button: bool = kwargs.get_or(Qstr::MP_QSTR_info_button, false)?; let paragraphs = Paragraphs::new([ @@ -1374,10 +1377,12 @@ extern "C" fn new_confirm_recovery(n_args: usize, args: *const Obj, kwargs: *mut ]) .with_spacing(theme::RECOVERY_SPACING); - let notification: TString = if dry_run { - TR::recovery__title_dry_run.into() - } else { - TR::recovery__title.into() + let notification = match kind { + RECOVERY_KIND_DRY_RUN => TR::recovery__title_dry_run.into(), + RECOVERY_KIND_UNLOCK_REPEATED_BACKUP => { + TR::recovery__title_unlock_repeated_backup.into() + } + _ => TR::recovery__title.into(), }; let obj = if info_button { @@ -1415,7 +1420,7 @@ extern "C" fn new_select_word_count(n_args: usize, args: *const Obj, kwargs: *mu let paragraphs = Paragraphs::new(Paragraph::new( &theme::TEXT_DEMIBOLD, - TR::recovery__select_num_of_words, + TR::recovery__num_of_words, )); let obj = LayoutObj::new(Frame::left_aligned( @@ -2042,7 +2047,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// description: str, /// button: str, - /// dry_run: bool, + /// kind: int, # RecoveryKind enum, passed as an int /// info_button: bool = False, /// ) -> LayoutObj[UiResult]: /// """Device recovery homescreen.""" diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 0bd64c86a5..d1bf1a6caf 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -980,7 +980,7 @@ def confirm_recovery( title: str, # unused on TR description: str, button: str, - dry_run: bool, + kind: int, # RecoveryKind enum, passed as an int info_button: bool, # unused on TR show_info: bool, ) -> LayoutObj[UiResult]: @@ -1515,7 +1515,7 @@ def confirm_recovery( title: str, description: str, button: str, - dry_run: bool, + kind: int, # RecoveryKind enum, passed as an int info_button: bool = False, ) -> LayoutObj[UiResult]: """Device recovery homescreen.""" diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index c1a18b1ff7..c072657a8b 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -573,7 +573,6 @@ class TR: recovery__num_of_words: str = "Select the number of words in your backup." recovery__only_first_n_letters: str = "You'll only have to select the first 2-4 letters of each word." recovery__progress_will_be_lost: str = "All progress will be lost." - recovery__select_num_of_words: str = "Select the number of words in your backup." recovery__share_already_entered: str = "Share already entered" recovery__share_does_not_match: str = "Share doesn't match" recovery__share_from_another_multi_share_backup: str = "You have entered a share from a different backup." @@ -584,7 +583,10 @@ class TR: recovery__title_dry_run: str = "Backup check" recovery__title_recover: str = "Recover wallet" recovery__title_remaining_shares: str = "Remaining shares" + recovery__title_unlock_repeated_backup: str = "Multi-share backup" recovery__type_word_x_of_y_template: str = "Type word {0} of {1}" + recovery__unlock_repeated_backup: str = "Do you want to unlock the seed for repeated backup?" + recovery__unlock_repeated_backup_verb: str = "Unlock backup" recovery__wallet_recovered: str = "Wallet recovery completed" recovery__wanna_cancel_dry_run: str = "Are you sure you want to cancel the backup check?" recovery__wanna_cancel_recovery: str = "Are you sure you want to cancel the recovery process?" diff --git a/core/src/all_modules.py b/core/src/all_modules.py index dcaf23bf02..2aa7062667 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -121,6 +121,10 @@ trezor.enums.PinMatrixRequestType import trezor.enums.PinMatrixRequestType trezor.enums.RecoveryDeviceType import trezor.enums.RecoveryDeviceType +trezor.enums.RecoveryKind +import trezor.enums.RecoveryKind +trezor.enums.RecoveryStatus +import trezor.enums.RecoveryStatus trezor.enums.RequestType import trezor.enums.RequestType trezor.enums.SafetyCheckLevel diff --git a/core/src/apps/base.py b/core/src/apps/base.py index fdf3f6afba..c25774d84e 100644 --- a/core/src/apps/base.py +++ b/core/src/apps/base.py @@ -60,7 +60,7 @@ def _language_version_matches() -> bool | None: def get_features() -> Features: import storage.recovery as storage_recovery from trezor import translations - from trezor.enums import Capability + from trezor.enums import Capability, RecoveryKind, RecoveryStatus from trezor.messages import Features from trezor.ui import HEIGHT, WIDTH @@ -160,7 +160,16 @@ def get_features() -> Features: f.unfinished_backup = storage_device.unfinished_backup() f.no_backup = storage_device.no_backup() f.flags = storage_device.get_flags() - f.recovery_mode = storage_recovery.is_in_progress() + if storage_recovery.is_in_progress(): + kind = storage_recovery.get_kind() + if kind == RecoveryKind.NormalRecovery: + f.recovery_status = RecoveryStatus.InNormalRecovery + elif kind == RecoveryKind.DryRun: + f.recovery_status = RecoveryStatus.InDryRunRecovery + elif kind == RecoveryKind.UnlockRepeatedBackup: + f.recovery_status = RecoveryStatus.InUnlockRepeatedBackupRecovery + else: + f.recovery_status = RecoveryStatus.NoRecovery f.backup_type = mnemonic.get_type() # Only some models are capable of SD card @@ -216,6 +225,7 @@ async def handle_GetFeatures(msg: GetFeatures) -> Features: async def handle_Cancel(msg: Cancel) -> Success: + workflow.close_others() raise wire.ActionCancelled @@ -370,7 +380,7 @@ def set_homescreen() -> None: def lock_device(interrupt_workflow: bool = True) -> None: if config.has_pin(): config.lock() - wire.find_handler = get_pinlocked_handler + wire.find_handler = _get_pinlocked_handler set_homescreen() if interrupt_workflow: workflow.close_others() @@ -409,7 +419,7 @@ async def unlock_device() -> None: wire.find_handler = workflow_handlers.find_registered_handler -def get_pinlocked_handler( +def _get_pinlocked_handler( iface: wire.WireInterface, msg_type: int ) -> wire.Handler[wire.Msg] | None: orig_handler = workflow_handlers.find_registered_handler(iface, msg_type) @@ -432,6 +442,38 @@ def get_pinlocked_handler( return wrapper +_ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED = ( + MessageType.Initialize, + MessageType.GetFeatures, + MessageType.EndSession, + MessageType.BackupDevice, + MessageType.WipeDevice, + MessageType.Cancel, +) + + +def _get_backup_handler( + iface: wire.WireInterface, msg_type: int +) -> wire.Handler[wire.Msg] | None: + orig_handler = workflow_handlers.find_registered_handler(iface, msg_type) + if orig_handler is None: + return None + + if __debug__: + import usb + + if iface is usb.iface_debug: + return orig_handler + + if msg_type in _ALLOW_WHILE_REPEATED_BACKUP_UNLOCKED: + return orig_handler + + async def wrapper(_msg: protobuf.MessageType) -> protobuf.MessageType: + raise wire.ProcessError("Operation not allowed when in repeated backup state") + + return wrapper + + # this function is also called when handling ApplySettings def reload_settings_from_storage() -> None: from trezor import ui @@ -464,7 +506,9 @@ def boot() -> None: workflow_handlers.register(msg_type, handler) reload_settings_from_storage() - if config.is_unlocked(): - wire.find_handler = workflow_handlers.find_registered_handler + if not config.is_unlocked(): + wire.find_handler = _get_pinlocked_handler + elif storage_cache.get_bool(storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED): + wire.find_handler = _get_backup_handler else: - wire.find_handler = get_pinlocked_handler + wire.find_handler = workflow_handlers.find_registered_handler diff --git a/core/src/apps/management/backup_device.py b/core/src/apps/management/backup_device.py index 6bbe866d19..bfa34a7c63 100644 --- a/core/src/apps/management/backup_device.py +++ b/core/src/apps/management/backup_device.py @@ -10,17 +10,25 @@ BAK_T_BIP39 = BackupType.Bip39 # global_import_cache async def backup_device(msg: BackupDevice) -> Success: + import storage.cache as storage_cache import storage.device as storage_device from trezor import wire from trezor.messages import Success + from apps import workflow_handlers from apps.common import backup_types, mnemonic from .reset_device import backup_seed, backup_slip39_custom, layout + # do this early before we show any UI + # the homescreen will clear the flag right after its own UI is gone + repeated_backup_unlocked = storage_cache.get_bool( + storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED + ) + if not storage_device.is_initialized(): raise wire.NotInitialized("Device is not initialized") - if not storage_device.needs_backup(): + if not storage_device.needs_backup() and not repeated_backup_unlocked: raise wire.ProcessError("Seed already backed up") mnemonic_secret, backup_type = mnemonic.get() @@ -40,7 +48,10 @@ async def backup_device(msg: BackupDevice) -> Success: elif len(groups) > 0: raise wire.DataError("group_threshold is missing") - storage_device.set_unfinished_backup(True) + if not repeated_backup_unlocked: + storage_device.set_unfinished_backup(True) + + storage_cache.delete(storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED) storage_device.set_backed_up() if group_threshold is not None: @@ -51,6 +62,7 @@ async def backup_device(msg: BackupDevice) -> Success: storage_device.set_unfinished_backup(False) + wire.find_handler = workflow_handlers.find_registered_handler await layout.show_backup_success() return Success(message="Seed successfully backed up") diff --git a/core/src/apps/management/recovery_device/__init__.py b/core/src/apps/management/recovery_device/__init__.py index a4f349ba8b..c52dd721fe 100644 --- a/core/src/apps/management/recovery_device/__init__.py +++ b/core/src/apps/management/recovery_device/__init__.py @@ -1,12 +1,14 @@ from typing import TYPE_CHECKING +from trezor.enums import RecoveryKind + if TYPE_CHECKING: from trezor.messages import RecoveryDevice, Success # List of RecoveryDevice fields that can be set when doing dry-run recovery. -# All except `dry_run` are allowed for T1 compatibility, but their values are ignored. +# All except `kind` are allowed for T1 compatibility, but their values are ignored. # If set, `enforce_wordlist` must be True, because we do not support non-enforcing. -DRY_RUN_ALLOWED_FIELDS = ("dry_run", "word_count", "enforce_wordlist", "type") +DRY_RUN_ALLOWED_FIELDS = ("kind", "word_count", "enforce_wordlist", "type") async def recovery_device(msg: RecoveryDevice) -> Success: @@ -20,9 +22,10 @@ async def recovery_device(msg: RecoveryDevice) -> Success: import storage.device as storage_device import storage.recovery as storage_recovery from trezor import TR, config, wire, workflow - from trezor.enums import ButtonRequestType + from trezor.enums import BackupType, ButtonRequestType from trezor.ui.layouts import confirm_action, confirm_reset_device + from apps.common import mnemonic from apps.common.request_pin import ( error_pin_invalid, request_pin_and_sd_salt, @@ -31,68 +34,79 @@ async def recovery_device(msg: RecoveryDevice) -> Success: from .homescreen import recovery_homescreen, recovery_process - dry_run = msg.dry_run # local_cache_attribute + recovery_kind = msg.kind # local_cache_attribute # -------------------------------------------------------- # validate - if not dry_run and storage_device.is_initialized(): - raise wire.UnexpectedMessage("Already initialized") - if dry_run and not storage_device.is_initialized(): - raise wire.NotInitialized("Device is not initialized") - if msg.enforce_wordlist is False: - raise wire.ProcessError( - "Value enforce_wordlist must be True, Trezor Core enforces words automatically." - ) - if dry_run: + if recovery_kind == RecoveryKind.NormalRecovery: + if storage_device.is_initialized(): + raise wire.UnexpectedMessage("Already initialized") + elif recovery_kind in (RecoveryKind.DryRun, RecoveryKind.UnlockRepeatedBackup): + if not storage_device.is_initialized(): + raise wire.NotInitialized("Device is not initialized") + if ( + recovery_kind == RecoveryKind.UnlockRepeatedBackup + and mnemonic.get_type() == BackupType.Bip39 + ): + raise wire.ProcessError("Repeated Backup not available for BIP39 backups") # check that only allowed fields are set for key, value in msg.__dict__.items(): if key not in DRY_RUN_ALLOWED_FIELDS and value is not None: raise wire.ProcessError(f"Forbidden field set in dry-run: {key}") + else: + raise RuntimeError # Unknown RecoveryKind + + if msg.enforce_wordlist is False: + raise wire.ProcessError( + "Value enforce_wordlist must be True, Trezor Core enforces words automatically." + ) # END validate # -------------------------------------------------------- if storage_recovery.is_in_progress(): return await recovery_process() - # -------------------------------------------------------- - # _continue_dialog - if not dry_run: + if recovery_kind == RecoveryKind.NormalRecovery: await confirm_reset_device(TR.recovery__title_recover, recovery=True) - else: - await confirm_action( - "confirm_seedcheck", - TR.recovery__title_dry_run, - description=TR.recovery__check_dry_run, - br_code=ButtonRequestType.ProtectCall, - verb=TR.buttons__check, - ) - # END _continue_dialog - # -------------------------------------------------------- - if not dry_run: # wipe storage to make sure the device is in a clear state storage.reset() - # for dry run pin needs to be entered - if dry_run: - curpin, salt = await request_pin_and_sd_salt(TR.pin__enter) - if not config.check_pin(curpin, salt): - await error_pin_invalid() - - if not dry_run: # set up pin if requested if msg.pin_protection: newpin = await request_pin_confirm(allow_cancel=False) config.change_pin("", newpin, None, None) storage_device.set_passphrase_enabled(bool(msg.passphrase_protection)) + if msg.u2f_counter is not None: storage_device.set_u2f_counter(msg.u2f_counter) + if msg.label is not None: storage_device.set_label(msg.label) + elif recovery_kind in (RecoveryKind.DryRun, RecoveryKind.UnlockRepeatedBackup): + title = ( + TR.recovery__title_dry_run + if recovery_kind == RecoveryKind.DryRun + else TR.recovery__title_unlock_repeated_backup + ) + await confirm_action( + "confirm_seedcheck", + title, + description=TR.recovery__check_dry_run, + br_code=ButtonRequestType.ProtectCall, + verb=TR.buttons__check, + ) + + curpin, salt = await request_pin_and_sd_salt(TR.pin__enter) + if not config.check_pin(curpin, salt): + await error_pin_invalid() + storage_recovery.set_in_progress(True) - storage_recovery.set_dry_run(bool(dry_run)) + + storage_recovery.set_kind(int(recovery_kind)) workflow.set_default(recovery_homescreen) + return await recovery_process() diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index 774876e59f..df5614a84d 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING +import storage.cache as storage_cache import storage.device as storage_device import storage.recovery as storage_recovery import storage.recovery_shares as storage_recovery_shares @@ -11,7 +12,7 @@ from apps.common import backup_types from . import layout, recover if TYPE_CHECKING: - from trezor.enums import BackupType + from trezor.enums import BackupType, RecoveryKind async def recovery_homescreen() -> None: @@ -19,35 +20,84 @@ async def recovery_homescreen() -> None: from apps.homescreen import homescreen - if not storage_recovery.is_in_progress(): + if storage_cache.get_bool(storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED): + await _continue_repeated_backup() + elif not storage_recovery.is_in_progress(): workflow.set_default(homescreen) - return - - await recovery_process() + else: + await recovery_process() async def recovery_process() -> Success: import storage - from trezor.enums import MessageType + from trezor.enums import MessageType, RecoveryKind - wire.AVOID_RESTARTING_FOR = (MessageType.Initialize, MessageType.GetFeatures) + is_special_kind = storage_recovery.get_kind() in ( + RecoveryKind.DryRun, + RecoveryKind.UnlockRepeatedBackup, + ) + + wire.AVOID_RESTARTING_FOR = ( + MessageType.Initialize, + MessageType.GetFeatures, + MessageType.EndSession, + ) try: return await _continue_recovery_process() except recover.RecoveryAborted: - dry_run = storage_recovery.is_dry_run() - if dry_run: + if is_special_kind: storage_recovery.end_progress() else: storage.wipe() raise wire.ActionCancelled +async def _continue_repeated_backup() -> None: + from trezor import workflow + from trezor.enums import ButtonRequestType, MessageType + from trezor.ui.layouts import confirm_action + from trezor.wire import ActionCancelled + + from apps import workflow_handlers + from apps.common import mnemonic + from apps.homescreen import homescreen + from apps.management.reset_device import backup_seed + + wire.AVOID_RESTARTING_FOR = ( + MessageType.Initialize, + MessageType.GetFeatures, + MessageType.EndSession, + ) + + try: + await confirm_action( + "confirm_repeated_backup", + TR.recovery__title_unlock_repeated_backup, + description=TR.recovery__unlock_repeated_backup, + br_code=ButtonRequestType.ProtectCall, + verb=TR.recovery__unlock_repeated_backup_verb, + ) + + mnemonic_secret, backup_type = mnemonic.get() + if mnemonic_secret is None: + raise RuntimeError + + await backup_seed(backup_type, mnemonic_secret) + except ActionCancelled: + workflow.set_default(homescreen) + finally: + storage_cache.delete(storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED) + wire.find_handler = workflow_handlers.find_registered_handler + storage_recovery.end_progress() + + async def _continue_recovery_process() -> Success: from trezor import utils + from trezor.enums import RecoveryKind from trezor.errors import MnemonicError # gather the current recovery state from storage - dry_run = storage_recovery.is_dry_run() + kind = storage_recovery.get_kind() word_count, backup_type = recover.load_slip39_state() # Both word_count and backup_type are derived from the same data. Both will be @@ -59,7 +109,7 @@ async def _continue_recovery_process() -> Success: if not is_first_step: assert word_count is not None # If we continue recovery, show starting screen with word count immediately. - await _request_share_first_screen(word_count) + await _request_share_first_screen(word_count, kind) secret = None while secret is None: @@ -72,9 +122,9 @@ async def _continue_recovery_process() -> Success: TR.buttons__continue, TR.recovery__num_of_words ) # ask for the number of words - word_count = await layout.request_word_count(dry_run) + word_count = await layout.request_word_count(kind == RecoveryKind.DryRun) # ...and only then show the starting screen with word count. - await _request_share_first_screen(word_count) + await _request_share_first_screen(word_count, kind) assert word_count is not None # ask for mnemonic words one by one @@ -94,23 +144,22 @@ async def _continue_recovery_process() -> Success: await layout.show_invalid_mnemonic(word_count) assert backup_type is not None - if dry_run: + if kind == RecoveryKind.DryRun: result = await _finish_recovery_dry_run(secret, backup_type) + elif kind == RecoveryKind.UnlockRepeatedBackup: + result = await _finish_recovery_unlock_repeated_backup(secret, backup_type) else: result = await _finish_recovery(secret, backup_type) return result -async def _finish_recovery_dry_run(secret: bytes, backup_type: BackupType) -> Success: +def _check_secret_against_stored_secret(secret: bytes, is_slip39: bool, backup_type: BackupType) -> bool: from trezor import utils from trezor.crypto.hashlib import sha256 from apps.common import mnemonic - if backup_type is None: - raise RuntimeError - digest_input = sha256(secret).digest() stored = mnemonic.get_secret() digest_stored = sha256(stored).digest() @@ -132,6 +181,17 @@ async def _finish_recovery_dry_run(secret: bytes, backup_type: BackupType) -> Su == storage_recovery.get_slip39_iteration_exponent() ) + return result + + +async def _finish_recovery_dry_run(secret: bytes, backup_type: BackupType) -> Success: + if backup_type is None: + raise RuntimeError + + is_slip39 = backup_types.is_slip39_backup_type(backup_type) + + result = _check_secret_against_stored_secret(secret, is_slip39, backup_type) + storage_recovery.end_progress() await layout.show_dry_run_result(result, is_slip39) @@ -142,6 +202,27 @@ async def _finish_recovery_dry_run(secret: bytes, backup_type: BackupType) -> Su raise wire.ProcessError("The seed does not match the one in the device") +async def _finish_recovery_unlock_repeated_backup( + secret: bytes, backup_type: BackupType +) -> Success: + import storage.cache as storage_cache + + if backup_type is None: + raise RuntimeError + + is_slip39 = backup_types.is_slip39_backup_type(backup_type) + + result = _check_secret_against_stored_secret(secret, is_slip39, backup_type) + + if result: + storage_cache.set_bool( + storage_cache.APP_RECOVERY_REPEATED_BACKUP_UNLOCKED, True + ) + return Success(message="Backup unlocked") + else: + raise wire.ProcessError("The seed does not match the one in the device") + + async def _finish_recovery(secret: bytes, backup_type: BackupType) -> Success: from trezor.ui.layouts import show_success @@ -191,15 +272,23 @@ async def _process_words(words: str) -> tuple[bytes | None, BackupType]: return secret, backup_type -async def _request_share_first_screen(word_count: int) -> None: +async def _request_share_first_screen(word_count: int, kind: RecoveryKind) -> None: + from trezor.enums import RecoveryKind + if backup_types.is_slip39_word_count(word_count): remaining = storage_recovery.fetch_slip39_remaining_shares() if remaining: await _request_share_next_screen() else: + if kind == RecoveryKind.UnlockRepeatedBackup: + text = TR.recovery__enter_backup + button_label = TR.buttons__continue + else: + text = TR.recovery__enter_any_share + button_label = TR.buttons__enter_share await layout.homescreen_dialog( - TR.buttons__enter_share, - TR.recovery__enter_any_share, + button_label, + text, TR.recovery__word_count_template.format(word_count), show_info=True, ) diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index 086b5da7ec..1475d669de 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -149,21 +149,23 @@ async def homescreen_dialog( show_info: bool = False, ) -> None: import storage.recovery as storage_recovery + from trezor.enums import RecoveryKind from trezor.ui.layouts.recovery import continue_recovery from trezor.wire import ActionCancelled from .recover import RecoveryAborted + kind = storage_recovery.get_kind() + while True: - dry_run = storage_recovery.is_dry_run() if await continue_recovery( - button_label, text, subtext, info_func, dry_run, show_info + button_label, text, subtext, info_func, kind, show_info ): # go forward in the recovery process break # user has chosen to abort, confirm the choice try: - await _confirm_abort(dry_run) + await _confirm_abort(kind != RecoveryKind.NormalRecovery) except ActionCancelled: pass else: diff --git a/core/src/storage/cache.py b/core/src/storage/cache.py index 1e1afdd845..ee36b3e60d 100644 --- a/core/src/storage/cache.py +++ b/core/src/storage/cache.py @@ -34,7 +34,7 @@ APP_COMMON_REQUEST_PIN_LAST_UNLOCK = const(3 | _SESSIONLESS_FLAG) APP_COMMON_BUSY_DEADLINE_MS = const(4 | _SESSIONLESS_FLAG) APP_MISC_COSI_NONCE = const(5 | _SESSIONLESS_FLAG) APP_MISC_COSI_COMMITMENT = const(6 | _SESSIONLESS_FLAG) - +APP_RECOVERY_REPEATED_BACKUP_UNLOCKED = const(7 | _SESSIONLESS_FLAG) # === Homescreen storage === # This does not logically belong to the "cache" functionality, but the cache module is @@ -57,7 +57,7 @@ class InvalidSessionError(Exception): class DataCache: - fields: Sequence[int] + fields: Sequence[int] # field sizes def __init__(self) -> None: self.data = [bytearray(f + 1) for f in self.fields] @@ -145,6 +145,7 @@ class SessionlessCache(DataCache): 8, # APP_COMMON_BUSY_DEADLINE_MS 32, # APP_MISC_COSI_NONCE 32, # APP_MISC_COSI_COMMITMENT + 0, # APP_RECOVERY_REPEATED_BACKUP_UNLOCKED ) super().__init__() @@ -233,13 +234,17 @@ def set(key: int, value: bytes) -> None: _SESSIONS[_active_session_idx].set(key, value) -def set_int(key: int, value: int) -> None: +def _get_length(key: int) -> int: if key & _SESSIONLESS_FLAG: - length = _SESSIONLESS_CACHE.fields[key ^ _SESSIONLESS_FLAG] + return _SESSIONLESS_CACHE.fields[key ^ _SESSIONLESS_FLAG] elif _active_session_idx is None: raise InvalidSessionError else: - length = _SESSIONS[_active_session_idx].fields[key] + return _SESSIONS[_active_session_idx].fields[key] + + +def set_int(key: int, value: int) -> None: + length = _get_length(key) encoded = value.to_bytes(length, "big") @@ -250,6 +255,14 @@ def set_int(key: int, value: int) -> None: set(key, encoded) +def set_bool(key: int, value: bool) -> None: + assert _get_length(key) == 0 # skipping get_length in production build + if value: + set(key, b"") + else: + delete(key) + + if TYPE_CHECKING: @overload @@ -276,6 +289,10 @@ def get_int(key: int, default: T | None = None) -> int | T | None: # noqa: F811 return int.from_bytes(encoded, "big") +def get_bool(key: int) -> bool: # noqa: F811 + return get(key) is not None + + def get_int_all_sessions(key: int) -> builtins.set[int]: sessions = [_SESSIONLESS_CACHE] if key & _SESSIONLESS_FLAG else _SESSIONS values = builtins.set() diff --git a/core/src/storage/recovery.py b/core/src/storage/recovery.py index 6816209fc6..17c63b9192 100644 --- a/core/src/storage/recovery.py +++ b/core/src/storage/recovery.py @@ -1,14 +1,18 @@ from micropython import const +from typing import TYPE_CHECKING from storage import common +if TYPE_CHECKING: + from trezor.enums import RecoveryKind + # Namespace: _NAMESPACE = common.APP_RECOVERY # fmt: off # Keys: _IN_PROGRESS = const(0x00) # bool -_DRY_RUN = const(0x01) # bool +_KIND = const(0x01) # int _SLIP39_IDENTIFIER = const(0x03) # bytes _REMAINING = const(0x05) # int _SLIP39_ITERATION_EXPONENT = const(0x06) # int @@ -37,14 +41,27 @@ def is_in_progress() -> bool: return common.get_bool(_NAMESPACE, _IN_PROGRESS) -def set_dry_run(val: bool) -> None: +def set_kind(val: int) -> None: _require_progress() - common.set_bool(_NAMESPACE, _DRY_RUN, val) + common.set_uint8(_NAMESPACE, _KIND, val) -def is_dry_run() -> bool: +def get_kind() -> RecoveryKind: + from trezor.enums import RecoveryKind + _require_progress() - return common.get_bool(_NAMESPACE, _DRY_RUN) + recovery_kind = common.get_uint8(_NAMESPACE, _KIND) + if recovery_kind is None: + recovery_kind = RecoveryKind.NormalRecovery + + if recovery_kind not in ( + RecoveryKind.NormalRecovery, + RecoveryKind.DryRun, + RecoveryKind.UnlockRepeatedBackup, + ): + # Invalid recovery kind + raise RuntimeError + return recovery_kind def set_slip39_identifier(identifier: int) -> None: @@ -129,7 +146,7 @@ def end_progress() -> None: _require_progress() for key in ( _IN_PROGRESS, - _DRY_RUN, + _KIND, _SLIP39_IDENTIFIER, _REMAINING, _SLIP39_ITERATION_EXPONENT, diff --git a/core/src/trezor/enums/RecoveryKind.py b/core/src/trezor/enums/RecoveryKind.py new file mode 100644 index 0000000000..8806a3063a --- /dev/null +++ b/core/src/trezor/enums/RecoveryKind.py @@ -0,0 +1,7 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +NormalRecovery = 0 +DryRun = 1 +UnlockRepeatedBackup = 2 diff --git a/core/src/trezor/enums/RecoveryStatus.py b/core/src/trezor/enums/RecoveryStatus.py new file mode 100644 index 0000000000..828fd7f89e --- /dev/null +++ b/core/src/trezor/enums/RecoveryStatus.py @@ -0,0 +1,8 @@ +# Automatically generated by pb2py +# fmt: off +# isort:skip_file + +NoRecovery = 0 +InNormalRecovery = 1 +InDryRunRecovery = 2 +InUnlockRepeatedBackupRecovery = 3 diff --git a/core/src/trezor/enums/__init__.py b/core/src/trezor/enums/__init__.py index 757ecf5acf..96fea755b1 100644 --- a/core/src/trezor/enums/__init__.py +++ b/core/src/trezor/enums/__init__.py @@ -440,6 +440,12 @@ if TYPE_CHECKING: Jpeg = 2 ToiG = 3 + class RecoveryStatus(IntEnum): + NoRecovery = 0 + InNormalRecovery = 1 + InDryRunRecovery = 2 + InUnlockRepeatedBackupRecovery = 3 + class Capability(IntEnum): Bitcoin = 1 Bitcoin_like = 2 @@ -472,6 +478,11 @@ if TYPE_CHECKING: ScrambledWords = 0 Matrix = 1 + class RecoveryKind(IntEnum): + NormalRecovery = 0 + DryRun = 1 + UnlockRepeatedBackup = 2 + class WordRequestType(IntEnum): Plain = 0 Matrix9 = 1 diff --git a/core/src/trezor/messages.py b/core/src/trezor/messages.py index e8afe64751..610e4eb01d 100644 --- a/core/src/trezor/messages.py +++ b/core/src/trezor/messages.py @@ -54,6 +54,8 @@ if TYPE_CHECKING: from trezor.enums import OutputScriptType # noqa: F401 from trezor.enums import PinMatrixRequestType # noqa: F401 from trezor.enums import RecoveryDeviceType # noqa: F401 + from trezor.enums import RecoveryKind # noqa: F401 + from trezor.enums import RecoveryStatus # noqa: F401 from trezor.enums import RequestType # noqa: F401 from trezor.enums import SafetyCheckLevel # noqa: F401 from trezor.enums import SdProtectOperationType # noqa: F401 @@ -2140,7 +2142,7 @@ if TYPE_CHECKING: fw_vendor: "str | None" unfinished_backup: "bool | None" no_backup: "bool | None" - recovery_mode: "bool | None" + recovery_status: "RecoveryStatus | None" capabilities: "list[Capability]" backup_type: "BackupType | None" sd_card_present: "bool | None" @@ -2194,7 +2196,7 @@ if TYPE_CHECKING: fw_vendor: "str | None" = None, unfinished_backup: "bool | None" = None, no_backup: "bool | None" = None, - recovery_mode: "bool | None" = None, + recovery_status: "RecoveryStatus | None" = None, backup_type: "BackupType | None" = None, sd_card_present: "bool | None" = None, sd_protection: "bool | None" = None, @@ -2600,7 +2602,7 @@ if TYPE_CHECKING: enforce_wordlist: "bool | None" type: "RecoveryDeviceType | None" u2f_counter: "int | None" - dry_run: "bool | None" + kind: "RecoveryKind" def __init__( self, @@ -2612,7 +2614,7 @@ if TYPE_CHECKING: enforce_wordlist: "bool | None" = None, type: "RecoveryDeviceType | None" = None, u2f_counter: "int | None" = None, - dry_run: "bool | None" = None, + kind: "RecoveryKind | None" = None, ) -> None: pass diff --git a/core/src/trezor/ui/layouts/tr/recovery.py b/core/src/trezor/ui/layouts/tr/recovery.py index db40ae9efc..dd5cf3da3c 100644 --- a/core/src/trezor/ui/layouts/tr/recovery.py +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -2,7 +2,7 @@ from typing import Callable, Iterable import trezorui2 from trezor import TR -from trezor.enums import ButtonRequestType +from trezor.enums import ButtonRequestType, RecoveryKind from ..common import interact from . import RustLayout, raise_if_not_confirmed, show_warning @@ -76,7 +76,7 @@ async def continue_recovery( text: str, subtext: str | None, info_func: Callable | None, - dry_run: bool, + kind: RecoveryKind, show_info: bool = False, ) -> bool: # TODO: implement info_func? @@ -84,7 +84,7 @@ async def continue_recovery( # (and having middle button would mean shortening the right button text) # Never showing info for dry-run, user already saw it and it is disturbing - if dry_run: + if kind in (RecoveryKind.DryRun, RecoveryKind.UnlockRepeatedBackup): show_info = False if subtext: @@ -95,8 +95,8 @@ async def continue_recovery( title="", description=text, button=button_label, + kind=kind, info_button=False, - dry_run=dry_run, show_info=show_info, # type: ignore [No parameter named "show_info"] ) ) diff --git a/core/src/trezor/ui/layouts/tt/recovery.py b/core/src/trezor/ui/layouts/tt/recovery.py index 82310480a9..5607ceea8c 100644 --- a/core/src/trezor/ui/layouts/tt/recovery.py +++ b/core/src/trezor/ui/layouts/tt/recovery.py @@ -2,7 +2,7 @@ from typing import Callable, Iterable import trezorui2 from trezor import TR -from trezor.enums import ButtonRequestType +from trezor.enums import ButtonRequestType, RecoveryKind from ..common import interact from . import RustLayout, raise_if_not_confirmed @@ -116,7 +116,7 @@ async def continue_recovery( text: str, subtext: str | None, info_func: Callable | None, - dry_run: bool, + kind: RecoveryKind, show_info: bool = False, # unused on TT ) -> bool: from ..common import button_request @@ -132,8 +132,8 @@ async def continue_recovery( title=text, description=description, button=button_label, + kind=kind, info_button=info_func is not None, - dry_run=dry_run, ) ) diff --git a/core/tools/translations/rules.json b/core/tools/translations/rules.json index 7b87a61fcd..cb550232b5 100644 --- a/core/tools/translations/rules.json +++ b/core/tools/translations/rules.json @@ -516,7 +516,6 @@ "recovery__num_of_words": "text,2", "recovery__only_first_n_letters": "text,4", "recovery__progress_will_be_lost": "text,2", - "recovery__select_num_of_words": "text,3", "recovery__share_already_entered": "text,2", "recovery__share_from_another_multi_share_backup": "text,3", "recovery__share_num_template": "text,1", diff --git a/core/translations/cs.json b/core/translations/cs.json index c2beae0e28..5b9dc29524 100644 --- a/core/translations/cs.json +++ b/core/translations/cs.json @@ -562,7 +562,6 @@ "recovery__num_of_words": "Vyberte počet slov v záloze.", "recovery__only_first_n_letters": "Musíte vybrat pouze první 2-4 písmena každého slova.", "recovery__progress_will_be_lost": "Veškerý postup bude ztracen.", - "recovery__select_num_of_words": "Vyberte počet slov v záloze.", "recovery__share_already_entered": "Část jste již zadali", "recovery__share_from_another_multi_share_backup": "Zadali jste část z jiné zálohy Shamir.", "recovery__share_num_template": "Část {0}", diff --git a/core/translations/de.json b/core/translations/de.json index 683e5273c5..ba52b1a4ac 100644 --- a/core/translations/de.json +++ b/core/translations/de.json @@ -562,7 +562,6 @@ "recovery__num_of_words": "Wörteranzahl in deinem Backup auswählen.", "recovery__only_first_n_letters": "Du musst nur die ersten 2-4 Buchstaben jedes Worts auswählen.", "recovery__progress_will_be_lost": "Alle Fortschritte gehen verloren.", - "recovery__select_num_of_words": "Wörteranzahl in deinem Backup auswählen.", "recovery__share_already_entered": "Share wurde bereits eingegeben", "recovery__share_from_another_multi_share_backup": "Share eines anderen Shamir-Backups eingegeben.", "recovery__share_num_template": "Share {0}", diff --git a/core/translations/en.json b/core/translations/en.json index ddb44814b0..0b22682da8 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -577,7 +577,6 @@ "recovery__num_of_words": "Select the number of words in your backup.", "recovery__only_first_n_letters": "You'll only have to select the first 2-4 letters of each word.", "recovery__progress_will_be_lost": "All progress will be lost.", - "recovery__select_num_of_words": "Select the number of words in your backup.", "recovery__share_already_entered": "Share already entered", "recovery__share_does_not_match": "Share doesn't match", "recovery__share_from_another_multi_share_backup": "You have entered a share from a different backup.", @@ -588,7 +587,10 @@ "recovery__title_dry_run": "Backup check", "recovery__title_recover": "Recover wallet", "recovery__title_remaining_shares": "Remaining shares", + "recovery__title_unlock_repeated_backup": "Multi-share backup", "recovery__type_word_x_of_y_template": "Type word {0} of {1}", + "recovery__unlock_repeated_backup": "Do you want to unlock the seed for repeated backup?", + "recovery__unlock_repeated_backup_verb": "Unlock backup", "recovery__wallet_recovered": "Wallet recovery completed", "recovery__wanna_cancel_dry_run": "Are you sure you want to cancel the backup check?", "recovery__wanna_cancel_recovery": "Are you sure you want to cancel the recovery process?", diff --git a/core/translations/es.json b/core/translations/es.json index fb42195c67..24ba4726b9 100644 --- a/core/translations/es.json +++ b/core/translations/es.json @@ -562,7 +562,6 @@ "recovery__num_of_words": "Elige el nro.de p. de la copia seg.", "recovery__only_first_n_letters": "Solo tendrás que seleccionar las primeras 2-4 letras de cada palabra.", "recovery__progress_will_be_lost": "Se perderá todo el progreso.", - "recovery__select_num_of_words": "Elige el nro.de p. de la copia seg.", "recovery__share_already_entered": "Ya se ha introducido el recurso compartido", "recovery__share_from_another_multi_share_backup": "El rec. comp. es de otra copia de seguridad de Shamir.", "recovery__share_num_template": "Recurso compartido {0}", diff --git a/core/translations/fr.json b/core/translations/fr.json index 8ce8d483e5..f3854d1333 100644 --- a/core/translations/fr.json +++ b/core/translations/fr.json @@ -562,7 +562,6 @@ "recovery__num_of_words": "Sélect. le nbre de mots dans votre sauv.", "recovery__only_first_n_letters": "Il vous suffit de sélectionner les 2 ou 4 premières lettres de chaque mot.", "recovery__progress_will_be_lost": "Toute progression sera perdue.", - "recovery__select_num_of_words": "Sélect. le nbre de mots dans votre sauv.", "recovery__share_already_entered": "Fragment déjà saisi", "recovery__share_from_another_multi_share_backup": "Vous avez saisi un fragm. d'une autre sauv. Shamir.", "recovery__share_num_template": "Fragment {0}", diff --git a/core/translations/order.json b/core/translations/order.json index 6915cbcc3d..172418c8df 100644 --- a/core/translations/order.json +++ b/core/translations/order.json @@ -931,5 +931,8 @@ "929": "words__settings", "930": "words__try_again", "931": "reset__slip39_checklist_num_groups_x_template", - "932": "brightness__title" + "932": "brightness__title", + "933": "recovery__title_unlock_repeated_backup", + "934": "recovery__unlock_repeated_backup", + "935": "recovery__unlock_repeated_backup_verb" } diff --git a/core/translations/signatures.json b/core/translations/signatures.json index 7515775c18..070161e6a7 100644 --- a/core/translations/signatures.json +++ b/core/translations/signatures.json @@ -1,8 +1,8 @@ { "current": { - "merkle_root": "1e9cb4df51f267f250b45ba4c130f2e46487dc60d8f5e0e19bd398ec4a613f24", - "datetime": "2024-05-28T09:19:32.720044", - "commit": "ebb35e8de3ea870b13d85891096136c16bac2cc5" + "merkle_root": "45979e4f395fcad03d843869023d35047483512ec797063806998dc553823c72", + "datetime": "2024-06-03T12:43:57.548072", + "commit": "510121bad342d1f900ca123e131448d577a25e39" }, "history": [ { diff --git a/docs/common/message-workflows.md b/docs/common/message-workflows.md index 7a72f9be4f..e0fbabc713 100644 --- a/docs/common/message-workflows.md +++ b/docs/common/message-workflows.md @@ -279,14 +279,24 @@ entering PIN) or standard recovery (with entering the seed to the host computer one by one in random order). The process continues with optional check of the seed validity and optional setting up the PIN, which has to be confirmed. Finally the recovered wallet is saved into -device storage. The same process is used with the dry run recovery, the +device storage. + +The same process is used with the dry run recovery, the differences are that this process can be done only with already -initialized deviice and that the mnemonic is not saved into the device +initialized device and that the mnemonic is not saved into the device but it is only compared to the mnemonic already loaded into the device with the successful result (The seed is valid and matches the one in the -device) or unsuccessful result(The seed is valid but does not match the +device) or unsuccessful result (The seed is valid but does not match the one in the device). +A third kind of recovery is one that is done in order to +unlock a repeated backup. This is similar to the dry run recovery in that +the device needs to be already initialized and that the mnemonic entered +is compared against the one stored in the device. Once successful, +a special mode is activated, which allows an additional backup +to be performed. This is useful for upgrading SLIP39 backups +to multiple shares. + ## LoadDevice Load device lets user to load the device with the specific recovery diff --git a/legacy/firmware/fsm_msg_common.h b/legacy/firmware/fsm_msg_common.h index 77a6ae43be..43fe926b31 100644 --- a/legacy/firmware/fsm_msg_common.h +++ b/legacy/firmware/fsm_msg_common.h @@ -491,7 +491,11 @@ void fsm_msgApplyFlags(const ApplyFlags *msg) { void fsm_msgRecoveryDevice(const RecoveryDevice *msg) { CHECK_PIN_UNCACHED - const bool dry_run = msg->has_dry_run ? msg->dry_run : false; + CHECK_PARAM(!msg->has_kind || msg->kind == RecoveryKind_NormalRecovery || + msg->kind == RecoveryKind_DryRun, + _("UnlockRepeatedBackup not supported")) + + const bool dry_run = msg->has_kind ? msg->kind == RecoveryKind_DryRun : false; if (!dry_run) { CHECK_NOT_INITIALIZED } else { diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py index 1949749f64..47dc313307 100644 --- a/python/src/trezorlib/cli/device.py +++ b/python/src/trezorlib/cli/device.py @@ -151,6 +151,7 @@ def load( "-t", "--type", "rec_type", type=ChoiceType(RECOVERY_TYPE), default="scrambled" ) @click.option("-d", "--dry-run", is_flag=True) +@click.option("-b", "--unlock-repeated-backup", is_flag=True) @with_client def recover( client: "TrezorClient", @@ -162,6 +163,7 @@ def recover( u2f_counter: int, rec_type: messages.RecoveryDeviceType, dry_run: bool, + unlock_repeated_backup: bool, ) -> "MessageType": """Start safe recovery workflow.""" if rec_type == messages.RecoveryDeviceType.ScrambledWords: @@ -170,6 +172,15 @@ def recover( input_callback = ui.matrix_words click.echo(ui.RECOVERY_MATRIX_DESCRIPTION) + if dry_run and unlock_repeated_backup: + raise click.ClickException("Cannot use -d and -b together.") + + recovery_kind = None + if dry_run: + recovery_kind = messages.RecoveryKind.DryRun + if unlock_repeated_backup: + recovery_kind = messages.RecoveryKind.UnlockRepeatedBackup + return device.recover( client, word_count=int(words), @@ -179,7 +190,7 @@ def recover( u2f_counter=u2f_counter, input_callback=input_callback, type=rec_type, - dry_run=dry_run, + recovery_kind=recovery_kind, ) diff --git a/python/src/trezorlib/device.py b/python/src/trezorlib/device.py index af473442be..dabec6f8e9 100644 --- a/python/src/trezorlib/device.py +++ b/python/src/trezorlib/device.py @@ -160,8 +160,10 @@ def recover( language: Optional[str] = None, input_callback: Optional[Callable] = None, type: messages.RecoveryDeviceType = messages.RecoveryDeviceType.ScrambledWords, - dry_run: bool = False, + dry_run: Optional[bool] = None, u2f_counter: Optional[int] = None, + *, + recovery_kind: Optional[messages.RecoveryKind] = None, ) -> "MessageType": if language is not None: warnings.warn( @@ -169,13 +171,34 @@ def recover( DeprecationWarning, ) + if dry_run is not None: + warnings.warn( + "Use recovery_kind=RecoveryKind.DryRun instead!", + DeprecationWarning, + ) + + if recovery_kind is not None: + raise ValueError( + "Cannot use both dry_run and recovery_kind simultaneously." + ) + elif dry_run: + recovery_kind = messages.RecoveryKind.DryRun + else: + recovery_kind = messages.RecoveryKind.NormalRecovery + + if recovery_kind is None: + recovery_kind = messages.RecoveryKind.NormalRecovery + if client.features.model == "1" and input_callback is None: raise RuntimeError("Input callback required for Trezor One") if word_count not in (12, 18, 24): raise ValueError("Invalid word count. Use 12/18/24") - if client.features.initialized and not dry_run: + if ( + client.features.initialized + and recovery_kind == messages.RecoveryKind.NormalRecovery + ): raise RuntimeError( "Device already initialized. Call device.wipe() and try again." ) @@ -184,10 +207,10 @@ def recover( u2f_counter = int(time.time()) msg = messages.RecoveryDevice( - word_count=word_count, enforce_wordlist=True, type=type, dry_run=dry_run + word_count=word_count, enforce_wordlist=True, type=type, kind=recovery_kind ) - if not dry_run: + if recovery_kind == messages.RecoveryKind.NormalRecovery: # set additional parameters msg.passphrase_protection = passphrase_protection msg.pin_protection = pin_protection diff --git a/python/src/trezorlib/messages.py b/python/src/trezorlib/messages.py index bd50df1918..e2df9676c8 100644 --- a/python/src/trezorlib/messages.py +++ b/python/src/trezorlib/messages.py @@ -472,6 +472,13 @@ class HomescreenFormat(IntEnum): ToiG = 3 +class RecoveryStatus(IntEnum): + NoRecovery = 0 + InNormalRecovery = 1 + InDryRunRecovery = 2 + InUnlockRepeatedBackupRecovery = 3 + + class Capability(IntEnum): Bitcoin = 1 Bitcoin_like = 2 @@ -507,6 +514,12 @@ class RecoveryDeviceType(IntEnum): Matrix = 1 +class RecoveryKind(IntEnum): + NormalRecovery = 0 + DryRun = 1 + UnlockRepeatedBackup = 2 + + class WordRequestType(IntEnum): Plain = 0 Matrix9 = 1 @@ -3241,7 +3254,7 @@ class Features(protobuf.MessageType): 25: protobuf.Field("fw_vendor", "string", repeated=False, required=False, default=None), 27: protobuf.Field("unfinished_backup", "bool", repeated=False, required=False, default=None), 28: protobuf.Field("no_backup", "bool", repeated=False, required=False, default=None), - 29: protobuf.Field("recovery_mode", "bool", repeated=False, required=False, default=None), + 29: protobuf.Field("recovery_status", "RecoveryStatus", repeated=False, required=False, default=None), 30: protobuf.Field("capabilities", "Capability", repeated=True, required=False, default=None), 31: protobuf.Field("backup_type", "BackupType", repeated=False, required=False, default=None), 32: protobuf.Field("sd_card_present", "bool", repeated=False, required=False, default=None), @@ -3297,7 +3310,7 @@ class Features(protobuf.MessageType): fw_vendor: Optional["str"] = None, unfinished_backup: Optional["bool"] = None, no_backup: Optional["bool"] = None, - recovery_mode: Optional["bool"] = None, + recovery_status: Optional["RecoveryStatus"] = None, backup_type: Optional["BackupType"] = None, sd_card_present: Optional["bool"] = None, sd_protection: Optional["bool"] = None, @@ -3348,7 +3361,7 @@ class Features(protobuf.MessageType): self.fw_vendor = fw_vendor self.unfinished_backup = unfinished_backup self.no_backup = no_backup - self.recovery_mode = recovery_mode + self.recovery_status = recovery_status self.backup_type = backup_type self.sd_card_present = sd_card_present self.sd_protection = sd_protection @@ -3783,7 +3796,7 @@ class RecoveryDevice(protobuf.MessageType): 6: protobuf.Field("enforce_wordlist", "bool", repeated=False, required=False, default=None), 8: protobuf.Field("type", "RecoveryDeviceType", repeated=False, required=False, default=None), 9: protobuf.Field("u2f_counter", "uint32", repeated=False, required=False, default=None), - 10: protobuf.Field("dry_run", "bool", repeated=False, required=False, default=None), + 10: protobuf.Field("kind", "RecoveryKind", repeated=False, required=False, default=RecoveryKind.NormalRecovery), } def __init__( @@ -3797,7 +3810,7 @@ class RecoveryDevice(protobuf.MessageType): enforce_wordlist: Optional["bool"] = None, type: Optional["RecoveryDeviceType"] = None, u2f_counter: Optional["int"] = None, - dry_run: Optional["bool"] = None, + kind: Optional["RecoveryKind"] = RecoveryKind.NormalRecovery, ) -> None: self.word_count = word_count self.passphrase_protection = passphrase_protection @@ -3807,7 +3820,7 @@ class RecoveryDevice(protobuf.MessageType): self.enforce_wordlist = enforce_wordlist self.type = type self.u2f_counter = u2f_counter - self.dry_run = dry_run + self.kind = kind class WordRequest(protobuf.MessageType): diff --git a/rust/trezor-client/src/client/mod.rs b/rust/trezor-client/src/client/mod.rs index 052a2fa8ad..400ea62c6e 100644 --- a/rust/trezor-client/src/client/mod.rs +++ b/rust/trezor-client/src/client/mod.rs @@ -169,7 +169,11 @@ impl Trezor { req.set_pin_protection(pin_protection); req.set_label(label); req.set_enforce_wordlist(true); - req.set_dry_run(dry_run); + if dry_run { + req.set_kind(protos::recovery_device::RecoveryKind::DryRun); + } else { + req.set_kind(protos::recovery_device::RecoveryKind::NormalRecovery); + } req.set_type( protos::recovery_device::RecoveryDeviceType::RecoveryDeviceType_ScrambledWords, ); diff --git a/rust/trezor-client/src/protos/generated/messages_management.rs b/rust/trezor-client/src/protos/generated/messages_management.rs index 4961d6cb78..f2f23992b4 100644 --- a/rust/trezor-client/src/protos/generated/messages_management.rs +++ b/rust/trezor-client/src/protos/generated/messages_management.rs @@ -416,8 +416,8 @@ pub struct Features { pub unfinished_backup: ::std::option::Option, // @@protoc_insertion_point(field:hw.trezor.messages.management.Features.no_backup) pub no_backup: ::std::option::Option, - // @@protoc_insertion_point(field:hw.trezor.messages.management.Features.recovery_mode) - pub recovery_mode: ::std::option::Option, + // @@protoc_insertion_point(field:hw.trezor.messages.management.Features.recovery_status) + pub recovery_status: ::std::option::Option<::protobuf::EnumOrUnknown>, // @@protoc_insertion_point(field:hw.trezor.messages.management.Features.capabilities) pub capabilities: ::std::vec::Vec<::protobuf::EnumOrUnknown>, // @@protoc_insertion_point(field:hw.trezor.messages.management.Features.backup_type) @@ -1110,23 +1110,26 @@ impl Features { self.no_backup = ::std::option::Option::Some(v); } - // optional bool recovery_mode = 29; + // optional .hw.trezor.messages.management.Features.RecoveryStatus recovery_status = 29; - pub fn recovery_mode(&self) -> bool { - self.recovery_mode.unwrap_or(false) + pub fn recovery_status(&self) -> features::RecoveryStatus { + match self.recovery_status { + Some(e) => e.enum_value_or(features::RecoveryStatus::NoRecovery), + None => features::RecoveryStatus::NoRecovery, + } } - pub fn clear_recovery_mode(&mut self) { - self.recovery_mode = ::std::option::Option::None; + pub fn clear_recovery_status(&mut self) { + self.recovery_status = ::std::option::Option::None; } - pub fn has_recovery_mode(&self) -> bool { - self.recovery_mode.is_some() + pub fn has_recovery_status(&self) -> bool { + self.recovery_status.is_some() } // Param is passed by value, moved - pub fn set_recovery_mode(&mut self, v: bool) { - self.recovery_mode = ::std::option::Option::Some(v); + pub fn set_recovery_status(&mut self, v: features::RecoveryStatus) { + self.recovery_status = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); } // optional .hw.trezor.messages.management.BackupType backup_type = 31; @@ -1724,9 +1727,9 @@ impl Features { |m: &mut Features| { &mut m.no_backup }, )); fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "recovery_mode", - |m: &Features| { &m.recovery_mode }, - |m: &mut Features| { &mut m.recovery_mode }, + "recovery_status", + |m: &Features| { &m.recovery_status }, + |m: &mut Features| { &mut m.recovery_status }, )); fields.push(::protobuf::reflect::rt::v2::make_vec_simpler_accessor::<_, _>( "capabilities", @@ -1949,7 +1952,7 @@ impl ::protobuf::Message for Features { self.no_backup = ::std::option::Option::Some(is.read_bool()?); }, 232 => { - self.recovery_mode = ::std::option::Option::Some(is.read_bool()?); + self.recovery_status = ::std::option::Option::Some(is.read_enum_or_unknown()?); }, 240 => { self.capabilities.push(is.read_enum_or_unknown()?); @@ -2113,8 +2116,8 @@ impl ::protobuf::Message for Features { if let Some(v) = self.no_backup { my_size += 2 + 1; } - if let Some(v) = self.recovery_mode { - my_size += 2 + 1; + if let Some(v) = self.recovery_status { + my_size += ::protobuf::rt::int32_size(29, v.value()); } for value in &self.capabilities { my_size += ::protobuf::rt::int32_size(30, value.value()); @@ -2269,8 +2272,8 @@ impl ::protobuf::Message for Features { if let Some(v) = self.no_backup { os.write_bool(28, v)?; } - if let Some(v) = self.recovery_mode { - os.write_bool(29, v)?; + if let Some(v) = self.recovery_status { + os.write_enum(29, ::protobuf::EnumOrUnknown::value(&v))?; } for v in &self.capabilities { os.write_enum(30, ::protobuf::EnumOrUnknown::value(v))?; @@ -2384,7 +2387,7 @@ impl ::protobuf::Message for Features { self.fw_vendor = ::std::option::Option::None; self.unfinished_backup = ::std::option::Option::None; self.no_backup = ::std::option::Option::None; - self.recovery_mode = ::std::option::Option::None; + self.recovery_status = ::std::option::Option::None; self.capabilities.clear(); self.backup_type = ::std::option::Option::None; self.sd_card_present = ::std::option::Option::None; @@ -2439,7 +2442,7 @@ impl ::protobuf::Message for Features { fw_vendor: ::std::option::Option::None, unfinished_backup: ::std::option::Option::None, no_backup: ::std::option::Option::None, - recovery_mode: ::std::option::Option::None, + recovery_status: ::std::option::Option::None, capabilities: ::std::vec::Vec::new(), backup_type: ::std::option::Option::None, sd_card_present: ::std::option::Option::None, @@ -2488,6 +2491,78 @@ impl ::protobuf::reflect::ProtobufValue for Features { /// Nested message and enums of message `Features` pub mod features { + #[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] + // @@protoc_insertion_point(enum:hw.trezor.messages.management.Features.RecoveryStatus) + pub enum RecoveryStatus { + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.Features.RecoveryStatus.NoRecovery) + NoRecovery = 0, + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.Features.RecoveryStatus.InNormalRecovery) + InNormalRecovery = 1, + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.Features.RecoveryStatus.InDryRunRecovery) + InDryRunRecovery = 2, + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.Features.RecoveryStatus.InUnlockRepeatedBackupRecovery) + InUnlockRepeatedBackupRecovery = 3, + } + + impl ::protobuf::Enum for RecoveryStatus { + const NAME: &'static str = "RecoveryStatus"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(RecoveryStatus::NoRecovery), + 1 => ::std::option::Option::Some(RecoveryStatus::InNormalRecovery), + 2 => ::std::option::Option::Some(RecoveryStatus::InDryRunRecovery), + 3 => ::std::option::Option::Some(RecoveryStatus::InUnlockRepeatedBackupRecovery), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "NoRecovery" => ::std::option::Option::Some(RecoveryStatus::NoRecovery), + "InNormalRecovery" => ::std::option::Option::Some(RecoveryStatus::InNormalRecovery), + "InDryRunRecovery" => ::std::option::Option::Some(RecoveryStatus::InDryRunRecovery), + "InUnlockRepeatedBackupRecovery" => ::std::option::Option::Some(RecoveryStatus::InUnlockRepeatedBackupRecovery), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [RecoveryStatus] = &[ + RecoveryStatus::NoRecovery, + RecoveryStatus::InNormalRecovery, + RecoveryStatus::InDryRunRecovery, + RecoveryStatus::InUnlockRepeatedBackupRecovery, + ]; + } + + impl ::protobuf::EnumFull for RecoveryStatus { + fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| super::file_descriptor().enum_by_package_relative_name("Features.RecoveryStatus").unwrap()).clone() + } + + fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { + let index = *self as usize; + Self::enum_descriptor().value_by_index(index) + } + } + + impl ::std::default::Default for RecoveryStatus { + fn default() -> Self { + RecoveryStatus::NoRecovery + } + } + + impl RecoveryStatus { + pub(in super) fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { + ::protobuf::reflect::GeneratedEnumDescriptorData::new::("Features.RecoveryStatus") + } + } + #[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] // @@protoc_insertion_point(enum:hw.trezor.messages.management.Features.Capability) pub enum Capability { @@ -7768,8 +7843,8 @@ pub struct RecoveryDevice { pub type_: ::std::option::Option<::protobuf::EnumOrUnknown>, // @@protoc_insertion_point(field:hw.trezor.messages.management.RecoveryDevice.u2f_counter) pub u2f_counter: ::std::option::Option, - // @@protoc_insertion_point(field:hw.trezor.messages.management.RecoveryDevice.dry_run) - pub dry_run: ::std::option::Option, + // @@protoc_insertion_point(field:hw.trezor.messages.management.RecoveryDevice.kind) + pub kind: ::std::option::Option<::protobuf::EnumOrUnknown>, // special fields // @@protoc_insertion_point(special_field:hw.trezor.messages.management.RecoveryDevice.special_fields) pub special_fields: ::protobuf::SpecialFields, @@ -7975,23 +8050,26 @@ impl RecoveryDevice { self.u2f_counter = ::std::option::Option::Some(v); } - // optional bool dry_run = 10; + // optional .hw.trezor.messages.management.RecoveryDevice.RecoveryKind kind = 10; - pub fn dry_run(&self) -> bool { - self.dry_run.unwrap_or(false) + pub fn kind(&self) -> recovery_device::RecoveryKind { + match self.kind { + Some(e) => e.enum_value_or(recovery_device::RecoveryKind::NormalRecovery), + None => recovery_device::RecoveryKind::NormalRecovery, + } } - pub fn clear_dry_run(&mut self) { - self.dry_run = ::std::option::Option::None; + pub fn clear_kind(&mut self) { + self.kind = ::std::option::Option::None; } - pub fn has_dry_run(&self) -> bool { - self.dry_run.is_some() + pub fn has_kind(&self) -> bool { + self.kind.is_some() } // Param is passed by value, moved - pub fn set_dry_run(&mut self, v: bool) { - self.dry_run = ::std::option::Option::Some(v); + pub fn set_kind(&mut self, v: recovery_device::RecoveryKind) { + self.kind = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); } fn generated_message_descriptor_data() -> ::protobuf::reflect::GeneratedMessageDescriptorData { @@ -8038,9 +8116,9 @@ impl RecoveryDevice { |m: &mut RecoveryDevice| { &mut m.u2f_counter }, )); fields.push(::protobuf::reflect::rt::v2::make_option_accessor::<_, _>( - "dry_run", - |m: &RecoveryDevice| { &m.dry_run }, - |m: &mut RecoveryDevice| { &mut m.dry_run }, + "kind", + |m: &RecoveryDevice| { &m.kind }, + |m: &mut RecoveryDevice| { &mut m.kind }, )); ::protobuf::reflect::GeneratedMessageDescriptorData::new_2::( "RecoveryDevice", @@ -8085,7 +8163,7 @@ impl ::protobuf::Message for RecoveryDevice { self.u2f_counter = ::std::option::Option::Some(is.read_uint32()?); }, 80 => { - self.dry_run = ::std::option::Option::Some(is.read_bool()?); + self.kind = ::std::option::Option::Some(is.read_enum_or_unknown()?); }, tag => { ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; @@ -8123,8 +8201,8 @@ impl ::protobuf::Message for RecoveryDevice { if let Some(v) = self.u2f_counter { my_size += ::protobuf::rt::uint32_size(9, v); } - if let Some(v) = self.dry_run { - my_size += 1 + 1; + if let Some(v) = self.kind { + my_size += ::protobuf::rt::int32_size(10, v.value()); } my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); self.special_fields.cached_size().set(my_size as u32); @@ -8156,8 +8234,8 @@ impl ::protobuf::Message for RecoveryDevice { if let Some(v) = self.u2f_counter { os.write_uint32(9, v)?; } - if let Some(v) = self.dry_run { - os.write_bool(10, v)?; + if let Some(v) = self.kind { + os.write_enum(10, ::protobuf::EnumOrUnknown::value(&v))?; } os.write_unknown_fields(self.special_fields.unknown_fields())?; ::std::result::Result::Ok(()) @@ -8184,7 +8262,7 @@ impl ::protobuf::Message for RecoveryDevice { self.enforce_wordlist = ::std::option::Option::None; self.type_ = ::std::option::Option::None; self.u2f_counter = ::std::option::Option::None; - self.dry_run = ::std::option::Option::None; + self.kind = ::std::option::Option::None; self.special_fields.clear(); } @@ -8198,7 +8276,7 @@ impl ::protobuf::Message for RecoveryDevice { enforce_wordlist: ::std::option::Option::None, type_: ::std::option::Option::None, u2f_counter: ::std::option::Option::None, - dry_run: ::std::option::Option::None, + kind: ::std::option::Option::None, special_fields: ::protobuf::SpecialFields::new(), }; &instance @@ -8285,6 +8363,73 @@ pub mod recovery_device { ::protobuf::reflect::GeneratedEnumDescriptorData::new::("RecoveryDevice.RecoveryDeviceType") } } + + #[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] + // @@protoc_insertion_point(enum:hw.trezor.messages.management.RecoveryDevice.RecoveryKind) + pub enum RecoveryKind { + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.RecoveryDevice.RecoveryKind.NormalRecovery) + NormalRecovery = 0, + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.RecoveryDevice.RecoveryKind.DryRun) + DryRun = 1, + // @@protoc_insertion_point(enum_value:hw.trezor.messages.management.RecoveryDevice.RecoveryKind.UnlockRepeatedBackup) + UnlockRepeatedBackup = 2, + } + + impl ::protobuf::Enum for RecoveryKind { + const NAME: &'static str = "RecoveryKind"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(RecoveryKind::NormalRecovery), + 1 => ::std::option::Option::Some(RecoveryKind::DryRun), + 2 => ::std::option::Option::Some(RecoveryKind::UnlockRepeatedBackup), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "NormalRecovery" => ::std::option::Option::Some(RecoveryKind::NormalRecovery), + "DryRun" => ::std::option::Option::Some(RecoveryKind::DryRun), + "UnlockRepeatedBackup" => ::std::option::Option::Some(RecoveryKind::UnlockRepeatedBackup), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [RecoveryKind] = &[ + RecoveryKind::NormalRecovery, + RecoveryKind::DryRun, + RecoveryKind::UnlockRepeatedBackup, + ]; + } + + impl ::protobuf::EnumFull for RecoveryKind { + fn enum_descriptor() -> ::protobuf::reflect::EnumDescriptor { + static descriptor: ::protobuf::rt::Lazy<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::Lazy::new(); + descriptor.get(|| super::file_descriptor().enum_by_package_relative_name("RecoveryDevice.RecoveryKind").unwrap()).clone() + } + + fn descriptor(&self) -> ::protobuf::reflect::EnumValueDescriptor { + let index = *self as usize; + Self::enum_descriptor().value_by_index(index) + } + } + + impl ::std::default::Default for RecoveryKind { + fn default() -> Self { + RecoveryKind::NormalRecovery + } + } + + impl RecoveryKind { + pub(in super) fn generated_enum_descriptor_data() -> ::protobuf::reflect::GeneratedEnumDescriptorData { + ::protobuf::reflect::GeneratedEnumDescriptorData::new::("RecoveryDevice.RecoveryKind") + } + } } // @@protoc_insertion_point(message:hw.trezor.messages.management.WordRequest) @@ -10839,7 +10984,7 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \x0emessages.proto\"\x80\x01\n\nInitialize\x12\x1d\n\nsession_id\x18\x01\ \x20\x01(\x0cR\tsessionId\x12,\n\x10_skip_passphrase\x18\x02\x20\x01(\ \x08R\x0eSkipPassphraseB\x02\x18\x01\x12%\n\x0ederive_cardano\x18\x03\ - \x20\x01(\x08R\rderiveCardano\"\r\n\x0bGetFeatures\"\xe1\x14\n\x08Featur\ + \x20\x01(\x08R\rderiveCardano\"\r\n\x0bGetFeatures\"\x8f\x16\n\x08Featur\ es\x12\x16\n\x06vendor\x18\x01\x20\x01(\tR\x06vendor\x12#\n\rmajor_versi\ on\x18\x02\x20\x02(\rR\x0cmajorVersion\x12#\n\rminor_version\x18\x03\x20\ \x02(\rR\x0cminorVersion\x12#\n\rpatch_version\x18\x04\x20\x02(\rR\x0cpa\ @@ -10861,22 +11006,23 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \x07fwMinor\x12\x19\n\x08fw_patch\x18\x18\x20\x01(\rR\x07fwPatch\x12\x1b\ \n\tfw_vendor\x18\x19\x20\x01(\tR\x08fwVendor\x12+\n\x11unfinished_backu\ p\x18\x1b\x20\x01(\x08R\x10unfinishedBackup\x12\x1b\n\tno_backup\x18\x1c\ - \x20\x01(\x08R\x08noBackup\x12#\n\rrecovery_mode\x18\x1d\x20\x01(\x08R\ - \x0crecoveryMode\x12V\n\x0ccapabilities\x18\x1e\x20\x03(\x0e22.hw.trezor\ - .messages.management.Features.CapabilityR\x0ccapabilities\x12J\n\x0bback\ - up_type\x18\x1f\x20\x01(\x0e2).hw.trezor.messages.management.BackupTypeR\ - \nbackupType\x12&\n\x0fsd_card_present\x18\x20\x20\x01(\x08R\rsdCardPres\ - ent\x12#\n\rsd_protection\x18!\x20\x01(\x08R\x0csdProtection\x120\n\x14w\ - ipe_code_protection\x18\"\x20\x01(\x08R\x12wipeCodeProtection\x12\x1d\n\ - \nsession_id\x18#\x20\x01(\x0cR\tsessionId\x12=\n\x1bpassphrase_always_o\ - n_device\x18$\x20\x01(\x08R\x18passphraseAlwaysOnDevice\x12T\n\rsafety_c\ - hecks\x18%\x20\x01(\x0e2/.hw.trezor.messages.management.SafetyCheckLevel\ - R\x0csafetyChecks\x12+\n\x12auto_lock_delay_ms\x18&\x20\x01(\rR\x0fautoL\ - ockDelayMs\x12)\n\x10display_rotation\x18'\x20\x01(\rR\x0fdisplayRotatio\ - n\x123\n\x15experimental_features\x18(\x20\x01(\x08R\x14experimentalFeat\ - ures\x12\x12\n\x04busy\x18)\x20\x01(\x08R\x04busy\x12\\\n\x11homescreen_\ - format\x18*\x20\x01(\x0e2/.hw.trezor.messages.management.HomescreenForma\ - tR\x10homescreenFormat\x129\n\x19hide_passphrase_from_host\x18+\x20\x01(\ + \x20\x01(\x08R\x08noBackup\x12_\n\x0frecovery_status\x18\x1d\x20\x01(\ + \x0e26.hw.trezor.messages.management.Features.RecoveryStatusR\x0erecover\ + yStatus\x12V\n\x0ccapabilities\x18\x1e\x20\x03(\x0e22.hw.trezor.messages\ + .management.Features.CapabilityR\x0ccapabilities\x12J\n\x0bbackup_type\ + \x18\x1f\x20\x01(\x0e2).hw.trezor.messages.management.BackupTypeR\nbacku\ + pType\x12&\n\x0fsd_card_present\x18\x20\x20\x01(\x08R\rsdCardPresent\x12\ + #\n\rsd_protection\x18!\x20\x01(\x08R\x0csdProtection\x120\n\x14wipe_cod\ + e_protection\x18\"\x20\x01(\x08R\x12wipeCodeProtection\x12\x1d\n\nsessio\ + n_id\x18#\x20\x01(\x0cR\tsessionId\x12=\n\x1bpassphrase_always_on_device\ + \x18$\x20\x01(\x08R\x18passphraseAlwaysOnDevice\x12T\n\rsafety_checks\ + \x18%\x20\x01(\x0e2/.hw.trezor.messages.management.SafetyCheckLevelR\x0c\ + safetyChecks\x12+\n\x12auto_lock_delay_ms\x18&\x20\x01(\rR\x0fautoLockDe\ + layMs\x12)\n\x10display_rotation\x18'\x20\x01(\rR\x0fdisplayRotation\x12\ + 3\n\x15experimental_features\x18(\x20\x01(\x08R\x14experimentalFeatures\ + \x12\x12\n\x04busy\x18)\x20\x01(\x08R\x04busy\x12\\\n\x11homescreen_form\ + at\x18*\x20\x01(\x0e2/.hw.trezor.messages.management.HomescreenFormatR\ + \x10homescreenFormat\x129\n\x19hide_passphrase_from_host\x18+\x20\x01(\ \x08R\x16hidePassphraseFromHost\x12%\n\x0einternal_model\x18,\x20\x01(\t\ R\rinternalModel\x12\x1d\n\nunit_color\x18-\x20\x01(\rR\tunitColor\x12!\ \n\x0cunit_btconly\x18.\x20\x01(\x08R\x0bunitBtconly\x12)\n\x10homescree\ @@ -10885,120 +11031,125 @@ static file_descriptor_proto_data: &'static [u8] = b"\ \x20\x01(\x08R\x10bootloaderLocked\x12>\n\x18language_version_matches\ \x182\x20\x01(\x08:\x04trueR\x16languageVersionMatches\x12%\n\x0eunit_pa\ ckaging\x183\x20\x01(\rR\runitPackaging\x12'\n\x0fhaptic_feedback\x184\ - \x20\x01(\x08R\x0ehapticFeedback\"\xc2\x04\n\nCapability\x12\x1c\n\x12Ca\ - pability_Bitcoin\x10\x01\x1a\x04\x80\xa6\x1d\x01\x12\x1b\n\x17Capability\ - _Bitcoin_like\x10\x02\x12\x16\n\x12Capability_Binance\x10\x03\x12\x16\n\ - \x12Capability_Cardano\x10\x04\x12\x1b\n\x11Capability_Crypto\x10\x05\ - \x1a\x04\x80\xa6\x1d\x01\x12\x12\n\x0eCapability_EOS\x10\x06\x12\x17\n\ - \x13Capability_Ethereum\x10\x07\x12\x17\n\x0fCapability_Lisk\x10\x08\x1a\ - \x02\x08\x01\x12\x15\n\x11Capability_Monero\x10\t\x12\x12\n\x0eCapabilit\ - y_NEM\x10\n\x12\x15\n\x11Capability_Ripple\x10\x0b\x12\x16\n\x12Capabili\ - ty_Stellar\x10\x0c\x12\x14\n\x10Capability_Tezos\x10\r\x12\x12\n\x0eCapa\ - bility_U2F\x10\x0e\x12\x1b\n\x11Capability_Shamir\x10\x0f\x1a\x04\x80\ - \xa6\x1d\x01\x12!\n\x17Capability_ShamirGroups\x10\x10\x1a\x04\x80\xa6\ - \x1d\x01\x12$\n\x1aCapability_PassphraseEntry\x10\x11\x1a\x04\x80\xa6\ - \x1d\x01\x12\x15\n\x11Capability_Solana\x10\x12\x12!\n\x17Capability_Tra\ - nslations\x10\x13\x1a\x04\x80\xa6\x1d\x01\x12\x1f\n\x15Capability_Bright\ - ness\x10\x14\x1a\x04\x80\xa6\x1d\x01\x12\x1b\n\x11Capability_Haptic\x10\ - \x15\x1a\x04\x80\xa6\x1d\x01\x1a\x04\xc8\xf3\x18\x01\"\x0c\n\nLockDevice\ - \"&\n\x07SetBusy\x12\x1b\n\texpiry_ms\x18\x01\x20\x01(\rR\x08expiryMs\"\ - \x0c\n\nEndSession\"\xc4\x04\n\rApplySettings\x12\x1e\n\x08language\x18\ - \x01\x20\x01(\tR\x08languageB\x02\x18\x01\x12\x14\n\x05label\x18\x02\x20\ - \x01(\tR\x05label\x12%\n\x0euse_passphrase\x18\x03\x20\x01(\x08R\rusePas\ - sphrase\x12\x1e\n\nhomescreen\x18\x04\x20\x01(\x0cR\nhomescreen\x120\n\ - \x12_passphrase_source\x18\x05\x20\x01(\rR\x10PassphraseSourceB\x02\x18\ - \x01\x12+\n\x12auto_lock_delay_ms\x18\x06\x20\x01(\rR\x0fautoLockDelayMs\ - \x12)\n\x10display_rotation\x18\x07\x20\x01(\rR\x0fdisplayRotation\x12=\ - \n\x1bpassphrase_always_on_device\x18\x08\x20\x01(\x08R\x18passphraseAlw\ - aysOnDevice\x12T\n\rsafety_checks\x18\t\x20\x01(\x0e2/.hw.trezor.message\ - s.management.SafetyCheckLevelR\x0csafetyChecks\x123\n\x15experimental_fe\ - atures\x18\n\x20\x01(\x08R\x14experimentalFeatures\x129\n\x19hide_passph\ - rase_from_host\x18\x0b\x20\x01(\x08R\x16hidePassphraseFromHost\x12'\n\ - \x0fhaptic_feedback\x18\r\x20\x01(\x08R\x0ehapticFeedback\"T\n\x0eChange\ - Language\x12\x1f\n\x0bdata_length\x18\x01\x20\x02(\rR\ndataLength\x12!\n\ - \x0cshow_display\x18\x02\x20\x01(\x08R\x0bshowDisplay\"Z\n\x16Translatio\ - nDataRequest\x12\x1f\n\x0bdata_length\x18\x01\x20\x02(\rR\ndataLength\ - \x12\x1f\n\x0bdata_offset\x18\x02\x20\x02(\rR\ndataOffset\"3\n\x12Transl\ - ationDataAck\x12\x1d\n\ndata_chunk\x18\x01\x20\x02(\x0cR\tdataChunk\"\"\ - \n\nApplyFlags\x12\x14\n\x05flags\x18\x01\x20\x02(\rR\x05flags\"#\n\tCha\ - ngePin\x12\x16\n\x06remove\x18\x01\x20\x01(\x08R\x06remove\"(\n\x0eChang\ - eWipeCode\x12\x16\n\x06remove\x18\x01\x20\x01(\x08R\x06remove\"\xaa\x01\ - \n\tSdProtect\x12]\n\toperation\x18\x01\x20\x02(\x0e2?.hw.trezor.message\ - s.management.SdProtect.SdProtectOperationTypeR\toperation\">\n\x16SdProt\ - ectOperationType\x12\x0b\n\x07DISABLE\x10\0\x12\n\n\x06ENABLE\x10\x01\ - \x12\x0b\n\x07REFRESH\x10\x02\"O\n\x04Ping\x12\x1a\n\x07message\x18\x01\ - \x20\x01(\t:\0R\x07message\x12+\n\x11button_protection\x18\x02\x20\x01(\ - \x08R\x10buttonProtection\"\x08\n\x06Cancel\"\x20\n\nGetEntropy\x12\x12\ - \n\x04size\x18\x01\x20\x02(\rR\x04size\"#\n\x07Entropy\x12\x18\n\x07entr\ - opy\x18\x01\x20\x02(\x0cR\x07entropy\"/\n\x0fGetFirmwareHash\x12\x1c\n\t\ - challenge\x18\x01\x20\x01(\x0cR\tchallenge\"\"\n\x0cFirmwareHash\x12\x12\ - \n\x04hash\x18\x01\x20\x02(\x0cR\x04hash\"2\n\x12AuthenticateDevice\x12\ - \x1c\n\tchallenge\x18\x01\x20\x02(\x0cR\tchallenge\"U\n\x11AuthenticityP\ - roof\x12\"\n\x0ccertificates\x18\x01\x20\x03(\x0cR\x0ccertificates\x12\ - \x1c\n\tsignature\x18\x02\x20\x02(\x0cR\tsignature\"\x0c\n\nWipeDevice\"\ - \xad\x02\n\nLoadDevice\x12\x1c\n\tmnemonics\x18\x01\x20\x03(\tR\tmnemoni\ - cs\x12\x10\n\x03pin\x18\x03\x20\x01(\tR\x03pin\x123\n\x15passphrase_prot\ - ection\x18\x04\x20\x01(\x08R\x14passphraseProtection\x12\x1e\n\x08langua\ - ge\x18\x05\x20\x01(\tR\x08languageB\x02\x18\x01\x12\x14\n\x05label\x18\ - \x06\x20\x01(\tR\x05label\x12#\n\rskip_checksum\x18\x07\x20\x01(\x08R\ - \x0cskipChecksum\x12\x1f\n\x0bu2f_counter\x18\x08\x20\x01(\rR\nu2fCounte\ - r\x12!\n\x0cneeds_backup\x18\t\x20\x01(\x08R\x0bneedsBackup\x12\x1b\n\tn\ - o_backup\x18\n\x20\x01(\x08R\x08noBackup\"\x99\x03\n\x0bResetDevice\x12%\ - \n\x0edisplay_random\x18\x01\x20\x01(\x08R\rdisplayRandom\x12\x1f\n\x08s\ - trength\x18\x02\x20\x01(\r:\x03256R\x08strength\x123\n\x15passphrase_pro\ - tection\x18\x03\x20\x01(\x08R\x14passphraseProtection\x12%\n\x0epin_prot\ - ection\x18\x04\x20\x01(\x08R\rpinProtection\x12\x1e\n\x08language\x18\ - \x05\x20\x01(\tR\x08languageB\x02\x18\x01\x12\x14\n\x05label\x18\x06\x20\ - \x01(\tR\x05label\x12\x1f\n\x0bu2f_counter\x18\x07\x20\x01(\rR\nu2fCount\ - er\x12\x1f\n\x0bskip_backup\x18\x08\x20\x01(\x08R\nskipBackup\x12\x1b\n\ - \tno_backup\x18\t\x20\x01(\x08R\x08noBackup\x12Q\n\x0bbackup_type\x18\n\ - \x20\x01(\x0e2).hw.trezor.messages.management.BackupType:\x05Bip39R\nbac\ - kupType\"\xe5\x01\n\x0cBackupDevice\x12'\n\x0fgroup_threshold\x18\x01\ - \x20\x01(\rR\x0egroupThreshold\x12O\n\x06groups\x18\x02\x20\x03(\x0b27.h\ - w.trezor.messages.management.BackupDevice.Slip39GroupR\x06groups\x1a[\n\ - \x0bSlip39Group\x12)\n\x10member_threshold\x18\x01\x20\x02(\rR\x0fmember\ - Threshold\x12!\n\x0cmember_count\x18\x02\x20\x02(\rR\x0bmemberCount\"\ - \x10\n\x0eEntropyRequest\"&\n\nEntropyAck\x12\x18\n\x07entropy\x18\x01\ - \x20\x02(\x0cR\x07entropy\"\xd8\x03\n\x0eRecoveryDevice\x12\x1d\n\nword_\ - count\x18\x01\x20\x01(\rR\twordCount\x123\n\x15passphrase_protection\x18\ - \x02\x20\x01(\x08R\x14passphraseProtection\x12%\n\x0epin_protection\x18\ - \x03\x20\x01(\x08R\rpinProtection\x12\x1e\n\x08language\x18\x04\x20\x01(\ - \tR\x08languageB\x02\x18\x01\x12\x14\n\x05label\x18\x05\x20\x01(\tR\x05l\ - abel\x12)\n\x10enforce_wordlist\x18\x06\x20\x01(\x08R\x0fenforceWordlist\ - \x12T\n\x04type\x18\x08\x20\x01(\x0e2@.hw.trezor.messages.management.Rec\ - overyDevice.RecoveryDeviceTypeR\x04type\x12\x1f\n\x0bu2f_counter\x18\t\ - \x20\x01(\rR\nu2fCounter\x12\x17\n\x07dry_run\x18\n\x20\x01(\x08R\x06dry\ - Run\"Z\n\x12RecoveryDeviceType\x12%\n!RecoveryDeviceType_ScrambledWords\ - \x10\0\x12\x1d\n\x19RecoveryDeviceType_Matrix\x10\x01\"\xc5\x01\n\x0bWor\ - dRequest\x12N\n\x04type\x18\x01\x20\x02(\x0e2:.hw.trezor.messages.manage\ - ment.WordRequest.WordRequestTypeR\x04type\"f\n\x0fWordRequestType\x12\ - \x19\n\x15WordRequestType_Plain\x10\0\x12\x1b\n\x17WordRequestType_Matri\ - x9\x10\x01\x12\x1b\n\x17WordRequestType_Matrix6\x10\x02\"\x1d\n\x07WordA\ - ck\x12\x12\n\x04word\x18\x01\x20\x02(\tR\x04word\"0\n\rSetU2FCounter\x12\ - \x1f\n\x0bu2f_counter\x18\x01\x20\x02(\rR\nu2fCounter\"\x13\n\x11GetNext\ - U2FCounter\"1\n\x0eNextU2FCounter\x12\x1f\n\x0bu2f_counter\x18\x01\x20\ - \x02(\rR\nu2fCounter\"\x11\n\x0fDoPreauthorized\"\x16\n\x14Preauthorized\ - Request\"\x15\n\x13CancelAuthorization\"\x9a\x02\n\x12RebootToBootloader\ - \x12o\n\x0cboot_command\x18\x01\x20\x01(\x0e2=.hw.trezor.messages.manage\ - ment.RebootToBootloader.BootCommand:\rSTOP_AND_WAITR\x0bbootCommand\x12'\ - \n\x0ffirmware_header\x18\x02\x20\x01(\x0cR\x0efirmwareHeader\x123\n\x14\ - language_data_length\x18\x03\x20\x01(\r:\x010R\x12languageDataLength\"5\ - \n\x0bBootCommand\x12\x11\n\rSTOP_AND_WAIT\x10\0\x12\x13\n\x0fINSTALL_UP\ - GRADE\x10\x01\"\x10\n\x08GetNonce:\x04\x88\xb2\x19\x01\"#\n\x05Nonce\x12\ - \x14\n\x05nonce\x18\x01\x20\x02(\x0cR\x05nonce:\x04\x88\xb2\x19\x01\";\n\ - \nUnlockPath\x12\x1b\n\taddress_n\x18\x01\x20\x03(\rR\x08addressN\x12\ - \x10\n\x03mac\x18\x02\x20\x01(\x0cR\x03mac\"'\n\x13UnlockedPathRequest\ - \x12\x10\n\x03mac\x18\x01\x20\x01(\x0cR\x03mac\"\x14\n\x12ShowDeviceTuto\ - rial\"\x12\n\x10UnlockBootloader\"%\n\rSetBrightness\x12\x14\n\x05value\ - \x18\x01\x20\x01(\rR\x05value*\x99\x01\n\nBackupType\x12\t\n\x05Bip39\ - \x10\0\x12\x10\n\x0cSlip39_Basic\x10\x01\x12\x13\n\x0fSlip39_Advanced\ - \x10\x02\x12\x1c\n\x18Slip39_Single_Extendable\x10\x03\x12\x1b\n\x17Slip\ - 39_Basic_Extendable\x10\x04\x12\x1e\n\x1aSlip39_Advanced_Extendable\x10\ - \x05*G\n\x10SafetyCheckLevel\x12\n\n\x06Strict\x10\0\x12\x10\n\x0cPrompt\ - Always\x10\x01\x12\x15\n\x11PromptTemporarily\x10\x02*0\n\x10HomescreenF\ - ormat\x12\x08\n\x04Toif\x10\x01\x12\x08\n\x04Jpeg\x10\x02\x12\x08\n\x04T\ - oiG\x10\x03BB\n#com.satoshilabs.trezor.lib.protobufB\x17TrezorMessageMan\ - agement\x80\xa6\x1d\x01\ + \x20\x01(\x08R\x0ehapticFeedback\"p\n\x0eRecoveryStatus\x12\x0e\n\nNoRec\ + overy\x10\0\x12\x14\n\x10InNormalRecovery\x10\x01\x12\x14\n\x10InDryRunR\ + ecovery\x10\x02\x12\"\n\x1eInUnlockRepeatedBackupRecovery\x10\x03\"\xc2\ + \x04\n\nCapability\x12\x1c\n\x12Capability_Bitcoin\x10\x01\x1a\x04\x80\ + \xa6\x1d\x01\x12\x1b\n\x17Capability_Bitcoin_like\x10\x02\x12\x16\n\x12C\ + apability_Binance\x10\x03\x12\x16\n\x12Capability_Cardano\x10\x04\x12\ + \x1b\n\x11Capability_Crypto\x10\x05\x1a\x04\x80\xa6\x1d\x01\x12\x12\n\ + \x0eCapability_EOS\x10\x06\x12\x17\n\x13Capability_Ethereum\x10\x07\x12\ + \x17\n\x0fCapability_Lisk\x10\x08\x1a\x02\x08\x01\x12\x15\n\x11Capabilit\ + y_Monero\x10\t\x12\x12\n\x0eCapability_NEM\x10\n\x12\x15\n\x11Capability\ + _Ripple\x10\x0b\x12\x16\n\x12Capability_Stellar\x10\x0c\x12\x14\n\x10Cap\ + ability_Tezos\x10\r\x12\x12\n\x0eCapability_U2F\x10\x0e\x12\x1b\n\x11Cap\ + ability_Shamir\x10\x0f\x1a\x04\x80\xa6\x1d\x01\x12!\n\x17Capability_Sham\ + irGroups\x10\x10\x1a\x04\x80\xa6\x1d\x01\x12$\n\x1aCapability_Passphrase\ + Entry\x10\x11\x1a\x04\x80\xa6\x1d\x01\x12\x15\n\x11Capability_Solana\x10\ + \x12\x12!\n\x17Capability_Translations\x10\x13\x1a\x04\x80\xa6\x1d\x01\ + \x12\x1f\n\x15Capability_Brightness\x10\x14\x1a\x04\x80\xa6\x1d\x01\x12\ + \x1b\n\x11Capability_Haptic\x10\x15\x1a\x04\x80\xa6\x1d\x01\x1a\x04\xc8\ + \xf3\x18\x01\"\x0c\n\nLockDevice\"&\n\x07SetBusy\x12\x1b\n\texpiry_ms\ + \x18\x01\x20\x01(\rR\x08expiryMs\"\x0c\n\nEndSession\"\xc4\x04\n\rApplyS\ + ettings\x12\x1e\n\x08language\x18\x01\x20\x01(\tR\x08languageB\x02\x18\ + \x01\x12\x14\n\x05label\x18\x02\x20\x01(\tR\x05label\x12%\n\x0euse_passp\ + hrase\x18\x03\x20\x01(\x08R\rusePassphrase\x12\x1e\n\nhomescreen\x18\x04\ + \x20\x01(\x0cR\nhomescreen\x120\n\x12_passphrase_source\x18\x05\x20\x01(\ + \rR\x10PassphraseSourceB\x02\x18\x01\x12+\n\x12auto_lock_delay_ms\x18\ + \x06\x20\x01(\rR\x0fautoLockDelayMs\x12)\n\x10display_rotation\x18\x07\ + \x20\x01(\rR\x0fdisplayRotation\x12=\n\x1bpassphrase_always_on_device\ + \x18\x08\x20\x01(\x08R\x18passphraseAlwaysOnDevice\x12T\n\rsafety_checks\ + \x18\t\x20\x01(\x0e2/.hw.trezor.messages.management.SafetyCheckLevelR\ + \x0csafetyChecks\x123\n\x15experimental_features\x18\n\x20\x01(\x08R\x14\ + experimentalFeatures\x129\n\x19hide_passphrase_from_host\x18\x0b\x20\x01\ + (\x08R\x16hidePassphraseFromHost\x12'\n\x0fhaptic_feedback\x18\r\x20\x01\ + (\x08R\x0ehapticFeedback\"T\n\x0eChangeLanguage\x12\x1f\n\x0bdata_length\ + \x18\x01\x20\x02(\rR\ndataLength\x12!\n\x0cshow_display\x18\x02\x20\x01(\ + \x08R\x0bshowDisplay\"Z\n\x16TranslationDataRequest\x12\x1f\n\x0bdata_le\ + ngth\x18\x01\x20\x02(\rR\ndataLength\x12\x1f\n\x0bdata_offset\x18\x02\ + \x20\x02(\rR\ndataOffset\"3\n\x12TranslationDataAck\x12\x1d\n\ndata_chun\ + k\x18\x01\x20\x02(\x0cR\tdataChunk\"\"\n\nApplyFlags\x12\x14\n\x05flags\ + \x18\x01\x20\x02(\rR\x05flags\"#\n\tChangePin\x12\x16\n\x06remove\x18\ + \x01\x20\x01(\x08R\x06remove\"(\n\x0eChangeWipeCode\x12\x16\n\x06remove\ + \x18\x01\x20\x01(\x08R\x06remove\"\xaa\x01\n\tSdProtect\x12]\n\toperatio\ + n\x18\x01\x20\x02(\x0e2?.hw.trezor.messages.management.SdProtect.SdProte\ + ctOperationTypeR\toperation\">\n\x16SdProtectOperationType\x12\x0b\n\x07\ + DISABLE\x10\0\x12\n\n\x06ENABLE\x10\x01\x12\x0b\n\x07REFRESH\x10\x02\"O\ + \n\x04Ping\x12\x1a\n\x07message\x18\x01\x20\x01(\t:\0R\x07message\x12+\n\ + \x11button_protection\x18\x02\x20\x01(\x08R\x10buttonProtection\"\x08\n\ + \x06Cancel\"\x20\n\nGetEntropy\x12\x12\n\x04size\x18\x01\x20\x02(\rR\x04\ + size\"#\n\x07Entropy\x12\x18\n\x07entropy\x18\x01\x20\x02(\x0cR\x07entro\ + py\"/\n\x0fGetFirmwareHash\x12\x1c\n\tchallenge\x18\x01\x20\x01(\x0cR\tc\ + hallenge\"\"\n\x0cFirmwareHash\x12\x12\n\x04hash\x18\x01\x20\x02(\x0cR\ + \x04hash\"2\n\x12AuthenticateDevice\x12\x1c\n\tchallenge\x18\x01\x20\x02\ + (\x0cR\tchallenge\"U\n\x11AuthenticityProof\x12\"\n\x0ccertificates\x18\ + \x01\x20\x03(\x0cR\x0ccertificates\x12\x1c\n\tsignature\x18\x02\x20\x02(\ + \x0cR\tsignature\"\x0c\n\nWipeDevice\"\xad\x02\n\nLoadDevice\x12\x1c\n\t\ + mnemonics\x18\x01\x20\x03(\tR\tmnemonics\x12\x10\n\x03pin\x18\x03\x20\ + \x01(\tR\x03pin\x123\n\x15passphrase_protection\x18\x04\x20\x01(\x08R\ + \x14passphraseProtection\x12\x1e\n\x08language\x18\x05\x20\x01(\tR\x08la\ + nguageB\x02\x18\x01\x12\x14\n\x05label\x18\x06\x20\x01(\tR\x05label\x12#\ + \n\rskip_checksum\x18\x07\x20\x01(\x08R\x0cskipChecksum\x12\x1f\n\x0bu2f\ + _counter\x18\x08\x20\x01(\rR\nu2fCounter\x12!\n\x0cneeds_backup\x18\t\ + \x20\x01(\x08R\x0bneedsBackup\x12\x1b\n\tno_backup\x18\n\x20\x01(\x08R\ + \x08noBackup\"\x99\x03\n\x0bResetDevice\x12%\n\x0edisplay_random\x18\x01\ + \x20\x01(\x08R\rdisplayRandom\x12\x1f\n\x08strength\x18\x02\x20\x01(\r:\ + \x03256R\x08strength\x123\n\x15passphrase_protection\x18\x03\x20\x01(\ + \x08R\x14passphraseProtection\x12%\n\x0epin_protection\x18\x04\x20\x01(\ + \x08R\rpinProtection\x12\x1e\n\x08language\x18\x05\x20\x01(\tR\x08langua\ + geB\x02\x18\x01\x12\x14\n\x05label\x18\x06\x20\x01(\tR\x05label\x12\x1f\ + \n\x0bu2f_counter\x18\x07\x20\x01(\rR\nu2fCounter\x12\x1f\n\x0bskip_back\ + up\x18\x08\x20\x01(\x08R\nskipBackup\x12\x1b\n\tno_backup\x18\t\x20\x01(\ + \x08R\x08noBackup\x12Q\n\x0bbackup_type\x18\n\x20\x01(\x0e2).hw.trezor.m\ + essages.management.BackupType:\x05Bip39R\nbackupType\"\xe5\x01\n\x0cBack\ + upDevice\x12'\n\x0fgroup_threshold\x18\x01\x20\x01(\rR\x0egroupThreshold\ + \x12O\n\x06groups\x18\x02\x20\x03(\x0b27.hw.trezor.messages.management.B\ + ackupDevice.Slip39GroupR\x06groups\x1a[\n\x0bSlip39Group\x12)\n\x10membe\ + r_threshold\x18\x01\x20\x02(\rR\x0fmemberThreshold\x12!\n\x0cmember_coun\ + t\x18\x02\x20\x02(\rR\x0bmemberCount\"\x10\n\x0eEntropyRequest\"&\n\nEnt\ + ropyAck\x12\x18\n\x07entropy\x18\x01\x20\x02(\x0cR\x07entropy\"\xef\x04\ + \n\x0eRecoveryDevice\x12\x1d\n\nword_count\x18\x01\x20\x01(\rR\twordCoun\ + t\x123\n\x15passphrase_protection\x18\x02\x20\x01(\x08R\x14passphrasePro\ + tection\x12%\n\x0epin_protection\x18\x03\x20\x01(\x08R\rpinProtection\ + \x12\x1e\n\x08language\x18\x04\x20\x01(\tR\x08languageB\x02\x18\x01\x12\ + \x14\n\x05label\x18\x05\x20\x01(\tR\x05label\x12)\n\x10enforce_wordlist\ + \x18\x06\x20\x01(\x08R\x0fenforceWordlist\x12T\n\x04type\x18\x08\x20\x01\ + (\x0e2@.hw.trezor.messages.management.RecoveryDevice.RecoveryDeviceTypeR\ + \x04type\x12\x1f\n\x0bu2f_counter\x18\t\x20\x01(\rR\nu2fCounter\x12^\n\ + \x04kind\x18\n\x20\x01(\x0e2:.hw.trezor.messages.management.RecoveryDevi\ + ce.RecoveryKind:\x0eNormalRecoveryR\x04kind\"Z\n\x12RecoveryDeviceType\ + \x12%\n!RecoveryDeviceType_ScrambledWords\x10\0\x12\x1d\n\x19RecoveryDev\ + iceType_Matrix\x10\x01\"H\n\x0cRecoveryKind\x12\x12\n\x0eNormalRecovery\ + \x10\0\x12\n\n\x06DryRun\x10\x01\x12\x18\n\x14UnlockRepeatedBackup\x10\ + \x02J\x04\x08\x07\x10\x08\"\xc5\x01\n\x0bWordRequest\x12N\n\x04type\x18\ + \x01\x20\x02(\x0e2:.hw.trezor.messages.management.WordRequest.WordReques\ + tTypeR\x04type\"f\n\x0fWordRequestType\x12\x19\n\x15WordRequestType_Plai\ + n\x10\0\x12\x1b\n\x17WordRequestType_Matrix9\x10\x01\x12\x1b\n\x17WordRe\ + questType_Matrix6\x10\x02\"\x1d\n\x07WordAck\x12\x12\n\x04word\x18\x01\ + \x20\x02(\tR\x04word\"0\n\rSetU2FCounter\x12\x1f\n\x0bu2f_counter\x18\ + \x01\x20\x02(\rR\nu2fCounter\"\x13\n\x11GetNextU2FCounter\"1\n\x0eNextU2\ + FCounter\x12\x1f\n\x0bu2f_counter\x18\x01\x20\x02(\rR\nu2fCounter\"\x11\ + \n\x0fDoPreauthorized\"\x16\n\x14PreauthorizedRequest\"\x15\n\x13CancelA\ + uthorization\"\x9a\x02\n\x12RebootToBootloader\x12o\n\x0cboot_command\ + \x18\x01\x20\x01(\x0e2=.hw.trezor.messages.management.RebootToBootloader\ + .BootCommand:\rSTOP_AND_WAITR\x0bbootCommand\x12'\n\x0ffirmware_header\ + \x18\x02\x20\x01(\x0cR\x0efirmwareHeader\x123\n\x14language_data_length\ + \x18\x03\x20\x01(\r:\x010R\x12languageDataLength\"5\n\x0bBootCommand\x12\ + \x11\n\rSTOP_AND_WAIT\x10\0\x12\x13\n\x0fINSTALL_UPGRADE\x10\x01\"\x10\n\ + \x08GetNonce:\x04\x88\xb2\x19\x01\"#\n\x05Nonce\x12\x14\n\x05nonce\x18\ + \x01\x20\x02(\x0cR\x05nonce:\x04\x88\xb2\x19\x01\";\n\nUnlockPath\x12\ + \x1b\n\taddress_n\x18\x01\x20\x03(\rR\x08addressN\x12\x10\n\x03mac\x18\ + \x02\x20\x01(\x0cR\x03mac\"'\n\x13UnlockedPathRequest\x12\x10\n\x03mac\ + \x18\x01\x20\x01(\x0cR\x03mac\"\x14\n\x12ShowDeviceTutorial\"\x12\n\x10U\ + nlockBootloader\"%\n\rSetBrightness\x12\x14\n\x05value\x18\x01\x20\x01(\ + \rR\x05value*\x99\x01\n\nBackupType\x12\t\n\x05Bip39\x10\0\x12\x10\n\x0c\ + Slip39_Basic\x10\x01\x12\x13\n\x0fSlip39_Advanced\x10\x02\x12\x1c\n\x18S\ + lip39_Single_Extendable\x10\x03\x12\x1b\n\x17Slip39_Basic_Extendable\x10\ + \x04\x12\x1e\n\x1aSlip39_Advanced_Extendable\x10\x05*G\n\x10SafetyCheckL\ + evel\x12\n\n\x06Strict\x10\0\x12\x10\n\x0cPromptAlways\x10\x01\x12\x15\n\ + \x11PromptTemporarily\x10\x02*0\n\x10HomescreenFormat\x12\x08\n\x04Toif\ + \x10\x01\x12\x08\n\x04Jpeg\x10\x02\x12\x08\n\x04ToiG\x10\x03BB\n#com.sat\ + oshilabs.trezor.lib.protobufB\x17TrezorMessageManagement\x80\xa6\x1d\x01\ "; /// `FileDescriptorProto` object which was a source for this generated file @@ -11064,13 +11215,15 @@ pub fn file_descriptor() -> &'static ::protobuf::reflect::FileDescriptor { messages.push(UnlockBootloader::generated_message_descriptor_data()); messages.push(SetBrightness::generated_message_descriptor_data()); messages.push(backup_device::Slip39Group::generated_message_descriptor_data()); - let mut enums = ::std::vec::Vec::with_capacity(8); + let mut enums = ::std::vec::Vec::with_capacity(10); enums.push(BackupType::generated_enum_descriptor_data()); enums.push(SafetyCheckLevel::generated_enum_descriptor_data()); enums.push(HomescreenFormat::generated_enum_descriptor_data()); + enums.push(features::RecoveryStatus::generated_enum_descriptor_data()); enums.push(features::Capability::generated_enum_descriptor_data()); enums.push(sd_protect::SdProtectOperationType::generated_enum_descriptor_data()); enums.push(recovery_device::RecoveryDeviceType::generated_enum_descriptor_data()); + enums.push(recovery_device::RecoveryKind::generated_enum_descriptor_data()); enums.push(word_request::WordRequestType::generated_enum_descriptor_data()); enums.push(reboot_to_bootloader::BootCommand::generated_enum_descriptor_data()); ::protobuf::reflect::GeneratedFileDescriptor::new_generated( diff --git a/tests/click_tests/recovery.py b/tests/click_tests/recovery.py index 65e4adae88..3f46fede21 100644 --- a/tests/click_tests/recovery.py +++ b/tests/click_tests/recovery.py @@ -48,25 +48,27 @@ def enter_word( raise ValueError("Unknown model") -def confirm_recovery(debug: "DebugLink") -> None: +def confirm_recovery(debug: "DebugLink", title: str = "recovery__title") -> None: layout = debug.wait_layout() - TR.assert_equals(layout.title(), "recovery__title") + TR.assert_equals(layout.title(), title) if debug.model in (models.T2T1,): debug.click(buttons.OK, wait=True) elif debug.model in (models.T3T1,): debug.swipe_up(wait=True) elif debug.model in (models.T2B1,): debug.press_right(wait=True) - debug.press_right() def select_number_of_words( - debug: "DebugLink", num_of_words: int = 20, wait: bool = True + debug: "DebugLink", + num_of_words: int = 20, + wait: bool = True, + unlock_repeated_backup=False, ) -> None: if wait: debug.wait_layout() - TR.assert_equals(debug.read_layout().text_content(), "recovery__num_of_words") if debug.model in (models.T2T1,): + TR.assert_equals(debug.read_layout().text_content(), "recovery__num_of_words") # click the number word_option_offset = 6 word_options = (12, 18, 20, 24, 33) @@ -102,12 +104,14 @@ def select_number_of_words( else: raise ValueError("Unknown model") - if num_of_words in (20, 33): + if unlock_repeated_backup: + TR.assert_in(layout.text_content(), "recovery__enter_backup") + elif num_of_words in (20, 33): TR.assert_in_multiple( layout.text_content(), ["recovery__enter_any_share", "recovery__only_first_n_letters"], ) - else: + else: # BIP-39 TR.assert_in_multiple( layout.text_content(), ["recovery__enter_backup", "recovery__only_first_n_letters"], @@ -115,7 +119,10 @@ def select_number_of_words( def enter_share( - debug: "DebugLink", share: str, is_first: bool = True + debug: "DebugLink", + share: str, + is_first: bool = True, + before_title: str = "recovery__title_recover", ) -> "LayoutContent": if debug.model in (models.T2B1,): TR.assert_in(debug.read_layout().title(), "recovery__title_recover") @@ -136,13 +143,21 @@ def enter_share( return layout -def enter_shares(debug: "DebugLink", shares: list[str]) -> None: +def enter_shares( + debug: "DebugLink", + shares: list[str], + enter_share_before_title: str = "recovery__title_recover", + text: str = "recovery__enter_any_share", + after_layout_text: str = "recovery__wallet_recovered", +) -> None: TR.assert_in_multiple( debug.read_layout().text_content(), - ["recovery__enter_any_share", "recovery__only_first_n_letters"], + ["recovery__enter_any_share", "recovery__only_first_n_letters", text], ) for index, share in enumerate(shares): - enter_share(debug, share, is_first=index == 0) + enter_share( + debug, share, is_first=index == 0, before_title=enter_share_before_title + ) if index < len(shares) - 1: # FIXME: when ui-t3t1 done for shamir, we want to check the template below TR.assert_in(debug.read_layout().title(), "recovery__title_recover") @@ -152,16 +167,22 @@ def enter_shares(debug: "DebugLink", shares: list[str]) -> None: # template=(index + 1, len(shares)), # ) - TR.assert_in(debug.read_layout().text_content(), "recovery__wallet_recovered") + TR.assert_in(debug.read_layout().text_content(), after_layout_text) -def enter_seed(debug: "DebugLink", seed_words: list[str]) -> None: - prepare_enter_seed(debug) +def enter_seed( + debug: "DebugLink", + seed_words: list[str], + is_slip39=False, + prepare_layout_text: str = "recovery__enter_backup", + after_layout_text: str = "recovery__wallet_recovered", +) -> None: + prepare_enter_seed(debug, prepare_layout_text) for word in seed_words: - enter_word(debug, word, is_slip39=False) + enter_word(debug, word, is_slip39=is_slip39) - TR.assert_in(debug.read_layout().text_content(), "recovery__wallet_recovered") + TR.assert_in(debug.read_layout().text_content(), after_layout_text) def enter_seed_previous_correct( @@ -209,10 +230,12 @@ def enter_seed_previous_correct( # TR.assert_in(debug.read_layout().text_content(), "recovery__wallet_recovered") -def prepare_enter_seed(debug: "DebugLink") -> None: +def prepare_enter_seed( + debug: "DebugLink", layout_text: str = "recovery__enter_backup" +) -> None: TR.assert_in_multiple( debug.read_layout().text_content(), - ["recovery__enter_backup", "recovery__only_first_n_letters"], + ["recovery__enter_backup", "recovery__only_first_n_letters", layout_text], ) if debug.model in (models.T2T1,): debug.click(buttons.OK, wait=True) @@ -221,7 +244,6 @@ def prepare_enter_seed(debug: "DebugLink") -> None: debug.swipe_up(wait=True) elif debug.model in (models.T2B1,): debug.press_right(wait=True) - TR.assert_equals(debug.read_layout().title(), "recovery__title_recover") debug.press_right() layout = debug.press_right(wait=True) assert "MnemonicKeyboard" in layout.all_components() diff --git a/tests/click_tests/reset.py b/tests/click_tests/reset.py index afaf14944b..ea82c538d2 100644 --- a/tests/click_tests/reset.py +++ b/tests/click_tests/reset.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from shamir_mnemonic import shamir # type: ignore -from trezorlib import messages, models +from trezorlib import models from .. import buttons from .. import translations as TR @@ -81,9 +81,7 @@ def set_selection(debug: "DebugLink", button: tuple[int, int], diff: int) -> Non debug.press_middle(wait=True) -def read_words( - debug: "DebugLink", backup_type: messages.BackupType, do_htc: bool = True -) -> list[str]: +def read_words(debug: "DebugLink", do_htc: bool = True) -> list[str]: words: list[str] = [] if debug.model in (models.T2B1,): diff --git a/tests/click_tests/test_backup_slip39_custom.py b/tests/click_tests/test_backup_slip39_custom.py index 02e2b5363f..24524b03ba 100644 --- a/tests/click_tests/test_backup_slip39_custom.py +++ b/tests/click_tests/test_backup_slip39_custom.py @@ -82,7 +82,7 @@ def test_backup_slip39_custom( all_words: list[str] = [] for _ in range(share_count): # read words - words = reset.read_words(debug, messages.BackupType.Slip39_Basic) + words = reset.read_words(debug) # confirm words reset.confirm_words(debug, words) diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py index 5f16433156..5292bf8ce4 100644 --- a/tests/click_tests/test_recovery.py +++ b/tests/click_tests/test_recovery.py @@ -47,7 +47,7 @@ def prepare_recovery_and_evaluate( assert isinstance(device_handler.result(), messages.Success) features = device_handler.features() assert features.initialized is True - assert features.recovery_mode is False + assert features.recovery_status == messages.RecoveryStatus.NoRecovery @pytest.mark.setup_client(uninitialized=True) diff --git a/tests/click_tests/test_repeated_backup.py b/tests/click_tests/test_repeated_backup.py new file mode 100644 index 0000000000..6edca7e020 --- /dev/null +++ b/tests/click_tests/test_repeated_backup.py @@ -0,0 +1,218 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2024 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from typing import TYPE_CHECKING + +import pytest + +from trezorlib import device, messages + +from .. import buttons +from ..common import WITH_MOCK_URANDOM +from . import recovery, reset +from .common import go_next + +if TYPE_CHECKING: + from ..device_handler import BackgroundDeviceHandler + + +pytestmark = [pytest.mark.skip_t1b1] + + +@pytest.mark.setup_client(uninitialized=True) +@WITH_MOCK_URANDOM +def test_repeated_backup( + device_handler: "BackgroundDeviceHandler", +): + features = device_handler.features() + debug = device_handler.debuglink() + + assert features.initialized is False + + device_handler.run( + device.reset, + strength=128, + backup_type=messages.BackupType.Slip39_Basic, + pin_protection=False, + ) + + # confirm new wallet + reset.confirm_new_wallet(debug) + + # confirm back up + reset.confirm_read(debug) + + # confirm backup warning + reset.confirm_read(debug, middle_r=True) + + # let's make a 1-of-1 backup to start with... + + # shares=1 + reset.set_selection(debug, buttons.RESET_MINUS, 5 - 1) + + # confirm checklist + reset.confirm_read(debug) + + # threshold=1 + reset.set_selection(debug, buttons.RESET_PLUS, 0) + + # confirm checklist + reset.confirm_read(debug) + + # confirm backup warning + reset.confirm_read(debug, middle_r=True) + + # read words + initial_backup_1_of_1 = reset.read_words(debug) + + # confirm words + reset.confirm_words(debug, initial_backup_1_of_1) + + # confirm share checked + reset.confirm_read(debug) + + # confirm backup done + reset.confirm_read(debug) + + # Your backup is done + go_next(debug) + + # great ... device is initialized, backup done, and we are not in recovery mode! + assert device_handler.result() == "Initialized" + features = device_handler.features() + assert features.backup_type is messages.BackupType.Slip39_Basic + assert features.initialized is True + assert features.needs_backup is False + assert features.no_backup is False + assert features.recovery_status == messages.RecoveryStatus.NoRecovery + + # run recovery to unlock backup + device_handler.run( + device.recover, + recovery_kind=messages.RecoveryKind.UnlockRepeatedBackup, + ) + + recovery.confirm_recovery(debug, "recovery__title_unlock_repeated_backup") + + recovery.select_number_of_words(debug, num_of_words=20, unlock_repeated_backup=True) + recovery.enter_seed( + debug, + initial_backup_1_of_1, + True, + "recovery__enter_backup", + "recovery__unlock_repeated_backup", + ) + + # backup is enabled + go_next(debug) + + assert device_handler.result().message == "Backup unlocked" + + # we are now in recovery mode + features = device_handler.features() + assert features.backup_type is messages.BackupType.Slip39_Basic + assert features.initialized is True + assert features.needs_backup is False + assert features.no_backup is False + assert ( + features.recovery_status + == messages.RecoveryStatus.InUnlockRepeatedBackupRecovery + ) + + # at this point, the backup is unlocked... + + # ... so let's try to do a 2-of-3 backup + + # confirm checklist + reset.confirm_read(debug) + + # shares=3 + reset.set_selection(debug, buttons.RESET_MINUS, 5 - 3) + + # confirm checklist + reset.confirm_read(debug) + + # threshold=2 + reset.set_selection(debug, buttons.RESET_MINUS, 1) + + # confirm checklist + reset.confirm_read(debug) + + # confirm backup warning + reset.confirm_read(debug, middle_r=True) + + second_backup_2_of_3: list[str] = [] + for _ in range(3): + # read words + words = reset.read_words(debug, do_htc=False) + + # confirm words + reset.confirm_words(debug, words) + + # confirm share checked + reset.confirm_read(debug) + + second_backup_2_of_3.append(" ".join(words)) + + # we are not in recovery mode anymore, because we finished the backup process! + features = device_handler.features() + assert features.backup_type is messages.BackupType.Slip39_Basic + assert features.initialized is True + assert features.needs_backup is False + assert features.no_backup is False + assert features.recovery_status == messages.RecoveryStatus.NoRecovery + + # try to unlock backup again... + device_handler.run( + device.recover, + recovery_kind=messages.RecoveryKind.UnlockRepeatedBackup, + ) + + recovery.confirm_recovery(debug, "recovery__title_unlock_repeated_backup") + + # ... this time with the 2 shares from the *new* backup, which was a 2-of-3! + recovery.select_number_of_words(debug, num_of_words=20, unlock_repeated_backup=True) + recovery.enter_shares( + debug, + second_backup_2_of_3[-2:], + "recovery__title_unlock_repeated_backup", + "recovery__enter_backup", + "recovery__unlock_repeated_backup", + ) + + assert device_handler.result().message == "Backup unlocked" + + # we are now in recovery mode again! + features = device_handler.features() + assert features.backup_type is messages.BackupType.Slip39_Basic + assert features.initialized is True + assert features.needs_backup is False + assert features.no_backup is False + assert ( + features.recovery_status + == messages.RecoveryStatus.InUnlockRepeatedBackupRecovery + ) + + # but if we cancel the backup at this point... + reset.cancel_backup(debug) + + # ...we are out of recovery mode! + features = device_handler.features() + assert features.backup_type is messages.BackupType.Slip39_Basic + assert features.initialized is True + assert features.needs_backup is False + assert features.no_backup is False + assert features.recovery_status == messages.RecoveryStatus.NoRecovery diff --git a/tests/click_tests/test_reset_bip39.py b/tests/click_tests/test_reset_bip39.py index 9433efcc6e..9e990ee9a9 100644 --- a/tests/click_tests/test_reset_bip39.py +++ b/tests/click_tests/test_reset_bip39.py @@ -56,7 +56,7 @@ def test_reset_bip39(device_handler: "BackgroundDeviceHandler"): reset.confirm_read(debug, middle_r=True) # read words - words = reset.read_words(debug, messages.BackupType.Bip39) + words = reset.read_words(debug) # confirm words reset.confirm_words(debug, words) diff --git a/tests/click_tests/test_reset_slip39_advanced.py b/tests/click_tests/test_reset_slip39_advanced.py index e37965db87..1b518227c6 100644 --- a/tests/click_tests/test_reset_slip39_advanced.py +++ b/tests/click_tests/test_reset_slip39_advanced.py @@ -114,9 +114,7 @@ def test_reset_slip39_advanced( for _ in range(group_count): for _ in range(share_count): # read words - words = reset.read_words( - debug, messages.BackupType.Slip39_Advanced, do_htc=False - ) + words = reset.read_words(debug, do_htc=False) # confirm words reset.confirm_words(debug, words) diff --git a/tests/click_tests/test_reset_slip39_basic.py b/tests/click_tests/test_reset_slip39_basic.py index 790e98bb6d..ab90ebc391 100644 --- a/tests/click_tests/test_reset_slip39_basic.py +++ b/tests/click_tests/test_reset_slip39_basic.py @@ -93,7 +93,7 @@ def test_reset_slip39_basic( all_words: list[str] = [] for _ in range(num_of_shares): # read words - words = reset.read_words(debug, messages.BackupType.Slip39_Basic) + words = reset.read_words(debug) # confirm words reset.confirm_words(debug, words) diff --git a/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py b/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py index 5e6494a766..ed1f1cf52f 100644 --- a/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py +++ b/tests/device_tests/reset_recovery/test_recovery_bip39_dryrun.py @@ -40,7 +40,7 @@ def do_recover_legacy(client: Client, mnemonic: list[str], **kwargs: Any): ret = device.recover( client, - dry_run=True, + recovery_kind=messages.RecoveryKind.DryRun, word_count=len(mnemonic), type=messages.RecoveryDeviceType.ScrambledWords, input_callback=input_callback, @@ -56,7 +56,7 @@ def do_recover_core(client: Client, mnemonic: list[str], mismatch: bool = False) client.watch_layout() IF = InputFlowBip39RecoveryDryRun(client, mnemonic, mismatch=mismatch) client.set_input_flow(IF.get()) - return device.recover(client, dry_run=True) + return device.recover(client, recovery_kind=messages.RecoveryKind.DryRun) def do_recover(client: Client, mnemonic: list[str], mismatch: bool = False): @@ -105,7 +105,7 @@ def test_uninitialized(client: Client): DRY_RUN_ALLOWED_FIELDS = ( - "dry_run", + "kind", "word_count", "enforce_wordlist", "type", @@ -131,6 +131,8 @@ def _make_bad_params(): yield field.name, True elif field.type == "string": yield field.name, "test" + elif field.type == "RecoveryKind": + yield field.name, 1 else: # Someone added a field to RecoveryDevice of a type that has no assigned # default value. This test must be fixed. @@ -140,13 +142,14 @@ def _make_bad_params(): @pytest.mark.parametrize("field_name, field_value", _make_bad_params()) def test_bad_parameters(client: Client, field_name: str, field_value: Any): msg = messages.RecoveryDevice( - dry_run=True, + kind=messages.RecoveryKind.DryRun, word_count=12, enforce_wordlist=True, type=messages.RecoveryDeviceType.ScrambledWords, ) setattr(msg, field_name, field_value) with pytest.raises( - exceptions.TrezorFailure, match="Forbidden field set in dry-run" + exceptions.TrezorFailure, + match="Forbidden field set in dry-run", ): client.call(msg) diff --git a/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py b/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py index 4717a0f430..12df18116f 100644 --- a/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py +++ b/tests/device_tests/reset_recovery/test_recovery_bip39_t1.py @@ -202,10 +202,10 @@ def test_already_initialized(client: Client): with pytest.raises(RuntimeError): device.recover( client, - 12, - False, - False, - "label", + word_count=12, + pin_protection=False, + passphrase_protection=False, + label="label", input_callback=client.mnemonic_callback, ) diff --git a/tests/device_tests/test_repeated_backup.py b/tests/device_tests/test_repeated_backup.py new file mode 100644 index 0000000000..abe1a70b39 --- /dev/null +++ b/tests/device_tests/test_repeated_backup.py @@ -0,0 +1,110 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2024 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + + +import pytest + +from trezorlib import device, messages +from trezorlib.debuglink import TrezorClientDebugLink as Client +from trezorlib.exceptions import Cancelled, TrezorFailure + +from ..common import WITH_MOCK_URANDOM, MNEMONIC_SLIP39_BASIC_20_3of6 +from ..input_flows import InputFlowSlip39BasicBackup, InputFlowSlip39BasicRecoveryDryRun + + +@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6) +@pytest.mark.skip_t1b1 +@WITH_MOCK_URANDOM +def test_repeated_backup(client: Client): + assert client.features.needs_backup is True + + # initial device backup + mnemonics = [] + with client: + IF = InputFlowSlip39BasicBackup(client, False) + client.set_input_flow(IF.get()) + device.backup(client) + mnemonics = IF.mnemonics + + assert len(mnemonics) == 5 + + # cannot backup, since we already just did that! + with pytest.raises(TrezorFailure, match=r".*Seed already backed up"): + device.backup(client) + + # unlock repeated backup by entering 3 of the 5 shares we have got + with client: + IF = InputFlowSlip39BasicRecoveryDryRun( + client, mnemonics[:3], unlock_repeated_backup=True + ) + client.set_input_flow(IF.get()) + ret = device.recover( + client, recovery_kind=messages.RecoveryKind.UnlockRepeatedBackup + ) + assert ret == messages.Success(message="Backup unlocked") + + # we can now perform another backup + with client: + IF = InputFlowSlip39BasicBackup(client, False) + client.set_input_flow(IF.get()) + device.backup(client) + + # the backup feature is locked again... + with pytest.raises(TrezorFailure, match=r".*Seed already backed up"): + device.backup(client) + + +@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6) +@pytest.mark.skip_t1b1 +@WITH_MOCK_URANDOM +def test_repeated_backup_cancel(client: Client): + assert client.features.needs_backup is True + + # initial device backup + mnemonics = [] + with client: + IF = InputFlowSlip39BasicBackup(client, False) + client.set_input_flow(IF.get()) + device.backup(client) + mnemonics = IF.mnemonics + + assert len(mnemonics) == 5 + + # cannot backup, since we already just did that! + with pytest.raises(TrezorFailure, match=r".*Seed already backed up"): + device.backup(client) + + # unlock repeated backup by entering 3 of the 5 shares we have got + with client: + IF = InputFlowSlip39BasicRecoveryDryRun( + client, mnemonics[:3], unlock_repeated_backup=True + ) + client.set_input_flow(IF.get()) + ret = device.recover( + client, recovery_kind=messages.RecoveryKind.UnlockRepeatedBackup + ) + assert ret == messages.Success(message="Backup unlocked") + + client.debug.wait_layout() + + # send a Cancel message + + with pytest.raises(Cancelled): + client.call(messages.Cancel()) + + # the backup feature is locked again... + with pytest.raises(TrezorFailure, match=r".*Seed already backed up"): + device.backup(client) diff --git a/tests/input_flows.py b/tests/input_flows.py index 9e99f74dde..5d630658ba 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -1979,15 +1979,25 @@ class InputFlowSlip39AdvancedRecoveryShareAlreadyEntered(InputFlowBase): class InputFlowSlip39BasicRecoveryDryRun(InputFlowBase): - def __init__(self, client: Client, shares: list[str], mismatch: bool = False): + def __init__( + self, + client: Client, + shares: list[str], + mismatch: bool = False, + unlock_repeated_backup=False, + ): super().__init__(client) self.shares = shares self.mismatch = mismatch + self.unlock_repeated_backup = unlock_repeated_backup self.word_count = len(shares[0].split(" ")) def input_flow_common(self) -> BRGeneratorType: yield from self.REC.confirm_dry_run() - yield from self.REC.setup_slip39_recovery(self.word_count) + if self.unlock_repeated_backup: + yield from self.REC.setup_repeated_backup_recovery(self.word_count) + else: + yield from self.REC.setup_slip39_recovery(self.word_count) yield from self.REC.input_all_slip39_shares(self.shares) if self.mismatch: yield from self.REC.warning_slip39_dryrun_mismatch() diff --git a/tests/input_flows_helpers.py b/tests/input_flows_helpers.py index 9c0a2860d0..343b363f61 100644 --- a/tests/input_flows_helpers.py +++ b/tests/input_flows_helpers.py @@ -73,6 +73,12 @@ class RecoveryFlow: yield from self.input_number_of_words(num_words) yield from self.enter_any_share() + def setup_repeated_backup_recovery(self, num_words: int) -> BRGeneratorType: + if self.client.model is models.T2B1: + yield from self.tr_recovery_homescreen() + yield from self.input_number_of_words(num_words) + yield from self.enter_your_backup() + def setup_bip39_recovery(self, num_words: int) -> BRGeneratorType: if self.client.model is models.T2B1: yield from self.tr_recovery_homescreen() diff --git a/tests/persistence_tests/test_shamir_persistence.py b/tests/persistence_tests/test_shamir_persistence.py index 6b919a018d..d2eae5ac90 100644 --- a/tests/persistence_tests/test_shamir_persistence.py +++ b/tests/persistence_tests/test_shamir_persistence.py @@ -18,6 +18,7 @@ import pytest from trezorlib import device, models from trezorlib.debuglink import DebugLink +from trezorlib.messages import RecoveryStatus from .. import buttons from ..click_tests import recovery @@ -43,7 +44,7 @@ def test_abort(core_emulator: Emulator): if debug.model is models.T3T1: pytest.skip("abort not supported on T3T1") - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery device_handler.run(device.recover, pin_protection=False) @@ -55,7 +56,7 @@ def test_abort(core_emulator: Emulator): debug = _restart(device_handler, core_emulator) features = device_handler.features() - assert features.recovery_mode is True + assert features.recovery_status == RecoveryStatus.InNormalRecovery # no waiting for layout because layout doesn't change assert "number of words" in debug.read_layout().text_content() @@ -71,7 +72,7 @@ def test_abort(core_emulator: Emulator): assert layout.main_component() == "Homescreen" features = device_handler.features() - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery @core_only @@ -81,7 +82,7 @@ def test_recovery_single_reset(core_emulator: Emulator): features = device_handler.features() assert features.initialized is False - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery device_handler.run(device.recover, pin_protection=False) @@ -91,7 +92,7 @@ def test_recovery_single_reset(core_emulator: Emulator): debug = _restart(device_handler, core_emulator) features = device_handler.features() - assert features.recovery_mode is True + assert features.recovery_status == RecoveryStatus.InNormalRecovery # we need to enter the number of words again, that's a feature recovery.select_number_of_words(debug, wait=False) @@ -100,7 +101,7 @@ def test_recovery_single_reset(core_emulator: Emulator): features = device_handler.features() assert features.initialized is True - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery @core_only @@ -118,7 +119,7 @@ def test_recovery_on_old_wallet(core_emulator: Emulator): features = device_handler.features() assert features.initialized is False - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery # enter recovery mode device_handler.run(device.recover, pin_protection=False) @@ -128,7 +129,7 @@ def test_recovery_on_old_wallet(core_emulator: Emulator): # restart to get into stand-alone recovery debug = _restart(device_handler, core_emulator) features = device_handler.features() - assert features.recovery_mode is True + assert features.recovery_status == RecoveryStatus.InNormalRecovery # enter number of words recovery.select_number_of_words(debug, wait=False) @@ -167,7 +168,7 @@ def test_recovery_on_old_wallet(core_emulator: Emulator): # check that the recovery succeeded features = device_handler.features() assert features.initialized is True - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery @core_only @@ -191,7 +192,7 @@ def test_recovery_multiple_resets(core_emulator: Emulator): features = device_handler.features() assert features.initialized is False - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery # start device and recovery device_handler.run(device.recover, pin_protection=False) @@ -204,7 +205,7 @@ def test_recovery_multiple_resets(core_emulator: Emulator): # restart debug = _restart(device_handler, core_emulator) features = device_handler.features() - assert features.recovery_mode is True + assert features.recovery_status == RecoveryStatus.InNormalRecovery # enter the number of words again, that's a feature! recovery.select_number_of_words(debug, wait=False) @@ -216,4 +217,4 @@ def test_recovery_multiple_resets(core_emulator: Emulator): features = device_handler.features() assert features.initialized is True - assert features.recovery_mode is False + assert features.recovery_status == RecoveryStatus.NoRecovery diff --git a/tests/translations.py b/tests/translations.py index c9be7881c5..3be33e88e8 100644 --- a/tests/translations.py +++ b/tests/translations.py @@ -90,10 +90,16 @@ def _resolve_path_to_texts( texts: list[str] = [] lookups = path.split(".") for language_data in all_language_data: + language_data_missing = False data: dict[str, t.Any] | str = language_data for lookup in lookups: assert isinstance(data, dict), f"{lookup} is not a dict" + if lookup not in data: + language_data_missing = True + break data = data[lookup] + if language_data_missing: + continue assert isinstance(data, str), f"{path} is not a string" if template: data = data.format(*template) diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json index 76bb31f0f3..be1dd8a58d 100644 --- a/tests/ui_tests/fixtures.json +++ b/tests/ui_tests/fixtures.json @@ -864,6 +864,9 @@ "T2B1_en_test_recovery.py::test_recovery_bip39": "fd055e526b66f7d0eb3b6968f3889eb4a3e589fd3700b6688e68ea5d6c0c18dd", "T2B1_en_test_recovery.py::test_recovery_bip39_previous_word": "10c4d76d4f636878540dcb6ba258a2d28489012298cf273af3808bcadbe38754", "T2B1_en_test_recovery.py::test_recovery_slip39_basic": "771d9bd01496b0fea72d5faca60fcc564e83d4a69e5fe02770238071dec7274d", +"T2B1_en_test_repeated_backup.py::test_repeated_backup": "cbdb2fc033538e4995a9ba5fc658a2e58936c209e5165244f3f13d65de7125bd", +"T2B1_en-test_repeated_backup.py::test_repeated_backup_cancel": "902b8f47380e0591bf3d5e410a9db86fe1a3b8221c968b89c97960fdd91e5100", +"T2B1_en-test_repeated_backup.py::test_repeated_backup_send_disallowed_message": "660c0dd0c290f8320d0194c6df2c44c9741a7f05414c0afddad0b23b95c0906d", "T2B1_en_test_reset_bip39.py::test_reset_bip39": "6d229eb212360ffad2bc72154588ec82f699ae6d0cbbf87fa950dd4c8e2ea9c9", "T2B1_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "5cd7eb5b96673f7f9dc066d2860ef4ab42926488a5117a2bc89caa667c3916e1", "T2B1_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "cb815314c0607f0d6f678f5a9b9b99d6e50cbb670a83b3463ad2bf8635f9319a", @@ -7995,6 +7998,7 @@ "T2T1_en_test_recovery.py::test_recovery_bip39": "ee619ed8e0cf0fbd75a685830d1873e23896dcca6477b2bbf39eaa8d9b4c8531", "T2T1_en_test_recovery.py::test_recovery_bip39_previous_word": "b41a576c6e879512c80337f5d58fdaa93d2259ed42f164f808e52e0d3ccf1211", "T2T1_en_test_recovery.py::test_recovery_slip39_basic": "81bc28e27af3667606f0479720e4dbdfdb97805b9d94a6f3dfe9d9cb3b045c65", +"T2T1_en_test_repeated_backup.py::test_repeated_backup": "4ee56b338d37f74f892ad80e3ad35bb8f9113b4de602b57136899f52711ac0d9", "T2T1_en_test_reset_bip39.py::test_reset_bip39": "e7d4a85f9ebfaec35006a566e0a25167f276620d50d847a08721da861cac725a", "T2T1_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[16of16]": "259315387e0837ac718654d9180f5730370fa76d77318d620288885ebbddb0fb", "T2T1_en_test_reset_slip39_advanced.py::test_reset_slip39_advanced[2of2]": "878b828c11353fbfcd854fd54ed172dc7d395c753195d5444b5b57e3499e2bca", @@ -12447,6 +12451,9 @@ "T2T1_en_test_protection_levels.py::test_unlocked": "4488e2b6f06fdff05749ac271d080182f1c95645de37898457ff12f0fb190381", "T2T1_en_test_protection_levels.py::test_verify_message_t2": "cc09f0acf9e48b1355dae0be65a2a97abbe2811808feee08d46ae7146ce6bb6f", "T2T1_en_test_protection_levels.py::test_wipe_device": "7ff017de646b7cf70832605e1750c635d0eb661d51534b56007b49e82b927011", +"T2T1_en_test_repeated_backup.py::test_repeated_backup": "321a6c7a274e20fbddcc0dbdff31181950a613c97a77ad48ce1bb31845ffdc0e", +"T2T1_en_test_repeated_backup.py::test_repeated_backup_cancel": "9ab2f1f9e49582efa1e98c728b38ce1a21c4f09d398b20f616507f67a6a292b2", +"T2T1_en_test_repeated_backup.py::test_repeated_backup_send_disallowed_message": "dc906965730138e588e4efb5e7eb71609c504f5910352ce04bd926358184af36", "T2T1_en_test_sdcard.py::test_sd_format": "83d0d9b4eab3bf0eafc22d7d95e8b70bce477ba9c8b4ba13eeca9380ad5fdafa", "T2T1_en_test_sdcard.py::test_sd_no_format": "14511e3d3ee535d97287d8ade25101e8c16db17c1dc5d3151b91e5e8eba61ba5", "T2T1_en_test_sdcard.py::test_sd_protect_unlock": "1302f9a0835cac621142a17031d2150553e676261a3eeeefd32fcf1e69c7bd1a", diff --git a/tests/upgrade_tests/test_firmware_upgrades.py b/tests/upgrade_tests/test_firmware_upgrades.py index a878b46ff0..5abd379812 100644 --- a/tests/upgrade_tests/test_firmware_upgrades.py +++ b/tests/upgrade_tests/test_firmware_upgrades.py @@ -21,7 +21,7 @@ import pytest from shamir_mnemonic import shamir from trezorlib import btc, debuglink, device, exceptions, fido, models -from trezorlib.messages import ApplySettings, BackupType, Success +from trezorlib.messages import ApplySettings, BackupType, RecoveryStatus, Success from trezorlib.tools import H_ from ..common import MNEMONIC_SLIP39_BASIC_20_3of6, MNEMONIC_SLIP39_BASIC_20_3of6_SECRET @@ -291,7 +291,7 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]): with EmulatorWrapper(gen, tag) as emu, BackgroundDeviceHandler( emu.client ) as device_handler: - assert emu.client.features.recovery_mode is False + assert emu.client.features.recovery_status == RecoveryStatus.NoRecovery emu.client.watch_layout(True) debug = device_handler.debuglink() @@ -312,7 +312,7 @@ def test_upgrade_shamir_recovery(gen: str, tag: Optional[str]): with EmulatorWrapper(gen, storage=storage) as emu: assert device_id == emu.client.features.device_id - assert emu.client.features.recovery_mode + assert emu.client.features.recovery_status == RecoveryStatus.InNormalRecovery debug = emu.client.debug emu.client.watch_layout(True)