< Summary

Information
Class: ProjectTemplate.Infrastructure.Data.Auditing.ApplicationAuditValueProtector
Assembly: ProjectTemplate.Infrastructure
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/Auditing/ApplicationAuditValueProtector.cs
Line coverage
92%
Covered lines: 47
Uncovered lines: 4
Coverable lines: 51
Total lines: 117
Line coverage: 92.1%
Branch coverage
80%
Covered branches: 36
Total branches: 45
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
TryProtect(...)90.9%1111100%
Hash(...)100%11100%
HmacSha256(...)100%22100%
ToCanonicalString(...)70%322069.23%
Truncate(...)83.33%1212100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/Auditing/ApplicationAuditValueProtector.cs

#LineLine coverage
 1using System.Collections;
 2using System.Globalization;
 3using System.Security.Cryptography;
 4using System.Text;
 5using System.Text.Json;
 6
 7namespace ProjectTemplate.Infrastructure.Data.Auditing;
 8
 9internal static class ApplicationAuditValueProtector
 10{
 11    private const string _maskedValue = "***";
 12
 13    internal static bool TryProtect(
 14        IApplicationAuditValuePolicy policy,
 15        Type entityType,
 16        string propertyName,
 17        object? value,
 18        out object protectedValue)
 19    {
 36420        ArgumentNullException.ThrowIfNull(policy);
 36421        ArgumentNullException.ThrowIfNull(entityType);
 36422        ArgumentException.ThrowIfNullOrWhiteSpace(propertyName);
 23
 36424        ApplicationAuditValueDecision decision = policy.Evaluate(entityType, propertyName, value)
 36425            ?? throw new InvalidOperationException("The application audit value policy returned no decision.");
 26
 36427        switch (decision.Disposition)
 28        {
 29            case ApplicationAuditValueDisposition.Include:
 25830                protectedValue = value ?? string.Empty;
 25831                return true;
 32            case ApplicationAuditValueDisposition.Mask:
 8633                protectedValue = _maskedValue;
 8634                return true;
 35            case ApplicationAuditValueDisposition.Hash:
 636                protectedValue = Hash(value);
 637                return true;
 38            case ApplicationAuditValueDisposition.HmacSha256:
 539                protectedValue = HmacSha256(value, decision.HmacSha256Key);
 440                return true;
 41            case ApplicationAuditValueDisposition.Omit:
 342                protectedValue = string.Empty;
 343                return false;
 44            case ApplicationAuditValueDisposition.Truncate:
 545                protectedValue = Truncate(value, decision.MaximumLength);
 446                return true;
 47            default:
 148                throw new InvalidOperationException($"Unsupported audit value disposition '{decision.Disposition}'.");
 49        }
 50    }
 51
 52    // Unkeyed SHA-256 is an integrity / change-detection digest only. It provides no confidentiality for
 53    // low-entropy values (email addresses, phone numbers, identifiers, booleans, enum names, small numbers),
 54    // which a holder of the audit table can recover by dictionary attack. HmacSha256 is the confidential option.
 55    private static string Hash(object? value)
 56    {
 657        byte[] canonicalValue = Encoding.UTF8.GetBytes(ToCanonicalString(value));
 658        byte[] hash = SHA256.HashData(canonicalValue);
 659        return Convert.ToHexString(hash);
 60    }
 61
 62    private static string HmacSha256(object? value, string? key)
 63    {
 564        if (string.IsNullOrWhiteSpace(key))
 65        {
 166            throw new ArgumentException("HMAC-SHA-256 audit values require a non-empty key.", nameof(key));
 67        }
 68
 469        byte[] canonicalValue = Encoding.UTF8.GetBytes(ToCanonicalString(value));
 470        byte[] keyBytes = Encoding.UTF8.GetBytes(key);
 471        byte[] hash = HMACSHA256.HashData(keyBytes, canonicalValue);
 472        return Convert.ToHexString(hash);
 73    }
 74
 75    // Produces a culture-invariant representation that is distinct for distinct values. Types whose
 76    // Object.ToString() is only the type name (byte[], collections) are expanded, so change detection on
 77    // binary and collection columns does not collapse to a single constant digest.
 78    internal static string ToCanonicalString(object? value)
 79    {
 2580        return value switch
 2581        {
 282            null => string.Empty,
 1283            string text => text,
 584            byte[] bytes => Convert.ToHexString(bytes),
 085            ReadOnlyMemory<byte> memory => Convert.ToHexString(memory.Span),
 086            Memory<byte> memory => Convert.ToHexString(memory.Span),
 287            DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture),
 088            DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("O", CultureInfo.InvariantCulture),
 189            IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
 390            IEnumerable sequence => JsonSerializer.Serialize(sequence.Cast<object?>().Select(ToCanonicalString).ToArray(
 091            _ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
 2592        };
 93    }
 94
 95    private static string Truncate(object? value, int? maximumLength)
 96    {
 597        if (maximumLength is null or <= 0)
 98        {
 199            throw new InvalidOperationException("Truncated audit values require a positive maximum length.");
 100        }
 101
 4102        string text = ToCanonicalString(value);
 4103        if (text.Length <= maximumLength.Value)
 104        {
 1105            return text;
 106        }
 107
 3108        int truncationLength = maximumLength.Value;
 3109        if (char.IsHighSurrogate(text[truncationLength - 1])
 3110            && char.IsLowSurrogate(text[truncationLength]))
 111        {
 1112            truncationLength--;
 113        }
 114
 3115        return text[..truncationLength];
 116    }
 117}