ModernUO/Projects/UOContent.Tests/Tests/Accounting/Security/PasswordProtectionTest.cs
Kamron Batman fcff2578dc test(accounts): add a type-only row to the NeedsRehash cost matrix
The existing "old shipping default" row (argon2i, m=8192, t=3) changed type,
memory and time all at once, so it did not independently prove the ArgonType
comparison was load-bearing -- deleting that clause would have left the row
passing on the memory/time checks alone. Add a row that isolates the type
dimension against the current-defaults baseline, alongside the ones that
already isolate memory, iterations and parallelism.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:56:28 -07:00

124 lines
5 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
[InlineData("argon2i", 16384, 1, 1, true)] // right cost, stale type
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"));
}
}