328 lines
7.9 KiB
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
|
|
|
|
}
|