Tracks upstream meshcore-dev/MeshCore#2556 — passwords sent as
plaintext over encrypted links are vulnerable to evil-twin/phishing
attacks (attacker advertises a repeater with the same name but a
different pubkey; user picks the wrong one and types the password).
The structural fix is a protocol change (PAKE adoption — SPAKE2,
OPAQUE, or HMAC-with-both-pubkeys) and must land synchronously
across all implementations. Diverging unilaterally would break
interop with Arduino-based companions and repeaters, so we wait
for upstream.
Two within-protocol mitigations applied server-side:
1. Constant-time password comparison. Replaced strcmp() in
RepeaterMesh::handleLoginReq with a local ct_memeq() helper.
Pads the received password to the full 16-byte storage size,
XOR-accumulates byte differences with no early exit. Compares
both admin and guest passwords unconditionally so timing is
identical regardless of which (if any) the attempt resembled.
Eliminates the timing oracle that lets an already-MITM
attacker recover the stored password byte-by-byte.
2. Failed-login rate limit. New login_fail_limiter(4, 180)
RateLimiter — 4 wrong-password attempts per 180s, matching the
existing anon_limiter pattern. Hitting the cap trips a distinct
LOG_WRN so operators see active brute-force attempts in logs.
Global rate (not per-sender) — simpler, no ACL state bloat;
trade-off documented in CRYPTO_AUDIT_INDEX.md.
What's NOT fixed: the wire protocol still carries plaintext
passwords. The evil-twin attack itself remains possible; these
mitigations raise the attacker's cost (no timing leak, no
brute-force at line rate) but don't replace the structural fix.
UI-side defenses (TOFU warnings on duplicate names, pubkey
fingerprint display) are valuable companion-side mitigations
but out of scope for this audit's server-side commit.
Both mesh::Packet::writePath and ::copyPath did a raw memcpy of the
decoded hash_count*hash_size bytes from src to dest with no bound on
src. Two call sites used phone-supplied or LoRa-anon-supplied buffers
where the path_len byte was attacker-controlled:
- CompanionMesh CMD_SEND_CHANNEL_DATA accepted len>=4 and called
writePath with no src bound; a paired phone could leak up to ~65
bytes of syswq stack into the outgoing LoRa channel-data frame.
- RepeaterMesh handleAnonRegionsReq / handleAnonOwnerReq /
handleAnonClockReq read reply_path_len from an unauthenticated
LoRa anon-request payload and called copyPath without any src
bound. Any LoRa neighbor could leak repeater stack into the
reply path.
Hardened the API: both functions now require an explicit src_len
and reject (return 0) when the decoded byte count exceeds it.
Updated all 14 call sites across Packet/Mesh/Dispatcher/BaseChatMesh/
CompanionMesh/RepeaterMesh. Trusted callers (internal MAX_PATH_SIZE
buffers) pass MAX_PATH_SIZE; untrusted callers pass real remaining
length. Added len-5 plumbing through the anon-handler signatures.
CMD_SEND_CHANNEL_DATA also gained a local len>=5 + path_bytes
sanity check for early rejection.
stolen from Zephyr main:
1. Issue StopTimerOnPreamble=1 before SetRxDutyCycle so the chip's
timer is not reset on every preamble detect (per §13.1 of the
datasheet). Without this, duty cycle effectively never sleeps in
noisy RF and current draw spikes.
2. On IRQ_RX_TX_TIMEOUT during duty-cycle RX, re-arm via
sx126x_restart_rx() instead of falling through to set_sleep().
The old path silently killed duty cycle after the first preamble
false-positive.
3. On recv_duty_cycle(NULL) cancel, wake the radio before issuing
SetStandby — BUSY stays asserted during the sleep phase and the
standby command was being dropped.
Also adds a dc_timeout_restarts atomic counter incremented on the Fix 2
path, exposed end-to-end: sx126x_ext.h accessors → LoRaRadioBase vtable
→ SX126xRadio override → CommonCLICallbacks → RepeaterMesh. Query via
`get dc.restarts` on the repeater CLI; cleared by `clear stats`. High
values indicate a noisy environment or a too-loose preamble threshold.
(+increase ESP BT stack because future zephyr pin advance will trip that mine)
GPS fixes no longer overwrite prefs lat/lon — adverts use only
manually-set coordinates (via gps setloc or companion app)
Precise GPS position served via telemetry requests only
getNodeLat()/getNodeLon() returns live GPS fix
Last good fix persisted to /lfs/gps_pos — survives reboots
| Command | Description |
|---------|-------------|
| `gps` | Show GPS status (`on` or `off`) |
| `gps on` | Enable GPS module |
| `gps off` | Disable GPS module |
| `gps setloc` | Update stored latitude/longitude from current GPS fix |
| `gps advert` | Show current location advertising policy |
| `gps advert none` | Do not include location in advertisements |
| `gps advert share` | Include live GPS location in advertisements |
| `gps advert prefs` | Include stored lat/lon from prefs in advertisements |