From 1f4952e40e35ca09cbe27be34220270a2f0252c4 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:44:31 -0700 Subject: [PATCH] feat(accounts): default to Argon2id at 16 MiB, t=1 Argon2.Bindings 1.20.0 resolves the Argon2 type from the stored hash, so existing Argon2i credentials keep verifying after this switch. Without that bump this change locks out every existing account -- the pinned legacy-hash test fails on 1.19.0 for exactly that reason. 16 MiB t=1 measures 8.51 ms against the old 8 MiB t=3 Argon2i at 10.11 ms: cheaper AND stronger. Memory-hardness resists GPU and ASIC cracking; iterations mostly buy wall-clock, so trade t down for m up. It also stays below the 16-32 MiB L3 inflection, which starts to matter once hashing moves off the game loop and contends with the loop's working set. Co-Authored-By: Claude Opus 5 (1M context) --- .../Security/PasswordProtectionTest.cs | 12 ++++++++++++ .../Security/Argon2PasswordProtection.cs | 17 ++++++++++++++--- Projects/UOContent/UOContent.csproj | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs b/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs index effd2da88..17a25cd3b 100644 --- a/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs +++ b/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs @@ -74,4 +74,16 @@ public class PasswordProtectionTest Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password")); } + + // Produced by ModernUO's shipping default before this change: Argon2i, m=8192, t=3, p=1. + // Pinned as a literal so it cannot drift with the configured defaults. Password: "hunter2". + private const string LegacyArgon2iHash = + "$argon2i$v=19$m=8192,t=3,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw"; + + [Fact] + public void Argon2_ValidatesLegacyArgon2iHash() + { + Assert.True(Argon2PasswordProtection.Instance.ValidatePassword(LegacyArgon2iHash, "hunter2")); + Assert.False(Argon2PasswordProtection.Instance.ValidatePassword(LegacyArgon2iHash, "wrong")); + } } diff --git a/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs b/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs index 99f320a78..a008de11b 100644 --- a/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs +++ b/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs @@ -21,11 +21,22 @@ public class Argon2PasswordProtection : IPasswordProtection { public static IPasswordProtection Instance = new Argon2PasswordProtection(); - private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: RandomNumberGenerator.Create()); + // Argon2id over Argon2i: RFC 9106 recommends Argon2i only where side-channel resistance is + // required and memory is scarce. 16 MiB at t=1 measures cheaper than the old 8 MiB at t=3 + // (8.5 ms vs 10.1 ms) while doubling memory-hardness, which is the property that resists GPU + // and ASIC cracking; iterations mostly buy wall-clock. p=1 because native argon2 spawns a + // thread per lane, which is oversubscription on the 1-2 core hosts this path exists to serve. + private readonly Argon2PasswordHasher _passwordHasher = new( + time: 1, + memory: 16384, + parallel: 1, + type: Argon2Type.Argon2id, + rng: RandomNumberGenerator.Create() + ); public string EncryptPassword(string plainPassword) => - m_PasswordHasher.Hash(plainPassword); + _passwordHasher.Hash(plainPassword); public bool ValidatePassword(string encryptedPassword, string plainPassword) => - m_PasswordHasher.Verify(encryptedPassword, plainPassword); + _passwordHasher.Verify(encryptedPassword, plainPassword); } diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index bf288e629..d9961f38d 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -45,7 +45,7 @@ - +