From 9da1d2e8ef73c5534808ad53914fe1f1bb76899a Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:18:10 -0700 Subject: [PATCH] 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) --- .../Tests/Network/Packets/AuthIdTests.cs | 29 ++++++++++++++++++ .../UOContent/Accounting/AccountHandler.cs | 5 +++- Projects/UOContent/Network/GameServer.cs | 10 ++++++- .../Network/Packets/IncomingAccountPackets.cs | 30 ++++++++++++------- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs b/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs index fc66c6277..0f93bdf66 100644 --- a/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs +++ b/Projects/UOContent.Tests/Tests/Network/Packets/AuthIdTests.cs @@ -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() { diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index ea0c4b9e0..1896883cf 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -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; diff --git a/Projects/UOContent/Network/GameServer.cs b/Projects/UOContent/Network/GameServer.cs index 9fc170d69..72f886164 100644 --- a/Projects/UOContent/Network/GameServer.cs +++ b/Projects/UOContent/Network/GameServer.cs @@ -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; } + /// + /// 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. + /// + public bool PreAuthenticated { get; } + public NetState State { get; } public string Username { get; } diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index 8d34bb06a..3d3f825a7 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -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);