too much for one commit

This commit is contained in:
Nils Blomgren
2026-05-13 14:01:32 +02:00
parent a96dd33039
commit 328d494530
394 changed files with 98104 additions and 78 deletions

View 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);
}
}