installDefaultConfig() restores a preserved identity key when the config
is reset with preserveKey=true. On that path it called:
printBytes("Restored key", config.security.private_key.bytes,
config.security.private_key.size);
printBytes() hex-dumps the buffer to LOG_DEBUG, so this emitted all 32
bytes of the raw X25519 identity private key to the serial/BLE debug log.
Debug logs are not a private channel. They are routinely captured over
serial or BLE and pasted verbatim into GitHub issues, Discord threads and
support requests. Anyone who reads such a log recovers the node's identity
private key, and can then impersonate the node and decrypt every PKI direct
message addressed to it -- past messages included, since the key is
long-lived and the DH shared secret is static per node pair. There is no
revocation story short of generating a new identity.
Replaced with a LOG_DEBUG that records that a key was restored and contains
no key-derived bytes. The restore/no-restore signal is the genuinely useful
diagnostic here ("did my key survive the reset?"), and it costs nothing to
keep; the bytes were never what made the line useful. Log level is unchanged
-- printBytes() already logged at LOG_DEBUG.
Deliberately NOT a truncated prefix or a hash. A prefix is still key
material: it hands an attacker free bytes and shrinks the search space.
A hash is a confirmation oracle -- it lets anyone holding a candidate key
verify it against the log, which is exactly the check an attacker needs.
Neither is a compromise; both leak. If a log line survives at all it must
carry zero key-derived bytes.
Sites changed:
- src/mesh/NodeDB.cpp:988 -- the only full private-key dump in src/.
Audited and deliberately left alone:
- NodeDB.cpp:3552,3604 ("Incoming Pubkey", "Saved Pubkey") -- public keys,
published to the mesh by design; not secret.
- CryptoEngine.cpp:245,285 -- nonces, not key material.
- CryptoEngine.cpp:246,286 -- first 8 bytes of the derived shared_key, and
AdminModule.cpp:2006,2013,2014 -- the 8-byte admin session passkey.
Both are secrets rather than public values, but neither is the identity
private key and both are out of scope for this fix; noted for follow-up.
No unused-variable fallout: private_key_temp is still read by the memcpy
above, and printBytes() is still used by the two pubkey sites, so the
meshUtils.h include is still required.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Clients send `add_contact` before every text-message DM, because a phone
often holds a larger contact database (with public keys) than the radio
can keep. That makes `addFromContact()` the highest-volume key-write path
on the device - and it had no protection against key erasure.
Its only key guard covered the manually-verified case: if the local entry
was marked manually verified and the incoming contact was not, a key
mismatch aborted the update. Every ordinary entry fell straight through to
`CopyUserToNodeInfoLite()`, which assigns `public_key` unconditionally. So
a SharedContact with `has_user` set and an empty `public_key` overwrote a
peer's stored, XEdDSA-proven key with zeros - and `addFromContact()` calls
`saveNodeDatabaseToDisk()`, so the erasure survived a reboot. Subsequent
DMs to that peer then failed with PKI_SEND_FAIL_PUBLIC_KEY, with no way to
recover until the peer's NodeInfo was re-exchanged.
`public_key` is a singular (non-optional) bytes field, so "absent" and
"empty" both decode to size 0; a client that simply has no key for a
contact is indistinguishable on the wire from one asking to clear it.
The fix is deliberately narrow: keep the stored key when the entry already
holds a full 32-byte key and the incoming contact does not. A well-formed
32-byte contact key still updates the entry exactly as before.
Deliberately NOT changed here:
- `updateUser()`'s first-key-wins pin is not applied to this path. Clients
legitimately use add_contact to supply keys the radio never had and to
update them (QR-code contact sharing); a blanket pin would break that
documented flow. Only erasure is refused.
- `CopyUserToNodeInfoLite()` itself is untouched - it has many other
callers (self-record refresh, updateUser, warm-tier rehydration), so the
guard lives at this call site.
- The manually-verified branch is unchanged.
- Node-number validation (reserved/broadcast/self) on this path remains
open and is tracked separately.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* esp32: release BTDM heap when Bluetooth inactive
Release ESP-IDF BTDM memory after config load when Bluetooth is disabled or WiFi is enabled, recovering heap on ESP32 targets where BLE won’t be used for this boot.
* Address BT memory release review comments
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Make the PowerFSM DARK-to-LS timeout immediate when Bluetooth support is compiled out or config.bluetooth.enabled is false. The configured wait_bluetooth_secs default behavior is unchanged when Bluetooth is enabled.
* fix: release Heltec v4 FEM sleep holds
* fix: increase FEM power settle delay
* Fix heltec_V4 documentation
Fixing the heltec_v4 documentation for enabling the PA after sleep.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Add comment for the next guy about why 5ms was chosen.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
* Add ESP32 Power Management lessons learned document
Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).
Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.
* docs(prompts): fix markdown fence language tags
* docs: remove ESP32 power management notes
* Add ESP32 Power Management lessons learned document
Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).
Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.
* docs(prompts): fix markdown fence language tags
* docs: remove ESP32 power management notes
Adds ROUTER_LATE and CLIENT_BASE to preferred rebroadcaster check
(skip unsolicited NodeInfo) and prevents ROUTER_LATE from setting
rebroadcast mode to NONE, which would silently break relaying.
ROUTER_LATE is an infrastructure relay that should use the same power
assumptions, interval defaults, and LoRa wake behavior as ROUTER.
Without this, ROUTER_LATE uses client-class defaults and will not
wake on LoRa activity from deep sleep, making it a broken relay.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
ROUTER_LATE already has high base intervals and should not be further
scaled by congestion. TAK_TRACKER is a tracker variant that should
skip congestion scaling like TRACKER does.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
ROUTER and ROUTER_LATE should not accumulate favorites by sending DMs.
Also replaces magic number 12 with meshtastic_Config_DeviceConfig_Role_CLIENT_BASE.
ROUTER_LATE should be treated as an impolite telemetry role like
ROUTER, responding to multi-hop broadcast requests and using
aggressive send timing without channel utilization checks.
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
ROUTER_LATE now preserves node_info_broadcast_secs during factory
reset and auto-enables Store & Forward server mode, matching ROUTER
infrastructure behavior.
* Add ESP32 Power Management lessons learned document
Documents our experimentation with ESP-IDF DFS and why it doesn't
work well for Meshtastic (RTOS locks, BLE locks, USB issues).
Proposes simpler alternative: manual setCpuFrequencyMhz() control
with explicit triggers for when to go fast vs slow.
* Added a lambda function to clear startup output in the MQTT unit test to ensure a clean state before and after the MQTT subscription process.
This is a non-breaking change that increases the internal representation
of node counts from uint8_t (max 255) to uint16_t (max 65535) to support
larger mesh networks, particularly on ESP32-S3 devices with PSRAM.
Changes:
- NodeStatus: numOnline, numTotal, lastNumTotal (uint8_t -> uint16_t)
- ProtobufModule: numOnlineNodes (uint8_t -> uint16_t)
- MapApplet: loop counters changed to size_t for consistency with getNumMeshNodes()
- NodeStatus: Fixed log format to use %u for unsigned integers
Note: Default class methods keep uint32_t for numOnlineNodes parameter
to match the public API and allow flexibility, even though internal node
counts use uint16_t (max 65535 nodes).
This change does NOT affect protobuf definitions, maintaining wire
compatibility with existing clients and devices.
* Add seenRecently = true if wasUpgraded is true but unable to remove from queue (i.e. already sent/processed).
* Consistent comment between FloodingRouter and HopRouter
* Add seenRecently = true if wasUpgraded is true but unable to remove from queue (i.e. already sent/processed).
* Consistent comment between FloodingRouter and HopRouter