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.
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();
}
}
}
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
{
}
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));
}
}
}
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);
}
}
}
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)