From 68d1e25aa013d9d8197587ff00751ddb1ee4d4b4 Mon Sep 17 00:00:00 2001 From: robert s Date: Tue, 18 Aug 2026 14:22:21 -0400 Subject: [PATCH] Fix searchPeersByHash() silently dropping contacts beyond the 8th hash match Identity/dest hashes for ANON_REQ (repeater admin login), REQ, and RESPONSE packets are truncated to a fixed PATH_HASH_SIZE of 1 byte on the wire (Identity::isHashMatch()/copyHashTo(), no-length overload), independent of the runtime-configurable path-hash-size setting used for routing. With only 256 possible values, a busy mesh's contact list routinely has multiple contacts sharing the same first public-key byte. searchPeersByHash() stopped scanning the contact list after collecting MAX_SEARCH_RESULTS (8) same-hash matches. Once a 9th contact sharing that byte is added, it can never be reached by the search regardless of position, so any reply from it (including a repeater admin login/telemetry response) permanently fails to decrypt -- a deterministic, silent, per-contact hard failure rather than an intermittent one, since every returned match is tried against its own real shared secret and a genuine mismatch just fails to authenticate (no security reason to cap the search early). Size MAX_SEARCH_RESULTS from MAX_CONTACTS+MAX_ANON_CONTACTS instead of a fixed 8, so the search can never be capped before it's checked every contact that could possibly match. Cost is trivial (a few hundred bytes at most, scaling with the same MAX_CONTACTS the contact list itself already commits to) -- verified building companion firmware at both default (32) and large (350) MAX_CONTACTS, plus a RAM-constrained nRF52 target. Co-Authored-By: Claude Sonnet 5 --- src/helpers/BaseChatMesh.h | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index d987854709..39a35619fc 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -9,8 +9,6 @@ #include "ContactInfo.h" -#define MAX_SEARCH_RESULTS 8 - #define MSG_SEND_FAILED 0 #define MSG_SEND_SENT_FLOOD 1 #define MSG_SEND_SENT_DIRECT 2 @@ -40,6 +38,18 @@ class ContactsIterator { #define MAX_ANON_CONTACTS 8 +// Must cover every possible contact, not just a small fixed sample: identity +// hashes are truncated to PATH_HASH_SIZE (1 byte) on the wire for ANON_REQ/ +// REQ/RESPONSE matching (see Identity::isHashMatch()/copyHashTo()), so with +// enough contacts, multiple can legitimately share the same hash. A fixed +// cap smaller than the contact list size means searchPeersByHash() can stop +// after collecting that many same-hash matches and never even look at a +// later-added contact sharing that byte -- its replies then fail to decrypt +// permanently, not intermittently, since decryption is tried against every +// returned candidate's real shared secret (a wrong match simply fails to +// authenticate, so there's no correctness reason to cap the search early). +#define MAX_SEARCH_RESULTS (MAX_CONTACTS+MAX_ANON_CONTACTS) + #ifndef MAX_CONNECTIONS #define MAX_CONNECTIONS 16 #endif