using System.Text; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Crypto; using ZB.MOM.WW.Secrets.Tests.Fakes; namespace ZB.MOM.WW.Secrets.Tests.Crypto; public class AesGcmEnvelopeCipherTests { [Fact] public void RoundTrips() { var provider = new FakeMasterKeyProvider("k1"); var cipher = new AesGcmEnvelopeCipher(provider); var name = new SecretName("sql/foo"); StoredSecret row = cipher.Encrypt(name, "hunter2", SecretContentType.Text); Assert.Equal("k1", row.KekId); // Ciphertext must not leak the plaintext in the clear. byte[] plaintextBytes = Encoding.UTF8.GetBytes("hunter2"); Assert.False(Contains(row.Ciphertext, plaintextBytes)); string decrypted = cipher.Decrypt(row); Assert.Equal("hunter2", decrypted); } [Fact] public void TamperedCiphertextThrows() { var provider = new FakeMasterKeyProvider("k1"); var cipher = new AesGcmEnvelopeCipher(provider); var name = new SecretName("sql/foo"); StoredSecret row = cipher.Encrypt(name, "hunter2", SecretContentType.Text); row.Ciphertext[0] ^= 0xFF; Assert.Throws(() => cipher.Decrypt(row)); } [Fact] public void AadBindingRejectsRenamedRow() { var provider = new FakeMasterKeyProvider("k1"); var cipher = new AesGcmEnvelopeCipher(provider); StoredSecret row = cipher.Encrypt(new SecretName("sql/foo"), "hunter2", SecretContentType.Text); var renamed = row with { Name = new SecretName("sql/bar") }; Assert.Throws(() => cipher.Decrypt(renamed)); } [Fact] public void WrongKekThrows() { // Both providers report KekId "k1" but hold different random key bytes. var providerA = new FakeMasterKeyProvider("k1"); var providerB = new FakeMasterKeyProvider("k1"); var cipherA = new AesGcmEnvelopeCipher(providerA); var cipherB = new AesGcmEnvelopeCipher(providerB); StoredSecret row = cipherA.Encrypt(new SecretName("sql/foo"), "hunter2", SecretContentType.Text); // Matching KekId string but wrong key material must still fail closed (unwrap tag mismatch). Assert.Throws(() => cipherB.Decrypt(row)); } [Fact] public void UnknownKekIdThrows() { var providerK1 = new FakeMasterKeyProvider("k1"); var providerK2 = new FakeMasterKeyProvider("k2"); var cipherK1 = new AesGcmEnvelopeCipher(providerK1); var cipherK2 = new AesGcmEnvelopeCipher(providerK2); StoredSecret row = cipherK1.Encrypt(new SecretName("sql/foo"), "hunter2", SecretContentType.Text); var ex = Assert.Throws(() => cipherK2.Decrypt(row)); Assert.Contains("unknown KEK", ex.Message, StringComparison.Ordinal); } private static bool Contains(byte[] haystack, byte[] needle) { if (needle.Length == 0 || haystack.Length < needle.Length) { return false; } for (int i = 0; i <= haystack.Length - needle.Length; i++) { bool match = true; for (int j = 0; j < needle.Length; j++) { if (haystack[i + j] != needle[j]) { match = false; break; } } if (match) { return true; } } return false; } }