Files
Omika/tools/Omika.SmtpRealProbe/Program.cs
T
2026-10-23 18:46:50 +02:00

363 lines
14 KiB
C#

using System.Data.OleDb;
using System.Text.Json;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using Omika.Core.Security;
// ============================================================
// OMIKA SMTP PROBE — READ-ONLY DB + (optionnel) SMTP connect/auth/send
// Modes :
// --audit [--db OMIKA_TEST|BATIK_TEST]
// Scan de TOUTES les configs EMAIL_ENVOI% : classification + structure
// + déchiffrement (composant DE PRODUCTION SmtpPasswordStandardDecryptor).
// Ne modifie aucune donnée.
// <PU_Code> <PU_IDUtilisateur> [--db ...] [--send]
// Audit d'une configuration unique + SMTP connect/auth (+envoi si --send).
// Ne JAMAIS afficher sMdp / sMdpStandard / plaintext / login / clé.
// ============================================================
const string server = "127.0.0.1";
const int port = 4900;
const string user = "susana";
const string pass = "123";
// ProtectionStd (ParametreUtilisateur) par base de test.
var dbMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["OMIKA_TEST"] = "A1BAF17C65BC565F2927BF1A85DAFEED",
["BATIK_TEST"] = "E2FC3072E6145E1B20DA7321755CD607",
};
bool audit = args.Contains("--audit");
int dbIdx = Array.IndexOf(args, "--db");
string db = dbIdx >= 0 && dbIdx + 1 < args.Length ? args[dbIdx + 1] : "OMIKA_TEST";
if (!dbMap.TryGetValue(db, out string? stdPwd))
{
Console.Error.WriteLine($"ERROR: unknown database '{db}' (known: OMIKA_TEST, BATIK_TEST).");
return 2;
}
string connStr =
$"Provider=PCSOFT.HFSQL;Data Source={server}:{port};Initial Catalog={db};" +
$"User ID={user};Password={pass};" +
$"Extended Properties=\"Password=ParametreUtilisateur:{stdPwd}\"";
var decryptor = new SmtpPasswordStandardDecryptor();
if (audit)
{
return RunAudit(connStr, db, decryptor);
}
// --- Mode config unique ---
string code = args.Where(a => !a.StartsWith("--")).ElementAtOrDefault(0) ?? "EMAIL_ENVOI_5";
string idUserText = args.Where(a => !a.StartsWith("--")).ElementAtOrDefault(1) ?? "7";
bool send = args.Contains("--send");
if (code.Any(c => !(char.IsLetterOrDigit(c) || c == '_')) || !long.TryParse(idUserText, out long idUser))
{
Console.Error.WriteLine("USAGE: Omika.SmtpRealProbe <PU_Code> <PU_IDUtilisateur> [--db DB] [--send]");
return 2;
}
string? json = null;
try
{
using var conn = new OleDbConnection(connStr);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = $"SELECT PU_Valeur FROM ParametreUtilisateur WHERE PU_Code = '{code}' AND PU_IDUtilisateur = {idUser}";
json = cmd.ExecuteScalar() as string;
}
catch (Exception ex)
{
Console.WriteLine($"SMTP CONFIG FOUND = NO (DB read failed: {ex.GetType().Name})");
return 3;
}
Console.WriteLine("SMTP CONFIG FOUND = " + (string.IsNullOrWhiteSpace(json) ? "NO" : "YES"));
Console.WriteLine("SOURCE TABLE = ParametreUtilisateur");
Console.WriteLine($"PU_CODE = {code}");
Console.WriteLine($"PU_IDUTILISATEUR = {idUser}");
if (string.IsNullOrWhiteSpace(json)) return 4;
var cfg = ExtractFirst(json);
if (cfg is null) { Console.WriteLine("JSON PARSE = FAILED"); return 5; }
Console.WriteLine("sMdp PRESENT = " + (string.IsNullOrWhiteSpace(cfg.SMdp) ? "NO" : "YES"));
Console.WriteLine("sMdpStandard PRESENT = " + (string.IsNullOrWhiteSpace(cfg.SMdpStandard) ? "NO" : "YES"));
Console.WriteLine("sMdpStandard EMPTY = " + (string.IsNullOrWhiteSpace(cfg.SMdpStandard) ? "YES" : "NO"));
Console.WriteLine("SMTP HOST = " + (string.IsNullOrWhiteSpace(cfg.Host) ? "(absent)" : cfg.Host));
Console.WriteLine("SMTP PORT = " + (string.IsNullOrWhiteSpace(cfg.Port) ? "(absent)" : cfg.Port));
Console.WriteLine("SMTP SECURITY MODE = " + cfg.Cryptage switch { 1 => "AUCUN", 2 => "TLS/STARTTLS", 3 => "SSL", _ => $"INCONNU({cfg.Cryptage})" });
if (string.IsNullOrWhiteSpace(cfg.SMdpStandard))
{
Console.WriteLine("REAL STANDARD PASSWORD DECRYPT = NOT TESTED (sMdpStandard absent)");
return 6;
}
return TestSingle(decryptor, cfg, send);
// ============================================================
static int RunAudit(string connStr, string db, SmtpPasswordStandardDecryptor decryptor)
{
Console.WriteLine("DATABASE = " + db);
Console.WriteLine("IS OMIKA DATABASE = " + (db.Equals("OMIKA_TEST", StringComparison.OrdinalIgnoreCase) ? "YES" : "NO"));
var rows = new List<(string Code, string IdUser, string? Valeur)>();
try
{
using var conn = new OleDbConnection(connStr);
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT PU_Code, PU_IDUtilisateur, PU_Valeur FROM ParametreUtilisateur WHERE PU_Code LIKE 'EMAIL_ENVOI%'";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
string c = (reader.GetValue(0)?.ToString() ?? "").Trim();
string i = (reader.GetValue(1)?.ToString() ?? "").Trim();
string? v = reader.IsDBNull(2) ? null : reader.GetValue(2)?.ToString();
rows.Add((c, i, v));
}
}
catch (Exception ex)
{
Console.WriteLine("QUERY FAILED = " + ex.GetType().Name);
return 3;
}
int emailEnvoi = 0, emailEnvoiSoc = 0, emailEnvoiPerso = 0;
int invalidJson = 0, legacyBinary = 0, emptyValue = 0;
int withMdp = 0, withStd = 0, withoutStd = 0, emptyStd = 0;
int base64Valid = 0, base64Invalid = 0;
int decryptPass = 0, decryptFail = 0, plaintextEmpty = 0;
foreach (var row in rows)
{
if (row.Code == "EMAIL_ENVOI") emailEnvoi++;
else if (System.Text.RegularExpressions.Regex.IsMatch(row.Code, @"^EMAIL_ENVOI_\d+$")) emailEnvoiSoc++;
else if (row.Code.Contains("Perso")) emailEnvoiPerso++;
if (string.IsNullOrWhiteSpace(row.Valeur)) { emptyValue++; continue; }
var configs = ExtractAll(row.Valeur);
if (configs is null)
{
// Non-JSON : legacy/binaire ou JSON invalide.
bool looksBinary = row.Valeur.Any(c => c < 32 && c != '\t' && c != '\n' && c != '\r');
bool startsJson = row.Valeur.TrimStart().StartsWith('{') || row.Valeur.TrimStart().StartsWith('[');
if (startsJson && !looksBinary) invalidJson++;
else legacyBinary++;
continue;
}
foreach (var cfg in configs)
{
if (!string.IsNullOrWhiteSpace(cfg.SMdp)) withMdp++;
if (!string.IsNullOrWhiteSpace(cfg.SMdpStandard))
{
withStd++;
byte[]? payload = TryBase64(cfg.SMdpStandard);
if (payload is null) base64Invalid++;
else
{
base64Valid++;
try
{
string pt = decryptor.Decrypt(cfg.SMdpStandard);
decryptPass++;
if (string.IsNullOrEmpty(pt)) plaintextEmpty++;
}
catch (SmtpStandardPasswordException) { decryptFail++; }
}
}
else
{
withoutStd++;
if (cfg.SMdpStandard is not null) emptyStd++; // champ présent mais vide
}
}
}
Console.WriteLine("TOTAL EMAIL CONFIGS = " + rows.Count);
Console.WriteLine("EMAIL_ENVOI = " + emailEnvoi);
Console.WriteLine("EMAIL_ENVOI_<idSoc> = " + emailEnvoiSoc);
Console.WriteLine("EMAIL_ENVOI_Perso_N = " + emailEnvoiPerso);
Console.WriteLine("EMPTY VALUE = " + emptyValue);
Console.WriteLine("INVALID JSON = " + invalidJson);
Console.WriteLine("LEGACY/BINARY = " + legacyBinary);
Console.WriteLine("CONFIGS WITH sMdp = " + withMdp);
Console.WriteLine("CONFIGS WITH sMdpStandard = " + withStd);
Console.WriteLine("CONFIGS WITHOUT sMdpStandard= " + withoutStd);
Console.WriteLine("CONFIGS WITH EMPTY sMdpStandard = " + emptyStd);
Console.WriteLine("STANDARD BASE64 VALID = " + (base64Valid > 0 ? (base64Invalid > 0 ? "PARTIAL" : "YES") : "NOT AVAILABLE"));
Console.WriteLine("REAL OMIKA CONFIGS TESTED = " + (base64Valid + base64Invalid));
Console.WriteLine("DECRYPT PASS = " + decryptPass);
Console.WriteLine("DECRYPT FAIL = " + decryptFail);
Console.WriteLine("PLAINTEXT EMPTY = " + plaintextEmpty);
Console.WriteLine("LEGACY sMdp USED BY OMIKA = NO (audit read-only, sMdp non déchiffré)");
return 0;
}
static int TestSingle(SmtpPasswordStandardDecryptor decryptor, SmtpConfig cfg, bool send)
{
byte[] payload;
try { payload = Convert.FromBase64String(cfg.SMdpStandard!.Trim()); }
catch { Console.WriteLine("STANDARD BASE64 VALID = NO"); return 7; }
int ctLen = payload.Length - 16;
Console.WriteLine("STANDARD BASE64 VALID = YES");
Console.WriteLine($"PAYLOAD LENGTH = {payload.Length}");
Console.WriteLine("IV LENGTH = 16");
Console.WriteLine($"CIPHERTEXT LENGTH = {ctLen}");
Console.WriteLine("CIPHERTEXT BLOCK VALID = " + (ctLen > 0 && ctLen % 16 == 0 ? "YES" : "NO"));
string plaintext;
try
{
plaintext = decryptor.Decrypt(cfg.SMdpStandard);
Console.WriteLine("REAL STANDARD PASSWORD DECRYPT = PASS");
Console.WriteLine("PLAINTEXT EMPTY = " + (string.IsNullOrEmpty(plaintext) ? "YES" : "NO"));
Console.WriteLine($"PLAINTEXT LENGTH = {plaintext.Length}");
}
catch (SmtpStandardPasswordException ex)
{
Console.WriteLine($"REAL STANDARD PASSWORD DECRYPT = FAIL ({ex.LogCode})");
return 8;
}
Console.WriteLine("LEGACY sMdp USED BY OMIKA = NO");
Console.WriteLine("PASSWORD LOGGED = NO");
if (string.IsNullOrWhiteSpace(cfg.Host)) { Console.WriteLine("SMTP CONNECT = NOT TESTED (host absent)"); return 9; }
int smtpPort = int.TryParse(cfg.Port, out int p) ? p : 25;
SecureSocketOptions secure = cfg.Cryptage switch
{
2 => SecureSocketOptions.StartTls,
>= 3 => SecureSocketOptions.SslOnConnect,
_ => SecureSocketOptions.None
};
try
{
using var client = new SmtpClient();
client.Connect(cfg.Host, smtpPort, secure);
Console.WriteLine("SMTP CONNECT = PASS");
if (!string.IsNullOrWhiteSpace(cfg.Login))
{
try
{
client.Authenticate(cfg.Login, plaintext);
Console.WriteLine("SMTP AUTH = PASS");
Console.WriteLine("PASSWORD SOURCE = sMdpStandard");
if (send)
{
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse(cfg.Login));
message.To.Add(MailboxAddress.Parse(cfg.Login));
message.Subject = "Omika SMTP validation test";
message.Body = new TextPart("plain") { Text = "Test automatique Omika de validation SMTP (sMdpStandard)." };
client.Send(message);
Console.WriteLine("SMTP SEND = PASS");
Console.WriteLine("MESSAGE ACCEPTED BY SMTP SERVER = YES");
}
else Console.WriteLine("SMTP SEND = NOT TESTED (--send non fourni)");
}
catch (AuthenticationException ex)
{
Console.WriteLine("SMTP AUTH = FAIL");
Console.WriteLine("AUTH ERROR TYPE = " + ex.GetType().Name);
Console.WriteLine("AUTH ERROR MESSAGE SANITIZED = " + Sanitize(ex.Message, cfg.Login));
}
}
else Console.WriteLine("SMTP AUTH = NOT TESTED (login absent)");
try { client.Disconnect(true); } catch { }
}
catch (SslHandshakeException ex)
{
Console.WriteLine("SMTP CONNECT = FAIL");
Console.WriteLine("ERROR TYPE = " + ex.GetType().Name);
Console.WriteLine("ERROR MESSAGE SANITIZED = " + Sanitize(ex.Message, cfg.Login));
}
catch (System.Net.Sockets.SocketException ex)
{
Console.WriteLine("SMTP CONNECT = FAIL");
Console.WriteLine("ERROR TYPE = " + ex.GetType().Name);
Console.WriteLine("ERROR MESSAGE SANITIZED = " + Sanitize(ex.Message, cfg.Login));
}
catch (Exception ex)
{
Console.WriteLine("SMTP CONNECT = FAIL");
Console.WriteLine("ERROR TYPE = " + ex.GetType().Name);
Console.WriteLine("ERROR MESSAGE SANITIZED = " + Sanitize(ex.Message, cfg.Login));
}
return 0;
}
static byte[]? TryBase64(string s)
{
try { return Convert.FromBase64String(s.Trim()); }
catch { return null; }
}
static SmtpConfig? ExtractFirst(string json)
{
var all = ExtractAll(json);
return all is { Count: > 0 } ? all[0] : null;
}
static List<SmtpConfig>? ExtractAll(string json)
{
try
{
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
var result = new List<SmtpConfig>();
IEnumerable<JsonElement> elements = root.ValueKind == JsonValueKind.Array
? root.EnumerateArray()
: [root];
foreach (JsonElement element in elements)
{
if (!element.TryGetProperty("SMTP", out JsonElement smtp) || smtp.ValueKind != JsonValueKind.Object)
continue;
var cfg = new SmtpConfig();
if (smtp.TryGetProperty("sLogin", out JsonElement e) && e.ValueKind == JsonValueKind.String) cfg.Login = e.GetString();
if (smtp.TryGetProperty("sMdp", out e) && e.ValueKind == JsonValueKind.String) cfg.SMdp = e.GetString();
if (smtp.TryGetProperty("sMdpStandard", out e) && e.ValueKind == JsonValueKind.String) cfg.SMdpStandard = e.GetString();
if (smtp.TryGetProperty("sAdresseServeur", out e) && e.ValueKind == JsonValueKind.String) cfg.Host = e.GetString();
if (smtp.TryGetProperty("sPortServeur", out e) && e.ValueKind == JsonValueKind.String) cfg.Port = e.GetString();
if (smtp.TryGetProperty("nCryptage", out e) && e.ValueKind == JsonValueKind.Number) cfg.Cryptage = e.GetInt32();
result.Add(cfg);
}
return result;
}
catch (JsonException) { return null; }
}
static string Sanitize(string message, string? login)
{
string s = message ?? string.Empty;
if (!string.IsNullOrWhiteSpace(login)) s = s.Replace(login, "<login>");
s = System.Text.RegularExpressions.Regex.Replace(s, "(?i)(password|pwd|sMdpStandard|sMdp)\\s*[:=]\\s*\\S+", "$1=<redacted>");
return s.Length > 200 ? s[..200] : s;
}
sealed class SmtpConfig
{
public string? Login;
public string? SMdp;
public string? SMdpStandard;
public string? Host;
public string? Port;
public int Cryptage;
}