feat(core/eckhart): backup needed menu item
What changed, and why it matters
This commit adds a new 'Backup Device' menu item to the Trezor Safe 5 (Eckhart layout) device menu when the device still needs a backup. It is a user-facing feature change, not a security fix or vulnerability. The change wires a new UI button through to the existing backup_device() flow, with no changes to security-critical logic, authentication, or access control.
No security action required. Treat as a normal feature commit. If reviewing for release readiness, verify that the new menu item correctly routes to the existing backup flow and that translations for `homescreen__title_backup_needed` are present.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends the device menu API across all UI layouts (bolt, caesar, delizia, eckhart) to accept a new needs_backup boolean, adds a BackupDevice result variant, and implements the menu item only for the Eckhart layout. When selected, apps/homescreen/device_menu.py invokes the existing apps.management.backup_device.backup_device() coroutine with a BackupDevice message. The other layouts receive the parameter but ignore it (stubbed). No privilege checks, storage handling, or cryptographic code is modified.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/src/apps/homescreen/device_menu.pycore/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout/device_menu_result.rscore/embed/rust/src/ui/ui_firmware.rscore/embed/rust/librust_qstr.hcore/mocks/generated/trezorui_api.pyiInspect captured patch +44 / −1
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index fb7d5ab6..0b922bc2 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -23,6 +23,7 @@ static void _librust_qstrs(void) {
MP_QSTR_BACK;
MP_QSTR_BLEIF;
MP_QSTR_BacklightLevels;
+ MP_QSTR_BackupDevice;
MP_QSTR_BackupFailed;
MP_QSTR_CANCELLED;
MP_QSTR_CONFIRMED;
@@ -465,6 +466,7 @@ static void _librust_qstrs(void) {
MP_QSTR_modify_fee__transaction_fee;
MP_QSTR_more_info_callback;
MP_QSTR_multiple_pages_texts;
+ MP_QSTR_needs_backup;
MP_QSTR_notification;
MP_QSTR_notification_level;
MP_QSTR_page_count;
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 49226909..7dbeca0e 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -939,6 +939,7 @@ extern "C" fn new_show_device_menu(n_args: usize, args: *const Obj, kwargs: *mut
let block = move |_args: &[Obj], kwargs: &Map| {
let init_submenu: Option<u8> = kwargs.get(Qstr::MP_QSTR_init_submenu)?.try_into_option()?;
let failed_backup: bool = kwargs.get(Qstr::MP_QSTR_failed_backup)?.try_into()?;
+ let needs_backup: bool = kwargs.get(Qstr::MP_QSTR_needs_backup)?.try_into()?;
let paired_devices: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?;
let paired_devices: Vec<TString, MAX_PAIRED_DEVICES> = util::iter_into_vec(paired_devices)?;
let connected_idx: Option<u8> =
@@ -964,6 +965,7 @@ extern "C" fn new_show_device_menu(n_args: usize, args: *const Obj, kwargs: *mut
let layout = ModelUI::show_device_menu(
init_submenu,
failed_backup,
+ needs_backup,
paired_devices,
connected_idx,
pin_code,
@@ -1919,6 +1921,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// *,
/// init_submenu: int | None,
/// failed_backup: bool,
+ /// needs_backup: bool,
/// paired_devices: Iterable[str],
/// connected_idx: int | None,
/// pin_code: bool | None,
@@ -2136,6 +2139,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// class DeviceMenuResult:
/// """Result of a device menu operation."""
/// BackupFailed: ClassVar[DeviceMenuResult]
+ /// BackupDevice: ClassVar[DeviceMenuResult]
/// DeviceDisconnect: ClassVar[DeviceMenuResult]
/// DevicePair: ClassVar[DeviceMenuResult]
/// DeviceUnpair: ClassVar[DeviceMenuResult]
diff --git a/core/embed/rust/src/ui/layout/device_menu_result.rs b/core/embed/rust/src/ui/layout/device_menu_result.rs
index 667df1b0..9f50d9a6 100644
--- a/core/embed/rust/src/ui/layout/device_menu_result.rs
+++ b/core/embed/rust/src/ui/layout/device_menu_result.rs
@@ -9,6 +9,7 @@ static DEVICE_MENU_RESULT_BASE_TYPE: Type = obj_type! { name: Qstr::MP_QSTR_Devi
// Root menu
pub static BACKUP_FAILED: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
+pub static BACKUP_DEVICE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
// "Pair & Connect"
pub static DEVICE_PAIR: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
pub static DEVICE_DISCONNECT: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
@@ -40,6 +41,7 @@ static DEVICE_MENU_RESULT_TYPE: Type = obj_type! {
name: Qstr::MP_QSTR_DeviceMenuResult,
locals: &obj_dict! { obj_map! {
Qstr::MP_QSTR_BackupFailed => BACKUP_FAILED.as_obj(),
+ Qstr::MP_QSTR_BackupDevice => BACKUP_DEVICE.as_obj(),
Qstr::MP_QSTR_DevicePair => DEVICE_PAIR.as_obj(),
Qstr::MP_QSTR_DeviceDisconnect => DEVICE_DISCONNECT.as_obj(),
Qstr::MP_QSTR_DeviceUnpair => DEVICE_UNPAIR.as_obj(),
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index e23305a8..5748b2af 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -937,6 +937,7 @@ impl FirmwareUI for UIBolt {
fn show_device_menu(
_init_submenu: Option<u8>,
_failed_backup: bool,
+ _needs_backup: bool,
_paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
_connected_idx: Option<u8>,
_pin_code: Option<bool>,
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index 7ef29516..3f4cc226 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1134,6 +1134,7 @@ impl FirmwareUI for UICaesar {
fn show_device_menu(
_init_submenu: Option<u8>,
_failed_backup: bool,
+ _needs_backup: bool,
_paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
_connected_idx: Option<u8>,
_pin_code: Option<bool>,
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index 82b23078..6f16d812 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -1019,6 +1019,7 @@ impl FirmwareUI for UIDelizia {
fn show_device_menu(
_init_submenu: Option<u8>,
_failed_backup: bool,
+ _needs_backup: bool,
_paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
_connected_idx: Option<u8>,
_pin_code: Option<bool>,
diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
index 7c2ec97a..5ed6be11 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
@@ -159,6 +159,7 @@ impl ComponentMsgObj for DeviceMenuScreen {
match msg {
// Root menu
DeviceMenuMsg::BackupFailed => Ok(BACKUP_FAILED.as_obj()),
+ DeviceMenuMsg::BackupDevice => Ok(BACKUP_DEVICE.as_obj()),
// "Pair & Connect"
DeviceMenuMsg::DevicePair => Ok(DEVICE_PAIR.as_obj()),
DeviceMenuMsg::DeviceDisconnect => Ok(DEVICE_DISCONNECT.as_obj()),
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
index 60147950..ad44de2f 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
@@ -108,6 +108,7 @@ enum Action {
pub enum DeviceMenuMsg {
// Root menu
BackupFailed,
+ BackupDevice,
// "Pair & Connect"
DevicePair, // pair a new device
@@ -306,6 +307,7 @@ impl DeviceMenuScreen {
pub fn new(
init_submenu: Option<u8>,
failed_backup: bool,
+ needs_backup: bool,
paired_devices: Vec<TString<'static>, MAX_PAIRED_DEVICES>,
connected_idx: Option<u8>,
pin_code: Option<bool>,
@@ -348,7 +350,7 @@ impl DeviceMenuScreen {
screen.register_pair_and_connect_menu(paired_devices, submenu_indices, connected_idx);
let pin_unset = pin_code == Some(false);
- screen.register_root_menu(failed_backup, pin_unset, connected_subtext);
+ screen.register_root_menu(failed_backup, needs_backup, pin_unset, connected_subtext);
// Activate the init submenu
let init_submenu_id = init_submenu
@@ -627,6 +629,7 @@ impl DeviceMenuScreen {
fn register_root_menu(
&mut self,
failed_backup: bool,
+ needs_backup: bool,
pin_unset: bool,
connected_subtext: Option<TString<'static>>,
) {
@@ -642,6 +645,16 @@ impl DeviceMenuScreen {
items.add(item);
}
+ if needs_backup {
+ let item = MenuItem::return_msg(
+ TR::homescreen__title_backup_needed.into(),
+ DeviceMenuMsg::BackupDevice,
+ )
+ .with_subtext(Some((TR::words__review.into(), None)))
+ .light_warn();
+ items.add(item);
+ }
+
if pin_unset {
let item = MenuItem::return_msg(
TR::homescreen__title_pin_not_set.into(),
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index 28b037dc..83349340 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -1203,6 +1203,7 @@ impl FirmwareUI for UIEckhart {
fn show_device_menu(
init_submenu: Option<u8>,
failed_backup: bool,
+ needs_backup: bool,
paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
connected_idx: Option<u8>,
pin_code: Option<bool>,
@@ -1218,6 +1219,7 @@ impl FirmwareUI for UIEckhart {
let layout = RootComponent::new(DeviceMenuScreen::new(
init_submenu,
failed_backup,
+ needs_backup,
paired_devices,
connected_idx,
pin_code,
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 11a34e65..9a1360e5 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -364,6 +364,7 @@ pub trait FirmwareUI {
fn show_device_menu(
init_submenu: Option<u8>,
failed_backup: bool,
+ needs_backup: bool,
paired_devices: heapless::Vec<TString<'static>, MAX_PAIRED_DEVICES>,
connected_idx: Option<u8>,
pin_code: Option<bool>,
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 1c39dd60..3015700a 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -627,6 +627,7 @@ def show_device_menu(
*,
init_submenu: int | None,
failed_backup: bool,
+ needs_backup: bool,
paired_devices: Iterable[str],
connected_idx: int | None,
pin_code: bool | None,
@@ -865,6 +866,7 @@ class LayoutState:
class DeviceMenuResult:
"""Result of a device menu operation."""
BackupFailed: ClassVar[DeviceMenuResult]
+ BackupDevice: ClassVar[DeviceMenuResult]
DeviceDisconnect: ClassVar[DeviceMenuResult]
DevicePair: ClassVar[DeviceMenuResult]
DeviceUnpair: ClassVar[DeviceMenuResult]
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index 65add15e..d4288d35 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -82,6 +82,7 @@ async def handle_device_menu() -> None:
led_configurable = is_initialized and utils.USE_RGB_LED
haptic_configurable = is_initialized and utils.USE_HAPTIC
failed_backup = is_initialized and storage_device.unfinished_backup()
+ needs_backup = is_initialized and storage_device.needs_backup()
bonds = ble.get_bonds()
if __debug__:
@@ -112,6 +113,7 @@ async def handle_device_menu() -> None:
trezorui_api.show_device_menu(
init_submenu=init_submenu,
failed_backup=failed_backup,
+ needs_backup=needs_backup,
paired_devices=paired_devices,
connected_idx=connected_idx,
pin_code=config.has_pin() if is_initialized else None,
@@ -166,6 +168,17 @@ async def handle_device_menu() -> None:
init_submenu = SubmenuId.ROOT
else:
break
+ elif menu_result is DeviceMenuResult.BackupDevice and needs_backup:
+ from trezor.messages import BackupDevice
+
+ from apps.management.backup_device import backup_device
+
+ try:
+ await backup_device(BackupDevice())
+ except ActionCancelled:
+ init_submenu = SubmenuId.ROOT
+ else:
+ break
# Pair & Connect
elif menu_result is DeviceMenuResult.DeviceDisconnect and ble.is_connected():
init_submenu = SubmenuId.PAIR_AND_CONNECT
Why this scored 15/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.