diff --git a/database/login.go b/database/login.go index d7a6885..5d70a90 100644 --- a/database/login.go +++ b/database/login.go @@ -29,7 +29,7 @@ const ( SELECT has_ban, ban_tos, ng_device_id, ban_reason FROM users WHERE has_ban = true - AND (profile_id = $2 + AND ((profile_id = $2 OR allow_default_keys = FALSE) OR ng_device_id && (SELECT * FROM known_ng_device_ids) OR last_ip_address = $3 OR ($4 != '' AND last_ip_address = $4)) @@ -38,11 +38,12 @@ const ( ) var ( - ErrDeviceIDMismatch = errors.New("NG device ID mismatch") - ErrProfileBannedTOS = errors.New("profile is banned for violating the Terms of Service") + ErrDeviceIDMismatch = errors.New("NG device ID mismatch") + ErrProhibitedDeviceID = errors.New("used prohibited NG device ID in request") + ErrProfileBannedTOS = errors.New("profile is banned for violating the Terms of Service") ) -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 LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, defaultKey bool, ngDeviceId uint32, ipAddress string, ingamesn string, deviceAuth bool) (User, error) { var exists bool err := pool.QueryRow(ctx, DoesUserExist, userId, gsbrcd).Scan(&exists) if err != nil { @@ -76,12 +77,17 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb } else { var firstName *string var lastName *string + var allowDefaultKeys bool - 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, &allowDefaultKeys) if err != nil { return User{}, err } + if defaultKey && !allowDefaultKeys && !common.GetConfig().AllowDefaultDolphinKeys { + return User{}, ErrProhibitedDeviceID + } + if firstName != nil { user.FirstName = *firstName } @@ -95,15 +101,13 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb for index, id := range user.NgDeviceId { if id == ngDeviceId { validDeviceId = true - break } - if id == 0 { + if !validDeviceId && id == 0 { // Replace the 0 with the actual device ID user.NgDeviceId[index] = ngDeviceId _, err = pool.Exec(ctx, UpdateUserNGDeviceID, user.ProfileId, user.NgDeviceId) validDeviceId = true - break } deviceIdList += aurora.Cyan(fmt.Sprintf("%08x", id)).String() + ", " @@ -171,7 +175,6 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb var banTOS bool var bannedDeviceIdList []uint32 var banReason string - timeNow := time.Now().UTC() err = pool.QueryRow(ctx, SearchUserBan, user.NgDeviceId, user.ProfileId, ipAddress, *lastIPAddress, timeNow).Scan(&banExists, &banTOS, &bannedDeviceIdList, &banReason) diff --git a/database/schema.go b/database/schema.go index 61b99d7..7d69421 100644 --- a/database/schema.go +++ b/database/schema.go @@ -19,8 +19,8 @@ func UpdateTables(pool *pgxpool.Pool, ctx context.Context) { 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 allow_default_keys boolean DEFAULT false; `) pool.Exec(ctx, ` diff --git a/database/user.go b/database/user.go index 0e79d89..e97956c 100644 --- a/database/user.go +++ b/database/user.go @@ -20,7 +20,7 @@ const ( 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, allow_default_keys 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` diff --git a/gpcm/login.go b/gpcm/login.go index 86c05d3..6e8e165 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -52,7 +52,7 @@ var msPublicKey = []byte{ } var commonDeviceIds = []uint32{ - 0x02000001, // Internal use (leaked) + 0x02000001, // Internal use 0x0403ac68, // Dolphin default // Publicly shared key dumps @@ -69,10 +69,13 @@ var commonDeviceIds = []uint32{ 0x247dd10b, } -func verifySignature(moduleName string, authToken string, signature string) uint32 { +func verifySignature(moduleName string, authToken string, signature string) (defaultKey bool, result uint32) { + result = 0 + defaultKey = false + sigBytes, err := common.Base64DwcEncoding.DecodeString(signature) if err != nil || (len(sigBytes) != 0x144 && len(sigBytes) != 0x148) { - return 0 + return } ngId := sigBytes[0x000:0x004] @@ -81,7 +84,12 @@ func verifySignature(moduleName string, authToken string, signature string) uint // Skip authentication signature verification for common device IDs (the caller should handle this) for _, defaultDeviceId := range commonDeviceIds { if binary.BigEndian.Uint32(ngId) == defaultDeviceId { - return defaultDeviceId + if !allowDefaultDolphinKeys { + logging.Warn(moduleName, "Using default NG device ID") + } + result = defaultDeviceId + defaultKey = true + return } } } @@ -115,7 +123,7 @@ func verifySignature(moduleName string, authToken string, signature string) uint if !verifyECDSA(msPublicKey, msSignature, ngCertBlobHash[:]) { logging.Error(moduleName, "NG cert verify failed") - return 0 + return } logging.Info(moduleName, "NG cert verified") @@ -134,18 +142,19 @@ func verifySignature(moduleName string, authToken string, signature string) uint if !verifyECDSA(ngPublicKey, ngSignature, apCertBlobHash[:]) { logging.Error(moduleName, "AP cert verify failed") - return 0 + return } logging.Info(moduleName, "AP cert verified") authTokenHash := sha1.Sum([]byte(authToken)) if !verifyECDSA(apPublicKey, apSignature, authTokenHash[:]) { logging.Error(moduleName, "Auth token signature failed") - return 0 + return } logging.Notice(moduleName, "Auth token signature verified; NG ID:", aurora.Cyan(fmt.Sprintf("%08x", ngId))) - return binary.BigEndian.Uint32(ngId) + result = binary.BigEndian.Uint32(ngId) + return } func (g *GameSpySession) login(command common.GameSpyCommand) { @@ -206,6 +215,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { } deviceAuth := false + defaultKey := false if g.UnitCode == UnitCodeWii { if isLocalhost && !payloadVerExists && !signatureExists { // Players using the DNS, need patching using a QR2 exploit @@ -222,7 +232,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { g.NeedsExploit = true deviceAuth = false } else { - deviceId = g.verifyExLoginInfo(command, authToken) + defaultKey, deviceId = g.verifyExLoginInfo(command, authToken) if deviceId == 0 { return } @@ -261,7 +271,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, defaultKey, deviceId, deviceAuth) { return } @@ -348,12 +358,12 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) { return } - deviceId := g.verifyExLoginInfo(command, g.AuthToken) + defaultKey, deviceId := g.verifyExLoginInfo(command, g.AuthToken) if deviceId == 0 { return } - if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, deviceId, true) { + if !g.performLoginWithDatabase(g.User.UserId, g.User.GsbrCode, 0, defaultKey, deviceId, true) { return } @@ -361,10 +371,11 @@ func (g *GameSpySession) exLogin(command common.GameSpyCommand) { qr2.SetDeviceAuthenticated(g.User.ProfileId) } -func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authToken string) uint32 { +func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authToken string) (defaultKey bool, deviceId uint32) { payloadVer, payloadVerExists := command.OtherValues["wl:ver"] signature, signatureExists := command.OtherValues["wl:sig"] - deviceId := uint32(0) + defaultKey = false + deviceId = 0 if !payloadVerExists || payloadVer != "5" { g.replyError(GPError{ @@ -373,7 +384,7 @@ func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authTo Fatal: true, WWFCMessage: WWFCMsgPayloadInvalid, }) - return 0 + return } if !signatureExists { @@ -383,59 +394,32 @@ func (g *GameSpySession) verifyExLoginInfo(command common.GameSpyCommand, authTo Fatal: true, WWFCMessage: WWFCMsgUnknownLoginError, }) - return 0 + return } - if deviceId = verifySignature(g.ModuleName, authToken, signature); deviceId == 0 { + defaultKey, deviceId = verifySignature(g.ModuleName, authToken, signature) + if deviceId == 0 { g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode, ErrorString: "The authentication signature is invalid.", Fatal: true, WWFCMessage: WWFCMsgUnknownLoginError, }) - return 0 + return } g.DeviceId = deviceId - - if !allowDefaultDolphinKeys { - // Check common device IDs - for _, defaultDeviceId := range commonDeviceIds { - if deviceId != defaultDeviceId { - continue - } - - if strings.HasPrefix(g.HostPlatform, "Dolphin") { - g.replyError(GPError{ - ErrorCode: ErrLogin.ErrorCode, - ErrorString: "Prohibited device ID used in signature.", - Fatal: true, - WWFCMessage: WWFCMsgDolphinSetupRequired, - }) - } else { - g.replyError(GPError{ - ErrorCode: ErrLogin.ErrorCode, - ErrorString: "Prohibited device ID used in signature.", - Fatal: true, - WWFCMessage: WWFCMsgUnknownLoginError, - }) - } - - return 0 - } - } - - return deviceId + return } -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, defaultKey bool, deviceId uint32, deviceAuth bool) 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, defaultKey, deviceId, ipAddress, g.InGameName, deviceAuth) g.User = user if err != nil { @@ -471,6 +455,22 @@ func (g *GameSpySession) performLoginWithDatabase(userId uint64, gsbrCode string WWFCMessage: WWFCMsgConsoleMismatch, }) } + } else if err == database.ErrProhibitedDeviceID { + if strings.HasPrefix(g.HostPlatform, "Dolphin") { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "Prohibited device ID used in signature.", + Fatal: true, + WWFCMessage: WWFCMsgDolphinSetupRequired, + }) + } else { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "Prohibited device ID used in signature.", + Fatal: true, + WWFCMessage: WWFCMsgUnknownLoginError, + }) + } } else if err == database.ErrProfileBannedTOS { g.replyError(GPError{ ErrorCode: ErrLogin.ErrorCode,