Encrypted columns
Encrypt sensitive fields at rest with the [Encrypted] attribute — transparent encrypt-on-write / decrypt-on-read, stored as bytea, using a built-in AES encryptor or a pluggable IFieldEncryptor (e.g. HashiCorp Vault).
Mark any property [Encrypted] and Socigy transparently encrypts it on write and decrypts it on read. The value is stored as bytea ciphertext; your code keeps working with the original CLR type.
using Socigy.OpenSource.DB.Attributes;
[Table("patients")]
public partial class Patient
{
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
public string Email { get; set; } = ""; // plain — use this to look rows up
[Encrypted] public string Ssn { get; set; } = "";
[Encrypted] public DateTime BirthDate { get; set; }
[Encrypted] public string? Notes { get; set; }
}How it works
- The column is generated as
bytea. On INSERT/UPDATE the value is serialized to bytes, encrypted, and bound asbytea. On SELECT the bytes are decrypted and deserialized back to the property's type. nullis never encrypted — a nullable encrypted column stays SQLNULL.- Encryption uses the ambient
IFieldEncryptorconfigured viaSocigyFieldEncryption(see below). Generated entity code has no DI access, so the encryptor is process-wide ambient state — exactly likeSocigyDbDiagnostics.
Deferred decryption (`AutoDecrypt = false`)
By default every encrypted column is decrypted automatically on read. When most reads don't need the plaintext, set AutoDecrypt = false to skip that cost — the source generator then:
- fills a read-only
{Property}RawEncrypted(byte[]) with the raw ciphertext, and - adds a getter-only
{Property}Decryptedthat decrypts on first access and caches the plaintext into the property itself.
[Table("patients")]
public partial class Patient
{
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
[Encrypted(AutoDecrypt = false)]
public string Ssn { get; set; } = "";
}
// On read, Ssn stays empty and PatientRawEncrypted holds the ciphertext:
var p = await Patient.Query(x => x.Id == id).WithConnection(conn).ExecuteAsync().FirstAsync();
byte[]? raw = p.SsnRawEncrypted; // raw ciphertext, no decryption performed
string ssn = p.SsnDecrypted; // decrypts once, caches into p.Ssn
// p.Ssn == ssn now (cached)Ssn). For a read-modify-write of an AutoDecrypt = false row, access {Property}Decrypted first so the plaintext is populated before you save.Supported types
Any common CLR value type plus string and byte[]: bool, all integer / floating / decimal types, char, string, Guid, DateTime, DateTimeOffset, TimeSpan, byte[], enums, and Nullable<> of these. Other types throw NotSupportedException.
[Encrypted] cannot be combined with [ValueConvertor], [JsonColumn], or [RawJsonColumn] in this version — the source generator reports error SCGDB002.Encrypted columns are not queryable
Encryption is non-deterministic (a fresh random IV each time), so the same plaintext produces different ciphertext. Using an encrypted column in a WHERE, ORDER BY, SELECT-projection, or LIKE clause throws a clear NotSupportedException:
// ❌ throws — Ssn is [Encrypted]
await foreach (var p in Patient.Query(x => x.Ssn == ssn).WithConnection(conn).ExecuteAsync()) { }
// ✅ filter by a non-encrypted key, then read the decrypted value
var patient = (await Patient.Query(x => x.Email == email).WithConnection(conn).ExecuteAsync().FirstAsync());
var ssn = patient.Ssn; // decrypted automaticallyConfiguring the encryptor
Built-in AES (no external KMS)
AesFieldEncryptor is an authenticated AES-256-CBC + HMAC-SHA256 encryptor. Give it a 32-byte key from your secret store (never hard-code keys) and configure it once at startup:
using Socigy.OpenSource.DB.Core.Encryption;
byte[] key = Convert.FromBase64String(builder.Configuration["Encryption:Key"]!); // 32 bytes
SocigyFieldEncryption.Configure(new AesFieldEncryptor(key));Do this before any encrypted column is read or written. If an [Encrypted] column is accessed with no encryptor configured, you get a clear InvalidOperationException.
HashiCorp Vault
For Vault-managed keys, use the optional Socigy.OpenSource.DB.HashiCorp package, which installs an IFieldEncryptor whose key is sourced from Vault.
Writing your own encryptor
Implement IFieldEncryptor (synchronous, local) and register it:
public sealed class MyEncryptor : IFieldEncryptor
{
public byte[] Encrypt(byte[] plaintext) => /* ... */;
public byte[] Decrypt(byte[] ciphertext) => /* ... */;
}
SocigyFieldEncryption.Configure(new MyEncryptor());