mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-08 22:29:03 -04:00
* Improve performance of encrypted packets with a shared secret cache Every PKI encrypt, decrypt and ack-proof ran Curve25519::dh2 plus a SHA256 to derive the pairwise key, so a node in a conversation paid a full X25519 per packet, on the main loop, under cryptLock. On a RAK4631 that is ~96 ms of the ~210 ms it takes to handle a DM. setCryptoSharedSecret() derives the key only when it is not already held for that peer, keeping the last MAX_CACHED_SHARED_SECRETS derivations (8 on nRF52, 2 on STM32WL, 10 elsewhere, under 400 bytes) and evicting the least recently used. encryptCurve25519, decryptCurve25519 and ackProofCompute all go through it, so the ack proof gets the same cache without a second DH path. The cache is emptied whenever our own private key changes, since every secret in it is then stale. The lookup key is the first 4 bytes of the peer's public key. A collision makes us derive against the wrong cached secret, which costs a failed decrypt for that pair; it cannot disclose either secret. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JLHxcWJuvSoSSMpz3LWdt3 * Do not let an empty cache slot answer for a zero-prefixed peer key The cache used lookup_key == 0 to mean "slot unused", so a peer key whose first 4 bytes are zero matched every unused slot and was handed that slot's zeroed secret as a hit. The all-zero key is exactly such a key, which is how test_proof_rejects_weak_peer_key caught it: dh2's weak-point check never ran. Entries carry an explicit valid flag instead, which the struct's existing padding absorbs. A weak peer key is also now rejected before the cache is consulted, since with 4 bytes of lookup key it could otherwise collide with a cached peer and be served that peer's secret rather than being refused. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JLHxcWJuvSoSSMpz3LWdt3 * Key the shared secret cache by the whole peer public key A four-byte lookup key is grindable: anyone can generate a keypair whose public key shares those bytes with a peer they want to shadow, get their own entry cached, and then be handed the secret this node uses to talk to that peer - readable by them, since they hold the matching private key. That is disclosure of traffic meant for the peer, and a forgeable ack proof in its name, not the failed exchange a chance collision would cause. Entries hold the peer key itself and are matched on all 32 bytes, so the weak-key check dh2 does on a miss can no longer be skipped either and the explicit isWeakPoint guard goes away with it. The cache costs 66 bytes per entry: 528 on nRF52, 660 where the default 10 entries apply. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JLHxcWJuvSoSSMpz3LWdt3 * Allowlist the cache's uptime shift for the millis deadline guard The guard's regex reads `millis() >> 22` as a comparison against the uptime clock. It is a right shift, coarsening uptime into the ~1.165 hour units the cache stamps entries with, and the eviction arithmetic handles that stamp's 8-bit wrap itself. Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JLHxcWJuvSoSSMpz3LWdt3 --------- Co-authored-by: Jason B. Cox <contact@jasonbcox.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>