Every ModernUO shard has always run Argon2, so the RunUO/ServUO migration shapes the repair path existed to recover do not occur in practice. It was never free: it retried a failed verify against the other phrase rule, and nothing in a stored hash separates a mis-migrated credential from a password that merely begins with the username -- which is why it needed a per-account tag on top of the config switch to be safe at all. Removing it takes accountSecurity.repairMigratedPasswords, the RepairPasswordTag opt-in and the forced rehash with it. The rehash on a successful login is now implicit: stale algorithm or stale parameters, nothing else. Also trims the development narrative out of the comments left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
58 lines
2.8 KiB
C#
58 lines
2.8 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();
|
|
|
|
// 16 MiB at t=1 is cheaper than 8 MiB at t=3 (8.5 ms vs 10.1 ms) and twice as memory-hard, which
|
|
// is what resists GPU and ASIC cracking. p=1: native argon2 spawns a thread per lane.
|
|
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);
|
|
|
|
// The PHC string carries the parameters it was hashed with, so verification uses those rather
|
|
// than the configured ones. Comparing them is what lets a parameter change reach existing
|
|
// accounts.
|
|
public bool NeedsRehash(string encryptedPassword)
|
|
{
|
|
// Unparseable but verified: a format this build does not understand, so rewrite it.
|
|
if (!Argon2PasswordHasher.TryExtractMetadataValues(encryptedPassword, out var values))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|