From ad79cb286a55255177afd5ce4309ad19697903f9 Mon Sep 17 00:00:00 2001 From: mkwcat Date: Thu, 14 Dec 2023 11:24:45 -0500 Subject: [PATCH] Don't use database for auth tokens --- api/groups.go | 2 +- common/auth_token.go | 158 ++++++++++++++++++++++++++++++++++++++ common/encoding.go | 2 +- common/gamespy_message.go | 4 + database/login.go | 111 +------------------------- database/user.go | 48 ++---------- go.mod | 2 +- gpcm/login.go | 45 ++++++----- gpcm/main.go | 1 + gpcm/profile.go | 2 +- nas/auth.go | 74 ++++++++++++++++-- qr2/group.go | 3 + qr2/logins.go | 4 +- qr2/session.go | 7 +- sake/storage.go | 9 ++- 15 files changed, 287 insertions(+), 185 deletions(-) create mode 100644 common/auth_token.go diff --git a/api/groups.go b/api/groups.go index 1592948..46df856 100644 --- a/api/groups.go +++ b/api/groups.go @@ -31,7 +31,7 @@ func HandleGroups(w http.ResponseWriter, r *http.Request) { filtered := map[string]string{} filtered["pid"] = player["dwc_pid"] - filtered["name"] = player["name"] + filtered["name"] = player["+ingamesn"] if player["gamename"] == "mariokartwii" { filtered["ev"] = player["ev"] diff --git a/common/auth_token.go b/common/auth_token.go new file mode 100644 index 0000000..f2d15e7 --- /dev/null +++ b/common/auth_token.go @@ -0,0 +1,158 @@ +package common + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "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(16) + + loginTicketKey = generateRandom(16) + loginTicketIV = generateRandom(16) + loginTicketMagic = generateRandom(4) +) + +func appendString(blob []byte, value string, maxlen int) []byte { + if len([]byte(value)) < maxlen { + blob = append(blob, append([]byte(value), make([]byte, maxlen-len(value))...)...) + } else { + blob = append(blob, []byte(value)[:maxlen]...) + } + + return blob +} + +func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string) (string, string) { + blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix())) + + blob = appendString(blob, gamecd, 4) + + blob = append(blob, binary.LittleEndian.AppendUint64([]byte{}, userid)[:6]...) + + blob = append(blob, byte(min(len([]byte(gsbrcd)), 16))) + blob = appendString(blob, gsbrcd, 16) + + blob = append(blob, binary.LittleEndian.AppendUint64([]byte{}, cfc)[:7]...) + blob = append(blob, region, lang) + + blob = append(blob, byte(min(len([]byte(ingamesn)), 75))) + blob = appendString(blob, ingamesn, 75) + + challenge := RandomString(8) + blob = append(blob, []byte(challenge)...) + blob = append(blob, authTokenMagic...) + + block, err := aes.NewCipher(authTokenKey) + if err != nil { + panic(err) + } + + cipher.NewCBCEncrypter(block, authTokenIV).CryptBlocks(blob, blob) + return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge +} + +func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string) { + if !strings.HasPrefix(token, "NDS") { + err = errors.New("invalid auth token prefix") + return + } + + blob, err := Base64DwcEncoding.DecodeString(token[3:]) + if err != nil { + return + } + + if len(blob) != 0x90 { + err = errors.New("invalid auth token length") + return + } + + block, err := aes.NewCipher(authTokenKey) + if err != nil { + panic(err) + } + + cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob) + + if !bytes.Equal(blob[0x80:0x90], authTokenMagic) { + err = errors.New("invalid auth token magic") + return + } + + issuetime = time.Unix(int64(binary.LittleEndian.Uint64(blob[0x0:0x8])), 0) + gamecd = string(blob[0x8:0xC]) + userid = binary.LittleEndian.Uint64(append(bytes.Clone(blob[0xC:0x12]), 0, 0)) + gsbrcd = string(blob[0x13 : 0x13+min(blob[0x12], 16)]) + cfc = binary.LittleEndian.Uint64(append(bytes.Clone(blob[0x23:0x2A]), 0)) + region = blob[0x2A] + lang = blob[0x2B] + ingamesn = string(blob[0x2D : 0x2D+min(blob[0x2C], 75)]) + challenge = string(blob[0x78:0x80]) + return +} + +func MarshalGPCMLoginTicket(profileId uint32) string { + blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix())) + blob = binary.LittleEndian.AppendUint32(blob, profileId) + blob = append(blob, loginTicketMagic...) + + block, err := aes.NewCipher(loginTicketKey) + if err != nil { + panic(err) + } + + cipher.NewCBCEncrypter(block, loginTicketIV).CryptBlocks(blob, blob) + return Base64DwcEncoding.EncodeToString(blob) +} + +func UnmarshalGPCMLoginTicket(ticket string) (err error, profileId uint32, issuetime time.Time) { + blob, err := Base64DwcEncoding.DecodeString(ticket) + if err != nil { + return + } + + if len(blob) != 0x10 { + err = errors.New("invalid login ticket length") + return + } + + block, err := aes.NewCipher(loginTicketKey) + if err != nil { + panic(err) + } + + cipher.NewCBCDecrypter(block, loginTicketIV).CryptBlocks(blob, blob) + + if !bytes.Equal(blob[0xC:0x10], loginTicketMagic) { + err = errors.New("invalid login ticket magic") + return + } + + issuetime = time.Unix(int64(binary.LittleEndian.Uint64(blob[0x0:0x8])), 0) + profileId = binary.LittleEndian.Uint32(blob[0x8:0xC]) + return +} diff --git a/common/encoding.go b/common/encoding.go index 7499398..86c2c9b 100644 --- a/common/encoding.go +++ b/common/encoding.go @@ -6,7 +6,7 @@ import ( var Base64DwcEncoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-").WithPadding('*') -func Base32Encode(value int64) string { +func Base32Encode(value uint64) string { alpha := "0123456789abcdefghijklmnopqrstuv" encoded := "" diff --git a/common/gamespy_message.go b/common/gamespy_message.go index 2bc1591..ace2118 100644 --- a/common/gamespy_message.go +++ b/common/gamespy_message.go @@ -30,6 +30,10 @@ func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) { for len(msg) > 0 && string(msg[0]) == `\` { keyEnd := strings.Index(msg[1:], `\`) + 1 + if keyEnd < 2 { + return nil, InvalidGameSpyCommand + } + key := msg[1:keyEnd] value := "" msg = msg[keyEnd+1:] diff --git a/database/login.go b/database/login.go index 8e75355..7693399 100644 --- a/database/login.go +++ b/database/login.go @@ -2,101 +2,13 @@ package database import ( "context" - "crypto/sha512" - "encoding/hex" - "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "github.com/logrusorgru/aurora/v3" - "os" - "strconv" "wwfc/common" "wwfc/logging" ) -const ( - DoesAuthTokenExist = `SELECT EXISTS(SELECT 1 FROM logins WHERE auth_token = $1)` - DoesNASUserExist = `SELECT EXISTS(SELECT 1 FROM logins WHERE user_id = $1 AND gsbrcd = $2)` - UpdateUserLogin = `UPDATE logins SET auth_token = $1, challenge = $2 WHERE user_id = $3 AND gsbrcd = $4` - InsertUserLogin = `INSERT INTO logins (auth_token, user_id, gsbrcd, challenge) VALUES ($1, $2, $3, $4)` - GetNASUserLogin = `SELECT user_id, gsbrcd FROM logins WHERE auth_token = $1 LIMIT 1` - GetNASChallenge = `SELECT challenge FROM logins WHERE auth_token = $1` -) - -var salt []byte - -// GenerateAuthToken generates and stores the auth token for this user as well as a challenge. -func GenerateAuthToken(pool *pgxpool.Pool, ctx context.Context, userId int64, gsbrcd string) (string, string) { - var userExists bool - err := pool.QueryRow(ctx, DoesNASUserExist, userId, gsbrcd).Scan(&userExists) - if err != nil { - panic(err) - } - - authToken := "NDS" + common.RandomString(80) - for { - // We must make sure that the auth token doesn't exist before attempting to insert it into the database. - var tokenExists bool - err := pool.QueryRow(ctx, DoesAuthTokenExist, authToken).Scan(&tokenExists) - if err != nil { - panic(err) - } - - if !tokenExists { - break - } - - authToken = "NDS" + common.RandomString(80) - } - - challenge := common.RandomString(8) - if userExists { - // UPDATE rather than INSERT - _, err = pool.Exec(ctx, UpdateUserLogin, authToken, challenge, userId, gsbrcd) - if err != nil { - panic(err) - } - } else { - _, err = pool.Exec(ctx, InsertUserLogin, authToken, userId, gsbrcd, challenge) - if err != nil { - panic(err) - } - } - - return authToken, challenge -} - -func GetNASLogin(pool *pgxpool.Pool, ctx context.Context, authToken string) (int64, string) { - var userId int64 - var gsbrcd string - err := pool.QueryRow(ctx, GetNASUserLogin, authToken).Scan(&userId, &gsbrcd) - if err != nil { - if err == pgx.ErrNoRows { - return 0, "" - } else { - panic(err) - } - } - - return userId, gsbrcd -} - -func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string) (User, bool) { - // Make sure salt is loaded - if salt == nil { - var err error - salt, err = os.ReadFile("salt.bin") - if err != nil { - panic(err) - } - } - - // Query login table with the auth token. - userId, gsbrcd := GetNASLogin(pool, ctx, authToken) - if userId == 0 { - logging.Error("DATABASE", "Invalid auth token:", aurora.Cyan(authToken)) - return User{}, false - } - +func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string) (User, bool) { var exists bool err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists) if err != nil { @@ -104,12 +16,10 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string) } uniqueNickname := common.Base32Encode(userId) + gsbrcd - password := sha512.Sum512(append(salt, []byte(gsbrcd)...)) user := User{ UserId: userId, GsbrCode: gsbrcd, - Password: hex.EncodeToString(password[:]), Email: uniqueNickname + "@nds", UniqueNick: uniqueNickname, } @@ -118,30 +28,15 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, authToken string) // Create the GPCM account user.CreateUser(pool, ctx) - logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)), aurora.Cyan(user.GsbrCode), "-", aurora.Cyan(strconv.FormatInt(int64(user.ProfileId), 10))) + logging.Notice("DATABASE", "Created new GPCM user:", aurora.Cyan(userId), aurora.Cyan(gsbrcd), "-", aurora.Cyan(user.ProfileId)) } else { err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId) if err != nil { panic(err) } - logging.Notice("DATABASE", "Log in GPCM user:", aurora.Cyan(strconv.FormatInt(user.UserId, 10)), aurora.Cyan(user.GsbrCode), "-", aurora.Cyan(strconv.FormatInt(int64(user.ProfileId), 10))) + logging.Notice("DATABASE", "Log in GPCM user:", aurora.Cyan(userId), aurora.Cyan(user.GsbrCode), "-", aurora.Cyan(user.ProfileId)) } return user, true } - -func GetChallenge(pool *pgxpool.Pool, ctx context.Context, authToken string) string { - var challenge string - err := pool.QueryRow(ctx, GetNASChallenge, authToken).Scan(&challenge) - if err != nil { - if err == pgx.ErrNoRows { - // Invalid auth token - return "" - } else { - panic(err) - } - } - - return challenge -} diff --git a/database/user.go b/database/user.go index aa132ca..a783863 100644 --- a/database/user.go +++ b/database/user.go @@ -2,17 +2,14 @@ package database import ( "context" - "errors" - "github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4/pgxpool" "math/rand" - "wwfc/common" ) const ( InsertUser = `INSERT INTO users (user_id, gsbrcd, password, email, unique_nick) VALUES ($1, $2, $3, $4, $5) RETURNING profile_id` - UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END WHERE profile_id = $1 RETURNING user_id, gsbrcd, password, email, unique_nick, firstname, lastname` - GetUser = `SELECT user_id, gsbrcd, password, email, unique_nick, firstname, lastname FROM users WHERE profile_id = $1` + UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname` + GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname FROM users WHERE profile_id = $1` CreateUserSession = `INSERT INTO sessions (session_key, profile_id, login_ticket) VALUES ($1, $2, $3)` GetTicketSession = `SELECT session_key, profile_id FROM sessions WHERE login_ticket = $1` DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)` @@ -25,9 +22,8 @@ const ( type User struct { ProfileId uint32 - UserId int64 + UserId uint64 GsbrCode string - Password string Email string UniqueNick string FirstName string @@ -35,15 +31,15 @@ type User struct { } func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) { - err := pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, user.Password, user.Email, user.UniqueNick).Scan(&user.ProfileId) + err := pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.Email, user.UniqueNick).Scan(&user.ProfileId) if err != nil { panic(err) } } -func GetUniqueUserID() int64 { +func GetUniqueUserID() uint64 { // Not guaranteed unique but doesn't matter in practice if multiple people have the same user ID. - return rand.Int63n(0x80000000000) + return uint64(rand.Int63n(0x80000000000)) } func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, data map[string]string) User { @@ -52,7 +48,7 @@ func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, da user := User{} row := pool.QueryRow(ctx, UpdateUserTable, profileId, firstName, firstNameExists, lastName, lastNameExists) - err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) + err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) if err != nil { panic(err) } @@ -61,38 +57,10 @@ func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, da return user } -func CreateSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32, loginTicket string) string { - sessionKey := common.RandomString(8) - _, err := pool.Exec(ctx, CreateUserSession, sessionKey, profileId, loginTicket) - if err != nil { - panic(err) - } - - return sessionKey -} - -func GetSession(pool *pgxpool.Pool, ctx context.Context, loginTicket string) (string, uint32) { - var sessionKey string - var profileId uint32 - err := pool.QueryRow(ctx, GetTicketSession, loginTicket).Scan(&sessionKey, &profileId) - if err != nil { - panic(err) - } - - return sessionKey, profileId -} - -func deleteSession(pool *pgxpool.Pool, ctx context.Context, profileId uint32) { - _, err := pool.Exec(ctx, DeleteUserSession, profileId) - if err != nil && !errors.Is(err, pgx.ErrNoRows) { - panic(err) - } -} - func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) { user := User{} row := pool.QueryRow(ctx, GetUser, profileId) - err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) + err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) if err != nil { return User{}, false } diff --git a/go.mod b/go.mod index 76ac342..57cf754 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module wwfc -go 1.20 +go 1.21 require ( github.com/jackc/pgx/v4 v4.18.1 diff --git a/gpcm/login.go b/gpcm/login.go index 83ab989..29fcc8b 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -3,12 +3,13 @@ package gpcm import ( "crypto/md5" "crypto/sha1" - "encoding/base64" "encoding/hex" "fmt" + "github.com/logrusorgru/aurora/v3" "math/rand" "strconv" "strings" + "time" "wwfc/common" "wwfc/database" "wwfc/logging" @@ -96,7 +97,7 @@ func verifySignature(authToken string, signature string) bool { logging.Error("GPCM", "Auth token signature failed") return false } - logging.Notice("GPCM", "Auth token signature verified") + logging.Notice("GPCM", "Auth token signature verified; NG ID:", aurora.Cyan(fmt.Sprintf("%08x", ngId))) return true } @@ -124,24 +125,27 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { } signature, exists := command.OtherValues["wwfc_sig"] - if exists { - // TODO: This is still in testing so it's only checked if it exists - if !verifySignature(authToken, signature) { - g.replyError(GPError{ - ErrorCode: ErrLogin.ErrorCode, - ErrorString: "The authentication signature is invalid.", - Fatal: true, - }) - return - } + if !exists || !verifySignature(authToken, signature) { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "The authentication signature is invalid.", + Fatal: true, + }) + return } - challenge := database.GetChallenge(pool, ctx, authToken) - if challenge == "" { + err, _, issueTime, userId, gsbrcd, cfc, _, _, ingamesn, challenge := common.UnmarshalNASAuthToken(authToken) + if err != nil { g.replyError(ErrLogin) return } + currentTime := time.Now() + if issueTime.Before(currentTime.Add(-10*time.Minute)) || issueTime.After(currentTime) { + g.replyError(ErrLoginLoginTicketExpired) + return + } + response := generateResponse(g.Challenge, challenge, authToken, command.OtherValues["challenge"]) if response != command.OtherValues["response"] { g.replyError(ErrLogin) @@ -151,7 +155,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { proof := generateProof(g.Challenge, challenge, command.OtherValues["authtoken"], command.OtherValues["challenge"]) // Perform the login with the database. - user, ok := database.LoginUserToGPCM(pool, ctx, authToken) + user, ok := database.LoginUserToGPCM(pool, ctx, userId, gsbrcd) if !ok { // There was an error logging in to the GP backend. g.replyError(ErrLogin) @@ -175,10 +179,9 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { sessions[g.User.ProfileId] = g mutex.Unlock() - g.LoginTicket = strings.Replace(base64.StdEncoding.EncodeToString([]byte(common.RandomString(16))), "=", "_", -1) - // Now initiate the session - _ = database.CreateSession(pool, ctx, g.User.ProfileId, g.LoginTicket) + g.LoginTicket = common.MarshalGPCMLoginTicket(g.User.ProfileId) g.SessionKey = rand.Int31n(290000000) + 10000000 + g.InGameName = ingamesn g.LoggedIn = true g.ModuleName = "GPCM:" + strconv.FormatInt(int64(g.User.ProfileId), 10) @@ -186,7 +189,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { // Notify QR2 of the login // TODO: Get ingamesn and cfc from NAS - qr2.Login(g.User.ProfileId, "", "", g.Conn.RemoteAddr().String()) + qr2.Login(g.User.ProfileId, ingamesn, cfc, g.Conn.RemoteAddr().String()) payload := common.CreateGameSpyMessage(common.GameSpyCommand{ Command: "lc", @@ -194,8 +197,8 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { OtherValues: map[string]string{ "sesskey": strconv.FormatInt(int64(g.SessionKey), 10), "proof": proof, - "userid": strconv.FormatInt(g.User.UserId, 10), - "profileid": strconv.FormatInt(int64(g.User.ProfileId), 10), + "userid": strconv.FormatUint(g.User.UserId, 10), + "profileid": strconv.FormatUint(uint64(g.User.ProfileId), 10), "uniquenick": g.User.UniqueNick, "lt": g.LoginTicket, "id": command.OtherValues["id"], diff --git a/gpcm/main.go b/gpcm/main.go index d2c11a5..21543f4 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -24,6 +24,7 @@ type GameSpySession struct { Challenge string LoginTicket string SessionKey int32 + InGameName string Status string LocString string FriendList []uint32 diff --git a/gpcm/profile.go b/gpcm/profile.go index b3a4d24..1e7df69 100644 --- a/gpcm/profile.go +++ b/gpcm/profile.go @@ -43,7 +43,7 @@ func (g *GameSpySession) getProfile(command common.GameSpyCommand) { OtherValues: map[string]string{ "profileid": command.OtherValues["profileid"], "nick": user.UniqueNick, - "userid": strconv.FormatInt(user.UserId, 10), + "userid": strconv.FormatUint(uint64(user.UserId), 10), "email": user.Email, "sig": common.RandomHexString(32), "uniquenick": user.UniqueNick, diff --git a/nas/auth.go b/nas/auth.go index 1c836ac..a151791 100644 --- a/nas/auth.go +++ b/nas/auth.go @@ -1,6 +1,8 @@ package nas import ( + "encoding/binary" + "encoding/hex" "fmt" "github.com/logrusorgru/aurora/v3" "net/http" @@ -10,6 +12,7 @@ import ( "strconv" "strings" "time" + "unicode/utf16" "wwfc/common" "wwfc/database" "wwfc/logging" @@ -30,7 +33,7 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request fields := map[string]string{} for key, values := range r.PostForm { if len(values) != 1 { - logging.Warn(moduleName, "Ignoring multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values)) + logging.Warn(moduleName, "Ignoring none or multiple POST form values:", aurora.Cyan(key).String()+":", aurora.Cyan(values)) continue } @@ -40,8 +43,20 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request replyHTTPError(w, 400, "400 Bad Request") return } - logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(string(parsed))) - fields[key] = string(parsed) + + value := string(parsed) + + if key == "ingamesn" { + // Special handling required for the UTF-16 string + var utf16String []uint16 + for i := 0; i < len(parsed)/2; i++ { + utf16String = append(utf16String, binary.BigEndian.Uint16(parsed[i*2:i*2+2])) + } + value = string(utf16.Decode(utf16String)) + } + + logging.Info(moduleName, aurora.Cyan(key).String()+":", aurora.Cyan(value)) + fields[key] = value } reply := map[string]string{} @@ -139,7 +154,7 @@ func acctcreate() map[string]string { "retry": "0", "datetime": getDateTime(), "returncd": "002", - "userid": strconv.FormatInt(database.GetUniqueUserID(), 10), + "userid": strconv.FormatUint(database.GetUniqueUserID(), 10), } } @@ -150,6 +165,13 @@ func login(moduleName string, fields map[string]string) map[string]string { "locator": "gamespy.com", } + gamecd, ok := fields["gamecd"] + if !ok { + logging.Error(moduleName, "No gamecd in form") + param["returncd"] = "103" + return param + } + strUserId, ok := fields["userid"] if !ok { logging.Error(moduleName, "No userid in form") @@ -157,8 +179,8 @@ func login(moduleName string, fields map[string]string) map[string]string { return param } - userId, err := strconv.ParseInt(strUserId, 10, 64) - if err != nil { + userId, err := strconv.ParseUint(strUserId, 10, 64) + if err != nil || userId >= 0x80000000000 { logging.Error(moduleName, "Invalid userid string in form") param["returncd"] = "103" return param @@ -171,7 +193,45 @@ func login(moduleName string, fields map[string]string) map[string]string { return param } - authToken, challenge := database.GenerateAuthToken(pool, ctx, userId, gsbrcd) + cfc, ok := fields["cfc"] + if !ok { + logging.Error(moduleName, "No cfc in form") + param["returncd"] = "103" + return param + } + + cfcInt, err := strconv.ParseUint(cfc, 10, 64) + if err != nil || cfcInt > 9999999999999999 { + logging.Error(moduleName, "Invalid cfc string in form") + param["returncd"] = "103" + return param + } + + region, ok := fields["region"] + if !ok { + region = "ff" + } + + regionByte, err := hex.DecodeString(region) + if err != nil || len(regionByte) != 1 { + logging.Error(moduleName, "Invalid region byte in form") + param["returncd"] = "103" + return param + } + + lang, ok := fields["lang"] + if !ok { + lang = "ff" + } + + langByte, err := hex.DecodeString(lang) + if err != nil || len(langByte) != 1 { + logging.Error(moduleName, "Invalid lang byte in form") + param["returncd"] = "103" + return param + } + + authToken, challenge := common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"]) param["returncd"] = "001" param["challenge"] = challenge diff --git a/qr2/group.go b/qr2/group.go index 2baba55..88b2b5a 100644 --- a/qr2/group.go +++ b/qr2/group.go @@ -187,6 +187,9 @@ func GetGroups(gameName string) []GroupInfo { for k, v := range session.Data { mapData[k] = v } + + mapData["+ingamesn"] = session.Login.InGameName + groupInfo.Players = append(groupInfo.Players, mapData) if mapData["dwc_hoststate"] == "2" && mapData["dwc_suspend"] == "0" { diff --git a/qr2/logins.go b/qr2/logins.go index c3d0af2..cd0fa68 100644 --- a/qr2/logins.go +++ b/qr2/logins.go @@ -3,13 +3,13 @@ package qr2 type LoginInfo struct { ProfileID uint32 InGameName string - ConsoleFriendCode string + ConsoleFriendCode uint64 GPPublicIP string } var logins = map[uint32]LoginInfo{} -func Login(profileID uint32, inGameName, consoleFriendCode, publicIP string) { +func Login(profileID uint32, inGameName string, consoleFriendCode uint64, publicIP string) { mutex.Lock() logins[profileID] = LoginInfo{ ProfileID: profileID, diff --git a/qr2/session.go b/qr2/session.go index 9eb464e..9a63b26 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -24,6 +24,7 @@ type Session struct { Addr net.Addr Challenge string Authenticated bool + Login LoginInfo LastKeepAlive int64 Endianness byte // Some fields depend on the client's endianness Data map[string]string @@ -155,7 +156,9 @@ func (session *Session) setProfileID(moduleName string, newPID string) bool { // Check if the public IP matches the one used for the GPCM session var gpPublicIP string - if loginInfo, ok := logins[uint32(profileID)]; ok { + var loginInfo LoginInfo + var ok bool + if loginInfo, ok = logins[uint32(profileID)]; ok { gpPublicIP = loginInfo.GPPublicIP } else { logging.Error(moduleName, "Provided dwc_pid is not logged in:", aurora.Cyan(newPID)) @@ -167,6 +170,8 @@ func (session *Session) setProfileID(moduleName string, newPID string) bool { return false } + session.Login = loginInfo + // Constraint: only one session can exist with a profile ID var outdated []uint64 for sessionAddr, otherSession := range sessions { diff --git a/sake/storage.go b/sake/storage.go index 571c253..61a98bc 100644 --- a/sake/storage.go +++ b/sake/storage.go @@ -191,8 +191,13 @@ func getRequestIdentity(moduleName string, request StorageRequestData) (uint32, panic("Invalid secret key") } - // GetSession panics if it can't find the session for the login ticket - _, profileId := database.GetSession(pool, ctx, request.LoginTicket) + logging.Notice(moduleName, request.LoginTicket) + + err, profileId, _ := common.UnmarshalGPCMLoginTicket(request.LoginTicket) + if err != nil { + panic(err) + } + logging.Info(moduleName, "Profile ID:", aurora.BrightCyan(profileId)) logging.Info(moduleName, "Game:", aurora.Cyan(request.GameID), "-", aurora.BrightCyan(gameInfo.Name)) logging.Info(moduleName, "Table ID:", aurora.Cyan(request.TableID))