Watchdog-Heartbeat + LicenseLabrador-Lizenzpruefung eingebunden

Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.

Watchdog (Dead-Man's-Switch, externer Server):
- WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch
  gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie.
- Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt
  aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz).
- Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt),
  Lifecycle-Events started/stopping.
- Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter
  Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:).

Lizenz (LicenseLabrador, Ed25519):
- LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die
  Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen)
  startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im
  Lauf hart sterben. TamperSuspected gilt als nicht nutzbar.
- LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit
  Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless.
- Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel
  entschluesselt werden muss; derselbe TerminalLogger wird als Singleton
  weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen.

Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf
..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen.

484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-04 09:04:14 +02:00
co-authored by Claude Opus 5
parent bb103a578a
commit ca750a06f5
11 changed files with 2122 additions and 8 deletions
+157
View File
@@ -0,0 +1,157 @@
namespace PolyTraderSharp.Ui
{
partial class LicenseDialog
{
/// <summary>Erforderliche Designer-Variable.</summary>
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Vom Windows Form-Designer generierter Code
private void InitializeComponent()
{
lblHeadline = new System.Windows.Forms.Label();
lblStatus = new System.Windows.Forms.Label();
lblHwCaption = new System.Windows.Forms.Label();
txtHardwareId = new System.Windows.Forms.TextBox();
btnCopyHwId = new System.Windows.Forms.Button();
lblKeyCaption = new System.Windows.Forms.Label();
txtKey = new System.Windows.Forms.TextBox();
btnValidate = new System.Windows.Forms.Button();
lblResult = new System.Windows.Forms.Label();
btnLimited = new System.Windows.Forms.Button();
SuspendLayout();
//
// lblHeadline
//
lblHeadline.AutoSize = true;
lblHeadline.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
lblHeadline.Location = new System.Drawing.Point(16, 14);
lblHeadline.Name = "lblHeadline";
lblHeadline.Size = new System.Drawing.Size(316, 17);
lblHeadline.TabIndex = 0;
lblHeadline.Text = "Für PolyTrader wird eine gültige Lizenz benötigt.";
//
// lblStatus
//
lblStatus.Location = new System.Drawing.Point(16, 40);
lblStatus.Name = "lblStatus";
lblStatus.Size = new System.Drawing.Size(504, 34);
lblStatus.TabIndex = 1;
lblStatus.Text = "Status:";
//
// lblHwCaption
//
lblHwCaption.AutoSize = true;
lblHwCaption.Location = new System.Drawing.Point(16, 82);
lblHwCaption.Name = "lblHwCaption";
lblHwCaption.Size = new System.Drawing.Size(214, 15);
lblHwCaption.TabIndex = 2;
lblHwCaption.Text = "Hardware-ID dieses Rechners (für die Aktivierung):";
//
// txtHardwareId
//
txtHardwareId.Location = new System.Drawing.Point(16, 100);
txtHardwareId.Name = "txtHardwareId";
txtHardwareId.ReadOnly = true;
txtHardwareId.Size = new System.Drawing.Size(400, 23);
txtHardwareId.TabIndex = 3;
//
// btnCopyHwId
//
btnCopyHwId.Location = new System.Drawing.Point(424, 99);
btnCopyHwId.Name = "btnCopyHwId";
btnCopyHwId.Size = new System.Drawing.Size(96, 25);
btnCopyHwId.TabIndex = 4;
btnCopyHwId.Text = "Kopieren";
btnCopyHwId.UseVisualStyleBackColor = true;
//
// lblKeyCaption
//
lblKeyCaption.AutoSize = true;
lblKeyCaption.Location = new System.Drawing.Point(16, 136);
lblKeyCaption.Name = "lblKeyCaption";
lblKeyCaption.Size = new System.Drawing.Size(96, 15);
lblKeyCaption.TabIndex = 5;
lblKeyCaption.Text = "Lizenzschlüssel:";
//
// txtKey
//
txtKey.CharacterCasing = System.Windows.Forms.CharacterCasing.Upper;
txtKey.Location = new System.Drawing.Point(16, 154);
txtKey.Name = "txtKey";
txtKey.Size = new System.Drawing.Size(400, 23);
txtKey.TabIndex = 6;
//
// btnValidate
//
btnValidate.Location = new System.Drawing.Point(424, 153);
btnValidate.Name = "btnValidate";
btnValidate.Size = new System.Drawing.Size(96, 25);
btnValidate.TabIndex = 7;
btnValidate.Text = "Validieren";
btnValidate.UseVisualStyleBackColor = true;
//
// lblResult
//
lblResult.Location = new System.Drawing.Point(16, 190);
lblResult.Name = "lblResult";
lblResult.Size = new System.Drawing.Size(504, 40);
lblResult.TabIndex = 8;
//
// btnLimited
//
btnLimited.Location = new System.Drawing.Point(16, 236);
btnLimited.Name = "btnLimited";
btnLimited.Size = new System.Drawing.Size(230, 27);
btnLimited.TabIndex = 9;
btnLimited.Text = "Ohne Lizenz starten (eingeschränkt)";
btnLimited.UseVisualStyleBackColor = true;
//
// LicenseDialog
//
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(536, 278);
Controls.Add(btnLimited);
Controls.Add(lblResult);
Controls.Add(btnValidate);
Controls.Add(txtKey);
Controls.Add(lblKeyCaption);
Controls.Add(btnCopyHwId);
Controls.Add(txtHardwareId);
Controls.Add(lblHwCaption);
Controls.Add(lblStatus);
Controls.Add(lblHeadline);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
Name = "LicenseDialog";
StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
Text = "PolyTrader Lizenz";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Label lblHeadline;
private System.Windows.Forms.Label lblStatus;
private System.Windows.Forms.Label lblHwCaption;
private System.Windows.Forms.TextBox txtHardwareId;
private System.Windows.Forms.Button btnCopyHwId;
private System.Windows.Forms.Label lblKeyCaption;
private System.Windows.Forms.TextBox txtKey;
private System.Windows.Forms.Button btnValidate;
private System.Windows.Forms.Label lblResult;
private System.Windows.Forms.Button btnLimited;
}
}
+129
View File
@@ -0,0 +1,129 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using LicenseLabrador.Client;
using PolyTraderSharp.Licensing;
namespace PolyTraderSharp.Ui
{
/// <summary>
/// Modaler Lizenzdialog beim Programmstart, wenn keine gültige Lizenz vorliegt. Zeigt die
/// Hardware-ID (kopierbar, für die Aktivierung im Lizenz-Admin), nimmt einen Lizenzschlüssel
/// entgegen und validiert ihn gegen den Lizenzserver. Bei Erfolg werden <see cref="ValidatedKey"/>
/// und <see cref="ValidatedResult"/> gesetzt; der Aufrufer (LicenseGate) speichert den Schlüssel.
/// Ohne gültige Lizenz kann der Nutzer eingeschränkt (nur Core-Shell) weiterstarten.
/// </summary>
public partial class LicenseDialog : Form
{
private readonly LicenseClient _client;
/// <summary>Nur gesetzt, wenn im Dialog erfolgreich validiert wurde.</summary>
public LicenseResult? ValidatedResult { get; private set; }
/// <summary>Der erfolgreich validierte Schlüssel (für die verschlüsselte Ablage).</summary>
public string? ValidatedKey { get; private set; }
/// <param name="startupContext">
/// true = Aufruf beim Programmstart (keine gültige Lizenz): der Abbruch-Button bietet
/// „eingeschränkt starten". false = Aufruf aus den Einstellungen (App läuft bereits): der
/// Button heißt „Schließen".
/// </param>
public LicenseDialog(LicenseClient client, string prefillKey, LicenseResult? initialResult, bool startupContext = true)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
InitializeComponent();
txtHardwareId.Text = SafeHardwareId(client);
txtKey.Text = prefillKey ?? string.Empty;
if (initialResult != null)
{
lblStatus.Text = "Status: " + LicenseGate.Describe(initialResult) +
(string.IsNullOrEmpty(initialResult.Message) ? string.Empty : $" {initialResult.Message}");
}
else
{
lblStatus.Text = "Status: noch nicht geprüft bitte „Validieren“ klicken.";
}
if (startupContext)
{
lblHeadline.Text = "Für PolyTrader wird eine gültige Lizenz benötigt.";
btnLimited.Text = "Ohne Lizenz starten (eingeschränkt)";
}
else
{
lblHeadline.Text = "Lizenz prüfen / hinterlegen";
btnLimited.Text = "Schließen";
}
btnValidate.Click += (_, _) => Validate();
btnCopyHwId.Click += (_, _) => CopyHardwareId();
btnLimited.Click += (_, _) => { DialogResult = DialogResult.Cancel; Close(); };
AcceptButton = btnValidate;
}
private static string SafeHardwareId(LicenseClient client)
{
try { return client.GetHardwareId(); }
catch { return "(nicht ermittelbar)"; }
}
private void Validate()
{
string key = txtKey.Text.Trim();
if (string.IsNullOrWhiteSpace(key))
{
lblResult.ForeColor = Color.Firebrick;
lblResult.Text = "Bitte einen Lizenzschlüssel eingeben.";
return;
}
btnValidate.Enabled = false;
Cursor = Cursors.WaitCursor;
lblResult.ForeColor = SystemColors.ControlText;
lblResult.Text = "Prüfe Lizenz …";
lblResult.Refresh();
try
{
// Blockierend: das SDK nutzt durchgängig ConfigureAwait(false) → kein Deadlock auf dem
// UI-Thread; der kurze Freeze (max. HTTP-Timeout) ist im modalen Startdialog vertretbar
// und vermeidet jede Abhängigkeit vom WinForms-SynchronizationContext vor Application.Run.
LicenseResult result = _client.ValidateAsync(key).GetAwaiter().GetResult();
if (result.IsUsable)
{
ValidatedResult = result;
ValidatedKey = key;
lblResult.ForeColor = Color.Green;
lblResult.Text = "Lizenz gültig PolyTrader startet.";
DialogResult = DialogResult.OK;
Close();
}
else
{
lblResult.ForeColor = Color.Firebrick;
string extra = string.IsNullOrEmpty(result.Message) ? string.Empty : $" ({result.Message})";
lblResult.Text = "Nicht gültig: " + LicenseGate.Describe(result) + extra;
}
}
catch (Exception ex)
{
lblResult.ForeColor = Color.Firebrick;
lblResult.Text = "Fehler bei der Prüfung: " + ex.Message;
}
finally
{
Cursor = Cursors.Default;
btnValidate.Enabled = true;
}
}
private void CopyHardwareId()
{
try { Clipboard.SetText(txtHardwareId.Text); }
catch { /* Zwischenablage kann kurzzeitig belegt sein kein harter Fehler */ }
}
}
}
+31 -1
View File
@@ -26,6 +26,9 @@ namespace PolyTraderSharp.Ui.Views
btn_loadsettings = new ToolStripButton();
btnGenMasterKey = new ToolStripButton();
btnSetOpenRouterKey = new ToolStripButton();
btnSetWatchdogToken = new ToolStripButton();
btnTestWatchdog = new ToolStripButton();
btnSetLicenseKey = new ToolStripButton();
tabPage2 = new TabPage();
pgAccount = new PropertyGrid();
dgvAccounts = new DataGridView();
@@ -74,7 +77,7 @@ namespace PolyTraderSharp.Ui.Views
// toolStrip2
//
toolStrip2.ImageScalingSize = new Size(24, 24);
toolStrip2.Items.AddRange(new ToolStripItem[] { btn_save, btn_loadsettings, btnGenMasterKey, btnSetOpenRouterKey });
toolStrip2.Items.AddRange(new ToolStripItem[] { btn_save, btn_loadsettings, btnGenMasterKey, btnSetOpenRouterKey, btnSetWatchdogToken, btnTestWatchdog, btnSetLicenseKey });
toolStrip2.Location = new Point(3, 3);
toolStrip2.Name = "toolStrip2";
toolStrip2.Size = new Size(1171, 34);
@@ -112,6 +115,30 @@ namespace PolyTraderSharp.Ui.Views
btnSetOpenRouterKey.Size = new Size(220, 29);
btnSetOpenRouterKey.Text = "OpenRouter-Key setzen …";
btnSetOpenRouterKey.ToolTipText = "Speichert den OpenRouter-API-Key für den Supervisor (gitignorierte Datei openrouter.key).";
//
// btnSetWatchdogToken
//
btnSetWatchdogToken.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnSetWatchdogToken.Name = "btnSetWatchdogToken";
btnSetWatchdogToken.Size = new Size(220, 29);
btnSetWatchdogToken.Text = "Watchdog-Token setzen …";
btnSetWatchdogToken.ToolTipText = "Speichert den Watchdog-Agent-Token (maskierte Eingabe, bei gesetztem Master-Key verschlüsselt).";
//
// btnTestWatchdog
//
btnTestWatchdog.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnTestWatchdog.Name = "btnTestWatchdog";
btnTestWatchdog.Size = new Size(210, 29);
btnTestWatchdog.Text = "Test-Heartbeat senden";
btnTestWatchdog.ToolTipText = "Sendet sofort einen Heartbeat an den konfigurierten Watchdog-Server und meldet das Ergebnis.";
//
// btnSetLicenseKey
//
btnSetLicenseKey.DisplayStyle = ToolStripItemDisplayStyle.Text;
btnSetLicenseKey.Name = "btnSetLicenseKey";
btnSetLicenseKey.Size = new Size(210, 29);
btnSetLicenseKey.Text = "Lizenz prüfen / setzen …";
btnSetLicenseKey.ToolTipText = "Öffnet den Lizenzdialog (Hardware-ID + Server-Validierung). Gültiger Schlüssel wird gespeichert (bei Master-Key verschlüsselt).";
//
// tabPage2
//
@@ -207,6 +234,9 @@ namespace PolyTraderSharp.Ui.Views
private ToolStripButton btn_loadsettings;
private ToolStripButton btnGenMasterKey;
private ToolStripButton btnSetOpenRouterKey;
private ToolStripButton btnSetWatchdogToken;
private ToolStripButton btnTestWatchdog;
private ToolStripButton btnSetLicenseKey;
private ToolStrip toolStripAccounts;
private ToolStripButton btnAccNew;
private ToolStripButton btnAccDelete;
+161 -1
View File
@@ -3,8 +3,10 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Windows.Forms;
using PolyTrader.Core.Persistence;
using PolyTrader.Core.Security;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
@@ -23,6 +25,7 @@ namespace PolyTraderSharp.Ui.Views
private ServerSettings _settings = new();
private ThreemaService? _threema;
private MullvadVpnService? _vpn;
private WatchdogHeartbeatService? _watchdog;
private TerminalLogger? _logger;
private IAccountRepository? _accountRepo;
@@ -37,6 +40,9 @@ namespace PolyTraderSharp.Ui.Views
btn_loadsettings.Click += (_, _) => Reload();
btnGenMasterKey.Click += (_, _) => GenerateMasterKey();
btnSetOpenRouterKey.Click += (_, _) => SetOpenRouterKey();
btnSetWatchdogToken.Click += (_, _) => SetWatchdogToken();
btnTestWatchdog.Click += async (_, _) => await TestWatchdogAsync();
btnSetLicenseKey.Click += (_, _) => SetLicenseKey();
UpdateMasterKeyButtonState();
btnAccNew.Click += (_, _) => AddAccount();
@@ -52,13 +58,14 @@ namespace PolyTraderSharp.Ui.Views
}
public void Initialize(ThreemaService threema, MullvadVpnService vpn, TerminalLogger logger,
IAccountRepository accountRepo, TradingState state)
IAccountRepository accountRepo, TradingState state, WatchdogHeartbeatService? watchdog = null)
{
_threema = threema;
_vpn = vpn;
_logger = logger;
_accountRepo = accountRepo;
_state = state;
_watchdog = watchdog;
Reload();
LoadAccounts();
@@ -79,6 +86,7 @@ namespace PolyTraderSharp.Ui.Views
_settings.Save(SettingsPath);
_threema?.ReloadSettings();
_vpn?.ReloadSettings();
_watchdog?.ReloadSettings();
_logger?.Info("Server-Einstellungen gespeichert und Services neu geladen.");
MessageBox.Show("Server-Einstellungen gespeichert.", "Erfolg",
MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -135,6 +143,158 @@ namespace PolyTraderSharp.Ui.Views
dgvAccounts.Refresh();
}
// ===== Watchdog =====
/// <summary>
/// Setzt/entfernt den Watchdog-Agent-Token über eine maskierte Eingabe damit das Secret
/// nicht offen im PropertyGrid steht und die server_settings.xml nicht per Hand bearbeitet
/// werden muss. Ist ein Master-Key gesetzt, wird der Token verschlüsselt abgelegt.
/// Wird sofort gespeichert, damit der Test-Button und der laufende Dienst ihn direkt nutzen.
/// </summary>
private void SetWatchdogToken()
{
bool exists = !string.IsNullOrWhiteSpace(_settings.WatchdogToken);
string? token = PromptForSecret("Watchdog Agent-Token",
"Agent-Token aus dem Watchdog-Admin eingeben (Header X-Watchdog-Key).\n" +
"Wird in der gitignorierten server_settings.xml gespeichert (leer = entfernen).\n" +
(exists ? $"Aktuell: {_settings.WatchdogTokenStatus}." : "Aktuell ist KEIN Token gesetzt."));
if (token == null) return; // Abbruch
try
{
if (string.IsNullOrWhiteSpace(token))
{
_settings.WatchdogToken = "";
_settings.Save(SettingsPath);
_watchdog?.ReloadSettings();
propertyGrid.Refresh();
_logger?.Info("Watchdog-Agent-Token entfernt.");
MessageBox.Show("Watchdog-Token entfernt.", "Watchdog",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// Protect() gibt ohne Master-Key den Klartext unverändert zurück kein stiller
// Sicherheitsverlust, der Nutzer wird darauf hingewiesen.
_settings.WatchdogToken = SecretProtection.Protect(token.Trim());
_settings.Save(SettingsPath);
_watchdog?.ReloadSettings();
propertyGrid.Refresh();
bool encrypted = SecretProtection.IsEncrypted(_settings.WatchdogToken);
_logger?.Info($"Watchdog-Agent-Token gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
MessageBox.Show(
"Watchdog-Token gespeichert und sofort aktiv.\n\n" +
(encrypted
? "Der Token liegt mit dem Master-Key verschlüsselt in server_settings.xml."
: "Hinweis: Es ist kein Master-Key gesetzt der Token liegt im Klartext in der " +
"(gitignorierten) server_settings.xml. Mit „Master-Key erzeugen\" kannst du das ändern."),
"Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Fehler beim Speichern des Watchdog-Tokens: {ex.Message}",
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// Sendet einen Heartbeat mit den GESPEICHERTEN Einstellungen und meldet das Ergebnis.
/// Ungespeicherte Änderungen im PropertyGrid wirken bewusst nicht sonst würde ein
/// erfolgreicher Test eine Konfiguration bestätigen, die so nicht auf der Platte liegt.
/// </summary>
private async Task TestWatchdogAsync()
{
if (_watchdog == null)
{
MessageBox.Show("Der Watchdog-Dienst ist nicht verfügbar.", "Watchdog",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
btnTestWatchdog.Enabled = false;
try
{
var result = await _watchdog.SendHeartbeatAsync(
"ok", "Test-Heartbeat aus dem PolyTrader-Settings-Fenster");
if (result.Success)
{
_logger?.Info($"✅ Watchdog-Test-Heartbeat erfolgreich gesendet ({result.Detail}).");
MessageBox.Show(
$"Test-Heartbeat erfolgreich gesendet ({result.Detail}).\n\n" +
"Der Monitor sollte im Watchdog-Dashboard jetzt auf 'up' stehen.",
"Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
_logger?.Warning($"⚠️ Watchdog-Test-Heartbeat fehlgeschlagen: {result.Detail}");
MessageBox.Show(
$"Test-Heartbeat fehlgeschlagen:\n\n{result.Detail}\n\n" +
"Bitte URL, Agent-Token und Source prüfen und die Einstellungen vorher speichern.",
"Watchdog", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
finally
{
btnTestWatchdog.Enabled = true;
}
}
// ===== Lizenz (LicenseLabrador) =====
/// <summary>
/// Öffnet denselben Lizenzdialog wie der Programmstart (Hardware-ID + Server-Validierung),
/// nur im Verwalten-Modus. Ein erfolgreich validierter Schlüssel wird bei gesetztem
/// Master-Key verschlüsselt in server_settings.xml gespeichert und gilt ab dem nächsten Start.
/// </summary>
private void SetLicenseKey()
{
string current = string.Empty;
try { current = SecretProtection.Unprotect(_settings.LicenseKey); }
catch { /* nicht lesbar → als leer behandeln */ }
LicenseLabrador.Client.LicenseClient client;
try
{
client = new LicenseLabrador.Client.LicenseClient(PolyTraderSharp.Licensing.LicenseGate.BuildConfig());
}
catch (Exception ex)
{
MessageBox.Show($"Lizenzprüfung nicht verfügbar: {ex.Message}", "Lizenz",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
using var dlg = new LicenseDialog(client, current, initialResult: null, startupContext: false);
dlg.ShowDialog(this);
if (dlg.ValidatedResult == null || !dlg.ValidatedResult.IsUsable || string.IsNullOrWhiteSpace(dlg.ValidatedKey))
return; // abgebrochen oder nicht validiert nichts speichern
try
{
_settings.LicenseKey = SecretProtection.Protect(dlg.ValidatedKey!.Trim());
_settings.Save(SettingsPath);
propertyGrid.Refresh();
bool encrypted = SecretProtection.IsEncrypted(_settings.LicenseKey);
_logger?.Info($"Lizenz validiert und gespeichert ({(encrypted ? "verschlüsselt" : "Klartext")}).");
MessageBox.Show(
"Lizenz validiert und gespeichert.\n\n" +
(encrypted
? "Der Schlüssel liegt mit dem Master-Key verschlüsselt in server_settings.xml."
: "Hinweis: Es ist kein Master-Key gesetzt der Schlüssel liegt im Klartext in der " +
"(gitignorierten) server_settings.xml."),
"Lizenz", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Fehler beim Speichern des Lizenzschlüssels: {ex.Message}",
"Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// ===== Master-Key (at-rest-Verschlüsselung, F1) =====
/// <summary>Pfad der Master-Key-Datei identisch zu Program.cs (App-Ordner, gitignored).</summary>