diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Cryptography/AesCryptoProvider.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Cryptography/AesCryptoProvider.cs index c75220f..8cf4e77 100644 --- a/src/StevanFreeborn.Extensions.Configuration.Secure/Cryptography/AesCryptoProvider.cs +++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Cryptography/AesCryptoProvider.cs @@ -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 Decrypt(string cipherText) - { - _ = _keyProvider.GetKey(); - throw new NotImplementedException(); - } - public string Encrypt(string plainText) { - throw new NotImplementedException(); + 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) + { + if (string.IsNullOrWhiteSpace(cipherText)) + { + return cipherText; + } + + var key = _keyProvider.GetKey(); + var combinedBytes = Convert.FromBase64String(cipherText).AsSpan(); + + if (combinedBytes.Length < NonceSize + TagSize) + { + 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); } } \ No newline at end of file diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Cryptography/AesCryptoProviderTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Cryptography/AesCryptoProviderTests.cs index f1a2dd1..ca66927 100644 --- a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Cryptography/AesCryptoProviderTests.cs +++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Cryptography/AesCryptoProviderTests.cs @@ -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(); + } } \ No newline at end of file