< Summary - Heimdall API

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

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
LockoutEndsFrom(...)100%22100%

File(s)

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

#LineLine coverage
 1namespace ArturRios.Heimdall.Command.Services;
 2
 3/// <summary>
 4///     FR-AU-12: the per-account budget for second factors presented at sign-in (UC-38) — an
 5///     authenticator-app code, an email code or a recovery code alike.
 6/// </summary>
 7/// <remarks>
 8///     <para>
 9///         <b>Why per account, and not per challenge.</b> The challenge's own budgets (FR-2F-13,
 10///         FR-2F-17) start afresh with every login, and a login costs whoever knows the password
 11///         nothing — which is the one caller a second factor exists to stop. So the failures are
 12///         counted on the account, across every challenge, and a correct password does not reset
 13///         them: only a second factor that checks out does.
 14///     </para>
 15///     <para>
 16///         <b>Why the lock escalates.</b> A fixed lock caps guessing at a steady rate, and at ten
 17///         guesses every fifteen minutes the third of a million guesses a six-digit code takes on
 18///         average still fit in a year. Doubling each lock, up to a day, brings a persistent attacker
 19///         down to ten guesses a day, while a person who mistypes ten times waits fifteen minutes, as
 20///         they would at the password. The count of locks resets when a second factor succeeds.
 21///     </para>
 22///     <para>
 23///         <b>Why the lock does not show.</b> A locked account answers UC-38's ordinary refusal, and
 24///         the factor is still verified — the result discarded — so neither the message nor the cost
 25///         of the answer tells the caller that guessing has stopped counting.
 26///     </para>
 27/// </remarks>
 28public static class SecondFactorLockout
 29{
 30    /// <summary>The failures that lock the second factor.</summary>
 31    public const int MaxFailedAttempts = 10;
 32
 33    /// <summary>How long the first lock lasts. Each one after it lasts twice as long as the last.</summary>
 234    public static readonly TimeSpan FirstLockout = TimeSpan.FromMinutes(15);
 35
 36    /// <summary>The longest any lock lasts.</summary>
 237    public static readonly TimeSpan LongestLockout = TimeSpan.FromHours(24);
 38
 39    /// <summary>
 40    ///     When each lock that started at <paramref name="now" /> would end, by how many locks came
 41    ///     before it: the first, the second, and so on, the last entry standing for every lock from the
 42    ///     cap on. The shape <c>IAtomicWrites.TryReserveSecondFactorAttemptAsync</c> takes.
 43    /// </summary>
 44    public static IReadOnlyList<DateTime> LockoutEndsFrom(DateTime now)
 45    {
 12046        var ends = new List<DateTime>();
 47
 96048        for (var lockout = FirstLockout; ; lockout *= 2)
 49        {
 96050            if (lockout >= LongestLockout)
 51            {
 12052                ends.Add(now + LongestLockout);
 53
 12054                return ends;
 55            }
 56
 84057            ends.Add(now + lockout);
 58        }
 59    }
 60}