@
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> @
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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(); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user