< Summary

Information
Class: Elsa.Identity.Services.DefaultSecretHasher
Assembly: Elsa.Identity
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/Services/DefaultSecretHasher.cs
Line coverage
93%
Covered lines: 84
Uncovered lines: 6
Coverable lines: 90
Total lines: 193
Line coverage: 93.3%
Branch coverage
86%
Covered branches: 33
Total branches: 38
Branch coverage: 86.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
HashSecret(...)100%11100%
HashSecret(...)100%11100%
VerifySecret(...)100%210%
VerifySecret(...)100%11100%
VerifySecret(...)100%210%
VerifySecret(...)100%1010100%
HashSecret(...)100%11100%
GenerateSalt(...)100%11100%
HashSecret(...)100%11100%
FormatHashEnvelope(...)50%8887.5%
HashLegacySha256(...)100%11100%
TryReadPbkdf2Hash(...)95%202095.45%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Identity/Services/DefaultSecretHasher.cs

#LineLine coverage
 1using System.Buffers;
 2using System.Buffers.Text;
 3using System.Security.Cryptography;
 4using System.Text;
 5using Elsa.Identity.Contracts;
 6using Elsa.Identity.Models;
 7
 8namespace Elsa.Identity.Services;
 9
 10/// <inheritdoc />
 11public class DefaultSecretHasher : ISecretHasher
 12{
 13    private const string Algorithm = "pbkdf2-sha256";
 14    private const char Separator = '$';
 15    private const byte SeparatorByte = (byte)Separator;
 16    private const int DefaultIterationCount = 600_000;
 17    private const int MaxIterationCount = DefaultIterationCount * 4;
 18    private const int KeySize = 32;
 119    private static readonly byte[] AlgorithmBytes = Encoding.UTF8.GetBytes(Algorithm);
 20
 21    /// <inheritdoc />
 22    public HashedSecret HashSecret(string secret)
 23    {
 324        var saltBytes = GenerateSalt();
 325        return HashSecret(secret, saltBytes);
 26    }
 27
 28    /// <inheritdoc />
 29    public HashedSecret HashSecret(string secret, byte[] salt)
 30    {
 331        var passwordBytes = Encoding.UTF8.GetBytes(secret);
 32        try
 33        {
 334            var hashedPassword = HashSecret(passwordBytes, salt);
 335            return HashedSecret.FromBytes(hashedPassword, salt);
 36        }
 37        finally
 38        {
 339            CryptographicOperations.ZeroMemory(passwordBytes);
 340        }
 341    }
 42
 43    /// <inheritdoc />
 44    public bool VerifySecret(string clearTextSecret, string secret, string salt)
 45    {
 046        var hashedPassword = HashedSecret.FromString(secret, salt);
 047        return VerifySecret(clearTextSecret, hashedPassword);
 48    }
 49
 50    /// <inheritdoc />
 51    public bool VerifySecret(string clearTextSecret, string secret, string salt, out bool needsRehash)
 52    {
 153        var hashedPassword = HashedSecret.FromString(secret, salt);
 154        return VerifySecret(clearTextSecret, hashedPassword, out needsRehash);
 55    }
 56
 57    /// <inheritdoc />
 58    public bool VerifySecret(string clearTextSecret, HashedSecret hashedSecret)
 59    {
 060        return VerifySecret(clearTextSecret, hashedSecret, out _);
 61    }
 62
 63    /// <inheritdoc />
 64    public bool VerifySecret(string clearTextSecret, HashedSecret hashedSecret, out bool needsRehash)
 65    {
 966        var storedSecretBytes = hashedSecret.Secret;
 967        var saltBytes = hashedSecret.Salt;
 968        var clearTextBytes = Encoding.UTF8.GetBytes(clearTextSecret);
 969        Span<byte> expectedHash = stackalloc byte[KeySize];
 970        byte[]? providedHash = null;
 971        byte[]? legacyHash = null;
 72
 73        try
 74        {
 975            if (TryReadPbkdf2Hash(storedSecretBytes, expectedHash, out var iterationCount))
 76            {
 577                providedHash = HashSecret(clearTextBytes, saltBytes, iterationCount);
 578                var matches = CryptographicOperations.FixedTimeEquals(providedHash, expectedHash);
 579                needsRehash = matches && iterationCount < DefaultIterationCount;
 580                return matches;
 81            }
 82
 483            legacyHash = HashLegacySha256(clearTextBytes, saltBytes);
 484            if (storedSecretBytes.Length != legacyHash.Length)
 85            {
 386                needsRehash = false;
 387                return false;
 88            }
 89
 190            var isLegacyMatch = CryptographicOperations.FixedTimeEquals(legacyHash, storedSecretBytes);
 191            needsRehash = isLegacyMatch;
 192            return isLegacyMatch;
 93        }
 94        finally
 95        {
 996            CryptographicOperations.ZeroMemory(clearTextBytes);
 97
 998            if (providedHash is not null)
 599                CryptographicOperations.ZeroMemory(providedHash);
 100
 9101            if (legacyHash is not null)
 4102                CryptographicOperations.ZeroMemory(legacyHash);
 103
 9104            CryptographicOperations.ZeroMemory(expectedHash);
 9105        }
 9106    }
 107
 108    /// <inheritdoc />
 109    public byte[] HashSecret(byte[] secret, byte[] salt)
 110    {
 3111        var hash = HashSecret(secret, salt, DefaultIterationCount);
 112        try
 113        {
 3114            return FormatHashEnvelope(hash);
 115        }
 116        finally
 117        {
 3118            CryptographicOperations.ZeroMemory(hash);
 3119        }
 3120    }
 121
 122    /// <inheritdoc />
 7123    public byte[] GenerateSalt(int saltSize = 32) => RandomNumberGenerator.GetBytes(saltSize);
 124
 125    private static byte[] HashSecret(byte[] secret, byte[] salt, int iterationCount)
 126    {
 8127        return Rfc2898DeriveBytes.Pbkdf2(secret, salt, iterationCount, HashAlgorithmName.SHA256, KeySize);
 128    }
 129
 130    private static byte[] FormatHashEnvelope(byte[] hash)
 131    {
 3132        Span<byte> iterationBytes = stackalloc byte[16];
 3133        if (!Utf8Formatter.TryFormat(DefaultIterationCount, iterationBytes, out var iterationBytesWritten))
 0134            throw new InvalidOperationException("Failed to format the PBKDF2 iteration count.");
 135
 3136        var encodedHashLength = ((hash.Length + 2) / 3) * 4;
 3137        var result = new byte[AlgorithmBytes.Length + 1 + iterationBytesWritten + 1 + encodedHashLength];
 3138        var resultSpan = result.AsSpan();
 3139        AlgorithmBytes.CopyTo(resultSpan);
 140
 3141        var offset = AlgorithmBytes.Length;
 3142        resultSpan[offset++] = SeparatorByte;
 3143        iterationBytes[..iterationBytesWritten].CopyTo(resultSpan[offset..]);
 3144        offset += iterationBytesWritten;
 3145        resultSpan[offset++] = SeparatorByte;
 146
 3147        var status = Base64.EncodeToUtf8(hash, resultSpan[offset..], out var consumed, out var written);
 3148        if (status != OperationStatus.Done || consumed != hash.Length || written != encodedHashLength)
 0149            throw new InvalidOperationException("Failed to encode the PBKDF2 hash.");
 150
 3151        return result;
 152    }
 153
 154    private static byte[] HashLegacySha256(byte[] secret, byte[] salt)
 155    {
 4156        using var sha256 = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
 4157        sha256.AppendData(secret);
 4158        sha256.AppendData(salt);
 4159        return sha256.GetHashAndReset();
 4160    }
 161
 162    private static bool TryReadPbkdf2Hash(byte[] storedSecretBytes, Span<byte> hash, out int iterationCount)
 163    {
 9164        iterationCount = 0;
 165
 9166        var envelope = storedSecretBytes.AsSpan();
 9167        var algorithmSeparatorIndex = envelope.IndexOf(SeparatorByte);
 9168        if (algorithmSeparatorIndex <= 0)
 1169            return false;
 170
 8171        var algorithmBytes = envelope[..algorithmSeparatorIndex];
 8172        if (!algorithmBytes.SequenceEqual(AlgorithmBytes))
 1173            return false;
 174
 7175        var iterationAndHashBytes = envelope[(algorithmSeparatorIndex + 1)..];
 7176        var iterationSeparatorIndex = iterationAndHashBytes.IndexOf(SeparatorByte);
 7177        if (iterationSeparatorIndex <= 0)
 0178            return false;
 179
 7180        var iterationBytes = iterationAndHashBytes[..iterationSeparatorIndex];
 7181        if (!Utf8Parser.TryParse(iterationBytes, out iterationCount, out var bytesConsumed) || bytesConsumed != iteratio
 1182            return false;
 183
 6184        var encodedHashBytes = iterationAndHashBytes[(iterationSeparatorIndex + 1)..];
 6185        var status = Base64.DecodeFromUtf8(encodedHashBytes, hash, out var consumed, out var written);
 6186        if (status == OperationStatus.Done && consumed == encodedHashBytes.Length && written == KeySize)
 5187            return true;
 188
 1189        iterationCount = 0;
 1190        CryptographicOperations.ZeroMemory(hash);
 1191        return false;
 192    }
 193}