Compare commits

...

2 Commits

Author SHA1 Message Date
you
2f09c8fd64 fix: hardcode default Public channel key in ingestor
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.
2026-04-23 03:38:42 +00:00
you
6214a2bd77 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.
2026-04-23 03:37:07 +00:00
3 changed files with 57 additions and 6 deletions

View File

@@ -294,5 +294,6 @@
"#colombia": "bea223a8c1d13ed9638ee000ea3a6aca",
"#bogota": "6d0864985b64350ce4cbfebf4979e970",
"#peru": "7e6fc347bf29a4c128ac3156865bd521",
"#lima": "5f167ce354eca08ab742463df10ef255"
}
"#lima": "5f167ce354eca08ab742463df10ef255",
"Public": "8b3387e9c5cdea6ac9e5edbaa115cd72"
}

View File

@@ -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

View File

@@ -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)
}
}