using System.Text; using EntKube.Secrets.Crypto; using FluentAssertions; namespace EntKube.Secrets.Tests.Crypto; /// /// Tests for the AES-256-GCM encryption primitive. This is the foundation of /// the entire secrets manager — every secret is encrypted with AES-256-GCM /// before being stored. We verify that encryption produces ciphertext that /// differs from plaintext, that decryption recovers the original data, and /// that tampering with any part of the ciphertext is detected. /// public class AesGcmEncryptorTests { [Fact] public void Encrypt_ThenDecrypt_RecoversOriginalPlaintext() { // Arrange — A 256-bit key and some plaintext to protect. // This is the most basic contract: encrypt then decrypt = identity. byte[] key = AesGcmEncryptor.GenerateKey(); byte[] plaintext = Encoding.UTF8.GetBytes("dns-solver-api-token-abc123"); // Act — Encrypt the plaintext, then decrypt the result. byte[] ciphertext = AesGcmEncryptor.Encrypt(key, plaintext); byte[] recovered = AesGcmEncryptor.Decrypt(key, ciphertext); // Assert — The recovered plaintext must match the original exactly. recovered.Should().BeEquivalentTo(plaintext); } [Fact] public void Encrypt_ProducesDifferentCiphertextEachTime() { // Arrange — Same key and same plaintext encrypted twice. // AES-GCM uses a random nonce, so each encryption should produce // different ciphertext even for identical inputs. byte[] key = AesGcmEncryptor.GenerateKey(); byte[] plaintext = Encoding.UTF8.GetBytes("same-secret-value"); // Act byte[] ciphertext1 = AesGcmEncryptor.Encrypt(key, plaintext); byte[] ciphertext2 = AesGcmEncryptor.Encrypt(key, plaintext); // Assert — Ciphertexts must differ (different nonces). ciphertext1.Should().NotBeEquivalentTo(ciphertext2); } [Fact] public void Encrypt_CiphertextDiffersFromPlaintext() { // Arrange byte[] key = AesGcmEncryptor.GenerateKey(); byte[] plaintext = Encoding.UTF8.GetBytes("my-super-secret-value"); // Act byte[] ciphertext = AesGcmEncryptor.Encrypt(key, plaintext); // Assert — The ciphertext must not contain the plaintext verbatim. string ciphertextStr = Encoding.UTF8.GetString(ciphertext); ciphertextStr.Should().NotContain("my-super-secret-value"); } [Fact] public void Decrypt_WithWrongKey_ThrowsCryptographicException() { // Arrange — Encrypt with one key, try to decrypt with a different key. // AES-GCM's authentication tag ensures this fails loudly. byte[] correctKey = AesGcmEncryptor.GenerateKey(); byte[] wrongKey = AesGcmEncryptor.GenerateKey(); byte[] plaintext = Encoding.UTF8.GetBytes("secret-data"); byte[] ciphertext = AesGcmEncryptor.Encrypt(correctKey, plaintext); // Act & Assert — Decryption with the wrong key must fail. Action act = () => AesGcmEncryptor.Decrypt(wrongKey, ciphertext); act.Should().Throw(); } [Fact] public void Decrypt_WithTamperedCiphertext_ThrowsCryptographicException() { // Arrange — Encrypt normally, then flip a bit in the ciphertext. // AES-GCM's authenticated encryption detects any tampering. byte[] key = AesGcmEncryptor.GenerateKey(); byte[] plaintext = Encoding.UTF8.GetBytes("integrity-protected-data"); byte[] ciphertext = AesGcmEncryptor.Encrypt(key, plaintext); // Tamper with a byte in the encrypted portion (after the nonce). ciphertext[15] ^= 0xFF; // Act & Assert — Tampered data must be rejected. Action act = () => AesGcmEncryptor.Decrypt(key, ciphertext); act.Should().Throw(); } [Fact] public void GenerateKey_Returns32Bytes() { // AES-256 requires a 256-bit (32-byte) key. byte[] key = AesGcmEncryptor.GenerateKey(); key.Should().HaveCount(32); } [Fact] public void GenerateKey_ProducesUniqueKeys() { // Two generated keys must never be the same (CSPRNG guarantee). byte[] key1 = AesGcmEncryptor.GenerateKey(); byte[] key2 = AesGcmEncryptor.GenerateKey(); key1.Should().NotBeEquivalentTo(key2); } [Fact] public void Encrypt_WithEmptyPlaintext_WorksCorrectly() { // Edge case — encrypting an empty byte array should still work. // Some secrets might be empty during initialization. byte[] key = AesGcmEncryptor.GenerateKey(); byte[] plaintext = Array.Empty(); // Act byte[] ciphertext = AesGcmEncryptor.Encrypt(key, plaintext); byte[] recovered = AesGcmEncryptor.Decrypt(key, ciphertext); // Assert recovered.Should().BeEmpty(); } [Fact] public void Encrypt_WithLargePayload_WorksCorrectly() { // Secrets can be large (e.g., PEM certificates, JSON blobs). // Verify AES-GCM handles payloads beyond typical small secrets. byte[] key = AesGcmEncryptor.GenerateKey(); byte[] plaintext = new byte[64 * 1024]; // 64KB Random.Shared.NextBytes(plaintext); // Act byte[] ciphertext = AesGcmEncryptor.Encrypt(key, plaintext); byte[] recovered = AesGcmEncryptor.Decrypt(key, ciphertext); // Assert recovered.Should().BeEquivalentTo(plaintext); } }