Files
minego/pkg/auth/auth.go

414 lines
10 KiB
Go

package auth
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/login/client"
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/login/server"
"github.com/Tnze/go-mc/chat"
"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"
"github.com/google/uuid"
)
var (
ErrLogin = errors.New("login")
ErrKick = errors.New("login.kicked")
ErrEncrypt = errors.New("login.encrypt")
)
type Profile struct {
Name string
UUID uuid.UUID
}
type Provider interface {
Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error
FetchProfile(ctx context.Context) *Profile
}
type Auth struct {
*net.Conn
Provider
}
func (a *Auth) HandleLogin(ctx context.Context) error {
profile := a.FetchProfile(ctx)
err := a.WritePacket(pk.Marshal(packetid.ServerboundLoginHello, server.LoginHello{
Name: profile.Name,
UUID: profile.UUID,
}))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("write login hello fail: %w", err))
}
var p pk.Packet
for {
err = a.ReadPacket(&p)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read packet fail: %w", err))
}
switch packetid.ClientboundPacketID(p.ID) {
case packetid.ClientboundLoginLoginDisconnect:
var reason chat.JsonMessage
err = p.Scan(&reason)
return errors.Join(ErrKick, fmt.Errorf("kicked by server: %s", chat.Message(reason).ClearString()))
case packetid.ClientboundLoginHello:
var hello client.LoginHello
err = p.Scan(&hello)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login hello fail: %w", err))
}
err = a.Authenticate(ctx, a.Conn, hello)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("authenticate fail: %w", err))
}
case packetid.ClientboundLoginLoginFinished:
err = a.WritePacket(pk.Marshal(packetid.ServerboundLoginLoginAcknowledged))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("write login ack fail: %w", err))
}
return nil
case packetid.ClientboundLoginLoginCompression:
var threshold int32
err = p.Scan((*pk.VarInt)(&threshold))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login compression fail: %w", err))
}
a.Conn.SetThreshold(int(threshold))
case packetid.ClientboundLoginCustomQuery:
var query client.LoginCustomQuery
err = p.Scan(&query)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login custom query fail: %w", err))
}
err = a.WritePacket(pk.Marshal(
packetid.ServerboundLoginCustomQueryAnswer,
&server.LoginCustomQueryAnswer{MessageID: query.MessageID},
))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login custom query fail: %w", err))
}
case packetid.ClientboundLoginCookieRequest:
var cookie client.LoginCookieRequest
err = p.Scan(&cookie)
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login cookie request fail: %w", err))
}
err = a.WritePacket(pk.Marshal(
packetid.ServerboundLoginCookieResponse,
&server.LoginCookieResponse{Key: cookie.Key},
))
if err != nil {
return errors.Join(ErrLogin, fmt.Errorf("read login cookie request fail: %w", err))
}
}
}
}
type OnlineAuthServer struct {
SessionServer string
AuthServer string
}
type OnlineAuth struct {
AccessToken string
Profile Profile
}
func (o *OnlineAuth) Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error {
key, encodeStream, decodeStream := newSymmetricEncryption()
err := o.LoginAuth(ctx, content, key)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("login auth fail: %w", err))
}
// Response with Encryption Key
var pkt pk.Packet
pkt, err = genEncryptionKeyResponse(key, content.PublicKey, content.VerifyToken)
if err != nil {
return fmt.Errorf("gen encryption key response fail: %v", err)
}
err = conn.WritePacket(pkt)
if err != nil {
return err
}
// Set Connection Encryption
conn.SetCipher(encodeStream, decodeStream)
return nil
}
func genEncryptionKeyResponse(shareSecret, publicKey, verifyToken []byte) (erp pk.Packet, err error) {
iPK, err := x509.ParsePKIXPublicKey(publicKey)
if err != nil {
err = fmt.Errorf("decode public key fail: %v", err)
return
}
rsaKey := iPK.(*rsa.PublicKey)
cryptPK, err := rsa.EncryptPKCS1v15(rand.Reader, rsaKey, shareSecret)
if err != nil {
err = fmt.Errorf("encryption share secret fail: %v", err)
return
}
verifyT, err := rsa.EncryptPKCS1v15(rand.Reader, rsaKey, verifyToken)
if err != nil {
err = fmt.Errorf("encryption verfy tokenfail: %v", err)
return erp, err
}
return pk.Marshal(
packetid.ServerboundLoginKey,
pk.ByteArray(cryptPK),
pk.ByteArray(verifyT),
), nil
}
func (o *OnlineAuth) LoginAuth(ctx context.Context, content client.LoginHello, key []byte) error {
digest := authDigest(content.ServerID, key, content.PublicKey)
request, err := json.Marshal(struct {
AccessToken string `json:"accessToken"`
SelectedProfile string `json:"selectedProfile"`
ServerID string `json:"serverId"`
}{
AccessToken: o.AccessToken,
SelectedProfile: hex.EncodeToString(o.Profile.UUID[:]),
ServerID: digest,
})
PostRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://sessionserver.mojang.com/session/minecraft/join",
bytes.NewReader(request))
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("make request fail: %w", err))
}
PostRequest.Header.Set("User-agent", "go-mc")
PostRequest.Header.Set("Connection", "keep-alive")
PostRequest.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(PostRequest)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("session mojang fail: %w", err))
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusNoContent {
return errors.Join(ErrEncrypt, fmt.Errorf("session join fail: %s", string(body)))
}
return nil
}
func newSymmetricEncryption() (key []byte, encoStream, decoStream cipher.Stream) {
key = make([]byte, 16)
if _, err := rand.Read(key); err != nil {
panic(err)
}
b, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
decoStream = CFB8.NewCFB8Decrypt(b, key)
encoStream = CFB8.NewCFB8Encrypt(b, key)
return
}
func (o *OnlineAuth) FetchProfile(ctx context.Context) *Profile {
return &o.Profile
}
func authDigest(serverID string, sharedSecret, publicKey []byte) string {
h := sha1.New()
h.Write([]byte(serverID))
h.Write(sharedSecret)
h.Write(publicKey)
hash := h.Sum(nil)
// Check for negative hashes
negative := (hash[0] & 0x80) == 0x80
if negative {
hash = twosComplement(hash)
}
// Trim away zeroes
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
if negative {
res = "-" + res
}
return res
}
// little endian
func twosComplement(p []byte) []byte {
carry := true
for i := len(p) - 1; i >= 0; i-- {
p[i] = ^p[i]
if carry {
carry = p[i] == 0xff
p[i]++
}
}
return p
}
type OfflineAuth struct {
Username string
}
// NameToUUID return the UUID from player name in offline mode
func NameToUUID(name string) uuid.UUID {
version := 3
h := md5.New()
h.Write([]byte("OfflinePlayer:"))
h.Write([]byte(name))
var id uuid.UUID
h.Sum(id[:0])
id[6] = (id[6] & 0x0f) | uint8((version&0xf)<<4)
id[8] = (id[8] & 0x3f) | 0x80 // RFC 4122 variant
return id
}
func (o *OfflineAuth) FetchProfile(ctx context.Context) *Profile {
return &Profile{
Name: o.Username,
UUID: NameToUUID(o.Username),
}
}
func (o *OfflineAuth) Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error {
return nil
}
type KonjacAuth struct {
UserCode string
}
func (k *KonjacAuth) Authenticate(ctx context.Context, conn *net.Conn, content client.LoginHello) error {
key, encodeStream, decodeStream := newSymmetricEncryption()
err := k.LoginAuth(ctx, content, key)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("login auth fail: %w", err))
}
// Response with Encryption Key
var pkt pk.Packet
pkt, err = genEncryptionKeyResponse(key, content.PublicKey, content.VerifyToken)
if err != nil {
return fmt.Errorf("gen encryption key response fail: %v", err)
}
err = conn.WritePacket(pkt)
if err != nil {
return err
}
// Set Connection Encryption
conn.SetCipher(encodeStream, decodeStream)
return nil
}
func (k *KonjacAuth) LoginAuth(ctx context.Context, content client.LoginHello, key []byte) error {
digest := authDigest(content.ServerID, key, content.PublicKey)
request, err := json.Marshal(struct {
AccessToken string `json:"accessToken"`
SelectedProfile string `json:"selectedProfile"`
ServerID string `json:"serverId"`
}{
AccessToken: k.UserCode,
SelectedProfile: "-",
ServerID: digest,
})
PostRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.1:37565/ss/session/minecraft/join",
bytes.NewReader(request))
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("make request fail: %w", err))
}
PostRequest.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(PostRequest)
if err != nil {
return errors.Join(ErrEncrypt, fmt.Errorf("session mojang fail: %w", err))
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusNoContent {
return errors.Join(ErrEncrypt, fmt.Errorf("session join fail: %s", string(body)))
}
return nil
}
func (k *KonjacAuth) FetchProfile(ctx context.Context) *Profile {
data, err := json.Marshal(map[string]any{
"agent": map[string]any{
"name": "minego",
"version": 0,
},
"username": k.UserCode,
"password": "",
"clientToken": "",
"requestUser": "",
})
if err != nil {
return nil
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.1:37565/as/authenticate", bytes.NewReader(data))
if err != nil {
return nil
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil
}
defer resp.Body.Close()
data, err = io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
return nil
}
var profile struct {
SelectedProfile Profile `json:"selectedProfile"`
}
err = json.Unmarshal(data, &profile)
if err != nil {
return nil
}
return &profile.SelectedProfile
}