EfCore.EncryptedProperties
1.0.5
dotnet add package EfCore.EncryptedProperties --version 1.0.5
NuGet\Install-Package EfCore.EncryptedProperties -Version 1.0.5
<PackageReference Include="EfCore.EncryptedProperties" Version="1.0.5" />
<PackageVersion Include="EfCore.EncryptedProperties" Version="1.0.5" />
<PackageReference Include="EfCore.EncryptedProperties" />
paket add EfCore.EncryptedProperties --version 1.0.5
#r "nuget: EfCore.EncryptedProperties, 1.0.5"
#:package EfCore.EncryptedProperties@1.0.5
#addin nuget:?package=EfCore.EncryptedProperties&version=1.0.5
#tool nuget:?package=EfCore.EncryptedProperties&version=1.0.5
EfCore.EncryptedProperties
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, ortokens, 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.
SaveChangesencrypts new or changed encrypted properties.- Materialization decrypts transparent properties automatically.
EncryptedValue<T>decrypts only whenGetDecryptedValueAsyncis called, then caches the plaintext in that wrapper instance.- Different key purposes rotate independently, so
EmailandSecretNotescan 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
samples/EfCore.EncryptedProperties.Samples.InMemory- console app showing both entity styles against EF InMemory.samples/EfCore.EncryptedProperties.Samples.AzureKeyVault- console app showing Azure KeyVault backed master key configuration.samples/EfCore.EncryptedProperties.Samples.AzuriteBlobs- console app showing Blob-backed RSA key ring and key-chain storage against local Azurite.samples/EfCore.EncryptedProperties.Samples.WebApi- minimal ASP.NET Core API using file-backed RSA and a SQL Server database key chain.
License
Apache License, Version 2.0.
| Product | Versions 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. |
-
net10.0
- Azure.Identity (>= 1.17.1)
- Azure.Security.KeyVault.Keys (>= 4.8.0)
- Azure.Storage.Blobs (>= 12.28.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.8)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Logging (>= 10.0.8)
-
net8.0
- Azure.Identity (>= 1.17.1)
- Azure.Security.KeyVault.Keys (>= 4.8.0)
- Azure.Storage.Blobs (>= 12.28.0)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.27)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Logging (>= 8.0.1)
-
net9.0
- Azure.Identity (>= 1.17.1)
- Azure.Security.KeyVault.Keys (>= 4.8.0)
- Azure.Storage.Blobs (>= 12.28.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.16)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Logging (>= 9.0.16)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
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.