feat(core/delizia): allow back in multishare setup
What changed, and why it matters
This commit adds a "go back" option during the multi-share backup setup on Trezor's Delizia user interface. It lets users return to a previous step if they made a mistake while choosing the number of shares or thresholds. There is no indication this fixes a security vulnerability; it is a usability improvement.
No security action required. Review as a normal feature commit.
Security signals we found
No security-relevant signals observed in the diff
Change is a UX/navigation feature, not a memory-safety, cryptographic, or access-control fix
Cancellation paths are explicitly marked as debuglink-only
Evidence from the diff
The change enables backward navigation in the SLIP-39 backup setup flow for the Delizia layout. In the Rust UI layer, the checklist screen now honors the existing back_button parameter by adding a menu button and external menu support when going back is allowed. In the Python reset layout, slip39_show_checklist and _prompt_number now handle the INFO event to show a menu containing a “Go back” item, returning trezorui_api.BACK to the caller. Tests are updated to exercise the new navigation paths on Delizia, mirroring existing Eckhart coverage.
Changed components
core/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/src/trezor/ui/layouts/delizia/reset.pytests/device_tests/test_msg_backup_device.pytests/input_flows.pyInspect captured patch +262 / −30
### core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -804,7 +804,7 @@ impl FirmwareUI for UIDelizia {
_button: TString<'static>,
active: usize,
items: [TString<'static>; MAX_CHECKLIST_ITEMS],
- _back_button: bool,
+ back_button: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
let mut paragraphs = ParagraphVecLong::new();
for (i, item) in items.into_iter().enumerate() {
@@ -833,13 +833,21 @@ impl FirmwareUI for UIDelizia {
.with_icon_done_color(theme::GREEN)
.with_done_offset(theme::CHECKLIST_DONE_OFFSET);
- let layout = RootComponent::new(SwipeUpScreen::new(
- Frame::with_header(
- Header::left_aligned(title),
- SwipeContent::new(checklist_content),
- )
- .with_swipeup_footer(None),
- ));
+ // When going back is possible, the header has a menu button emitting
+ // `FlowMsg::Info`; the menu itself is external, shown from Python.
+ let header = if back_button {
+ Header::left_aligned(title).with_menu_button()
+ } else {
+ Header::left_aligned(title)
+ };
+ let frame = Frame::with_header(header, SwipeContent::new(checklist_content))
+ .with_swipeup_footer(None);
+ let frame = if back_button {
+ frame.with_external_menu()
+ } else {
+ frame
+ };
+ let layout = RootComponent::new(SwipeUpScreen::new(frame));
Ok(layout)
}
### core/src/trezor/ui/layouts/delizia/reset.py
@@ -89,26 +89,50 @@ async def select_word(
return words[result]
+async def _go_back() -> ui.UiResult:
+ return trezorui_api.BACK
+
+
async def slip39_show_checklist(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
back_button: bool = False,
) -> ui.UiResult:
+ from trezor.ui.layouts.menu import Menu, MenuLeaf, show_menu
+
items = _slip_39_checklist_items(step, advanced, count, threshold)
with trezorui_api.show_checklist(
title=TR.reset__title_shamir_backup,
button=TR.buttons__continue,
active=step,
items=items,
+ back_button=back_button,
) as layout:
- result = await interact(
- layout, "slip39_checklist", ButtonRequestType.ResetDevice
- )
- if result != CONFIRMED:
- raise ActionCancelled
- return result
+ br_name_once: str | None = "slip39_checklist"
+ while True:
+ result = await interact(
+ layout,
+ br_name_once,
+ ButtonRequestType.ResetDevice,
+ raise_on_cancel=None,
+ )
+ br_name_once = None # ButtonRequest should be sent only once
+
+ if result is CONFIRMED:
+ return result
+
+ if result is trezorui_api.INFO:
+ # shows the menu with the "go back" option
+ menu_result = await show_menu(
+ Menu([MenuLeaf(TR.buttons__go_back, _go_back)])
+ )
+ if menu_result is not None:
+ return menu_result.value # BACK
+ else:
+ # not reachable from the UI, only via debuglink
+ raise ActionCancelled
def _slip_39_checklist_items(
@@ -153,8 +177,8 @@ async def _prompt_number(
min_count: int,
max_count: int,
br_name: str,
-) -> int:
- from trezor.ui.layouts.menu import Menu, leaf_from_layout, show_menu
+) -> int | ui.UiResult:
+ from trezor.ui.layouts.menu import Menu, MenuLeaf, leaf_from_layout, show_menu
with trezorui_api.request_number(
title=title,
@@ -174,7 +198,8 @@ async def _prompt_number(
br_name_once = None # ButtonRequest should be sent only once
if result is trezorui_api.CANCELLED:
- raise ActionCancelled # user cancelled request number prompt
+ # not reachable from the UI, only via debuglink
+ raise ActionCancelled
if __debug__ and not isinstance(result, tuple):
# sent by debuglink. debuglink does not change the number of
@@ -187,15 +212,20 @@ async def _prompt_number(
return value
if status is trezorui_api.INFO:
- # shows the menu with the "more info" screen
- leaf = leaf_from_layout(
- TR.buttons__more_info,
- lambda: trezorui_api.show_info_with_cancel(
- title="",
- items=[("", info(value), False)],
+ # shows the menu with "more info" and "go back" options
+ leaves = [
+ leaf_from_layout(
+ TR.buttons__more_info,
+ lambda: trezorui_api.show_info_with_cancel(
+ title="",
+ items=[("", info(value), False)],
+ ),
),
- )
- await show_menu(Menu([leaf]))
+ MenuLeaf(TR.buttons__go_back, _go_back),
+ ]
+ menu_result = await show_menu(Menu(leaves))
+ if menu_result is not None:
+ return menu_result.value # BACK
else:
raise RuntimeError
### tests/device_tests/test_msg_backup_device.py
@@ -190,7 +190,7 @@ def test_backup_slip39_advanced(
assert expected_ms == actual_ms
-@pytest.mark.models("eckhart") # going back is supported only on Eckhart
+@pytest.mark.models("delizia,eckhart") # going back is supported on these layouts
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_BASIC_20_3of6)
def test_backup_slip39_basic_back_navigation(
session: Session, backup_method: messages.BackupMethod
@@ -216,15 +216,13 @@ def test_backup_slip39_basic_back_navigation(
assert expected_ms == actual_ms
-@pytest.mark.models("eckhart") # going back is supported only on Eckhart
+@pytest.mark.models("delizia,eckhart") # going back is supported on these layouts
@pytest.mark.setup_client(needs_backup=True, mnemonic=MNEMONIC_SLIP39_ADVANCED_20)
def test_backup_slip39_advanced_back_navigation(
session: Session, backup_method: messages.BackupMethod
):
with session.test_ctx as client:
- IF = InputFlowSlip39AdvancedBackupBackNavigation(
- session, method=backup_method
- )
+ IF = InputFlowSlip39AdvancedBackupBackNavigation(session, method=backup_method)
client.set_input_flow(IF.get())
device.backup(session, backup_method=backup_method)
### tests/input_flows.py
@@ -2730,6 +2730,91 @@ def __init__(
self.mnemonics: list[str] = []
self.method = method
+ def input_flow_delizia(self) -> BRGeneratorType:
+ assert self.method in (
+ messages.BackupMethod.Display,
+ messages.BackupMethod.N1W1,
+ )
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_intro"
+ self.debug.press_yes()
+
+ # checklist: set number of shares (no going back from the first step)
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ # number of shares prompt: change the default 5 to 3
+ assert (yield).name == "slip39_shares"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 5
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set threshold -- go back via the "Go back" menu item
+ assert (yield).name == "slip39_checklist"
+ self.debug.click(self.debug.screen_buttons.menu())
+ layout = self.debug.read_layout()
+ assert "ScrolledVerticalMenu" in layout.all_components()
+ self.debug.button_actions.navigate_to_menu_item(0)
+
+ # number of shares prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_shares"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set threshold
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ # threshold prompt: increase the default 2 to 3, then go back via the
+ # "Go back" menu item
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.number_input_plus())
+ self.debug.click(self.debug.screen_buttons.menu())
+ layout = self.debug.read_layout()
+ assert "ScrolledVerticalMenu" in layout.all_components()
+ self.debug.button_actions.navigate_to_menu_item(1)
+
+ # checklist: set threshold
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ # threshold prompt: the default 2 is shown again, increase it to 3
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.number_input_plus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: write down the shares -- go back via the "Go back" menu
+ # item
+ assert (yield).name == "slip39_checklist"
+ self.debug.click(self.debug.screen_buttons.menu())
+ layout = self.debug.read_layout()
+ assert "ScrolledVerticalMenu" in layout.all_components()
+ self.debug.button_actions.navigate_to_menu_item(0)
+
+ # threshold prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: write down the shares
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_warning"
+ self.debug.press_yes()
+
+ # Mnemonic phrases
+ self.mnemonics = yield from load_N_shares(self.debug, 3, self.method)
+
def input_flow_eckhart(self) -> BRGeneratorType:
assert self.method in (
messages.BackupMethod.Display,
@@ -2808,6 +2893,117 @@ def __init__(
self.mnemonics: list[list[str]] = []
self.method = method
+ def input_flow_delizia(self) -> BRGeneratorType:
+ assert self.method in (
+ messages.BackupMethod.Display,
+ messages.BackupMethod.N1W1,
+ )
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_intro"
+ self.debug.press_yes()
+
+ # checklist: set number of groups (no going back from the first step)
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ # number of groups prompt: change the default 5 to 2
+ assert (yield).name == "slip39_groups"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 5
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set group threshold
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ # group threshold prompt: keep the default 2
+ assert (yield).name == "slip39_group_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set sizes and thresholds of the groups -- go back via the
+ # "Go back" menu item
+ assert (yield).name == "slip39_checklist"
+ self.debug.click(self.debug.screen_buttons.menu())
+ layout = self.debug.read_layout()
+ assert "ScrolledVerticalMenu" in layout.all_components()
+ self.debug.button_actions.navigate_to_menu_item(0)
+
+ # group threshold prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_group_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # checklist: set sizes and thresholds of the groups
+ assert (yield).name == "slip39_checklist"
+ self.debug.swipe_up()
+
+ # group 1 shares prompt: change the default 5 to 3
+ assert (yield).name == "slip39_shares"
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.number_input_minus())
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 1 threshold prompt: keep the default 2
+ assert (yield).name == "slip39_threshold"
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 shares prompt: keep the default 5 -- go back via the
+ # "Go back" menu item
+ assert (yield).name == "slip39_shares"
+ self.debug.click(self.debug.screen_buttons.menu())
+ layout = self.debug.read_layout()
+ assert "ScrolledVerticalMenu" in layout.all_components()
+ self.debug.button_actions.navigate_to_menu_item(1)
+
+ # group 1 threshold prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 2
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 shares prompt: keep the default 5
+ assert (yield).name == "slip39_shares"
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 threshold prompt: change the default 3 to 4, then go back
+ # via the "Go back" menu item
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.number_input_plus())
+ self.debug.click(self.debug.screen_buttons.menu())
+ layout = self.debug.read_layout()
+ assert "ScrolledVerticalMenu" in layout.all_components()
+ self.debug.button_actions.navigate_to_menu_item(1)
+
+ # group 2 shares prompt: the previously entered value is preselected
+ assert (yield).name == "slip39_shares"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 5
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ # group 2 threshold prompt: the default 3 is shown again -- keep it
+ assert (yield).name == "slip39_threshold"
+ layout = self.debug.read_layout()
+ assert layout.find_unique_value_by_key("value", None, only_type=int) == 3
+ self.debug.click(self.debug.screen_buttons.ok())
+
+ if self.method is messages.BackupMethod.Display:
+ assert (yield).name == "backup_warning"
+ self.debug.press_yes()
+
+ # Mnemonic phrases - show & confirm shares for all groups
+ # 2 groups: 2-of-3 and 3-of-5
+ self.mnemonics = yield from load_N_groups(
+ self.debug, [(2, 3), (3, 5)], self.method
+ )
+
def input_flow_eckhart(self) -> BRGeneratorType:
assert self.method in (
messages.BackupMethod.Display,Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.