Klean.EntityFrameworkCore.DataProtection
1.2.1
dotnet add package Klean.EntityFrameworkCore.DataProtection --version 1.2.1
NuGet\Install-Package Klean.EntityFrameworkCore.DataProtection -Version 1.2.1
<PackageReference Include="Klean.EntityFrameworkCore.DataProtection" Version="1.2.1" />
paket add Klean.EntityFrameworkCore.DataProtection --version 1.2.1
#r "nuget: Klean.EntityFrameworkCore.DataProtection, 1.2.1"
// Install Klean.EntityFrameworkCore.DataProtection as a Cake Addin #addin nuget:?package=Klean.EntityFrameworkCore.DataProtection&version=1.2.1 // Install Klean.EntityFrameworkCore.DataProtection as a Cake Tool #tool nuget:?package=Klean.EntityFrameworkCore.DataProtection&version=1.2.1
Klean.EntityFrameworkCore.DataProtection
Klean.EntityFrameworkCore.DataProtection
is a Microsoft Entity Framework Core extension which
adds support for data protection and querying for encrypted properties for your entities.
What problem does this library solve?
When you need to store sensitive data in your database, you may want to encrypt it to protect it from unauthorized access, however, when you encrypt data, it becomes impossible to query it by EF-core, which is not really convenient if you want to encrypt, for example, email addresses, or SSNs AND then filter entities by them.
This library has support for hashing the salted sensitive data and storing their (Hmac Sha256) hashes in a shadow property alongside the encrypted data.
This allows you to query for the encrypted properties without decrypting them first. using QueryableExt.WherePdEquals
Disclaimer
This project is maintained by one (10x) developer and is not affiliated with Microsoft.
I made this library to solve my own problems with EFCore. I needed to store a bunch of protected personal data encrypted, among these properties were personal IDs, Emails, SocialSecurityNumbers and so on. As you know, you cannot query encrypted data with EFCore, and I wanted a simple yet boilerplate-free solution. Thus, I made this library.
What this library allows you to do, is to encrypt your properties and query them without decrypting them first. It does so by hashing the encrypted data and storing the hash in a shadow property alongside the encrypted data.
I do not take responsibility for any damage done in production environments and lose of your encryption key or corruption of your data.
Keeping your encryption keys secure is your responsibility. If you lose your encryption key, you will lose your data.
Currently supported property types
- string
- byte[]
Getting started
Installing the package
Install the package from NuGet or from the Package Manager Console
:
PM> Install-Package Klean.EntityFrameworkCore.DataProtection
Configuring Data Protection in your DbContext
YourDbContext.cs
public class Your(DbContextOptions<Your> options, IDataProtectionProvider dataProtectionProvider) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.UseDataProtection(dataProtectionProvider);
}
}
[!WARNING] The call to
builder.UseDataProtection
MUST come after the call tobase.OnModelCreating
in yourDbContext
class and before any other configuration you might have.
Registering the services
Program.cs
builder.Services.AddDataProtectionServices();
To persist keys in file system use the following code:
var keyDirectory = new DirectoryInfo("path/to/solution/.aspnet/dp/keys");
builder.Services.AddDataProtectionServices()
.PersistKeysToFileSystem(keyDirectory);
[!WARNING] If you want to query for encrypted properties, along with marking your properties as queryable, you MUST set
EFCORE_DATA_PROTECTION__HASHING_SALT
in the environment. I could suggest you setting it to a random guid, or a very long string. This was implemented to prevent rainbow attacks on sensitive data.
[!TIP] See the Microsoft documentation for more information on how to configure the data protection services, and how to store your encryption keys securely.
Configure the data protection options on your DbContext
Program.cs
services.AddDbContext<YourDbContext>(opt => opt
.AddDataProtectionInterceptors()
/* ... */);
[!WARNING] You MUST call
AddDataProtectionInterceptors
if you are using any encrypted properties that are queryable in your entities. If you are not using any queryable encrypted properties, you can skip this step.
Usage:
Marking your properties as encrypted
There are three ways you can mark your properties as encrypted:
Using the EncryptedAttribute:
class User
{
[Encrypt(isQueryable: true, isUnique: true)]
public string SocialSecurityNumber { get; set; }
[Encrypt(isQueryable: false, isUnique: false)]
public byte[] IdPicture { get; set; }
}
[!TIP]
isQueryable
marks a property as queryable (this will generate a shadow hash) <br/>isUnique
marks a property as unique, and adds a unique index on the property. An index is added by default, even if the property is not marked as unique. However, that default index is not Unique.
[!WARNING] If you have a property that is marked as queryable, you MUST call
AddDataProtectionInterceptors
in yourDbContext
configuration. And you MUST setEFCORE_DATA_PROTECTION__HASHING_SALT
in the environment. Otherwise, an exception will be thrown while trying to save the entity.
Using the FluentApi (in your DbContext.OnModelCreating
method):
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<User>(entity =>
{
// v defaults to true
entity.Property(e => e.SocialSecurityNumber).IsEncryptedQueryable(isUnique: true);
entity.Property(e => e.IdPicture).IsEncrypted();
});
}
The above step also applies to custom EntityTypeConfiguration
s
Querying encrypted properties
You can query encrypted properties that are marked as Queryable using the IQueryable<T>.WherePdEquals
extension method:
var foo = await DbContext.Users
.WherePdEquals(nameof(User.SocialSecurityNumber), "404-69-1337")
.SingleOrDefaultAsync();
[!WARNING] The
QueryableExt.WherePdEquals
method is only available for properties that are marked as Queryable using the[Encrypt(isQueryable: true)]
attribute or theIsEncryptedQueryable()
method.
[!CAUTION] Before using
WherePdEquals
you MUST callAddDataProtectionInterceptors
in yourDbContext
configuration. There will be no error if you forget to callAddDataProtectionInterceptors
, but the query will not work as expected.
[!NOTE] The
WherePdEquals
extension method generates an expression like this one under the hood:<br/>Where(e => EF.Property<string>(e, $"{propertyName}ShadowHash") == value.HmacSha256Hash())
Profit!
Esoteric usage:
Q: How to use intermediary converters? <br/> A: If you have an entity that needs a custom converter, you are covered, all you have to do is specify that the property has a custom converter along with the
IsEncrypted
attribute.
sealed record AddressData(string Country, string ZipCode)
{
public static AddressData Parse(string str) => str.Split('-') switch
{
[var country, var zipCode] => new AddressData(country, zipCode),
_ => throw new FormatException("Invalid format"),
};
public override string ToString() => $"{Country}-{ZipCode}";
}
// intermediary converter
class AddressToStringIntermediaryConverter() : ValueConverter<AddressData, string>(
to => to.ToString(),
from => AddressData.Parse(from));
// user configuration
class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.Property(x => x.Address)
// the order does not matter.
.HasConversion<AddressToStringIntermediaryConverter>()
.IsEncrypted();
}
}
Thank you for using this library!
ddjerqq ❤️
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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. |
-
net6.0
- Microsoft.AspNetCore.DataProtection (>= 6.0.36 && < 8.0.0)
- Microsoft.EntityFrameworkCore (>= 6.0.36 && < 8.0.0)
-
net7.0
- Microsoft.AspNetCore.DataProtection (>= 7.0.0 && < 9.0.0)
- Microsoft.EntityFrameworkCore (>= 7.0.0 && < 9.0.0)
-
net8.0
- Microsoft.AspNetCore.DataProtection (>= 8.0.11)
- Microsoft.EntityFrameworkCore (>= 8.0.11)
-
net9.0
- Microsoft.AspNetCore.DataProtection (>= 9.0.0)
- Microsoft.EntityFrameworkCore (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.