Address identity hasher review feedback
This commit is contained in:
parent
28ee3d6fb3
commit
95640cbf7d
|
|
@ -6,7 +6,7 @@ Compare: [`3.5.3...3.6.0`](https://github.com/elsa-workflows/elsa-core/compare/3
|
|||
|
||||
## ⚠️ Breaking changes / upgrade notes
|
||||
|
||||
- **Identity secret hashing hardened**: New user passwords, client secrets, and API keys are hashed with PBKDF2-SHA256 using per-record salts and version metadata instead of fast SHA-256 hashes. Existing legacy hashes still verify and are upgraded opportunistically after successful login/API-key validation. Generated identity secrets now use cryptographic randomness.
|
||||
- **Identity secret hashing hardened**: New user passwords, client secrets, and API keys are hashed with PBKDF2-SHA256 using 600,000 iterations, per-record salts, and version metadata instead of fast SHA-256 hashes. Existing legacy hashes still verify and are upgraded opportunistically after successful login/API-key validation. Generated identity secrets now use cryptographic randomness.
|
||||
|
||||
- **EF Core package names have changed**: EF Core persistence packages were renamed from `Elsa.EntityFrameworkCore.*` to `Elsa.Persistence.EFCore.*`. If your application references any of the old package names, you must update them to the new package names when upgrading to 3.6.0. Be sure to review your project files, internal package feeds, CI pipelines, and deployment manifests for old package references.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ public interface ISecretHasher
|
|||
/// <param name="salt">The salt.</param>
|
||||
/// <param name="needsRehash">Whether the stored hash should be upgraded.</param>
|
||||
/// <returns>True if the secret is valid, otherwise false.</returns>
|
||||
bool VerifySecret(string clearTextSecret, string secret, string salt, out bool needsRehash);
|
||||
bool VerifySecret(string clearTextSecret, string secret, string salt, out bool needsRehash)
|
||||
{
|
||||
needsRehash = false;
|
||||
return VerifySecret(clearTextSecret, secret, salt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the secret.
|
||||
|
|
@ -65,7 +69,11 @@ public interface ISecretHasher
|
|||
/// <param name="hashedSecret">The hashed secret.</param>
|
||||
/// <param name="needsRehash">Whether the stored hash should be upgraded.</param>
|
||||
/// <returns>True if the secret is valid, otherwise false.</returns>
|
||||
bool VerifySecret(string clearTextSecret, HashedSecret hashedSecret, out bool needsRehash);
|
||||
bool VerifySecret(string clearTextSecret, HashedSecret hashedSecret, out bool needsRehash)
|
||||
{
|
||||
needsRehash = false;
|
||||
return VerifySecret(clearTextSecret, hashedSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a salt.
|
||||
|
|
|
|||
|
|
@ -80,4 +80,4 @@ identity.UseDefaultAdmin("admin", "password", "admin", new List<string> { "*" })
|
|||
|
||||
## Secret Hashing
|
||||
|
||||
New identity passwords, client secrets, and API keys are hashed with PBKDF2-SHA256 using a per-record salt and version metadata. Existing legacy SHA-256 hashes remain valid and are upgraded opportunistically after a successful user login or API-key validation.
|
||||
New identity passwords, client secrets, and API keys are hashed with PBKDF2-SHA256 using 600,000 iterations, a per-record salt, and version metadata. Existing legacy SHA-256 hashes remain valid and are upgraded opportunistically after a successful user login or API-key validation.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ public class DefaultSecretHasher : ISecretHasher
|
|||
{
|
||||
private const string Algorithm = "pbkdf2-sha256";
|
||||
private const char Separator = '$';
|
||||
private const int DefaultIterationCount = 210_000;
|
||||
private const int DefaultIterationCount = 600_000;
|
||||
private const int MaxIterationCount = DefaultIterationCount * 4;
|
||||
private const int KeySize = 32;
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class DefaultSecretHasherTests
|
|||
{
|
||||
var hashedSecret = _hasher.HashSecret("secret");
|
||||
|
||||
Assert.StartsWith("pbkdf2-sha256$", Encoding.UTF8.GetString(hashedSecret.Secret));
|
||||
Assert.StartsWith("pbkdf2-sha256$600000$", Encoding.UTF8.GetString(hashedSecret.Secret));
|
||||
Assert.True(_hasher.VerifySecret("secret", hashedSecret, out var needsRehash));
|
||||
Assert.False(needsRehash);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
using System.Text;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Models;
|
||||
|
||||
namespace Elsa.Identity.UnitTests.Services;
|
||||
|
||||
public class ISecretHasherTests
|
||||
{
|
||||
private readonly ISecretHasher _hasher = new BackwardCompatibleSecretHasher();
|
||||
|
||||
[Fact]
|
||||
public void VerifySecret_WithStringSaltAndNeedsRehash_DelegatesToExistingImplementation()
|
||||
{
|
||||
var isVerified = _hasher.VerifySecret("secret", "secret", "salt", out var needsRehash);
|
||||
|
||||
Assert.True(isVerified);
|
||||
Assert.False(needsRehash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifySecret_WithHashedSecretAndNeedsRehash_DelegatesToExistingImplementation()
|
||||
{
|
||||
var hashedSecret = HashedSecret.FromBytes(Encoding.UTF8.GetBytes("secret"), Encoding.UTF8.GetBytes("salt"));
|
||||
|
||||
var isVerified = _hasher.VerifySecret("secret", hashedSecret, out var needsRehash);
|
||||
|
||||
Assert.True(isVerified);
|
||||
Assert.False(needsRehash);
|
||||
}
|
||||
|
||||
private sealed class BackwardCompatibleSecretHasher : ISecretHasher
|
||||
{
|
||||
public HashedSecret HashSecret(string secret) => throw new NotSupportedException();
|
||||
|
||||
public HashedSecret HashSecret(string secret, byte[] salt) => throw new NotSupportedException();
|
||||
|
||||
public byte[] HashSecret(byte[] secret, byte[] salt) => throw new NotSupportedException();
|
||||
|
||||
public bool VerifySecret(string clearTextSecret, string secret, string salt)
|
||||
{
|
||||
return clearTextSecret == secret && salt == "salt";
|
||||
}
|
||||
|
||||
public bool VerifySecret(string clearTextSecret, HashedSecret hashedSecret)
|
||||
{
|
||||
return clearTextSecret == Encoding.UTF8.GetString(hashedSecret.Secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue