DEV Community

Krzysztof Begiedza
Krzysztof Begiedza

Posted on

Secure Your Data with Entity Framework Core Encryption

Abstract

Storing sensitive data in a database is not an easy task. Many things can go wrong, especially if proper precautions aren’t taken. Sensitive information, such as personal details or financial records, can become vulnerable to unauthorized access if encryption and security measures aren’t carefully implemented.

When it comes to encrypting and decrypting data in an application, ASP.NET Core provides a brilliant option for handling encryption with the powerful DataProtection API. Alternatively, you can implement custom encryption logic tailored to your application's requirements. However, the challenge is not only encrypting and decrypting the data but also managing how this encrypted data is stored and retrieved efficiently from the database.

There are multiple ways to handle the storage and retrieval of encrypted data in Entity Framework Core. One common approach is to use backing fields, where encryption and decryption are handled on the fly when the data is accessed via property getters or setters. This way, the sensitive data remains encrypted while at rest in the database but is decrypted just in time when needed by the application.

public record PlayerProfile(ICryptographyService cryptographyService)
{
    private string _email;
    private string _realName;

    public string Email
    {
        get => cryptographyService.Decrypt(_email);
        set => _email = cryptographyService.Encrypt(value);
    }

    public string RealName
    {
        get => cryptographyService.Decrypt(_realName);
        set => _realName = cryptographyService.Encrypt(value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Another more robust approach leverages Entity Framework Core's ValueConverters, which can be registered at the model creation level. Value converters allow you to specify how data is transformed when being read from or written to the database, making them ideal for encrypting sensitive data at the column level without affecting the rest of the business logic.

In this post, I would like to share a flexible solution based on the second approach to encrypt and decrypt columns in your database.

Implementation

Suppose you have a player profile entity in your application that contains sensitive information, such as a player's real name and email address. To comply with data protection regulations like GDPR, as well as to ensure the privacy of your users, this information must be securely stored in an encrypted format within the database.

public record PlayerProfile
{
    public required Guid Id { get; init; }

    public required string Email { get; init; }

    public required string RealName { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

To achieve this, we can implement encryption using a value converter that automatically encrypts the data before it is written to the database and decrypts it when it is retrieved. To perform encryption and decryption, we will use a custom ICryptographyService interface that provides methods for encrypting and decrypting data. For simplicity, we will use a simple reverse encryption algorithm in this example.

public interface ICryptographyService
{
    string Encrypt(string data);

    string Decrypt(string data);
}

public class ReverseCryptographyService : ICryptographyService
{
    public string Encrypt(string value)
    {
        ArgumentNullException.ThrowIfNull(value);
        var sb = new StringBuilder();

        foreach (var c in value.Reverse())
            sb.Append(c);

        return sb.ToString();
    }

    public string Decrypt(string value)
    {
        ArgumentNullException.ThrowIfNull(value);
        var sb = new StringBuilder();

        foreach (var c in value.Reverse())
            sb.Append(c);

        return sb.ToString();
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, we should register our ICryptographyService implementation in the DI container in the Startup class.

var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddSingleton<ICryptographyService, ReverseCryptographyService>();
Enter fullscreen mode Exit fullscreen mode

Now, we are ready to implement the EncryptedConverter class that will be used to encrypt and decrypt the data. The EncryptedConverter class inherits from the ValueConverter class provided by Entity Framework Core. It takes an instance of the ICryptographyService interface in its constructor and uses it to encrypt and decrypt the data.

public class EncryptedConverter : ValueConverter<string, string>
{
    public EncryptedConverter(ICryptographyService cryptographyService, ConverterMappingHints? mappingHints = null)
        : base(v => cryptographyService.Encrypt(v),
               v => cryptographyService.Decrypt(v),
               mappingHints) { }
}
Enter fullscreen mode Exit fullscreen mode

From now on, we can use fluent configuration or define a new attribute to mark properties that need to be encrypted. Let's start with the first approach.

Fluent configuration

To use the EncryptedConverter class, we need to configure it in the OnModelCreating method of the DbContext class. We can do this by calling the HasConversion method on the PropertyBuilder instance for each property that needs to be encrypted. To keep DbContext clean, we can create a fluent configuration for our PlayerProfile.

public class PlayerProfileConfiguration : IEntityTypeConfiguration<PlayerProfile>
{
    private readonly ICryptographyService _cryptographyService;

    public PlayerProfileFluentConfiguration(ICryptographyService cryptographyService)
    {
        _cryptographyService = cryptographyService;
    }

    public void Configure(EntityTypeBuilder<PlayerProfile> builder)
    {
        var converter = new EncryptedConverter(_cryptographyService);

        builder.HasKey(b => b.Id);

        builder.Property(b => b.Email)
            .IsRequired()
            .HasConversion(converter);

        builder.Property(b => b.RealName)
            .IsRequired()
            .HasConversion(converter);
    }
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to register your configuration in the OnModelCreating method of your DbContext class.

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

    modelBuilder.ApplyConfiguration(new PlayerProfileConfiguration(_cryptographyService));
}
Enter fullscreen mode Exit fullscreen mode

Attribute-based configuration

If you prefer to use attributes to mark properties that need to be encrypted, you can create a custom attribute and use it to decorate the properties in your entity class.

[AttributeUsage(AttributeTargets.Property)]
public sealed class EncryptedAttribute : Attribute
{
}
Enter fullscreen mode Exit fullscreen mode

Next step is to add our newly created attribute to the properties that need to be encrypted.

public record PlayerProfile
{
    [Required]
    public required Guid Id { get; init; }

    [Required]
    [Encrypted]
    public required string Email { get; init; }

    [Required]
    [Encrypted]
    public required string RealName { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Then we can scan all properties in the OnModelCreating method of the DbContext class and apply the EncryptedConverter to those properties that are marked with the EncryptedAttribute.

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

    var stringType = typeof(string);
    var encryptedAttribute = typeof(EncryptedAttribute);
    var converter = new EncryptedConverter(_cryptographyService);

    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var property in entityType.GetProperties())
        {
            if (!IsDiscriminator(property)
                && property.ClrType == stringType
                && property.PropertyInfo is not null)
            {
                var attributes = property.PropertyInfo.GetCustomAttributes(encryptedAttribute, false);

                if (attributes.Length != 0)
                {
                    property.SetValueConverter(converter);
                }
            }
        }
    }

    bool IsDiscriminator(IMutableProperty property) => property.Name == "Discriminator";
}

Enter fullscreen mode Exit fullscreen mode

One gotcha with this approach is ensuring that the scanned property is not a discriminator column. The discriminator column is used by EF Core to store information about the entity type in Table Per Hierarchy (TPH) inheritance scenarios.

Results

Finally, let's see how the conversion of PlayerProfile works!

After inserting a few new records into the database, we can see that the data is stored in an encrypted format.

Id Email RealName
035de6f3-9c58-4c39-abfe-997f172cef0e moc.elpmaxe@divaD senoJ divaD
052d8c82-a05f-4590-b444-8590829ab1e1 moc.elpmaxe@eilrahC smailliW eilrahC
2a10c13e-27e2-4a0f-9e87-f1eed8b15354 moc.elpmaxe@evE nworB evE
9f1eccaa-5574-411e-8b40-38f362b650aa moc.elpmaxe@ecilA htimS ecilA
fcfcd03c-d0d1-4bba-ae24-a97b829ee1b8 moc.elpmaxe@boB nosnhoJ boB

When we retrieve the data from the database, the encrypted values should be automatically decrypted. Let's test this scenario by querying and listing items in our store.

info: Encrypto.DemoService[0]
      Profile:
      Id:       520f79c9-a16e-47ce-9ede-6aeeac857ac4
      Email:    Eve@example.com
      Name:     Eve Brown
info: Encrypto.DemoService[0]
      Profile:
      Id:       669e4809-0bcb-4523-8bc9-54c7fec8b752
      Email:    Bob@example.com
      Name:     Bob Johnson
info: Encrypto.DemoService[0]
      Profile:
      Id:       8b68946a-b490-47df-a9f6-fcc7c481de6a
      Email:    Charlie@example.com
      Name:     Charlie Williams
info: Encrypto.DemoService[0]
      Profile:
      Id:       8c7f41a7-bb6f-4297-b17f-612a6e8881a4
      Email:    Alice@example.com
      Name:     Alice Smith
info: Encrypto.DemoService[0]
      Profile:
      Id:       f21dcba3-40e1-4fa4-9239-e8bbf2532928
      Email:    David@example.com
      Name:     David Jones
Enter fullscreen mode Exit fullscreen mode

Conclusion

Securing sensitive data is crucial for both user privacy and legal compliance. By utilizing value converters in Entity Framework Core, you can efficiently encrypt and decrypt data stored in your database columns with minimal overhead.

Whether you use the built-in DataProtection API or implement your own encryption service, these two approaches are providing a flexible and powerful solution for handling encrypted data at scale.

Further reading

  1. Entity Framework Core Value Conversions
  2. ASP.NET Data Protection

Top comments (1)

Collapse
 
ericjohannsen_ profile image
Eric Johannsen

This will work fine in a small project, but managing keys at scale is non-trivial. I'd prefer to have the database layer handle encryption.