using System; using LicenseLabrador.Client; using PolyTrader.Core.Security; using PolyTraderSharp.Models; using PolyTraderSharp.Services; namespace PolyTraderSharp.Licensing { /// /// Lizenz-Torwächter für den Programmstart (B.4). Prüft beim Start die LicenseLabrador-Lizenz und /// entscheidet, ob PolyTrader voll (mit allen Modulen) oder eingeschränkt (nur Core-Shell, damit /// Terminal und Einstellungen zum Eintragen einer Lizenz erreichbar bleiben) startet. /// /// **Grundsatz:** kein harter Abbruch (Environment.Exit) bei ungültiger Lizenz. Ein /// Trading-Bot mit offenen Positionen darf nicht mitten im Lauf hart abgeschossen werden – im /// Zweifel startet die Shell ohne die Trading-/Analyse-Module weiter. Der Gate wirft nie; jeder /// Fehler in der Prüfung führt zum eingeschränkten Modus, nicht zum Absturz. /// /// Die eigentliche Kryptografie (Ed25519-Signaturprüfung, Nonce, Offline-Kulanz, Hardware-Bindung) /// liegt im des SDK; dieser Gate ist nur die Anwendungslogik drumherum. /// public static class LicenseGate { /// /// Öffentlicher Ed25519-Schlüssel des Lizenzservers (license.mhdf.de), Base64. Bewusst fest /// eingebettet: nur mit diesem Schlüssel lässt sich eine Server-Antwort verifizieren – ein /// Angreifer kann ohne den (nur serverseitigen) privaten Schlüssel kein „valid" fälschen. /// private const string PublicKeyBase64 = "L7YR1wMKk8+lNefatzL+DMvAtHFVkZWYXAxXGrro+/U="; /// Produkt-Slug im LicenseLabrador-Admin. private const string ProductSlug = "pt"; /// /// Lizenzserver-Endpunkt. Ausschließlich HTTPS – über http:// gingen Lizenzschlüssel und /// Hardware-ID im Klartext übers Netz (die signierte Antwort bliebe zwar fälschungssicher, /// der Schlüssel würde aber geleakt). /// private static readonly string[] Endpoints = { "https://license.mhdf.de" }; /// Offline-Kulanz, falls der Server das Feld nicht liefert (7 Tage). private const int OfflineGraceHoursFallback = 168; public static LicenseConfig BuildConfig() => new LicenseConfig { ProductSlug = ProductSlug, PublicKeyBase64 = PublicKeyBase64, Endpoints = Endpoints, OfflineGraceHoursFallback = OfflineGraceHoursFallback }; /// /// Führt die Startprüfung durch. Rückgabe true = App darf voll (mit Modulen) starten. /// Zeigt bei ungültiger Lizenz einen modalen Dialog (Schlüssel eingeben, Hardware-ID anzeigen, /// eingeschränkt starten). Wirft nie. /// public static bool RunStartupGate(ServerSettings settings, TerminalLogger logger, string settingsPath) { LicenseClient client; try { client = new LicenseClient(BuildConfig()); } catch (Exception ex) { logger.Error($"❌ Lizenzprüfung nicht initialisierbar: {ex.Message}. Start im eingeschränkten Modus."); return false; } string storedKey = ""; try { // Erlaubt einen mit dem Master-Key verschlüsselten Schlüssel (enc:v1:…); Klartext // wird unverändert durchgereicht. storedKey = SecretProtection.Unprotect(settings.LicenseKey); } catch (Exception ex) { logger.Warning($"⚠️ Gespeicherter Lizenzschlüssel nicht lesbar ({ex.Message}) – behandle als 'keine Lizenz'."); } LicenseResult result = EvaluateBlocking(client, storedKey); if (result.IsUsable) { LogUsable(result, logger); return true; } logger.Warning($"⚠️ Keine gültige Lizenz: {Describe(result)}. Öffne Lizenzdialog."); LicenseResult? dialogResult; string? dialogKey; try { using var dlg = new PolyTraderSharp.Ui.LicenseDialog(client, storedKey, result); dlg.ShowDialog(); dialogResult = dlg.ValidatedResult; dialogKey = dlg.ValidatedKey; } catch (Exception ex) { logger.Error($"❌ Lizenzdialog fehlgeschlagen: {ex.Message}. Start im eingeschränkten Modus."); return false; } if (dialogResult != null && dialogResult.IsUsable && !string.IsNullOrWhiteSpace(dialogKey)) { try { // Bei gesetztem Master-Key verschlüsselt speichern; sonst Klartext (Passthrough). settings.LicenseKey = SecretProtection.Protect(dialogKey!.Trim()); settings.Save(settingsPath); logger.Info("🔑 Lizenz validiert und gespeichert."); } catch (Exception ex) { logger.Error($"Lizenz gültig, aber Speichern des Schlüssels fehlgeschlagen: {ex.Message}."); } LogUsable(dialogResult, logger); return true; } logger.Warning( "⚠️ Start im EINGESCHRÄNKTEN Modus – keine Module aktiv, nur Terminal und Einstellungen. " + "Lizenz in den Einstellungen setzen und PolyTrader neu starten."); return false; } /// /// Validiert synchron (blockierend). Das SDK arbeitet durchgängig mit ConfigureAwait(false), /// daher ist das Blockieren vor der Message-Loop unbedenklich. Ein leerer Schlüssel ergibt /// planmäßig . /// internal static LicenseResult EvaluateBlocking(LicenseClient client, string? key) { return client.ValidateAsync(key ?? string.Empty).GetAwaiter().GetResult(); } private static void LogUsable(LicenseResult result, TerminalLogger logger) { switch (result.State) { case LicenseState.Valid: logger.Info("🔑 Lizenz gültig (online geprüft)."); break; case LicenseState.ValidOffline: logger.Warning($"🔑 Lizenz im Offline-Kulanzmodus gültig bis {result.GraceUntil:g} " + "(Server nicht erreichbar)."); break; case LicenseState.ValidLocalFile: logger.Info("🔑 Lizenz über lokale Offline-Lizenzdatei gültig."); break; default: logger.Info($"🔑 Lizenz nutzbar ({result.State})."); break; } } /// Kurzbeschreibung eines nicht nutzbaren Ergebnisses für das Log/den Dialog. internal static string Describe(LicenseResult result) { return result.State switch { LicenseState.NoLicense => "kein oder ungültiger Schlüssel / Server nicht erreichbar", LicenseState.NotFound => "Schlüssel dem Server unbekannt", LicenseState.Revoked => "Lizenz wurde widerrufen", LicenseState.Expired => "Lizenz abgelaufen", LicenseState.ActivationLimit => "maximale Aktivierungen erreicht", LicenseState.TamperSuspected => "Manipulation erkannt (Signatur/Uhr) – Prüfung verweigert", _ => result.State.ToString() }; } } }