DEV Community

Cover image for Encryption at rest using EF
Csaba Boros
Csaba Boros

Posted on • Originally published at boroscsaba.com

Encryption at rest using EF

Encryption at rest can come in handy when we have some sensitive data that we need to store in our database and we don't want to store it in plain text for everyone to see.

In my case I needed to store some API keys in the database. The keys are inputted by the app users and the app later uses these keys for accessing some external services.

In this tutorial I will show you a seamless way to use encryption at rest in your .NET application using EF value converters.

How it will work

First we will create a ValueConverter that encrypts values on write and decrypts on read. We will also create an attribute, EncryptedAttribute, and apply the value converter to each of the properties that has the EncryptedAttribute.

Image description

The value converter class

We will need a string-string value converter that can encrypt on write and decrypt on read. For this we will use AES encryption and we will need an encryption key. This key is a simple base64 string but we need to store it securely (in Azure Key Vault for example). I won't go in detail here, but we well also need a unique IV (initialization vector) key that will basically ensure that we always get different ciphers even if the input string is the same. Because the IV value is also needed when we decrypt the data we will just append the IV value to the cipher and store it in the database as part of the encrypted value. Here is the source code of my value converter:

using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System.Security.Cryptography;
using System.Text;

namespace EncryptionAtRest.Database.Encryption
{
  public class EncryptedConverter : ValueConverter<string, string>
  {

    public EncryptedConverter(string encryptionSecretKey)
    : base(
        v => Encrypt(v, encryptionSecretKey),
        v => Decrypt(v, encryptionSecretKey))
    { }

    private static string Encrypt(string inputString, string encryptionSecretKey)
    {
      using var aes = Aes.Create();
      using var encryptor = aes.CreateEncryptor(Encoding.UTF8.GetBytes(encryptionSecretKey), aes.IV);
      using var memoryStream = new MemoryStream();
      using var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);
      using (var streamWriter = new StreamWriter(cryptoStream))
      {
        streamWriter.Write(inputString);
      }

      var cipher = memoryStream.ToArray();
      // append the IV to the start of the cipher
      var result = new byte[aes.IV.Length + cipher.Length];
      Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
      Buffer.BlockCopy(cipher, 0, result, aes.IV.Length, cipher.Length);

      return Convert.ToBase64String(result);
    }

    private static string Decrypt(string cipherText, string encryptionSecretKey)
    {
      using var aes = Aes.Create();

      var cipherByteArray = Convert.FromBase64String(cipherText);
      var iv = new byte[aes.IV.Length];
      var cipher = new byte[cipherByteArray.Length - aes.IV.Length];
      // get the IV from the start of the cipher
      Buffer.BlockCopy(cipherByteArray, 0, iv, 0, iv.Length);
      Buffer.BlockCopy(cipherByteArray, iv.Length, cipher, 0, cipher.Length);

      using var decryptor = aes.CreateDecryptor(Encoding.UTF8.GetBytes(encryptionSecretKey), iv);
      using var memoryStream = new MemoryStream(cipher);
      using var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);
      using var streamReader = new StreamReader(cryptoStream);

      return streamReader.ReadToEnd();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Encrypted attribute

We will create an attribute and write some logic to apply the converter to the properties that use this attribute.
The source code for the attribute:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class EncryptedAttribute : Attribute
{
}
Enter fullscreen mode Exit fullscreen mode

And here is the code that will apply the converter based on the attribute. I will place this static method inside my converter:

public static void Apply(ModelBuilder builder, string encryptionSecretKey)
{
    var entityTypes = builder.Model.GetEntityTypes();
    var properties = entityTypes.SelectMany(entity => entity.GetProperties());

    foreach (var property in properties)
    {
        var attributes = property.PropertyInfo?.GetCustomAttributes(typeof(EncryptedAttribute), true);
        if (attributes != null && attributes.Any())
        {
            property.SetValueConverter(new EncryptedConverter(encryptionSecretKey));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And the final step is to call the EncryptedConverter.Apply function from the OnModelCreating method of our db context:

using EncryptionAtRest.Database.Encryption;
using EncryptionAtRest.Database.Models;
using Microsoft.EntityFrameworkCore;

namespace EncryptionAtRest.Database
{
  public class AppDbContext : DbContext
  {
    public DbSet<Api> Apis { get; set; } = null!;
    private readonly string _encryptionSecretKey;

    public AppDbContext(string connectionString, string encryptionSecretKey)
      : base(new DbContextOptionsBuilder().UseNpgsql(connectionString).Options)
    {
      _encryptionSecretKey = encryptionSecretKey ?? throw new ArgumentNullException(nameof(encryptionSecretKey));
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
      base.OnModelCreating(modelBuilder);
      EncryptedConverter.Apply(modelBuilder, _encryptionSecretKey);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Ready to go

Now we just need to add the [Encrypted] attribute to the properties we want to store in an encrypted form. Your DbContext will work as usually, you can write and read your plain text properties but in the underlaying database table the data will be encrypted.

You can also find the source code and a working example in this repository: https://github.com/boros-csaba/encryption-at-rest-with-property-attribute

Top comments (0)