diff --git a/cmd/ingestor/client_uplink.go b/cmd/ingestor/client_uplink.go index 2680f2019..f4790594f 100644 --- a/cmd/ingestor/client_uplink.go +++ b/cmd/ingestor/client_uplink.go @@ -170,6 +170,7 @@ func (s *Store) InsertClientUplinkPing(p *ClientUplinkPing) (bool, error) { // uplinkChannel is the ping channel the ingestor decrypts to learn tag → hash. type uplinkChannel struct { + name string keyHex string hashByte int } @@ -185,37 +186,51 @@ func newUplinkChannel(name string) *uplinkChannel { k := sha256.Sum256([]byte(name)) key := k[:16] h := sha256.Sum256(key) - return &uplinkChannel{keyHex: hex.EncodeToString(key), hashByte: int(h[0])} + return &uplinkChannel{name: name, keyHex: hex.EncodeToString(key), hashByte: int(h[0])} } // noteUplinkTransmission links a ping's tag to the packet hash an observer -// reported. Only GRP_TXT the ingestor could not decrypt with its own channel -// keys reaches here, on the ping channel's hash byte; that byte is shared with -// unrelated channels, so the MAC check in decryptChannelMessage is what tells -// a ping from them. +// reported. The ping channel's hash byte is shared with unrelated channels. +// When the ingestor's own channel keys include the ping channel, the decoder +// has already decrypted it (type CHAN) and the channel name tells a ping from +// the others. Otherwise it arrives as GRP_TXT and the MAC check in +// decryptChannelMessage does. func noteUplinkTransmission(store *Store, ch *uplinkChannel, decoded *DecodedPacket, pktHash string) { if ch == nil || decoded == nil || pktHash == "" { return } pl := decoded.Payload - if pl.Type != "GRP_TXT" || pl.ChannelHash != ch.hashByte || pl.EncryptedData == "" || pl.MAC == "" { + if pl.ChannelHash != ch.hashByte { return } - res, err := decryptChannelMessage(pl.EncryptedData, pl.MAC, ch.keyHex) - if err != nil { + var text, sender string + switch { + case pl.Type == "CHAN" && sameChannelName(pl.Channel, ch.name): + text, sender = pl.Text, pl.Sender + case pl.Type == "GRP_TXT" && pl.EncryptedData != "" && pl.MAC != "": + res, err := decryptChannelMessage(pl.EncryptedData, pl.MAC, ch.keyHex) + if err != nil { + return + } + text, sender = res.Message, res.Sender + if res.Sender != "" { + text = res.Sender + ": " + res.Message + } + default: return } - text := res.Message - if res.Sender != "" { - text = res.Sender + ": " + res.Message - } m := uplinkTextTagRe.FindStringSubmatch(text) if m == nil { return } if _, err := store.db.Exec(` INSERT INTO client_uplink_tx (tag, pkt_hash, sender, seen_at) VALUES (?,?,?,?) - ON CONFLICT(tag) DO NOTHING`, m[1], pktHash, res.Sender, time.Now().UTC().Format(time.RFC3339)); err != nil { + ON CONFLICT(tag) DO NOTHING`, m[1], pktHash, sender, time.Now().UTC().Format(time.RFC3339)); err != nil { log.Printf("[uplink] client_uplink_tx insert: %v", err) } } + +// sameChannelName compares hashtag channel names ignoring case and the "#". +func sameChannelName(a, b string) bool { + return strings.EqualFold(strings.TrimPrefix(strings.TrimSpace(a), "#"), strings.TrimPrefix(strings.TrimSpace(b), "#")) +} diff --git a/cmd/ingestor/client_uplink_test.go b/cmd/ingestor/client_uplink_test.go index e168ef32e..7fbc5c4e7 100644 --- a/cmd/ingestor/client_uplink_test.go +++ b/cmd/ingestor/client_uplink_test.go @@ -5,6 +5,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "fmt" "testing" "time" ) @@ -160,6 +161,42 @@ func TestNoteUplinkTransmissionLinksTagToHash(t *testing.T) { } } +// The live ingestor holds #coredrive among its own channel keys, so the decoder +// hands over a decrypted CHAN payload instead of GRP_TXT. +func TestNoteUplinkTransmissionAlreadyDecrypted(t *testing.T) { + s := newTestStore(t) + ch := newUplinkChannel("#coredrive") + chanPayload := func(channel, text string) *DecodedPacket { + return &DecodedPacket{Payload: Payload{Type: "CHAN", Channel: channel, ChannelHash: ch.hashByte, + DecryptionStatus: "decrypted", Sender: "BE-ON8AR-Mobile", Text: text}} + } + noteUplinkTransmission(s, ch, chanPayload("#CoreDrive", "BE-ON8AR-Mobile: cdrx 71c131bd"), "19602811bbca85e5") + // Same hash byte, another channel: not a ping, whatever the text says. + noteUplinkTransmission(s, ch, chanPayload("#other", "x: cdrx aaaaaaaa"), "1111111111111111") + var hash, sender string + if err := s.db.QueryRow(`SELECT pkt_hash, sender FROM client_uplink_tx WHERE tag = '71c131bd'`).Scan(&hash, &sender); err != nil { + t.Fatal(err) + } + if hash != "19602811bbca85e5" || sender != "BE-ON8AR-Mobile" { + t.Fatalf("hash %s sender %q", hash, sender) + } + if n := uplinkCount(t, s, "client_uplink_tx"); n != 1 { + t.Fatalf("client_uplink_tx rows = %d, want 1", n) + } + // Through the real decoder with the key loaded: also CHAN, also linked. + key := ch.keyHex + keyed := grpTxtPayload(t, "#coredrive", "BE-ON8AR-Mobile: cdrx 036ec636") + raw, _ := hex.DecodeString(fmt.Sprintf("%02x", ch.hashByte) + keyed.Payload.MAC + keyed.Payload.EncryptedData) + dec := &DecodedPacket{Payload: decodeGrpTxt(raw, map[string]string{"#coredrive": key})} + if dec.Payload.Type != "CHAN" { + t.Fatalf("decoder type %s, want CHAN", dec.Payload.Type) + } + noteUplinkTransmission(s, ch, dec, "2222333344445555") + if n := uplinkCount(t, s, "client_uplink_tx"); n != 2 { + t.Fatalf("client_uplink_tx rows = %d, want 2", n) + } +} + func TestClientUplinkConfig(t *testing.T) { if (&Config{}).ClientUplinkEnabled() || (&Config{}).UplinkChannel() != nil { t.Fatal("absent config must be off")