too much for one commit
This commit is contained in:
143
tests/EntKube.Secrets.Tests/Crypto/EnvelopeEncryptionTests.cs
Normal file
143
tests/EntKube.Secrets.Tests/Crypto/EnvelopeEncryptionTests.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.Text;
|
||||
using EntKube.Secrets.Crypto;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Secrets.Tests.Crypto;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the envelope encryption engine. Envelope encryption is the core
|
||||
/// pattern: each secret gets its own Data Encryption Key (DEK), the DEK encrypts
|
||||
/// the secret, and the Master Encryption Key (MEK) encrypts the DEK. This way:
|
||||
/// - Rotating the MEK only requires re-encrypting DEKs (not all secrets)
|
||||
/// - Compromising one DEK only exposes one secret
|
||||
/// - The MEK never directly touches secret data
|
||||
/// </summary>
|
||||
public class EnvelopeEncryptionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Seal_ThenOpen_RecoversOriginalPlaintext()
|
||||
{
|
||||
// Arrange — A master key and some secret data to protect.
|
||||
// "Seal" wraps the data in an envelope (generates DEK, encrypts data,
|
||||
// encrypts DEK). "Open" reverses the process.
|
||||
|
||||
byte[] masterKey = AesGcmEncryptor.GenerateKey();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes("azure-dns-client-secret-value");
|
||||
|
||||
// Act
|
||||
|
||||
EncryptedEnvelope envelope = EnvelopeEncryption.Seal(masterKey, plaintext);
|
||||
byte[] recovered = EnvelopeEncryption.Open(masterKey, envelope);
|
||||
|
||||
// Assert
|
||||
|
||||
recovered.Should().BeEquivalentTo(plaintext);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Seal_ProducesDifferentEnvelopesForSameData()
|
||||
{
|
||||
// Arrange — Each seal operation generates a new random DEK, so
|
||||
// two envelopes for the same data should be completely different.
|
||||
|
||||
byte[] masterKey = AesGcmEncryptor.GenerateKey();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes("same-secret");
|
||||
|
||||
// Act
|
||||
|
||||
EncryptedEnvelope envelope1 = EnvelopeEncryption.Seal(masterKey, plaintext);
|
||||
EncryptedEnvelope envelope2 = EnvelopeEncryption.Seal(masterKey, plaintext);
|
||||
|
||||
// Assert — Both the encrypted DEK and encrypted data should differ.
|
||||
|
||||
envelope1.EncryptedDek.Should().NotBeEquivalentTo(envelope2.EncryptedDek);
|
||||
envelope1.EncryptedData.Should().NotBeEquivalentTo(envelope2.EncryptedData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_WithWrongMasterKey_Fails()
|
||||
{
|
||||
// Arrange — Seal with one MEK, try to open with another.
|
||||
// The wrong MEK can't decrypt the DEK, so decryption fails.
|
||||
|
||||
byte[] correctMek = AesGcmEncryptor.GenerateKey();
|
||||
byte[] wrongMek = AesGcmEncryptor.GenerateKey();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes("secret-data");
|
||||
|
||||
EncryptedEnvelope envelope = EnvelopeEncryption.Seal(correctMek, plaintext);
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Action act = () => EnvelopeEncryption.Open(wrongMek, envelope);
|
||||
act.Should().Throw<System.Security.Cryptography.CryptographicException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_WithTamperedEncryptedDek_Fails()
|
||||
{
|
||||
// Arrange — Tamper with the encrypted DEK.
|
||||
|
||||
byte[] masterKey = AesGcmEncryptor.GenerateKey();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes("integrity-check");
|
||||
|
||||
EncryptedEnvelope envelope = EnvelopeEncryption.Seal(masterKey, plaintext);
|
||||
|
||||
// Tamper with the encrypted DEK.
|
||||
|
||||
byte[] tamperedDek = (byte[])envelope.EncryptedDek.Clone();
|
||||
tamperedDek[10] ^= 0xFF;
|
||||
EncryptedEnvelope tampered = new(tamperedDek, envelope.EncryptedData);
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Action act = () => EnvelopeEncryption.Open(masterKey, tampered);
|
||||
act.Should().Throw<System.Security.Cryptography.CryptographicException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Open_WithTamperedEncryptedData_Fails()
|
||||
{
|
||||
// Arrange — Tamper with the encrypted data payload.
|
||||
|
||||
byte[] masterKey = AesGcmEncryptor.GenerateKey();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes("tamper-detection");
|
||||
|
||||
EncryptedEnvelope envelope = EnvelopeEncryption.Seal(masterKey, plaintext);
|
||||
|
||||
byte[] tamperedData = (byte[])envelope.EncryptedData.Clone();
|
||||
tamperedData[15] ^= 0xFF;
|
||||
EncryptedEnvelope tampered = new(envelope.EncryptedDek, tamperedData);
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Action act = () => EnvelopeEncryption.Open(masterKey, tampered);
|
||||
act.Should().Throw<System.Security.Cryptography.CryptographicException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReWrap_ChangesEncryptedDekButPreservesData()
|
||||
{
|
||||
// Arrange — Re-wrapping is used during master key rotation. The secret
|
||||
// data stays the same but the DEK is re-encrypted with the new MEK.
|
||||
|
||||
byte[] oldMek = AesGcmEncryptor.GenerateKey();
|
||||
byte[] newMek = AesGcmEncryptor.GenerateKey();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes("rewrap-test-secret");
|
||||
|
||||
EncryptedEnvelope original = EnvelopeEncryption.Seal(oldMek, plaintext);
|
||||
|
||||
// Act — Re-wrap: decrypt the DEK with old MEK, re-encrypt with new MEK.
|
||||
|
||||
EncryptedEnvelope rewrapped = EnvelopeEncryption.ReWrap(oldMek, newMek, original);
|
||||
|
||||
// Assert — The rewrapped envelope opens with the new MEK.
|
||||
|
||||
byte[] recovered = EnvelopeEncryption.Open(newMek, rewrapped);
|
||||
recovered.Should().BeEquivalentTo(plaintext);
|
||||
|
||||
// Assert — The old MEK no longer works.
|
||||
|
||||
Action act = () => EnvelopeEncryption.Open(oldMek, rewrapped);
|
||||
act.Should().Throw<System.Security.Cryptography.CryptographicException>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user