hexasync.dataprotection 1.10.35-pre.7

hexasync.dataprotection

Envelope encryption (AES-256-GCM) with pluggable KEK custody and key rotation — the HexaSync at-rest data-protection library. Self-describing hxdp: wire format, code-registry algorithm versioning, per-service DEK keyring with in-memory cache, EF Core integration ([Encrypted]), and a self-healing DEK/KEK lifecycle driver.

The Azure Key Vault KEK adapter ships separately as hexasync.dataprotection.azurekeyvault.


How it works (30 seconds)

A per-service KEK (key-encryption key) wraps per-generation DEKs (data-encryption keys). Your data is encrypted with the active DEK; the wrapped DEKs live in a data_encryption_keys table in your database. On boot the library unwraps the keyring into an in-memory cache (one KEK round-trip), then encrypt/decrypt are local. It is hxdp:-only and fails closed — it never falls back to plaintext or a legacy format.

Install

dotnet add package hexasync.dataprotection
dotnet add package hexasync.dataprotection.azurekeyvault   # production KEK custody (optional in dev)

Configuration (flat DATA_PROTECTION_* env vars)

House convention: config via flat UPPER_SNAKE_CASE environment variables (no appsettings.json). Each maps to a property via [ConfigurationKeyName], bound as IOptions<DataProtectionConfig>.

DATA_PROTECTION_KEK_PROVIDER=Local              # "Local" (dev/low envs) or "AzureKeyVault" (prod)
DATA_PROTECTION_MASTER_KEY_BASE64=<b64 32 bytes> # REQUIRED for Local (`openssl rand -base64 32`); boot throws without it
DATA_PROTECTION_KEK_ID=local                    # bookkeeping id stamped on locally-wrapped DEKs (optional)
DATA_PROTECTION_ACTIVE_SCHEME=v1                # optional; default = newest registered scheme; unknown → boot throws
DATA_PROTECTION_EXPECTED_MAX_PODS=16            # must be >= your HPA maxReplicas; drives the nonce early-rotation trip
DATA_PROTECTION_STARTUP_MAX_ATTEMPTS=5          # optional; bounded startup-warm retries (KEK may be transiently down)
DATA_PROTECTION_STARTUP_RETRY_DELAY=00:00:02    # optional

Local is for dev / low environments. The master key sits in config; a KV-wrapped DEK cannot be unwrapped locally (by design — prod data is not decryptable off-cluster). Production should use AzureKeyVault (see that package's README).

KeyringMaintenance is not a JSON section — it is configured in code via the AddKeyringMaintenance(cfg => …) lambda (below).

Wiring (EF-backed, in order)

// 1. Your DbContext factory — the library needs a FRESH context per keyring call (the provider is a singleton;
//    a captured scoped DbContext would throw ObjectDisposedException under load).
services.AddDbContextFactory<MyDbContext>(o => o.UseNpgsql(connectionString));

// 2. Compose the library (store + provider + IDataProtector + startup warm); binds the DATA_PROTECTION_* env vars.
services.AddHexaSyncDataProtectionWithEf<MyDbContext>(configuration);   // bundles AddEfDekKeyringStore<MyDbContext>()

// 3. Production KEK custody (only when Provider=AzureKeyVault; do NOT also register Local — it throws):
services.AddAzureKeyVaultKeyWrapper(configuration);

// 4. Health check (readiness): after AddHexaSyncDataProtection.
services.AddHealthChecks().AddHexaSyncDataProtectionHealthCheck();

// 5. Opt-in self-healing maintenance (DEK bootstrap/rotation + KEK re-wrap). Requires a leader lock (below).
services.AddSingleton<IDataProtectionLeaderLock, RedisMedallionLeaderLock>();
services.AddKeyringMaintenance(cfg => { cfg.MaxDekAge = TimeSpan.FromDays(180); });

A custom audit sink (optional) must be registered before AddHexaSyncDataProtection (it wins a TryAdd over the default no-op): services.AddSingleton<IDataProtectionAuditSink, MySink>();.

Leader-lock adapter (Medallion.Threading Redis)

AddKeyringMaintenance needs an IDataProtectionLeaderLock so exactly one replica per service runs the cycle. The library ships no Redis dependency — adapt your existing distributed lock (the shape is a single blocking acquire returning a handle held until disposed):

public sealed class RedisMedallionLeaderLock(IDistributedLockProvider locks) : IDataProtectionLeaderLock
{
    public async Task<IAsyncDisposable> AcquireAsync(string key, CancellationToken ct)
        => await locks.AcquireLockAsync(key, cancellationToken: ct);   // Medallion handle is already IAsyncDisposable
}

AcquireAsync should block until acquired (so a dead leader's lock is picked up as soon as it releases).

Your DbContext

Implement IDataProtectionDbContext (exposes the keyring DbSet), inject the resolved IDataProtector into the ctor, and call ApplyDataProtection in OnModelCreating:

public class MyDbContext(DbContextOptions options, IDataProtector protector)
    : DbContext(options), IDataProtectionDbContext
{
    public DbSet<DataEncryptionKeyRecord> DataEncryptionKeys => Set<DataEncryptionKeyRecord>();
    public DbSet<UserSecret> UserSecrets => Set<UserSecret>();

    protected override void OnModelCreating(ModelBuilder mb) => mb.ApplyDataProtection(protector);
    // ApplyDataProtection wires the [Encrypted] converter, guards (no index/non-string [Encrypted]), and maps the
    // data_encryption_keys table by construction.
}

[Table("user_secrets")]
public class UserSecret
{
    [Key] public int Id { get; set; }
    [Encrypted] public string TotpSeed { get; set; } = "";   // stored as an hxdp: blob, transparently en/decrypted
    public string Label { get; set; } = "";                  // plaintext, queryable
}

Migration (you own it — the library ships none)

The keyring table your migration must create:

CREATE TABLE data_encryption_keys (
    key_id               text        PRIMARY KEY,
    wrapped_dek          bytea       NOT NULL,
    kek_id               text        NOT NULL,
    state                text        NOT NULL,   -- pending | active | retired | destroyed  (plain text, not an enum)
    created_under_scheme text        NOT NULL,   -- e.g. 'v1'
    created_at           timestamptz NOT NULL
);

Each [Encrypted] business column is text (it holds the ASCII hxdp: blob — not bytea). If an entity implements IEncryptedEntity (for the future 1.9 re-encrypt sweep), also add a nullable dek_reconciled_version text column. Do not put an index / WHERE / ORDER BY / FK on an [Encrypted] column (see the ops runbook).

Health check

AddHexaSyncDataProtectionHealthCheck() reports Unhealthy until the keyring warms (IsInitialized) — wire it to readiness so a pod that can't reach the KEK at boot doesn't take traffic. When AddKeyringMaintenance is wired, an overdue rotation reports Degraded (never Unhealthy — the pod still serves).

Migrating off a legacy format

The library is hxdp:-only; migrating legacy AES-CBC (or any prior format) is the consumer's job. Read the raw legacy value with your own code (raw SQL, or a temporary non-[Encrypted] mapping — you can't read it through the [Encrypted] property, which would try to decrypt it as hxdp: and fail closed), decrypt it with your legacy code, then write the plaintext through the [Encrypted] property so the converter re-encrypts it on save. Use HxdpEnvelope.IsProtected on the raw value to skip already-migrated rows:

foreach (var (id, raw) in ReadRawLegacyColumn())          // your code — bypasses the [Encrypted] converter
{
    if (HxdpEnvelope.IsProtected(raw)) continue;          // already "hxdp:" → migrated; leave it

    // Attach a stub and mark ONLY the [Encrypted] property modified. This emits an UPDATE that encrypts the
    // assigned plaintext — WITHOUT a prior SELECT, so the legacy value is never read through the converter
    // (which would fail closed). Do NOT db.Find/materialize the row here.
    var stub = new UserSecret { Id = id };
    db.Attach(stub);
    stub.TotpSeed = DecryptLegacy(raw);                   // plaintext in → converter encrypts on save
}
db.SaveChanges();

Non-EF callers use IDataProtector directly — note it is bytes-in / bytes-out (the result is the ASCII hxdp: blob): var blob = Encoding.ASCII.GetString(await protector.EncryptAsync(Encoding.UTF8.GetBytes(plaintext)));.

Roll out decrypt-before-encrypt: deploy the version that can decrypt hxdp: everywhere before you start writing hxdp: anywhere, so no pod reads a blob it can't decrypt.

Sensitive-data-logging prohibition

EnableSensitiveDataLogging would write decrypted plaintext + key material to logs. Call context.WarnIfSensitiveDataLoggingWithEncryptedProperties(logger) at startup — it logs a warning if the trap is on for a context with [Encrypted] properties. (It warns; it does not block.)

Observability

Metrics ship on a Meter named HexaSync.DataProtection (tag-free counters: encrypt/decrypt/decrypt_miss/ rotation/rewrap/kek_unwrap; gauges: active-DEK nonce consumption + age). Subscribe your OpenTelemetry exporter to that meter name. For per-event auditing (rotate / re-wrap / destroy / decrypt-miss / kek-version-change), implement IDataProtectionAuditSink and register it before AddHexaSyncDataProtection.

Crypto inventory

EncryptedFieldInventory.ToMarkdown(ctx.Model) renders the declared [Encrypted] fields as a Markdown table for a CRYPTO-INVENTORY.md artifact (emit it at boot or from a CLI dump mode). Schema metadata only — never key material.

Name note: IDataProtector

This library's hexasync.dataprotection.IDataProtector shares its simple name with Microsoft.AspNetCore.DataProtection.IDataProtector. If a consumer uses both, namespace-qualify or alias (using IHxDataProtector = hexasync.dataprotection.IDataProtector;). They are unrelated types.

Operations

KEK-retention, compromise/rotation, encrypted-column rules, and the KV rotation burst budget are in the ops runbook: docs/data-protection/OPS_RUNBOOK.md. The KEK-retention rule is data-loss-class — read it before rotating a KEK in production.

Showing the top 20 packages that depend on hexasync.dataprotection.

Packages Downloads
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
43
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
30
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
27
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
26
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
24
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
24
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
19
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
19
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
15
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
15
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
12
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
12
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
10
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
8
hexasync.dataprotection.endpoints
ASP.NET Core admin endpoints for hexasync.dataprotection — the KEK self-test. The ASP.NET dependency is isolated to this package (the core crypto package stays HTTP-free); authorization is consumer-supplied via a configure callback, so this package depends on no auth middleware.
8
hexasync.dataprotection.azurekeyvault
Azure Key Vault KEK custody adapter for hexasync.dataprotection. Wraps/unwraps DEKs via KV (KEK never leaves the vault), secretless auth, version-pinned. The Azure SDK is isolated to this package.
6

Version Downloads Last updated
1.10.62 16 08/26/2026
1.10.61 56 08/12/2026
1.10.60 10 08/11/2026
1.10.59 0 08/10/2026
1.10.58 9 08/08/2026
1.10.57 19 08/05/2026
1.10.56 0 08/04/2026
1.10.55 10 08/03/2026
1.10.54 0 08/03/2026
1.10.54-pre 0 08/01/2026
1.10.53 6 07/31/2026
1.10.49 1 07/29/2026
1.10.48 4 07/28/2026
1.10.47 12 07/27/2026
1.10.46 0 07/25/2026
1.10.45 0 07/25/2026
1.10.44 27 07/22/2026
1.10.43 4 07/21/2026
1.10.42 0 07/21/2026
1.10.41 0 07/21/2026
1.10.40 0 07/20/2026
1.10.39 11 07/15/2026
1.10.38 0 07/14/2026
1.10.38-pre.1 0 07/14/2026
1.10.37 0 07/13/2026
1.10.36 24 07/13/2026
1.10.35 0 07/10/2026
1.10.35-pre.13 0 07/10/2026
1.10.35-pre.8 6 07/09/2026
1.10.35-pre.7 2 07/08/2026