From bb98d90db361431c5dcc39aaf9de04c8f09a5917 Mon Sep 17 00:00:00 2001 From: Tnze Date: Sun, 1 Jan 2023 18:43:21 +0800 Subject: [PATCH] Support chat validation --- bot/msg/cache.go | 47 ---------- bot/msg/chat.go | 135 +++++++++++++++++---------- bot/msg/events.go | 2 +- bot/playerlist/playerlist.go | 7 +- chat/sign/cache.go | 42 +++++++++ {bot/msg => chat/sign}/cache_test.go | 51 ++++------ chat/sign/session.go | 73 ++++++++++++--- chat/sign/sign.go | 26 ++++++ examples/daze/daze.go | 8 +- 9 files changed, 239 insertions(+), 152 deletions(-) delete mode 100644 bot/msg/cache.go create mode 100644 chat/sign/cache.go rename {bot/msg => chat/sign}/cache_test.go (53%) diff --git a/bot/msg/cache.go b/bot/msg/cache.go deleted file mode 100644 index 80ed038..0000000 --- a/bot/msg/cache.go +++ /dev/null @@ -1,47 +0,0 @@ -package msg - -import ( - "github.com/Tnze/go-mc/chat/sign" -) - -type signatureCache struct { - signatures [128]*sign.Signature - signIndexes map[sign.Signature]int - cachedBuffer []*sign.Signature -} - -func newSignatureCache() signatureCache { - return signatureCache{ - signIndexes: make(map[sign.Signature]int), - } -} - -func (s *signatureCache) popOrInsert(self *sign.Signature, lastSeen []sign.PackedSignature) error { - var tmp *sign.Signature - s.cachedBuffer = s.cachedBuffer[:0] // clear buffer - if self != nil { - s.cachedBuffer = append(s.cachedBuffer, self) - } - for _, v := range lastSeen { - if v.Signature != nil { - s.cachedBuffer = append(s.cachedBuffer, v.Signature) - } else if v.ID >= 0 && int(v.ID) < len(s.signatures) { - s.cachedBuffer = append(s.cachedBuffer, s.signatures[v.ID]) - } else { - return InvalidChatPacket - } - } - for i := 0; i < len(s.cachedBuffer) && i < len(s.signatures); i++ { - v := s.cachedBuffer[i] - if i, ok := s.signIndexes[*v]; ok { - s.signatures[i] = nil - } - tmp, s.signatures[i] = s.signatures[i], v - s.signIndexes[*v] = i - if tmp != nil { - s.cachedBuffer = append(s.cachedBuffer, tmp) - delete(s.signIndexes, *tmp) - } - } - return nil -} diff --git a/bot/msg/chat.go b/bot/msg/chat.go index 644dec2..ac22396 100644 --- a/bot/msg/chat.go +++ b/bot/msg/chat.go @@ -4,9 +4,10 @@ import ( "crypto/rand" "encoding/binary" "errors" - "fmt" "time" + "github.com/google/uuid" + "github.com/Tnze/go-mc/bot" "github.com/Tnze/go-mc/bot/basic" "github.com/Tnze/go-mc/bot/playerlist" @@ -17,11 +18,12 @@ import ( ) type Manager struct { - c *bot.Client - p *basic.Player - pl *playerlist.PlayerList + c *bot.Client + p *basic.Player + pl *playerlist.PlayerList + events EventsHandler - signatureCache + sign.SignatureCache } func New(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, events EventsHandler) *Manager { @@ -29,58 +31,86 @@ func New(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, events Event c: c, p: p, pl: pl, - signatureCache: newSignatureCache(), + events: events, + SignatureCache: sign.NewSignatureCache(), } - m.attachPlayerMsg(c, p, pl, events.PlayerChatMessage) - return m -} - -func (m *Manager) attachPlayerMsg(c *bot.Client, p *basic.Player, pl *playerlist.PlayerList, handler func(msg chat.Message) error) { c.Events.AddListener( bot.PacketHandler{ Priority: 64, ID: packetid.ClientboundPlayerChat, - F: func(packet pk.Packet) error { - var ( - sender pk.UUID - index pk.VarInt - signature pk.Option[sign.Signature, *sign.Signature] - body sign.PackedMessageBody - unsignedContent pk.Option[chat.Message, *chat.Message] - filter sign.FilterMask - chatType chat.Type - ) - if err := packet.Scan(&sender, &index, &signature, &body, &unsignedContent, &filter, &chatType); err != nil { - return err - } + F: m.handlePacket, + }, + ) + return m +} - //senderInfo, ok := pl.PlayerInfos[uuid.UUID(sender)] - //if !ok { - // return bot.DisconnectErr(chat.TranslateMsg("multiplayer.disconnect.chat_validation_failed")) - //} - //senderInfo.ChatSession.Update() - //if err := senderInfo.ChatSession.Validate(); err != nil { - // return bot.DisconnectErr(chat.TranslateMsg("multiplayer.disconnect.chat_validation_failed")) - //} - // store signature into signatureCache - //if err := m.popOrInsert(signature.Pointer(), body.LastSeen); err != nil { - // return err - //} - var content chat.Message - if unsignedContent.Has { - content = unsignedContent.Val - } else { - content = chat.Text(body.PlainMsg) - } +func (m *Manager) handlePacket(packet pk.Packet) error { + var ( + sender pk.UUID + index pk.VarInt + signature pk.Option[sign.Signature, *sign.Signature] + body sign.PackedMessageBody + unsignedContent pk.Option[chat.Message, *chat.Message] + filter sign.FilterMask + chatType chat.Type + ) + if err := packet.Scan(&sender, &index, &signature, &body, &unsignedContent, &filter, &chatType); err != nil { + return err + } - ct := p.WorldInfo.RegistryCodec.ChatType.FindByID(chatType.ID) - if ct == nil { - return fmt.Errorf("chat type %d not found", chatType.ID) - } + unpackedMsg, err := body.Unpack(&m.SignatureCache) + if err != nil { + return InvalidChatPacket + } + senderInfo, ok := m.pl.PlayerInfos[uuid.UUID(sender)] + if !ok { + return InvalidChatPacket + } + ct := m.p.WorldInfo.RegistryCodec.ChatType.FindByID(chatType.ID) + if ct == nil { + return InvalidChatPacket + } - msg := chatType.Decorate(content, &ct.Chat) - return handler(msg) - }, - }) + var message sign.Message + if senderInfo.ChatSession != nil { + message.Prev = sign.Prev{ + Index: int(index), + Sender: uuid.UUID(sender), + Session: senderInfo.ChatSession.SessionID, + } + } else { + message.Prev = sign.Prev{ + Index: 0, + Sender: uuid.UUID(sender), + Session: uuid.Nil, + } + } + message.Signature = signature.Pointer() + message.MessageBody = unpackedMsg + message.Unsigned = unsignedContent.Pointer() + message.FilterMask = filter + + var validated bool + if senderInfo.ChatSession != nil { + if !senderInfo.ChatSession.VerifyAndUpdate(&message) { + return ValidationFailed + } + validated = true + // store signature into signatureCache + m.PopOrInsert(signature.Pointer(), message.LastSeen) + } + + var content chat.Message + if unsignedContent.Has { + content = unsignedContent.Val + } else { + content = chat.Text(body.PlainMsg) + } + + if m.events.PlayerChatMessage == nil { + return nil + } + msg := chatType.Decorate(content, &ct.Chat) + return m.events.PlayerChatMessage(msg, validated) } // SendMessage send chat message to server. @@ -108,4 +138,7 @@ func (m *Manager) SendMessage(msg string) error { return err } -var InvalidChatPacket = errors.New("invalid chat packet") +var ( + InvalidChatPacket = errors.New("invalid chat packet") + ValidationFailed error = bot.DisconnectErr(chat.TranslateMsg("multiplayer.disconnect.chat_validation_failed")) +) diff --git a/bot/msg/events.go b/bot/msg/events.go index f4e7648..5a73fa7 100644 --- a/bot/msg/events.go +++ b/bot/msg/events.go @@ -3,5 +3,5 @@ package msg import "github.com/Tnze/go-mc/chat" type EventsHandler struct { - PlayerChatMessage func(msg chat.Message) error + PlayerChatMessage func(msg chat.Message, validated bool) error } diff --git a/bot/playerlist/playerlist.go b/bot/playerlist/playerlist.go index e7f6a91..2b235ef 100644 --- a/bot/playerlist/playerlist.go +++ b/bot/playerlist/playerlist.go @@ -78,7 +78,12 @@ func (pl *PlayerList) handlePlayerInfoUpdatePacket(p pk.Packet) error { if _, err := chatSession.ReadFrom(r); err != nil { return err } - player.ChatSession = chatSession.Pointer() + if chatSession.Has { + player.ChatSession = chatSession.Pointer() + player.ChatSession.InitValidate() + } else { + player.ChatSession = nil + } } // update gamemode if action.Get(2) { diff --git a/chat/sign/cache.go b/chat/sign/cache.go new file mode 100644 index 0000000..cd3a56b --- /dev/null +++ b/chat/sign/cache.go @@ -0,0 +1,42 @@ +package sign + +import ( + "errors" +) + +type SignatureCache struct { + signatures [128]*Signature + signIndexes map[Signature]int + cachedBuffer []*Signature +} + +func NewSignatureCache() SignatureCache { + return SignatureCache{ + signIndexes: make(map[Signature]int), + } +} + +func (s *SignatureCache) PopOrInsert(self *Signature, lastSeen []*Signature) { + var tmp *Signature + s.cachedBuffer = s.cachedBuffer[:0] // clear buffer + if self != nil { + s.cachedBuffer = append(s.cachedBuffer, self) + } + for _, v := range lastSeen { + s.cachedBuffer = append(s.cachedBuffer, v) + } + for i := 0; i < len(s.cachedBuffer) && i < len(s.signatures); i++ { + v := s.cachedBuffer[i] + if i, ok := s.signIndexes[*v]; ok { + s.signatures[i] = nil + } + tmp, s.signatures[i] = s.signatures[i], v + s.signIndexes[*v] = i + if tmp != nil { + s.cachedBuffer = append(s.cachedBuffer, tmp) + delete(s.signIndexes, *tmp) + } + } +} + +var UncachedSignature = errors.New("uncached signature") diff --git a/bot/msg/cache_test.go b/chat/sign/cache_test.go similarity index 53% rename from bot/msg/cache_test.go rename to chat/sign/cache_test.go index a61bb7b..2126e59 100644 --- a/bot/msg/cache_test.go +++ b/chat/sign/cache_test.go @@ -1,36 +1,24 @@ -package msg +package sign import ( "crypto/rand" "testing" - - "github.com/Tnze/go-mc/chat/sign" ) func TestSignatureCache(t *testing.T) { - cache := newSignatureCache() - s1 := &sign.Signature{1} - s2 := &sign.Signature{2} - s3 := &sign.Signature{3} - s4 := &sign.Signature{4} + cache := NewSignatureCache() + s1 := &Signature{1} + s2 := &Signature{2} + s3 := &Signature{3} + s4 := &Signature{4} // t.Logf("%p, %p, %p, %p", s1, s2, s3, s4) - err := cache.popOrInsert(nil, []sign.PackedSignature{ - {Signature: s1}, - {Signature: s2}, - {Signature: s3}, - }) - if err != nil { - t.Fatal(err) - } + cache.PopOrInsert(nil, []*Signature{s1, s2, s3}) // cache: [s1, s2, s3, nil...] if cache.signatures[0] != s1 || cache.signatures[1] != s2 || cache.signatures[2] != s3 { t.Log(cache.signatures) t.Fatal("insert s1~3 error") } - err = cache.popOrInsert(s4, []sign.PackedSignature{{Signature: s3}}) - if err != nil { - t.Fatal(err) - } + cache.PopOrInsert(s4, []*Signature{s3}) // cache: [s4, s3, s1, s2, nil...] if cache.signatures[0] != s4 { t.Log(cache.signatures) @@ -47,36 +35,29 @@ func TestSignatureCache(t *testing.T) { } func TestSignatureCache_2(t *testing.T) { - cache := newSignatureCache() - signs := make([]sign.PackedSignature, len(cache.signatures)+5) + cache := NewSignatureCache() + signs := make([]*Signature, len(cache.signatures)+5) for i := range signs { - newSign := new(sign.Signature) - _, _ = rand.Read(newSign[:]) - signs[i] = sign.PackedSignature{Signature: newSign} + signs[i] = new(Signature) + _, _ = rand.Read(signs[i][:]) } - err := cache.popOrInsert(nil, signs[:len(cache.signatures)]) + cache.PopOrInsert(nil, signs[:len(cache.signatures)]) if !signatureEquals(cache.signatures[:], signs[:len(cache.signatures)]) { t.Fatal("insert error") } - if err != nil { - t.Fatal(err) - } insert2 := signs[len(cache.signatures)-5:] - err = cache.popOrInsert(nil, insert2) - if err != nil { - t.Fatal(err) - } + cache.PopOrInsert(nil, insert2) if !signatureEquals(cache.signatures[:10], insert2) { t.Fatal("insert and pop error") } } -func signatureEquals(a []*sign.Signature, b []sign.PackedSignature) bool { +func signatureEquals(a, b []*Signature) bool { if len(a) != len(b) { return false } for i := range a { - if a[i] != b[i].Signature { + if a[i] != b[i] { return false } } diff --git a/chat/sign/session.go b/chat/sign/session.go index 13c0e77..4164b1d 100644 --- a/chat/sign/session.go +++ b/chat/sign/session.go @@ -1,6 +1,8 @@ package sign import ( + "crypto/sha256" + "encoding/binary" "io" "github.com/google/uuid" @@ -11,40 +13,81 @@ import ( ) type Message struct { - Link struct { - Index int - Sender uuid.UUID - Session uuid.UUID - } - Signature []byte - PackedMessageBody + Prev Prev + Signature *Signature + *MessageBody Unsigned *chat.Message FilterMask } +type Prev struct { + Index int + Sender uuid.UUID + Session uuid.UUID +} + type Session struct { - sessionID uuid.UUID - publicKey user.PublicKey + SessionID uuid.UUID + PublicKey user.PublicKey + + valid bool + lastMsg *Message } func (s Session) WriteTo(w io.Writer) (n int64, err error) { - n1, err := pk.UUID(s.sessionID).WriteTo(w) + n1, err := pk.UUID(s.SessionID).WriteTo(w) if err != nil { return n1, err } - n2, err := s.publicKey.WriteTo(w) + n2, err := s.PublicKey.WriteTo(w) return n1 + n2, err } func (s *Session) ReadFrom(r io.Reader) (n int64, err error) { - n1, err := ((*pk.UUID)(&s.sessionID)).ReadFrom(r) + n1, err := ((*pk.UUID)(&s.SessionID)).ReadFrom(r) if err != nil { return n1, err } - n2, err := s.publicKey.ReadFrom(r) + n2, err := s.PublicKey.ReadFrom(r) return n1 + n2, err } -func (s *Session) Update(msg Message) bool { - panic("todo") +func (s *Session) InitValidate() { + s.valid = true + s.lastMsg = nil +} + +func (s *Session) VerifyAndUpdate(msg *Message) bool { + s.valid = s.valid && s.verifyHash(msg) && s.verifyChain(msg) + if s.valid { + s.lastMsg = msg + return true + } + return false +} + +func (s *Session) verifyHash(msg *Message) bool { + h := sha256.New() + // 1 + _ = binary.Write(h, binary.BigEndian, int32(1)) + // Prev + _, _ = h.Write(msg.Prev.Sender[:]) + _, _ = h.Write(msg.Prev.Session[:]) + _ = binary.Write(h, binary.BigEndian, msg.Prev.Index) + // Body + _ = binary.Write(h, binary.BigEndian, msg.Salt) + _ = binary.Write(h, binary.BigEndian, msg.Timestamp.Unix()) + content := []byte(msg.PlainMsg) + _ = binary.Write(h, binary.BigEndian, int32(len(content))) + _, _ = h.Write(content) + // Body.LastSeen + _ = binary.Write(h, binary.BigEndian, int32(len(msg.LastSeen))) + for _, v := range msg.LastSeen { + _, _ = h.Write((*v)[:]) + } + return s.PublicKey.VerifyMessage(h.Sum(nil), msg.Signature[:]) == nil +} + +func (s *Session) verifyChain(msg *Message) bool { + return s.lastMsg != nil && (msg.Prev.Index < s.lastMsg.Prev.Index || msg.Prev.Sender != s.lastMsg.Prev.Sender || msg.Prev.Session != s.lastMsg.Prev.Session) } diff --git a/chat/sign/sign.go b/chat/sign/sign.go index f266568..02c548a 100644 --- a/chat/sign/sign.go +++ b/chat/sign/sign.go @@ -9,6 +9,13 @@ import ( pk "github.com/Tnze/go-mc/net/packet" ) +type MessageBody struct { + PlainMsg string + Timestamp time.Time + Salt int64 + LastSeen []*Signature +} + type PackedMessageBody struct { PlainMsg string Timestamp time.Time @@ -37,6 +44,25 @@ func (m *PackedMessageBody) ReadFrom(r io.Reader) (n int64, err error) { return } +func (m *PackedMessageBody) Unpack(cache *SignatureCache) (*MessageBody, error) { + LastSeen := make([]*Signature, len(m.LastSeen)) + for i, v := range m.LastSeen { + if v.Signature != nil { + LastSeen[i] = v.Signature + } else if v.ID >= 0 && int(v.ID) < len(cache.signatures) { + LastSeen[i] = cache.signatures[v.ID] + } else { + return nil, UncachedSignature + } + } + return &MessageBody{ + PlainMsg: m.PlainMsg, + Timestamp: m.Timestamp, + Salt: m.Salt, + LastSeen: LastSeen, + }, nil +} + type HistoryMessage struct { Sender uuid.UUID Signature []byte diff --git a/examples/daze/daze.go b/examples/daze/daze.go index 8b143b7..bee9864 100644 --- a/examples/daze/daze.go +++ b/examples/daze/daze.go @@ -116,8 +116,12 @@ func onGameStart() error { return nil // if err isn't nil, HandleGame() will return it. } -func onPlayerMsg(msg chat.Message) error { - log.Printf("Player: %v", msg) +func onPlayerMsg(msg chat.Message, validated bool) error { + var prefix string + if !validated { + prefix = "[Not Secure] " + } + log.Printf("%sPlayer: %v", prefix, msg) return nil }