Security: Agent-Key in config.json per Windows DPAPI verschlüsselt (v2.9.0)
agent_key liegt jetzt nicht mehr im Klartext auf der Platte, sondern via ProtectedData.Protect (DataProtectionScope.LocalMachine) verschlüsselt — nur das SYSTEM-Konto auf genau diesem einen Rechner kann den Wert wieder entschlüsseln. Reines Auslesen von config.json bringt einem lokalen Angreifer/Malware also nichts mehr. Migration automatisch beim ersten Start von v2.9.0: erkennt das alte Klartext-Format, verschlüsselt beim nächsten Save() automatisch — kein manueller Eingriff nötig, läuft über den bestehenden Staged-Rollout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ namespace ITNexusAgent;
|
||||
|
||||
public class AgentWorker
|
||||
{
|
||||
private const string Version = "2.8.0";
|
||||
private const string Version = "2.9.0";
|
||||
private const string DataDir = @"C:\ProgramData\IT Nexus Agent";
|
||||
private const string ConfigPath = @"C:\ProgramData\IT Nexus Agent\config.json";
|
||||
private const string StatusPath = @"C:\ProgramData\IT Nexus Agent\status.json";
|
||||
@@ -44,10 +44,11 @@ public class AgentWorker
|
||||
// Idempotent (Server liefert bestehenden Key erneut) — daher bei jedem Start sicher aufrufbar.
|
||||
var hostname = SystemInfoService.GetHostname();
|
||||
var enrolledKey = await _api.EnrollAsync(hostname);
|
||||
if (!string.IsNullOrEmpty(enrolledKey) && enrolledKey != _config.AgentKey)
|
||||
var configWasUnencrypted = !File.ReadAllText(ConfigPath).Contains("dpapi:");
|
||||
if (!string.IsNullOrEmpty(enrolledKey) && (enrolledKey != _config.AgentKey || configWasUnencrypted))
|
||||
{
|
||||
_config.AgentKey = enrolledKey;
|
||||
_config.Save(ConfigPath);
|
||||
_config.Save(ConfigPath); // schreibt agent_key jetzt DPAPI-verschlüsselt statt im Klartext
|
||||
_api.UpdateKey(enrolledKey);
|
||||
Log("ENROLL: Per-Device-Key erhalten und gespeichert");
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyName>IT-Nexus-Agent</AssemblyName>
|
||||
<RootNamespace>ITNexusAgent</RootNamespace>
|
||||
<Version>2.8.0</Version>
|
||||
<AssemblyVersion>2.8.0.0</AssemblyVersion>
|
||||
<Version>2.9.0</Version>
|
||||
<AssemblyVersion>2.9.0.0</AssemblyVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
@@ -31,6 +31,7 @@
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,25 +1,69 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ITNexusAgent.Models;
|
||||
|
||||
public class AgentConfig
|
||||
{
|
||||
[JsonProperty("server_url")]
|
||||
private const string ProtectedPrefix = "dpapi:";
|
||||
|
||||
[JsonIgnore]
|
||||
public string ServerUrl { get; set; } = "";
|
||||
|
||||
[JsonProperty("agent_key")]
|
||||
// Im Speicher immer Klartext — nur auf der Platte (config.json) liegt der verschlüsselte Wert.
|
||||
[JsonIgnore]
|
||||
public string AgentKey { get; set; } = "";
|
||||
|
||||
private class RawConfig
|
||||
{
|
||||
[JsonProperty("server_url")] public string ServerUrl { get; set; } = "";
|
||||
[JsonProperty("agent_key")] public string AgentKey { get; set; } = "";
|
||||
}
|
||||
|
||||
public static AgentConfig Load(string path)
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonConvert.DeserializeObject<AgentConfig>(json)
|
||||
var raw = JsonConvert.DeserializeObject<RawConfig>(json)
|
||||
?? throw new Exception("Ungültige config.json");
|
||||
|
||||
return new AgentConfig
|
||||
{
|
||||
ServerUrl = raw.ServerUrl,
|
||||
AgentKey = Unprotect(raw.AgentKey),
|
||||
};
|
||||
}
|
||||
|
||||
// Verschlüsselt den Key per Windows DPAPI (LocalMachine-Scope) bevor er auf die Platte geschrieben
|
||||
// wird — ein Klartext-Auslesen von config.json bringt einem Angreifer dann nichts mehr, da der Wert
|
||||
// nur vom SYSTEM-Konto auf genau diesem Rechner wieder entschlüsselt werden kann.
|
||||
public void Save(string path)
|
||||
{
|
||||
File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented));
|
||||
var raw = new RawConfig { ServerUrl = ServerUrl, AgentKey = Protect(AgentKey) };
|
||||
File.WriteAllText(path, JsonConvert.SerializeObject(raw, Formatting.Indented));
|
||||
}
|
||||
|
||||
private static string Protect(string plaintext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(plaintext)) return plaintext;
|
||||
var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(plaintext), null, DataProtectionScope.LocalMachine);
|
||||
return ProtectedPrefix + Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
// Erkennt das alte Klartext-Format (z.B. frisch aus dem Installer-Template) und lässt es unverändert
|
||||
// durch — wird beim nächsten Save() automatisch verschlüsselt persistiert.
|
||||
private static string Unprotect(string stored)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stored) || !stored.StartsWith(ProtectedPrefix)) return stored;
|
||||
try
|
||||
{
|
||||
var bytes = ProtectedData.Unprotect(Convert.FromBase64String(stored[ProtectedPrefix.Length..]), null, DataProtectionScope.LocalMachine);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return stored; // Korrupt/falsche Maschine → unverändert zurückgeben statt Crash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#define MyAppName "IT Nexus Agent"
|
||||
#define MyAppVersion "2.8.0"
|
||||
#define MyAppVersion "2.9.0"
|
||||
#define MyAppPublisher "Cereda Systems GmbH"
|
||||
#define MyAppURL "https://it-nexus.cereda-systems.de"
|
||||
#define MyAppExeName "IT-Nexus-Agent.exe"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const ANN_TYPES = { maintenance: { icon: '🔧', label: 'Wartung', color: '#f59e0b' }, warning: { icon: '⚠️', label: 'Warnung', color: '#ef4444' }, info: { icon: 'ℹ️', label: 'Info', color: '#6366f1' } };
|
||||
|
||||
const LATEST_AGENT_VERSION = '2.8.0';
|
||||
const LATEST_AGENT_VERSION = '2.9.0';
|
||||
const SEVERITY_LABELS = { critical: 'Kritisch', important: 'Wichtig', moderate: 'Moderat', low: 'Niedrig', all: 'Alle' };
|
||||
const SEVERITY_COLORS = { critical: '#EF4444', important: '#F59E0B', moderate: '#3B82F6', low: '#6B7280', all: '#0D9488' };
|
||||
const COMMAND_LABELS = { check_updates: '🔍 Update-Scan', install_updates: '⬇️ Installation', reboot: '🔄 Neustart', upgrade_win11: '🪟 Win 11 Upgrade', update_agent: '⬆️ Agent-Update' };
|
||||
|
||||
Reference in New Issue
Block a user