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) <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-07 16:44:31 -07:00
parent 1b0bdcf609
commit 1f4952e40e
3 changed files with 27 additions and 4 deletions

View file

@ -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"));
}
}

View file

@ -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);
}

View file

@ -45,7 +45,7 @@
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.10" />
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.2" />
<PackageReference Include="Argon2.Bindings" Version="1.19.0" />
<PackageReference Include="Argon2.Bindings" Version="1.20.0" />
<PackageReference Include="ModernUO.CodeGeneratedEvents.Annotations" Version="1.0.0" />
<PackageReference Include="ModernUO.CodeGeneratedEvents.Generator" Version="1.0.3.2" PrivateAssets="all" />
<PackageReference Include="ZstdNet" Version="1.5.7" />