using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace ScadaLink.ConfigurationDatabase; /// /// 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. /// /// /// 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 implementing /// IDataProtectionKeyContext), so all central nodes share the same key ring /// and can decrypt each other's writes. /// public sealed class EncryptedStringConverter : ValueConverter { /// The Data Protection purpose string shared by all encrypted configuration columns. 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); } } }