perf(network): skip the redundant password verify on game login

A full login hashed the password twice: once on the account login packet
(0x80) and again on the game login (0x91). The second one re-authenticates a
session authenticated milliseconds earlier, in the same process -- the auth id
guarding it is only issued by PlayServer, which is reachable only after the
first verify succeeded.

Now that the id is bound to the account and address it was issued to, matching
it is proof enough. Per-login game loop cost drops from ~17ms to ~8.5ms.

An id that is unknown, expired, or from another address is treated as an
invalid client and disconnected, which is what an unknown id already did. An id
that is live but issued for a different account proves nothing, so that login
falls back to verifying the password exactly as before.

Also adds the missing return on the unknown-id path, which previously fell
through with a default entry and nulled the client version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-08 00:18:10 -07:00
parent d59da47220
commit 9da1d2e8ef
4 changed files with 61 additions and 13 deletions

View file

@ -3,6 +3,7 @@ using System.Net;
using Server.Accounting;
using Server.Accounting.Security;
using Server.Network;
using Server.Tests.Network;
using Xunit;
namespace Server.Tests.Network.Packets;
@ -142,6 +143,34 @@ public class AuthIdTests : IDisposable
);
}
[Fact]
public void PreAuthenticatedGameLogin_SkipsThePasswordCheck()
{
var account = CreateAccount("authid-preauth-user");
using var ns = PacketTestUtilities.CreateTestNetState();
// A wrong password is accepted only because the auth id already vouched for the account.
var e = new GameServer.GameLoginEventArgs(ns, account.Username, "wrong-password", true);
GameServer.GameServerLoginEvent(e);
Assert.True(e.Accepted);
}
[Fact]
public void GameLoginWithoutPreAuthentication_StillChecksThePassword()
{
var account = CreateAccount("authid-nopreauth-user");
using var ns = PacketTestUtilities.CreateTestNetState();
var wrong = new GameServer.GameLoginEventArgs(ns, account.Username, "wrong-password", false);
GameServer.GameServerLoginEvent(wrong);
Assert.False(wrong.Accepted);
var right = new GameServer.GameLoginEventArgs(ns, account.Username, "hunter2", false);
GameServer.GameServerLoginEvent(right);
Assert.True(right.Accepted);
}
[Fact]
public void GeneratesDistinctAuthIds()
{

View file

@ -343,7 +343,10 @@ public static class AccountHandler
logger.Information("Login: {NetState} Access denied for '{Username}'", e.State, un);
e.Accepted = false;
}
else if (!acct.CheckPassword(pw))
// The auth id already vouched for this account from this address, and it was only issued
// after the account login packet verified the password. Re-deriving the hash here costs
// another full Argon2 verify to answer a question already answered.
else if (!e.PreAuthenticated && !acct.CheckPassword(pw))
{
logger.Information("Login: {NetState} Invalid password for '{Username}'", e.State, un);
e.Accepted = false;

View file

@ -6,13 +6,21 @@ public static partial class GameServer
{
public class GameLoginEventArgs
{
public GameLoginEventArgs(NetState state, string un, string pw)
public GameLoginEventArgs(NetState state, string un, string pw, bool preAuthenticated)
{
State = state;
Username = un;
Password = pw;
PreAuthenticated = preAuthenticated;
}
/// <summary>
/// The auth id presented on this game login was issued to this account, from this address,
/// after the account login packet already verified the password. Read-only: a subscriber
/// must not be able to grant itself the skip.
/// </summary>
public bool PreAuthenticated { get; }
public NetState State { get; }
public string Username { get; }

View file

@ -416,12 +416,6 @@ public static class IncomingAccountPackets
var authId = reader.ReadInt32();
if (!_authIDWindow.TryGetValue(authId, out var ap))
{
state.LogInfo("Invalid client detected, disconnecting...");
state.Disconnect("Unable to find auth id.");
}
if (state.AuthId != 0 && authId != state.AuthId || state.AuthId == 0 && authId != state.Seed)
{
state.LogInfo("Invalid client detected, disconnecting...");
@ -429,14 +423,28 @@ public static class IncomingAccountPackets
return;
}
_authIDWindow.Remove(authId);
state.Version = ap.Version;
state.Seeded = true;
var username = reader.ReadLatin1Safe(30);
var password = reader.ReadLatin1Safe(30);
var e = new GameServer.GameLoginEventArgs(state, username, password);
// Spends the id either way, so a guessed one cannot be reused to probe usernames.
var authResult = ConsumeAuthId(authId, username, state.Address, out var ap);
if (authResult == AuthIdResult.Rejected)
{
state.LogInfo("Invalid client detected, disconnecting...");
state.Disconnect("Unable to find auth id.");
return;
}
state.Version = ap.Version;
state.Seeded = true;
var e = new GameServer.GameLoginEventArgs(
state,
username,
password,
authResult == AuthIdResult.Vouched
);
GameServer.GameServerLoginEvent(e);