using System.Text.Json; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Security; using Shouldly; namespace ClawdDotNet.Core.Tests.Security; /// /// S7 aus der Bestandsaufnahme: OpenRouter-Schlüssel, Datenbank-Verbindungszeichenfolgen /// samt Passwort, Mail-Zugangsdaten und das Telegram-2FA-Passwort lagen im Klartext in /// den JSON-Dateien. Wer die Dateien lesen konnte, hatte alle Zugänge. /// public sealed class SecretProtectorTests { [Fact] public void Ein_verschluesselter_Wert_laesst_sich_wieder_lesen() { const string secret = "sk-or-v1-sehr-geheim-12345"; var protectedValue = SecretProtector.Protect(secret); SecretProtector.Unprotect(protectedValue).ShouldBe(secret); } [Fact] public void Der_verschluesselte_Wert_enthaelt_den_Klartext_nicht() { const string secret = "sk-or-v1-sehr-geheim-12345"; var protectedValue = SecretProtector.Protect(secret); protectedValue.ShouldNotContain(secret); protectedValue.ShouldNotBe(secret); } [Fact] public void Verschluesselte_Werte_sind_als_solche_erkennbar() { SecretProtector.IsProtected(SecretProtector.Protect("geheim")).ShouldBeTrue(); SecretProtector.IsProtected("klartext").ShouldBeFalse(); } [Fact] public void Mehrfaches_Verschluesseln_veraendert_nichts() { // Wichtig, weil beim Speichern nicht bekannt ist, ob ein Wert schon // verschlüsselt war. var once = SecretProtector.Protect("geheim"); var twice = SecretProtector.Protect(once); twice.ShouldBe(once); SecretProtector.Unprotect(twice).ShouldBe("geheim"); } [Fact] public void Klartext_aus_alten_Konfigurationen_wird_unveraendert_gelesen() { // Bestehende Installationen müssen ohne Zutun weiterlaufen. SecretProtector.Unprotect("sk-or-alter-klartext").ShouldBe("sk-or-alter-klartext"); } [Theory] [InlineData(null)] [InlineData("")] public void Leere_Werte_bleiben_leer(string? value) { SecretProtector.Protect(value).ShouldBe(value); SecretProtector.Unprotect(value).ShouldBe(value); } [Fact] public void Ein_beschaedigter_Wert_wird_gemeldet_statt_stillschweigend_durchgereicht() { // Nach Benutzer- oder Rechnerwechsel ist der Wert hier nicht lesbar. Ihn als // Klartext auszugeben würde einen unbrauchbaren Schlüssel an die API schicken. Should.Throw( () => SecretProtector.Unprotect("enc:v1:das-ist-kein-gueltiger-block")); } [Fact] public void Umlaute_und_Sonderzeichen_ueberstehen_den_Vorgang() { const string secret = "P@ssw0rt mit Grüßen & Ümlauten 🦀"; SecretProtector.Unprotect(SecretProtector.Protect(secret)).ShouldBe(secret); } } public sealed class ConfigSecretsTests { [Theory] [InlineData("password", true)] [InlineData("apiKey", true)] [InlineData("xApiKey", true)] [InlineData("connectionString", true)] [InlineData("password2FA", true)] [InlineData("APIKEY", true)] [InlineData("allowedTables", false)] [InlineData("smtpHost", false)] [InlineData("rootPath", false)] public void Nur_bekannte_Geheimnisfelder_werden_behandelt(string key, bool expected) { ConfigSecrets.IsSecretKey(key).ShouldBe(expected); } private static AgentConfig AgentWithSecrets() => new() { AgentId = "test", Tools = { ["Database"] = new Dictionary { ["type"] = "mysql", ["connectionString"] = "Server=x;Database=y;User=z;Password=geheim;", ["allowedTables"] = "prices" }, ["Mail"] = new Dictionary { ["smtpHost"] = "smtp.example.com", ["password"] = "mail-passwort" } } }; [Fact] public void Geheimnisse_einer_Agentenkonfiguration_werden_verschluesselt() { var agent = AgentWithSecrets(); ConfigSecrets.Protect(agent); agent.Tools["Database"]["connectionString"]!.ToString().ShouldNotContain("geheim"); agent.Tools["Mail"]["password"]!.ToString().ShouldNotContain("mail-passwort"); } [Fact] public void Nicht_geheime_Felder_bleiben_lesbar() { var agent = AgentWithSecrets(); ConfigSecrets.Protect(agent); agent.Tools["Database"]["type"].ShouldBe("mysql"); agent.Tools["Mail"]["smtpHost"].ShouldBe("smtp.example.com"); } [Fact] public void Der_Rundlauf_stellt_die_Originalwerte_wieder_her() { var agent = AgentWithSecrets(); ConfigSecrets.Protect(agent); ConfigSecrets.Unprotect(agent); agent.Tools["Database"]["connectionString"] .ShouldBe("Server=x;Database=y;User=z;Password=geheim;"); agent.Tools["Mail"]["password"].ShouldBe("mail-passwort"); } [Fact] public void Werte_aus_deserialisiertem_JSON_werden_ebenfalls_erfasst() { // Nach dem Deserialisieren stecken die Werte als JsonElement im Wörterbuch, // nicht als String. var json = """ { "agentId": "test", "tools": { "Mail": { "smtpHost": "smtp.example.com", "password": "geheim123" } } } """; var agent = JsonSerializer.Deserialize(json)!; ConfigSecrets.Protect(agent); agent.Tools["Mail"]["password"]!.ToString().ShouldNotContain("geheim123"); ConfigSecrets.Unprotect(agent); agent.Tools["Mail"]["password"].ShouldBe("geheim123"); } [Fact] public void Der_Instanzschluessel_und_Telegram_Zugangsdaten_werden_erfasst() { var config = new InstanceConfig { OpenRouterApiKey = "sk-or-v1-geheim", TelegramClient = new TelegramClientConfig { ApiHash = "hash-geheim", Password2FA = "2fa-geheim" } }; ConfigSecrets.Protect(config); config.OpenRouterApiKey.ShouldNotContain("sk-or-v1-geheim"); config.TelegramClient.ApiHash.ShouldNotContain("hash-geheim"); config.TelegramClient.Password2FA.ShouldNotContain("2fa-geheim"); ConfigSecrets.Unprotect(config); config.OpenRouterApiKey.ShouldBe("sk-or-v1-geheim"); config.TelegramClient.Password2FA.ShouldBe("2fa-geheim"); } [Fact] public void Eine_Konfiguration_ohne_Telegram_stoert_nicht() { var config = new InstanceConfig { OpenRouterApiKey = "sk-test", TelegramClient = null }; Should.NotThrow(() => ConfigSecrets.Protect(config)); } [Fact] public void Leere_Geheimnisfelder_bleiben_leer() { var agent = new AgentConfig { AgentId = "test", Tools = { ["Mail"] = new Dictionary { ["password"] = "" } } }; ConfigSecrets.Protect(agent); agent.Tools["Mail"]["password"].ShouldBe(""); } }