using System; using System.Security.Cryptography; using System.Text; namespace StellaOps.Auth.Security.Dpop; internal static class DpopNonceUtilities { private static readonly char[] Base64Padding = { '=' }; internal static string GenerateNonce() { Span buffer = stackalloc byte[32]; RandomNumberGenerator.Fill(buffer); return Convert.ToBase64String(buffer) .TrimEnd(Base64Padding) .Replace('+', '-') .Replace('/', '_'); } internal static byte[] ComputeNonceHash(string nonce) { ArgumentException.ThrowIfNullOrWhiteSpace(nonce); var bytes = Encoding.UTF8.GetBytes(nonce); return SHA256.HashData(bytes); } internal static string EncodeHash(ReadOnlySpan hash) => Convert.ToHexString(hash); internal static string ComputeStorageKey(string audience, string clientId, string keyThumbprint) { ArgumentException.ThrowIfNullOrWhiteSpace(audience); ArgumentException.ThrowIfNullOrWhiteSpace(clientId); ArgumentException.ThrowIfNullOrWhiteSpace(keyThumbprint); return string.Create( "dpop-nonce:".Length + audience.Length + clientId.Length + keyThumbprint.Length + 2, (audience.Trim(), clientId.Trim(), keyThumbprint.Trim()), static (span, parts) => { var index = 0; const string Prefix = "dpop-nonce:"; Prefix.CopyTo(span); index += Prefix.Length; index = Append(span, index, parts.Item1); span[index++] = ':'; index = Append(span, index, parts.Item2); span[index++] = ':'; _ = Append(span, index, parts.Item3); }); static int Append(Span span, int index, string value) { if (value.Length == 0) { throw new ArgumentException("Value must not be empty after trimming."); } value.AsSpan().CopyTo(span[index..]); return index + value.Length; } } }