too much for one commit
This commit is contained in:
169
tests/EntKube.Secrets.Tests/Crypto/AesGcmEncryptorTests.cs
Normal file
169
tests/EntKube.Secrets.Tests/Crypto/AesGcmEncryptorTests.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System.Text;
|
||||
using EntKube.Secrets.Crypto;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Secrets.Tests.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<System.Security.Cryptography.CryptographicException>();
|
||||
}
|
||||
|
||||
[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<System.Security.Cryptography.CryptographicException>();
|
||||
}
|
||||
|
||||
[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<byte>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user