< Summary

Information
Class: ArturRios.Util.Hashing.Hash
Assembly: ArturRios.Util
File(s): /home/runner/work/dotnet-util/dotnet-util/src/Hashing/Hash.cs
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 120
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage
100%
Covered methods: 5
Fully covered methods: 5
Total methods: 5
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
EncodeWithSalt(...)100%44100%
EncodeWithRandomSalt(...)100%22100%
TextMatches(...)100%11100%
Derive(...)100%11100%
CreateSalt()100%11100%

File(s)

/home/runner/work/dotnet-util/dotnet-util/src/Hashing/Hash.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using Konscious.Security.Cryptography;
 4
 5namespace ArturRios.Util.Hashing;
 6
 7/// <summary>
 8/// Provides hashing helpers based on Argon2id for secure password / secret derivation.
 9/// </summary>
 10/// <remarks>
 11/// All methods use a key length of 128 bytes and allow optional configuration overrides via <see cref="HashConfiguratio
 12/// A hash can only be verified with the configuration it was produced with, so store the cost parameters
 13/// alongside the hash and the salt and pass them back to <see cref="TextMatches"/>.
 14/// </remarks>
 15public static class Hash
 16{
 17    private const int Argon2IdKeyBytes = 128;
 18    private const int SaltByteSize = 16;
 19
 20    /// <summary>
 21    /// Smallest salt Argon2 accepts. Shorter salts weaken the hash and are rejected outright.
 22    /// </summary>
 23    private const int MinimumSaltByteSize = 8;
 24
 25    /// <summary>
 26    /// Hashes <paramref name="text"/> using Argon2id with a provided salt and optional configuration.
 27    /// </summary>
 28    /// <param name="text">The input text to hash (e.g. password). Must not be empty.</param>
 29    /// <param name="salt">A cryptographically strong random salt of at least 8 bytes.</param>
 30    /// <param name="configuration">Optional hashing configuration; defaults are used if null.</param>
 31    /// <returns>The derived hash bytes.</returns>
 32    /// <exception cref="ArgumentNullException"><paramref name="text"/> or <paramref name="salt"/> is <c>null</c>.</exce
 33    /// <exception cref="ArgumentException">
 34    /// <paramref name="text"/> is empty, or <paramref name="salt"/> is shorter than 8 bytes.
 35    /// </exception>
 36    public static byte[] EncodeWithSalt(string text, byte[] salt, HashConfiguration? configuration = null)
 37    {
 1638        ArgumentException.ThrowIfNullOrEmpty(text);
 1439        ArgumentNullException.ThrowIfNull(salt);
 40
 1241        if (salt.Length < MinimumSaltByteSize)
 42        {
 343            throw new ArgumentException($"Salt must be at least {MinimumSaltByteSize} bytes long.", nameof(salt));
 44        }
 45
 946        return Derive(text, salt, configuration ?? new HashConfiguration());
 47    }
 48
 49    /// <summary>
 50    /// Hashes <paramref name="text"/> using Argon2id and a newly generated random 16-byte salt.
 51    /// </summary>
 52    /// <param name="text">The input text to hash. Must not be empty.</param>
 53    /// <param name="salt">Outputs the generated salt used for hashing.</param>
 54    /// <param name="configuration">Optional hashing configuration.</param>
 55    /// <returns>The derived hash bytes.</returns>
 56    /// <exception cref="ArgumentNullException"><paramref name="text"/> is <c>null</c>.</exception>
 57    /// <exception cref="ArgumentException"><paramref name="text"/> is empty.</exception>
 58    /// <remarks>
 59    /// An empty <paramref name="text"/> is rejected here rather than deep inside Argon2, which reports it
 60    /// as a null "password" parameter that does not exist in this API.
 61    /// </remarks>
 62    public static byte[] EncodeWithRandomSalt(string text, out byte[] salt, HashConfiguration? configuration = null)
 63    {
 1264        ArgumentException.ThrowIfNullOrEmpty(text);
 65
 1066        salt = CreateSalt();
 67
 1068        return Derive(text, salt, configuration ?? new HashConfiguration());
 69    }
 70
 71    /// <summary>
 72    /// Verifies if the provided <paramref name="hash"/> matches hashing <paramref name="text"/> with <paramref name="sa
 73    /// </summary>
 74    /// <param name="text">Plain text to verify.</param>
 75    /// <param name="hash">Expected hash value.</param>
 76    /// <param name="salt">Salt associated with the stored hash.</param>
 77    /// <param name="configuration">
 78    /// The configuration the stored hash was produced with; defaults are used if null. Passing the wrong
 79    /// cost parameters produces a different hash and therefore a <c>false</c> result.
 80    /// </param>
 81    /// <returns><c>true</c> if the hashes match; otherwise <c>false</c>.</returns>
 82    /// <exception cref="ArgumentNullException"><paramref name="text"/>, <paramref name="hash"/> or <paramref name="salt
 83    /// <exception cref="ArgumentException">
 84    /// <paramref name="text"/> is empty, or <paramref name="salt"/> is shorter than 8 bytes.
 85    /// </exception>
 86    /// <remarks>
 87    /// The comparison runs in time independent of how many leading bytes match, so it does not leak the
 88    /// correct hash to an attacker who can measure it.
 89    /// </remarks>
 90    public static bool TextMatches(string text, byte[] hash, byte[] salt, HashConfiguration? configuration = null)
 91    {
 992        ArgumentNullException.ThrowIfNull(hash);
 93
 894        var hashToMatch = EncodeWithSalt(text, salt, configuration);
 95
 796        return CryptographicOperations.FixedTimeEquals(hash, hashToMatch);
 97    }
 98
 99    /// <summary>
 100    /// Runs the Argon2id key derivation, releasing the working memory afterwards.
 101    /// </summary>
 102    private static byte[] Derive(string text, byte[] salt, HashConfiguration configuration)
 103    {
 19104        using Argon2id argon2Id = new(Encoding.UTF8.GetBytes(text))
 19105        {
 19106            Salt = salt,
 19107            DegreeOfParallelism = configuration.DegreeOfParallelism,
 19108            Iterations = configuration.NumberOfIterations,
 19109            MemorySize = configuration.MemoryToUseInKb
 19110        };
 111
 19112        return argon2Id.GetBytes(Argon2IdKeyBytes);
 19113    }
 114
 115    /// <summary>
 116    /// Creates a cryptographically strong random salt.
 117    /// </summary>
 118    /// <returns>A 16-byte salt.</returns>
 10119    private static byte[] CreateSalt() => RandomNumberGenerator.GetBytes(SaltByteSize);
 120}