wfc-server/common/auth_token.go
2026-04-07 15:49:05 -04:00

171 lines
3.9 KiB
Go

package common
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"errors"
"strings"
"time"
)
func generateRandom(n int) []byte {
key := make([]byte, n)
read, err := rand.Read(key)
if err != nil {
panic(err)
}
if read != n {
panic("short rand.Read()")
}
return key
}
var (
authTokenKey = generateRandom(16)
authTokenIV = generateRandom(16)
authTokenMagic = generateRandom(8)
loginTicketKey = generateRandom(16)
loginTicketIV = generateRandom(16)
loginTicketMagic = generateRandom(4)
)
var (
ErrTokenMagic = errors.New("invalid auth token or login ticket magic")
ErrTokenExpired = errors.New("auth token or login ticket expired")
ErrTokenLength = errors.New("invalid auth token or login ticket length")
)
type NASAuthToken struct {
IssueTime uint64
UserID uint64
ConsoleFriendCode uint64
Region byte
Lang byte
UnitCode byte
GameCode [4]byte
GsbrCode [16]byte
Challenge [8]byte
InGameScreenName [64]byte
Magic [8]byte
}
var nasAuthTokenSize = (binary.Size(NASAuthToken{}) + aes.BlockSize - 1) & ^(aes.BlockSize - 1)
func (t NASAuthToken) Marshal() string {
t.IssueTime = uint64(time.Now().UTC().Unix())
copy(t.Magic[:], authTokenMagic)
var buf bytes.Buffer
ShouldNotError(binary.Write(&buf, binary.LittleEndian, t))
// Pad to CBC block size
data := append(buf.Bytes(), make([]byte, nasAuthTokenSize-len(buf.Bytes()))...)
block, err := aes.NewCipher(authTokenKey)
ShouldNotError(err)
cipher.NewCBCEncrypter(block, authTokenIV).CryptBlocks(data, data)
return "NDS" + Base64DwcEncoding.EncodeToString(data)
}
func (t *NASAuthToken) Unmarshal(data string) error {
if !strings.HasPrefix(data, "NDS") {
return ErrTokenLength
}
blob, err := Base64DwcEncoding.DecodeString(data[3:])
if err != nil {
return err
}
if len(blob) != nasAuthTokenSize {
return ErrTokenLength
}
block, err := aes.NewCipher(authTokenKey)
ShouldNotError(err)
cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob)
reader := bytes.NewReader(blob)
if err := binary.Read(reader, binary.LittleEndian, t); err != nil {
return err
}
if !bytes.Equal(t.Magic[:], authTokenMagic) {
return ErrTokenMagic
}
currentTime := time.Now().UTC()
issueTime := time.Unix(int64(t.IssueTime), 0)
if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) {
return ErrTokenExpired
}
return nil
}
type GPCMLoginTicket struct {
IssueTime uint64
ProfileID uint32
Magic [4]byte
}
var gpcmLoginTicketSize = (binary.Size(GPCMLoginTicket{}) + aes.BlockSize - 1) & ^(aes.BlockSize - 1)
func (t GPCMLoginTicket) Marshal() string {
t.IssueTime = uint64(time.Now().UTC().Unix())
copy(t.Magic[:], loginTicketMagic)
var buf bytes.Buffer
ShouldNotError(binary.Write(&buf, binary.LittleEndian, t))
// Pad to CBC block size
data := append(buf.Bytes(), make([]byte, gpcmLoginTicketSize-len(buf.Bytes()))...)
block, err := aes.NewCipher(loginTicketKey)
ShouldNotError(err)
cipher.NewCBCEncrypter(block, loginTicketIV).CryptBlocks(data, data)
return base64.StdEncoding.EncodeToString(data)
}
func (t *GPCMLoginTicket) Unmarshal(ticket string) error {
blob, err := base64.StdEncoding.DecodeString(ticket)
if err != nil {
return err
}
if len(blob) != gpcmLoginTicketSize {
return ErrTokenLength
}
block, err := aes.NewCipher(loginTicketKey)
ShouldNotError(err)
cipher.NewCBCDecrypter(block, loginTicketIV).CryptBlocks(blob, blob)
reader := bytes.NewReader(blob)
if err := binary.Read(reader, binary.LittleEndian, t); err != nil {
return err
}
if !bytes.Equal(t.Magic[:], loginTicketMagic) {
return ErrTokenMagic
}
currentTime := time.Now().UTC()
issueTime := time.Unix(int64(t.IssueTime), 0)
if issueTime.Before(currentTime.Add(-48*time.Hour)) || issueTime.After(currentTime) {
return ErrTokenExpired
}
return nil
}