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("");
}
}