(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
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

View File

@ -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<string> { "Admin", "User" },
};
Secret = "supersecretkey12345678901234567890",
Issuer = "testIssuer",
Audience = "testAudience",
Expiration = 30, // 30 minutes
Username = "testUser",
Roles = new List<string> { "Admin", "User" },
};
[Fact]
[Fact]
public void GenerateToken_ShouldReturnValidToken() {
// Act
var result = JwtGenerator.TryGenerateToken(jWTTokenGenerateRequest, out var tokenData, out var errorMessage);

View File

@ -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

View File

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

View File

@ -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),