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:
parent
d59da47220
commit
9da1d2e8ef
4 changed files with 61 additions and 13 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue