(feature): updated password hasher utilities to support pepper for additional security

This commit is contained in:
Maksym Sadovnychyy 2025-11-08 12:15:07 +01:00
parent 2dbef10f37
commit 186e99a6f4
5 changed files with 90 additions and 36 deletions

View File

@ -868,17 +868,18 @@ ChecksumUtility.TryCalculateCRC32Checksum(data, out var checksum, out var error)
### Password Hasher ### 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 #### Features
1. **Salted Hashing**: 1. **Salted & Peppered Hashing**:
- Hash passwords with a unique salt. - Hash passwords with a unique salt and a required application-level pepper (secret).
2. **Validation**: 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 ##### Hashing a Password
```csharp ```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 ### JWT Generator

View File

@ -3,13 +3,15 @@ using Xunit;
namespace MaksIT.Core.Tests.Security { namespace MaksIT.Core.Tests.Security {
public class PasswordHasherTests { public class PasswordHasherTests {
private const string Pepper = "TestPepper";
[Fact] [Fact]
public void CreateSaltedHash_ValidPassword_ReturnsSaltAndHash() { public void CreateSaltedHash_ValidPassword_ReturnsSaltAndHash() {
// Arrange // Arrange
var password = "SecurePassword123!"; var password = "SecurePassword123!";
// Act // 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
Assert.True(result); Assert.True(result);
@ -25,7 +27,7 @@ namespace MaksIT.Core.Tests.Security {
var password = ""; var password = "";
// Act // 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
Assert.True(result); Assert.True(result);
@ -41,7 +43,7 @@ namespace MaksIT.Core.Tests.Security {
var password = " "; var password = " ";
// Act // 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
Assert.True(result); Assert.True(result);
@ -55,10 +57,10 @@ namespace MaksIT.Core.Tests.Security {
public void ValidateHash_CorrectPassword_ReturnsTrue() { public void ValidateHash_CorrectPassword_ReturnsTrue() {
// Arrange // Arrange
var password = "SecurePassword123!"; var password = "SecurePassword123!";
PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage); PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var createErrorMessage);
// Act // 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
Assert.True(result); Assert.True(result);
@ -71,10 +73,10 @@ namespace MaksIT.Core.Tests.Security {
// Arrange // Arrange
var password = "SecurePassword123!"; var password = "SecurePassword123!";
var wrongPassword = "WrongPassword456!"; var wrongPassword = "WrongPassword456!";
PasswordHasher.TryCreateSaltedHash(password, out var saltedHash, out var createErrorMessage); PasswordHasher.TryCreateSaltedHash(password, Pepper, out var saltedHash, out var createErrorMessage);
// Act // 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
Assert.True(result); Assert.True(result);
@ -90,7 +92,7 @@ namespace MaksIT.Core.Tests.Security {
var salt = ""; // Assuming empty salt var salt = ""; // Assuming empty salt
// Act // 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
Assert.True(result); Assert.True(result);
@ -106,7 +108,7 @@ namespace MaksIT.Core.Tests.Security {
var salt = " "; var salt = " ";
// Act // 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
Assert.True(result); Assert.True(result);
@ -122,7 +124,7 @@ namespace MaksIT.Core.Tests.Security {
var invalidSalt = "InvalidSaltValue"; var invalidSalt = "InvalidSaltValue";
// Act // 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
Assert.True(result); Assert.True(result);
@ -136,8 +138,8 @@ namespace MaksIT.Core.Tests.Security {
var password = "SecurePassword123!"; var password = "SecurePassword123!";
// Act // Act
PasswordHasher.TryCreateSaltedHash(password, out var hashResult1, out var errorMessage1); PasswordHasher.TryCreateSaltedHash(password, Pepper, out var hashResult1, out var errorMessage1);
PasswordHasher.TryCreateSaltedHash(password, out var hashResult2, out var errorMessage2); PasswordHasher.TryCreateSaltedHash(password, Pepper, out var hashResult2, out var errorMessage2);
// Assert // Assert
Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash); Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash);
@ -147,7 +149,7 @@ namespace MaksIT.Core.Tests.Security {
public void ValidateHash_ModifiedStoredHash_ReturnsFalse() { public void ValidateHash_ModifiedStoredHash_ReturnsFalse() {
// Arrange // Arrange
var password = "SecurePassword123!"; 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 // Modify the stored hash
var hashChars = hashResult?.Hash.ToCharArray(); var hashChars = hashResult?.Hash.ToCharArray();
@ -157,7 +159,7 @@ namespace MaksIT.Core.Tests.Security {
var modifiedHash = new string(hashChars); var modifiedHash = new string(hashChars);
// Act // 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
Assert.True(result); Assert.True(result);
@ -172,8 +174,8 @@ namespace MaksIT.Core.Tests.Security {
var password2 = "PasswordTwo"; var password2 = "PasswordTwo";
// Act // Act
PasswordHasher.TryCreateSaltedHash(password1, out var hashResult1, out var errorMessage1); PasswordHasher.TryCreateSaltedHash(password1, Pepper, out var hashResult1, out var errorMessage1);
PasswordHasher.TryCreateSaltedHash(password2, out var hashResult2, out var errorMessage2); PasswordHasher.TryCreateSaltedHash(password2, Pepper, out var hashResult2, out var errorMessage2);
// Assert // Assert
Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash); Assert.NotEqual(hashResult1?.Hash, hashResult2?.Hash);
@ -185,7 +187,7 @@ namespace MaksIT.Core.Tests.Security {
var password = "SecurePassword123!"; var password = "SecurePassword123!";
// Act // 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
// For 16 bytes salt, Base64 length is 24 characters // For 16 bytes salt, Base64 length is 24 characters

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata --> <!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId> <PackageId>MaksIT.Core</PackageId>
<Version>1.5.2</Version> <Version>1.5.3</Version>
<Authors>Maksym Sadovnychyy</Authors> <Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company> <Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product> <Product>MaksIT.Core</Product>

View File

@ -13,9 +13,11 @@ public static class PasswordHasher {
return randomBytes; 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( var valueBytes = KeyDerivation.Pbkdf2(
password: value, password: valueWithPepper,
salt: saltBytes, salt: saltBytes,
prf: KeyDerivationPrf.HMACSHA512, prf: KeyDerivationPrf.HMACSHA512,
iterationCount: 100_000, // Increased iteration count iterationCount: 100_000, // Increased iteration count
@ -26,12 +28,13 @@ public static class PasswordHasher {
public static bool TryCreateSaltedHash( public static bool TryCreateSaltedHash(
string value, string value,
string pepper,
[NotNullWhen(true)] out (string Salt, string Hash)? saltedHash, [NotNullWhen(true)] out (string Salt, string Hash)? saltedHash,
[NotNullWhen(false)] out string? errorMessage [NotNullWhen(false)] out string? errorMessage
) { ) {
try { try {
var saltBytes = CreateSaltBytes(); var saltBytes = CreateSaltBytes();
var hash = CreateHash(value, saltBytes); var hash = CreateHash(value, saltBytes, pepper);
var salt = Convert.ToBase64String(saltBytes); var salt = Convert.ToBase64String(saltBytes);
saltedHash = (salt, hash); saltedHash = (salt, hash);
@ -49,12 +52,13 @@ public static class PasswordHasher {
string value, string value,
string salt, string salt,
string hash, string hash,
string pepper,
[NotNullWhen(true)] out bool isValid, [NotNullWhen(true)] out bool isValid,
[NotNullWhen(false)] out string? errorMessage [NotNullWhen(false)] out string? errorMessage
) { ) {
try { try {
var saltBytes = Convert.FromBase64String(salt); var saltBytes = Convert.FromBase64String(salt);
var hashToCompare = CreateHash(value, saltBytes); var hashToCompare = CreateHash(value, saltBytes, pepper);
isValid = CryptographicOperations.FixedTimeEquals( isValid = CryptographicOperations.FixedTimeEquals(
Convert.FromBase64String(hashToCompare), Convert.FromBase64String(hashToCompare),