* docs: add C5a mission handoff * security: classify channel keys and baseline reviewed findings * security: revalidate MQTT auth and revoke owner sessions * test(ops): cover concurrent newuser grant updates * fix(ops): preserve executable newuser test * fix(ops): cover partial newuser rollback failures * fix(owner): preserve password whitespace during login * docs: add C5c mission handoff * test(cache): drop stale authCache policy entry (cache removed by auth revalidation) * docs: keep HANDOFF on current mission --------- Co-authored-by: gadgethd <111318106+gadgethd@users.noreply.github.com> Co-authored-by: hermes-gadget <284726080+hermes-gadget@users.noreply.github.com>
3.3 KiB
MeshCore channel decryption
How the UK Mesh site decrypts group-text (packet type 5) traffic live, and how historical traffic was retroactively decrypted.
Key store
backend/src/mqtt/channelRegistry.tsis the single source of truth:PUBLIC_CHANNELS— 41 recovered/community-known values intentionally classified as public and baked into the image. Each was validated against real human-readable UK Mesh group text.buildCombinedKeyStore()— merges baked defaults withMESHCORE_CHANNEL_SECRETS(managed environment configuration, with comma-separatedname:hexor bare hex entries; dedupes by secret). Environment-supplied entries are classified as confidential; never commit their values or copy them intoPUBLIC_CHANNELS.buildSummary()/identifyChannel()— shared by ingest and offline tools.
- The repo is public.
PUBLIC_CHANNELSis the explicit source classification; the scanner baseline in.gitleaksignorerecords exact historical commit/path/rule/line fingerprints for reviewed public values, test fixtures, and generated revision identifiers. New findings remain blocking by default. - Reading env at startup only — after a change:
docker compose -f docker-compose.yml -f docker-compose.live.yml up -d --no-deps --force-recreate backend.
Decryption format
GRP_TXT payload = [channel hash 1B][MAC 2B][AES-128-ECB ciphertext].
Hashtag channel keys are derivable: sha256("#name")[:16]. The public
channel key (8b3387e9c5cdea6ac9e5edbaa115cd72, hash byte 11) is published
in MeshCore docs. Other derivation schemes exist in the wild (sha1/md5
variants, direct ASCII passphrases).
Stored decrypted data
- Live ingest stores decrypted content inline in
payload.decrypted(message/sender/timestamp/flags) and_summary=[ChannelName] sender: message. - Historical decryption lives in the
packet_decryptionsside table (migration 035), filled by the backfill tool. The feed joins it withCOALESCE(p.payload->>'_summary', pd.summary). - ⛔ Never bulk-UPDATE the
packetshypertable — TimescaleDB plans a seq-scan over every chunk. INSERT into the side table instead.
Backfill tool
node dist/tools/backfillDecrypt.js (run inside the backend container).
Two-phase keyset-paginated scan over packets_hash_idx (~1,250 hashes/s).
2026-08-06 run: 387,161 packets processed, 173,778 decrypted.
Feed integration
GET /api/feed/messages?channel=<scope>&limit<=50— up to 50 unique historical messages per channel (90-day bound, dedup by packet hash).- The feed sidebar lists every decrypted channel
(
MESSAGE_SCOPE_CHANNELSinfrontend/src/pages/ukmesh/feedModel.ts); new channels need a frontend rebuild to appear as filters.
Validation gate (Ben, 2026-08-06)
Never deploy a recovered key whose plaintext isn't verified USEFUL — sane
epoch timestamps + printable UTF-8 on a time-spread sample. 1-byte hash
buckets collide across channels (MAC pass rate can reach ~97% on junk);
always validate before .env deploy. Honest decrypt coverage measured at
~43.5% of the 90-day volume (raw hash-bucket sums overcount).
Out-of-band keys
~/ukmesh/meshcore-discord-bot/.env holds NORTHEAST_CHANNEL_SECRET
(== #northeast) and THENORF_CHANNEL_SECRET (hash 8A) — the discord bot
forwards those channels. Check it before attacking new keys.