fix(configuration-database): resolve ConfigurationDatabase-002..007 — remove hardcoded sa creds, fail-fast no-arg DI, encrypt secret columns, resilient audit serialization

This commit is contained in:
Joseph Doherty
2026-05-16 21:11:24 -04:00
parent 8fc04d43c2
commit 0c82ffcbe6
17 changed files with 2029 additions and 40 deletions

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace ScadaLink.ConfigurationDatabase;
/// <summary>
/// EF Core value converter that encrypts a string column at rest using ASP.NET
/// Data Protection. Plaintext is protected when written to the database and
/// transparently unprotected when read back, so secret-bearing columns
/// (SMTP credentials, external-system auth config, database connection strings)
/// are never persisted verbatim.
/// </summary>
/// <remarks>
/// The protector is purpose-scoped so ciphertext from one column cannot be
/// unprotected as another. Data Protection keys are persisted to the
/// configuration database itself (see <see cref="ScadaLinkDbContext"/> implementing
/// <c>IDataProtectionKeyContext</c>), so all central nodes share the same key ring
/// and can decrypt each other's writes.
/// </remarks>
public sealed class EncryptedStringConverter : ValueConverter<string?, string?>
{
/// <summary>The Data Protection purpose string shared by all encrypted configuration columns.</summary>
public const string ProtectorPurpose = "ScadaLink.ConfigurationDatabase.EncryptedColumn";
public EncryptedStringConverter(IDataProtector protector)
: base(
plaintext => plaintext == null ? null : protector.Protect(plaintext),
ciphertext => ciphertext == null ? null : Unprotect(protector, ciphertext))
{
}
private static string Unprotect(IDataProtector protector, string ciphertext)
{
// A row that predates encryption (or test fixtures inserting raw text) is not valid
// protected payload. Unprotect throws CryptographicException in that case; surface a
// clearer message rather than a bare crypto failure.
try
{
return protector.Unprotect(ciphertext);
}
catch (System.Security.Cryptography.CryptographicException ex)
{
throw new InvalidOperationException(
"Failed to decrypt an encrypted configuration column. The Data Protection key " +
"ring may be unavailable, or the stored value was not written by this system.",
ex);
}
}
}