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.
63 lines
2.9 KiB
C#
63 lines
2.9 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: PBKDF2PasswordProtection.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.Buffers.Binary;
|
|
using System.Security.Cryptography;
|
|
using Server.Text;
|
|
|
|
namespace Server.Accounting.Security;
|
|
|
|
public class PBKDF2PasswordProtection : IPasswordProtection
|
|
{
|
|
private const ushort m_MinIterations = 1024;
|
|
private const ushort m_MaxIterations = 1536;
|
|
private const int m_SaltSize = 8;
|
|
private const int m_HashSize = 32;
|
|
private const int m_OutputSize = 2 + m_SaltSize + m_HashSize;
|
|
public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection();
|
|
|
|
public string EncryptPassword(string plainPassword)
|
|
{
|
|
Span<byte> output = stackalloc byte[m_OutputSize];
|
|
|
|
// The cryptographic RNG, not Utility's. The game RNG is a shared System.Random -- unsafe to
|
|
// touch from another thread, and game state besides.
|
|
var iterations = RandomNumberGenerator.GetInt32(m_MinIterations, m_MaxIterations + 1);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations);
|
|
|
|
var salt = output.Slice(2, m_SaltSize);
|
|
RandomNumberGenerator.Fill(salt);
|
|
|
|
var hash = output.Slice(2 + m_SaltSize, m_HashSize);
|
|
Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256);
|
|
|
|
return output.ToHexString();
|
|
}
|
|
|
|
public bool ValidatePassword(string encryptedPassword, string plainPassword)
|
|
{
|
|
Span<byte> encryptedBytes = stackalloc byte[m_OutputSize];
|
|
encryptedPassword.GetBytes(encryptedBytes);
|
|
|
|
var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]);
|
|
var salt = encryptedBytes.Slice(2, m_SaltSize);
|
|
|
|
Span<byte> hash = stackalloc byte[m_HashSize];
|
|
Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256);
|
|
|
|
return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]);
|
|
}
|
|
}
|