mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-20 09:01:04 +00:00
## Fixes #1729 The firmware-default **Public** channel (channel-hash byte `0x11` = 17) was rendered as an opaque **"Encrypted (0x11)"** row at the bottom of the analytics Channels tab, despite the key being well-known and builtin. ## Root cause `computeAnalyticsChannels` applied the #978 rainbow-table validation (`SHA256(SHA256("#name")[:16])[0]`, the **hashtag** hash scheme) to every decoded channel name. The Public channel is a **PSK** channel whose hash byte is key-derived (`SHA256(key)[0]` = 17), not hashtag-derived (`186` for `#Public`). So the ingestor-decoded name `"Public"` failed the hashtag check and was discarded, the row forced to `encrypted=true, name="ch17"`. ## Fix Trust the ingestor's `decryptionStatus`. The ingestor already persists `decryptionStatus:"decrypted"` when it decoded a packet with a real key (PSK), and `"no_key"` / `"decryption_failed"` otherwise. When the packet is `decrypted`, skip the hashtag hash check and keep the name — it came from a key-based decryption, not a rainbow-table lookup. The #978 mismatch rejection still applies to non-decrypted packets, so rainbow-table collisions are still caught. Frontend needs no change: `encrypted=false, name="Public"` lands in the "Network" group (top), not "Encrypted". ## Tests - `makeGrpTx` gains `makeGrpTxWithStatus` companion to set `decryptionStatus`. - `TestComputeAnalyticsChannels_PublicChannelPreserved`: hash 17 / "Public" / `decrypted` → name stays `"Public"`, `encrypted=false`. - `TestComputeAnalyticsChannels_UndecryptedNameStillValidated`: a non-`decrypted` name failing the hashtag check is still downgraded to `ch17` (#978 regression guard). All channel-analytics tests pass; `go build ./...` clean. Co-authored-by: Waydroid Builder <build@waydroid.local> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Waydroid Builder
Claude
parent
750b8742a7
commit
6a32ec2b2d
@@ -38,6 +38,14 @@ func newChannelTestStore(packets []*StoreTx) *PacketStore {
|
||||
}
|
||||
|
||||
func makeGrpTx(channelHash int, channel, text, sender string) *StoreTx {
|
||||
return makeGrpTxWithStatus(channelHash, channel, text, sender, "")
|
||||
}
|
||||
|
||||
// makeGrpTxWithStatus is like makeGrpTx but lets the caller set the ingestor's
|
||||
// decryptionStatus ("decrypted" / "no_key" / "decryption_failed" / ""), which
|
||||
// computeAnalyticsChannels consults to decide whether a channel name is
|
||||
// trustworthy (see #1729).
|
||||
func makeGrpTxWithStatus(channelHash int, channel, text, sender, decryptionStatus string) *StoreTx {
|
||||
decoded := map[string]interface{}{
|
||||
"type": "CHAN",
|
||||
"channelHash": float64(channelHash),
|
||||
@@ -45,6 +53,9 @@ func makeGrpTx(channelHash int, channel, text, sender string) *StoreTx {
|
||||
"text": text,
|
||||
"sender": sender,
|
||||
}
|
||||
if decryptionStatus != "" {
|
||||
decoded["decryptionStatus"] = decryptionStatus
|
||||
}
|
||||
b, _ := json.Marshal(decoded)
|
||||
pt := 5
|
||||
return &StoreTx{
|
||||
@@ -166,3 +177,63 @@ func TestIsPlaceholderName(t *testing.T) {
|
||||
t.Error("Public should NOT be placeholder")
|
||||
}
|
||||
}
|
||||
|
||||
// TestComputeAnalyticsChannels_PublicChannelPreserved is the regression test for
|
||||
// #1729: the firmware-default "Public" channel (channel-hash byte 0x11 = 17,
|
||||
// key-derived via SHA256(key)[0], NOT the hashtag scheme's 186) was being
|
||||
// discarded by the #978 rainbow-table validation and rendered as
|
||||
// "Encrypted (0x11)". The ingestor decrypts Public packets with the builtin
|
||||
// well-known key and marks them decryptionStatus:"decrypted"; the server must
|
||||
// trust that name instead of re-applying the hashtag hash check.
|
||||
func TestComputeAnalyticsChannels_PublicChannelPreserved(t *testing.T) {
|
||||
packets := []*StoreTx{
|
||||
makeGrpTxWithStatus(17, "Public", "hello net", "alice", "decrypted"),
|
||||
makeGrpTxWithStatus(17, "Public", "morning", "bob", "decrypted"),
|
||||
}
|
||||
|
||||
store := newChannelTestStore(packets)
|
||||
result := store.computeAnalyticsChannels("", "", TimeWindow{})
|
||||
|
||||
channels := result["channels"].([]map[string]interface{})
|
||||
if len(channels) != 1 {
|
||||
t.Fatalf("expected 1 channel bucket, got %d: %+v", len(channels), channels)
|
||||
}
|
||||
ch := channels[0]
|
||||
if ch["name"] != "Public" {
|
||||
t.Errorf("expected name 'Public' preserved, got %q (must not be downgraded to ch17)", ch["name"])
|
||||
}
|
||||
if ch["encrypted"] != false {
|
||||
t.Errorf("expected encrypted=false for decrypted Public channel, got %v", ch["encrypted"])
|
||||
}
|
||||
if ch["hash"] != "17" {
|
||||
t.Errorf("expected hash '17', got %v", ch["hash"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestComputeAnalyticsChannels_UndecryptedNameStillValidated ensures the #1729
|
||||
// fix does not weaken #978: when the ingestor did NOT mark the packet
|
||||
// "decrypted" (no_key / decryption_failed / absent), a channel name that fails
|
||||
// the hashtag hash check is still discarded to "chNN", even if text/sender
|
||||
// happen to be present (e.g. a stale/foreign rainbow-table hit).
|
||||
func TestComputeAnalyticsChannels_UndecryptedNameStillValidated(t *testing.T) {
|
||||
// Hash 17 is NOT the hashtag hash of #Public (that is 186). Without a
|
||||
// "decrypted" status, the name must be rejected.
|
||||
packets := []*StoreTx{
|
||||
makeGrpTxWithStatus(17, "#Public", "leaked", "eve", ""),
|
||||
}
|
||||
|
||||
store := newChannelTestStore(packets)
|
||||
result := store.computeAnalyticsChannels("", "", TimeWindow{})
|
||||
|
||||
channels := result["channels"].([]map[string]interface{})
|
||||
if len(channels) != 1 {
|
||||
t.Fatalf("expected 1 channel bucket, got %d: %+v", len(channels), channels)
|
||||
}
|
||||
ch := channels[0]
|
||||
if ch["name"] != "ch17" {
|
||||
t.Errorf("expected undecrypted mismatch downgraded to 'ch17', got %q", ch["name"])
|
||||
}
|
||||
if ch["encrypted"] != true {
|
||||
t.Errorf("expected encrypted=true for rejected rainbow-table name, got %v", ch["encrypted"])
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -5644,12 +5644,13 @@ func (s *PacketStore) computeAnalyticsChannels(region, area string, window TimeW
|
||||
}
|
||||
|
||||
type decodedGrp struct {
|
||||
Type string `json:"type"`
|
||||
Channel string `json:"channel"`
|
||||
ChannelHash interface{} `json:"channelHash"`
|
||||
ChannelHash2 string `json:"channel_hash"`
|
||||
Text string `json:"text"`
|
||||
Sender string `json:"sender"`
|
||||
Type string `json:"type"`
|
||||
Channel string `json:"channel"`
|
||||
ChannelHash interface{} `json:"channelHash"`
|
||||
ChannelHash2 string `json:"channel_hash"`
|
||||
Text string `json:"text"`
|
||||
Sender string `json:"sender"`
|
||||
DecryptionStatus string `json:"decryptionStatus,omitempty"`
|
||||
}
|
||||
|
||||
// Convert channelHash (number or string in JSON) to string
|
||||
@@ -5727,9 +5728,17 @@ func (s *PacketStore) computeAnalyticsChannels(region, area string, window TimeW
|
||||
}
|
||||
encrypted := decoded.Text == "" && decoded.Sender == ""
|
||||
|
||||
// Bug #978 fix: validate channel name against hash to reject rainbow-table mismatches.
|
||||
// If the claimed channel name doesn't hash to the observed channelHash byte, discard it.
|
||||
if name != "" && name != "ch"+hash && !channelNameMatchesHash(name, hash) {
|
||||
// Bug #978 fix: validate channel name against hash to reject rainbow-table
|
||||
// mismatches. If the claimed channel name doesn't hash to the observed
|
||||
// channelHash byte, discard it — UNLESS the ingestor marked the packet
|
||||
// `decryptionStatus:"decrypted"`. That flag means the name came from a
|
||||
// successful key-based (PSK) decryption, not a rainbow-table lookup, so
|
||||
// it is trustworthy regardless of the hashtag-derived hash check. This
|
||||
// keeps the firmware-default Public channel (0x11, key-derived hash
|
||||
// SHA256(key)[0]=17, not the hashtag scheme's 186) from being wrongly
|
||||
// discarded and rendered as "Encrypted (0x11)". See #1729.
|
||||
ingestorDecrypted := decoded.DecryptionStatus == "decrypted"
|
||||
if name != "" && name != "ch"+hash && !ingestorDecrypted && !channelNameMatchesHash(name, hash) {
|
||||
name = "ch" + hash
|
||||
encrypted = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user