EfCore.EncryptedProperties 1.0.5

dotnet add package EfCore.EncryptedProperties --version 1.0.5
                    
NuGet\Install-Package EfCore.EncryptedProperties -Version 1.0.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EfCore.EncryptedProperties" Version="1.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EfCore.EncryptedProperties" Version="1.0.5" />
                    
Directory.Packages.props
<PackageReference Include="EfCore.EncryptedProperties" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EfCore.EncryptedProperties --version 1.0.5
                    
#r "nuget: EfCore.EncryptedProperties, 1.0.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EfCore.EncryptedProperties@1.0.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EfCore.EncryptedProperties&version=1.0.5
                    
Install as a Cake Addin
#tool nuget:?package=EfCore.EncryptedProperties&version=1.0.5
                    
Install as a Cake Tool

EfCore.EncryptedProperties

Build and Publish

Property-level encryption for Entity Framework Core 8, 9, and 10. Mark the properties that should be protected, configure where keys live, and keep using your entities in a normal EF workflow.

EfCore.EncryptedProperties is aimed at applications that need more than a value converter wrapped around a single AES key. It encrypts individual EF properties before they reach database storage, uses authenticated encryption for the stored payload, and includes a key-chain layer for creating, wrapping, storing, rotating, caching, and preloading data keys.

  • Targets: .NET 8/9/10 with matching EF Core 8/9/10 dependency groups
  • Use it for: PII, notes, tokens, small secrets, and values the database should never see in plaintext
  • Entity experience: normal CLR properties for transparent reads, or EncryptedValue<T> when you want explicit async decryption
  • Crypto shape: AES-256-GCM payload encryption, a fresh content-encryption key per encrypted value, AES-GCM key wrapping, and RSA-wrapped key-encryption keys
  • Key management: file PEM, file PFX, OS certificate store, in-memory, Azure Blob, and Azure Key Vault RSA providers, plus in-memory, file-backed, Azure Blob, or database-backed key-chain storage

Why This Package

Many EF Core encryption approaches stop at the first step: convert a property to ciphertext on save and back to plaintext on read. This package also handles the parts that usually become application-specific security plumbing:

  • Envelope encryption out of the box. Each encrypted value gets its own content-encryption key. Content keys are wrapped by per-purpose key-encryption keys, and key-encryption keys are wrapped by an RSA provider.
  • Key purposes and rotation. Use separate key chains for different data classes, such as email, notes, or tokens, and rotate new writes without losing access to old rows.
  • Production master key locations. Keep the RSA wrapping key in a PEM file, a read-only PFX file, an OS certificate store, Azure Blob Storage, in Azure Key Vault when the private key should stay outside the host, or in memory for tests and demos.
  • Durable key chains. Store wrapped key records in files, Azure Blob Storage, or beside the application database, with one active key per purpose.
  • Two entity styles. Use ordinary CLR properties when transparency matters, or EncryptedValue<T> when you want decryption to be explicit and async at the call site.
  • Typed values, not only strings. Supported values include primitives, string, byte[], DateTime, DateTimeOffset, Guid, enums, and nullable variants.

Install

dotnet add package EfCore.EncryptedProperties
<PackageReference Include="EfCore.EncryptedProperties" Version="1.0.5" />

Quick Start

Register encryption services once in application DI, then enable the EF integration on each encrypted DbContext.

using EfCore.EncryptedProperties.Extensions;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

services.AddEncryptedProperties(cfg => cfg
    .WithFileRsaKeyProvider("rsa-key.pem", "rsa-v1")
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString)
    .WithKeyChainPreloadOnStartup());

services.AddDbContext<AppDbContext>((sp, options) =>
{
    options
        .UseSqlServer(connectionString)
        .UseEncryptedProperties(sp);
});

If you use the database key chain, add its table to your model. Mark encrypted properties with the fluent API:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.UseEncryptedPropertiesKekStorage();

    modelBuilder.Entity<Customer>(entity =>
    {
        entity.HasKey(e => e.Id);
        entity.Property(e => e.Email).IsEncrypted();
        entity.Property(e => e.SecretNotes).IsEncrypted(opts => opts.KeyPurpose = "notes");
    });
}

Or use the [Encrypted] data annotation on the entity:

using EfCore.EncryptedProperties;

public sealed class Customer
{
    public Guid Id { get; set; }

    [Encrypted("email")]
    public string Email { get; set; } = string.Empty;

    [Encrypted(KeyPurpose = "notes")]
    public EncryptedValue<string> SecretNotes { get; set; } = default!;
}

Then use the entity normally:

var customer = new Customer
{
    Name = "Alice",
    Email = "alice@example.com",
    SecretNotes = "private message"
};

db.Customers.Add(customer);
await db.SaveChangesAsync();

var saved = await db.Customers.FindAsync(customer.Id);
Console.WriteLine(saved!.Email);
Console.WriteLine(await saved.SecretNotes.GetDecryptedValueAsync());

Entity Styles

Choose the style by choosing the CLR type.

Transparent Reads

Use the real property type when you want the value decrypted as soon as EF materializes the entity.

public sealed class Customer
{
    public Guid Id { get; set; }
    public string Email { get; set; } = string.Empty;
}

This is the easiest option for everyday fields like email, phone number, or a short identifier.

Explicit Async Reads

Use EncryptedValue<T> when you want to defer decryption until application code asks for the value.

public sealed class Customer
{
    public Guid Id { get; set; }
    public EncryptedValue<string> SecretNotes { get; set; } = default!;
}

customer.SecretNotes = "private message";

var notes = await customer.SecretNotes.GetDecryptedValueAsync(ct);

This is useful for larger values, values rarely shown to users, or code paths where you want decryption to be obvious.

Setup Recipes

Tests and Local Demos

services.AddEncryptedProperties(cfg => cfg
    .WithInMemoryRsaKeyProvider(RSA.Create(2048), "test-rsa-v1")
    .WithInMemoryKeyChain());

In-memory keys are lost when the process exits. They are for tests, demos, and short-lived local runs.

Self-hosted Apps

services.AddEncryptedProperties(cfg => cfg
    .WithFileRsaKeyRingProvider(options =>
    {
        options.CurrentKeyId = "rsa-v1";
        options.AddKey("rsa-v1", "keys/rsa-v1.pem");
    })
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString));

The file key-ring provider creates the current PEM file if it does not exist. Back it up and protect it like any other application secret. Historical key files must already exist, so a missing old key fails fast instead of silently creating a replacement that cannot decrypt existing records.

For simple single-key deployments, WithFileRsaKeyProvider("keys/rsa-key.pem", "rsa-v1") is still available.

Read-Only PFX Files

services.AddEncryptedProperties(cfg => cfg
    .WithFilePfxRsaKeyRingProvider(options =>
    {
        options.CurrentKeyId = "rsa-v2";
        options.AddKey("rsa-v1", "keys/rsa-v1.pfx", oldPassword);
        options.AddKey("rsa-v2", "keys/rsa-v2.pfx", currentPassword);
    })
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString));

PFX providers are read-only. They never create certificate files, so every configured current and historical PFX must already exist and contain an RSA private key. For a single PFX, use WithFilePfxRsaKeyProvider("keys/rsa-v1.pfx", "rsa-v1", password).

File-Backed Key Chain

services.AddEncryptedProperties(cfg => cfg
    .WithFileRsaKeyProvider("keys/rsa-key.pem", "rsa-v1")
    .WithFileKeyChain("keys/key-chain"));

The file key chain stores wrapped KEK records as one JSON file per key purpose in the configured directory. Protect and back up this directory alongside the RSA private key; losing either the RSA key or the key-chain files can make existing encrypted data unreadable.

Azure Blob Storage

var container = new BlobContainerClient(
    new Uri("https://account.blob.core.windows.net/encryption"),
    new DefaultAzureCredential());

services.AddEncryptedProperties(cfg => cfg
    .WithAzureBlobRsaKeyRingProvider(container, options =>
    {
        options.BlobPrefix = "rsa/";
        options.CurrentKeyId = "rsa-v1";
        options.CreateContainerIfNotExists = true;
        options.AddKey("rsa-v1", "rsa-v1.pem");
    })
    .WithAzureBlobKeyChain(container, options =>
    {
        options.BlobPrefix = "key-chain/";
        options.CreateContainerIfNotExists = true;
    }));

The Blob PEM key-ring provider can create the current PEM blob if it is missing; historical PEM blobs must already exist. WithAzureBlobPfxRsaKeyRingProvider is available for read-only PFX blobs. Blob key-chain storage uses optimistic ETag writes to keep one active KEK per purpose under concurrent callers.

For a local Azurite-backed sample that stores both the RSA PEM key ring and key-chain JSON documents in blobs:

docker run --rm -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
dotnet run --project samples/EfCore.EncryptedProperties.Samples.AzuriteBlobs

OS Certificate Store

services.AddEncryptedProperties(cfg => cfg
    .WithX509StoreRsaKeyProvider(options =>
    {
        options.CurrentCertificateThumbprint = "00112233445566778899AABBCCDDEEFF00112233";
    })
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString)
    .WithKeyChainPreloadOnStartup());

By default, the provider reads from CurrentUser\My. New KEKs are wrapped with the configured current certificate thumbprint, and the stored KEK record keeps a self-describing RSA key ID such as x509store:CurrentUser:My:{thumbprint}. Historical KEKs unwrap by the stored thumbprint, so keep old certificates and their private keys in the store while any KEKs still reference them.

For Windows services, LocalMachine\My can be used when the service identity has private-key access. Prefer CNG-backed RSA certificates; older CAPI keys may not support RSA-OAEP-256. On Linux, prefer CurrentUser\My; LocalMachine\My is not a portable place for private-key certificates in .NET.

The provider does not export private keys from store.

Azure Key Vault

services.AddEncryptedProperties(cfg => cfg
    .WithAzureKeyVaultRsaKeyProvider(
        new Uri("https://my-vault.vault.azure.net/keys/my-key"),
        new DefaultAzureCredential())
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString));

Use this when the RSA private key should stay outside the application host. Pass the unversioned Key Vault key URI; new KEKs use the latest key version, while existing KEKs store and use the exact versioned Key Vault key ID that wrapped them.

Keep old Key Vault key versions enabled and recoverable while any KEKs wrapped by those versions still exist.

Key Rotation

services.AddEncryptedProperties(cfg => cfg
    .WithFileRsaKeyRingProvider(options =>
    {
        options.CurrentKeyId = "rsa-v2";
        options.AddKey("rsa-v1", "keys/rsa-v1.pem");
        options.AddKey("rsa-v2", "keys/rsa-v2.pem");
    })
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString)
    .WithKeyChainRotation(policy =>
    {
        policy.KeyRotateAfter = TimeSpan.FromDays(90);
    }));

New writes use the current active KEK for the property's purpose. When key-chain rotation creates a new KEK, it is wrapped with the key ring's CurrentKeyId; existing KEKs remain readable through their stored RSA key IDs.

To rewrap existing KEKs after an RSA master-key rotation, configure the provider with both the old and new RSA keys, set the new key as current, then run a dry run before writing changes:

using EfCore.EncryptedProperties.Abstractions;
using EfCore.EncryptedProperties.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

var rewrapper = serviceProvider.GetRequiredService<IKeyChainRewrapper>();

var dryRun = await rewrapper.RewrapAsync(new KeyChainRewrapOptions
{
    OldRsaKeyId = "rsa-v1",
    DryRun = true
});

var result = await rewrapper.RewrapAsync(new KeyChainRewrapOptions
{
    OldRsaKeyId = "rsa-v1"
});

Rewrap updates only key-chain records. It preserves KEK IDs, so encrypted entity rows do not need to be rewritten. After rewrap, verify startup preload and encrypted reads with only the new RSA key configured. Remove old RSA material only after no key-chain records reference it.

Startup KEK Preload

services.AddEncryptedProperties(cfg => cfg
    .WithFileRsaKeyProvider("rsa-key.pem", "rsa-v1")
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString)
    .WithKeyChainPreloadOnStartup());

This registers an IHostedService that unwraps all stored KEKs during host startup. If preload fails, the app fails fast instead of discovering key access problems on the first encrypted read or write.

What To Expect

  • The database column stores ciphertext, not the original value.
  • SaveChanges encrypts new or changed encrypted properties.
  • Materialization decrypts transparent properties automatically.
  • EncryptedValue<T> decrypts only when GetDecryptedValueAsync is called, then caches the plaintext in that wrapper instance.
  • Different key purposes rotate independently, so Email and SecretNotes can use separate key chains.

Supported value types are primitives, string, byte[], bool, DateTime, DateTimeOffset, Guid, enums backed by supported primitives, and nullable variants.

Custom Value Serializers

Register a serializer when an encrypted property uses a plaintext type outside the built-in set. Built-in formats cannot be overridden, so existing encrypted values stay readable.

using System.Text;
using EfCore.EncryptedProperties.Serialization;

services.AddEncryptedProperties(cfg => cfg
    .WithFileRsaKeyProvider("rsa-key.pem", "rsa-v1")
    .WithDatabaseKeyChain(SqlClientFactory.Instance, connectionString)
    .WithValueSerializer<Uri>(new UriValueSerializer()));

public sealed class Customer
{
    public Guid Id { get; set; }

    [Encrypted("website")]
    public Uri Website { get; set; } = new("https://example.com");
}

public sealed class UriValueSerializer : IEncryptedPropertyValueSerializer<Uri>
{
    public byte[] Serialize(Uri value)
        => Encoding.UTF8.GetBytes(value.ToString());

    public Uri Deserialize(byte[] data)
        => new(Encoding.UTF8.GetString(data), UriKind.Absolute);
}

Edge Cases

Queries

Do not query encrypted columns by plaintext:

// This will not work reliably.
var customer = await db.Customers.SingleOrDefaultAsync(c => c.Email == "alice@example.com");

Ciphertext changes on each write, even for the same plaintext. For lookups, keep a separate non-encrypted lookup column such as a normalized hash.

Migrations

If you use WithDatabaseKeyChain, call modelBuilder.UseEncryptedPropertiesKekStorage() and create the table with migrations or EnsureCreated().

Encrypted entity properties are still mapped to normal database columns, but those columns hold ciphertext.

The key-chain table enforces one active KEK per purpose with a filtered unique index on Purpose where IsActive = 1.

Nulls and Defaults

null encrypted reference or nullable values are stored as null. For non-nullable value types, a missing encrypted payload materializes as the CLR default value.

Keys

Keep the RSA key stable. If the file key is deleted, replaced, or a different Key Vault key is configured, previously stored key-chain records may no longer unwrap.

For local RSA master-key rotation, use WithFileRsaKeyRingProvider, add the new PEM file under a new key ID, set CurrentKeyId to that new ID, and keep older AddKey entries until KEK rewrap has completed and no EncryptedPropertyKeks.RsaKeyId values reference them.

For the OS certificate store provider, rotate RSA wrapping keys by provisioning a new certificate with a private key, deploying its thumbprint as CurrentCertificateThumbprint, and running KEK rewrap. Keep previous certificates available for decrypt and startup preload until no EncryptedPropertyKeks.RsaKeyId values reference their thumbprints.

The library rotates data-encryption keys and can manually rewrap stored KEKs, but it does not automatically mutate RSA master-key wrapping on startup.

Plaintext Change Tracking

For transparent properties, assign the new value and call SaveChanges as usual. For EncryptedValue<T>, assigning from T marks the wrapper as modified:

customer.SecretNotes = "updated private message";
await db.SaveChangesAsync();

Plaintext Is Still In Your Process

This protects data from being stored in plaintext in the database. It does not hide values from your application code, logs, memory dumps, or API responses. Treat decrypted values carefully once you read them.

Samples

License

Apache License, Version 2.0.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.5 46 5/27/2026
1.0.4 110 5/20/2026
1.0.3 107 5/20/2026
1.0.2 96 5/17/2026
1.0.0 94 5/16/2026

Changes since 1.0.4:
- Added custom encrypted-property value serializer support via `IEncryptedPropertyValueSerializer<T>`.
- Added operator-invoked KEK rewrap for RSA master-key rotation via `IKeyChainRewrapper`, including dry-run and filtering options.