feat(core/eckhart): waiting for host FwUI function
What changed, and why it matters
This commit adds a new on-screen step to the Bluetooth pairing flow on Trezor hardware wallets. After the user confirms a pairing code, the device now shows a 'Waiting for host...' screen and waits for the host computer or phone to finish the pairing. It is a normal user-interface feature, not a fix for a known security bug.
No immediate security action required. Treat as a normal feature commit. Reviewers may want to verify that cancelling or disconnecting during the new wait screen correctly calls `reject_pairing()` and does not leave the BLE stack in a half-paired state.
Security signals we found
Bluetooth pairing flow change
New UI wait state during security-critical pairing sequence
Removal of broad exception catch around pairing code screen
Evidence from the diff
The change introduces a third UI screen in the BLE pairing sequence: wait_ble_host_confirmation. It adds a new BLE handler mode WaitingForPairingCompletion, a new BLEEvent::PairingCompleted handling path returning CONFIRMED, and wires the flow in pair_new_device.py so that after the user allows pairing, the device waits for the host-side confirmation before exiting. Non-Eckhart layouts return NotImplementedError for the new API. Translation and mock files are updated accordingly.
Changed components
core/src/apps/management/ble/pair_new_device.pycore/embed/rust/src/ui/component/ble.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/ui_firmware.rsInspect captured patch +129 / −34
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 1dc1da53..9e808a0f 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -202,6 +202,7 @@ static void _librust_qstrs(void) {
MP_QSTR_ble__unpair_current;
MP_QSTR_ble__unpair_title;
MP_QSTR_ble__version;
+ MP_QSTR_ble__waiting_for_host;
MP_QSTR_ble_event;
MP_QSTR_bootscreen;
MP_QSTR_br_code;
@@ -927,6 +928,7 @@ static void _librust_qstrs(void) {
MP_QSTR_verb_info;
MP_QSTR_verify;
MP_QSTR_version;
+ MP_QSTR_wait_ble_host_confirmation;
MP_QSTR_warning;
MP_QSTR_warning_footer;
MP_QSTR_wipe__info;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index f0c0d928..539985e9 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1550,6 +1550,7 @@ pub enum TranslatedString {
homescreen__backup_needed_info = 1160, // "Open Trezor Suite and create a wallet backup. This is the only way to recover access to your assets."
ble__host_info = 1161, // "Host info"
ble__mac_address = 1162, // "MAC address"
+ ble__waiting_for_host = 1163, // "Waiting for host..."
}
impl TranslatedString {
@@ -3534,6 +3535,7 @@ impl TranslatedString {
(Self::homescreen__backup_needed_info, "Open Trezor Suite and create a wallet backup. This is the only way to recover access to your assets."),
(Self::ble__host_info, "Host info"),
(Self::ble__mac_address, "MAC address"),
+ (Self::ble__waiting_for_host, "Waiting for host..."),
];
#[cfg(feature = "micropython")]
@@ -3634,6 +3636,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_ble__unpair_current, Self::ble__unpair_current),
(Qstr::MP_QSTR_ble__unpair_title, Self::ble__unpair_title),
(Qstr::MP_QSTR_ble__version, Self::ble__version),
+ (Qstr::MP_QSTR_ble__waiting_for_host, Self::ble__waiting_for_host),
(Qstr::MP_QSTR_brightness__change_title, Self::brightness__change_title),
(Qstr::MP_QSTR_brightness__changed_title, Self::brightness__changed_title),
(Qstr::MP_QSTR_brightness__title, Self::brightness__title),
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 06ff8d61..e3b59103 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -1019,6 +1019,27 @@ extern "C" fn new_show_pairing_device_name(
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
+// Prefix parameters with `_` to avoid unused variable warning when building
+// without "ble".
+extern "C" fn new_wait_ble_host_confirmation(
+ _n_args: usize,
+ _args: *const Obj,
+ _kwargs: *mut Map,
+) -> Obj {
+ #[cfg(feature = "ble")]
+ {
+ let block = move |_args: &[Obj], _kwargs: &Map| {
+ let layout = ModelUI::wait_ble_host_confirmation()?;
+ let layout_obj = LayoutObj::new_root(layout)?;
+ Ok(layout_obj.into())
+ };
+ unsafe { util::try_with_args_and_kwargs(_n_args, _args, _kwargs, block) }
+ }
+
+ #[cfg(not(feature = "ble"))]
+ unimplemented!()
+}
+
// Prefix parameters with `_` to avoid unused variable warning when building
// without "ble".
extern "C" fn new_show_ble_pairing_code(
@@ -1973,6 +1994,13 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// Returns on BLEEvent::{PairingCanceled, Disconnected}."""
Qstr::MP_QSTR_show_ble_pairing_code => obj_fn_kw!(0, new_show_ble_pairing_code).as_obj(),
+ /// def wait_ble_host_confirmation(
+ /// *,
+ /// ) -> LayoutObj[UiResult]:
+ /// """Pairing device: third screen (waiting for host confirmation).
+ /// Returns on BLEEvent::{PairingCanceled, Disconnected}."""
+ Qstr::MP_QSTR_wait_ble_host_confirmation => obj_fn_kw!(0, new_wait_ble_host_confirmation).as_obj(),
+
/// def confirm_thp_pairing(
/// *,
/// title: str,
diff --git a/core/embed/rust/src/ui/component/ble.rs b/core/embed/rust/src/ui/component/ble.rs
index 1462fd82..ade703cc 100644
--- a/core/embed/rust/src/ui/component/ble.rs
+++ b/core/embed/rust/src/ui/component/ble.rs
@@ -5,23 +5,31 @@ use crate::ui::{
shape::Renderer,
};
+pub enum BLEHandlerMode {
+ /// Advertising without whitelist started, waiting for some host to respond
+ WaitingForPairingRequest,
+ /// Pairing in progress, Trezor needs to allow/reject
+ WaitingForPairingCancel,
+ /// Pairing in progress, allowed from Trezor side, waiting for BLE module to
+ /// confirm success
+ WaitingForPairingCompletion,
+}
+
pub struct BLEHandler<T> {
inner: T,
- waiting_for_pairing: bool,
+ state: BLEHandlerMode,
}
pub enum BLEHandlerMsg<TMsg> {
Content(TMsg),
PairingCode(u32),
+ PairingCompleted,
Cancelled,
}
impl<T> BLEHandler<T> {
- pub fn new(inner: T, waiting_for_pairing: bool) -> Self {
- Self {
- inner,
- waiting_for_pairing,
- }
+ pub fn new(inner: T, state: BLEHandlerMode) -> Self {
+ Self { inner, state }
}
}
@@ -36,10 +44,15 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
- match (event, self.waiting_for_pairing) {
- (Event::BLE(BLEEvent::PairingRequest(num)), true) => {
- return Some(BLEHandlerMsg::PairingCode(num))
- }
+ match (event, &self.state) {
+ (
+ Event::BLE(BLEEvent::PairingRequest(num)),
+ BLEHandlerMode::WaitingForPairingRequest,
+ ) => return Some(BLEHandlerMsg::PairingCode(num)),
+ (
+ Event::BLE(BLEEvent::PairingCompleted),
+ BLEHandlerMode::WaitingForPairingCompletion,
+ ) => return Some(BLEHandlerMsg::PairingCompleted),
(Event::BLE(BLEEvent::PairingCanceled | BLEEvent::Disconnected), _) => {
return Some(BLEHandlerMsg::Cancelled)
}
@@ -69,7 +82,10 @@ mod micropython {
use crate::{
error::Error,
micropython::obj::Obj,
- ui::layout::{obj::ComponentMsgObj, result::CANCELLED},
+ ui::layout::{
+ obj::ComponentMsgObj,
+ result::{CANCELLED, CONFIRMED},
+ },
};
impl<T> ComponentMsgObj for BLEHandler<T>
where
@@ -80,6 +96,7 @@ mod micropython {
BLEHandlerMsg::Content(msg) => self.inner.msg_try_into_obj(msg),
BLEHandlerMsg::PairingCode(num) => num.try_into(),
BLEHandlerMsg::Cancelled => Ok(CANCELLED.as_obj()),
+ BLEHandlerMsg::PairingCompleted => Ok(CONFIRMED.as_obj()),
}
}
}
diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs
index 99137310..a27ba692 100644
--- a/core/embed/rust/src/ui/component/mod.rs
+++ b/core/embed/rust/src/ui/component/mod.rs
@@ -35,7 +35,7 @@ pub mod timeout;
pub use bar::Bar;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, FlowMsg, Never, Timer};
#[cfg(feature = "ble")]
-pub use ble::{BLEHandler, BLEHandlerMsg};
+pub use ble::{BLEHandler, BLEHandlerMode, BLEHandlerMsg};
pub use border::Border;
pub use button_request::{ButtonRequestExt, SendButtonRequest};
#[cfg(all(
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 4fe84413..768ca034 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -968,6 +968,11 @@ impl FirmwareUI for UIBolt {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
}
+ #[cfg(feature = "ble")]
+ fn wait_ble_host_confirmation() -> Result<impl LayoutMaybeTrace, Error> {
+ Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
+ }
+
fn show_thp_pairing_code(
title: TString<'static>,
description: TString<'static>,
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 5fbe7ae2..400382eb 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1165,6 +1165,11 @@ impl FirmwareUI for UICaesar {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
}
+ #[cfg(feature = "ble")]
+ fn wait_ble_host_confirmation() -> Result<impl LayoutMaybeTrace, Error> {
+ Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
+ }
+
fn show_thp_pairing_code(
title: TString<'static>,
description: TString<'static>,
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 da53a9e5..169554d9 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -1050,6 +1050,11 @@ impl FirmwareUI for UIDelizia {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
}
+ #[cfg(feature = "ble")]
+ fn wait_ble_host_confirmation() -> Result<impl LayoutMaybeTrace, Error> {
+ Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
+ }
+
fn show_thp_pairing_code(
title: TString<'static>,
description: TString<'static>,
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 bbf2b0c9..07addaaa 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -34,6 +34,9 @@ use crate::{
util::interpolate,
};
+#[cfg(feature = "ble")]
+use crate::ui::component::{BLEHandler, BLEHandlerMode};
+
use super::{
component::Button,
firmware::{
@@ -1259,7 +1262,7 @@ impl FirmwareUI for UIEckhart {
.with_header(Header::new(TR::thp__pair_new_device.into()).with_close_button())
.with_action_bar(ActionBar::new_text_only(TR::thp__continue_on_host.into()));
#[cfg(feature = "ble")]
- let screen = crate::ui::component::BLEHandler::new(screen, true);
+ let screen = BLEHandler::new(screen, BLEHandlerMode::WaitingForPairingRequest);
let layout = RootComponent::new(screen);
Ok(layout)
}
@@ -1277,11 +1280,26 @@ impl FirmwareUI for UIEckhart {
.add_newline()
.add_alignment(Alignment::Center)
.add_text_with_font(code, fonts::FONT_SATOSHI_EXTRALIGHT_72);
- let screen = crate::ui::component::BLEHandler::new(
+ let screen = BLEHandler::new(
TextScreen::new(FormattedText::new(ops))
.with_header(Header::new(title))
.with_action_bar(ActionBar::new_cancel_confirm()),
- false,
+ BLEHandlerMode::WaitingForPairingCancel,
+ );
+ let layout = RootComponent::new(screen);
+ Ok(layout)
+ }
+
+ #[cfg(feature = "ble")]
+ fn wait_ble_host_confirmation() -> Result<impl LayoutMaybeTrace, Error> {
+ let screen = BLEHandler::new(
+ TextScreen::new(
+ Paragraph::new(&theme::TEXT_REGULAR, TR::ble__waiting_for_host)
+ .into_paragraphs()
+ .with_placement(LinearPlacement::vertical()),
+ )
+ .with_header(Header::new(TR::ble__pairing_title.into()).with_close_button()),
+ BLEHandlerMode::WaitingForPairingCompletion,
);
let layout = RootComponent::new(screen);
Ok(layout)
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 9517cbbc..d27b5d2e 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -389,6 +389,9 @@ pub trait FirmwareUI {
code: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error>;
+ #[cfg(feature = "ble")]
+ fn wait_ble_host_confirmation() -> Result<impl LayoutMaybeTrace, Error>;
+
fn show_thp_pairing_code(
title: TString<'static>,
description: TString<'static>,
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 53a67da8..3278769b 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -663,6 +663,14 @@ def show_ble_pairing_code(
Returns on BLEEvent::{PairingCanceled, Disconnected}."""
+# rust/src/ui/api/firmware_micropython.rs
+def wait_ble_host_confirmation(
+ *,
+) -> LayoutObj[UiResult]:
+ """Pairing device: third screen (waiting for host confirmation).
+ Returns on BLEEvent::{PairingCanceled, Disconnected}."""
+
+
# rust/src/ui/api/firmware_micropython.rs
def confirm_thp_pairing(
*,
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index 25716489..654dd0fa 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -98,6 +98,7 @@ class TR:
ble__unpair_current: str = "Unpair connected device"
ble__unpair_title: str = "Unpair"
ble__version: str = "Bluetooth version"
+ ble__waiting_for_host: str = "Waiting for host..."
brightness__change_title: str = "Change display brightness"
brightness__changed_title: str = "Display brightness changed"
brightness__title: str = "Display brightness"
diff --git a/core/src/apps/management/ble/pair_new_device.py b/core/src/apps/management/ble/pair_new_device.py
index 20780ad3..9914db52 100644
--- a/core/src/apps/management/ble/pair_new_device.py
+++ b/core/src/apps/management/ble/pair_new_device.py
@@ -41,21 +41,19 @@ async def pair_new_device() -> None:
if not isinstance(code, int):
raise ActionCancelled
- try:
- result = await interact(
- trezorui_api.show_ble_pairing_code(
- title=TR.ble__pairing_title,
- description=TR.ble__pairing_match,
- code=f"{code:0>6}",
- ),
- None,
- )
- except Exception:
- ble.reject_pairing()
- raise
- else:
- if result is CONFIRMED:
- ble.allow_pairing(code)
+ result = await interact(
+ trezorui_api.show_ble_pairing_code(
+ title=TR.ble__pairing_title,
+ description=TR.ble__pairing_match,
+ code=f"{code:0>6}",
+ ),
+ None,
+ )
+ if result is CONFIRMED:
+ ble.allow_pairing(code)
+
+ # wait for the host code confirmation
+ await interact(trezorui_api.wait_ble_host_confirmation(), None)
finally:
if result is not CONFIRMED:
ble.reject_pairing()
diff --git a/core/translations/en.json b/core/translations/en.json
index 56b7a34e..bb87db84 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -130,6 +130,7 @@
"ble__unpair_current": "Unpair connected device",
"ble__unpair_title": "Unpair",
"ble__version": "Bluetooth version",
+ "ble__waiting_for_host": "Waiting for host...",
"brightness__change_title": "Change display brightness",
"brightness__changed_title": "Display brightness changed",
"brightness__title": "Display brightness",
diff --git a/core/translations/order.json b/core/translations/order.json
index 237b4a5a..4bce2985 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1161,5 +1161,6 @@
"1159": "wipe_code__cancel_setup",
"1160": "homescreen__backup_needed_info",
"1161": "ble__host_info",
- "1162": "ble__mac_address"
+ "1162": "ble__mac_address",
+ "1163": "ble__waiting_for_host"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index 5611899c..18f8c25d 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "87b46b530535655085857ba56dfb88814f1403079a22693c534b6ffa4493fa84",
- "datetime": "2025-09-25T16:59:32.599036+00:00",
- "commit": "cc0e94cb5c0f9704efd366d9be7d26c98fa3e940"
+ "merkle_root": "d83452bcbd293dadbe58e63101a596fe54b03a8df002746241cc4c206201de7c",
+ "datetime": "2025-09-26T07:32:48.356972+00:00",
+ "commit": "671847de53ee2bd2c27119bf378e8ef07197f24c"
},
"history": [
{
Why this scored 20/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.