Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions cmd/ingestor/client_uplink.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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), "#"))
}
37 changes: 37 additions & 0 deletions cmd/ingestor/client_uplink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"testing"
"time"
)
Expand Down Expand Up @@ -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")
Expand Down
Loading