416 lines
10 KiB
Go
416 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"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"git.konjactw.dev/falloutBot/go-mc/chat"
|
|
"git.konjactw.dev/falloutBot/go-mc/data/packetid"
|
|
"git.konjactw.dev/falloutBot/go-mc/net"
|
|
"git.konjactw.dev/falloutBot/go-mc/net/CFB8"
|
|
pk "git.konjactw.dev/falloutBot/go-mc/net/packet"
|
|
|
|
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/login/client"
|
|
"git.konjactw.dev/patyhank/minego/pkg/protocol/packet/login/server"
|
|
)
|
|
|
|
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
|
|
}
|