(feature): updated password hasher utilities to support pepper for additional security
This commit is contained in:
parent
2dbef10f37
commit
186e99a6f4
60
README.md
60
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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user