ModernUO/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs
Kamron Batman ccfbc1878a fix(accounts): compare salt and digest lengths in NeedsRehash
HashMetadataValues carries SaltLength and HashLength alongside the type and
the cost parameters, and NeedsRehash ignored both. A hash with a 16-byte
digest or an 8-byte salt verified fine and was judged current forever --
exactly the drift this method exists to catch. Unreachable today, since
every hash ModernUO has written uses the library defaults of 32 and 16, but
that is a property of the current configuration rather than of the check.

The two lengths are the decoded sizes of the base64 segments rather than
entries in the m/t/p list, so they cannot be varied through the existing
theory template; they get their own rows with pinned literals. The
"current defaults" row of that theory is the negative control.

Also name the benchmark behind the 8.5 ms / 10.1 ms figures in the parameter
comment: ModernUO-Benchmarks/Benchmarks/Argon2Hashing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:30:54 -07:00

67 lines
3.7 KiB
C#

/*************************************************************************
* ModernUO *
* Copyright 2019-2026 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Argon2PasswordProtection.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System.Security.Cryptography;
namespace Server.Accounting.Security;
public class Argon2PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new Argon2PasswordProtection();
// 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, measured by ModernUO-Benchmarks/Benchmarks/Argon2Hashing) 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) =>
_passwordHasher.Hash(plainPassword);
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
_passwordHasher.Verify(encryptedPassword, plainPassword);
public bool NeedsRehash(string encryptedPassword)
{
// Argon2's PHC string embeds m, t and p, so verification uses the parameters stored with
// each account rather than the configured ones. Without this comparison a parameter change
// would reach nobody: CheckPassword only rehashed when the *algorithm* changed.
if (!Argon2PasswordHasher.TryExtractMetadataValues(encryptedPassword, out var values))
{
// Only reached after a successful verify, so an unparseable string means a format this
// build does not understand. Rewriting it into the current one is strictly better.
return true;
}
// The digest and salt lengths live in the base64 segments rather than the parameter list,
// but they are just as much a part of "was this produced with the parameters we configure
// today". Casting the hasher's uint properties keeps the comparison signed-vs-signed; both
// are small byte counts, so the narrowing cannot lose anything.
return values.ArgonType != _passwordHasher.ArgonType
|| values.MemoryCost != _passwordHasher.MemoryCost
|| values.TimeCost != _passwordHasher.TimeCost
|| values.Parallelism != _passwordHasher.Parallelism
|| values.HashLength != (int)_passwordHasher.HashLength
|| values.SaltLength != (int)_passwordHasher.SaltLength;
}
}