using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Options; public class PhiEncryptionService : IPhiEncryptionService { private readonly IDataProtector _protector; private readonly byte[] _searchKey; public PhiEncryptionService( IDataProtectionProvider provider, IOptions options) { var opts = options.Value; _protector = provider.CreateProtector(opts.ProtectorPurpose); _searchKey = Convert.FromBase64String( Convert.ToBase64String(Encoding.UTF8.GetBytes(opts.SearchTokenKey))[..44]); // normalize dev key } public string Encrypt(string plaintext) { if (string.IsNullOrEmpty(plaintext)) return plaintext; return _protector.Protect(plaintext); } public string Decrypt(string ciphertext) { if (string.IsNullOrEmpty(ciphertext)) return ciphertext; if (!IsEncrypted(ciphertext)) return ciphertext; // migration transition: plaintext rows still readable return _protector.Unprotect(ciphertext); } public bool IsEncrypted(string value) => value.StartsWith("CfDJ8", StringComparison.Ordinal) || // DataProtection prefix value.Length > 50; // heuristic for protected payloads public string ComputeNameSearchToken(string firstName, string lastName) { var normalized = $"{firstName.Trim().ToLowerInvariant()}|{lastName.Trim().ToLowerInvariant()}"; using var hmac = new HMACSHA256(_searchKey); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(normalized)); return Convert.ToHexString(hash).ToLowerInvariant(); } }