This commit is contained in:
adman
2026-07-16 14:02:28 +02:00
parent 5c5c391aa6
commit 7baa9990cb
7 changed files with 267 additions and 10 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"log/slog"
"os" "os"
"github.com/admin-else/strom/mc/api" "github.com/admin-else/strom/mc/api"
@@ -52,7 +53,7 @@ func Run(args []string) (err error) {
return return
} }
} else { } else {
err = PleaseProvideArgumentErr slog.Info("account", "name", a.Name, "uuid", a.Uuid)
return return
} }
err = json.NewEncoder(os.Stdout).Encode(data) err = json.NewEncoder(os.Stdout).Encode(data)
+11
View File
@@ -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
}
+1 -1
View File
@@ -77,7 +77,7 @@ func Help(args []string) (err error) {
func mainE() (err error) { func mainE() (err error) {
subcommands["help"] = Help subcommands["help"] = Help
args := os.Args args := os.Args
args = []string{"TEST", "messenger", "-addr", "localhost"} //args = []string{"TEST", "messenger", "-addr", "localhost"}
if len(args) < 2 { if len(args) < 2 {
err = ExpectedASubcommandErr err = ExpectedASubcommandErr
+24 -5
View File
@@ -3,7 +3,9 @@ package messenger
import ( import (
"flag" "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/client"
"github.com/admin-else/strom/mc/event" "github.com/admin-else/strom/mc/event"
"github.com/admin-else/strom/mc/proto" "github.com/admin-else/strom/mc/proto"
@@ -13,11 +15,12 @@ import (
var ( var (
cmd = flag.NewFlagSet("status", flag.ContinueOnError) cmd = flag.NewFlagSet("status", flag.ContinueOnError)
connectToFlag = cmd.String("addr", "", "server address to connect to") 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 { type Messenger struct {
*proto.Conn *proto.Conn
chat *chat.Module
} }
func (m *Messenger) OnChat(e *v1_21_11.PlayToClientPacketPlayerChat) (err error) { 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) { 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) { func Run(args []string) (err error) {
err = cmd.Parse(args) 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 { if err != nil {
return return
} }
defer c.Close() 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) event.StartListingStdin(m.Loop)
m.Register(m.OnStdin) m.Register(m.OnStdin)
m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem) m.RegisterUntilLatest(m.OnChat, m.OnChatUnsigned, m.OnChatSystem)
keepalive.Start(m.Conn)
return m.StartConn() return m.StartConn()
} }
+207
View File
@@ -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
}
+19
View File
@@ -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)
}
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"github.com/google/uuid" "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 // 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 := md5.New()
h.Reset() h.Reset()
h.Write(space) //nolint:errcheck h.Write(space) //nolint:errcheck
@@ -22,5 +22,5 @@ func UUIDv3(space []byte, data []byte) uuid.UUID {
} }
func FromOfflinePlayer(displayName string) uuid.UUID { func FromOfflinePlayer(displayName string) uuid.UUID {
return UUIDv3([]byte("OfflinePlayer:"), []byte(displayName)) return uuidV3([]byte("OfflinePlayer:"), []byte(displayName))
} }