using System.Security.Cryptography; using System.Text; using Omika.Core.Security; namespace Omika.Tests.Security; /// /// Tests du déchiffreur du mot de passe SMTP standard (SMTP.sMdpStandard), /// algorithme SHA-256 + AES-256-CBC-PKCS7 + IV aléatoire préfixé + Base64. /// /// Les tests de roundtrip utilisent une clé de test explicite (indépendante des /// constantes de production) via le constructeur à 3 paramètres. Ils prouvent la /// cohérence C# du format mais NE remplacent PAS un vecteur réellement généré par /// WinDev (voir note TEST 8). /// public class SmtpPasswordStandardDecryptorTests { private const string Key1 = "K1"; private const string Key2 = "K2"; private const string Key3 = "K3"; private static SmtpPasswordStandardDecryptor Decryptor( string k1 = Key1, string k2 = Key2, string k3 = Key3) => new(k1, k2, k3); // ===== TEST 1 : vide → erreur métier (contrat : SmtpStandardPasswordError.Missing) ===== [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Decrypt_Vide_LeveMissing(string? valeur) { SmtpStandardPasswordException ex = Assert.Throws( () => Decryptor().Decrypt(valeur!)); Assert.Equal(SmtpStandardPasswordError.Missing, ex.Error); Assert.Equal("SMTP_STANDARD_PASSWORD_MISSING", ex.LogCode); } // ===== TEST 2 : Base64 invalide → erreur contrôlée ===== [Theory] [InlineData("%%%%")] [InlineData("!!!!")] public void Decrypt_Base64Invalide_LeveInvalidBase64(string valeur) { SmtpStandardPasswordException ex = Assert.Throws( () => Decryptor().Decrypt(valeur)); Assert.Equal(SmtpStandardPasswordError.InvalidBase64, ex.Error); Assert.Equal("SMTP_STANDARD_PASSWORD_INVALID_BASE64", ex.LogCode); } // ===== TEST 3 : payload < 17 octets → erreur contrôlée ===== [Fact] public void Decrypt_PayloadTropCourt_LeveInvalidPayload() { string base64 = Convert.ToBase64String(new byte[16]); SmtpStandardPasswordException ex = Assert.Throws( () => Decryptor().Decrypt(base64)); Assert.Equal(SmtpStandardPasswordError.InvalidPayload, ex.Error); Assert.Equal("SMTP_STANDARD_PASSWORD_INVALID_PAYLOAD", ex.LogCode); } // ===== TEST 4 : ciphertext non multiple de 16 → erreur contrôlée ===== [Fact] public void Decrypt_CiphertextNonMultipleDe16_LeveInvalidPayload() { // IV (16 octets) + 15 octets de ciphertext = 31 octets. string base64 = Convert.ToBase64String(new byte[31]); SmtpStandardPasswordException ex = Assert.Throws( () => Decryptor().Decrypt(base64)); Assert.Equal(SmtpStandardPasswordError.InvalidPayload, ex.Error); } // ===== TEST 5 : clé SHA-256 = 32 octets ===== [Fact] public void DeriveAesKey_Retourne32Octets() { byte[] key = SmtpPasswordStandardDecryptor.DeriveAesKey(Key1, Key2, Key3); Assert.Equal(32, key.Length); } // ===== TEST 6 : roundtrip UTF-8 avec accents ===== [Fact] public void Decrypt_RoundtripAccents_RetourneClair() { const string clair = "MotDePasse-éà-2026!"; string chiffre = EncryptStandard(clair, Key1, Key2, Key3); Assert.Equal(clair, Decryptor().Decrypt(chiffre)); } // ===== TEST 7 : mauvaise clé → decrypt fail ===== [Fact] public void Decrypt_MauvaiseCle_LeveDecryptFailed() { string chiffre = EncryptStandard("secret", Key1, Key2, Key3); SmtpStandardPasswordException ex = Assert.Throws( () => Decryptor("X1", "X2", "X3").Decrypt(chiffre)); Assert.Equal(SmtpStandardPasswordError.DecryptFailed, ex.Error); Assert.Equal("SMTP_STANDARD_PASSWORD_DECRYPT_FAILED", ex.LogCode); } // ===== TEST 8 : vecteur mot de passe de test (format WinDev) ===== // NOTE HONNÊTETÉ : ce vecteur est généré par l'implémentation C# du même // algorithme (roundtrip), PAS par WinDev. La compatibilité WinDev→C# réelle // reste PENDING tant qu'aucune valeur sMdpStandard migrée n'est fournie. [Fact] public void Decrypt_VecteurMotDePasseTest_RetourneAttendu() { const string attendu = "Omika-Test-SMTP-2026!"; string chiffre = EncryptStandard(attendu, Key1, Key2, Key3); Assert.Equal(attendu, Decryptor().Decrypt(chiffre)); } // ===== VECTEUR RÉEL WINDEV (source de vérité) ===== // Valeur réellement produite par WinDev via CrypteMdpSMTPStandard pour le // plaintext volontairement non sensible "Omika-Test-SMTP-2026!". Ce test // appelle le decryptor DE PRODUCTION (clé = SmtpStandardCryptoConstants, // ordre cMdp1 + cMdp3 + cMdp2) sans AUCUN chiffrement C# : la donnée d'entrée // est exclusivement la valeur retournée par WinDev. [Fact] public void WinDevStandardVector_ShouldDecryptCorrectly() { const string vecteurWinDev = "BjzyEIJ1aTVO2/ss4/Y2UOrgzt153SBaeIrpaqmJMXSn1C40A/c0FNOBrU8kkYOE"; const string attendu = "Omika-Test-SMTP-2026!"; var decryptor = new SmtpPasswordStandardDecryptor(); // clé de production string resultat = decryptor.Decrypt(vecteurWinDev); Assert.Equal(attendu, resultat); } // ===== Helper : chiffrement C# équivalent à CrypteMdpSMTPStandard ===== private static string EncryptStandard(string plaintext, string cMdp1, string cMdp2, string cMdp3) { byte[] key = SmtpPasswordStandardDecryptor.DeriveAesKey(cMdp1, cMdp2, cMdp3); using var aes = Aes.Create(); aes.KeySize = 256; aes.BlockSize = 128; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = key; aes.GenerateIV(); // IV aléatoire, inclus en tête du payload (comportement WinDev). using ICryptoTransform encryptor = aes.CreateEncryptor(); byte[] clear = Encoding.UTF8.GetBytes(plaintext); byte[] ciphertext = encryptor.TransformFinalBlock(clear, 0, clear.Length); byte[] payload = new byte[aes.IV.Length + ciphertext.Length]; Buffer.BlockCopy(aes.IV, 0, payload, 0, aes.IV.Length); Buffer.BlockCopy(ciphertext, 0, payload, aes.IV.Length, ciphertext.Length); return Convert.ToBase64String(payload); } }