-
-
Save salesHgabriel/da2f896123370b76f037651a9596075e to your computer and use it in GitHub Desktop.
Encrypter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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