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);
}
}