using System;
using PolyTrader.Core.Security;
using Xunit;
namespace PolyTrader.Tests
{
///
/// Sicherheitsnetz für die at-rest-Verschlüsselung sensibler Felder (F1): Round-Trip, Alt-Klartext-
/// Passthrough, Manipulations-/Falscher-Key-Erkennung (AES-GCM), Verhalten ohne Master-Key.
/// Geldkritisch – ein Fehler hier macht Wallet-Keys unlesbar.
///
public class SecretProtectionTests : IDisposable
{
// 32-Byte-Testschlüssel als Base64.
private const string KeyA = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="; // 0..31
private const string KeyB = "/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v4=";
public SecretProtectionTests() => SecretProtection.Reset();
public void Dispose() => SecretProtection.Reset();
[Fact]
public void Roundtrip_encrypts_and_decrypts()
{
SecretProtection.Configure(KeyA);
const string secret = "0x1234567890abcdef_private_key";
string enc = SecretProtection.Protect(secret);
Assert.StartsWith(SecretProtection.Prefix, enc);
Assert.DoesNotContain(secret, enc); // Klartext nicht sichtbar
Assert.Equal(secret, SecretProtection.Unprotect(enc));
}
[Fact]
public void Protect_uses_fresh_nonce_each_time()
{
SecretProtection.Configure(KeyA);
Assert.NotEqual(SecretProtection.Protect("same"), SecretProtection.Protect("same"));
}
[Fact]
public void Empty_stays_empty()
{
SecretProtection.Configure(KeyA);
Assert.Equal("", SecretProtection.Protect(""));
Assert.Equal("", SecretProtection.Unprotect(""));
}
[Fact]
public void Legacy_plaintext_passes_through_on_read()
{
SecretProtection.Configure(KeyA);
Assert.Equal("legacy-plain", SecretProtection.Unprotect("legacy-plain")); // kein Präfix
}
[Fact]
public void Double_protect_does_not_wrap_twice()
{
SecretProtection.Configure(KeyA);
string once = SecretProtection.Protect("x");
Assert.Equal(once, SecretProtection.Protect(once)); // schon verschlüsselt -> unverändert
}
[Fact]
public void Without_master_key_plaintext_passthrough()
{
// nicht konfiguriert
Assert.False(SecretProtection.IsConfigured);
Assert.Equal("plain", SecretProtection.Protect("plain")); // kein Zwang zu Klartextverlust, aber Passthrough
Assert.Equal("plain", SecretProtection.Unprotect("plain"));
}
[Fact]
public void Without_master_key_encrypted_value_throws()
{
SecretProtection.Configure(KeyA);
string enc = SecretProtection.Protect("secret");
SecretProtection.Reset(); // Key entfernt
Assert.Throws(() => SecretProtection.Unprotect(enc));
}
[Fact]
public void Wrong_key_throws()
{
SecretProtection.Configure(KeyA);
string enc = SecretProtection.Protect("secret");
SecretProtection.Configure(KeyB);
Assert.Throws(() => SecretProtection.Unprotect(enc));
}
[Fact]
public void Tampered_ciphertext_throws()
{
SecretProtection.Configure(KeyA);
string enc = SecretProtection.Protect("secret");
// letztes Base64-Zeichen kippen
char last = enc[^1];
string tampered = enc.Substring(0, enc.Length - 1) + (last == 'A' ? 'B' : 'A');
Assert.Throws(() => SecretProtection.Unprotect(tampered));
}
[Theory]
[InlineData("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=")] // Base64 32 Byte
[InlineData("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")] // Hex 64
[InlineData("eine-beliebige-passphrase")] // abgeleitet via SHA-256
public void Various_key_formats_roundtrip(string key)
{
SecretProtection.Configure(key);
string enc = SecretProtection.Protect("value");
Assert.Equal("value", SecretProtection.Unprotect(enc));
}
[Fact]
public void IsEncrypted_detects_prefix()
{
SecretProtection.Configure(KeyA);
Assert.True(SecretProtection.IsEncrypted(SecretProtection.Protect("x")));
Assert.False(SecretProtection.IsEncrypted("plain"));
Assert.False(SecretProtection.IsEncrypted(""));
}
}
}