mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-15 12:10:42 -04:00
fix(lockdown): UI/pairing fixes for first-pair + content-flash + e-ink (audit)
Closes H13, M19, M20, L4 from the lockdown audit. (L3 dropped per explicit decision — battery level is not a meaningful security side channel.) H13 — BLE pairing PIN was suppressed by the lockdown lock screen on locked devices. Screen.cpp updateUiFrame's lockdown short-circuit intercepts before ui->update() runs, so the pairing-PIN overlay banner that NRF52Bluetooth::onPairingPasskey queued never painted. Net effect: a freshly-locked device on first BLE pair could not be unlocked over BLE because the operator could never see the PIN — chicken and egg. Adds a new notificationTypeEnum::pairing_pin value and special-cases it in the short-circuit: paint the LOCKED frame first (so the underlying background remains the redacted view, never dashboard content) then let ui->update() composite the PIN banner overlay on top. The PIN itself is an ephemeral pair-handshake artifact (regenerated per attempt, dies on banner timeout) and is not operator content, so this does not regress the redaction guarantee. NRF52Bluetooth::onPairingPasskey switches from showSimpleBanner to showOverlayBanner with notificationType = pairing_pin so the short-circuit's lookup matches. M19 — Brief content-visible window on Screen::handleSetOn(true) wake. OLED GDDRAM physically retains the last-rendered frame while the panel is powered off; the next ui->update() after displayOn() is async, so an observer (or shoulder-surfer) could see the previous frame's content for 16-50 ms on every wake. Under MESHTASTIC_LOCKDOWN we now paint the LOCKED frame into GDDRAM in handleSetOn(false) before calling displayOff(). On wake the only thing the panel can flash is the redacted view. Gated on lockdown only — non-lockdown builds keep the previous frame as a UX cue. M20 — E-ink panels physically retain the last-rendered image without power. A power-cycled lockdown handheld kept showing operator-identifying content (position, messages, nodeinfo) until the firmware's first natural refresh — which on e-ink can be seconds into boot. Now, under MESHTASTIC_LOCKDOWN && USE_EINK, the panel init path in Screen::setup() paints the LOCKED frame and forces a full refresh (forceDisplay) immediately after ui->init() and before any other rendering. Persistent pixels are wiped to the redacted view before an observer can see them. Build-tested on seeed_wio_tracker_L1_eink; hardware-verified visual confirmation is pending a T-Echo session. L4 — Screen::blink() bypasses the normal ui->update() path that the lockdown short-circuit gates. It draws arbitrary geometry, not node data, so it does not actually leak today; but any future change that puts content into blink would silently leak past redaction. Added an early-return on shouldRedactDisplay() to make the function honor the redaction contract. Verified with nRF52 lockdown builds on both rak4631 (OLED) and seeed_wio_tracker_L1_eink (e-ink).
This commit is contained in:
@@ -151,7 +151,19 @@ static inline void updateUiFrame(OLEDDisplayUi *ui)
|
||||
{
|
||||
#ifdef MESHTASTIC_LOCKDOWN
|
||||
if (meshtastic_security::shouldRedactDisplay() && screen != nullptr) {
|
||||
// Always paint the LOCKED frame first so the background underneath
|
||||
// any banner overlay is the redacted view, never dashboard content.
|
||||
drawLockdownLockScreen(screen->getDisplayDevice());
|
||||
// Special-case the BLE pairing PIN banner. The PIN is needed to
|
||||
// complete first-pair against a locked device, but the lockdown
|
||||
// short-circuit would otherwise swallow ui->update() and the PIN
|
||||
// would never render — locking the operator out of BLE entirely.
|
||||
// The PIN is an ephemeral pair-handshake artifact (regenerated per
|
||||
// attempt, dies on banner timeout), not operator content. Letting
|
||||
// ui->update() composite the banner over the LOCKED frame is safe.
|
||||
if (NotificationRenderer::current_notification_type == notificationTypeEnum::pairing_pin) {
|
||||
ui->update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -617,6 +629,21 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
setScreensaverFrames(einkScreensaver);
|
||||
#endif
|
||||
|
||||
#ifdef MESHTASTIC_LOCKDOWN
|
||||
// M19: before turning the panel off, paint a safe frame into the
|
||||
// OLED's GDDRAM. The panel retains whatever was last written even
|
||||
// while powered down, so when displayOn() is called later the
|
||||
// screen would otherwise flash the previous frame's content for
|
||||
// 16-50 ms before the next ui->update() lands. Painting the
|
||||
// LOCKED frame now ensures the only thing the operator (or
|
||||
// someone over their shoulder) can see on wake is the redacted
|
||||
// view. Gated on lockdown — non-lockdown builds keep the
|
||||
// previous frame as a UX cue that the display is just dimmed.
|
||||
if (dispdev) {
|
||||
drawLockdownLockScreen(dispdev);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef PIN_EINK_EN
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
#elif defined(PCA_PIN_EINK_EN)
|
||||
@@ -732,6 +759,27 @@ void Screen::setup()
|
||||
#endif
|
||||
LOG_INFO("Applied screen brightness: %d", brightness);
|
||||
|
||||
#if defined(MESHTASTIC_LOCKDOWN) && defined(USE_EINK)
|
||||
// M20: e-ink panels physically retain the last-rendered image without
|
||||
// power, so a power-cycled lockdown handheld would keep showing
|
||||
// operator-identifying content (position, messages, node info) until
|
||||
// the firmware's first natural refresh — which on e-ink can be seconds
|
||||
// into boot. Force a full refresh to the LOCKED frame here, immediately
|
||||
// after the display is initialised and before any other rendering, so
|
||||
// the persistent pixels are wiped to the redacted view before an
|
||||
// observer can see them.
|
||||
if (meshtastic_security::shouldRedactDisplay()) {
|
||||
drawLockdownLockScreen(dispdev);
|
||||
#if defined(USE_EINK_PARALLELDISPLAY)
|
||||
// Parallel-display variants drive refresh through a different path;
|
||||
// a bare drawLockdownLockScreen above lands the frame into the
|
||||
// panel buffer and the next ui->update() commits it as normal.
|
||||
#else
|
||||
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// Set custom overlay callbacks
|
||||
static OverlayCallback overlays[] = {
|
||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||
@@ -1507,6 +1555,15 @@ void Screen::handleStartFirmwareUpdateScreen()
|
||||
|
||||
void Screen::blink()
|
||||
{
|
||||
#ifdef MESHTASTIC_LOCKDOWN
|
||||
// L4: defensive guard. blink() paints arbitrary geometry, not node
|
||||
// data, so it doesn't actually leak today. But it bypasses the normal
|
||||
// ui->update() path that the lockdown short-circuit gates, so any
|
||||
// future change that puts content into blink would silently leak past
|
||||
// redaction. Refuse to draw when the redaction latch is set.
|
||||
if (meshtastic_security::shouldRedactDisplay())
|
||||
return;
|
||||
#endif
|
||||
setFastFramerate();
|
||||
uint8_t count = 10;
|
||||
dispdev->setBrightness(254);
|
||||
|
||||
@@ -12,7 +12,20 @@
|
||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||
namespace graphics
|
||||
{
|
||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
||||
enum notificationTypeEnum {
|
||||
none,
|
||||
text_banner,
|
||||
selection_picker,
|
||||
node_picker,
|
||||
number_picker,
|
||||
text_input,
|
||||
// BLE pairing PIN banner. Treated specially by the lockdown short-circuit
|
||||
// in Screen.cpp: the PIN is ephemeral (regenerated per pair attempt) and
|
||||
// not a real secret, so we allow ui->update() to composite it over the
|
||||
// LOCKED frame. Without this, a first-pair on a locked device cannot
|
||||
// complete because the PIN never renders.
|
||||
pairing_pin,
|
||||
};
|
||||
|
||||
struct BannerOverlayOptions {
|
||||
const char *message;
|
||||
|
||||
@@ -391,7 +391,15 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
|
||||
std::string configuredPasskeyText = std::to_string(configuredPasskey);
|
||||
std::string ble_message =
|
||||
"Bluetooth\nPIN\n[M]" + configuredPasskeyText.substr(0, 3) + " " + configuredPasskeyText.substr(3, 6);
|
||||
screen->showSimpleBanner(ble_message.c_str(), 30000);
|
||||
// Use the pairing_pin notification type so the lockdown UI short-
|
||||
// circuit (Screen.cpp updateUiFrame) allows the overlay through
|
||||
// even on a locked device — see H13 audit fix. The banner content
|
||||
// is the per-attempt ephemeral pair PIN, not operator content.
|
||||
graphics::BannerOverlayOptions opts;
|
||||
opts.message = ble_message.c_str();
|
||||
opts.durationMs = 30000;
|
||||
opts.notificationType = graphics::notificationTypeEnum::pairing_pin;
|
||||
screen->showOverlayBanner(opts);
|
||||
}
|
||||
#endif
|
||||
passkeyShowing = true;
|
||||
|
||||
Reference in New Issue
Block a user