73 lines
2.3 KiB
C#
73 lines
2.3 KiB
C#
|
|
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";
|
||
|
|
|
||
|
|
/// <summary>Deployment master secret. A 32-byte AES key is derived from it (SHA-256).</summary>
|
||
|
|
public string MasterKey { get; set; } = string.Empty;
|
||
|
|
}
|
||
|
|
|
||
|
|
internal interface ISecretProtector
|
||
|
|
{
|
||
|
|
string Protect(string plaintext);
|
||
|
|
|
||
|
|
string Unprotect(string protectedValue);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// AES-256-GCM authenticated encryption with a key derived from the deployment master secret.
|
||
|
|
/// Output blob = nonce(12) ‖ tag(16) ‖ ciphertext, base64-encoded.
|
||
|
|
/// </summary>
|
||
|
|
internal sealed class AesGcmSecretProtector : ISecretProtector
|
||
|
|
{
|
||
|
|
private const int NonceSize = 12;
|
||
|
|
private const int TagSize = 16;
|
||
|
|
private readonly byte[] _key;
|
||
|
|
|
||
|
|
public AesGcmSecretProtector(IOptions<EncryptionOptions> 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);
|
||
|
|
}
|
||
|
|
}
|