< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Services.SingleUseTokenHash
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/SingleUseTokenHash.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 52
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 1
Fully covered methods: 1
Total methods: 1
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Of(...)100%11100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/SingleUseTokenHash.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3
 4namespace ArturRios.Heimdall.Command.Services;
 5
 6/// <summary>
 7///     Hashes the single-use tokens delivered by email — the password reset token of UC-12 and the
 8///     email verification token of UC-14 — so that neither is stored in a form that can be presented
 9///     back to the API.
 10/// </summary>
 11/// <remarks>
 12///     <para>
 13///         This exists because both were stored in plaintext (Threat Model TH-14) while the
 14///         second-factor recovery codes beside them were hashed, even though a reset token is the
 15///         stronger primitive of the two: a recovery code still requires the password, and a reset
 16///         token replaces it. Anyone who could read the table could complete a reset for any account
 17///         holding a live token, which did not break the Argon2id work protecting the password so
 18///         much as walk around it.
 19///     </para>
 20///     <para>
 21///         Unsalted SHA-256, which is the same choice already made for recovery codes and is
 22///         deliberate rather than an oversight. A salt defends a low-entropy secret against a
 23///         precomputed table; these tokens are 48 characters drawn from a CSPRNG, so there is no
 24///         table to precompute and nothing for a salt to add. What it costs to leave out is what
 25///         makes this workable at all: an unsalted digest is deterministic, so the presented token
 26///         can be hashed once and looked up against a unique index. A per-row salt would force a
 27///         scan of every live token on every attempt, because the caller presents the token alone
 28///         and there is nothing else to narrow the candidates by.
 29///     </para>
 30///     <para>
 31///         Both directions go through here so the value written at issue and the value compared at
 32///         presentation cannot drift apart.
 33///     </para>
 34///     <para>
 35///         Lowercase hex rather than the raw bytes, and that choice is about equality rather than
 36///         about storage. <c>byte[] == byte[]</c> is value equality once EF has translated it into
 37///         SQL and reference equality everywhere else, so the handler's lookup matched correctly
 38///         against PostgreSQL and silently matched nothing against an in-memory repository — which
 39///         is exactly how the unit suite reads it, and how this was caught. A string compares by
 40///         value in both, so the expression means one thing wherever it runs. The digest is 32 bytes
 41///         either way; hex spends 64 characters to remove that ambiguity.
 42///     </para>
 43/// </remarks>
 44public static class SingleUseTokenHash
 45{
 46    /// <summary>The length of the value <see cref="Of" /> returns: SHA-256 as lowercase hex.</summary>
 47    public const int Length = 64;
 48
 49    /// <summary>The stored form of <paramref name="token" />.</summary>
 50    public static string Of(string token) =>
 62351        Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token))).ToLowerInvariant();
 52}

Methods/Properties

Of(System.String)