namespace StellaOps.Cryptography.Digests; /// /// Shared helpers for working with SHA-256 digests in the canonical sha256:<hex> form. /// public static class Sha256Digest { public const string Prefix = "sha256:"; public const int HexLength = 64; /// /// Normalizes an input digest to the canonical sha256:<lower-hex> form. /// /// Digest in either sha256:<hex> or bare-hex form. /// If true, requires the sha256: prefix to be present. /// Optional parameter name used in exception messages. /// Thrown when the input is null/empty/whitespace. /// Thrown when the input is not a valid SHA-256 hex digest. public static string Normalize(string digest, bool requirePrefix = false, string? parameterName = null) { if (string.IsNullOrWhiteSpace(digest)) { throw new ArgumentException("Digest is required.", parameterName ?? nameof(digest)); } var trimmed = digest.Trim(); string hex; if (trimmed.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase)) { hex = trimmed[Prefix.Length..]; } else if (requirePrefix) { var name = string.IsNullOrWhiteSpace(parameterName) ? "Digest" : parameterName; throw new FormatException($"{name} must start with '{Prefix}'."); } else if (trimmed.Contains(':', StringComparison.Ordinal)) { throw new FormatException($"Unsupported digest algorithm in '{digest}'. Only sha256 is supported."); } else { hex = trimmed; } hex = hex.Trim(); if (hex.Length != HexLength || !IsHex(hex.AsSpan())) { var name = string.IsNullOrWhiteSpace(parameterName) ? "Digest" : parameterName; throw new FormatException($"{name} must contain {HexLength} hexadecimal characters."); } return Prefix + hex.ToLowerInvariant(); } /// /// Normalizes a digest to the canonical form, returning null when the input is null/empty. /// public static string? NormalizeOrNull(string? digest, bool requirePrefix = false, string? parameterName = null) => string.IsNullOrWhiteSpace(digest) ? null : Normalize(digest, requirePrefix, parameterName); /// /// Extracts the lowercase hex value from a digest (with optional sha256: prefix). /// public static string ExtractHex(string digest, bool requirePrefix = false, string? parameterName = null) => Normalize(digest, requirePrefix, parameterName)[Prefix.Length..]; /// /// Computes a canonical sha256:<hex> digest for the provided content using the StellaOps crypto stack. /// public static string Compute(ICryptoHash hash, ReadOnlySpan content) { ArgumentNullException.ThrowIfNull(hash); return Prefix + hash.ComputeHashHex(content, HashAlgorithms.Sha256); } private static bool IsHex(ReadOnlySpan value) { foreach (var c in value) { if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { continue; } return false; } return true; } }