From 6214a2bd77b036d0b93921eb658f1b8fd796243a Mon Sep 17 00:00:00 2001 From: you Date: Thu, 23 Apr 2026 03:37:07 +0000 Subject: [PATCH 1/2] fix: add default Public channel key to rainbow The MeshCore default Public channel uses the well-known PSK 8b3387e9c5cdea6ac9e5edbaa115cd72 (channel hash byte 0x11), per the companion protocol spec. Without this entry, GRP_TXT messages on the default Public channel land in the rainbow lookup with no key and report decryption_failed even though the key is publicly known. Add it as 'Public' so the ingestor decrypts these messages out of the box on fresh deploys. --- channel-rainbow.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/channel-rainbow.json b/channel-rainbow.json index 5aaa38d9..73ad3a23 100644 --- a/channel-rainbow.json +++ b/channel-rainbow.json @@ -294,5 +294,6 @@ "#colombia": "bea223a8c1d13ed9638ee000ea3a6aca", "#bogota": "6d0864985b64350ce4cbfebf4979e970", "#peru": "7e6fc347bf29a4c128ac3156865bd521", - "#lima": "5f167ce354eca08ab742463df10ef255" -} \ No newline at end of file + "#lima": "5f167ce354eca08ab742463df10ef255", + "Public": "8b3387e9c5cdea6ac9e5edbaa115cd72" +} From 2f09c8fd6405d295ff0b0f79f40db5e98120275e Mon Sep 17 00:00:00 2001 From: you Date: Thu, 23 Apr 2026 03:38:42 +0000 Subject: [PATCH 2/2] fix: hardcode default Public channel key in ingestor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MeshCore default Public channel uses well-known PSK 8b3387e9c5cdea6ac9e5edbaa115cd72 — it is part of the firmware spec, not a community-named channel. Don't rely on the rainbow JSON to ship it; bake it into a builtinChannelKeys() floor that loads before the rainbow file. Rainbow / hashChannels / explicit config can still override it (priority order preserved). This means fresh deploys decrypt default Public traffic out of the box even if channel-rainbow.json is missing or stale. Tests: - TestLoadChannelKeysBuiltinPublic: confirms Public is present with no rainbow / config. - TestLoadChannelKeysBuiltinOverridable: confirms explicit config still wins. --- cmd/ingestor/main.go | 21 +++++++++++++++++++-- cmd/ingestor/main_test.go | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/cmd/ingestor/main.go b/cmd/ingestor/main.go index 481c7cc1..22e22bd5 100644 --- a/cmd/ingestor/main.go +++ b/cmd/ingestor/main.go @@ -752,12 +752,29 @@ func deriveHashtagChannelKey(channelName string) string { return hex.EncodeToString(h[:16]) } +// builtinChannelKeys returns channel keys that are part of the MeshCore firmware +// defaults and should always be available, regardless of the rainbow file or config. +// Adding new entries here is the right move when a key is part of the protocol spec +// (not a community-named hashtag channel). +func builtinChannelKeys() map[string]string { + return map[string]string{ + // Default Public channel — well-known PSK from the MeshCore companion + // protocol spec. Channel-hash byte = 0x11. + "Public": "8b3387e9c5cdea6ac9e5edbaa115cd72", + } +} + // loadChannelKeys loads channel decryption keys from config and/or a JSON file. -// Merge priority: rainbow (lowest) → derived from hashChannels → explicit config (highest). +// Merge priority: builtin (lowest) → rainbow → derived from hashChannels → explicit config (highest). func loadChannelKeys(cfg *Config, configPath string) map[string]string { keys := make(map[string]string) - // 1. Rainbow table keys (lowest priority) + // 0. Built-in firmware-default keys (lowest priority — overridable by everything else) + for k, v := range builtinChannelKeys() { + keys[k] = v + } + + // 1. Rainbow table keys keysPath := os.Getenv("CHANNEL_KEYS_PATH") if keysPath == "" { keysPath = cfg.ChannelKeysPath diff --git a/cmd/ingestor/main_test.go b/cmd/ingestor/main_test.go index 6a10bcb9..b84f1ca8 100644 --- a/cmd/ingestor/main_test.go +++ b/cmd/ingestor/main_test.go @@ -607,8 +607,41 @@ func TestLoadChannelKeysHashChannelsNormalization(t *testing.T) { if _, ok := keys["#Spaced"]; !ok { t.Error("should derive key for #Spaced (trimmed)") } - if len(keys) != 3 { - t.Errorf("expected 3 keys, got %d", len(keys)) + // 3 derived + builtins (Public) + expected := 3 + len(builtinChannelKeys()) + if len(keys) != expected { + t.Errorf("expected %d keys, got %d", expected, len(keys)) + } +} + +// Default Public channel must always be present from the built-in floor, +// regardless of whether a rainbow file is provided. +func TestLoadChannelKeysBuiltinPublic(t *testing.T) { + t.Setenv("CHANNEL_KEYS_PATH", "") + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.json") + cfg := &Config{} + + keys := loadChannelKeys(cfg, cfgPath) + + if got := keys["Public"]; got != "8b3387e9c5cdea6ac9e5edbaa115cd72" { + t.Errorf("Public key = %q, want firmware-default 8b3387e9c5cdea6ac9e5edbaa115cd72", got) + } +} + +// Explicit config and rainbow entries must still override the built-in floor. +func TestLoadChannelKeysBuiltinOverridable(t *testing.T) { + t.Setenv("CHANNEL_KEYS_PATH", "") + dir := t.TempDir() + cfgPath := filepath.Join(dir, "config.json") + cfg := &Config{ + ChannelKeys: map[string]string{"Public": "deadbeefdeadbeefdeadbeefdeadbeef"}, + } + + keys := loadChannelKeys(cfg, cfgPath) + + if got := keys["Public"]; got != "deadbeefdeadbeefdeadbeefdeadbeef" { + t.Errorf("Public key = %q, want explicit override deadbeef...", got) } }