Encrypting Columns in .NET Without Third-Party Dependencies

fabrcio_marcondessantos

Fabrício Marcondes Santos

Posted on August 17, 2024

Encrypting Columns in .NET Without Third-Party Dependencies

Introduction
Imagine you have a vault where you store confidential information. To ensure that no one else has access, you need a secure key that encrypts and decrypts this data. In programming, this concept is implemented using encryption, and ensuring your information is secure is essential.

In my recent .NET project, after upgrading to .NET 8, I realized that the 'EntityFrameworkCore.EncryptColumn' NuGet package was no longer compatible. Instead of being tied to a potentially abandoned package, I decided to create my own encryption solution. I'll share with you how I implemented this functionality to protect sensitive data without relying on third-party packages.

Why Create Your Own Solution?
Relying on third-party packages can be risky, especially if those packages are no longer maintained or regularly updated. Creating your own solution offers several advantages:

  • Total Control: You have complete control over how the data is encrypted and decrypted.

  • Flexibility: You can adjust the implementation as needed without waiting for external updates.

  • Security: Ensures that the libraries and methods used are secure and compatible with the latest version of .NET.

Implementing Column Encryption
Here's how you can implement column encryption in Entity Framework Core using a custom approach:

1. Create the 'EncryptColumnAttribute'
This attribute will be used to mark the properties that need to be encrypted.

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

2. Create the Encryption Converter
The 'EncryptionConverter' converts property values for encryption and decryption.

internal sealed class EncryptionConverter : ValueConverter<string, string>
{
    public EncryptionConverter(IEncryptionProvider encryptionProvider, ConverterMappingHints mappingHints = null) 
        : base(x => encryptionProvider.Encrypt(x), x => encryptionProvider.Decrypt(x), mappingHints)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Extension for Configuring the ModelBuilder
This extension allows you to configure the 'ModelBuilder' to apply encryption to properties marked with the 'EncryptColumn' attribute.

public static class ModelBuilderExtension
{
    public static void UseEncryption(this ModelBuilder modelBuilder, IEncryptionProvider encryptionProvider)
    {
        if (modelBuilder is null)
            throw new ArgumentNullException(nameof(modelBuilder));
        if (encryptionProvider is null)
            throw new ArgumentNullException(nameof(encryptionProvider));

        var encryptionConverter = new EncryptionConverter(encryptionProvider);
        foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (IMutableProperty property in entityType.GetProperties())
            {
                if(property.ClrType == typeof(string) && !IsDiscriminator(property))
                {
                    object[] attributes = property.PropertyInfo.GetCustomAttributes(typeof(EncryptColumnAttribute), false);
                    if(attributes.Any())
                        property.SetValueConverter(encryptionConverter);
                }
            }
        }
    }

    private static bool IsDiscriminator(IMutableProperty property) 
        => property.Name == "Discriminator" || property.PropertyInfo == null;
}
Enter fullscreen mode Exit fullscreen mode
  1. Interface for the Encryption Provider This interface defines the methods for encrypting and decrypting values.
public interface IEncryptionProvider
{
    string Encrypt(string value);
    string Decrypt(string value);
}
Enter fullscreen mode Exit fullscreen mode

5. Implement the Custom Encryption Provider
The 'CustomEncryptionProvider' class implements the 'IEncryptionProvider' interface using the AES algorithm for encryption.

public class CustomEncryptionProvider : IEncryptionProvider
{
    private readonly byte[] _key;
    private readonly byte[] _iv;

    public CustomEncryptionProvider(IOptions<EncryptionOptions> options)
    {
        var key = options.Value.Key;
        if (string.IsNullOrEmpty(key))
        {
            throw new ArgumentNullException(nameof(key));
        }

        _key = Encoding.UTF8.GetBytes(key);
        _iv = new byte[16];
    }

    public string Encrypt(string value)
    {
        if (string.IsNullOrEmpty(value))
            return string.Empty;

        byte[] array; 

        using (Aes aes = Aes.Create())
        {
            aes.Key = _key;
            aes.IV = _iv;

            ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, encryptor, CryptoStreamMode.Write))
                {
                    using (StreamWriter streamWriter = new StreamWriter((Stream)cryptoStream))
                    {
                        streamWriter.Write(value);
                    }
                    array = memoryStream.ToArray();
                }
            }
        }
        return Convert.ToBase64String(array);
    }

    public string Decrypt(string value)
    {
        if (string.IsNullOrEmpty(value))
            return string.Empty;

        using (Aes aes = Aes.Create())
        {
            aes.Key = _key;
            aes.IV = _iv;
            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

            var buffer = Convert.FromBase64String(value);
            using (MemoryStream memoryStream = new MemoryStream(buffer))
            {
                using (CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, decryptor, CryptoStreamMode.Read))
                {
                    using (StreamReader streamReader = new StreamReader((Stream)cryptoStream))
                    {
                        return streamReader.ReadToEnd();
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Configuration in the 'DbContext'
In your 'DbContext', configure the 'ModelBuilder' to use encryption.

public class Context : DbContext
{
    private readonly IConfiguration _config;
    private readonly IEncryptionProvider _provider;

    public Context(IConfiguration config, IEncryptionProvider encryptionProvider)
    {
        _config = config;
        _provider = encryptionProvider;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.UseEncryption(_provider);
    }
}
Enter fullscreen mode Exit fullscreen mode

7. AppSettings and Startup Configuration
Add the encryption settings in 'appsettings.json' and configure the service in 'Startup'.

"Encryption": {
    "Key": "Insert your key here"
}
Enter fullscreen mode Exit fullscreen mode
services.Configure<EncryptionOptions>(Configuration.GetSection("Encryption"));
Enter fullscreen mode Exit fullscreen mode

8. Marking Properties for Encryption
Now you can mark the properties you want to encrypt with the '[EncryptColumn]' attribute.

public class Message
{
    public Guid Id { get; set; }
    [EncryptColumn]
    public string Message { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

Conclusion
By creating your own encryption solution, you no longer rely on third-party libraries that may be outdated or abandoned. This implementation not only solves the compatibility issue with .NET 8 but also strengthens your application's security by avoiding external dependencies.

💖 💪 🙅 🚩
fabrcio_marcondessantos
Fabrício Marcondes Santos

Posted on August 17, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related