From fd59c2ce887d13691f5c6c7d360886d1afa49a37 Mon Sep 17 00:00:00 2001 From: obrusvit Date: Tue, 4 Jun 2024 01:24:09 +0200 Subject: [PATCH] feat(core/ui): T3T1 backup cannot be skipped This commit disallows the user to exit backup flow once initiated. It also ensurer the intro screen is shown when initiating backup via protobuf msg after it was previously skipped during wallet creation. [no changelog] --- .../ui/model_mercury/flow/prompt_backup.rs | 10 +++------ .../ui/model_mercury/flow/request_number.rs | 5 +---- .../ui/model_mercury/flow/show_share_words.rs | 6 +++++- .../embed/rust/src/ui/model_mercury/layout.rs | 1 + core/mocks/generated/trezorui2.pyi | 1 + .../apps/management/reset_device/__init__.py | 17 +++++++++++++-- .../apps/management/reset_device/layout.py | 15 ++++++++----- .../src/trezor/ui/layouts/mercury/__init__.py | 1 - core/src/trezor/ui/layouts/mercury/reset.py | 21 +++++++++++++++++++ core/src/trezor/ui/layouts/tr/reset.py | 19 ++++++++++++++++- core/src/trezor/ui/layouts/tt/reset.py | 21 +++++++++++++++++++ tests/input_flows.py | 2 +- .../test_shamir_persistence.py | 4 ++-- 13 files changed, 99 insertions(+), 24 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs b/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs index fbce08f018..ba2b43bb9b 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/prompt_backup.rs @@ -97,13 +97,9 @@ pub extern "C" fn new_prompt_backup(n_args: usize, args: *const Obj, kwargs: *mu impl PromptBackup { fn new_obj(_args: &[Obj], _kwargs: &Map) -> Result { let title: TString = TR::backup__title_create_wallet_backup.into(); - let par_array: [Paragraph<'static>; 1] = [Paragraph::new( - &theme::TEXT_MAIN_GREY_LIGHT, - // FIXME: should be "contains X words" but the mnemonic/shares are not yet generated at - // this point. We might need to merge the PromptBackup and ShowShareWords flows - TString::from_str("Your wallet backup contains words in a specific order."), - )]; - let paragraphs = Paragraphs::new(par_array); + let text_intro: TString = TR::backup__it_should_be_backed_up.into(); + + let paragraphs = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, text_intro)); let content_intro = Frame::left_aligned(title, SwipeContent::new(paragraphs)) .with_menu_button() .with_footer(TR::instructions__swipe_up.into(), None) diff --git a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs index df940c9d81..55ffad8a20 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/request_number.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/request_number.rs @@ -52,7 +52,6 @@ impl FlowState for RequestNumber { (RequestNumber::Menu, FlowMsg::Choice(0)) => { Decision::Goto(RequestNumber::Info, SwipeDirection::Left) } - (RequestNumber::Menu, FlowMsg::Choice(1)) => Decision::Return(FlowMsg::Cancelled), (RequestNumber::Menu, FlowMsg::Cancelled) => { Decision::Goto(RequestNumber::Number, SwipeDirection::Right) } @@ -119,9 +118,7 @@ impl RequestNumber { let content_menu = Frame::left_aligned( "".into(), - VerticalMenu::empty() - .item(theme::ICON_CHEVRON_RIGHT, TR::buttons__more_info.into()) - .danger(theme::ICON_CANCEL, TR::backup__title_skip.into()), + VerticalMenu::empty().item(theme::ICON_CHEVRON_RIGHT, TR::buttons__more_info.into()), ) .with_cancel_button() .with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) diff --git a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs index 96e0bd3174..37337020f2 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs @@ -79,6 +79,10 @@ impl ShowShareWords { let subtitle: TString = kwargs.get(Qstr::MP_QSTR_subtitle)?.try_into()?; let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let share_words_vec: Vec = util::iter_into_vec(share_words_obj)?; + let description: Option = kwargs + .get(Qstr::MP_QSTR_description)? + .try_into_option()? + .and_then(|desc: TString| if desc.is_empty() { None } else { Some(desc) }); let text_info: Obj = kwargs.get(Qstr::MP_QSTR_text_info)?; let text_confirm: TString = kwargs.get(Qstr::MP_QSTR_text_confirm)?.try_into()?; let nwords = share_words_vec.len(); @@ -98,7 +102,7 @@ impl ShowShareWords { ), ) .with_subtitle(TR::words__instructions.into()) - .with_footer(TR::instructions__swipe_up.into(), None) + .with_footer(TR::instructions__swipe_up.into(), description) .with_swipe(SwipeDirection::Up, SwipeSettings::default()) .map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed)) .one_button_request(ButtonRequestCode::ResetDevice.with_type("share_words")) diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 48a22a6565..7ea0b5d2a0 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -1688,6 +1688,7 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// subtitle: str, /// words: Iterable[str], + /// description: str, /// text_info: Iterable[str], /// text_confirm: str, /// ) -> LayoutObj[UiResult]: diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 8318286ab4..8a50f68eaf 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -402,6 +402,7 @@ def flow_show_share_words( title: str, subtitle: str, words: Iterable[str], + description: str, text_info: Iterable[str], text_confirm: str, ) -> LayoutObj[UiResult]: diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 4cf51a3db0..94974dae24 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -126,13 +126,21 @@ async def reset_device(msg: ResetDevice) -> Success: return Success(message="Initialized") +async def _backup_bip39(mnemonic: str) -> None: + words = mnemonic.split() + await layout.show_backup_intro(single_share=True, num_of_words=len(words)) + await layout.show_and_confirm_single_share(words) + + async def _backup_slip39_single( encrypted_master_secret: bytes, extendable: bool ) -> None: mnemonics = _get_slip39_mnemonics(encrypted_master_secret, 1, ((1, 1),), extendable) + words = mnemonics[0][0].split() # for a single 1-of-1 group, we use the same layouts as for BIP39 - await layout.show_and_confirm_mnemonic(mnemonics[0][0]) + await layout.show_backup_intro(single_share=True, num_of_words=len(words)) + await layout.show_and_confirm_single_share(words) async def _backup_slip39_basic( @@ -140,6 +148,8 @@ async def _backup_slip39_basic( ) -> None: group_threshold = 1 + await layout.show_backup_intro(single_share=False) + # get number of shares await layout.slip39_show_checklist(0, advanced=False) share_count = await layout.slip39_prompt_number_of_shares() @@ -165,6 +175,9 @@ async def _backup_slip39_basic( async def _backup_slip39_advanced( encrypted_master_secret: bytes, extendable: bool ) -> None: + + await layout.show_backup_intro(single_share=False) + # get number of groups await layout.slip39_show_checklist(0, advanced=True) groups_count = await layout.slip39_advanced_prompt_number_of_groups() @@ -287,4 +300,4 @@ async def backup_seed(backup_type: BackupType, mnemonic_secret: bytes) -> None: else: await _backup_slip39_basic(mnemonic_secret, extendable) else: - await layout.show_and_confirm_mnemonic(mnemonic_secret.decode()) + await _backup_bip39(mnemonic_secret.decode()) diff --git a/core/src/apps/management/reset_device/layout.py b/core/src/apps/management/reset_device/layout.py index f2958290be..8376d038e3 100644 --- a/core/src/apps/management/reset_device/layout.py +++ b/core/src/apps/management/reset_device/layout.py @@ -3,7 +3,6 @@ from typing import Sequence from trezor import TR from trezor.enums import ButtonRequestType -from trezor.ui.layouts import show_success from trezor.ui.layouts.reset import ( # noqa: F401 show_share_words, slip39_advanced_prompt_group_threshold, @@ -70,8 +69,8 @@ async def _share_words_confirmed( Return true if the words are confirmed successfully. """ from trezor.ui.layouts.reset import ( - show_share_confirmation_success, show_share_confirmation_failure, + show_share_confirmation_success, ) if await _do_confirm_share_words(share_index, share_words, group_index): @@ -108,6 +107,14 @@ async def _do_confirm_share_words( return True +async def show_backup_intro( + single_share: bool, num_of_words: int | None = None +) -> None: + from trezor.ui.layouts.reset import show_intro_backup + + await show_intro_backup(single_share, num_of_words) + + async def show_backup_warning() -> None: from trezor.ui.layouts.reset import show_warning_backup @@ -124,12 +131,10 @@ async def show_backup_success() -> None: # === -async def show_and_confirm_mnemonic(mnemonic: str) -> None: +async def show_and_confirm_single_share(words: Sequence[str]) -> None: # warn user about mnemonic safety await show_backup_warning() - words = mnemonic.split() - while True: # display paginated mnemonic on the screen await show_share_words(words) diff --git a/core/src/trezor/ui/layouts/mercury/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index 5c057ae5e7..e74cc2deb4 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -382,7 +382,6 @@ async def prompt_backup() -> bool: "backup_device", ButtonRequestType.ResetDevice, ) - return result is CONFIRMED diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index ef5de17cdb..e3cc9b0ae2 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -32,12 +32,14 @@ async def show_share_words( group_index + 1, share_index + 1 ) words_count = len(share_words) + description = "" text_info = [TR.reset__write_down_words_template.format(words_count)] if words_count == 20 and share_index is None: # 1-of-1 SLIP39: inform the user about repeated words text_info.append(TR.reset__words_may_repeat) if share_index == 0: # regular SLIP39, 1st share + description = TR.instructions__shares_start_with_1 text_info.append(TR.reset__repeat_for_all_shares) text_confirm = TR.reset__words_written_down_template.format(words_count) @@ -46,6 +48,7 @@ async def show_share_words( title=title, subtitle=subtitle, words=share_words, + description=description, text_info=text_info, text_confirm=text_confirm, ) @@ -304,6 +307,24 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) +async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: + if single_share: + assert num_of_words is not None + description = TR.backup__info_single_share_backup.format(num_of_words) + else: + description = TR.backup__info_multi_share_backup + + await interact( + RustLayout( + trezorui2.show_info( + title=TR.backup__title_create_wallet_backup, description=description + ) + ), + "backup_warning", + ButtonRequestType.ResetDevice, + ) + + async def show_warning_backup() -> None: result = await interact( RustLayout( diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index 2d17232430..13ca822af7 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -6,7 +6,7 @@ from trezor.enums import ButtonRequestType from trezor.wire import ActionCancelled from ..common import interact -from . import RustLayout, confirm_action, show_warning, show_success +from . import RustLayout, confirm_action, show_success, show_warning CONFIRMED = trezorui2.CONFIRMED # global_import_cache @@ -254,6 +254,23 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) +async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: + if single_share: + assert num_of_words is not None + description = TR.backup__info_single_share_backup.format(num_of_words) + else: + description = TR.backup__info_multi_share_backup + + await confirm_action( + "backup_warning", + title=TR.backup__title_backup_wallet, + verb=TR.buttons__continue, + description=description, + verb_cancel=None, + br_code=ButtonRequestType.ResetDevice, + ) + + async def show_warning_backup() -> None: await show_warning( "backup_warning", diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index 53c51bbc4f..5794c27792 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -309,6 +309,27 @@ async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int: ) +async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None: + if single_share: + assert num_of_words is not None + description = TR.backup__info_single_share_backup.format(num_of_words) + else: + description = TR.backup__info_multi_share_backup + + await interact( + RustLayout( + trezorui2.show_info( + title="", + button=TR.buttons__continue, + description=description, + allow_cancel=False, + ) + ), + "backup_warning", + ButtonRequestType.ResetDevice, + ) + + async def show_warning_backup() -> None: result = await interact( RustLayout( diff --git a/tests/input_flows.py b/tests/input_flows.py index f2b7babb18..56a7e201bf 100644 --- a/tests/input_flows.py +++ b/tests/input_flows.py @@ -1310,7 +1310,7 @@ class InputFlowBip39ResetBackup(InputFlowBase): # 1. Confirm Reset x3 # 2. Backup your seed # 3. Confirm warning - yield from click_through(self.debug, screens=4, code=B.ResetDevice) + yield from click_through(self.debug, screens=5, code=B.ResetDevice) # mnemonic phrases and rest self.mnemonic = yield from get_mnemonic_and_confirm_success(self.debug) diff --git a/tests/persistence_tests/test_shamir_persistence.py b/tests/persistence_tests/test_shamir_persistence.py index aa958d49e7..bfe3f5b4f5 100644 --- a/tests/persistence_tests/test_shamir_persistence.py +++ b/tests/persistence_tests/test_shamir_persistence.py @@ -138,7 +138,7 @@ def test_recovery_on_old_wallet(core_emulator: Emulator): words = first_share.split(" ") # start entering first share - assert "the first 2-4 letters" in debug.read_layout().text_content() + assert "Enter each word of your wallet backup" in debug.read_layout().text_content() debug.press_yes() assert debug.wait_layout().main_component() == "MnemonicKeyboard" @@ -176,7 +176,7 @@ def test_recovery_multiple_resets(core_emulator: Emulator): def enter_shares_with_restarts(debug: DebugLink) -> None: shares = MNEMONIC_SLIP39_ADVANCED_20 layout = debug.read_layout() - expected_text = "the first 2-4 letters" + expected_text = "Enter each word of your wallet backup" remaining = len(shares) for share in shares: assert expected_text in layout.text_content()