178 lines
6.4 KiB
C#
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);
|
|
}
|
|
}
|