using LiteCharms.Features.Abstractions; using LiteCharms.Features.Hasher.Configuration; namespace LiteCharms.Features.Hasher; public sealed partial class HashService(IHashids hasher, IOptions options) : IService { private readonly HasherSettings settings = options.Value; [System.Text.RegularExpressions.GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")] private static partial System.Text.RegularExpressions.Regex HexHashRegex(); public static readonly Func StringToSha256Hash = (input) => string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input))); public static readonly Func StreamToSha256Hash = (stream) => stream is null ? null : Convert.ToHexString(SHA256.HashData(stream)); public static readonly Func BytesToSha256Hash = (bytes) => bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes)); public static Result ComputeMd5Hash(string input) { if (string.IsNullOrEmpty(input)) return Result.Fail("Input content cannot be null or empty for MD5 processing."); byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input)); return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant()); } public Result VerifyPayfastWebhookSignature(IDictionary incomingFormData, string incomingSignature) { try { if (string.IsNullOrWhiteSpace(incomingSignature)) return Result.Fail("Validation failed: Missing signature string parameter."); // 1. Sort the parameters alphabetically and exclude the signature parameter to prevent recursive checking var sortedFields = incomingFormData .Where(field => field.Key != "signature") .OrderBy(field => field.Key) .Select(field => $"{field.Key}={Uri.EscapeDataString(field.Value).Replace("%20", "+")}"); string payload = string.Join("&", sortedFields); // 2. Append the secure, passphrase injected into the container pod from your environment variables if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase)) { payload += $"&passphrase={Uri.EscapeDataString(settings.PayfastPassphrase).Replace("%20", "+")}"; } // 3. Compute localized hex token var localHashResult = ComputeMd5Hash(payload); if (!localHashResult.IsSuccess) return Result.Fail(localHashResult.Errors); // 4. Constant-time secure text comparison to fully block timing analysis attacks bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase); return Result.Ok(isValid); } catch (Exception ex) { return Result.Fail(new Error("An error occurred during MD5 verification loop.").CausedBy(ex)); } } public Result HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex().IsMatch(input) ? Result.Fail("Input must be a valid hexadecimal string.") : Result.Ok(hasher.EncodeHex(input)); public Result HashEncodeIntId(int id) => id < 0 ? Result.Fail("Id cannot be negative.") : Result.Ok(hasher.Encode(id)); public Result HashEncodeLongId(long id) => id < 0 ? Result.Fail("Id cannot be negative.") : Result.Ok(hasher.EncodeLong(id)); public Result DecodeIntIdHash(string hash) { if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); int[] decoded = hasher.Decode(hash); return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Int hash token."); } public Result DecodeLongIdHash(string hash) { if (string.IsNullOrWhiteSpace(hash)) return Result.Fail("Invalid token layout."); long[] decoded = hasher.DecodeLong(hash); return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail("Invalid or modified Long hash token."); } public Result DecodeHexHash(string hex) { try { string decoded = hasher.DecodeHex(hex); return string.IsNullOrEmpty(decoded) ? Result.Fail("Invalid or corrupted hex hash.") : Result.Ok(decoded); } catch (FormatException fex) { return Result.Fail(new Error("Invalid hash structure.").CausedBy(fex)); } catch (Exception ex) { return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } }