chore(core/eckhart): add device menu notification entries
What changed, and why it matters
This commit adds new notification entries and warning styles to the device menu on the Trezor hardware wallet. It is a routine user-interface feature patch: it introduces a 'PIN not set' reminder, changes the visual style of a failed-backup warning, and wires the failed-backup menu item to a device-wipe confirmation prompt. There is no evidence of a security vulnerability being fixed.
No security action required; treat as normal UI/UX feature commit. Review the new wipe-device confirmation flow during QA to ensure it cannot be triggered without explicit user confirmation.
Security signals we found
UI-only feature addition
Defensive guard conditions added to menu actions
Failed-backup flow now triggers a wipe confirmation prompt
No memory-safety, cryptographic, or authorization fixes visible
Evidence from the diff
The change extends the Eckhart layout device menu screen with new stylesheet constants (light warning, error) and adds a ‘PIN not set’ notification when pin_code == Some(false). In the Python homescreen handler it refactors initialization checks into local variables and, when the user selects the backup-failed entry, shows a danger-styled warning and calls wipe_device() if confirmed. It also adds guard conditions (is_initialized, has_pin, etc.) to several menu-result handlers. These are defensive hardening additions inside a feature commit, not a patch for a disclosed vulnerability.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/src/apps/homescreen/device_menu.pycore/translations/en.jsoncore/translations/order.jsoncore/translations/signatures.jsoncore/mocks/trezortranslate_keys.pyicore/embed/rust/librust_qstr.hcore/embed/rust/src/translations/generated/translated_string.rsInspect captured patch +86 / −47
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 1e6ba4e3..80625658 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -882,6 +882,7 @@ static void _librust_qstrs(void) {
MP_QSTR_warning;
MP_QSTR_warning_footer;
MP_QSTR_wipe__info;
+ MP_QSTR_wipe__start_again;
MP_QSTR_wipe__title;
MP_QSTR_wipe__want_to_wipe;
MP_QSTR_wipe_code;
@@ -963,6 +964,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__review;
MP_QSTR_words__security;
MP_QSTR_words__send;
+ MP_QSTR_words__set;
MP_QSTR_words__settings;
MP_QSTR_words__sign;
MP_QSTR_words__signer;
@@ -985,6 +987,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__unlocked;
MP_QSTR_words__wallet;
MP_QSTR_words__warning;
+ MP_QSTR_words__wipe;
MP_QSTR_words__writable;
MP_QSTR_words__yes;
MP_QSTR_write;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index 9d991c52..510a8e02 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1506,6 +1506,9 @@ pub enum TranslatedString {
ble__disable = 1115, // "Turn Bluetooth off?"
ble__enable = 1116, // "Turn Bluetooth on?"
words__bluetooth = 1117, // "Bluetooth"
+ wipe__start_again = 1118, // "Wipe your Trezor and start the setup process again."
+ words__set = 1119, // "Set"
+ words__wipe = 1120, // "Wipe"
}
impl TranslatedString {
@@ -3331,6 +3334,9 @@ impl TranslatedString {
(Self::ble__disable, "Turn Bluetooth off?"),
(Self::ble__enable, "Turn Bluetooth on?"),
(Self::words__bluetooth, "Bluetooth"),
+ (Self::wipe__start_again, "Wipe your Trezor and start the setup process again."),
+ (Self::words__set, "Set"),
+ (Self::words__wipe, "Wipe"),
];
#[cfg(feature = "micropython")]
@@ -4727,6 +4733,7 @@ impl TranslatedString {
#[cfg(feature = "universal_fw")]
(Qstr::MP_QSTR_u2f__title_set, Self::u2f__title_set),
(Qstr::MP_QSTR_wipe__info, Self::wipe__info),
+ (Qstr::MP_QSTR_wipe__start_again, Self::wipe__start_again),
(Qstr::MP_QSTR_wipe__title, Self::wipe__title),
(Qstr::MP_QSTR_wipe__want_to_wipe, Self::wipe__want_to_wipe),
(Qstr::MP_QSTR_wipe_code__change, Self::wipe_code__change),
@@ -4806,6 +4813,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_words__review, Self::words__review),
(Qstr::MP_QSTR_words__security, Self::words__security),
(Qstr::MP_QSTR_words__send, Self::words__send),
+ (Qstr::MP_QSTR_words__set, Self::words__set),
(Qstr::MP_QSTR_words__settings, Self::words__settings),
(Qstr::MP_QSTR_words__sign, Self::words__sign),
(Qstr::MP_QSTR_words__signer, Self::words__signer),
@@ -4828,6 +4836,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_words__unlocked, Self::words__unlocked),
(Qstr::MP_QSTR_words__wallet, Self::words__wallet),
(Qstr::MP_QSTR_words__warning, Self::words__warning),
+ (Qstr::MP_QSTR_words__wipe, Self::words__wipe),
(Qstr::MP_QSTR_words__writable, Self::words__writable),
(Qstr::MP_QSTR_words__yes, Self::words__yes),
];
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 cb22dc5d..3e37638b 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
@@ -100,7 +100,9 @@ struct MenuItem {
action: Option<Action>,
}
const MENU_ITEM_TITLE_STYLE_SHEET: &ButtonStyleSheet = &theme::menu_item_title();
+const MENU_ITEM_LIGHT_WARNING: &ButtonStyleSheet = &theme::menu_item_title_yellow();
const MENU_ITEM_WARNING: &ButtonStyleSheet = &theme::menu_item_title_orange();
+const MENU_ITEM_ERROR: &ButtonStyleSheet = &theme::menu_item_title_red();
impl MenuItem {
pub fn new(text: TString<'static>, action: Option<Action>) -> Self {
@@ -250,9 +252,14 @@ impl DeviceMenuScreen {
let devices = screen.add_paired_devices_menu(paired_devices, paired_device_indices);
let pair_and_connect = screen.add_pair_and_connect_menu(devices, connected_subtext);
-
- let root =
- screen.add_root_menu(failed_backup, pair_and_connect, settings, connected_subtext);
+ let pin_unset = pin_code == Some(false);
+ let root = screen.add_root_menu(
+ failed_backup,
+ pin_unset,
+ pair_and_connect,
+ settings,
+ connected_subtext,
+ );
screen.set_active_subscreen(root);
@@ -523,6 +530,7 @@ impl DeviceMenuScreen {
fn add_root_menu(
&mut self,
failed_backup: bool,
+ pin_unset: bool,
pair_and_connect_index: usize,
settings_index: usize,
connected_subtext: Option<TString<'static>>,
@@ -534,9 +542,18 @@ impl DeviceMenuScreen {
Some(Action::Return(DeviceMenuMsg::BackupFailed)),
);
item_backup_failed.with_subtext(Some((TR::words__review.into(), None)));
- item_backup_failed.with_stylesheet(MENU_ITEM_TITLE_STYLE_SHEET);
+ item_backup_failed.with_stylesheet(MENU_ITEM_ERROR);
unwrap!(items.push(item_backup_failed));
}
+ if pin_unset {
+ let mut item_pin_unset = MenuItem::new(
+ TR::homescreen__title_pin_not_set.into(),
+ Some(Action::Return(DeviceMenuMsg::PinCode)),
+ );
+ item_pin_unset.with_subtext(Some((TR::words__set.into(), None)));
+ item_pin_unset.with_stylesheet(MENU_ITEM_LIGHT_WARNING);
+ unwrap!(items.push(item_pin_unset));
+ }
let mut item_pair_and_connect = MenuItem::new(
TR::ble__pair_title.into(),
Some(Action::GoTo(pair_and_connect_index)),
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index d75fbb3c..6c383d0d 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -976,6 +976,7 @@ class TR:
u2f__title_get: str = "Get U2F counter"
u2f__title_set: str = "Set U2F counter"
wipe__info: str = "All data will be erased."
+ wipe__start_again: str = "Wipe your Trezor and start the setup process again."
wipe__title: str = "Wipe device"
wipe__want_to_wipe: str = "Do you really want to wipe the device?\n"
wipe_code__change: str = "Change wipe code"
@@ -1055,6 +1056,7 @@ class TR:
words__review: str = "Review"
words__security: str = "Security"
words__send: str = "Send"
+ words__set: str = "Set"
words__settings: str = "Settings"
words__sign: str = "Sign"
words__signer: str = "Signer"
@@ -1077,6 +1079,7 @@ class TR:
words__unlocked: str = "Unlocked"
words__wallet: str = "Wallet"
words__warning: str = "Warning"
+ words__wipe: str = "Wipe"
words__writable: str = "Writable"
words__yes: str = "Yes"
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index 441b7b0d..3f7d4900 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -9,10 +9,10 @@ from trezorui_api import DeviceMenuResult
async def handle_device_menu() -> None:
from trezor import strings
- # TODO: unify with notification handling in `apps/homescreen/__init__.py:homescreen()`
- failed_backup = (
- storage_device.is_initialized() and storage_device.unfinished_backup()
- )
+ is_initialized = storage_device.is_initialized()
+ 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()
# MOCK DATA
paired_devices = ["Trezor Suite"] if ble.is_connected() else []
bluetooth_version = "2.3.1.1"
@@ -38,30 +38,18 @@ async def handle_device_menu() -> None:
paired_devices=paired_devices,
connected_idx=None,
bluetooth=True, # TODO implement bluetooth handling
- pin_code=config.has_pin() if storage_device.is_initialized() else None,
+ pin_code=config.has_pin() if is_initialized else None,
auto_lock_delay=auto_lock_delay,
- wipe_code=(
- config.has_wipe_code() if storage_device.is_initialized() else None
- ),
- check_backup=storage_device.is_initialized(),
+ wipe_code=config.has_wipe_code() if is_initialized else None,
+ check_backup=is_initialized,
device_name=(
- (storage_device.get_label() or "Trezor")
- if storage_device.is_initialized()
- else None
- ),
- screen_brightness=(
- TR.brightness__title if storage_device.is_initialized() else None
+ (storage_device.get_label() or "Trezor") if is_initialized else None
),
+ screen_brightness=TR.brightness__title if is_initialized else None,
haptic_feedback=(
- storage_device.get_haptic_feedback()
- if (storage_device.is_initialized() and utils.USE_HAPTIC)
- else None
- ),
- led_enabled=(
- storage_device.get_rgb_led()
- if (storage_device.is_initialized() and utils.USE_RGB_LED)
- else None
+ storage_device.get_haptic_feedback() if haptic_configurable else None
),
+ led_enabled=(storage_device.get_rgb_led() if led_configurable else None),
about_items=[
(TR.homescreen__firmware_version, firmware_version, False),
(TR.homescreen__firmware_type, firmware_type, False),
@@ -71,8 +59,23 @@ async def handle_device_menu() -> None:
"device_menu",
)
# Root menu
- if menu_result is DeviceMenuResult.BackupFailed:
- pass # TODO implement backup failed handling
+ if menu_result is DeviceMenuResult.BackupFailed and failed_backup:
+ from trezor.messages import WipeDevice
+ from trezor.ui.layouts import raise_if_cancelled
+
+ from apps.management.wipe_device import wipe_device
+
+ await raise_if_cancelled(
+ trezorui_api.show_warning(
+ title=TR.homescreen__title_backup_failed,
+ button=TR.words__wipe,
+ description=TR.wipe__start_again,
+ danger=True,
+ ),
+ "prompt_device_wipe",
+ )
+
+ await wipe_device(WipeDevice())
# Pair & Connect
elif menu_result is DeviceMenuResult.DeviceDisconnect:
pass # TODO implement device disconnect handling
@@ -96,7 +99,6 @@ async def handle_device_menu() -> None:
from trezor.ui.layouts import confirm_action
turned_on = ble.is_connected()
-
await confirm_action(
"ble__settings",
TR.words__bluetooth,
@@ -104,24 +106,23 @@ async def handle_device_menu() -> None:
)
pass # TODO implement bluetooth handling
# Security settings
- elif menu_result is DeviceMenuResult.PinCode:
+ elif menu_result is DeviceMenuResult.PinCode and is_initialized:
from trezor.messages import ChangePin
from apps.management.change_pin import change_pin
await change_pin(ChangePin())
- elif menu_result is DeviceMenuResult.PinRemove:
+ elif menu_result is DeviceMenuResult.PinRemove and config.has_pin():
from trezor.messages import ChangePin
from apps.management.change_pin import change_pin
await change_pin(ChangePin(remove=True))
- elif menu_result is DeviceMenuResult.AutoLockDelay:
+ elif menu_result is DeviceMenuResult.AutoLockDelay and config.has_pin():
from trezor.messages import ApplySettings
from apps.management.apply_settings import apply_settings
- assert config.has_pin()
auto_lock_delay_ms = await interact(
trezorui_api.request_duration(
title=TR.auto_lock__title,
@@ -132,25 +133,26 @@ async def handle_device_menu() -> None:
),
br_name=None,
)
+ # Necessary for the style check not to raise type error
assert isinstance(auto_lock_delay_ms, int)
await apply_settings(
ApplySettings(
auto_lock_delay_ms=auto_lock_delay_ms,
)
)
- elif menu_result is DeviceMenuResult.WipeCode:
+ elif menu_result is DeviceMenuResult.WipeCode and is_initialized:
from trezor.messages import ChangeWipeCode
from apps.management.change_wipe_code import change_wipe_code
await change_wipe_code(ChangeWipeCode())
- elif menu_result is DeviceMenuResult.WipeRemove:
+ elif menu_result is DeviceMenuResult.WipeRemove and config.has_wipe_code():
from trezor.messages import ChangeWipeCode
from apps.management.change_wipe_code import change_wipe_code
await change_wipe_code(ChangeWipeCode(remove=True))
- elif menu_result is DeviceMenuResult.CheckBackup:
+ elif menu_result is DeviceMenuResult.CheckBackup and is_initialized:
from trezor.enums import RecoveryType
from trezor.messages import RecoveryDevice
@@ -162,12 +164,11 @@ async def handle_device_menu() -> None:
)
)
# Device settings
- elif menu_result is DeviceMenuResult.DeviceName:
+ elif menu_result is DeviceMenuResult.DeviceName and is_initialized:
from trezor.messages import ApplySettings
from apps.management.apply_settings import apply_settings
- assert storage_device.is_initialized()
label = await interact(
trezorui_api.request_string(
prompt=TR.device_name__enter,
@@ -177,26 +178,26 @@ async def handle_device_menu() -> None:
),
"device_name",
)
+ # Necessary for the style check not to raise type error
assert isinstance(label, str)
await apply_settings(ApplySettings(label=label))
- elif menu_result is DeviceMenuResult.ScreenBrightness:
+ elif menu_result is DeviceMenuResult.ScreenBrightness and is_initialized:
from trezor.messages import SetBrightness
from apps.management.set_brightness import set_brightness
await set_brightness(SetBrightness())
- elif menu_result is DeviceMenuResult.HapticFeedback:
+ elif menu_result is DeviceMenuResult.HapticFeedback and haptic_configurable:
from trezor.messages import ApplySettings
from apps.management.apply_settings import apply_settings
- assert storage_device.is_initialized()
await apply_settings(
ApplySettings(
haptic_feedback=not storage_device.get_haptic_feedback(),
)
)
- elif menu_result is DeviceMenuResult.LedEnabled:
+ elif menu_result is DeviceMenuResult.LedEnabled and led_configurable:
from trezor import io
from trezor.ui.layouts import confirm_action
diff --git a/core/translations/en.json b/core/translations/en.json
index 7ef0efc9..5ac0c8dd 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -1193,6 +1193,7 @@
"u2f__title_get": "Get U2F counter",
"u2f__title_set": "Set U2F counter",
"wipe__info": "All data will be erased.",
+ "wipe__start_again": "Wipe your Trezor and start the setup process again.",
"wipe__title": "Wipe device",
"wipe__want_to_wipe": "Do you really want to wipe the device?\n",
"wipe_code__change": "Change wipe code",
@@ -1272,6 +1273,7 @@
"words__review": "Review",
"words__security": "Security",
"words__send": "Send",
+ "words__set": "Set",
"words__settings": "Settings",
"words__sign": "Sign",
"words__signer": "Signer",
@@ -1299,6 +1301,7 @@
"words__unlocked": "Unlocked",
"words__wallet": "Wallet",
"words__warning": "Warning",
+ "words__wipe": "Wipe",
"words__writable": "Writable",
"words__yes": "Yes"
}
diff --git a/core/translations/order.json b/core/translations/order.json
index c6b7413d..9c12fa64 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1116,5 +1116,8 @@
"1114": "words__enabled",
"1115": "ble__disable",
"1116": "ble__enable",
- "1117": "words__bluetooth"
+ "1117": "words__bluetooth",
+ "1118": "wipe__start_again",
+ "1119": "words__set",
+ "1120": "words__wipe"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index 367597d4..ea1cb684 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "1f03e0ef6491c8ade57998fa0dd1af61f2c00c75296c3597a55ff6314a419f14",
- "datetime": "2025-08-26T12:39:46.580646+00:00",
- "commit": "29495149aa499ebb08c4c6b707e38998ca4818b2"
+ "merkle_root": "cafc9d93400eedb808aa2359441c3075d9896571a461bdec68e2c5d3765e084f",
+ "datetime": "2025-08-26T12:40:10.870291+00:00",
+ "commit": "91505b9c07e2cdeeb5f471946095a28bed59ef90"
},
"history": [
{
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.