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> @
87 lines
2.6 KiB
C#
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(); }
|
|
}
|
|
}
|