Files
Entkube/tests/EntKube.Web.Tests/VaultEncryptionServiceTests.cs
2026-05-18 08:37:09 +02:00

178 lines
5.3 KiB
C#

using EntKube.Web.Services;
using FluentAssertions;
namespace EntKube.Web.Tests;
/// <summary>
/// Tests for the envelope encryption service that powers the secrets vault.
/// The service uses AES-256-GCM at both layers: root key → DEK, DEK → secret values.
/// </summary>
public class VaultEncryptionServiceTests
{
// A stable 32-byte root key for testing purposes.
private static readonly byte[] TestRootKey = Convert.FromBase64String(
"dGhpcyBpcyBhIDMyIGJ5dGUga2V5ISEhMTIzNDU2Nzg=");
private readonly VaultEncryptionService sut = new(TestRootKey);
// --- Data Key Generation ---
[Fact]
public void GenerateDataKey_ReturnsThirtyTwoBytes()
{
// A data encryption key should be 256 bits (32 bytes) for AES-256.
byte[] dataKey = sut.GenerateDataKey();
dataKey.Should().HaveCount(32);
}
[Fact]
public void GenerateDataKey_ProducesUniqueKeysEachCall()
{
// Every call should produce a cryptographically random key.
byte[] key1 = sut.GenerateDataKey();
byte[] key2 = sut.GenerateDataKey();
key1.Should().NotEqual(key2);
}
// --- Sealing and Unsealing Data Keys ---
[Fact]
public void SealDataKey_ProducesNonEmptyCiphertextAndNonce()
{
// Sealing a DEK with the root key should produce ciphertext + nonce.
byte[] dataKey = sut.GenerateDataKey();
(byte[] encryptedKey, byte[] nonce) = sut.SealDataKey(dataKey);
encryptedKey.Should().NotBeEmpty();
nonce.Should().NotBeEmpty();
encryptedKey.Should().NotEqual(dataKey);
}
[Fact]
public void UnsealDataKey_RecoversOriginalKey()
{
// Unsealing a previously sealed DEK should return the exact same key.
byte[] originalKey = sut.GenerateDataKey();
(byte[] encryptedKey, byte[] nonce) = sut.SealDataKey(originalKey);
byte[] recovered = sut.UnsealDataKey(encryptedKey, nonce);
recovered.Should().Equal(originalKey);
}
[Fact]
public void UnsealDataKey_WithWrongRootKey_Throws()
{
// If the root key is different, unsealing must fail.
byte[] dataKey = sut.GenerateDataKey();
(byte[] encryptedKey, byte[] nonce) = sut.SealDataKey(dataKey);
// Create a service with a different root key.
byte[] wrongRoot = new byte[32];
Array.Fill(wrongRoot, (byte)0xFF);
VaultEncryptionService wrongService = new(wrongRoot);
Action act = () => wrongService.UnsealDataKey(encryptedKey, nonce);
act.Should().Throw<Exception>();
}
// --- Encrypting and Decrypting Secret Values ---
[Fact]
public void Encrypt_ProducesCiphertextDifferentFromPlaintext()
{
// Encrypting a secret value should produce unreadable ciphertext.
byte[] dataKey = sut.GenerateDataKey();
string plaintext = "super-secret-database-password";
(byte[] ciphertext, byte[] nonce) = sut.Encrypt(dataKey, plaintext);
ciphertext.Should().NotBeEmpty();
nonce.Should().NotBeEmpty();
System.Text.Encoding.UTF8.GetString(ciphertext).Should().NotBe(plaintext);
}
[Fact]
public void Decrypt_RecoversOriginalPlaintext()
{
// Decrypting should return the exact original secret value.
byte[] dataKey = sut.GenerateDataKey();
string original = "my-api-key-12345!@#$%";
(byte[] ciphertext, byte[] nonce) = sut.Encrypt(dataKey, original);
string recovered = sut.Decrypt(dataKey, ciphertext, nonce);
recovered.Should().Be(original);
}
[Fact]
public void Decrypt_WithWrongDataKey_Throws()
{
// Using the wrong DEK to decrypt must fail — tenant isolation.
byte[] dataKey1 = sut.GenerateDataKey();
byte[] dataKey2 = sut.GenerateDataKey();
string plaintext = "tenant-a-secret";
(byte[] ciphertext, byte[] nonce) = sut.Encrypt(dataKey1, plaintext);
Action act = () => sut.Decrypt(dataKey2, ciphertext, nonce);
act.Should().Throw<Exception>();
}
[Fact]
public void Encrypt_SamePlaintext_ProducesDifferentCiphertext()
{
// Each encryption uses a unique nonce so identical plaintexts
// produce different ciphertexts — preventing pattern analysis.
byte[] dataKey = sut.GenerateDataKey();
string plaintext = "repeated-secret";
(byte[] ciphertext1, byte[] nonce1) = sut.Encrypt(dataKey, plaintext);
(byte[] ciphertext2, byte[] nonce2) = sut.Encrypt(dataKey, plaintext);
ciphertext1.Should().NotEqual(ciphertext2);
nonce1.Should().NotEqual(nonce2);
}
[Fact]
public void Encrypt_EmptyString_CanBeDecrypted()
{
// Edge case: empty secrets should encrypt/decrypt cleanly.
byte[] dataKey = sut.GenerateDataKey();
(byte[] ciphertext, byte[] nonce) = sut.Encrypt(dataKey, "");
string recovered = sut.Decrypt(dataKey, ciphertext, nonce);
recovered.Should().BeEmpty();
}
[Fact]
public void Encrypt_LargeValue_CanBeDecrypted()
{
// Secrets like certificates or multi-line configs can be large.
byte[] dataKey = sut.GenerateDataKey();
string large = new string('X', 100_000);
(byte[] ciphertext, byte[] nonce) = sut.Encrypt(dataKey, large);
string recovered = sut.Decrypt(dataKey, ciphertext, nonce);
recovered.Should().Be(large);
}
}