Require a valid csnum to log in

This commit is contained in:
ppeb 2025-03-20 00:01:29 -05:00
parent 14419b35ed
commit 08b6ef218e
No known key found for this signature in database
GPG Key ID: CC147AD1B3D318D0
8 changed files with 96 additions and 26 deletions

View File

@ -46,7 +46,7 @@ func appendString(blob []byte, value string, maxlen int) []byte {
return blob
}
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, unitcd byte, isLocalhost bool) (string, string) {
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, unitcd byte, isLocalhost bool, csnum string) (string, string) {
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix()))
blob = appendString(blob, gamecd, 4)
@ -73,6 +73,10 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
blob = append(blob, 0x00)
}
// Set max length to 15 for padding
blob = append(blob, byte(min(len([]byte(csnum)), 15)))
blob = appendString(blob, csnum, 15)
blob = append(blob, authTokenMagic...)
block, err := aes.NewCipher(authTokenKey)
@ -84,7 +88,7 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge
}
func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, unitcd byte, isLocalhost bool, err error) {
func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, unitcd byte, isLocalhost bool, csnum string, err error) {
err = nil
if !strings.HasPrefix(token, "NDS") {
@ -97,7 +101,7 @@ func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, us
return
}
if len(blob) != 0x90 {
if len(blob) != 0xA0 {
err = errors.New("invalid auth token length")
return
}
@ -109,7 +113,7 @@ func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, us
cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob)
if !bytes.Equal(blob[0x90-len(authTokenMagic):0x90], authTokenMagic) {
if !bytes.Equal(blob[0xA0-len(authTokenMagic):0xA0], authTokenMagic) {
err = errors.New("invalid auth token magic")
return
}
@ -125,6 +129,7 @@ func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, us
challenge = string(blob[0x78:0x80])
unitcd = blob[0x80]
isLocalhost = blob[0x81] == 0x01
csnum = string(blob[0x83 : 0x83+min(blob[0x82], 15)])
return
}

View File

@ -44,6 +44,8 @@ type Config struct {
AllowMultipleDeviceIDs string `xml:"allowMultipleDeviceIDs"`
AllowConnectWithoutDeviceID bool `xml:"allowConnectWithoutDeviceID"`
AllowMultipleCsnums string `xml:"allowMultipleCsnums"`
ServerName string `xml:"serverName,omitempty"`
}

View File

@ -39,9 +39,52 @@ const (
var (
ErrDeviceIDMismatch = errors.New("NG device ID mismatch")
ErrProfileBannedTOS = errors.New("profile is banned for violating the Terms of Service")
ErrCsnumMismatch = errors.New("csnum mismatch")
)
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32, ipAddress string, ingamesn string, deviceAuth bool) (User, error) {
func handleCsnum(pool *pgxpool.Pool, ctx context.Context, user *User, csnum string, lastIPAddress *string, ipAddress string) (bool, error) {
success := false
csnumList := ""
var err error
for i, validCsnum := range user.Csnum {
if validCsnum == csnum {
success = true
break
}
if validCsnum == "" {
user.Csnum[i] = csnum
_, err = pool.Exec(ctx, UpdateUserCsnum, user.ProfileId, csnum)
success = true
break
}
csnumList += aurora.Cyan(validCsnum).String() + ", "
}
if !success && csnum != "" {
if len(user.Csnum) > 0 && common.GetConfig().AllowMultipleCsnums != "always" {
if common.GetConfig().AllowMultipleCsnums == "SameIPAddress" && (lastIPAddress == nil || ipAddress != *lastIPAddress) {
logging.Error("DATABASE", "Csnum mismatch for profile", aurora.Cyan(user.ProfileId), "- expected one of {", csnumList[:len(csnumList)-2], "} but got", aurora.Cyan(csnum))
return success, ErrCsnumMismatch
}
}
if len(user.Csnum) > 0 {
logging.Warn("DATABASE", "Adding csnum", aurora.Cyan(csnum), "to profile", aurora.Cyan(user.ProfileId))
}
user.Csnum = append(user.Csnum, csnum)
_, err = pool.Exec(ctx, UpdateUserCsnum, user.ProfileId, user.Csnum)
success = err == nil
}
return success, err
}
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32, ipAddress string, ingamesn string, deviceAuth bool, csnum string) (User, error) {
var exists bool
err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists)
if err != nil {
@ -63,6 +106,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
}
user.UniqueNick = common.Base32Encode(userId) + gsbrcd
user.Email = user.UniqueNick + "@nds"
user.Csnum = []string{csnum}
// Create the GPCM account
err := user.CreateUser(pool, ctx)
@ -76,7 +120,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
var firstName *string
var lastName *string
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress)
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress, &user.Csnum)
if err != nil {
return User{}, err
}
@ -133,6 +177,16 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
return User{}, err
}
success, err := handleCsnum(pool, ctx, &user, csnum, lastIPAddress, ipAddress)
if !success {
if err != nil {
return User{}, err
}
return User{}, ErrCsnumMismatch
}
if profileId != 0 && user.ProfileId != profileId {
err := user.UpdateProfileID(pool, ctx, profileId)
if err != nil {
@ -225,7 +279,7 @@ func LoginUserToGameStats(pool *pgxpool.Pool, ctx context.Context, userId uint64
var firstName *string
var lastName *string
var lastIPAddress *string
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress)
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress, &user.Csnum)
if err != nil {
return User{}, err
}

View File

@ -10,18 +10,18 @@ import (
)
const (
// NOTE: SearchUserBan is only used in one place, so we can change it for Retro Rewind with no issues
InsertUser = `INSERT INTO users (user_id, gsbrcd, password, ng_device_id, email, unique_nick) VALUES ($1, $2, $3, $4, $5, $6) RETURNING profile_id`
InsertUserWithProfileID = `INSERT INTO users (profile_id, user_id, gsbrcd, password, ng_device_id, email, unique_nick) VALUES ($1, $2, $3, $4, $5, $6, $7)`
InsertUser = `INSERT INTO users (user_id, gsbrcd, password, ng_device_id, email, unique_nick, csnum) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING profile_id`
InsertUserWithProfileID = `INSERT INTO users (profile_id, user_id, gsbrcd, password, ng_device_id, email, unique_nick, csnum) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END, open_host = CASE WHEN $7 THEN $6 ELSE open_host END WHERE profile_id = $1`
UpdateUserProfileID = `UPDATE users SET profile_id = $3 WHERE user_id = $1 AND gsbrcd = $2`
UpdateUserNGDeviceID = `UPDATE users SET ng_device_id = $2 WHERE profile_id = $1`
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn FROM users WHERE profile_id = $1`
ClearProfileQuery = `DELETE FROM users WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn`
UpdateUserCsnum = `UPDATE users SET csnum = $2 WHERE profile_id = $1`
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn, csnum FROM users WHERE profile_id = $1`
ClearProfileQuery = `DELETE FROM users WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn, csnum`
DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)`
IsProfileIDInUse = `SELECT EXISTS(SELECT 1 FROM users WHERE profile_id = $1)`
DeleteUserSession = `DELETE FROM sessions WHERE profile_id = $1`
GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname, open_host, last_ip_address FROM users WHERE user_id = $1 AND gsbrcd = $2`
GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname, open_host, last_ip_address, csnum FROM users WHERE user_id = $1 AND gsbrcd = $2`
UpdateUserLastIPAddress = `UPDATE users SET last_ip_address = $2, last_ingamesn = $3 WHERE profile_id = $1`
UpdateUserBan = `UPDATE users SET has_ban = true, ban_issued = $2, ban_expires = $3, ban_reason = $4, ban_reason_hidden = $5, ban_moderator = $6, ban_tos = $7 WHERE profile_id = $1`
DisableUserBan = `UPDATE users SET has_ban = false WHERE profile_id = $1`
@ -45,6 +45,7 @@ type User struct {
OpenHost bool
LastInGameSn string
LastIPAddress string
Csnum []string
}
var (
@ -54,7 +55,7 @@ var (
func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
if user.ProfileId == 0 {
return pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick).Scan(&user.ProfileId)
return pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick, user.Csnum).Scan(&user.ProfileId)
}
if user.ProfileId >= 1000000000 {
@ -71,7 +72,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
return ErrProfileIDInUse
}
_, err = pool.Exec(ctx, InsertUserWithProfileID, user.ProfileId, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick)
_, err = pool.Exec(ctx, InsertUserWithProfileID, user.ProfileId, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick, user.Csnum)
return err
}
@ -133,7 +134,7 @@ func (user *User) UpdateProfile(pool *pgxpool.Pool, ctx context.Context, data ma
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.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn, &user.Csnum)
if err != nil {
return User{}, false
}
@ -145,7 +146,7 @@ func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User
func ClearProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
user := User{}
row := pool.QueryRow(ctx, ClearProfileQuery, profileId)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn)
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn, &user.Csnum)
if err != nil {
return User{}, false

View File

@ -63,7 +63,7 @@ func (g *GameStatsSession) authp(command common.GameSpyCommand) {
return
}
_, issueTime, userId, gsbrcd, _, _, _, _, _, _, _, err := common.UnmarshalNASAuthToken(authToken)
_, issueTime, userId, gsbrcd, _, _, _, _, _, _, _, _, err := common.UnmarshalNASAuthToken(authToken)
if err != nil {
logging.Error(g.ModuleName, "Error unmarshalling authtoken:", err.Error())
g.Write(errorCmd)

View File

@ -333,7 +333,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
cmdProfileId = uint32(cmdProfileId2)
}
if !g.performLoginWithDatabase(userId, gsbrcd, cmdProfileId, deviceId, deviceAuth) {
if !g.performLoginWithDatabase(userId, gsbrcd, cmdProfileId, deviceId, deviceAuth, csnum) {
return
}
@ -425,7 +425,7 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) {
return
}
if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, deviceId, true) {
if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, deviceId, true, g.User.Csnum[0]) {
return
}
@ -500,14 +500,14 @@ func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authTo
return deviceId
}
func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string, profileId uint32, deviceId uint32, deviceAuth bool) bool {
func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string, profileId uint32, deviceId uint32, deviceAuth bool, csnum string) bool {
// Get IP address without port
ipAddress := g.RemoteAddr
if strings.Contains(ipAddress, ":") {
ipAddress = ipAddress[:strings.Index(ipAddress, ":")]
}
user, err := database.LoginUserToGPCM(pool, ctx, userId, gsbrCode, profileId, deviceId, ipAddress, g.InGameName, deviceAuth)
user, err := database.LoginUserToGPCM(pool, ctx, userId, gsbrCode, profileId, deviceId, ipAddress, g.InGameName, deviceAuth, csnum)
g.User = user
if err != nil {

View File

@ -271,6 +271,13 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
}
}
csnum, ok := fields["csnum"]
if !ok || len(csnum) > 16 { // Picked a random length. Serial numbers appear to be anywhere from 9-12?
logging.Error(moduleName, "Missing or invalid csnum in form")
param["returncd"] = "103"
return param
}
var authToken, challenge string
switch unitcdInt {
// ds
@ -285,10 +292,10 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
// Only later DS games send this
ingamesn, ok := fields["ingamesn"]
if ok {
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, 0, isLocalhost)
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, 0, isLocalhost, csnum)
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname), "ingamesn:", aurora.Cyan(ingamesn))
} else {
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", 0, isLocalhost)
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", 0, isLocalhost, csnum)
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname))
}
@ -320,7 +327,7 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
return param
}
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], 1, isLocalhost)
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], 1, isLocalhost, csnum)
logging.Notice(moduleName, "Login (Wii)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "ingamesn:", aurora.Cyan(fields["ingamesn"]))
}

View File

@ -48,7 +48,8 @@ ALTER TABLE ONLY public.users
ADD IF NOT EXISTS ban_reason_hidden character varying,
ADD IF NOT EXISTS ban_moderator character varying,
ADD IF NOT EXISTS ban_tos boolean,
ADD IF NOT EXISTS open_host boolean DEFAULT false;
ADD IF NOT EXISTS open_host boolean DEFAULT false,
ADD IF NOT EXISTS csnum character varying[];
--
-- Change ng_device_id from bigint to bigint[]