< Summary

Information
Class: Anichron.API.Services.PwnedPasswordClient
Assembly: Anichron.API
File(s): /home/runner/work/anichron/anichron/src/Anichron.API/Services/PwnedPasswordClient.cs
Tag: 228_36821334185
Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 48
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

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

File(s)

/home/runner/work/anichron/anichron/src/Anichron.API/Services/PwnedPasswordClient.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3
 4namespace Anichron.API.Services;
 5
 6public interface IPwnedPasswordClient
 7{
 8    Task<bool> IsPwnedAsync(string password, CancellationToken ct);
 9}
 10
 911public sealed partial class PwnedPasswordClient(HttpClient http, ILogger<PwnedPasswordClient> logger) : IPwnedPasswordCl
 12{
 13    public async Task<bool> IsPwnedAsync(string password, CancellationToken ct)
 914    {
 915        var passwordBytes = Encoding.UTF8.GetBytes(password);
 16        try
 917        {
 18            // SHA1 is not a choice here: the HIBP range endpoint is defined in terms of SHA-1
 19            // prefixes, so any other algorithm queries a namespace the service does not have.
 20            // S4790 is Sonar's equivalent of CA5350 and arrived with SonarAnalyzer 10.34; both
 21            // are listed so neither analyzer alone fails the build.
 22#pragma warning disable CA5350, S4790 // SHA1 is required by the HIBP k-anonymity API protocol
 923            var hash = Convert.ToHexString(SHA1.HashData(passwordBytes));
 24#pragma warning restore CA5350, S4790
 925            var prefix = hash[..5];
 926            var suffix = hash[5..];
 27
 928            var body = await http.GetStringAsync($"range/{prefix}", ct);
 729            return body.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Any(line =>
 730                line.StartsWith(suffix, StringComparison.OrdinalIgnoreCase));
 31        }
 232        catch (Exception ex)
 233        {
 234            Log.PwnedCheckUnavailable(logger, ex, ex.GetType().Name);
 235            return false;
 36        }
 37        finally
 938        {
 939            CryptographicOperations.ZeroMemory(passwordBytes);
 940        }
 941    }
 42
 43    private static partial class Log
 44    {
 45        [LoggerMessage(Level = LogLevel.Warning, Message = "Pwned Passwords check unavailable ({ExceptionType}); failing
 46        public static partial void PwnedCheckUnavailable(ILogger logger, Exception ex, string exceptionType);
 47    }
 48}