The worker was Argon2-only and kept its own protection instance. Both are now unnecessary, but not for the reason the code gave. CreateIsolated() was justified by the RNG, which was wrong. Argon2's Verify is static-backed and stackalloc throughout, and the salt RNG is a stateless syscall wrapper -- neither has state to race over. The real blocker was HashAlgorithmPasswordProtection, which retains a HashAlgorithm carrying the running digest across HashCore/HashFinal, shared through process-wide singletons. Two threads there corrupt each other. That is fixed at the source: hashing now goes through the one-shot static APIs, which have no such state, allocate nothing, and produce identical bytes. Literal digests are pinned in a test first, because these are compared as strings against every account database -- any drift would lock out every SHA and MD5 account at once. PBKDF2 drew its iteration count from Utility.RandomMinMax, a shared System.Random that is both thread-unsafe and game state. It now uses the cryptographic RNG, matching the salt beside it. With all three safe, the worker no longer needs to know which algorithm it is running, and the dispatch conditions collapse to "is off-loop available". A cheap digest now pays a thread hop it does not need, which costs login latency we have already decided not to care about, and saves loop time we do.
67 lines
2.9 KiB
C#
67 lines
2.9 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: HashAlgorithmPasswordProtection.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;
|
|
using System.Security.Cryptography;
|
|
using Server.Text;
|
|
|
|
namespace Server.Accounting.Security;
|
|
|
|
/// <summary>
|
|
/// The obsolete unsalted digests, kept only so imported accounts can log in once and be upgraded.
|
|
///
|
|
/// Hashing goes through the one-shot static APIs rather than a retained <see cref="HashAlgorithm"/>.
|
|
/// A <see cref="HashAlgorithm"/> instance carries the running digest across HashCore/HashFinal, so
|
|
/// two threads sharing one corrupt each other's result -- and these are process-wide singletons.
|
|
/// The static form has no such state, allocates nothing, and produces identical bytes.
|
|
/// </summary>
|
|
public class HashAlgorithmPasswordProtection : IPasswordProtection
|
|
{
|
|
private enum Kind
|
|
{
|
|
MD5,
|
|
SHA1,
|
|
SHA512
|
|
}
|
|
|
|
public static readonly IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(Kind.MD5);
|
|
public static readonly IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(Kind.SHA1);
|
|
public static readonly IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(Kind.SHA512);
|
|
|
|
private const int MaxDigestLength = 64; // SHA512, the largest of the three.
|
|
|
|
private readonly Kind _kind;
|
|
|
|
private HashAlgorithmPasswordProtection(Kind kind) => _kind = kind;
|
|
|
|
public string EncryptPassword(string plainPassword)
|
|
{
|
|
var bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii();
|
|
|
|
Span<byte> digest = stackalloc byte[MaxDigestLength];
|
|
|
|
var written = _kind switch
|
|
{
|
|
Kind.MD5 => MD5.HashData(bytes, digest),
|
|
Kind.SHA1 => SHA1.HashData(bytes, digest),
|
|
_ => SHA512.HashData(bytes, digest)
|
|
};
|
|
|
|
return digest[..written].ToHexString();
|
|
}
|
|
|
|
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
|
|
EncryptPassword(plainPassword) == encryptedPassword;
|
|
}
|