package xboxlive import ( "bytes" "compress/flate" "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/x509" "errors" "io" "log" "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 { log.Fatal(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 }