< Summary - Heimdall API

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

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
VerifyAsync()100%66100%

File(s)

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

#LineLine coverage
 1using System.Security.Cryptography;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Domain.Persistence;
 4using OtpNet;
 5
 6namespace ArturRios.Heimdall.Command.Services;
 7
 8/// <inheritdoc cref="ITotpCodeVerifier" />
 3789public class TotpCodeVerifier(
 37810    ITotpSecretProtector totpSecretProtector,
 37811    IAtomicWrites atomicWrites) : ITotpCodeVerifier
 12{
 13    // A one time-step (30s) tolerance on either side of "now", the conventional allowance for clock
 14    // drift between the server and whatever device generated the code — wide enough to forgive a
 15    // little skew, narrow enough that it does not meaningfully widen the guessing window.
 216    private static readonly VerificationWindow TotpVerificationWindow = new(previous: 1, future: 1);
 17
 18    public async Task<bool> VerifyAsync(TwoFactorAuth twoFactorAuth, string? code)
 19    {
 12520        if (string.IsNullOrWhiteSpace(code) || twoFactorAuth.TotpSecretEncrypted is null)
 21        {
 1922            return false;
 23        }
 24
 25        string base32Secret;
 26
 27        try
 28        {
 10629            base32Secret = totpSecretProtector.Unprotect(twoFactorAuth.TotpSecretEncrypted);
 9030        }
 1631        catch (CryptographicException)
 32        {
 33            // A corrupted or otherwise unreadable TotpSecretEncrypted value (e.g. protected under a
 34            // Data Protection key that is no longer available) can never be decrypted back into a
 35            // valid app code, so it fails the same way a wrong code does instead of surfacing as a
 36            // 500.
 1637            return false;
 38        }
 39
 9040        var totp = new Totp(Base32Encoding.ToBytes(base32Secret));
 41
 9042        if (!totp.VerifyTotp(code, out var matchedTimeStep, TotpVerificationWindow))
 43        {
 4544            return false;
 45        }
 46
 47        // FR-2F-14 / RFC 6238 §5.2: a code is good once. The window above accepts the neighbouring
 48        // steps too, so without this an observed code stays usable for up to ninety seconds and
 49        // across every endpoint that takes a second factor — verify, disable, and regenerate alike.
 50        // Strictly-greater rather than not-equal, so a replay of an older step inside the window is
 51        // refused as well.
 52        //
 53        // The comparison is made by the database, in the write that records the step: two requests
 54        // carrying the same code at the same moment both pass a check made in memory, and both used
 55        // to be accepted. Only one of them can advance the step, and only that one is accepted. A
 56        // failure to record it is a failure to verify for the same reason — accepting a code whose
 57        // use was not recorded would leave it replayable.
 4558        return await atomicWrites.TryAdvanceTotpStepAsync(twoFactorAuth, matchedTimeStep, DateTime.UtcNow);
 12559    }
 60}