diff --git a/bot/login.go b/bot/login.go index e973b62..3e51a0e 100644 --- a/bot/login.go +++ b/bot/login.go @@ -13,6 +13,7 @@ import ( "encoding/json" "encoding/pem" "fmt" + "github.com/Tnze/go-mc/data/packetid" "io" "net/http" "strings" @@ -217,7 +218,7 @@ func genEncryptionKeyResponse(shareSecret, publicKey, verifyToken []byte, keyPai return erp, err } return pk.Marshal( - 0x01, + packetid.LoginEncryptionResponse, pk.ByteArray(cryptPK), pk.Boolean(false), l, @@ -230,7 +231,7 @@ func genEncryptionKeyResponse(shareSecret, publicKey, verifyToken []byte, keyPai return erp, err } return pk.Marshal( - 0x01, + packetid.LoginEncryptionResponse, pk.ByteArray(cryptPK), pk.Boolean(true), pk.ByteArray(verifyT), diff --git a/bot/mcbot.go b/bot/mcbot.go index 6ccf1d6..7237d60 100644 --- a/bot/mcbot.go +++ b/bot/mcbot.go @@ -121,7 +121,7 @@ func (c *Client) join(ctx context.Context, d *mcnet.Dialer, addr string) error { } return nil - case packetid.SetCompression: //Set Compression + case packetid.LoginCompression: //Set Compression var threshold pk.VarInt if err := p.Scan(&threshold); err != nil { return LoginErr{"compression", err} diff --git a/data/packetid/packetid.go b/data/packetid/packetid.go index 3431511..97f39f9 100644 --- a/data/packetid/packetid.go +++ b/data/packetid/packetid.go @@ -5,7 +5,7 @@ const ( LoginDisconnect = iota LoginEncryptionRequest LoginSuccess - SetCompression + LoginCompression LoginPluginRequest ) diff --git a/server/auth/auth.go b/server/auth/auth.go index 311e1b7..84c3e76 100644 --- a/server/auth/auth.go +++ b/server/auth/auth.go @@ -2,20 +2,22 @@ package auth import ( "bytes" + "crypto" "crypto/aes" "crypto/rand" "crypto/rsa" "crypto/sha1" + "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" - "github.com/Tnze/go-mc/data/packetid" "io/ioutil" "net/http" "strings" + "github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net/CFB8" pk "github.com/Tnze/go-mc/net/packet" @@ -25,7 +27,7 @@ import ( const verifyTokenLen = 16 //Encrypt a connection, with authentication -func Encrypt(conn *net.Conn, name string) (*Resp, error) { +func Encrypt(conn *net.Conn, name string, profilePubKey *rsa.PublicKey) (*Resp, error) { //generate keys key, err := rsa.GenerateKey(rand.Reader, 1024) if err != nil { @@ -38,32 +40,18 @@ func Encrypt(conn *net.Conn, name string) (*Resp, error) { } //encryption request - VT1, err := encryptionRequest(conn, publicKey) + nonce, err := encryptionRequest(conn, publicKey) if err != nil { return nil, err } //encryption response - ESharedSecret, EVerifyToken, err := encryptionResponse(conn) + SharedSecret, err := encryptionResponse(conn, profilePubKey, nonce, key) if err != nil { return nil, err } //encryption the connection - SharedSecret, err := rsa.DecryptPKCS1v15(rand.Reader, key, ESharedSecret) - if err != nil { - return nil, err - } - VT2, err := rsa.DecryptPKCS1v15(rand.Reader, key, EVerifyToken) - if err != nil { - return nil, err - } - - //confirm to verify token - if !bytes.Equal(VT1, VT2) { - return nil, errors.New("verify token not match") - } - block, err := aes.NewCipher(SharedSecret) if err != nil { return nil, errors.New("load aes encryption key fail") @@ -71,8 +59,8 @@ func Encrypt(conn *net.Conn, name string) (*Resp, error) { conn.SetCipher( //启用加密 CFB8.NewCFB8Encrypt(block, SharedSecret), - CFB8.NewCFB8Decrypt(block, SharedSecret)) - + CFB8.NewCFB8Decrypt(block, SharedSecret), + ) hash := authDigest("", SharedSecret, publicKey) resp, err := authentication(name, hash) //auth if err != nil { @@ -97,22 +85,64 @@ func encryptionRequest(conn *net.Conn, publicKey []byte) ([]byte, error) { return verifyToken[:], err } -func encryptionResponse(conn *net.Conn) ([]byte, []byte, error) { +func encryptionResponse(conn *net.Conn, profilePubKey *rsa.PublicKey, nonce []byte, key *rsa.PrivateKey) ([]byte, error) { var p pk.Packet err := conn.ReadPacket(&p) if err != nil { - return nil, nil, err + return nil, err } if p.ID != packetid.LoginEncryptionResponse { - return nil, nil, fmt.Errorf("0x%02X is not Encryption Response", p.ID) + return nil, fmt.Errorf("0x%02X is not Encryption Response", p.ID) } - var SharedSecret, VerifyToken pk.ByteArray - if err = p.Scan(&SharedSecret, &VerifyToken); err != nil { - return nil, nil, err + r := bytes.NewReader(p.Data) + var ESharedSecret pk.ByteArray + if _, err = ESharedSecret.ReadFrom(r); err != nil { + return nil, err + } + var isNonce pk.Boolean + if _, err = isNonce.ReadFrom(r); err != nil { + return nil, err + } + if isNonce { + var nonce2 pk.ByteArray + _, err = nonce2.ReadFrom(r) + if err != nil { + return nil, err + } + + nonce2, err = rsa.DecryptPKCS1v15(rand.Reader, key, nonce2) + if err != nil { + return nil, err + } + if !bytes.Equal(nonce, nonce2) { + return nil, errors.New("nonce not match") + } + + } else { + var salt pk.Long + var signature pk.ByteArray + _, err = pk.Tuple{&salt, &signature}.ReadFrom(r) + if err != nil { + return nil, err + } + + hash := sha256.New() + unwrap(hash.Write(nonce)) + unwrap(salt.WriteTo(hash)) + err := rsa.VerifyPKCS1v15(profilePubKey, crypto.SHA256, hash.Sum(nil), signature) + if err != nil { + return nil, err + } } - return SharedSecret, VerifyToken, nil + //confirm to verify token + SharedSecret, err := rsa.DecryptPKCS1v15(rand.Reader, key, ESharedSecret) + if err != nil { + return nil, err + } + + return SharedSecret, nil } func authentication(name, hash string) (*Resp, error) { @@ -178,9 +208,11 @@ func twosComplement(p []byte) []byte { type Resp struct { Name string ID uuid.UUID - Properties [1]struct { - Name, Value, Signature string - } + Properties []Property +} + +type Property struct { + Name, Value, Signature string } //Texture includes player's skin and cape diff --git a/server/auth/validator.go b/server/auth/validator.go new file mode 100644 index 0000000..4a0dc23 --- /dev/null +++ b/server/auth/validator.go @@ -0,0 +1,92 @@ +package auth + +import ( + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + _ "embed" + "encoding/base64" + "io" +) + +//go:embed yggdrasil_session_pubkey.der +var pubKeyBytes []byte +var pubKey = unwrap(x509.ParsePKIXPublicKey(pubKeyBytes)).(*rsa.PublicKey) + +// VerifySignature has the same functional as +// net.minecraft.world.entity.player.ProfilePublicKey.Data#validateSignature +func VerifySignature(profilePubKey, signature []byte) bool { + hash := sha256.New() + unwrap(hash.Write([]byte("-----BEGIN RSA PRIVATE KEY-----\n"))) + breaker := lineBreaker{out: hash} + enc := base64.NewEncoder(base64.StdEncoding, &breaker) + unwrap(enc.Write(profilePubKey)) + must(enc.Close()) + must(breaker.Close()) + unwrap(hash.Write([]byte("\n-----END RSA PRIVATE KEY-----\n"))) + return rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hash.Sum(nil), signature) != nil +} + +const pemLineLength = 76 + +var nl = []byte{'\n'} + +type lineBreaker struct { + line [pemLineLength]byte + used int + out io.Writer +} + +func (l *lineBreaker) Write(b []byte) (n int, err error) { + if l.used+len(b) < pemLineLength { + copy(l.line[l.used:], b) + l.used += len(b) + return len(b), nil + } + + n, err = l.out.Write(l.line[0:l.used]) + if err != nil { + return + } + excess := pemLineLength - l.used + l.used = 0 + + n1, err := l.out.Write(b[0:excess]) + if err != nil { + return n + n1, err + } + + n2, err := l.out.Write(nl) + if err != nil { + return n + n1 + n2, err + } + + n3, err := l.Write(b[excess:]) + return n1 + n2 + n3, err +} + +func (l *lineBreaker) Close() (err error) { + if l.used > 0 { + _, err = l.out.Write(l.line[0:l.used]) + if err != nil { + return + } + _, err = l.out.Write(nl) + } + + return +} + +func must(err error) { + if err != nil { + panic(err) + } +} + +func unwrap[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/server/auth/yggdrasil_session_pubkey.der b/server/auth/yggdrasil_session_pubkey.der new file mode 100644 index 0000000..9c79a3a Binary files /dev/null and b/server/auth/yggdrasil_session_pubkey.der differ diff --git a/server/gameplay.go b/server/gameplay.go index 0b114dc..8c5c713 100644 --- a/server/gameplay.go +++ b/server/gameplay.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/rsa" _ "embed" "sync" "time" @@ -16,7 +17,7 @@ type GamePlay interface { // // Note: the connection will be closed after this function returned. // You don't need to close the connection, but to keep not returning while the player is playing. - AcceptPlayer(name string, id uuid.UUID, protocol int32, conn *net.Conn) + AcceptPlayer(name string, id uuid.UUID, profilePubKey *rsa.PublicKey, protocol int32, conn *net.Conn) } type Game struct { diff --git a/server/login.go b/server/login.go index 8c40808..600c476 100644 --- a/server/login.go +++ b/server/login.go @@ -1,9 +1,14 @@ package server import ( + "crypto/rsa" + "crypto/x509" + "errors" "fmt" - "github.com/google/uuid" + "io" + "time" + "unsafe" "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data/packetid" @@ -16,7 +21,7 @@ import ( // LoginHandler is used to handle player login process, that is, // from clientbound "LoginStart" packet to serverbound "LoginSuccess" packet. type LoginHandler interface { - AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, err error) + AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *rsa.PublicKey, err error) } // LoginChecker is the interface to check if a player is allowed to log in the server. @@ -35,6 +40,9 @@ type MojangLoginHandler struct { // And also encrypt the connection after login. OnlineMode bool + // EnforceSecureProfile enforce to check the player's profile public key + EnforceSecureProfile bool + // Threshold set the smallest size of raw network payload to compress. // Set to 0 to compress all packets. Set to -1 to disable compression. Threshold int @@ -46,7 +54,7 @@ type MojangLoginHandler struct { } // AcceptLogin implement LoginHandler for MojangLoginHandler -func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, err error) { +func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name string, id uuid.UUID, profilePubKey *rsa.PublicKey, err error) { //login start var p pk.Packet err = conn.ReadPacket(&p) @@ -58,21 +66,56 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s return } - err = p.Scan((*pk.String)(&name)) //decode username as pk.String + var hasSignature pk.Boolean + var timestamp pk.Long + var profilePubKeyBytes pk.ByteArray + var signature pk.ByteArray + err = p.Scan( + (*pk.String)(&name), + &hasSignature, pk.Opt{ + Has: &hasSignature, + Field: pk.Tuple{×tamp, &profilePubKeyBytes, &signature}, + }, + ) //decode username as pk.String if err != nil { return } + if hasSignature { + if time.UnixMilli(int64(timestamp)).Before(time.Now()) || + !auth.VerifySignature(profilePubKeyBytes, signature) { + err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.invalid_public_key_signature")} + return + } + } else if d.EnforceSecureProfile { + err = LoginFailErr{reason: chat.TranslateMsg("multiplayer.disconnect.missing_public_key")} + return + } + + var properties []property //auth if d.OnlineMode { var resp *auth.Resp //Auth, Encrypt - resp, err = auth.Encrypt(conn, name) + var key any + key, err = x509.ParsePKIXPublicKey(profilePubKeyBytes) + if err != nil { + return + } + if rsaPubKey, ok := key.(*rsa.PublicKey); !ok { + err = errors.New("expect RSA public key") + return + } else { + profilePubKey = rsaPubKey + } + + resp, err = auth.Encrypt(conn, name, profilePubKey) if err != nil { return } name = resp.Name id = resp.ID + properties = *(*[]property)(unsafe.Pointer(&resp.Properties)) } else { // offline-mode UUID id = offline.NameToUUID(name) @@ -81,7 +124,7 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s //set compression if d.Threshold >= 0 { err = conn.WritePacket(pk.Marshal( - packetid.SetCompression, + packetid.LoginCompression, pk.VarInt(d.Threshold), )) if err != nil { @@ -94,27 +137,40 @@ func (d *MojangLoginHandler) AcceptLogin(conn *net.Conn, protocol int32) (name s if d.LoginChecker != nil { if ok, result := d.CheckPlayer(name, id, protocol); !ok { // player is not allowed to join the server - err = conn.WritePacket(pk.Marshal( - packetid.LoginDisconnect, - result, - )) - if err != nil { - return - } - err = loginFailErr{reason: result} + err = LoginFailErr{reason: result} return } } - // send login success err = conn.WritePacket(pk.Marshal( packetid.LoginSuccess, pk.UUID(id), pk.String(name), + pk.Array(properties), )) return } +type GameProfile struct { + ID uuid.UUID + Name string +} + +type property auth.Property + +func (p property) WriteTo(w io.Writer) (n int64, err error) { + hasSignature := len(p.Signature) > 0 + return pk.Tuple{ + pk.String(p.Name), + pk.String(p.Value), + pk.Boolean(hasSignature), + pk.Opt{ + Has: hasSignature, + Field: pk.String(p.Signature), + }, + }.WriteTo(w) +} + type wrongPacketErr struct { expect, get int32 } @@ -123,10 +179,10 @@ func (w wrongPacketErr) Error() string { return fmt.Sprintf("wrong packet id: expect %#02X, get %#02X", w.expect, w.get) } -type loginFailErr struct { +type LoginFailErr struct { reason chat.Message } -func (l loginFailErr) Error() string { +func (l LoginFailErr) Error() string { return "login error: " + l.reason.ClearString() } diff --git a/server/server.go b/server/server.go index 4776f8e..acd81b1 100644 --- a/server/server.go +++ b/server/server.go @@ -27,13 +27,18 @@ package server import ( + "errors" + "github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/net" + pk "github.com/Tnze/go-mc/net/packet" + "log" ) const ProtocolName = "1.19" const ProtocolVersion = 759 type Server struct { + *log.Logger ListPingHandler LoginHandler GamePlay @@ -65,10 +70,20 @@ func (s *Server) acceptConn(conn *net.Conn) { case 1: // list ping s.acceptListPing(conn) case 2: // login - name, id, err := s.AcceptLogin(conn, protocol) + name, id, profilePubKey, err := s.AcceptLogin(conn, protocol) if err != nil { + var loginErr *LoginFailErr + if errors.As(err, &loginErr) { + _ = conn.WritePacket(pk.Marshal( + packetid.LoginDisconnect, + loginErr.reason, + )) + } + if s.Logger != nil { + s.Logger.Printf("client %v login error: %v", conn.Socket.RemoteAddr(), err) + } return } - s.AcceptPlayer(name, id, protocol, conn) + s.AcceptPlayer(name, id, profilePubKey, protocol, conn) } }