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:
2026-06-26 14:27:56 +02:00
parent 07fa8d6020
commit b1e0bd9548
5 changed files with 57 additions and 11 deletions

View File

@@ -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
}
}
}