From f91424236b3e3b1086fc96bd3e97b46e329fe5b6 Mon Sep 17 00:00:00 2001 From: adman Date: Tue, 11 Nov 2025 09:29:32 +0100 Subject: [PATCH] ONLINE MODE --- cfb8.go | 139 +++++++++++++++++++ connection.go | 26 ++-- examples/status/main.go | 12 +- modules/login.go | 66 --------- modules/loginclient/login.go | 197 +++++++++++++++++++++++++++ modules/{ => statusclient}/status.go | 2 +- 6 files changed, 354 insertions(+), 88 deletions(-) create mode 100644 cfb8.go delete mode 100644 modules/login.go create mode 100644 modules/loginclient/login.go rename modules/{ => statusclient}/status.go (98%) diff --git a/cfb8.go b/cfb8.go new file mode 100644 index 0000000..c237e47 --- /dev/null +++ b/cfb8.go @@ -0,0 +1,139 @@ +package strom + +// https://github.com/Tnze/go-mc/blob/master/net/CFB8/cfb8.go +// Package CFB8 implements CFB8 block cipher mode of operation used by Minecraft protocol. + +import ( + "crypto/cipher" + "crypto/subtle" + "unsafe" +) + +type CFB8 struct { + c cipher.Block + blockSize int + ivPos int + iv []byte + de bool +} + +func NewCFB8Decrypt(c cipher.Block, iv []byte) *CFB8 { + return newCFB8(c, iv, true) +} + +func NewCFB8Encrypt(c cipher.Block, iv []byte) *CFB8 { + return newCFB8(c, iv, false) +} + +func newCFB8(c cipher.Block, iv []byte, de bool) *CFB8 { + cp := make([]byte, len(iv)*3) + copy(cp, iv) + return &CFB8{ + c: c, + blockSize: c.BlockSize(), + iv: cp, + de: de, + } +} + +func (cf *CFB8) XORKeyStream(dst, src []byte) { + if len(src) == 0 { + return + } + if len(dst) < len(src) { + panic("cfb8: output smaller than input") + } + + // If dst and src does not overlap in first block size, + // and the length of src is greater than 2*blockSize, + // we can use an optimized implementation. + if len(src) > cf.blockSize<<1 && + (uintptr(unsafe.Pointer(&dst[0]))+uintptr(cf.blockSize) <= uintptr(unsafe.Pointer(&src[0])) || + uintptr(unsafe.Pointer(&src[0]))+uintptr(len(src)) <= uintptr(unsafe.Pointer(&dst[0]))) { + // encrypt/decrypt first blockSize bytes + // After this, the IV will come to the same as + // the last blockSize of ciphertext, so + // we can reuse them without copy. + cf.xorKeyStream(dst, src[:cf.blockSize]) + var ciphertext []byte + if cf.de { + ciphertext = src + } else { + ciphertext = dst + } + dst = dst[cf.blockSize:] + src = src[cf.blockSize:] + iv := cf.iv + _ = iv[0] // bounds check hint to compiler; see golang.org/issue/14808 + var ( + i int + val byte + ) + dst = dst[:len(src)] + if cf.de && // and requires to be non-overlapping at all + uintptr(unsafe.Pointer(&dst[0])) <= uintptr(unsafe.Pointer(&src[len(src)-1])) && + uintptr(unsafe.Pointer(&src[0])) <= uintptr(unsafe.Pointer(&dst[len(dst)-1])) { + for i = 0; i < len(src)-cf.blockSize; i += 1 { + cf.c.Encrypt(dst[i:], ciphertext[i:]) + } + subtle.XORBytes(dst, src[:i], dst) + for ; i < len(src); i += 1 { + cf.c.Encrypt(iv, ciphertext[i:]) + dst[i] = src[i] ^ iv[0] + } + } else { + _ = ciphertext[len(src)] + for i, val = range src { + cf.c.Encrypt(iv, ciphertext[i:]) + dst[i] = val ^ iv[0] + } + // for-range does not increase i in the last loop, + // compared to the classic for clause + i += 1 + } + // copy the current IV for next operation + copy(iv, ciphertext[i:i+cf.blockSize]) + cf.ivPos = 0 + return + } + + cf.xorKeyStream(dst, src) +} + +func (cf *CFB8) xorKeyStream(dst, src []byte) { + dst = dst[:len(src)] // remove bounds check in loop + for i, val := range src { + posPlusBlockSize := cf.ivPos + cf.blockSize + // fast mod; 2*blockSize must be a non-negative integer power of 2 + tempPos := posPlusBlockSize & (cf.blockSize<<1 - 1) + // reuse space to store encrypted block + cf.c.Encrypt(cf.iv[tempPos:], cf.iv[cf.ivPos:]) + // Only the first byte of the encrypted block is used + // for encryption/decryption, other bytes are ignored. + val ^= cf.iv[tempPos] + + if cf.ivPos == cf.blockSize<<1 { + // bound reached; move to next round for next operation + // copy next block to the start of the ring buffer + copy(cf.iv, cf.iv[cf.ivPos+1:]) + // insert the encrypted byte to the end of IV + if cf.de { + cf.iv[cf.blockSize-1] = src[i] + } else { + cf.iv[cf.blockSize-1] = val + } + cf.ivPos = 0 + } else { + // insert the encrypted byte to the end of IV + if cf.de { + cf.iv[posPlusBlockSize] = src[i] + } else { + cf.iv[posPlusBlockSize] = val + } + // move to next block + cf.ivPos += 1 + } + + dst[i] = val + } +} diff --git a/connection.go b/connection.go index c18ae20..4911b16 100644 --- a/connection.go +++ b/connection.go @@ -44,6 +44,8 @@ func (a Actor) ReceiveDirection() queser.Direction { type Connection struct { net.Conn + R io.Reader + W io.Writer CompressionThreshold int32 State queser.State Actor Actor @@ -69,7 +71,7 @@ func (c *Connection) Send(packet any) (err error) { if err != nil { return } - _, err = zlib.NewWriter(packetBuffer).Write(packetBytes) + _, err = zlib.NewWriter(packetBuffer).Write(rawPacketBuffer.Bytes()) if err != nil { return } @@ -78,29 +80,30 @@ func (c *Connection) Send(packet any) (err error) { if err != nil { return } - _, err = packetBuffer.Write(packetBytes) + _, err = packetBuffer.Write(rawPacketBuffer.Bytes()) if err != nil { return } } + packetBytes = packetBuffer.Bytes() } else { packetBytes = rawPacketBuffer.Bytes() } - err = queser.VarInt(len(packetBytes)).Encode(c) + err = queser.VarInt(len(packetBytes)).Encode(c.W) if err != nil { return } - _, err = c.Write(packetBytes) + _, err = c.W.Write(packetBytes) return } func (c *Connection) Receive() (packet any, err error) { var rawPacketLen queser.VarInt - rawPacketLen, err = rawPacketLen.Decode(c) + rawPacketLen, err = rawPacketLen.Decode(c.R) if err != nil { return } - rawPacketBytes, err := io.ReadAll(io.LimitReader(c, int64(rawPacketLen))) + rawPacketBytes, err := io.ReadAll(io.LimitReader(c.R, int64(rawPacketLen))) if err != nil { return } @@ -143,14 +146,7 @@ func ClientConnect(addr string) (ret *Connection, err error) { if err != nil { return } - return -} - -func ConnectAndStart(addr string, inst HandlerInst) (ret *Connection, err error) { - ret, err = ClientConnect(addr) - if err != nil { - return - } - err = ret.Start(inst) + ret.R = ret.Conn + ret.W = ret.Conn return } diff --git a/examples/status/main.go b/examples/status/main.go index 0eda6a5..3a625b4 100644 --- a/examples/status/main.go +++ b/examples/status/main.go @@ -2,7 +2,7 @@ package main import ( "strom" - "strom/modules" + "strom/modules/loginclient" "github.com/google/uuid" ) @@ -16,11 +16,11 @@ func must(err error) { func main() { c, err := strom.ClientConnect("127.0.0.1:25566") must(err) - err = c.Start(&modules.LoginClient{ - Account: modules.Account{ - Username: "Admin_rizz", - UUID: uuid.UUID{}, - Token: "", + err = c.Start(&loginclient.LoginClient{ + Account: loginclient.Account{ + Username: "449stan_Ytb", + Uuid: uuid.MustParse("caec076c80c34637a31b44874ffa009d"), + Token: "eyJraWQiOiIwNDkxODEiLCJhbGciOiJSUzI1NiJ9.eyJ4dWlkIjoiMjUzNTQwOTA1OTUxMzgxOSIsImFnZyI6IkFkdWx0Iiwic3ViIjoiMjY4OGQ0ZTktZmZiZi00N2FkLTlkYWYtZDU3ZWY5Yzg5YWE0IiwiYXV0aCI6IlhCT1giLCJucyI6ImRlZmF1bHQiLCJyb2xlcyI6W10sImlzcyI6ImF1dGhlbnRpY2F0aW9uIiwiZmxhZ3MiOlsib3JkZXJzXzIwMjIiLCJtdWx0aXBsYXllciIsInR3b2ZhY3RvcmF1dGgiLCJtc2FtaWdyYXRpb25fc3RhZ2U0Il0sInByb2ZpbGVzIjp7Im1jIjoiY2FlYzA3NmMtODBjMy00NjM3LWEzMWItNDQ4NzRmZmEwMDlkIn0sInBsYXRmb3JtIjoiUENfTEFVTkNIRVIiLCJwZmQiOlt7InR5cGUiOiJtYyIsImlkIjoiY2FlYzA3NmMtODBjMy00NjM3LWEzMWItNDQ4NzRmZmEwMDlkIiwibmFtZSI6IjQ0OXN0YW5fWXRiIn1dLCJuYmYiOjE3NjI4NDc4OTgsImV4cCI6MTc2MjkzNDI5OCwiaWF0IjoxNzYyODQ3ODk4LCJhaWQiOiJjMzZhOWZiNi00ZjJhLTQxZmYtOTBiZC1hZTdjYzkyMDMxZWIifQ.bcyq-FrP4XXESyKBr4TFstsjw8tgjY5cz_p7ansohPV_VfbKqBeLV-E1XYySwKj6BRuc0dAuW6nGdwE9qlv94tRtapVb1q1W__-GguVWOD16TaEi6dmzDyw8--lpI6eL657lPkzFaVeNrSHNPCPn0ltGcZzlOv1XYJwZM8D1yj57x7aXaOMmJqLY8R5zCgfWp_TnGPjJrbd_gV-N5wP9z_vL6jQKj5bvaTkYqItLq5tTFWbX1wAoeSV47D3mtZfx2N4rGgeUSQ1bYanUZ_eMuH6hSwrwxzqn_-HwN2PA7j0VFVUOu54FRqJbw8D4H6tAf3SY1xIjFC5GUu22IbVP3Q", }, Connection: c, }) diff --git a/modules/login.go b/modules/login.go deleted file mode 100644 index c6ca1d0..0000000 --- a/modules/login.go +++ /dev/null @@ -1,66 +0,0 @@ -package modules - -import ( - "fmt" - "strconv" - "strings" - "strom" - - "github.com/admin-else/queser" - "github.com/admin-else/queser/data" - "github.com/admin-else/queser/generated/v1_21_8" - "github.com/google/uuid" -) - -type Account struct { - Username string - UUID uuid.UUID - Token string -} -type LoginClient struct { - *strom.Connection - Account Account -} - -func (s *LoginClient) Default(event any) (err error) { - err = fmt.Errorf("unexpected event: %T%v", event, event) - return -} - -func (s *LoginClient) OnStart(_ strom.OnStart) (err error) { - parts := strings.Split(s.RemoteAddr().String(), ":") - versionData, err := data.LookUpProtocolVersionByName(s.Version) - if err != nil { - return - } - port, err := strconv.Atoi(parts[1]) - if err != nil { - return - } - err = s.Send(v1_21_8.HandshakingToServerPacketSetProtocol{ - ProtocolVersion: queser.VarInt(versionData.Version), - ServerHost: parts[0], - ServerPort: uint16(port), - NextState: queser.VarInt(queser.Login), - }) - if err != nil { - return - } - s.State = queser.Login - err = s.Send(v1_21_8.LoginToServerPacketLoginStart{Username: s.Account.Username, PlayerUUID: s.Account.UUID}) - return -} - -func (s *LoginClient) OnCompress(compress v1_21_8.LoginToClientPacketCompress) (err error) { - s.CompressionThreshold = int32(compress.Threshold) - return -} - -func (s *LoginClient) OnSuccess(success v1_21_8.LoginToClientPacketSuccess) (err error) { - err = s.Send(v1_21_8.LoginToServerPacketLoginAcknowledged{}) - if err != nil { - return - } - s.State = queser.Configuration - return -} diff --git a/modules/loginclient/login.go b/modules/loginclient/login.go new file mode 100644 index 0000000..a83bca4 --- /dev/null +++ b/modules/loginclient/login.go @@ -0,0 +1,197 @@ +package loginclient + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "strom" + + "github.com/admin-else/queser" + "github.com/admin-else/queser/data" + "github.com/admin-else/queser/generated/v1_21_8" + "github.com/google/uuid" +) + +type Account struct { + Username string + Uuid uuid.UUID + Token string +} +type LoginClient struct { + *strom.Connection + Account Account +} + +func (s *LoginClient) Default(event any) (err error) { + err = fmt.Errorf("unexpected event: %T%v", event, event) + return +} + +func (s *LoginClient) OnStart(_ strom.OnStart) (err error) { + parts := strings.Split(s.RemoteAddr().String(), ":") + versionData, err := data.LookUpProtocolVersionByName(s.Version) + if err != nil { + return + } + port, err := strconv.Atoi(parts[1]) + if err != nil { + return + } + err = s.Send(v1_21_8.HandshakingToServerPacketSetProtocol{ + ProtocolVersion: queser.VarInt(versionData.Version), + ServerHost: parts[0], + ServerPort: uint16(port), + NextState: queser.VarInt(queser.Login), + }) + if err != nil { + return + } + s.State = queser.Login + err = s.Send(v1_21_8.LoginToServerPacketLoginStart{Username: s.Account.Username, PlayerUUID: s.Account.Uuid}) + return +} + +func (s *LoginClient) OnCompress(compress v1_21_8.LoginToClientPacketCompress) (err error) { + s.CompressionThreshold = int32(compress.Threshold) + return +} + +func twosComplement(p []byte) []byte { + carry := true + for i := len(p) - 1; i >= 0; i-- { + p[i] = byte(^p[i]) + if carry { + carry = p[i] == 0xff + p[i]++ + } + } + return p +} + +// AuthDigest stolen from https://gist.github.com/toqueteos/5372776 +func AuthDigest(elems ...[]byte) string { + h := sha1.New() + for _, elem := range elems { + h.Write(elem) + } + hash := h.Sum(nil) + + negative := (hash[0] & 0x80) == 0x80 + if negative { + hash = twosComplement(hash) + } + + res := strings.TrimLeft(hex.EncodeToString(hash), "0") + if negative { + res = "-" + res + } + + return res +} + +func JoinServerAPI(a Account, serverId string) (err error) { + var body []byte + body, err = json.Marshal(struct { + AccessToken string `json:"accessToken"` + SelectedProfile string `json:"selectedProfile"` + ServerId string `json:"serverId"` + }{ + AccessToken: a.Token, + SelectedProfile: strings.ReplaceAll(a.Uuid.String(), "-", ""), + ServerId: serverId, + }) + if err != nil { + return + } + var resp *http.Response + resp, err = http.Post("https://sessionserver.mojang.com/session/minecraft/join", "application/json", bytes.NewReader(body)) + if err != nil { + return + } + if resp.StatusCode != 204 { + body, err = io.ReadAll(resp.Body) + if err != nil { + return + } + err = fmt.Errorf("bad status code %v body %v", resp.StatusCode, string(body)) + return + } + return +} + +func (s *LoginClient) OnEncrypt(packet v1_21_8.LoginToClientPacketEncryptionBegin) (err error) { + sharedSecret := make([]byte, 16) + _, _ = rand.Read(sharedSecret) //never fails + + if packet.ShouldAuthenticate { + if s.Account.Token == "" { + err = fmt.Errorf("the account has no token so we cant join online servers") + return + } + serverId := AuthDigest([]byte(packet.ServerId), sharedSecret, packet.PublicKey) + err = JoinServerAPI(s.Account, serverId) + if err != nil { + return + } + } + + pubAny, err := x509.ParsePKIXPublicKey(packet.PublicKey) + if err != nil { + return + } + pub, ok := pubAny.(*rsa.PublicKey) + if !ok { + err = fmt.Errorf("public key is not rsa") + return + } + verifyTokenEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, packet.VerifyToken) + if err != nil { + return + } + + sharedSecretEnc, err := rsa.EncryptPKCS1v15(rand.Reader, pub, sharedSecret) + if err != nil { + return + } + err = s.Send(v1_21_8.LoginToServerPacketEncryptionBegin{SharedSecret: sharedSecretEnc, VerifyToken: verifyTokenEnc}) + if err != nil { + return + } + + var b cipher.Block + b, err = aes.NewCipher(sharedSecret) + if err != nil { + return + } + s.R = cipher.StreamReader{ + S: strom.NewCFB8Decrypt(b, sharedSecret), + R: s.Conn, + } + s.W = cipher.StreamWriter{ + S: strom.NewCFB8Encrypt(b, sharedSecret), + W: s.Conn, + } + return + +} + +func (s *LoginClient) OnSuccess(success v1_21_8.LoginToClientPacketSuccess) (err error) { + fmt.Println("login success", success) + err = s.Send(v1_21_8.LoginToServerPacketLoginAcknowledged{}) + if err != nil { + return + } + s.State = queser.Configuration + return +} diff --git a/modules/status.go b/modules/statusclient/status.go similarity index 98% rename from modules/status.go rename to modules/statusclient/status.go index eac859b..454477e 100644 --- a/modules/status.go +++ b/modules/statusclient/status.go @@ -1,4 +1,4 @@ -package modules +package statusclient import ( "fmt"