From 8405d09992e0f18d3c89b85bf0bbb8aa9845267b Mon Sep 17 00:00:00 2001 From: wagesj45 Date: Mon, 20 Jul 2026 00:30:37 -0500 Subject: [PATCH] Generalize password hash comparison --- CapyKit/Helpers/SecurityHelper.cs | 81 +++++++++++++++++++--------- Tests/Helpers/SecurityHelperTests.cs | 9 ++-- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/CapyKit/Helpers/SecurityHelper.cs b/CapyKit/Helpers/SecurityHelper.cs index 78a0915..680bf47 100644 --- a/CapyKit/Helpers/SecurityHelper.cs +++ b/CapyKit/Helpers/SecurityHelper.cs @@ -33,10 +33,9 @@ namespace CapyKit.Helpers public static bool CompareHashedPassword(Password existingPassword, string password, string salt, params object[] args) + where T : IPasswordAlgorithm { - var saltBytes = Convert.FromBase64String(salt); - - return CompareHashedPassword(existingPassword, password, saltBytes, args); + return CompareHashedPassword(Convert.ToBase64String(existingPassword.Hash), password, salt, args); } /// @@ -54,13 +53,61 @@ namespace CapyKit.Helpers /// public static bool CompareHashedPassword(Password existingPassword, string password, byte[] salt, params object[] args) + where T : IPasswordAlgorithm { - var providedPassword = typeof(SecurityHelper) - .GetMethod("GetPassword", new Type[] { typeof(string), typeof(byte[]), typeof(object[]) }) - ?.MakeGenericMethod(typeof(T)) - ?.Invoke(null, new object[] { password, salt, args }); + return CompareHashedPassword(existingPassword.Hash, password, salt, args); + } - return existingPassword.Equals(providedPassword); + /// + /// Compares a plaintext password with a persisted Base64-encoded hash and salt. + /// + /// The password hashing algorithm. + /// The Base64-encoded persisted hash. + /// The plaintext password to verify. + /// The Base64-encoded persisted salt. + /// Arguments used to construct the password algorithm. + /// when the password matches the persisted hash. + public static bool CompareHashedPassword(string storedHash, string password, string salt, + params object[] args) + where T : IPasswordAlgorithm + { + if (string.IsNullOrWhiteSpace(storedHash) || string.IsNullOrEmpty(password) || string.IsNullOrWhiteSpace(salt)) + { + return false; + } + + try + { + return CompareHashedPassword(Convert.FromBase64String(storedHash), password, + Convert.FromBase64String(salt), args); + } + catch (FormatException) + { + return false; + } + } + + /// + /// Compares a plaintext password with a persisted hash and salt. + /// + /// The password hashing algorithm. + /// The persisted hash. + /// The plaintext password to verify. + /// The persisted salt. + /// Arguments used to construct the password algorithm. + /// when the password matches the persisted hash. + public static bool CompareHashedPassword(byte[] storedHash, string password, byte[] salt, + params object[] args) + where T : IPasswordAlgorithm + { + if (storedHash == null || storedHash.Length == 0 || string.IsNullOrEmpty(password) || salt == null || salt.Length == 0) + { + return false; + } + + var candidatePassword = GetPassword(password, salt, args); + return candidatePassword is not null && candidatePassword.Hash.Length == storedHash.Length && + CryptographicOperations.FixedTimeEquals(storedHash, candidatePassword.Hash); } public static bool CompareHashedPassword(Password existingPassword, string password, string salt, @@ -97,24 +144,6 @@ namespace CapyKit.Helpers return existingPassword.Equals(providedPassword); } - /// - /// Verifies a plaintext password against a persisted PBKDF2 hash and salt. - /// - /// The plaintext password supplied by the caller. - /// The persisted PBKDF2 hash. - /// The persisted PBKDF2 salt. - /// when the password matches the persisted hash. - public static bool VerifyPbkdf2(string password, byte[] storedHash, byte[] salt) - { - if (string.IsNullOrEmpty(password) || storedHash == null || salt == null) - { - return false; - } - - var candidateHash = Pbkdf2(password, salt).Hash; - return CryptographicOperations.FixedTimeEquals(storedHash, candidateHash); - } - /// /// Produces a deterministic SHA-256 digest for an opaque secret that must be looked up /// without persisting the raw value. diff --git a/Tests/Helpers/SecurityHelperTests.cs b/Tests/Helpers/SecurityHelperTests.cs index d35fc3c..0eef89d 100644 --- a/Tests/Helpers/SecurityHelperTests.cs +++ b/Tests/Helpers/SecurityHelperTests.cs @@ -83,14 +83,17 @@ namespace Tests.Helpers } [Test] - public void VerifyPbkdf2_WithPersistedCredential_ReturnsExpectedResult() + public void CompareHashedPassword_WithPersistedHashAndSalt_ReturnsExpectedResult() { var credential = SecurityHelper.Pbkdf2(Password, Salt); Assert.Multiple(() => { - Assert.That(SecurityHelper.VerifyPbkdf2(Password, credential.Hash, credential.Salt), Is.True); - Assert.That(SecurityHelper.VerifyPbkdf2("WrongPassword", credential.Hash, credential.Salt), Is.False); + Assert.That(SecurityHelper.CompareHashedPassword(credential.Hash, Password, credential.Salt), Is.True); + Assert.That(SecurityHelper.CompareHashedPassword(credential.Hash, "WrongPassword", credential.Salt), Is.False); + Assert.That(SecurityHelper.CompareHashedPassword( + Convert.ToBase64String(credential.Hash), Password, Convert.ToBase64String(credential.Salt)), Is.True); + Assert.That(SecurityHelper.CompareHashedPassword("not-base64", Password, "not-base64"), Is.False); }); }