Skip to content

Instantly share code, notes, and snippets.

@salesHgabriel
Last active June 4, 2025 20:07
Show Gist options
  • Save salesHgabriel/da2f896123370b76f037651a9596075e to your computer and use it in GitHub Desktop.
Save salesHgabriel/da2f896123370b76f037651a9596075e to your computer and use it in GitHub Desktop.
Encrypter
"Encryption": {
"Key": "sua-key",
"Cipher": "aes-256-gcm"
}
//pode gerar uma com seguinte codigo
var key = new byte[32];
RandomNumberGenerator.Fill(key);
var base64Key = Convert.ToBase64String(key);
////
public class EncryptionOptions{ public string? Key { get; set; } public string? Cipher { get; set; } }
builder.Services.Configure<EncryptionOptions>(builder.Configuration.GetSection("Encryption"));
builder.Services.AddScoped<IEncrypter, Encrypter>();
builder.Services.AddScoped<IStringEncrypter>(sp => (sp.GetRequiredService<IEncrypter>() as IStringEncrypter)!);
////
public interface IEncrypter
{
string Encrypt(object value, bool serialize = true);
string EncryptString(string value);
T? Decrypt<T>(string payload, bool unserialize = true);
string? DecryptString(string payload);
byte[] GetKey();
}
public interface IStringEncrypter
{
string EncryptString(string value);
string DecryptString(string payload);
}
////////
public class Encrypter : IEncrypter, IStringEncrypter
{
private readonly byte[] _key;
private readonly List<byte[]> _previousKeys = new();
private readonly string _cipher;
private static readonly Dictionary<string, (int Size, bool Aead)> _supportedCiphers =
new(StringComparer.OrdinalIgnoreCase)
{
["aes-128-cbc"] = (16, false),
["aes-256-cbc"] = (32, false),
["aes-128-gcm"] = (16, true),
["aes-256-gcm"] = (32, true)
};
public Encrypter(IOptions<EncryptionOptions> options)
{
var key = Convert.FromBase64String(options.Value.Key!);
var cipher = options.Value.Cipher ?? "aes-256-gcm";
if (!IsSupported(key, cipher))
{
var ciphers = string.Join(", ", _supportedCiphers.Keys);
throw new InvalidOperationException(
$"Unsupported cipher or incorrect key length. Supported ciphers are: {ciphers}.");
}
_key = key;
_cipher = cipher.ToLowerInvariant();
}
public Encrypter(byte[] key, string cipher = "aes-256-gcm")
{
if (!IsSupported(key, cipher))
{
var ciphers = string.Join(", ", _supportedCiphers.Keys);
throw new InvalidOperationException(
$"Unsupported cipher or incorrect key length. Supported ciphers are: {ciphers}.");
}
_key = key;
_cipher = cipher.ToLowerInvariant();
}
public static bool IsSupported(byte[] key, string cipher)
{
if (!_supportedCiphers.TryGetValue(cipher.ToLowerInvariant(), out var cipherInfo))
{
return false;
}
return key.Length == cipherInfo.Size;
}
public static byte[] GenerateKey(string cipher = "aes-256-gcm")
{
if (!_supportedCiphers.TryGetValue(cipher.ToLowerInvariant(), out var cipherInfo))
{
throw new InvalidOperationException($"Unsupported cipher: {cipher}");
}
using var randomNumberGenerator = RandomNumberGenerator.Create();
var key = new byte[cipherInfo.Size];
randomNumberGenerator.GetBytes(key);
return key;
}
public string Encrypt(object value, bool serialize = true)
{
var iv = new byte[GetIvSize()];
using (var randomNumberGenerator = RandomNumberGenerator.Create())
{
randomNumberGenerator.GetBytes(iv);
}
byte[] tag = null;
byte[] encrypted;
if (serialize)
{
string serialized = JsonSerializer.Serialize(value);
encrypted = EncryptData(Encoding.UTF8.GetBytes(serialized), iv, ref tag);
}
else
{
if (value is string stringValue)
{
encrypted = EncryptData(Encoding.UTF8.GetBytes(stringValue), iv, ref tag);
}
else
{
throw new ArgumentException("Value must be a string when serialize is false");
}
}
// Calculate MAC for all modes, even AEAD (for AES-GCM, it will be ignored during decryption)
string mac = ComputeHash(iv, encrypted, _key);
string base64Iv = Convert.ToBase64String(iv);
string base64Value = Convert.ToBase64String(encrypted);
string base64Tag = tag != null ? Convert.ToBase64String(tag) : string.Empty;
var payload = new Dictionary<string, string> { ["iv"] = base64Iv, ["value"] = base64Value, ["mac"] = mac };
// Only add tag if it's not empty
if (!string.IsNullOrEmpty(base64Tag))
{
payload["tag"] = base64Tag;
}
string json = JsonSerializer.Serialize(payload);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
}
public string EncryptString(string value)
{
return Encrypt(value, false);
}
public T? Decrypt<T>(string payload, bool unserialize = true)
{
var payloadData = GetJsonPayload(payload);
var iv = Convert.FromBase64String(payloadData["iv"]);
byte[] tag = string.IsNullOrEmpty(payloadData["tag"]) ? null : Convert.FromBase64String(payloadData["tag"]);
EnsureTagIsValid(tag);
bool foundValidMac = false;
byte[] decrypted = null;
// Try all keys, starting with the current one
foreach (var key in GetAllKeys())
{
if (ShouldValidateMac() && !(foundValidMac = foundValidMac || ValidateMacForKey(payloadData, key)))
{
continue;
}
try
{
decrypted = DecryptData(Convert.FromBase64String(payloadData["value"]), iv, tag, key);
break;
}
catch
{
// Continue to try next key
}
}
if (ShouldValidateMac() && !foundValidMac)
{
throw new InvalidOperationException("The MAC is invalid.");
}
if (decrypted == null)
{
throw new InvalidOperationException("Could not decrypt the data.");
}
string decryptedString = Encoding.UTF8.GetString(decrypted);
if (typeof(T) == typeof(string))
{
return (T)(object)decryptedString;
}
return JsonSerializer.Deserialize<T>(decryptedString);
}
public string? DecryptString(string payload)
{
return Decrypt<string?>(payload, false);
}
public Encrypter PreviousKeys(IEnumerable<byte[]> keys)
{
foreach (var key in keys)
{
if (!IsSupported(key, _cipher))
{
var ciphers = string.Join(", ", _supportedCiphers.Keys);
throw new InvalidOperationException(
$"Unsupported cipher or incorrect key length. Supported ciphers are: {ciphers}.");
}
}
_previousKeys.Clear();
_previousKeys.AddRange(keys);
return this;
}
public byte[] GetKey()
{
return _key;
}
protected IEnumerable<byte[]> GetAllKeys()
{
yield return _key;
foreach (var key in _previousKeys)
{
yield return key;
}
}
protected IEnumerable<byte[]> GetPreviousKeys()
{
return _previousKeys;
}
#region Helper Methods
private byte[] EncryptData(byte[] data, byte[] iv, ref byte[] tag)
{
if (_cipher.EndsWith("-cbc"))
{
// CBC mode
using var aes = Aes.Create();
aes.Key = _key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
return encryptor.TransformFinalBlock(data, 0, data.Length);
}
else if (_cipher.EndsWith("-gcm"))
{
// GCM mode
tag = new byte[16]; // GCM tag size is 16 bytes
byte[] ciphertext = new byte[data.Length];
using var aesGcm = new AesGcm(_key);
aesGcm.Encrypt(iv, data, ciphertext, tag);
return ciphertext;
}
throw new InvalidOperationException($"Unsupported cipher mode: {_cipher}");
}
private byte[] DecryptData(byte[] encryptedData, byte[] iv, byte[] tag, byte[] key)
{
if (_cipher.EndsWith("-cbc"))
{
// CBC mode
using var aes = Aes.Create();
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
return decryptor.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
}
else if (_cipher.EndsWith("-gcm"))
{
// GCM mode
byte[] plaintext = new byte[encryptedData.Length];
using var aesGcm = new AesGcm(key);
aesGcm.Decrypt(iv, encryptedData, tag, plaintext);
return plaintext;
}
throw new InvalidOperationException($"Unsupported cipher mode: {_cipher}");
}
private int GetIvSize()
{
return _cipher.EndsWith("-cbc") ? 16 : 12; // CBC uses 16 bytes, GCM uses 12 bytes
}
protected string ComputeHash(byte[] iv, byte[] value, byte[] key)
{
using var hmac = new HMACSHA256(key);
var data = new byte[iv.Length + value.Length];
Buffer.BlockCopy(iv, 0, data, 0, iv.Length);
Buffer.BlockCopy(value, 0, data, iv.Length, value.Length);
var hash = hmac.ComputeHash(data);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
protected Dictionary<string, string> GetJsonPayload(string payload)
{
if (string.IsNullOrEmpty(payload))
{
throw new InvalidOperationException("The payload is invalid.");
}
try
{
var json = Encoding.UTF8.GetString(Convert.FromBase64String(payload));
var result = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (!ValidatePayload(result))
{
throw new InvalidOperationException("The payload is invalid.");
}
return result;
}
catch
{
throw new InvalidOperationException("The payload is invalid.");
}
}
protected bool ValidatePayload(Dictionary<string, string> payload)
{
if (payload == null)
{
return false;
}
foreach (var item in new[] { "iv", "value", "mac" })
{
if (!payload.ContainsKey(item) || string.IsNullOrEmpty(payload[item]))
{
return false;
}
}
if (payload.ContainsKey("tag") && payload["tag"] != null && !_supportedCiphers[_cipher].Aead)
{
return false;
}
try
{
var iv = Convert.FromBase64String(payload["iv"]);
return iv.Length == GetIvSize();
}
catch
{
return false;
}
}
protected bool ValidateMac(Dictionary<string, string> payload)
{
return ValidateMacForKey(payload, _key);
}
protected bool ValidateMacForKey(Dictionary<string, string> payload, byte[] key)
{
var iv = Convert.FromBase64String(payload["iv"]);
var value = Convert.FromBase64String(payload["value"]);
var computedMac = ComputeHash(iv, value, key);
return CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(computedMac),
Encoding.ASCII.GetBytes(payload["mac"]));
}
protected void EnsureTagIsValid(byte[] tag)
{
if (_supportedCiphers[_cipher].Aead)
{
if (tag == null || tag.Length != 16)
{
throw new InvalidOperationException("AEAD cipher requires a 16-byte authentication tag.");
}
}
else if (tag != null)
{
throw new InvalidOperationException(
"Unable to use tag because the cipher algorithm does not support AEAD.");
}
}
protected bool ShouldValidateMac()
{
return !_supportedCiphers[_cipher].Aead;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment