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