# 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.ts` is 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 with `MESHCORE_CHANNEL_SECRETS` (managed environment configuration, with comma-separated `name:hex` or bare hex entries; dedupes by secret). Environment-supplied entries are classified as confidential; never commit their values or copy them into `PUBLIC_CHANNELS`. - `buildSummary()` / `identifyChannel()` — shared by ingest and offline tools. - The repo is **public**. `PUBLIC_CHANNELS` is the explicit source classification; the scanner baseline in `.gitleaksignore` records 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_decryptions` side table (migration 035), filled by the backfill tool. The feed joins it with `COALESCE(p.payload->>'_summary', pd.summary)`. - ⛔ Never bulk-UPDATE the `packets` hypertable — 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=&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_CHANNELS` in `frontend/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.