From 186e99a6f4df0eae45b8cdcf5c8ee17567490066 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sat, 8 Nov 2025 12:15:07 +0100 Subject: [PATCH] (feature): updated password hasher utilities to support pepper for additional security --- README.md | 60 +++++++++++++++++-- .../Security/JwtGeneratorTests.cs | 16 ++--- .../Security/PasswordHasherTests.cs | 36 +++++------ src/MaksIT.Core/MaksIT.Core.csproj | 2 +- src/MaksIT.Core/Security/PasswordHasher.cs | 12 ++-- 5 files changed, 90 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index cfdfc63..bbd775f 100644 --- a/README.md +++ b/README.md @@ -868,17 +868,18 @@ ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var error) ### Password Hasher -The `PasswordHasher` class provides methods for securely hashing and validating passwords. +The `PasswordHasher` class provides methods for securely hashing and validating passwords using salt and pepper. --- #### Features -1. **Salted Hashing**: - - Hash passwords with a unique salt. - +1. **Salted & Peppered Hashing**: + - Hash passwords with a unique salt and a required application-level pepper (secret). 2. **Validation**: - - Validate passwords against stored hashes. + - Validate passwords against stored hashes using the same salt and pepper. +3. **Strong Security**: + - Uses PBKDF2 with HMACSHA512 and 100,000 iterations. --- @@ -886,9 +887,56 @@ The `PasswordHasher` class provides methods for securely hashing and validating ##### Hashing a Password ```csharp -PasswordHasher.TryCreateSaltedHash("password", out var hash, out var error); +const string pepper = "YourAppSecretPepper"; +PasswordHasher.TryCreateSaltedHash("password", pepper, out var hashResult, out var error); +// hashResult.Salt and hashResult.Hash are Base64 strings ``` +##### Validating a Password +```csharp +const string pepper = "YourAppSecretPepper"; +PasswordHasher.TryValidateHash("password", hashResult.Salt, hashResult.Hash, pepper, out var isValid, out var error); +``` + +--- + +#### API + +```csharp +public static bool TryCreateSaltedHash( + string value, + string pepper, + out (string Salt, string Hash)? saltedHash, + out string? errorMessage) +``` +- `value`: The password to hash. +- `pepper`: Application-level secret (not stored with the hash). +- `saltedHash`: Tuple containing the generated salt and hash (Base64 strings). +- `errorMessage`: Error message if hashing fails. + +```csharp +public static bool TryValidateHash( + string value, + string salt, + string hash, + string pepper, + out bool isValid, + out string? errorMessage) +``` +- `value`: The password to validate. +- `salt`: The Base64-encoded salt used for hashing. +- `hash`: The Base64-encoded hash to validate against. +- `pepper`: Application-level secret (must match the one used for hashing). +- `isValid`: True if the password is valid. +- `errorMessage`: Error message if validation fails. + +--- + +#### Security Notes +- **Pepper** should be kept secret and not stored alongside the hash or salt. +- Changing the pepper will invalidate all existing password hashes. +- Always use a strong, random pepper value for your application. + --- ### JWT Generator diff --git a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs index 4d2b258..174e4f0 100644 --- a/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs +++ b/src/MaksIT.Core.Tests/Security/JwtGeneratorTests.cs @@ -7,15 +7,15 @@ namespace MaksIT.Core.Tests.Security { private JWTTokenGenerateRequest jWTTokenGenerateRequest = new JWTTokenGenerateRequest { - Secret = "supersecretkey12345678901234567890", - Issuer = "testIssuer", - Audience = "testAudience", - Expiration = 30, // 30 minutes - Username = "testUser", - Roles = new List { "Admin", "User" }, - }; + Secret = "supersecretkey12345678901234567890", + Issuer = "testIssuer", + Audience = "testAudience", + Expiration = 30, // 30 minutes + Username = "testUser", + Roles = new List { "Admin", "User" }, + }; - [Fact] + [Fact] public void GenerateToken_ShouldReturnValidToken() { // Act var result = JwtGenerator.TryGenerateToken(jWTTokenGenerateRequest, out var tokenData, out var errorMessage); diff --git a/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs b/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs index 9425388..0f05d5f 100644 --- a/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs +++ b/src/MaksIT.Core.Tests/Security/PasswordHasherTests.cs @@ -3,13 +3,15 @@ using Xunit; namespace MaksIT.Core.Tests.Security { public class PasswordHasherTests { + private const string Pepper = "TestPepper"; + [Fact] public void CreateSaltedHash_ValidPassword_ReturnsSaltAndHash() { // Arrange var password = "SecurePassword123!"; // Act - var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); + var result = PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var errorMessage); // Assert Assert.True(result); @@ -25,7 +27,7 @@ namespace MaksIT.Core.Tests.Security { var password = ""; // Act - var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); + var result = PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var errorMessage); // Assert Assert.True(result); @@ -41,7 +43,7 @@ namespace MaksIT.Core.Tests.Security { var password = " "; // Act - var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); + var result = PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var errorMessage); // Assert Assert.True(result); @@ -55,10 +57,10 @@ namespace MaksIT.Core.Tests.Security { public void ValidateHash_CorrectPassword_ReturnsTrue() { // Arrange var password = "SecurePassword123!"; - PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage); + PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var createErrorMessage); // Act - var result = PasswordHasher.TryValidateHash(password, saltedHash?.Salt, saltedHash?.Hash, out var isValid, out var validateErrorMessage); + var result = PasswordHasher.TryValidateHash(password, saltedHash?.Salt, saltedHash?.Hash, Pepper, out var isValid, out var validateErrorMessage); // Assert Assert.True(result); @@ -71,10 +73,10 @@ namespace MaksIT.Core.Tests.Security { // Arrange var password = "SecurePassword123!"; var wrongPassword = "WrongPassword456!"; - PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage); + PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var createErrorMessage); // Act - var result = PasswordHasher.TryValidateHash(wrongPassword, saltedHash?.Salt, saltedHash?.Hash, out var isValid, out var validateErrorMessage); + var result = PasswordHasher.TryValidateHash(wrongPassword, saltedHash?.Salt, saltedHash?.Hash, Pepper, out var isValid, out var validateErrorMessage); // Assert Assert.True(result); @@ -90,7 +92,7 @@ namespace MaksIT.Core.Tests.Security { var salt = ""; // Assuming empty salt // Act - var result = PasswordHasher.TryValidateHash(password, salt, storedHash, out var isValid, out var errorMessage); + var result = PasswordHasher.TryValidateHash(password, salt, storedHash, Pepper, out var isValid, out var errorMessage); // Assert Assert.True(result); @@ -106,7 +108,7 @@ namespace MaksIT.Core.Tests.Security { var salt = " "; // Act - var result = PasswordHasher.TryValidateHash(password, salt, storedHash, out var isValid, out var errorMessage); + var result = PasswordHasher.TryValidateHash(password, salt, storedHash, Pepper, out var isValid, out var errorMessage); // Assert Assert.True(result); @@ -122,7 +124,7 @@ namespace MaksIT.Core.Tests.Security { var invalidSalt = "InvalidSaltValue"; // Act - var result = PasswordHasher.TryValidateHash(password, invalidSalt, invalidStoredHash, out var isValid, out var errorMessage); + var result = PasswordHasher.TryValidateHash(password, invalidSalt, invalidStoredHash, Pepper, out var isValid, out var errorMessage); // Assert Assert.True(result); @@ -136,8 +138,8 @@ namespace MaksIT.Core.Tests.Security { var password = "SecurePassword123!"; // Act - PasswordHasher.TryCreateSaltedHash(password, out var hashResult1, out var errorMessage1); - PasswordHasher.TryCreateSaltedHash(password, out var hashResult2, out var errorMessage2); + PasswordHasher.TryCreateSaltedHash(password, Pepper, out var hashResult1, out var errorMessage1); + PasswordHasher.TryCreateSaltedHash(password, Pepper, out var hashResult2, out var errorMessage2); // Assert Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash); @@ -147,7 +149,7 @@ namespace MaksIT.Core.Tests.Security { public void ValidateHash_ModifiedStoredHash_ReturnsFalse() { // Arrange var password = "SecurePassword123!"; - PasswordHasher.TryCreateSaltedHash(password, out var hashResult, out var createErrorMessage); + PasswordHasher.TryCreateSaltedHash(password, Pepper, out var hashResult, out var createErrorMessage); // Modify the stored hash var hashChars = hashResult?.Hash.ToCharArray(); @@ -157,7 +159,7 @@ namespace MaksIT.Core.Tests.Security { var modifiedHash = new string(hashChars); // Act - var result = PasswordHasher.TryValidateHash(password, hashResult?.Salt, modifiedHash, out var isValid, out var validateErrorMessage); + var result = PasswordHasher.TryValidateHash(password, hashResult?.Salt, modifiedHash, Pepper, out var isValid, out var validateErrorMessage); // Assert Assert.True(result); @@ -172,8 +174,8 @@ namespace MaksIT.Core.Tests.Security { var password2 = "PasswordTwo"; // Act - PasswordHasher.TryCreateSaltedHash(password1, out var hashResult1, out var errorMessage1); - PasswordHasher.TryCreateSaltedHash(password2, out var hashResult2, out var errorMessage2); + PasswordHasher.TryCreateSaltedHash(password1, Pepper, out var hashResult1, out var errorMessage1); + PasswordHasher.TryCreateSaltedHash(password2, Pepper, out var hashResult2, out var errorMessage2); // Assert Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash); @@ -185,7 +187,7 @@ namespace MaksIT.Core.Tests.Security { var password = "SecurePassword123!"; // Act - var result = PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var errorMessage); + var result = PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var errorMessage); // Assert // For 16 bytes salt, Base64 length is 24 characters diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 5608ddf..16298de 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.5.2 + 1.5.3 Maksym Sadovnychyy MAKS-IT MaksIT.Core diff --git a/src/MaksIT.Core/Security/PasswordHasher.cs b/src/MaksIT.Core/Security/PasswordHasher.cs index ae858c1..94e15bc 100644 --- a/src/MaksIT.Core/Security/PasswordHasher.cs +++ b/src/MaksIT.Core/Security/PasswordHasher.cs @@ -13,9 +13,11 @@ public static class PasswordHasher { return randomBytes; } - private static string CreateHash(string value, byte[] saltBytes) { + private static string CreateHash(string value, byte[] saltBytes, string pepper) { + // Combine password and pepper + var valueWithPepper = value + pepper; var valueBytes = KeyDerivation.Pbkdf2( - password: value, + password: valueWithPepper, salt: saltBytes, prf: KeyDerivationPrf.HMACSHA512, iterationCount: 100_000, // Increased iteration count @@ -26,12 +28,13 @@ public static class PasswordHasher { public static bool TryCreateSaltedHash( string value, + string pepper, [NotNullWhen(true)] out (string Salt, string Hash)? saltedHash, [NotNullWhen(false)] out string? errorMessage ) { try { var saltBytes = CreateSaltBytes(); - var hash = CreateHash(value, saltBytes); + var hash = CreateHash(value, saltBytes, pepper); var salt = Convert.ToBase64String(saltBytes); saltedHash = (salt, hash); @@ -49,12 +52,13 @@ public static class PasswordHasher { string value, string salt, string hash, + string pepper, [NotNullWhen(true)] out bool isValid, [NotNullWhen(false)] out string? errorMessage ) { try { var saltBytes = Convert.FromBase64String(salt); - var hashToCompare = CreateHash(value, saltBytes); + var hashToCompare = CreateHash(value, saltBytes, pepper); isValid = CryptographicOperations.FixedTimeEquals( Convert.FromBase64String(hashToCompare),