Skip to content

fix(bridge): refuse to start ESPNowBridge with default secret#2529

Open
swaits wants to merge 1 commit into
meshcore-dev:mainfrom
swaits:fix/espnow-reject-default-secret
Open

fix(bridge): refuse to start ESPNowBridge with default secret#2529
swaits wants to merge 1 commit into
meshcore-dev:mainfrom
swaits:fix/espnow-reject-default-secret

Conversation

@swaits
Copy link
Copy Markdown

@swaits swaits commented May 12, 2026

Summary

ESPNowBridge (src/helpers/bridges/ESPNowBridge.cpp) "encrypts" packets between bridges using a repeating-key XOR (xorCrypt()) keyed on _prefs->bridge_secret. The build default for that secret is the literal string "LVSITANOS", hard-coded in examples/simple_repeater/MyMesh.cpp. Until the operator changes it, an attacker who reads the source has the key. This change refuses to start the bridge while the secret is at the published default or empty.

Background

ESP-NOW peer registration in ESPNowBridge::begin sets peerInfo.encrypt = false, so the only link-layer protection is the xorCrypt repeating-key XOR. Even with a custom secret, repeating-key XOR is known-plaintext-trivially broken (every packet has the known fletcher16 + the leading mesh header byte; a few sniffed packets recover the full key). With the default secret it's nothing. The result is that any attacker within ESP-NOW range (~100 m, much wider than typical LoRa-mesh deployments) can inject arbitrary mesh packets via the bridge.

A full fix is to switch to ESP-NOW's built-in CCM (peerInfo.encrypt = true with a 16-byte LMK provisioned out-of-band). That's a wire-format-ish change between bridge peers and requires both sides to upgrade. This PR is the minimal defensive layer: refuse to operate while the secret is the published default.

Change

src/helpers/bridges/ESPNowBridge.cpp — guard begin() with:

if (_prefs->bridge_secret[0] == 0 || strcmp(_prefs->bridge_secret, "LVSITANOS") == 0) {
  BRIDGE_DEBUG_PRINTLN("ESPNowBridge: refusing to start with default/empty bridge_secret\n");
  return;
}

_initialized stays false, so loop() and sendPacket() no-op as before for the "not configured" case.

Why this is the minimal fix

The two improvements available are (1) refuse the default and (2) move to ESP-NOW CCM. (2) requires every bridge peer to upgrade in lockstep — appropriate for a coordinated release, not for a focused PR. (1) is a four-line refusal that closes the worst case immediately.

Risk / compatibility

  • Wire format / ESP-NOW protocol: unchanged.
  • Behaviour: bridges that have been operated with the default secret will stop bridging until the operator runs set bridge.secret <new> via CLI. This is the intent.

References

  • CWE-327 — Use of a Broken or Risky Cryptographic Algorithm.
  • CWE-798 — Use of Hard-coded Credentials.
  • Espressif ESP-NOW reference, esp_now_peer_info_t::encrypt — built-in CCM is the supported way to authenticate-and-encrypt ESP-NOW peer-to-peer traffic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant