Every ModernUO shard has always run Argon2, so the RunUO/ServUO migration shapes the repair path existed to recover do not occur in practice. It was never free: it retried a failed verify against the other phrase rule, and nothing in a stored hash separates a mis-migrated credential from a password that merely begins with the username -- which is why it needed a per-account tag on top of the config switch to be safe at all. Removing it takes accountSecurity.repairMigratedPasswords, the RepairPasswordTag opt-in and the forced rehash with it. The rehash on a successful login is now implicit: stale algorithm or stale parameters, nothing else. Also trims the development narrative out of the comments left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
74 lines
3 KiB
C#
74 lines
3 KiB
C#
using System;
|
|
using Server.Accounting;
|
|
using Server.Accounting.Security;
|
|
using Xunit;
|
|
|
|
namespace Server.Tests.Accounting;
|
|
|
|
[Collection("Sequential UOContent Tests")]
|
|
public class AccountPasswordTests : IDisposable
|
|
{
|
|
private const string Password = "hunter2";
|
|
|
|
// CurrentAlgorithm is process-wide state shared with the rest of the collection.
|
|
private readonly PasswordProtectionAlgorithm _originalAlgorithm = AccountSecurity.CurrentAlgorithm;
|
|
|
|
public void Dispose() => AccountSecurity.CurrentAlgorithm = _originalAlgorithm;
|
|
|
|
[Theory]
|
|
[InlineData(PasswordProtectionAlgorithm.SHA1)]
|
|
[InlineData(PasswordProtectionAlgorithm.SHA2)]
|
|
[InlineData(PasswordProtectionAlgorithm.PBKDF2)]
|
|
[InlineData(PasswordProtectionAlgorithm.Argon2)]
|
|
public void NewAccount_CanLogIn(PasswordProtectionAlgorithm algorithm)
|
|
{
|
|
AccountSecurity.CurrentAlgorithm = algorithm;
|
|
var account = new Account($"new-{algorithm}-user", Password);
|
|
|
|
Assert.Equal(algorithm, account.PasswordAlgorithm);
|
|
Assert.True(account.CheckPassword(Password));
|
|
Assert.False(account.CheckPassword("wrong-password"));
|
|
}
|
|
|
|
// SetPassword assigns PasswordAlgorithm before deriving the phrase from it. Reversing those two
|
|
// lines salts the hash by the outgoing algorithm's rule and stores it under the incoming one,
|
|
// which verifies once and then never again.
|
|
[Theory]
|
|
[InlineData(PasswordProtectionAlgorithm.SHA1)]
|
|
[InlineData(PasswordProtectionAlgorithm.SHA2)]
|
|
[InlineData(PasswordProtectionAlgorithm.PBKDF2)]
|
|
public void UpgradingAlgorithm_DoesNotLockTheAccountOut(PasswordProtectionAlgorithm from)
|
|
{
|
|
AccountSecurity.CurrentAlgorithm = from;
|
|
var account = new Account($"upgrade-{from}-user", Password);
|
|
Assert.True(account.CheckPassword(Password));
|
|
|
|
AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
|
|
|
|
Assert.True(account.CheckPassword(Password));
|
|
Assert.Equal(PasswordProtectionAlgorithm.Argon2, account.PasswordAlgorithm);
|
|
|
|
// Must verify against what the rehash wrote.
|
|
Assert.True(account.CheckPassword(Password));
|
|
Assert.False(account.CheckPassword("wrong-password"));
|
|
}
|
|
|
|
[Fact]
|
|
public void StaleArgon2Parameters_AreRehashedOnLogin()
|
|
{
|
|
AccountSecurity.CurrentAlgorithm = PasswordProtectionAlgorithm.Argon2;
|
|
var account = new Account("stale-params-user", Password);
|
|
|
|
// The shipping default before this change: Argon2i, m=8192, t=3, p=1.
|
|
account.Password =
|
|
"$argon2i$v=19$m=8192,t=3,p=1$LD1XJz7P3wQmIJ+Tu6ScgA$NO5hBABsHQ172C5nDO2X4gWnB4jDef3x6WhLdVE2LFw";
|
|
|
|
Assert.True(account.CheckPassword(Password));
|
|
Assert.StartsWith("$argon2id$v=19$m=16384,t=1,p=1$", account.Password);
|
|
|
|
// Already current: verifying again must not rewrite the hash.
|
|
var afterFirst = account.Password;
|
|
Assert.True(account.CheckPassword(Password));
|
|
Assert.Equal(afterFirst, account.Password);
|
|
}
|
|
}
|