Files
gocommon/xboxlive/xboxlive.go

328 lines
7.9 KiB
Go

package xboxlive
import (
"bytes"
"compress/flate"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"errors"
"io"
"net/http"
"os"
"strings"
"sync"
b64 "encoding/base64"
"encoding/binary"
"encoding/json"
"encoding/pem"
"github.com/golang-jwt/jwt"
"golang.org/x/crypto/pkcs12"
//"software.sslmate.com/src/go-pkcs12"
)
type JWT_Header struct {
Typ string `json:"typ"`
Alg string `json:"alg"`
X5t string `json:"x5t"`
X5u string `json:"x5u"`
}
type JWT_XDI_data struct {
Dty string `json:"dty"` // Device Type : ex) XboxOne
}
type JWT_XUI_data struct {
Ptx string `json:"ptx"` // 파트너 Xbox 사용자 ID (ptx) - PXUID (ptx), publisher별로 고유한 ID : ex) 293812B467D21D3295ADA06B121981F805CC38F0
Gtg string `json:"gtg"` // 게이머 태그
}
type JWT_XBoxLiveBody struct {
XDI JWT_XDI_data `json:"xdi"`
XUI []JWT_XUI_data `json:"xui"`
Sbx string `json:"sbx"` // SandBoxID : ex) BLHLQG.99
}
var cachedCert map[string]map[string]string
var cachedCertLock = new(sync.RWMutex)
func getcachedCert(x5u string, x5t string) string {
cachedCertLock.Lock()
defer cachedCertLock.Unlock()
if cachedCert == nil {
cachedCert = make(map[string]map[string]string)
}
var certKey string
certKey = ""
if CachedCertURI, existCachedCertURI := cachedCert[x5u]; existCachedCertURI {
if CachedCert, existCachedCert := CachedCertURI[x5t]; existCachedCert {
certKey = CachedCert
}
}
return certKey
}
func setcachedCert(x5u string, x5t string, certKey string) {
cachedCertLock.Lock()
defer cachedCertLock.Unlock()
if cachedCert[x5u] == nil {
cachedCert[x5u] = make(map[string]string)
}
cachedCert[x5u][x5t] = certKey
}
func JWT_DownloadXSTSSigningCert(x5u string, x5t string) string {
certKey := getcachedCert(x5u, x5t)
// -- 캐싱된 자료가 없으면 웹에서 받아 온다.
if certKey == "" {
resp, err := http.Get(x5u) // GET 호출
if err != nil {
panic(err)
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
// 결과 출력
data, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var parseddata map[string]string
err = json.Unmarshal([]byte(data), &parseddata)
if err != nil {
panic(err)
}
if downloadedkey, exist := parseddata[x5t]; exist {
// downloadedkey = strings.Replace(downloadedkey, "-----BEGIN CERTIFICATE-----\n", "", -1)
// downloadedkey = strings.Replace(downloadedkey, "\n-----END CERTIFICATE-----\n", "", -1)
certKey = downloadedkey
} else {
panic("JWT_DownloadXSTSSigningCert : Key not found : " + x5t)
}
}
setcachedCert(x5u, x5t, certKey)
return certKey
}
func jwt_Decrypt_forXBoxLive(jwt_token string) (JWT_Header, JWT_XBoxLiveBody) {
parts := strings.Split(jwt_token, ".")
jwt_header, err := b64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
panic(err)
}
JWT_Header_obj := JWT_Header{}
json.Unmarshal([]byte(string(jwt_header)), &JWT_Header_obj)
if JWT_Header_obj.Typ != "JWT" {
panic("JWT Decrypt Error : typ is not JWT")
}
if JWT_Header_obj.Alg != "RS256" {
panic("JWT Decrypt Error : alg is not RS256")
}
var publicKey string
if len(JWT_Header_obj.X5u) >= len("https://xsts.auth.xboxlive.com") && JWT_Header_obj.X5u[:len("https://xsts.auth.xboxlive.com")] == "https://xsts.auth.xboxlive.com" {
publicKey = JWT_DownloadXSTSSigningCert(JWT_Header_obj.X5u, JWT_Header_obj.X5t)
} else {
panic("JWT Decrypt Error : Invalid x5u host that is not trusted" + JWT_Header_obj.X5u)
}
block, _ := pem.Decode([]byte(publicKey))
var cert *x509.Certificate
cert, _ = x509.ParseCertificate(block.Bytes)
rsaPublicKey := cert.PublicKey.(*rsa.PublicKey)
err = verifyJWT_forXBoxLive(jwt_token, rsaPublicKey)
if err != nil {
panic(err)
}
jwt_body, err := b64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
panic(err)
}
JWT_XBoxLiveBody_obj := JWT_XBoxLiveBody{}
json.Unmarshal([]byte(string(jwt_body)), &JWT_XBoxLiveBody_obj)
return JWT_Header_obj, JWT_XBoxLiveBody_obj
}
func verifyJWT_forXBoxLive(decompressed string, rsaPublicKey *rsa.PublicKey) error {
token, err := jwt.Parse(decompressed, func(token *jwt.Token) (interface{}, error) {
return rsaPublicKey, nil
})
if err != nil {
if err := err.(*jwt.ValidationError); err != nil {
if err.Errors == jwt.ValidationErrorExpired {
return nil
}
}
return err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if claims["iss"].(string) != "xsts.auth.xboxlive.com" {
return errors.New("issuer is not valid")
}
if claims["aud"].(string) != "rp://actionsquaredev.com/" {
return errors.New("audience is not valid")
}
return nil
}
return errors.New("token is not valid")
}
func splitSecretKey(data []byte) ([]byte, []byte) {
if len(data) < 2 {
panic(" SplitSecretKey : secretkey is too small.. ")
}
if len(data)%2 != 0 {
panic(" SplitSecretKey : data error ")
}
midpoint := len(data) / 2
firstHalf := data[0:midpoint]
secondHalf := data[midpoint : midpoint+midpoint]
return firstHalf, secondHalf
}
func pkcs7UnPadding(origData []byte) []byte {
length := len(origData)
unpadding := int(origData[length-1])
return origData[:(length - unpadding)]
}
func aesCBCDncrypt(plaintext []byte, key []byte, iv []byte) []byte {
block, _ := aes.NewCipher(key)
ciphertext := make([]byte, len(plaintext))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, plaintext)
ciphertext = pkcs7UnPadding(ciphertext)
return ciphertext
}
func deflate(inflated []byte) string {
byteReader := bytes.NewReader(inflated)
wBuf := new(strings.Builder)
zr := flate.NewReader(byteReader)
if _, err := io.Copy(wBuf, zr); err != nil {
panic(err)
}
return wBuf.String()
}
var errHashMismatch = errors.New("authentication tag does not match with the computed hash")
func verifyAuthenticationTag(aad []byte, iv []byte, cipherText []byte, hmacKey []byte, authTag []byte) error {
aadBitLength := make([]byte, 8)
binary.BigEndian.PutUint64(aadBitLength, uint64(len(aad)*8))
dataToSign := append(append(append(aad, iv...), cipherText...), aadBitLength...)
h := hmac.New(sha256.New, []byte(hmacKey))
h.Write([]byte(dataToSign))
hash := h.Sum(nil)
computedAuthTag, _ := splitSecretKey(hash)
// Check if the auth tag is equal
// The authentication tag is the first half of the hmac result
if !bytes.Equal(authTag, computedAuthTag) {
return errHashMismatch
}
return nil
}
var privateKeydata []byte
func privateKeyFile() string {
return os.Getenv("XBOXLIVE_PTX_FILE_NAME")
}
func privateKeyFilePass() string {
return os.Getenv("XBOXLIVE_PTX_FILE_PASSWORD")
}
func Init() {
if len(privateKeyFile()) == 0 || len(privateKeyFilePass()) == 0 {
return
}
var err error
privateKeydata, err = os.ReadFile(privateKeyFile())
if err != nil {
panic("Error reading private key file")
}
}
// 실제 체크 함수
func AuthCheck(token string) (ptx string, err error) {
parts := strings.Split(token, ".")
encryptedData, _ := b64.RawURLEncoding.DecodeString(parts[1])
privateKey, _, e := pkcs12.Decode(privateKeydata, privateKeyFilePass())
if e != nil {
return "", e
}
if e := privateKey.(*rsa.PrivateKey).Validate(); e != nil {
return "", e
}
hash := sha1.New()
random := rand.Reader
decryptedData, decryptErr := rsa.DecryptOAEP(hash, random, privateKey.(*rsa.PrivateKey), encryptedData, nil)
if decryptErr != nil {
return "", decryptErr
}
hmacKey, aesKey := splitSecretKey(decryptedData)
iv, _ := b64.RawURLEncoding.DecodeString(parts[2])
encryptedContent, _ := b64.RawURLEncoding.DecodeString(parts[3])
// Decrypt the payload using the AES + IV
decrypted := aesCBCDncrypt(encryptedContent, aesKey, iv)
decompressed := deflate(decrypted)
_, body := jwt_Decrypt_forXBoxLive(decompressed)
authTag, _ := b64.RawURLEncoding.DecodeString(parts[4])
authData := []byte(parts[0])
err = verifyAuthenticationTag(authData, iv, encryptedContent, hmacKey, authTag)
return body.XUI[0].Ptx, err
}