Argon2's PHC string embeds m, t and p, so verification uses the parameters stored with each account rather than the configured ones -- and verify is the hot path. CheckPassword only rehashed when the ALGORITHM changed, never when its cost parameters did, so changing the defaults reached nobody on an established shard and the change was cosmetic. IPasswordProtection.NeedsRehash defaults to false, leaving PBKDF2 and the HashAlgorithm protections untouched; only Argon2 carries cost in its stored value. Logged at Debug because a restart migrates the whole population at once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
123 lines
4.9 KiB
C#
123 lines
4.9 KiB
C#
using System;
|
|
using Server.Accounting;
|
|
using Server.Accounting.Security;
|
|
using Xunit;
|
|
|
|
namespace Server.Tests.Accounting.Security;
|
|
|
|
public class PasswordProtectionTest
|
|
{
|
|
private const string plainPassword = "hello-good-sir";
|
|
|
|
[Theory]
|
|
[InlineData(typeof(Argon2PasswordProtection), null)]
|
|
[InlineData(typeof(PBKDF2PasswordProtection), null)]
|
|
[InlineData(typeof(HashAlgorithmPasswordProtection), "MD5")]
|
|
[InlineData(typeof(HashAlgorithmPasswordProtection), "SHA1")]
|
|
[InlineData(typeof(HashAlgorithmPasswordProtection), "SHA2")]
|
|
public void TestValidates(Type protectionType, string algorithmType)
|
|
{
|
|
IPasswordProtection passwordProtection;
|
|
if (protectionType == typeof(HashAlgorithmPasswordProtection))
|
|
{
|
|
passwordProtection = algorithmType switch
|
|
{
|
|
"SHA1" => HashAlgorithmPasswordProtection.SHA1Instance,
|
|
"SHA2" => HashAlgorithmPasswordProtection.SHA2Instance,
|
|
_ => HashAlgorithmPasswordProtection.MD5Instance,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection;
|
|
}
|
|
|
|
if (passwordProtection == null)
|
|
{
|
|
Assert.Fail($"{protectionType.Name} is not an IPasswordProtection.");
|
|
}
|
|
|
|
var encryptedPassword = passwordProtection.EncryptPassword(plainPassword);
|
|
|
|
Assert.True(passwordProtection.ValidatePassword(encryptedPassword, plainPassword));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(typeof(Argon2PasswordProtection), null)]
|
|
[InlineData(typeof(PBKDF2PasswordProtection), null)]
|
|
[InlineData(typeof(HashAlgorithmPasswordProtection), "MD5")]
|
|
[InlineData(typeof(HashAlgorithmPasswordProtection), "SHA1")]
|
|
[InlineData(typeof(HashAlgorithmPasswordProtection), "SHA2")]
|
|
public void TestPasswordDoesNotValidate(Type protectionType, string algorithmType)
|
|
{
|
|
IPasswordProtection passwordProtection;
|
|
if (protectionType == typeof(HashAlgorithmPasswordProtection))
|
|
{
|
|
passwordProtection = algorithmType switch
|
|
{
|
|
"SHA1" => HashAlgorithmPasswordProtection.SHA1Instance,
|
|
"SHA2" => HashAlgorithmPasswordProtection.SHA2Instance,
|
|
_ => HashAlgorithmPasswordProtection.MD5Instance,
|
|
};
|
|
}
|
|
else
|
|
{
|
|
passwordProtection = Activator.CreateInstance(protectionType) as IPasswordProtection;
|
|
}
|
|
|
|
if (passwordProtection == null)
|
|
{
|
|
Assert.Fail($"{protectionType.Name} is not an IPasswordProtection.");
|
|
}
|
|
|
|
var encryptedPassword = passwordProtection.EncryptPassword(plainPassword);
|
|
|
|
Assert.False(passwordProtection.ValidatePassword(encryptedPassword, "Not the same password"));
|
|
}
|
|
|
|
// Produced by ModernUO's shipping default before this change: Argon2i, m=8192, t=3, p=1.
|
|
// Pinned as a literal so it cannot drift with the configured defaults. Password: "hunter2".
|
|
private const string LegacyArgon2iHash =
|
|
"$argon2i$v=19$m=8192,t=3,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
|
|
|
|
[Fact]
|
|
public void Argon2_ValidatesLegacyArgon2iHash()
|
|
{
|
|
Assert.True(Argon2PasswordProtection.Instance.ValidatePassword(LegacyArgon2iHash, "hunter2"));
|
|
Assert.False(Argon2PasswordProtection.Instance.ValidatePassword(LegacyArgon2iHash, "wrong"));
|
|
}
|
|
|
|
[Theory]
|
|
// type, memory, time, parallelism -> expected NeedsRehash
|
|
[InlineData("argon2id", 16384, 1, 1, false)] // current defaults
|
|
[InlineData("argon2i", 8192, 3, 1, true)] // the old shipping default
|
|
[InlineData("argon2id", 8192, 1, 1, true)] // right type, stale memory
|
|
[InlineData("argon2id", 16384, 3, 1, true)] // right type, stale iterations
|
|
[InlineData("argon2id", 16384, 1, 2, true)] // right type, stale parallelism
|
|
public void Argon2_NeedsRehash_ComparesTypeAndCost(
|
|
string type, int memory, int time, int parallelism, bool expected
|
|
)
|
|
{
|
|
var hash = $"${type}$v=19$m={memory},t={time},p={parallelism}$" +
|
|
"LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
|
|
|
|
Assert.Equal(expected, Argon2PasswordProtection.Instance.NeedsRehash(hash));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("not-a-hash")]
|
|
public void Argon2_NeedsRehash_IsTrueForUnparseableHashes(string hash)
|
|
{
|
|
Assert.True(Argon2PasswordProtection.Instance.NeedsRehash(hash));
|
|
}
|
|
|
|
[Fact]
|
|
public void NonArgon2Protections_NeverNeedRehash()
|
|
{
|
|
Assert.False(PBKDF2PasswordProtection.Instance.NeedsRehash("anything"));
|
|
Assert.False(HashAlgorithmPasswordProtection.SHA2Instance.NeedsRehash("anything"));
|
|
Assert.False(HashAlgorithmPasswordProtection.SHA1Instance.NeedsRehash("anything"));
|
|
Assert.False(HashAlgorithmPasswordProtection.MD5Instance.NeedsRehash("anything"));
|
|
}
|
|
}
|