using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.ConfigurationDatabase; namespace ScadaLink.ConfigurationDatabase.Tests; /// /// Regression guard for ConfigurationDatabase-004: secret-bearing columns /// (SMTP credentials, external-system auth config, database connection strings) /// must be encrypted at rest, not persisted verbatim. /// public class SecretEncryptionTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly IDataProtectionProvider _protectionProvider; public SecretEncryptionTests() { _protectionProvider = new EphemeralDataProtectionProvider(); var options = new DbContextOptionsBuilder() .UseSqlite("DataSource=:memory:") .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)) .Options; _context = new ScadaLinkDbContext(options, _protectionProvider); _context.Database.OpenConnection(); _context.Database.EnsureCreated(); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task DatabaseConnectionDefinition_ConnectionString_StoredEncrypted_RoundTrips() { const string secret = "Server=db;Database=X;User Id=svc;Password=SuperSecret123!"; var def = new DatabaseConnectionDefinition("PrimaryDb", secret); _context.DatabaseConnectionDefinitions.Add(def); await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); // Raw column value must not be the plaintext secret. var raw = await ReadRawColumnAsync("DatabaseConnectionDefinitions", "ConnectionString", def.Id); Assert.NotNull(raw); Assert.NotEqual(secret, raw); Assert.DoesNotContain("SuperSecret123!", raw); // Reading back through EF must transparently decrypt. var loaded = await _context.DatabaseConnectionDefinitions.SingleAsync(d => d.Id == def.Id); Assert.Equal(secret, loaded.ConnectionString); } [Fact] public async Task SmtpConfiguration_Credentials_StoredEncrypted_RoundTrips() { const string secret = "client_secret=oauth2-abc-very-secret"; var smtp = new SmtpConfiguration("smtp.example.com", "OAuth2", "noreply@example.com") { Credentials = secret }; _context.SmtpConfigurations.Add(smtp); await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); var raw = await ReadRawColumnAsync("SmtpConfigurations", "Credentials", smtp.Id); Assert.NotNull(raw); Assert.NotEqual(secret, raw); Assert.DoesNotContain("oauth2-abc-very-secret", raw); var loaded = await _context.SmtpConfigurations.SingleAsync(s => s.Id == smtp.Id); Assert.Equal(secret, loaded.Credentials); } [Fact] public async Task ExternalSystemDefinition_AuthConfiguration_StoredEncrypted_RoundTrips() { const string secret = "{\"apiKey\":\"live-key-do-not-leak\"}"; var ext = new ExternalSystemDefinition("Erp", "https://erp.example.com", "ApiKey") { AuthConfiguration = secret }; _context.ExternalSystemDefinitions.Add(ext); await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); var raw = await ReadRawColumnAsync("ExternalSystemDefinitions", "AuthConfiguration", ext.Id); Assert.NotNull(raw); Assert.NotEqual(secret, raw); Assert.DoesNotContain("live-key-do-not-leak", raw); var loaded = await _context.ExternalSystemDefinitions.SingleAsync(e => e.Id == ext.Id); Assert.Equal(secret, loaded.AuthConfiguration); } [Fact] public async Task SmtpConfiguration_NullCredentials_RoundTripsAsNull() { var smtp = new SmtpConfiguration("smtp.example.com", "None", "noreply@example.com") { Credentials = null }; _context.SmtpConfigurations.Add(smtp); await _context.SaveChangesAsync(); _context.ChangeTracker.Clear(); var loaded = await _context.SmtpConfigurations.SingleAsync(s => s.Id == smtp.Id); Assert.Null(loaded.Credentials); } private async Task ReadRawColumnAsync(string table, string column, int id) { var connection = _context.Database.GetDbConnection(); await using var cmd = connection.CreateCommand(); cmd.CommandText = $"SELECT \"{column}\" FROM \"{table}\" WHERE \"Id\" = $id"; var p = cmd.CreateParameter(); p.ParameterName = "$id"; p.Value = id; cmd.Parameters.Add(p); var result = await cmd.ExecuteScalarAsync(); return result == null || result == DBNull.Value ? null : (string)result; } }