using System.Text; using EntKube.Secrets.Crypto; using FluentAssertions; namespace EntKube.Secrets.Tests.Crypto; /// /// 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 /// 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(); } [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(); } [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(); } [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(); } }