feat: implement AEAD encryption using AES api

This commit is contained in:
Stevan Freeborn
2026-04-01 09:12:49 -05:00
parent b843cf9f08
commit 923aeb4ec1
2 changed files with 87 additions and 9 deletions
@@ -1,20 +1,67 @@
using System.Security.Cryptography;
using System.Text;
namespace StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
internal sealed class AesCryptoProvider(IEncryptionKeyProvider keyProvider) : ICryptoProvider
{
private const int NonceSize = 12;
private const int TagSize = 16;
private readonly IEncryptionKeyProvider _keyProvider = keyProvider
?? throw new ArgumentNullException(nameof(keyProvider));
// TODO: Implement this shit
public string Encrypt(string plainText)
{
if (string.IsNullOrWhiteSpace(plainText))
{
return plainText;
}
var key = _keyProvider.GetKey();
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var nonce = new byte[NonceSize].AsSpan();
RandomNumberGenerator.Fill(nonce);
var tag = new byte[TagSize].AsSpan();
var cipherBytes = new byte[plainBytes.Length];
using var aesGcm = new AesGcm(key);
aesGcm.Encrypt(nonce, plainBytes, cipherBytes, tag);
var combinedBytes = new byte[NonceSize + TagSize + plainBytes.Length];
nonce.CopyTo(combinedBytes.AsSpan(0, NonceSize));
tag.CopyTo(combinedBytes.AsSpan(NonceSize, TagSize));
cipherBytes.CopyTo(combinedBytes.AsSpan(NonceSize + TagSize));
return Convert.ToBase64String(combinedBytes);
}
public string Decrypt(string cipherText)
{
_ = _keyProvider.GetKey();
throw new NotImplementedException();
if (string.IsNullOrWhiteSpace(cipherText))
{
return cipherText;
}
public string Encrypt(string plainText)
var key = _keyProvider.GetKey();
var combinedBytes = Convert.FromBase64String(cipherText).AsSpan();
if (combinedBytes.Length < NonceSize + TagSize)
{
throw new NotImplementedException();
throw new CryptographicException($"Invalid playload. {nameof(cipherText)} is not of expected length");
}
var nonce = combinedBytes[..NonceSize];
var tag = combinedBytes.Slice(NonceSize, TagSize);
var cipherBytes = combinedBytes[(NonceSize + TagSize)..];
var plainBytes = new byte[cipherBytes.Length];
using var aesGcm = new AesGcm(key);
aesGcm.Decrypt(nonce, cipherBytes, tag, plainBytes);
return Encoding.UTF8.GetString(plainBytes);
}
}
@@ -1,3 +1,5 @@
using System.Security.Cryptography;
using Moq;
using StevanFreeborn.Extensions.Configuration.Secure.Cryptography;
@@ -11,6 +13,10 @@ public class AesCryptoProviderTests
public AesCryptoProviderTests()
{
var key = new byte[32];
RandomNumberGenerator.Fill(key);
_mockKeyProvider.Setup(static m => m.GetKey()).Returns(key);
_sut = new(_mockKeyProvider.Object);
}
@@ -25,4 +31,29 @@ public class AesCryptoProviderTests
encryptedText.Should().NotBe(originalText);
decryptedText.Should().Be(originalText);
}
[Fact]
public void Encrypt_WhenCalledWithSameInputTwitch_ItShouldProduceDifferentCipherText()
{
var plainText = "identicalInput";
var cipherTextOne = _sut.Encrypt(plainText);
var cipherTextTwo = _sut.Encrypt(plainText);
cipherTextOne.Should().NotBe(cipherTextTwo);
}
[Fact]
public void Decrypt_WhenCalledWithTamperedData_ItShouldThrowCrytographicException()
{
var cipherText = _sut.Encrypt("some data");
var rawBytes = Convert.FromBase64String(cipherText);
rawBytes[^1] = (byte)(rawBytes[^1] ^ 0xFF);
var tamperedText = Convert.ToBase64String(rawBytes);
var act = () => _sut.Decrypt(tamperedText);
act.Should().Throw<CryptographicException>();
}
}