using System.Security.Cryptography; using System.Text; namespace MiaoJiZhang.Api.Services; public sealed class PushTokenProtector(IConfiguration configuration) { private readonly byte[]? key = ReadKey(configuration["Push:TokenEncryptionKey"]); public bool IsConfigured => key is { Length: 32 }; public string Protect(string value) { if (key is null) throw new InvalidOperationException("Push__TokenEncryptionKey must be a base64-encoded 32-byte key"); var plaintext = Encoding.UTF8.GetBytes(value); var nonce = RandomNumberGenerator.GetBytes(12); var tag = new byte[16]; var ciphertext = new byte[plaintext.Length]; using var aes = new AesGcm(key, tag.Length); aes.Encrypt(nonce, plaintext, ciphertext, tag); var envelope = new byte[nonce.Length + tag.Length + ciphertext.Length]; Buffer.BlockCopy(nonce, 0, envelope, 0, nonce.Length); Buffer.BlockCopy(tag, 0, envelope, nonce.Length, tag.Length); Buffer.BlockCopy(ciphertext, 0, envelope, nonce.Length + tag.Length, ciphertext.Length); return Convert.ToBase64String(envelope); } public string Unprotect(string value) { if (key is null) throw new InvalidOperationException("Push__TokenEncryptionKey must be a base64-encoded 32-byte key"); var envelope = Convert.FromBase64String(value); if (envelope.Length < 29) throw new CryptographicException("Invalid push token envelope"); var nonce = envelope.AsSpan(0, 12); var tag = envelope.AsSpan(12, 16); var ciphertext = envelope.AsSpan(28); var plaintext = new byte[ciphertext.Length]; using var aes = new AesGcm(key, tag.Length); aes.Decrypt(nonce, ciphertext, tag, plaintext); return Encoding.UTF8.GetString(plaintext); } public static string Hash(string value) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))); public static string CreateUnbindToken() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)) .TrimEnd('=').Replace('+', '-').Replace('/', '_'); private static byte[]? ReadKey(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; try { var parsed = Convert.FromBase64String(value); return parsed.Length == 32 ? parsed : null; } catch (FormatException) { return null; } } }