Skip to content

security: add MESHTASTIC_LOCKDOWN hardened build option#10349

Open
niccellular wants to merge 1 commit into
meshtastic:developfrom
niccellular:feature/lockdown-security
Open

security: add MESHTASTIC_LOCKDOWN hardened build option#10349
niccellular wants to merge 1 commit into
meshtastic:developfrom
niccellular:feature/lockdown-security

Conversation

@niccellular
Copy link
Copy Markdown
Member

@niccellular niccellular commented Apr 30, 2026

Summary

Opt-in hardening flag (MESHTASTIC_LOCKDOWN=1) that raises the security
posture of a physical Meshtastic node — encrypted at-rest storage, redacted
config for unauthenticated clients, USB/serial silence, on-screen content
privacy, and SWD lockout — behind a single build flag. nRF52 gets the full
feature set via the CC310 hardware crypto block; other platforms get
DEBUG_MUTE only and a #warning.

Replaces #9771 — same threat model, scoped down to one commit, addresses
prior review feedback (Jorropo: std::unique_ptr + RAII zeroizing
wipers; Copilot: scope/race fixes, backoff bypass, key-material wipes),
generic build flag instead of an nRF52 variant directory.

What it adds

On nRF52 with -DMESHTASTIC_LOCKDOWN=1:

  • MESHTASTIC_PHONEAPI_ACCESS_CONTROL — channel PSKs, security private key,
    admin keys, and wifi_psk redacted from any unauthenticated BLE/USB/TCP
    client. Per-connection auth (atomic flag + global auth-epoch counter for
    O(1) revoke-all on Lock Now).
  • MESHTASTIC_ENCRYPTED_STORAGE — AES-128-CTR + HMAC-SHA256 at-rest
    encryption of LocalConfig, channel file, NodeDB, etc. Passphrase-gated
    DEK with TTL/boot-count unlock token and three-layer failed-attempt
    backoff (within-boot millis, wall-clock when RTC valid, persisted
    bootsSinceFail counter that's bumped each boot — closes the
    reboot-bypass).
  • MESHTASTIC_ENABLE_APPROTECT — one-way UICR APPROTECT write to lock
    SWD/JTAG; the device resets immediately so the lockout takes effect on
    the same boot. Recoverable only via SWD-side nrfjprog --recover,
    which wipes the entire chip including the encrypted DEK.
  • DEBUG_MUTE — silence USB/serial logs to prevent passive info leakage.
  • Display privacysrc/security/LockdownDisplay.{h,cpp} exposes a
    small policy (shouldRedactDisplay() + noteUserActivity()). Redaction
    is true when storage is locked OR no user input for
    MESHTASTIC_LOCKDOWN_SCREEN_TIMEOUT_MS (default 30s). When active, the
    display renders a centered "LOCKED" + battery instead of node list,
    messages, GPS, channels, etc. InputBroker::handleInputEvent() resets
    the timer on any button/keyboard/touch/rotary event.

On non-nRF52 platforms: a #warning fires; only DEBUG_MUTE activates.

MESHTASTIC_LOCKDOWN_DEBUG=1 — debug variant that exercises the
access-control + encrypted-storage code paths without the irreversible
bits (no APPROTECT, no DEBUG_MUTE). For development and bring-up only.

Wire format

Uses the proper proto schema added in
meshtastic/protobufs#911
(merged):

Client → device: AdminMessage.lockdown_auth (tag 104), carrying:

  • passphrase (1–32 bytes; capped at proto level via .options)
  • boots_remaining (optional override; 0 = firmware default)
  • valid_until_epoch (optional wall-clock TTL; 0 = no time limit)
  • lock_now (bool sentinel for Lock Now)

Device → client: FromRadio.lockdown_status (tag 18), carrying:

  • state enum: NEEDS_PROVISION / LOCKED / UNLOCKED / UNLOCK_FAILED
  • lock_reason (machine-readable detail for LOCKEDneeds_auth,
    token_missing, token_expired, token_boots_zero, token_hmac_fail,
    token_rtc_unavailable, etc.)
  • boots_remaining, valid_until_epoch (for UNLOCKED)
  • backoff_seconds (for UNLOCK_FAILED)

PhoneAPI emits LockdownStatus post-config (so unauthenticated clients
learn how to authenticate) and after each lockdown_auth command.

A matching client-side implementation is in
Meshtastic-Android features/lockdown-v2.
That branch will need a follow-up update to consume the new schema.

Known gaps — display privacy

Only graphics/Screen.cpp (OLED via OLEDDisplayUi) is gated by the
display-privacy policy. The other renderers do not yet consult
shouldRedactDisplay() and will continue to render full content under
lockdown:

  • graphics/InkHUD/ (e-ink rich UI)
  • graphics/niche/ (TFT niche graphics)
  • meshtastic/device-ui (T-Deck/TFT, separate submodule)

Operators running lockdown on hardware using those UIs should treat the
screen as a plaintext leak surface until follow-up work wires those
renderers in.

Notes for review

  • All raw new[]/delete[] in EncryptedStorage.cpp replaced with
    std::unique_ptr via ZeroizingArrayPtr (Jorropo). Key-material
    memsets replaced with secure_zero() (volatile-pointer wipe so it
    isn't elided as dead store on dying stack buffers).
  • Passphrase-attempt backoff is RTC-tampering-resistant: three layers
    (within-boot millis, wall-clock when getValidTime() is valid,
    persisted bootsSinceFail counter). Token wall-clock expiry uses
    getValidTime() and refuses the token rather than honor a TTL it
    can't verify.
  • APProtect.h doc spells out that the operation is recoverable only via
    full chip erase (which destroys all on-device state including the
    encrypted DEK).
  • No new variant directory; any env can opt in by appending the flag to
    its build_flags.
  • tools/lockdown_provision.py provides CLI provisioning/unlock/lock-now
    for testing. Requires a meshtastic Python package built against
    protobufs that include the new schema.

@github-actions github-actions Bot added the enhancement New feature or request label Apr 30, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in hardened firmware build mode (MESHTASTIC_LOCKDOWN=1) intended to improve physical-device security by gating sensitive local APIs, encrypting configuration at rest (nRF52 only), muting debug output, and optionally enabling nRF52 APPROTECT.

Changes:

  • Introduces nRF52-only encrypted-at-rest config storage (EncryptedStorage) plus passphrase provision/unlock/lock-now control via AdminModule and a companion provisioning CLI script.
  • Adds PhoneAPI access-control/redaction behavior for unauthenticated local clients (and new authorization plumbing).
  • Adds optional nRF52 SWD/JTAG lockout support via UICR APPROTECT, wired into setup() under the lockdown build flag.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tools/lockdown_provision.py CLI utility to provision/unlock/lock a lockdown device via the Python Meshtastic library.
src/security/SecureZero.h Adds secure zeroization helpers + zeroizing RAII buffers for key material.
src/security/EncryptedStorage.h Declares encrypted storage API, formats, and lockdown boot/provision/unlock flows.
src/security/EncryptedStorage.cpp Implements nRF52 CC310-backed AES-CTR + HMAC encrypted file storage, unlock tokens, and backoff.
src/security/APProtect.h Documents and declares the APPROTECT enablement helper.
src/security/APProtect.cpp Implements APPROTECT UICR write logic (nRF52).
src/modules/AdminModule.cpp Adds lockdown passphrase transport and “Lock Now” handling in set_config(security); ties auth into PhoneAPI + NodeDB reload.
src/mesh/PhoneAPI.h Adds access-control flags/APIs and atomic auth state for lockdown builds.
src/mesh/PhoneAPI.cpp Enforces ToRadio gating and redacts secrets for unauthenticated local clients; emits LOCKDOWN_* notifications.
src/mesh/NodeDB.h Adds reloadFromDisk() for post-unlock reloading.
src/mesh/NodeDB.cpp Hooks encrypted read/write into load/save proto paths and handles “locked boot” defaulting + migration.
src/main.cpp Calls enableAPProtect() and EncryptedStorage::initLocked() during setup under feature flags.
src/configuration.h Defines the MESHTASTIC_LOCKDOWN / MESHTASTIC_LOCKDOWN_DEBUG build-flag behavior and platform gating.
src/PowerFSM.cpp Avoids disabling BLE in serialEnter() on nRF52.

Comment thread src/mesh/PhoneAPI.cpp Outdated
Comment thread src/modules/AdminModule.cpp Outdated
Comment thread src/security/APProtect.cpp Outdated
Comment thread src/security/EncryptedStorage.h Outdated
Comment thread src/security/EncryptedStorage.cpp Outdated
Comment thread src/modules/AdminModule.cpp Outdated
Comment thread src/mesh/NodeDB.cpp Outdated
Comment thread src/mesh/NodeDB.cpp Outdated
@niccellular niccellular force-pushed the feature/lockdown-security branch 2 times, most recently from 6d6a6c5 to 886d26a Compare April 30, 2026 15:32
niccellular added a commit to niccellular/protobufs that referenced this pull request May 7, 2026
…ilds

Companion proto changes for meshtastic/firmware#10349 (MESHTASTIC_LOCKDOWN
hardened build option). Replaces a previous "no schema change" hack that
repurposed SecurityConfig.private_key as the passphrase byte transport.

AdminMessage.lockdown_auth (= 104):
  LockdownAuth carries a passphrase plus optional boots/until-epoch
  overrides, and a lock_now sentinel. Used for first-time provisioning,
  unlock on subsequent reboots, re-verification on already-unlocked
  devices, and Lock Now. Firmware decides between provision and unlock
  based on its own state.

FromRadio.lockdown_status (= 18):
  LockdownStatus reports lockdown state to the client (NEEDS_PROVISION /
  LOCKED / UNLOCKED / UNLOCK_FAILED) plus structured fields for lock
  reason, token TTL, and unlock-failure backoff. Sent post-config and in
  response to each LockdownAuth command. Replaces the earlier scheme of
  encoding state as magic-string prefixes inside ClientNotification.

Both messages are documented inline. No existing fields are altered.
@niccellular niccellular force-pushed the feature/lockdown-security branch 2 times, most recently from cc5cd7e to f35d3f4 Compare May 11, 2026 21:43
@thebentern thebentern requested a review from Copilot May 11, 2026 22:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Comment thread src/security/EncryptedStorage.cpp Outdated
Comment thread src/security/EncryptedStorage.cpp Outdated
Comment thread src/security/LockdownDisplay.h Outdated
@niccellular niccellular force-pushed the feature/lockdown-security branch from f35d3f4 to 0c6d160 Compare May 12, 2026 00:15
Opt-in hardening via -DMESHTASTIC_LOCKDOWN=1, off by default.

On nRF52 (CC310 hardware crypto), the flag derives:
  DEBUG_MUTE                         — silence USB/serial log output
  MESHTASTIC_PHONEAPI_ACCESS_CONTROL — redact channel PSKs, private key,
                                       admin keys, and wifi_psk from any
                                       BLE/USB/TCP client that has not
                                       authenticated; per-connection auth
                                       (atomic flag + global auth-epoch
                                       counter for O(1) revoke-all)
  MESHTASTIC_ENCRYPTED_STORAGE       — AES-128-CTR + HMAC-SHA256 at-rest
                                       encryption of LocalConfig, channel
                                       file, NodeDB, etc., backed by CC310
                                       hardware crypto. Passphrase-gated
                                       DEK with TTL/boot-count unlock
                                       token and failed-attempt backoff.
                                       Locked-boot early return; plaintext
                                       -> MENC migration on first encrypted
                                       boot.
  MESHTASTIC_ENABLE_APPROTECT        — one-way UICR APPROTECT write to
                                       lock SWD/JTAG; resets immediately
                                       so the lockout takes effect on the
                                       same boot. Recoverable only via
                                       SWD-side nrfjprog --recover, which
                                       wipes the entire chip including
                                       the encrypted DEK.

On non-nRF52 platforms a #warning fires and only DEBUG_MUTE is active.
Access control is intentionally NOT enabled there because the passphrase
delivery path in AdminModule is gated on MESHTASTIC_ENCRYPTED_STORAGE;
turning on access control alone would lock all non-PKC clients out of
admin permanently.

Add -DMESHTASTIC_LOCKDOWN_DEBUG=1 alongside MESHTASTIC_LOCKDOWN to build
a debuggable variant that exercises the access-control and encrypted-
storage code paths without the irreversible bits (no APPROTECT, no
DEBUG_MUTE). For development and hardware bring-up only.

AdminModule gains passphrase delivery via set_config(security)
(repurposing private_key as the passphrase byte transport, no schema
change), provision/unlock/re-verify flow, Lock Now sentinel (private_key
== 0xFF), and PKC admin auth callback. PhoneAPI sends LOCKDOWN_LOCKED /
LOCKDOWN_UNLOCKED / LOCKDOWN_UNLOCK_FAILED / LOCKDOWN_NEEDS_PROVISION
client notifications post-config so clients know to prompt.

Display privacy (screen leak mitigation):
  src/security/LockdownDisplay.{h,cpp} — small policy module exposing
  shouldRedactDisplay() and noteUserActivity(). Redaction is true when
  encrypted storage is locked OR no input has been received for
  MESHTASTIC_LOCKDOWN_SCREEN_TIMEOUT_MS (default 30s).

  graphics/Screen.cpp (OLED via OLEDDisplayUi) consults the policy in
  updateUiFrame() and renders a centered "LOCKED" + battery instead of
  the normal node list / messages / GPS / channel content when active.

  InputBroker::handleInputEvent() calls noteUserActivity() on every
  input event — physical button, keyboard, touch, rotary, ExpressLRS,
  etc. — through the single existing input funnel.

  KNOWN GAPS: InkHUD, niche/TFT, and device-ui renderers do not yet
  consult shouldRedactDisplay() and will continue to render full content
  under lockdown. Operators running lockdown on devices that use those
  UIs should treat the screen as a plaintext leak surface until follow-up
  work wires those renderers in.

Key material handling uses a small SecureZero.h helper:
  secure_zero()        — compiler-barrier wipe (volatile fn pointer) so
                         the wipe is not optimized away on dying buffers.
  ZeroizingBuffer<N>   — fixed-size RAII wiper for stack key material.
  ZeroizingArrayPtr    — std::unique_ptr<uint8_t[]> with a deleter that
                         wipes before delete[]; replaces all raw new[] /
                         delete[] in EncryptedStorage.cpp.

No new variant directory: any env can opt in by appending
-DMESHTASTIC_LOCKDOWN=1 to its build_flags.

tools/lockdown_provision.py is included for CLI provisioning, unlock,
lock-now, and notification monitoring over USB.
@niccellular niccellular force-pushed the feature/lockdown-security branch from 2a7d419 to 6eb8ab8 Compare May 12, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants