using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Options; namespace TeamUp.Modules.Integrations.Security; internal sealed class EncryptionOptions { public const string SectionName = "Encryption"; /// Deployment master secret. A 32-byte AES key is derived from it (SHA-256). public string MasterKey { get; set; } = string.Empty; } internal interface ISecretProtector { string Protect(string plaintext); string Unprotect(string protectedValue); } /// /// AES-256-GCM authenticated encryption with a key derived from the deployment master secret. /// Output blob = nonce(12) ‖ tag(16) ‖ ciphertext, base64-encoded. /// internal sealed class AesGcmSecretProtector : ISecretProtector { private const int NonceSize = 12; private const int TagSize = 16; private readonly byte[] _key; public AesGcmSecretProtector(IOptions options) { var masterKey = options.Value.MasterKey; if (string.IsNullOrWhiteSpace(masterKey)) { throw new InvalidOperationException("Missing 'Encryption:MasterKey'."); } _key = SHA256.HashData(Encoding.UTF8.GetBytes(masterKey)); } public string Protect(string plaintext) { var plain = Encoding.UTF8.GetBytes(plaintext); var nonce = RandomNumberGenerator.GetBytes(NonceSize); var cipher = new byte[plain.Length]; var tag = new byte[TagSize]; using var aes = new AesGcm(_key, TagSize); aes.Encrypt(nonce, plain, cipher, tag); var blob = new byte[NonceSize + TagSize + cipher.Length]; Buffer.BlockCopy(nonce, 0, blob, 0, NonceSize); Buffer.BlockCopy(tag, 0, blob, NonceSize, TagSize); Buffer.BlockCopy(cipher, 0, blob, NonceSize + TagSize, cipher.Length); return Convert.ToBase64String(blob); } public string Unprotect(string protectedValue) { var blob = Convert.FromBase64String(protectedValue); var nonce = blob.AsSpan(0, NonceSize); var tag = blob.AsSpan(NonceSize, TagSize); var cipher = blob.AsSpan(NonceSize + TagSize); var plain = new byte[cipher.Length]; using var aes = new AesGcm(_key, TagSize); aes.Decrypt(nonce, cipher, tag, plain); return Encoding.UTF8.GetString(plain); } }