using System.Security.Cryptography; using System.Text; namespace MiaoJiZhang.Api.Services; public sealed class LlmSecretProtector(IConfiguration configuration) { public const string ConfigKey = "llm.api_key_encrypted"; private const string Prefix = "v1"; public string Protect(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("API Key 不能为空", nameof(value)); var key = ReadEncryptionKey(); var nonce = RandomNumberGenerator.GetBytes(12); var plaintext = Encoding.UTF8.GetBytes(value.Trim()); var ciphertext = new byte[plaintext.Length]; var tag = new byte[16]; using var aes = new AesGcm(key, tag.Length); aes.Encrypt(nonce, plaintext, ciphertext, tag); return string.Join(':', Prefix, Convert.ToBase64String(nonce), Convert.ToBase64String(ciphertext), Convert.ToBase64String(tag)); } public string Unprotect(string protectedValue) { var parts = protectedValue.Split(':'); if (parts.Length != 4 || parts[0] != Prefix) throw new CryptographicException("不支持的密钥密文格式"); var nonce = Convert.FromBase64String(parts[1]); var ciphertext = Convert.FromBase64String(parts[2]); var tag = Convert.FromBase64String(parts[3]); var plaintext = new byte[ciphertext.Length]; using var aes = new AesGcm(ReadEncryptionKey(), tag.Length); aes.Decrypt(nonce, ciphertext, tag, plaintext); return Encoding.UTF8.GetString(plaintext); } public bool TryUnprotect(string? protectedValue, out string value) { value = ""; if (string.IsNullOrWhiteSpace(protectedValue)) return false; try { value = Unprotect(protectedValue); return !string.IsNullOrWhiteSpace(value); } catch (Exception exception) when ( exception is ArgumentException or FormatException or CryptographicException or InvalidOperationException) { return false; } } public static string Mask(string value) { if (string.IsNullOrEmpty(value)) return ""; var suffixLength = Math.Min(4, value.Length); return $"••••{value[^suffixLength..]}"; } private byte[] ReadEncryptionKey() { var raw = configuration["Secrets:EncryptionKey"]; byte[]? key = null; try { key = string.IsNullOrWhiteSpace(raw) ? null : Convert.FromBase64String(raw); } catch (FormatException) { } if (key?.Length != 32) throw new InvalidOperationException( "请通过 Secrets__EncryptionKey 配置 base64 编码的 32 字节密钥后再保存 API Key"); return key; } }