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" +} 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) } }