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)}"; } }