From 08b6ef218eac7cbca02bfb9bef8d21e940760ecd Mon Sep 17 00:00:00 2001 From: ppeb Date: Thu, 20 Mar 2025 00:01:29 -0500 Subject: [PATCH] Require a valid csnum to log in --- common/auth_token.go | 13 +++++++--- common/config.go | 2 ++ database/login.go | 60 +++++++++++++++++++++++++++++++++++++++++--- database/user.go | 21 ++++++++-------- gamestats/auth.go | 2 +- gpcm/login.go | 8 +++--- nas/auth.go | 13 +++++++--- schema.sql | 3 ++- 8 files changed, 96 insertions(+), 26 deletions(-) diff --git a/common/auth_token.go b/common/auth_token.go index 226d63d..2960d94 100644 --- a/common/auth_token.go +++ b/common/auth_token.go @@ -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 } diff --git a/common/config.go b/common/config.go index 9ee68e0..7232947 100644 --- a/common/config.go +++ b/common/config.go @@ -44,6 +44,8 @@ type Config struct { AllowMultipleDeviceIDs string `xml:"allowMultipleDeviceIDs"` AllowConnectWithoutDeviceID bool `xml:"allowConnectWithoutDeviceID"` + AllowMultipleCsnums string `xml:"allowMultipleCsnums"` + ServerName string `xml:"serverName,omitempty"` } diff --git a/database/login.go b/database/login.go index 6227350..a92b104 100644 --- a/database/login.go +++ b/database/login.go @@ -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 } diff --git a/database/user.go b/database/user.go index 0172a75..cf9deb4 100644 --- a/database/user.go +++ b/database/user.go @@ -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 diff --git a/gamestats/auth.go b/gamestats/auth.go index 3828df5..e51ca66 100644 --- a/gamestats/auth.go +++ b/gamestats/auth.go @@ -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) diff --git a/gpcm/login.go b/gpcm/login.go index 39640d7..4af3a15 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -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 { diff --git a/nas/auth.go b/nas/auth.go index 3e0f257..f94d061 100644 --- a/nas/auth.go +++ b/nas/auth.go @@ -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"])) } diff --git a/schema.sql b/schema.sql index 9963aad..2fb84ff 100644 --- a/schema.sql +++ b/schema.sql @@ -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[]