Files
Omika/tests/Omika.Tests/Security/SmtpPasswordStandardDecryptorTests.cs
T
2026-10-23 18:46:50 +02:00

178 lines
6.4 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Omika.Core.Security;
namespace Omika.Tests.Security;
/// <summary>
/// Tests du déchiffreur du mot de passe SMTP standard (<c>SMTP.sMdpStandard</c>),
/// 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).
/// </summary>
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<SmtpStandardPasswordException>(
() => 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<SmtpStandardPasswordException>(
() => 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<SmtpStandardPasswordException>(
() => 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<SmtpStandardPasswordException>(
() => 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<SmtpStandardPasswordException>(
() => 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);
}
}