From 7baa9990cbe4fcd699ec13a8ef3eada86640aa5b Mon Sep 17 00:00:00 2001 From: adman Date: Thu, 16 Jul 2026 14:02:28 +0200 Subject: [PATCH] yes222 --- cmd/strom/api/main.go | 3 +- cmd/strom/cmd_util/account.go | 11 ++ cmd/strom/main.go | 2 +- cmd/strom/messenger/main.go | 29 ++++- mc/bot/chat/mod.go | 207 ++++++++++++++++++++++++++++++++++ mc/bot/keepalive/mod.go | 19 ++++ mc/crypto/offline.go | 6 +- 7 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 cmd/strom/cmd_util/account.go create mode 100644 mc/bot/chat/mod.go create mode 100644 mc/bot/keepalive/mod.go diff --git a/cmd/strom/api/main.go b/cmd/strom/api/main.go index 6926124..c397dfd 100644 --- a/cmd/strom/api/main.go +++ b/cmd/strom/api/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "flag" + "log/slog" "os" "github.com/admin-else/strom/mc/api" @@ -52,7 +53,7 @@ func Run(args []string) (err error) { return } } else { - err = PleaseProvideArgumentErr + slog.Info("account", "name", a.Name, "uuid", a.Uuid) return } err = json.NewEncoder(os.Stdout).Encode(data) diff --git a/cmd/strom/cmd_util/account.go b/cmd/strom/cmd_util/account.go new file mode 100644 index 0000000..6488f6d --- /dev/null +++ b/cmd/strom/cmd_util/account.go @@ -0,0 +1,11 @@ +package cmd_util + +import "github.com/admin-else/strom/mc/api" + +func Account(s string) (a *api.Account, err error) { + if len(s) > 16 { + return api.NewAccountFromYGG(s) + } + a = api.NewOfflineAccount(s) + return +} diff --git a/cmd/strom/main.go b/cmd/strom/main.go index 9cd77f1..5047a0d 100644 --- a/cmd/strom/main.go +++ b/cmd/strom/main.go @@ -77,7 +77,7 @@ func Help(args []string) (err error) { func mainE() (err error) { subcommands["help"] = Help args := os.Args - args = []string{"TEST", "messenger", "-addr", "localhost"} + //args = []string{"TEST", "messenger", "-addr", "localhost"} if len(args) < 2 { err = ExpectedASubcommandErr diff --git a/cmd/strom/messenger/main.go b/cmd/strom/messenger/main.go index ab108b4..3db3528 100644 --- a/cmd/strom/messenger/main.go +++ b/cmd/strom/messenger/main.go @@ -3,7 +3,9 @@ package messenger import ( "flag" - "github.com/admin-else/strom/mc/api" + "github.com/admin-else/strom/cmd/strom/cmd_util" + "github.com/admin-else/strom/mc/bot/chat" + "github.com/admin-else/strom/mc/bot/keepalive" "github.com/admin-else/strom/mc/client" "github.com/admin-else/strom/mc/event" "github.com/admin-else/strom/mc/proto" @@ -13,11 +15,12 @@ import ( var ( cmd = flag.NewFlagSet("status", flag.ContinueOnError) connectToFlag = cmd.String("addr", "", "server address to connect to") - nameFlag = cmd.String("name", "strom_messenger", "the name to use") + accFlag = cmd.String("acc", "strom_messenger", "the name to use if longer will be treated as ygg token") ) type Messenger struct { *proto.Conn + chat *chat.Module } func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) { @@ -36,22 +39,38 @@ func (m *Messenger) OnChatSystem(e *v1_21_11.PlayToClientPacketSystemChat) (err } func (m *Messenger) OnStdin(e event.Stdin) (err error) { - return m.Send(&v1_21_11.PlayToServerPacketChatMessage{Message: e.Val}) + return m.chat.SendMessage(e.Val) } func Run(args []string) (err error) { err = cmd.Parse(args) + if err != nil { + return + } - c, err := client.Login(*connectToFlag, api.NewOfflineAccount(*nameFlag)) + acc, err := cmd_util.Account(*accFlag) + if err != nil { + return + } + c, err := client.Login(*connectToFlag, acc) if err != nil { return } defer c.Close() - m := Messenger{Conn: c} + chatMod, err := chat.Start(c, acc) + if err != nil { + return + } + m := &Messenger{ + Conn: c, + chat: chatMod, + } + event.StartListingStdin(m.Loop) m.Register(m.OnStdin) m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem) + keepalive.Start(m.Conn) return m.StartConn() } diff --git a/mc/bot/chat/mod.go b/mc/bot/chat/mod.go new file mode 100644 index 0000000..79bee4f --- /dev/null +++ b/mc/bot/chat/mod.go @@ -0,0 +1,207 @@ +package chat + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/binary" + "encoding/pem" + "fmt" + "time" + + "github.com/admin-else/strom/mc/api" + "github.com/admin-else/strom/mc/proto" + "github.com/admin-else/strom/mc/proto_generated/v1_21_11" + "github.com/google/uuid" +) + +type Module struct { + *proto.Conn + + Account *api.Account + + // Chat signing state + canSign bool + privKey *rsa.PrivateKey + sessionUUID uuid.UUID + msgIndex int32 +} + +// Start creates a chat module for the connection. If acc is an online account, +// it fetches the profile key pair from Mojang and enables signed chat. +func Start(c *proto.Conn, acc *api.Account) (*Module, error) { + m := &Module{Conn: c, Account: acc} + if acc != nil && acc.Ygg != "" { + if err := m.initSigning(acc); err != nil { + return nil, err + } + } + return m, nil +} + +func (m *Module) initSigning(acc *api.Account) (err error) { + keys, err := acc.FetchKeys() + if err != nil { + return fmt.Errorf("fetch profile keys: %w", err) + } + + priv, err := parsePrivateKey(keys.KeyPair.PrivateKey) + if err != nil { + return fmt.Errorf("parse private key: %w", err) + } + + m.privKey = priv + m.sessionUUID = uuid.New() + + pubDer, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + return fmt.Errorf("marshal public key: %w", err) + } + + sigBytes, err := base64.StdEncoding.DecodeString(keys.PublicKeySignatureV2) + if err != nil { + return fmt.Errorf("decode key signature: %w", err) + } + + if err = m.Conn.Send(&v1_21_11.PlayToServerPacketChatSessionUpdate{ + SessionUUID: m.sessionUUID, + ExpireTime: keys.ExpiresAt.UnixMilli(), + PublicKey: v1_21_11.ByteArray{Val: pubDer}, + Signature: v1_21_11.ByteArray{Val: sigBytes}, + }); err != nil { + return fmt.Errorf("send chat session update: %w", err) + } + + m.canSign = true + return nil +} + +// SendMessage sends a chat message. If signing is enabled, the message is signed; +// otherwise it is sent unsigned. +func (m *Module) SendMessage(message string) (err error) { + if m.canSign { + return m.sendSigned(message) + } + return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{Message: message}) +} + +func (m *Module) sendSigned(message string) (err error) { + now := time.Now() + + saltBytes := make([]byte, 8) + if _, err = rand.Read(saltBytes); err != nil { + return + } + salt := int64(binary.BigEndian.Uint64(saltBytes)) + + timestamp := now.UnixMilli() + + payload, err := m.buildSignaturePayload(message, salt, timestamp) + if err != nil { + return + } + + hashed := sha256.Sum256(payload) + sigBytes, err := rsa.SignPKCS1v15(rand.Reader, m.privKey, crypto.SHA256, hashed[:]) + if err != nil { + return + } + if len(sigBytes) != 256 { + return fmt.Errorf("unexpected signature length: %d", len(sigBytes)) + } + var signature [256]byte + copy(signature[:], sigBytes) + + m.msgIndex++ + + return m.Conn.Send(&v1_21_11.PlayToServerPacketChatMessage{ + Message: message, + Timestamp: timestamp, + Salt: salt, + Signature: &signature, + Offset: 0, + Acknowledged: [3]byte{}, + Checksum: 0, + }) +} + +func (m *Module) buildSignaturePayload(content string, salt, timestampMillis int64) ([]byte, error) { + buf := make([]byte, 0, 64+len(content)) + w := func(b []byte) { buf = append(buf, b...) } + + // Header + w(int32Bytes(1)) + + // SignedMessageLink: sender UUID, session UUID, index + w(uuidBytes(m.Account.Uuid)) + w(uuidBytes(m.sessionUUID)) + w(int32Bytes(m.msgIndex)) + + // SignedMessageBody: salt, timestamp seconds, content + w(int64Bytes(uint64(salt))) + w(int64Bytes(uint64(timestampMillis / 1000))) + contentBytes := []byte(content) + w(int32Bytes(int32(len(contentBytes)))) + w(contentBytes) + + // LastSeenMessages: empty + w(int32Bytes(0)) + + return buf, nil +} + +func parsePrivateKey(s string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(s)) + if block != nil { + k, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + k, err = x509.ParsePKCS1PrivateKey(block.Bytes) + } + if err != nil { + return nil, err + } + if key, ok := k.(*rsa.PrivateKey); ok { + return key, nil + } + return nil, fmt.Errorf("not an RSA private key") + } + + // Some implementations return raw base64 DER instead of PEM + raw, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + k, err := x509.ParsePKCS8PrivateKey(raw) + if err != nil { + k, err = x509.ParsePKCS1PrivateKey(raw) + } + if err != nil { + return nil, err + } + key, ok := k.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("not an RSA private key") + } + return key, nil +} + +func int32Bytes(v int32) []byte { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, uint32(v)) + return b +} + +func int64Bytes(v uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, v) + return b +} + +func uuidBytes(u uuid.UUID) []byte { + b := make([]byte, 16) + copy(b, u[:]) + return b +} diff --git a/mc/bot/keepalive/mod.go b/mc/bot/keepalive/mod.go new file mode 100644 index 0000000..b49578d --- /dev/null +++ b/mc/bot/keepalive/mod.go @@ -0,0 +1,19 @@ +package keepalive + +import ( + "github.com/admin-else/strom/mc/proto" + "github.com/admin-else/strom/mc/proto_generated/v1_21_8" +) + +type mod struct { + *proto.Conn +} + +func (m *mod) onKeepAlive(p *v1_21_8.PlayToClientPacketKeepAlive) (err error) { + return m.Send(&v1_21_8.PlayToServerPacketKeepAlive{KeepAliveId: p.KeepAliveId}) +} + +func Start(c *proto.Conn) { + m := &mod{Conn: c} + m.RegisterUntilLatest(m.onKeepAlive) +} diff --git a/mc/crypto/offline.go b/mc/crypto/offline.go index c1a7719..b074c19 100644 --- a/mc/crypto/offline.go +++ b/mc/crypto/offline.go @@ -6,9 +6,9 @@ import ( "github.com/google/uuid" ) -// UUIDv3 Variation of google.uuid.NewHash +// uuidV3 Variation of google.uuid.NewHash // We need this because we want a 14-byte namespace and Google forces 16 -func UUIDv3(space []byte, data []byte) uuid.UUID { +func uuidV3(space []byte, data []byte) uuid.UUID { h := md5.New() h.Reset() h.Write(space) //nolint:errcheck @@ -22,5 +22,5 @@ func UUIDv3(space []byte, data []byte) uuid.UUID { } func FromOfflinePlayer(displayName string) uuid.UUID { - return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName)) + return uuidV3([]byte("OfflinePlayer:"), []byte(displayName)) }