using GB5Shared.EncryptionHelper;
using Xunit;
namespace EntitlementTests;
///
/// Covers GB5Shared.EncryptionHelper.PasswordHasher — the shared PBKDF2-SHA256 password hasher
/// built for Entitlement's client-facing auth (mirrors DXPBLL.Auth.DXPPasswordHasher's algorithm
/// exactly: 210k iterations, 16-byte random salt, constant-time verify, self-describing
/// "{iterations}.{salt}.{hash}" stored format so a future iteration-count bump doesn't invalidate
/// already-hashed passwords).
///
public class PasswordHasherTests
{
[Fact]
public void Test_Hash_ThenVerify_SamePassword_RoundTrips()
{
var stored = PasswordHasher.Hash("Correct-Horse-Battery-Staple");
Assert.True(PasswordHasher.Verify("Correct-Horse-Battery-Staple", stored));
}
[Fact]
public void Test_Verify_WrongPassword_IsRejected()
{
var stored = PasswordHasher.Hash("Correct-Horse-Battery-Staple");
Assert.False(PasswordHasher.Verify("wrong-password", stored));
}
[Fact]
public void Test_Hash_IsSelfDescribing_StoresIterationCountAndIsNotPlaintext()
{
var stored = PasswordHasher.Hash("some-password");
var parts = stored.Split('.', 3);
Assert.Equal(3, parts.Length);
Assert.Equal("210000", parts[0]);
Assert.DoesNotContain("some-password", stored);
}
[Fact]
public void Test_Hash_TwoCallsSamePassword_ProduceDifferentHashes_DueToRandomSalt()
{
var first = PasswordHasher.Hash("same-password");
var second = PasswordHasher.Hash("same-password");
// Per-password random salt means the stored strings must differ even for identical input...
Assert.NotEqual(first, second);
// ...yet both must still verify correctly against their own stored hash.
Assert.True(PasswordHasher.Verify("same-password", first));
Assert.True(PasswordHasher.Verify("same-password", second));
}
[Fact]
public void Test_Verify_ToleratesLowerLegacyIterationCount_SelfDescribingFormatUpgradePath()
{
// Simulates a password hashed under a lower (now-legacy) iteration count before a future
// bump to Iterations — Verify must read the count from the stored string itself, not the
// class's current constant, so old hashes keep verifying without a forced rehash.
var legacyStored = LegacyHashWithIterationCount("some-password", iterations: 100_000);
Assert.True(PasswordHasher.Verify("some-password", legacyStored));
Assert.False(PasswordHasher.Verify("wrong-password", legacyStored));
}
[Theory]
[InlineData("")]
[InlineData("not-the-right-format")]
[InlineData("210000.onlyoneseparator")]
[InlineData("notanumber.c2FsdA==.aGFzaA==")]
public void Test_Verify_MalformedStoredHash_ReturnsFalse_DoesNotThrow(string malformed)
{
Assert.False(PasswordHasher.Verify("any-password", malformed));
}
///
/// Builds a stored hash string using an arbitrary iteration count, mirroring what Hash()
/// would have produced back when Iterations was set to that lower value.
///
private static string LegacyHashWithIterationCount(string password, int iterations)
{
var salt = System.Security.Cryptography.RandomNumberGenerator.GetBytes(16);
var hash = System.Security.Cryptography.Rfc2898DeriveBytes.Pbkdf2(
password, salt, iterations, System.Security.Cryptography.HashAlgorithmName.SHA256, 32);
return $"{iterations}.{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
}
}