security: add MESHTASTIC_LOCKDOWN hardened build option#10349
Open
niccellular wants to merge 1 commit into
Open
security: add MESHTASTIC_LOCKDOWN hardened build option#10349niccellular wants to merge 1 commit into
niccellular wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
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 viaAdminModuleand 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. |
6d6a6c5 to
886d26a
Compare
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.
cc5cd7e to
f35d3f4
Compare
f35d3f4 to
0c6d160
Compare
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.
2a7d419 to
6eb8ab8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Opt-in hardening flag (
MESHTASTIC_LOCKDOWN=1) that raises the securityposture 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 zeroizingwipers; 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-restencryption 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 lockSWD/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.src/security/LockdownDisplay.{h,cpp}exposes asmall policy (
shouldRedactDisplay()+noteUserActivity()). Redactionis true when storage is locked OR no user input for
MESHTASTIC_LOCKDOWN_SCREEN_TIMEOUT_MS(default 30s). When active, thedisplay renders a centered "LOCKED" + battery instead of node list,
messages, GPS, channels, etc.
InputBroker::handleInputEvent()resetsthe timer on any button/keyboard/touch/rotary event.
On non-nRF52 platforms: a
#warningfires; onlyDEBUG_MUTEactivates.MESHTASTIC_LOCKDOWN_DEBUG=1— debug variant that exercises theaccess-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:stateenum:NEEDS_PROVISION/LOCKED/UNLOCKED/UNLOCK_FAILEDlock_reason(machine-readable detail forLOCKED—needs_auth,token_missing,token_expired,token_boots_zero,token_hmac_fail,token_rtc_unavailable, etc.)boots_remaining,valid_until_epoch(forUNLOCKED)backoff_seconds(forUNLOCK_FAILED)PhoneAPI emits
LockdownStatuspost-config (so unauthenticated clientslearn how to authenticate) and after each
lockdown_authcommand.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 viaOLEDDisplayUi) is gated by thedisplay-privacy policy. The other renderers do not yet consult
shouldRedactDisplay()and will continue to render full content underlockdown:
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
new[]/delete[]inEncryptedStorage.cppreplaced withstd::unique_ptrviaZeroizingArrayPtr(Jorropo). Key-materialmemsets replaced withsecure_zero()(volatile-pointer wipe so itisn't elided as dead store on dying stack buffers).
(within-boot millis, wall-clock when
getValidTime()is valid,persisted
bootsSinceFailcounter). Token wall-clock expiry usesgetValidTime()and refuses the token rather than honor a TTL itcan't verify.
APProtect.hdoc spells out that the operation is recoverable only viafull chip erase (which destroys all on-device state including the
encrypted DEK).
its
build_flags.tools/lockdown_provision.pyprovides CLI provisioning/unlock/lock-nowfor testing. Requires a meshtastic Python package built against
protobufs that include the new schema.