diff --git a/database/login.go b/database/login.go index f326441..acadc5d 100644 --- a/database/login.go +++ b/database/login.go @@ -2,30 +2,31 @@ package database import ( "context" + "fmt" "github.com/jackc/pgx/v4/pgxpool" "github.com/logrusorgru/aurora/v3" "wwfc/common" "wwfc/logging" ) -func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32) (User, bool) { +func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32) (User, bool) { var exists bool err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists) if err != nil { return User{}, false } - uniqueNickname := common.Base32Encode(userId) + gsbrcd - user := User{ - ProfileId: profileId, - UserId: userId, - GsbrCode: gsbrcd, - Email: uniqueNickname + "@nds", - UniqueNick: uniqueNickname, + UserId: userId, + GsbrCode: gsbrcd, } if !exists { + user.ProfileId = profileId + user.NgDeviceId = ngDeviceId + user.UniqueNick = common.Base32Encode(userId) + gsbrcd + user.Email = user.UniqueNick + "@nds" + // Create the GPCM account err := user.CreateUser(pool, ctx) if err != nil { @@ -35,9 +36,24 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb 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, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) + var expectedNgId *uint32 + err := pool.QueryRow(ctx, GetUserProfileID, userId, gsbrcd).Scan(&user.ProfileId, &expectedNgId, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName) if err != nil { - return User{}, false + panic(err) + } + + if expectedNgId != nil { + user.NgDeviceId = *expectedNgId + if ngDeviceId != 0 && user.NgDeviceId != ngDeviceId { + logging.Error("DATABASE", "NG device ID mismatch for profile", aurora.Cyan(user.ProfileId), "- expected", aurora.Cyan(fmt.Sprintf("%08x", user.NgDeviceId)), "but got", aurora.Cyan(fmt.Sprintf("%08x", ngDeviceId))) + return User{}, false + } + } else if ngDeviceId != 0 { + user.NgDeviceId = ngDeviceId + _, err := pool.Exec(ctx, UpdateUserNGDeviceID, user.ProfileId, ngDeviceId) + if err != nil { + panic(err) + } } if profileId != 0 && user.ProfileId != profileId { diff --git a/database/user.go b/database/user.go index 45ba166..9c1dc79 100644 --- a/database/user.go +++ b/database/user.go @@ -9,14 +9,15 @@ import ( const ( InsertUser = `INSERT INTO users (user_id, gsbrcd, password, email, unique_nick) VALUES ($1, $2, $3, $4, $5) RETURNING profile_id` - InsertUserWithProfileID = `INSERT INTO users (user_id, gsbrcd, password, email, unique_nick) VALUES ($1, $2, $3, $4, $5)` + InsertUserWithProfileID = `INSERT INTO users (user_id, gsbrcd, password, ng_device_id, email, unique_nick) VALUES ($1, $2, $3, $4, $5, $6)` 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` 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 FROM users WHERE profile_id = $1` 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, email, unique_nick, firstname, lastname FROM users WHERE user_id = $1 AND gsbrcd = $2` + GetUserProfileID = `SELECT profile_id, ng_device_id, email, unique_nick, firstname, lastname FROM users WHERE user_id = $1 AND gsbrcd = $2` GetMKWFriendInfoQuery = `SELECT mariokartwii_friend_info FROM users WHERE profile_id = $1` UpdateMKWFriendInfoQuery = `UPDATE users SET mariokartwii_friend_info = $2 WHERE profile_id = $1` @@ -26,6 +27,7 @@ type User struct { ProfileId uint32 UserId uint64 GsbrCode string + NgDeviceId uint32 Email string UniqueNick string FirstName string @@ -56,7 +58,7 @@ func (user *User) CreateUser(pool *pgxpool.Pool, ctx context.Context) error { return ErrProfileIDInUse } - _, err = pool.Exec(ctx, InsertUserWithProfileID, user.UserId, user.GsbrCode, "", user.Email, user.UniqueNick, user.ProfileId) + _, err = pool.Exec(ctx, InsertUserWithProfileID, user.UserId, user.GsbrCode, "", user.NgDeviceId, user.Email, user.UniqueNick, user.ProfileId) return err } diff --git a/gpcm/login.go b/gpcm/login.go index 19dacfd..58f5393 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -3,6 +3,7 @@ package gpcm import ( "crypto/md5" "crypto/sha1" + "encoding/binary" "encoding/hex" "fmt" "github.com/logrusorgru/aurora/v3" @@ -35,10 +36,10 @@ func generateProof(gpcmChallenge, nasChallenge, authToken, clientChallenge strin return generateResponse(clientChallenge, nasChallenge, authToken, gpcmChallenge) } -func verifySignature(authToken string, signature string) bool { +func verifySignature(authToken string, signature string) uint32 { sigBytes, err := common.Base64DwcEncoding.DecodeString(signature) if err != nil || len(sigBytes) != 0x144 { - return false + return 0 } ngId := sigBytes[0x000:0x004] @@ -69,7 +70,7 @@ func verifySignature(authToken string, signature string) bool { if !verifyECDSA(msPublicKey, msSignature, ngCertBlobHash[:]) { logging.Error("GPCM", "NG cert verify failed") - return false + return 0 } logging.Info("GPCM", "NG cert verified") @@ -88,18 +89,18 @@ func verifySignature(authToken string, signature string) bool { if !verifyECDSA(ngPublicKey, ngSignature, apCertBlobHash[:]) { logging.Error("GPCM", "AP cert verify failed") - return false + return 0 } logging.Info("GPCM", "AP cert verified") authTokenHash := sha1.Sum([]byte(authToken)) if !verifyECDSA(apPublicKey, apSignature, authTokenHash[:]) { logging.Error("GPCM", "Auth token signature failed") - return false + return 0 } logging.Notice("GPCM", "Auth token signature verified; NG ID:", aurora.Cyan(fmt.Sprintf("%08x", ngId))) - return true + return binary.BigEndian.Uint32(ngId) } func (g *GameSpySession) login(command common.GameSpyCommand) { @@ -125,7 +126,17 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { } signature, exists := command.OtherValues["wwfc_sig"] - if !exists || !verifySignature(authToken, signature) { + if !exists { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "Missing authentication signature.", + Fatal: true, + }) + return + } + + var deviceId uint32 + if deviceId = verifySignature(authToken, signature); deviceId == 0 { g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, ErrorString: "The authentication signature is invalid.", @@ -170,7 +181,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { } // Perform the login with the database. - user, ok := database.LoginUserToGPCM(pool, ctx, userId, gsbrcd, cmdProfileId) + user, ok := database.LoginUserToGPCM(pool, ctx, userId, gsbrcd, cmdProfileId, deviceId) if !ok { // There was an error logging in to the GP backend. g.replyError(ErrLogin) diff --git a/schema.sql b/schema.sql index 9fed0cd..f321ee3 100644 --- a/schema.sql +++ b/schema.sql @@ -25,10 +25,11 @@ SET default_table_access_method = heap; -- CREATE TABLE public.users ( - profile_id integer NOT NULL, + profile_id bigint NOT NULL, user_id bigint NOT NULL, gsbrcd character varying NOT NULL, password character varying NOT NULL, + ng_device_id bigint, email character varying NOT NULL, unique_nick character varying NOT NULL, firstname character varying,