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