Skip to content

Commit 08b6ef2

Browse files
committed
Require a valid csnum to log in
1 parent 14419b3 commit 08b6ef2

File tree

8 files changed

+96
-26
lines changed

8 files changed

+96
-26
lines changed

common/auth_token.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func appendString(blob []byte, value string, maxlen int) []byte {
4646
return blob
4747
}
4848

49-
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, unitcd byte, isLocalhost bool) (string, string) {
49+
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, unitcd byte, isLocalhost bool, csnum string) (string, string) {
5050
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix()))
5151

5252
blob = appendString(blob, gamecd, 4)
@@ -73,6 +73,10 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
7373
blob = append(blob, 0x00)
7474
}
7575

76+
// Set max length to 15 for padding
77+
blob = append(blob, byte(min(len([]byte(csnum)), 15)))
78+
blob = appendString(blob, csnum, 15)
79+
7680
blob = append(blob, authTokenMagic...)
7781

7882
block, err := aes.NewCipher(authTokenKey)
@@ -84,7 +88,7 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
8488
return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge
8589
}
8690

87-
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) {
91+
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) {
8892
err = nil
8993

9094
if !strings.HasPrefix(token, "NDS") {
@@ -97,7 +101,7 @@ func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, us
97101
return
98102
}
99103

100-
if len(blob) != 0x90 {
104+
if len(blob) != 0xA0 {
101105
err = errors.New("invalid auth token length")
102106
return
103107
}
@@ -109,7 +113,7 @@ func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, us
109113

110114
cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob)
111115

112-
if !bytes.Equal(blob[0x90-len(authTokenMagic):0x90], authTokenMagic) {
116+
if !bytes.Equal(blob[0xA0-len(authTokenMagic):0xA0], authTokenMagic) {
113117
err = errors.New("invalid auth token magic")
114118
return
115119
}
@@ -125,6 +129,7 @@ func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, us
125129
challenge = string(blob[0x78:0x80])
126130
unitcd = blob[0x80]
127131
isLocalhost = blob[0x81] == 0x01
132+
csnum = string(blob[0x83 : 0x83+min(blob[0x82], 15)])
128133
return
129134
}
130135

common/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ type Config struct {
4444
AllowMultipleDeviceIDs string `xml:"allowMultipleDeviceIDs"`
4545
AllowConnectWithoutDeviceID bool `xml:"allowConnectWithoutDeviceID"`
4646

47+
AllowMultipleCsnums string `xml:"allowMultipleCsnums"`
48+
4749
ServerName string `xml:"serverName,omitempty"`
4850
}
4951

database/login.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,52 @@ const (
3939
var (
4040
ErrDeviceIDMismatch = errors.New("NG device ID mismatch")
4141
ErrProfileBannedTOS = errors.New("profile is banned for violating the Terms of Service")
42+
ErrCsnumMismatch = errors.New("csnum mismatch")
4243
)
4344

44-
func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32, ipAddress string, ingamesn string, deviceAuth bool) (User, error) {
45+
func handleCsnum(pool *pgxpool.Pool, ctx context.Context, user *User, csnum string, lastIPAddress *string, ipAddress string) (bool, error) {
46+
success := false
47+
csnumList := ""
48+
var err error
49+
50+
for i, validCsnum := range user.Csnum {
51+
if validCsnum == csnum {
52+
success = true
53+
break
54+
}
55+
56+
if validCsnum == "" {
57+
user.Csnum[i] = csnum
58+
_, err = pool.Exec(ctx, UpdateUserCsnum, user.ProfileId, csnum)
59+
success = true
60+
break
61+
}
62+
63+
csnumList += aurora.Cyan(validCsnum).String() + ", "
64+
}
65+
66+
if !success && csnum != "" {
67+
if len(user.Csnum) > 0 && common.GetConfig().AllowMultipleCsnums != "always" {
68+
if common.GetConfig().AllowMultipleCsnums == "SameIPAddress" && (lastIPAddress == nil || ipAddress != *lastIPAddress) {
69+
logging.Error("DATABASE", "Csnum mismatch for profile", aurora.Cyan(user.ProfileId), "- expected one of {", csnumList[:len(csnumList)-2], "} but got", aurora.Cyan(csnum))
70+
return success, ErrCsnumMismatch
71+
}
72+
}
73+
74+
if len(user.Csnum) > 0 {
75+
logging.Warn("DATABASE", "Adding csnum", aurora.Cyan(csnum), "to profile", aurora.Cyan(user.ProfileId))
76+
}
77+
78+
user.Csnum = append(user.Csnum, csnum)
79+
_, err = pool.Exec(ctx, UpdateUserCsnum, user.ProfileId, user.Csnum)
80+
81+
success = err == nil
82+
}
83+
84+
return success, err
85+
}
86+
87+
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) {
4588
var exists bool
4689
err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists)
4790
if err != nil {
@@ -63,6 +106,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
63106
}
64107
user.UniqueNick = common.Base32Encode(userId) + gsbrcd
65108
user.Email = user.UniqueNick + "@nds"
109+
user.Csnum = []string{csnum}
66110

67111
// Create the GPCM account
68112
err := user.CreateUser(pool, ctx)
@@ -76,7 +120,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
76120
var firstName *string
77121
var lastName *string
78122

79-
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress)
123+
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress, &user.Csnum)
80124
if err != nil {
81125
return User{}, err
82126
}
@@ -133,6 +177,16 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb
133177
return User{}, err
134178
}
135179

180+
success, err := handleCsnum(pool, ctx, &user, csnum, lastIPAddress, ipAddress)
181+
182+
if !success {
183+
if err != nil {
184+
return User{}, err
185+
}
186+
187+
return User{}, ErrCsnumMismatch
188+
}
189+
136190
if profileId != 0 && user.ProfileId != profileId {
137191
err := user.UpdateProfileID(pool, ctx, profileId)
138192
if err != nil {
@@ -225,7 +279,7 @@ func LoginUserToGameStats(pool *pgxpool.Pool, ctx context.Context, userId uint64
225279
var firstName *string
226280
var lastName *string
227281
var lastIPAddress *string
228-
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress)
282+
err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &user.NgDeviceId, &user.Email, &user.UniqueNick, &firstName, &lastName, &user.OpenHost, &lastIPAddress, &user.Csnum)
229283
if err != nil {
230284
return User{}, err
231285
}

database/user.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import (
1010
)
1111

1212
const (
13-
// NOTE: SearchUserBan is only used in one place, so we can change it for Retro Rewind with no issues
14-
InsertUser = `INSERT INTO users (user_id, gsbrcd, password, ng_device_id, email, unique_nick) VALUES ($1, $2, $3, $4, $5, $6) RETURNING profile_id`
15-
InsertUserWithProfileID = `INSERT INTO users (profile_id, user_id, gsbrcd, password, ng_device_id, email, unique_nick) VALUES ($1, $2, $3, $4, $5, $6, $7)`
13+
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`
14+
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)`
1615
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`
1716
UpdateUserProfileID = `UPDATE users SET profile_id = $3 WHERE user_id = $1 AND gsbrcd = $2`
1817
UpdateUserNGDeviceID = `UPDATE users SET ng_device_id = $2 WHERE profile_id = $1`
19-
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn FROM users WHERE profile_id = $1`
20-
ClearProfileQuery = `DELETE FROM users WHERE profile_id = $1 RETURNING user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn`
18+
UpdateUserCsnum = `UPDATE users SET csnum = $2 WHERE profile_id = $1`
19+
GetUser = `SELECT user_id, gsbrcd, email, unique_nick, firstname, lastname, open_host, last_ip_address, last_ingamesn, csnum FROM users WHERE profile_id = $1`
20+
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`
2121
DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)`
2222
IsProfileIDInUse = `SELECT EXISTS(SELECT 1 FROM users WHERE profile_id = $1)`
2323
DeleteUserSession = `DELETE FROM sessions WHERE profile_id = $1`
24-
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`
24+
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`
2525
UpdateUserLastIPAddress = `UPDATE users SET last_ip_address = $2, last_ingamesn = $3 WHERE profile_id = $1`
2626
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`
2727
DisableUserBan = `UPDATE users SET has_ban = false WHERE profile_id = $1`
@@ -45,6 +45,7 @@ type User struct {
4545
OpenHost bool
4646
LastInGameSn string
4747
LastIPAddress string
48+
Csnum []string
4849
}
4950

5051
var (
@@ -54,7 +55,7 @@ var (
5455

5556
func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
5657
if user.ProfileId == 0 {
57-
return pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick).Scan(&user.ProfileId)
58+
return pool.QueryRow(ctx, InsertUser, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick, user.Csnum).Scan(&user.ProfileId)
5859
}
5960

6061
if user.ProfileId >= 1000000000 {
@@ -71,7 +72,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error {
7172
return ErrProfileIDInUse
7273
}
7374

74-
_, err = pool.Exec(ctx, InsertUserWithProfileID, user.ProfileId, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick)
75+
_, err = pool.Exec(ctx, InsertUserWithProfileID, user.ProfileId, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick, user.Csnum)
7576
return err
7677
}
7778

@@ -133,7 +134,7 @@ func (user *User) UpdateProfile(pool *pgxpool.Pool, ctx context.Context, data ma
133134
func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
134135
user := User{}
135136
row := pool.QueryRow(ctx, GetUser, profileId)
136-
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn)
137+
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn, &user.Csnum)
137138
if err != nil {
138139
return User{}, false
139140
}
@@ -145,7 +146,7 @@ func GetProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User
145146
func ClearProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32) (User, bool) {
146147
user := User{}
147148
row := pool.QueryRow(ctx, ClearProfileQuery, profileId)
148-
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn)
149+
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName, &user.OpenHost, &user.LastIPAddress, &user.LastInGameSn, &user.Csnum)
149150

150151
if err != nil {
151152
return User{}, false

gamestats/auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (g *GameStatsSession) authp(command common.GameSpyCommand) {
6363
return
6464
}
6565

66-
_, issueTime, userId, gsbrcd, _, _, _, _, _, _, _, err := common.UnmarshalNASAuthToken(authToken)
66+
_, issueTime, userId, gsbrcd, _, _, _, _, _, _, _, _, err := common.UnmarshalNASAuthToken(authToken)
6767
if err != nil {
6868
logging.Error(g.ModuleName, "Error unmarshalling authtoken:", err.Error())
6969
g.Write(errorCmd)

gpcm/login.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
333333
cmdProfileId = uint32(cmdProfileId2)
334334
}
335335

336-
if !g.performLoginWithDatabase(userId, gsbrcd, cmdProfileId, deviceId, deviceAuth) {
336+
if !g.performLoginWithDatabase(userId, gsbrcd, cmdProfileId, deviceId, deviceAuth, csnum) {
337337
return
338338
}
339339

@@ -425,7 +425,7 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) {
425425
return
426426
}
427427

428-
if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, deviceId, true) {
428+
if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, deviceId, true, g.User.Csnum[0]) {
429429
return
430430
}
431431

@@ -500,14 +500,14 @@ func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authTo
500500
return deviceId
501501
}
502502

503-
func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string, profileId uint32, deviceId uint32, deviceAuth bool) bool {
503+
func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string, profileId uint32, deviceId uint32, deviceAuth bool, csnum string) bool {
504504
// Get IP address without port
505505
ipAddress := g.RemoteAddr
506506
if strings.Contains(ipAddress, ":") {
507507
ipAddress = ipAddress[:strings.Index(ipAddress, ":")]
508508
}
509509

510-
user, err := database.LoginUserToGPCM(pool, ctx, userId, gsbrCode, profileId, deviceId, ipAddress, g.InGameName, deviceAuth)
510+
user, err := database.LoginUserToGPCM(pool, ctx, userId, gsbrCode, profileId, deviceId, ipAddress, g.InGameName, deviceAuth, csnum)
511511
g.User = user
512512

513513
if err != nil {

nas/auth.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,13 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
271271
}
272272
}
273273

274+
csnum, ok := fields["csnum"]
275+
if !ok || len(csnum) > 16 { // Picked a random length. Serial numbers appear to be anywhere from 9-12?
276+
logging.Error(moduleName, "Missing or invalid csnum in form")
277+
param["returncd"] = "103"
278+
return param
279+
}
280+
274281
var authToken, challenge string
275282
switch unitcdInt {
276283
// ds
@@ -285,10 +292,10 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
285292
// Only later DS games send this
286293
ingamesn, ok := fields["ingamesn"]
287294
if ok {
288-
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, 0, isLocalhost)
295+
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, 0, isLocalhost, csnum)
289296
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname), "ingamesn:", aurora.Cyan(ingamesn))
290297
} else {
291-
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", 0, isLocalhost)
298+
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", 0, isLocalhost, csnum)
292299
logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname))
293300
}
294301

@@ -320,7 +327,7 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st
320327
return param
321328
}
322329

323-
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], 1, isLocalhost)
330+
authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], 1, isLocalhost, csnum)
324331
logging.Notice(moduleName, "Login (Wii)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "ingamesn:", aurora.Cyan(fields["ingamesn"]))
325332
}
326333

schema.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ ALTER TABLE ONLY public.users
4848
ADD IF NOT EXISTS ban_reason_hidden character varying,
4949
ADD IF NOT EXISTS ban_moderator character varying,
5050
ADD IF NOT EXISTS ban_tos boolean,
51-
ADD IF NOT EXISTS open_host boolean DEFAULT false;
51+
ADD IF NOT EXISTS open_host boolean DEFAULT false,
52+
ADD IF NOT EXISTS csnum character varying[];
5253

5354
--
5455
-- Change ng_device_id from bigint to bigint[]

0 commit comments

Comments
 (0)