mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-15 10:40:01 +00:00
security: classify channel keys and baseline reviewed findings
This commit is contained in:
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,6 +16,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
if: github.event_name != 'schedule'
|
||||
name: Backend
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
@@ -62,6 +65,7 @@ jobs:
|
||||
- run: npm run build
|
||||
|
||||
frontend:
|
||||
if: github.event_name != 'schedule'
|
||||
name: Frontend
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -81,6 +85,7 @@ jobs:
|
||||
- run: npm run build
|
||||
|
||||
workers-and-compose:
|
||||
if: github.event_name != 'schedule'
|
||||
name: Workers and Compose
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -407,3 +412,7 @@ jobs:
|
||||
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITLEAKS_VERSION: 8.30.0
|
||||
- name: Scan full repository history on push and pull request
|
||||
if: github.event_name != 'schedule'
|
||||
run: gitleaks git --redact --no-banner --log-opts=--all
|
||||
|
||||
@@ -2,38 +2,3 @@ title = "UK Mesh gitleaks configuration"
|
||||
|
||||
[extend]
|
||||
useDefault = true
|
||||
|
||||
[[rules]]
|
||||
id = "generic-api-key"
|
||||
|
||||
[[rules.allowlists]]
|
||||
description = "Documented MeshCore public-channel interoperability key"
|
||||
paths = ['''^backend/backfill-transport-codes\.mjs$''']
|
||||
regexTarget = "line"
|
||||
regexes = ['''(?i)const DEFAULT_PUBLIC_CHANNEL_KEY\s*=\s*'[0-9a-f]{32}'\s*;''']
|
||||
condition = "AND"
|
||||
|
||||
[[rules.allowlists]]
|
||||
description = "Reviewed public/community MeshCore channel secrets (channelRegistry.ts VALIDATED_CHANNELS) — each entry verified to decrypt real UK Mesh group text; classified intentionally public in the channel registry"
|
||||
paths = ['''^backend/src/mqtt/channelRegistry\.ts$''']
|
||||
regexTarget = "line"
|
||||
regexes = ['''\{ name: '[A-Za-z0-9_-]+',\s+secret: '[0-9a-f]{32}'\s*\},?''']
|
||||
condition = "AND"
|
||||
|
||||
[[rules.allowlists]]
|
||||
description = "Documented Public channel key in any file/historical commit (exact fingerprint)"
|
||||
regexTarget = "secret"
|
||||
regexes = ['''^8b3387e9c5cdea6ac9e5edbaa115cd72$''']
|
||||
|
||||
[[rules.allowlists]]
|
||||
description = "Fake test fixtures (exact fingerprints)"
|
||||
regexTarget = "secret"
|
||||
regexes = [
|
||||
'''^0123456789abcdef0123456789abcdef$''',
|
||||
'''^abcdef0123456789$''',
|
||||
]
|
||||
|
||||
[[rules.allowlists]]
|
||||
description = "Deployed-commit SHA in live website manifests"
|
||||
regexTarget = "secret"
|
||||
regexes = ['''^bef003f072283fd5e3f14b29890f7610adf3fb9808e8c6e700bd48d3e7a8bc87$''']
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Exact reviewed Gitleaks fingerprints (commit:path:rule:line).
|
||||
# Public MeshCore channel material; keep private channel keys in MESHCORE_CHANNEL_SECRETS.
|
||||
8b902dfb3ff08128556f22de2d239c458f782949:backend/src/mqtt/channelRegistry.ts:generic-api-key:38
|
||||
8b902dfb3ff08128556f22de2d239c458f782949:backend/src/mqtt/channelRegistry.ts:generic-api-key:42
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:30
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:31
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:32
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:33
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:34
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:35
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:36
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:37
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:38
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:39
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:42
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:43
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:46
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:47
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:48
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:49
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:50
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:51
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:52
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:53
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:54
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:55
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:56
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:57
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:58
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:59
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:60
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:61
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:62
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:63
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:65
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:66
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:67
|
||||
61fdcd9d11ef9a3aed93feb1d6cc4bf7c91e17d8:backend/src/mqtt/channelRegistry.ts:generic-api-key:68
|
||||
5db382e181591b9bf736647eb903c699223cfdc8:backend/backfill-transport-codes.mjs:generic-api-key:35
|
||||
be45a2e967ca51d909940e29ecfd531a4566d404:backend/backfill-transport-codes.mjs:generic-api-key:35
|
||||
288c66a0b6e1037c74dea4392ad0a910525333df:backend/backfill-transport-codes.mjs:generic-api-key:35
|
||||
cb713d6c1a469f634a5a11678a1b9a81fa6413a0:backend/src/mqtt/client.ts:generic-api-key:96
|
||||
# Test-only credential and token fixtures.
|
||||
1ebc496965ff61f398851dde81c9da6910ceb7e0:backend/src/security/operatorAuth.test.ts:generic-api-key:5
|
||||
d278c5072891fcfc643e050c55fb6ace908fdaa3:backend/src/security/operatorAuth.test.ts:generic-api-key:5
|
||||
93cc3e1b385fe2034876caf4af843d040da47cf1:backend/src/spam/sanitize.test.ts:generic-api-key:50
|
||||
# Generated deployment manifests containing source revision identifiers.
|
||||
cd9ee586364ed70c5298d36eba0bb92958725b2d:website-live-manifest.staging.json:generic-api-key:40
|
||||
cd9ee586364ed70c5298d36eba0bb92958725b2d:website-live-manifest.json:generic-api-key:40
|
||||
5210aba5e37b928db3744057beb78f2c17f92263:website-live-manifest.json:generic-api-key:38
|
||||
ac4ee2afecc16ed67fdc09cf70953dc85d3a4c5a:website-live-manifest.staging.json:generic-api-key:38
|
||||
@@ -170,7 +170,7 @@ Copy `.env.example` to `.env` and fill in your values. All variables used by the
|
||||
| `ALLOWED_ORIGINS` | `http://localhost:3001,http://localhost:3002` | Comma-separated browser origins allowed for CORS and WebSocket |
|
||||
| `API_RATE_LIMIT_MAX` | `120` | Per-client public API requests/minute; raise only in an isolated load-test project |
|
||||
| `VITE_APP_HOSTNAME` | *(blank — always shows dashboard)* | If set, only this hostname serves the analytics dashboard; all others serve the public website layout |
|
||||
| `MESHCORE_CHANNEL_SECRETS` | *(blank)* | Comma-separated channel secrets for decrypting GroupText packets. Format: `name:hex` or bare hex. The default MeshCore public channel key is always included. |
|
||||
| `MESHCORE_CHANNEL_SECRETS` | *(blank)* | Confidential channel keys for decrypting GroupText packets, managed through environment configuration. Format: `name:hex` or bare hex. The explicitly public `PUBLIC_CHANNELS` defaults are always included. |
|
||||
| `OWNER_DATABASE_URL` | *(optional)* | Separate Postgres database URL for owner portal username → repeater mappings |
|
||||
| `OWNER_COOKIE_SECRET` | *(optional but recommended)* | Secret used to encrypt/sign the owner session cookie |
|
||||
| `OWNER_MQTT_USERNAME_MAP` | *(empty)* | Operator-managed owner grants in the format `user=nodeId1|nodeId2,...` |
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { buildChannelEntries, PUBLIC_CHANNELS, VALIDATED_CHANNELS } from './channelRegistry.js';
|
||||
|
||||
test('committed channel material is explicitly classified as public', () => {
|
||||
assert.strictEqual(VALIDATED_CHANNELS, PUBLIC_CHANNELS);
|
||||
assert.ok(PUBLIC_CHANNELS.length > 0);
|
||||
assert.ok(buildChannelEntries().every((entry) => entry.classification === 'public'));
|
||||
});
|
||||
|
||||
test('environment-supplied channel keys are classified as confidential', () => {
|
||||
const entry = buildChannelEntries(`private:${'a'.repeat(32)}`)
|
||||
.find((channel) => channel.name === 'private');
|
||||
|
||||
assert.equal(entry?.classification, 'confidential');
|
||||
});
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* Channel registry — the single source of truth for MeshCore channel secrets.
|
||||
* Channel registry — explicitly public defaults plus confidential environment keys.
|
||||
*
|
||||
* VALIDATED_CHANNELS are the default secrets baked into the service (all
|
||||
* recovered/community-known channels, verified to decrypt real group text).
|
||||
* MESHCORE_CHANNEL_SECRETS (env, 'name:hex' or bare 'hex', comma-separated)
|
||||
* APPENDS extra channels on top — useful for community-shared secrets that
|
||||
* shouldn't be committed.
|
||||
* PUBLIC_CHANNELS contains only recovered/community-known material intentionally
|
||||
* classified as public. MESHCORE_CHANNEL_SECRETS ('name:hex' or bare 'hex',
|
||||
* comma-separated) adds confidential keys without committing their values.
|
||||
*
|
||||
* Also hosts the summary + channel-identification helpers so the ingest
|
||||
* pipeline and offline tools (e.g. backfillDecrypt) share one implementation.
|
||||
@@ -15,18 +13,21 @@ import type { GroupTextPayload } from '@michaelhart/meshcore-decoder';
|
||||
import { decodePacketCompat } from './decodePacket.js';
|
||||
import { BoundedTtlMap } from '../cache/boundedTtlMap.js';
|
||||
|
||||
export type ChannelClassification = 'public' | 'confidential';
|
||||
|
||||
export interface ChannelEntry {
|
||||
classification: ChannelClassification;
|
||||
name: string;
|
||||
secret: string;
|
||||
keyStore: ReturnType<typeof MeshCoreDecoder.createKeyStore>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default channels — recovered via the meshcore wordlist/derivation audit
|
||||
* (2026-08-06) and validated: each key decrypts real human-readable group
|
||||
* text (sane epoch timestamp + printable text) on the UK Mesh network.
|
||||
* Every committed value is intentionally public: recovered/community-known
|
||||
* channels from the 2026-08-06 derivation audit, validated against real UK Mesh
|
||||
* group text. Confidential channel keys belong in MESHCORE_CHANNEL_SECRETS.
|
||||
*/
|
||||
export const VALIDATED_CHANNELS: ReadonlyArray<{ name: string; secret: string }> = [
|
||||
export const PUBLIC_CHANNELS: ReadonlyArray<{ name: string; secret: string }> = [
|
||||
{ name: 'Public', secret: '8b3387e9c5cdea6ac9e5edbaa115cd72' },
|
||||
{ name: 'test', secret: '9cd8fcf22a47333b591d96a2b848b73f' },
|
||||
{ name: 'bot', secret: 'eb50a1bcb3e4e5d7bf69a57c9dada211' },
|
||||
@@ -70,9 +71,13 @@ export const VALIDATED_CHANNELS: ReadonlyArray<{ name: string; secret: string }>
|
||||
{ name: 'uk', secret: '22b2eed34b5cc429ce1dc5e88635ff84' },
|
||||
];
|
||||
|
||||
/** @deprecated Use PUBLIC_CHANNELS to make the source classification explicit. */
|
||||
export const VALIDATED_CHANNELS = PUBLIC_CHANNELS;
|
||||
|
||||
/** Build the channel entry list: committed defaults + env extras (dedup by secret). */
|
||||
export function buildChannelEntries(envValue?: string): ChannelEntry[] {
|
||||
const entries: ChannelEntry[] = VALIDATED_CHANNELS.map(({ name, secret }) => ({
|
||||
const entries: ChannelEntry[] = PUBLIC_CHANNELS.map(({ name, secret }) => ({
|
||||
classification: 'public',
|
||||
name,
|
||||
secret,
|
||||
keyStore: MeshCoreDecoder.createKeyStore({ channelSecrets: [secret] }),
|
||||
@@ -84,7 +89,12 @@ export function buildChannelEntries(envValue?: string): ChannelEntry[] {
|
||||
const secret = colon > 0 ? raw.slice(colon + 1) : raw;
|
||||
if (seen.has(secret.toLowerCase())) continue;
|
||||
seen.add(secret.toLowerCase());
|
||||
entries.push({ name, secret, keyStore: MeshCoreDecoder.createKeyStore({ channelSecrets: [secret] }) });
|
||||
entries.push({
|
||||
classification: 'confidential',
|
||||
name,
|
||||
secret,
|
||||
keyStore: MeshCoreDecoder.createKeyStore({ channelSecrets: [secret] }),
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
+11
-7
@@ -6,16 +6,20 @@ how historical traffic was retroactively decrypted.
|
||||
## Key store
|
||||
|
||||
- `backend/src/mqtt/channelRegistry.ts` is the single source of truth:
|
||||
- `VALIDATED_CHANNELS` — default secrets baked into the image (39 entries
|
||||
incl. Public; recovered 2026-08-06 and validated to decrypt real
|
||||
human-readable group text).
|
||||
- `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` (env, comma-separated `name:hex` or bare hex;
|
||||
dedupes by secret) for secrets that shouldn't be committed.
|
||||
`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**: keys in `channelRegistry.ts` are public. Keep
|
||||
non-derivable/community keys in the env var only.
|
||||
- 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`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user