go-ayo/common을 gocommon으로 분리
This commit is contained in:
328
xboxlive/xboxlive.go
Normal file
328
xboxlive/xboxlive.go
Normal file
@ -0,0 +1,328 @@
|
||||
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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user