feat(core): add notifications on important state changes
What changed, and why it matters
This commit adds new notification messages that the Trezor device sends when certain things happen, such as locking/unlocking, changing settings, changing the PIN, or disconnecting from Bluetooth. It is a feature addition, not a fix for a known security bug. The change itself does not appear to introduce a vulnerability, though it slightly increases the amount of internal state information broadcast to other parts of the system.
No immediate security action is required. Reviewers should confirm that any consumers of these notifications handle them safely and that the 300 ms delay before Bluetooth disconnect does not introduce a race condition or unexpected user experience. If notification data is later forwarded to a host or BLE peer, ensure it does not leak sensitive state.
Security signals we found
New notification events expose device state transitions (lock/unlock, PIN change, settings change, disconnect)
Bluetooth disconnect now preceded by 300 ms sleep to allow notification delivery
Constants added to C module and Python type stubs only; no logic change to security controls
No input validation, buffer handling, or cryptographic code is modified
Evidence from the diff
The patch extends the existing notification/event subsystem in Trezor firmware with five new event types: NOTIFY_DISCONNECT, NOTIFY_SETTING_CHANGE, NOTIFY_SOFTLOCK, NOTIFY_SOFTUNLOCK, and NOTIFY_PIN_CHANGE. It exposes these constants to MicroPython, updates the type stub, and emits the events from lock_manager.py, device_menu.py, apply_settings.py, and change_pin.py. The implementation also adds a 300 ms delay before Bluetooth disconnect so the notification can be observed. No existing security boundary is weakened; the events are informational and the consumers are not shown in this diff.
Changed components
core/embed/sys/notify/inc/sys/notify.hcore/embed/upymod/modtrezorutils/modtrezorutils.ccore/mocks/generated/trezorutils.pyicore/src/apps/common/lock_manager.pycore/src/apps/homescreen/device_menu.pycore/src/apps/management/apply_settings.pycore/src/apps/management/change_pin.pycore/src/trezor/utils.pyInspect captured patch +58 / −4
diff --git a/core/embed/sys/notify/inc/sys/notify.h b/core/embed/sys/notify/inc/sys/notify.h
index aa00b2e9..d146980a 100644
--- a/core/embed/sys/notify/inc/sys/notify.h
+++ b/core/embed/sys/notify/inc/sys/notify.h
@@ -39,9 +39,16 @@
*/
typedef enum {
- NOTIFY_BOOT = 0, /**< Device boot/startup notification */
- NOTIFY_UNLOCK = 1, /**< Device unlocked and ready to accept messages */
- NOTIFY_LOCK = 2 /**< Device hard-locked and won't accept messages */
+ NOTIFY_BOOT = 0, /**< Device boot/startup notification */
+ NOTIFY_UNLOCK = 1, /**< Device unlocked and ready to accept messages */
+ NOTIFY_LOCK = 2, /**< Device hard-locked and won't accept messages */
+ NOTIFY_DISCONNECT = 3, /**< User-initiated disconnect from host */
+ NOTIFY_SETTING_CHANGE = 4, /**< Change of settings */
+ NOTIFY_SOFTLOCK =
+ 5, /**< Device soft-locked (e.g., after clicking power button) */
+ NOTIFY_SOFTUNLOCK =
+ 6, /**< Device soft-unlocked (e.g., after successful pin entry) */
+ NOTIFY_PIN_CHANGE = 7 /**< Pin changed on the device */
// Additional notification types can be added here as needed
} notification_event_t;
diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c
index d3ef902a..5900c097 100644
--- a/core/embed/upymod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c
@@ -685,6 +685,17 @@ STATIC mp_obj_tuple_t mod_trezorutils_version_obj = {
/// """Notification event: device unlocked from hardlock"""
/// NOTIFY_LOCK: int
/// """Notification event: device locked to hardlock"""
+/// NOTIFY_DISCONNECT: int
+/// """Notification event: user-initiated disconnect from host"""
+/// NOTIFY_SETTING_CHANGE: int
+/// """Notification event: change of settings"""
+/// NOTIFY_SOFTLOCK: int
+/// """Notification event: device soft-locked (e.g., after pressing power
+/// button)""" NOTIFY_SOFTUNLOCK: int
+/// """Notification event: device soft-unlocked (e.g., after successful PIN
+/// entry)""" NOTIFY_PIN_CHANGE: int
+/// """Notification event: PIN changed on the device"""
+///
/// if __debug__:
/// DISABLE_ANIMATION: bool
/// """Whether the firmware should disable animations."""
@@ -712,6 +723,12 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_NOTIFY_BOOT), MP_ROM_INT(NOTIFY_BOOT)},
{MP_ROM_QSTR(MP_QSTR_NOTIFY_UNLOCK), MP_ROM_INT(NOTIFY_UNLOCK)},
{MP_ROM_QSTR(MP_QSTR_NOTIFY_LOCK), MP_ROM_INT(NOTIFY_LOCK)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_DISCONNECT), MP_ROM_INT(NOTIFY_DISCONNECT)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_SETTING_CHANGE),
+ MP_ROM_INT(NOTIFY_SETTING_CHANGE)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_SOFTLOCK), MP_ROM_INT(NOTIFY_SOFTLOCK)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_SOFTUNLOCK), MP_ROM_INT(NOTIFY_SOFTUNLOCK)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_PIN_CHANGE), MP_ROM_INT(NOTIFY_PIN_CHANGE)},
#ifdef USE_NRF
{MP_ROM_QSTR(MP_QSTR_nrf_get_version),
MP_ROM_PTR(&mod_trezorutils_nrf_get_version_obj)},
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index 8562c25f..00d7ac84 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -246,6 +246,17 @@ NOTIFY_UNLOCK: int
"""Notification event: device unlocked from hardlock"""
NOTIFY_LOCK: int
"""Notification event: device locked to hardlock"""
+NOTIFY_DISCONNECT: int
+"""Notification event: user-initiated disconnect from host"""
+NOTIFY_SETTING_CHANGE: int
+"""Notification event: change of settings"""
+NOTIFY_SOFTLOCK: int
+"""Notification event: device soft-locked (e.g., after pressing power
+button)""" NOTIFY_SOFTUNLOCK: int
+"""Notification event: device soft-unlocked (e.g., after successful PIN
+entry)""" NOTIFY_PIN_CHANGE: int
+"""Notification event: PIN changed on the device"""
+
if __debug__:
DISABLE_ANIMATION: bool
"""Whether the firmware should disable animations."""
diff --git a/core/src/apps/common/lock_manager.py b/core/src/apps/common/lock_manager.py
index 84271690..c7145528 100644
--- a/core/src/apps/common/lock_manager.py
+++ b/core/src/apps/common/lock_manager.py
@@ -143,6 +143,7 @@ def lock_device(interrupt_workflow: bool = True) -> None:
if interrupt_workflow:
workflow.close_others()
# TODO: should we suspend the device here?
+ utils.notify_send(utils.NOTIFY_SOFTLOCK)
def lock_device_if_unlocked() -> None:
@@ -184,6 +185,7 @@ async def unlock_device() -> None:
_SCREENSAVER_IS_ON = False
set_homescreen()
remove_filter(_pinlock_filter)
+ utils.notify_send(utils.NOTIFY_SOFTUNLOCK)
def _pinlock_filter(msg_type: int, prev_handler: Handler[Msg]) -> Handler[Msg]:
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index c64d582d..2133a7f2 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -1,3 +1,4 @@
+import utime
from micropython import const
from typing import TYPE_CHECKING
@@ -172,6 +173,8 @@ async def handle_device_menu() -> None:
elif menu_result is DeviceMenuResult.DisconnectDevice and ble.is_connected():
init_submenu_idx = SubmenuId.PAIR_AND_CONNECT
try:
+ utils.notify_send(utils.NOTIFY_DISCONNECT)
+ utime.sleep_ms(300)
ble.disconnect()
except ActionCancelled:
pass
@@ -184,6 +187,8 @@ async def handle_device_menu() -> None:
try:
if ble.is_connected():
+ utils.notify_send(utils.NOTIFY_DISCONNECT)
+ utime.sleep_ms(300)
ble.disconnect()
if len(paired_devices) < BLE_MAX_BONDS:
@@ -364,6 +369,7 @@ async def handle_device_menu() -> None:
try:
await set_brightness(SetBrightness())
+ utils.notify_send(utils.NOTIFY_SETTING_CHANGE)
except ActionCancelled:
pass
finally:
@@ -375,6 +381,7 @@ async def handle_device_menu() -> None:
enable = not storage_device.get_haptic_feedback()
io.haptic.haptic_set_enabled(enable)
storage_device.set_haptic_feedback(enable)
+ utils.notify_send(utils.NOTIFY_SETTING_CHANGE)
except ActionCancelled:
pass
finally:
@@ -386,6 +393,7 @@ async def handle_device_menu() -> None:
enable = not storage_device.get_rgb_led()
io.rgb_led.rgb_led_set_enabled(enable)
storage_device.set_rgb_led(enable)
+ utils.notify_send(utils.NOTIFY_SETTING_CHANGE)
except ActionCancelled:
pass
finally:
diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py
index b90ba49a..e22a2721 100644
--- a/core/src/apps/management/apply_settings.py
+++ b/core/src/apps/management/apply_settings.py
@@ -170,6 +170,8 @@ async def apply_settings(msg: ApplySettings) -> Success:
reload_settings_from_storage()
+ utils.notify_send(utils.NOTIFY_SETTING_CHANGE)
+
return Success(message="Settings applied")
diff --git a/core/src/apps/management/change_pin.py b/core/src/apps/management/change_pin.py
index d98d5495..b82a690e 100644
--- a/core/src/apps/management/change_pin.py
+++ b/core/src/apps/management/change_pin.py
@@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
-from trezor import TR, config, wire
+from trezor import TR, config, utils, wire
if TYPE_CHECKING:
from typing import Awaitable
@@ -56,6 +56,8 @@ async def change_pin(msg: ChangePin) -> Success:
else:
await error_pin_invalid()
+ utils.notify_send(utils.NOTIFY_PIN_CHANGE)
+
if newpin:
if curpin:
msg_wire = "PIN changed"
diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py
index 8f790bb8..c953599b 100644
--- a/core/src/trezor/utils.py
+++ b/core/src/trezor/utils.py
@@ -10,7 +10,12 @@ from trezorutils import ( # noqa: F401
MODEL_USB_MANUFACTURER,
MODEL_USB_PRODUCT,
NOTIFY_BOOT,
+ NOTIFY_DISCONNECT,
NOTIFY_LOCK,
+ NOTIFY_PIN_CHANGE,
+ NOTIFY_SETTING_CHANGE,
+ NOTIFY_SOFTLOCK,
+ NOTIFY_SOFTUNLOCK,
NOTIFY_UNLOCK,
SCM_REVISION,
UI_LAYOUT,
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.