246 lines
5.6 KiB
Go
246 lines
5.6 KiB
Go
package bot
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha1"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/Tnze/go-mc/net/CFB8"
|
|
pk "github.com/Tnze/go-mc/net/packet"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Auth includes a account
|
|
type Auth struct {
|
|
Name string
|
|
UUID string
|
|
AsTk string
|
|
}
|
|
|
|
// OfflineUUID return the UUID from player name in offline mode
|
|
func OfflineUUID(name string) uuid.UUID {
|
|
var version = 3
|
|
h := md5.New()
|
|
h.Reset()
|
|
h.Write([]byte("OfflinePlayer:" + name))
|
|
s := h.Sum(nil)
|
|
var id uuid.UUID
|
|
copy(id[:], s)
|
|
id[6] = (id[6] & 0x0f) | uint8((version&0xf)<<4)
|
|
id[8] = (id[8] & 0x3f) | 0x80 // RFC 4122 variant
|
|
return id
|
|
}
|
|
|
|
// 加密请求
|
|
func handleEncryptionRequest(c *Client, pack pk.Packet) error {
|
|
//创建AES对称加密密钥
|
|
key, encoStream, decoStream := newSymmetricEncryption()
|
|
|
|
//解析EncryptionRequest包
|
|
var er encryptionRequest
|
|
if err := pack.Scan(&er); err != nil {
|
|
return err
|
|
}
|
|
err := loginAuth(c.AsTk, c.Name, c.Auth.UUID, key, er) //向Mojang验证
|
|
if err != nil {
|
|
return fmt.Errorf("login fail: %v", err)
|
|
}
|
|
|
|
// 响应加密请求
|
|
var p pk.Packet // Encryption Key Response
|
|
p, err = genEncryptionKeyResponse(key, er.PublicKey, er.VerifyToken)
|
|
if err != nil {
|
|
return fmt.Errorf("gen encryption key response fail: %v", err)
|
|
}
|
|
err = c.conn.WritePacket(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 设置连接加密
|
|
c.conn.SetCipher(encoStream, decoStream)
|
|
return nil
|
|
}
|
|
|
|
type encryptionRequest struct {
|
|
ServerID string
|
|
PublicKey []byte
|
|
VerifyToken []byte
|
|
}
|
|
|
|
func (e *encryptionRequest) Decode(r pk.DecodeReader) error {
|
|
var serverID pk.String
|
|
if err := serverID.Decode(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var publicKeyLength, verifyTokenLength pk.VarInt
|
|
|
|
if err := publicKeyLength.Decode(r); err != nil {
|
|
return err
|
|
}
|
|
publicKey, err := pk.ReadNBytes(r, int(publicKeyLength))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := verifyTokenLength.Decode(r); err != nil {
|
|
return err
|
|
}
|
|
verifyToken, err := pk.ReadNBytes(r, int(verifyTokenLength))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
e.ServerID = string(serverID)
|
|
e.PublicKey = publicKey
|
|
e.VerifyToken = verifyToken
|
|
return nil
|
|
}
|
|
|
|
// authDigest computes a special SHA-1 digest required for Minecraft web
|
|
// authentication on Premium servers (online-mode=true).
|
|
// Source: http://wiki.vg/Protocol_Encryption#Server
|
|
//
|
|
// Also many, many thanks to SirCmpwn and his wonderful gist (C#):
|
|
// https://gist.github.com/SirCmpwn/404223052379e82f91e6
|
|
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(fmt.Sprintf("%x", 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 profile struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type request struct {
|
|
AccessToken string `json:"accessToken"`
|
|
SelectedProfile profile `json:"selectedProfile"`
|
|
ServerID string `json:"serverId"`
|
|
}
|
|
|
|
func loginAuth(AsTk, name, UUID string, shareSecret []byte, er encryptionRequest) error {
|
|
digest := authDigest(er.ServerID, shareSecret, er.PublicKey)
|
|
|
|
client := http.Client{}
|
|
requestPacket, err := json.Marshal(
|
|
request{
|
|
AccessToken: AsTk,
|
|
SelectedProfile: profile{
|
|
ID: UUID,
|
|
Name: name,
|
|
},
|
|
ServerID: digest,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("create request packet to yggdrasil faile: %v", err)
|
|
}
|
|
|
|
PostRequest, err := http.NewRequest(http.MethodPost, "https://sessionserver.mojang.com/session/minecraft/join",
|
|
bytes.NewReader(requestPacket))
|
|
if err != nil {
|
|
return fmt.Errorf("make request error: %v", err)
|
|
}
|
|
PostRequest.Header.Set("User-agent", "go-mc")
|
|
PostRequest.Header.Set("Connection", "keep-alive")
|
|
PostRequest.Header.Set("Content-Type", "application/json")
|
|
resp, err := client.Do(PostRequest)
|
|
if err != nil {
|
|
return fmt.Errorf("post fail: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
|
if resp.Status != "204 No Content" {
|
|
return fmt.Errorf("auth fail: %s", string(body))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AES/CFB8 with random key
|
|
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 genEncryptionKeyResponse(shareSecret, publicKey, verifyToken []byte) (erp pk.Packet, err error) {
|
|
iPK, err := x509.ParsePKIXPublicKey(publicKey) // Decode Public Key
|
|
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
|
|
}
|
|
var data []byte
|
|
data = append(data, pk.VarInt(int32(len(cryptPK))).Encode()...)
|
|
data = append(data, cryptPK...)
|
|
data = append(data, pk.VarInt(int32(len(verifyT))).Encode()...)
|
|
data = append(data, verifyT...)
|
|
erp = pk.Packet{
|
|
ID: 0x01,
|
|
Data: data,
|
|
}
|
|
return
|
|
}
|