An Argon2 verify is ~8.9 ms of frozen world per login attempt, successful or not, so a credential flood is a full-cost stall per packet without needing valid credentials. Measurement puts the on-loop saving at 3.5-8.9 ms: the hand-off costs ~220 ns, and the only real residue is the loop's own work slowing while a memory-hard KDF evicts shared L3. One worker, not a pool. The per-login contention tax falls with concurrency while total loop damage rises, so one hasher harms the loop least; and a single hasher cannot cost the loop more than the inline verify under any scheduling regime, because at worst it takes an equal share of one core. That bound is what lets the measurement extrapolate to hardware we cannot inspect, and a pool breaks it. It also caps live Argon2 arenas at one, which answers memory exhaustion without a separate mechanism. Scoped to Argon2-stored accounts. SHA and MD5 protections share a HashAlgorithm instance whose ComputeHash is not thread safe, and they cost microseconds anyway. Their one-time rehash into Argon2 stays on the loop too: it costs a migrating account a single 8.9 ms login exactly as today. The worker parks on an AutoResetEvent and never spins. The spin in SerializationThreadWorker exists to wait on a producer mid-drain; there is no such race here, so this is strictly cheaper at idle. It yields while the world is in PendingSave or Saving -- PendingSave included, because the serialization threads are already awake and spinning on an empty queue by then. Phrase derivation moves to AccountSecurity.DerivePhrase so verification and rehash cannot disagree about the rule, which is the shape of the lockout fixed in #2562. ApplyPasswordUpgrade refuses to write when the stored hash changed while the verify ran, so a password set mid-flight is not replaced by a rehash of the one it superseded.
82 lines
3.6 KiB
C#
82 lines
3.6 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: AccountSecurity.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;
|
|
|
|
namespace Server.Accounting.Security;
|
|
|
|
public enum PasswordProtectionAlgorithm
|
|
{
|
|
// Obsolete algorithms from RunUO. These are not secure!
|
|
// They are included for password upgrades only.
|
|
None,
|
|
MD5,
|
|
SHA1,
|
|
|
|
// Supported algorithms
|
|
SHA2, // ServUO compatibility
|
|
PBKDF2,
|
|
Argon2 // Recommended algorithm for real security.
|
|
}
|
|
|
|
public static class AccountSecurity
|
|
{
|
|
public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; }
|
|
|
|
public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm);
|
|
|
|
public static void Configure()
|
|
{
|
|
CurrentAlgorithm =
|
|
ServerConfiguration.GetOrUpdateSetting(
|
|
"accountSecurity.encryptionAlgorithm",
|
|
PasswordProtectionAlgorithm.Argon2
|
|
);
|
|
|
|
if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2)
|
|
{
|
|
throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The string actually fed to the KDF. SHA1 and SHA2 salt by username; everything else hashes
|
|
/// the password alone. Verification must derive with the algorithm the stored hash was made
|
|
/// with, and a rehash with the one it is moving to -- deriving with the wrong one produces a
|
|
/// hash that verifies once and never again.
|
|
/// </summary>
|
|
public static string DerivePhrase(
|
|
PasswordProtectionAlgorithm algorithm, string username, string plainPassword
|
|
) =>
|
|
algorithm is PasswordProtectionAlgorithm.SHA1 or PasswordProtectionAlgorithm.SHA2
|
|
? $"{username}{plainPassword}"
|
|
: plainPassword;
|
|
|
|
public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
|
|
{
|
|
var passwordProtection = algorithm switch
|
|
{
|
|
PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance,
|
|
PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance,
|
|
PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance,
|
|
PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
|
|
PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
|
|
PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"),
|
|
_ => throw new Exception("No algorithm")
|
|
};
|
|
|
|
return passwordProtection;
|
|
}
|
|
}
|