Files
Richard 9effc43206 @
R6: Security - Master-Key + AES-256-GCM at-rest + TLS-Warnung

- Core/Security/SecretProtection: AES-256-GCM at-rest, Master-Key aus env IBKRTRADER_MASTER_KEY
  bzw. gitignorierte master.key; selbstheilendes enc:v1:-Format; Passthrough ohne Key (mit Warnung)
- Core/Security/EncryptedStringConverter (EF-ValueConverter, bereit fuer kuenftige Credentials)
- Program: ConfigureSecretProtection (Master-Key laden) + WarnIfDbTlsNotEnforced (SslMode) beim Start
- master.key gitignored
- Tests: SecretProtection (Round-Trip/Idempotenz/Passthrough/Tamper/Key-Fehler) -> 56/56 gruen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-28 19:53:21 +02:00

87 lines
2.6 KiB
C#

using FluentAssertions;
using IBKRTrader.Core.Security;
namespace IBKRTrader.Tests.Security;
/// <summary>
/// Testet die at-rest-Verschlüsselung. Nutzt den statischen SecretProtection-Zustand → nicht
/// parallelisierbar mit anderen Tests dieser Collection; Reset() im Finally hält es isoliert.
/// </summary>
[Trait("cat", "unit")]
[Collection("SecretProtection")]
public class SecretProtectionTests
{
// 32-Byte-Key als Base64.
private const string Key = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";
[Fact]
public void RoundTrip_EncryptsAndDecrypts()
{
try
{
SecretProtection.Configure(Key);
var enc = SecretProtection.Protect("geheim123");
enc.Should().StartWith("enc:v1:");
enc.Should().NotContain("geheim123");
SecretProtection.Unprotect(enc).Should().Be("geheim123");
}
finally { SecretProtection.Reset(); }
}
[Fact]
public void Protect_IsIdempotent_ForAlreadyEncrypted()
{
try
{
SecretProtection.Configure(Key);
var enc = SecretProtection.Protect("x");
var enc2 = SecretProtection.Protect(enc); // nicht doppelt verschlüsseln
enc2.Should().Be(enc);
}
finally { SecretProtection.Reset(); }
}
[Fact]
public void NoKey_IsPassthrough_Plaintext()
{
SecretProtection.Reset();
SecretProtection.Protect("klartext").Should().Be("klartext");
SecretProtection.Unprotect("klartext").Should().Be("klartext"); // Alt-Klartext unverändert
}
[Fact]
public void Unprotect_Encrypted_WithoutKey_Throws()
{
string enc;
try
{
SecretProtection.Configure(Key);
enc = SecretProtection.Protect("secret");
}
finally { SecretProtection.Reset(); }
var act = () => SecretProtection.Unprotect(enc);
act.Should().Throw<InvalidOperationException>();
}
[Fact]
public void Unprotect_TamperedData_Throws()
{
try
{
SecretProtection.Configure(Key);
var enc = SecretProtection.Protect("secret");
// Erstes Base64-Zeichen des Payloads kippen → Nonce/Ciphertext korrupt, Base64 bleibt gültig.
var payload = enc[SecretProtection.Prefix.Length..].ToCharArray();
payload[0] = payload[0] == 'A' ? 'B' : 'A';
var tampered = SecretProtection.Prefix + new string(payload);
var act = () => SecretProtection.Unprotect(tampered);
act.Should().Throw<InvalidOperationException>();
}
finally { SecretProtection.Reset(); }
}
}