feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Security;
|
||||
using ClawdDotNet.Core.Tests.Infrastructure;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Security;
|
||||
@@ -9,9 +10,29 @@ 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.
|
||||
///
|
||||
/// Mit der Linux-Portierung kam <c>enc:v2</c> dazu (AES-GCM statt DPAPI). Die Tests
|
||||
/// laufen deshalb gegen einen eigenen Schlüssel in einem Wegwerfverzeichnis — sonst
|
||||
/// würden sie den echten Benutzerschlüssel anlegen oder lesen.
|
||||
/// </summary>
|
||||
public sealed class SecretProtectorTests
|
||||
[Collection(SecretKeyCollection.Name)]
|
||||
public sealed class SecretProtectorTests : IDisposable
|
||||
{
|
||||
private readonly string _keyDir;
|
||||
|
||||
public SecretProtectorTests()
|
||||
{
|
||||
_keyDir = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
SecretKeyStore.UseDirectory(_keyDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SecretKeyStore.UseDirectory(null);
|
||||
try { Directory.Delete(_keyDir, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_verschluesselter_Wert_laesst_sich_wieder_lesen()
|
||||
{
|
||||
@@ -75,6 +96,9 @@ public sealed class SecretProtectorTests
|
||||
// Klartext auszugeben würde einen unbrauchbaren Schlüssel an die API schicken.
|
||||
Should.Throw<SecretProtectionException>(
|
||||
() => SecretProtector.Unprotect("enc:v1:das-ist-kein-gueltiger-block"));
|
||||
|
||||
Should.Throw<SecretProtectionException>(
|
||||
() => SecretProtector.Unprotect("enc:v2:das-ist-kein-gueltiger-block"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -84,10 +108,120 @@ public sealed class SecretProtectorTests
|
||||
|
||||
SecretProtector.Unprotect(SecretProtector.Protect(secret)).ShouldBe(secret);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Plattformübergreifendes Format (enc:v2)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Neue_Werte_werden_im_plattformuebergreifenden_Format_geschrieben()
|
||||
{
|
||||
// Der Kern der Portierung: Unter Linux gab die vorige Fassung hier den Klartext
|
||||
// zurück. Auf einem Server, der gesichert wird, war das schlechter als nichts.
|
||||
var value = SecretProtector.Protect("sk-or-v1-geheim");
|
||||
|
||||
value.ShouldStartWith("enc:v2:");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dieselbe_Eingabe_ergibt_zweimal_verschiedene_Ausgaben()
|
||||
{
|
||||
// AES-GCM mit zufälligem Nonce: Aus gleichen Werten dürfen keine gleichen
|
||||
// Blöcke werden, sonst verrät die Konfigurationsdatei, wo dasselbe Passwort
|
||||
// mehrfach benutzt wird.
|
||||
var a = SecretProtector.Protect("dasselbe-passwort");
|
||||
var b = SecretProtector.Protect("dasselbe-passwort");
|
||||
|
||||
a.ShouldNotBe(b);
|
||||
SecretProtector.Unprotect(a).ShouldBe("dasselbe-passwort");
|
||||
SecretProtector.Unprotect(b).ShouldBe("dasselbe-passwort");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_veraenderter_Block_wird_erkannt_und_nicht_entschluesselt()
|
||||
{
|
||||
// Der Zweck der Authentifizierung in AES-GCM: Ein manipulierter Block darf
|
||||
// keinen halb geratenen Klartext ergeben.
|
||||
var value = SecretProtector.Protect("sk-or-v1-geheim")!;
|
||||
var payload = Convert.FromBase64String(value["enc:v2:".Length..]);
|
||||
payload[^1] ^= 0xFF;
|
||||
var tampered = "enc:v2:" + Convert.ToBase64String(payload);
|
||||
|
||||
Should.Throw<SecretProtectionException>(() => SecretProtector.Unprotect(tampered));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_fremder_Schluessel_kann_den_Wert_nicht_lesen()
|
||||
{
|
||||
// Genau die Eigenschaft, die den Schutz ausmacht: Die Konfigurationsdatei allein
|
||||
// nützt auf einem anderen Rechner nichts, weil der Schlüssel nicht mitreist.
|
||||
var value = SecretProtector.Protect("sk-or-v1-geheim");
|
||||
|
||||
var fremd = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
{
|
||||
SecretKeyStore.UseDirectory(fremd);
|
||||
Should.Throw<SecretProtectionException>(() => SecretProtector.Unprotect(value));
|
||||
}
|
||||
finally
|
||||
{
|
||||
SecretKeyStore.UseDirectory(_keyDir);
|
||||
try { Directory.Delete(fremd, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Der_Schluessel_wird_einmal_angelegt_und_dann_wiederverwendet()
|
||||
{
|
||||
var first = SecretProtector.Protect("geheim");
|
||||
var keyBytes = File.ReadAllBytes(SecretKeyStore.KeyFilePath);
|
||||
|
||||
var second = SecretProtector.Protect("noch-geheimer");
|
||||
|
||||
File.ReadAllBytes(SecretKeyStore.KeyFilePath).ShouldBe(keyBytes);
|
||||
SecretProtector.Unprotect(first).ShouldBe("geheim");
|
||||
SecretProtector.Unprotect(second).ShouldBe("noch-geheimer");
|
||||
}
|
||||
|
||||
[LinuxFact]
|
||||
public void Die_Schluesseldatei_ist_nur_fuer_den_Besitzer_lesbar()
|
||||
{
|
||||
SecretProtector.Protect("geheim");
|
||||
|
||||
File.GetUnixFileMode(SecretKeyStore.KeyFilePath)
|
||||
.ShouldBe(UnixFileMode.UserRead | UnixFileMode.UserWrite);
|
||||
}
|
||||
|
||||
[LinuxFact]
|
||||
public void Ein_DPAPI_Wert_aus_einer_Windows_Instanz_wird_mit_Begruendung_abgewiesen()
|
||||
{
|
||||
// Der Umzugsfall. Ihn als Klartext durchzureichen würde einen unbrauchbaren
|
||||
// Schlüssel an die API schicken — die Meldung muss sagen, was zu tun ist.
|
||||
var ex = Should.Throw<SecretProtectionException>(
|
||||
() => SecretProtector.Unprotect("enc:v1:" + Convert.ToBase64String([1, 2, 3, 4])));
|
||||
|
||||
ex.Message.ShouldContain("neu eingetragen");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ConfigSecretsTests
|
||||
[Collection(SecretKeyCollection.Name)]
|
||||
public sealed class ConfigSecretsTests : IDisposable
|
||||
{
|
||||
private readonly string _keyDir;
|
||||
|
||||
public ConfigSecretsTests()
|
||||
{
|
||||
_keyDir = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
|
||||
SecretKeyStore.UseDirectory(_keyDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SecretKeyStore.UseDirectory(null);
|
||||
try { Directory.Delete(_keyDir, recursive: true); }
|
||||
catch { /* Aufräumen ist Nebensache */ }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("password", true)]
|
||||
[InlineData("apiKey", true)]
|
||||
|
||||
Reference in New Issue
Block a user