(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
|
### 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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user