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 @@ - +