fix(mqtt): store ingested publicKey as base64 (not hex), backfill via migration 069#3133
Conversation
The MQTT NodeInfo ingest path at src/server/mqttIngestion.ts:224 was
writing nodes.publicKey as hex (bytesToHex(user.publicKey)) while every
other write path stored base64 — direct serial/TCP NodeInfo
(meshtasticManager.ts:5546) and the device's security-config handshake
(meshtasticManager.ts:3594) both use Buffer.toString('base64').
Net effect: any node first seen via MQTT was stored hex-encoded; when
that same node later sent NodeInfo over the direct radio link, the
key-mismatch detector at meshtasticManager.ts:5572 (string-equality
compare on existingNode.publicKey vs nodeData.publicKey) fired as a
false positive on every packet — the underlying 32 bytes were
identical, only the string encoding differed (e.g. stored
"1ec4c0f23909..." vs mesh "HsTA8jkJ..."). The "self-heal" upsert path
then overwrote hex with base64, so subsequent packets came back as
match — until the next user-initiated MQTT ingest flipped it back.
Fix: encode the bytes as base64 in the MQTT path, matching every other
write site.
Migration 069 backfills existing hex-encoded rows by scanning
`nodes.publicKey` for `^[0-9a-f]{64}$` (lowercase 32-byte hex) and
converting each to its base64 equivalent in JS. Idempotent — base64
output is 44 chars and includes uppercase / `+/=`, so a second run
finds nothing to convert. Implemented for SQLite / PostgreSQL / MySQL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @Yeraze's task —— View job PR Review: MQTT PublicKey Encoding Fix ✅Todo List
SummaryThis is a high-quality fix for a subtle but impactful encoding inconsistency bug. The implementation is production-ready with excellent attention to detail. ✅ Code Quality & Best PracticesMigration 069:
MQTT Ingestion Fix:
✅ No Security Concerns
✅ Performance Considerations
|
…ey after purge (#3140) When key-repair detects a mismatch and (via processKeyRepairs or the immediate-purge path) calls sendRemoveNode, the firmware purges the destination's entry — including its public key — from its NodeDB. Our local nodes.publicKey column still holds the (now-stale) key, so sendTextMessage was still setting pkiEncrypted=true. The firmware, with no key to use, silently drops the outbound DM. Net effect: DMs to a node with an active keyMismatchDetected appear to "send" from MeshMonitor's side but never reach the destination. This was masked by — and the symptom surfaced through — #3133 (MQTT publicKey hex vs base64). MQTT-first-seen nodes had their key written as hex; every subsequent direct-radio NodeInfo string-compared unequal against the base64 form, triggering a false mismatch, triggering the auto-purge, leaving the firmware keyless for that destination. Once the hex/base64 normalization landed (2430152), the false positives stopped and DMs flowed again. The underlying "PKI=true with no key → silent drop" problem remains for genuine key changes, hence this guard. The fix consults targetNode.keyMismatchDetected before requesting PKI. When the flag is set, we skip pkiEncrypted=true and fall back to channel encryption — matching what the post-purge node-info-exchange code already does (lines 2174-2175, 5604-5605). Regression test: src/server/meshtasticManager.test.ts adds a "DM PKI guard" describe block covering the five publicKey/keyMismatchDetected combinations, mirroring the production decision logic so future refactors of that branch fail loudly. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Fixes a long-standing false-positive in the key-mismatch detector caused by an encoding disagreement between the MQTT ingest path and every other publicKey write site.
The bug
When a node arrived via MQTT first, its key was stored hex-encoded. When the same node later sent NodeInfo over the direct radio link, the comparison at `meshtasticManager.ts:5572`
```ts
if (existingNode && existingNode.publicKey && nodeData.publicKey && existingNode.publicKey !== nodeData.publicKey)
```
compared `"1ec4c0f23909..."` (hex) against `"HsTA8jkJ..."` (base64) — never equal, even though the 32 underlying bytes were identical. The user-visible symptom: repeated `🔐 Key mismatch detected` log warnings for nodes whose keys had never actually changed. The upsert path then "self-healed" by overwriting hex with base64, but the next MQTT-side update flipped it back, and the cycle repeated.
Diagnosed by decoding the live log fragments side-by-side:
```
stored=1ec4c0f2... → hex → 0x1e 0xc4 0xc0 0xf2 ...
mesh=HsTA8jkJ... → base64 → 0x1e 0xc4 0xc0 0xf2 0x39 0x09 ...
```
Identical bytes.
Fix
`src/server/mqttIngestion.ts` now encodes `user.publicKey` (and the snake-case fallback) as base64, matching all other paths.
Cleanup migration
`src/server/migrations/069_normalize_node_public_keys_to_base64.ts` scans `nodes.publicKey` for lowercase 32-byte hex (`^[0-9a-f]{64}$`) and converts each to its base64 equivalent. Idempotent across SQLite / PostgreSQL / MySQL — base64 output is 44 chars and contains uppercase / `+` / `/` / `=`, so a second run finds nothing to match.
After this lands and the migration runs, any node whose key was stored hex by historical MQTT ingest will be converted in-place. No more spurious mismatch warnings; the key-mismatch detector only fires on actual key changes.
Test plan
🤖 Generated with Claude Code