Encryption and Data Security in Clean Architecture using EF Core Value Converters: A Guide to Database Encryption Implementation

Gor Grigoryan
10 min readAug 7, 2023

If you are already familiar with data encryption and are looking for implementation with EF core, you can proceed directly to EF core value converters.

Table of content:

· Overview
· What is Data Encryption?
Comparative analyses between Historical and Modern Data Encryption Techniques
Example of usage
How to hack the encryption?
· EF core value converters:
Problem
Implementation
· Set up with 2 different approaches:
1. Utilizing EF Core Model Configuration
2. Implementing Attribute-based Encryption
In-action
· Conclusion:
GitHub sample project

Overview:

In the rapidly progressing world of data-driven applications, ensuring the security and integrity of sensitive information is one of the essential things. Clean Architecture has turned up as a robust and scalable software design approach, providing a structured framework for building maintainable, adaptable, well-organized, and, most important, scaleable applications. However, as more data is shared and stored, the importance of solid data security increases.

What is Data Encryption?

Data encryption is a method of securing sensitive information by converting it into a coded or unreadable format. This process ensures that even if someone gains unauthorized access to the data, they won’t be able to understand it without the proper decryption key / password.

When data is encrypted, it is transformed using a mathematical algorithm and an encryption key. The resulting encrypted data, known as ciphertext, appears as non-readable characters or symbols and to access the original information, the recipient must have the correct decryption key to convert the ciphertext back into its original readable form, known as plaintext.

Comparative analyses between Historical and Modern Data Encryption Techniques:

Just as Julius tries to protect his battle plans from enemy interception, the need for secure communication emerged in the digital age as technology advanced and data started traveling through electronic channels.

Caesar cipher

Early pioneers, much like Julius, developed algorithms and methods to transform data into secret codes that could only be deciphered using specific keys. Their most important objectives were:

  1. Quick encryption: Making sure that turning information into secret code happened fast and efficiently.
  2. Strong Protection: Creating a strong defense against people who shouldn’t read the secret codes. Only those with the right key could change the secret code back to understandable information. If someone tried to do it without the key, it would take a long time and be really hard.

Example of usage:

Let’s say we need to encrypt the text: “Hello World”. First, we need to pick an encryption key (the key that will be used to encrypt and decrypt given text). For instance, let’s use the key “1234” (although in real situations, we would opt for a randomly generated, highly secure key like1^n128A$jS3P@u”). If we put “1234” as an encryption key in the production environment, then hackers can guess the password using hacking techniques such as brute-forcing. This key acts as a special lock that only those who know the key can use to access and reveal the hidden message.

So in our example if we encrypt text “Hello World” we will get this text “oO64D2IzNWKSQnDM8fcZ/w==”. To see the power of encryption, let’s also encrypt variations of the text: “HelloWorld” (without a space) and “Hello world” (with lowercase), while also experimenting with a different encryption key. Here are the outcomes:



╔═════════════╦═══════════╦══════════════════════════╗
║ Text ║ Password ║ Encoded value ║
╠═════════════╬═══════════╬══════════════════════════╣
║ Hello World ║ 1234 ║ oO64D2IzNWKSQnDM8fcZ/w== ║
╠─────────────╬───────────╬──────────────────────────╣
║ HelloWorld ║ 1234 ║ KvqAEHQhP9iBdFWhOUcYVg== ║
╠─────────────╬───────────╬──────────────────────────╣
║ Hello world ║ 1234 ║ jdKRaAw9ULCFb627e3mNpQ== ║
╠─────────────╬───────────╬──────────────────────────╣
║ Hello World ║ 123 ║ S/eGTyDQsgLwcEIrCWUAJw== ║
╠─────────────╬───────────╬──────────────────────────╣
║ HelloWorld ║ 123 ║ /JRa5+mllydL/F0m7NuxYA== ║
╠─────────────╬───────────╬──────────────────────────╣
║ Hello world ║ 123 ║ s3AydwlvlgHCcpiAhaurXg== ║
╚═════════════╩═══════════╩══════════════════════════╝

Observing the table, you’ll notice that even a small change, such as a change in spacing or a single character, leads to a complete transformation of the encrypted text. It means that if the intruder manages to obtain both the original text and its encrypted form, they would still face a significant challenge in trying to guess the password required to unlock the entire database.

How to hack the encryption?

Suppose you own an exceptionally powerful supercomputer, coupled with cutting-edge technology and virtually unlimited resources. Let’s say the computer has a whopping 1 terabyte (TB) of RAM allowing it to handle lots of tasks at once. For the CPU, this supercomputer boasts a mind-boggling speed of 1 exaflop, which means it can do about 1 quintillion calculations in just one second. 1 exaflop is equal to 1,000,000 gigaflops. So, to achieve 1 exaflop of computing power using Intel i9 processors with a performance of 300 gigaflops each, you would need 1,000,000 gigaflops / 300 gigaflops = 3,333,333 Intel i9 processors. This hypothetical supercomputer, performing mind-blowing calculations at lightning speed, could do a brute-force attack on an encryption algorithm.

If our hypothetical supercomputer were to attempt every possible combination of text to decipher the encrypted data, it would be faced with an astronomical number of possibilities — 2²⁵⁶. It’s estimated that it would take not just years, not even centuries, but potentially tens of thousands of decades.

EF core value converters

In the EF Core, data encryption can be achieved using value converters. EF Core Value Converters offer an ordered way to integrate encryption mechanisms into your clean architecture.

Here is the raw explanation of how EF core value converters work.

How does the value converter work?

Problem:

Of course, we can have encrypt and decrypt functions that can be called each time we need to encrypt or decrypt data, like so:

//Creating
var entity = new Entitiy(id: Guid.NewGuid(),
encrptedText: EncryptionHelper.Encrypt("Hello World", "1234"));

//Getting
var entity = context.Entities.First(x => ...);
var data = EncryptionHelper.Decrypt(entity.EncrptedText, "1234");

However, consider the scenario where the same encryption or decryption process is implemented in numerous locations. Automatically, the risk of forgetting to perform decryption or encryption in a single place becomes a concern, potentially leading to unplanned data leaks. Here comes the clean architecture. Our goal is to integrate the encryption logic seamlessly within the project so that even developers working on it can remain unaware of its existence. It will be something like this:

//Creating
var entity = new Entitiy(id: Guid.NewGuid(), encrptedText: "Hello World");

//Getting
var entity = context.Entities.First(x => ...);
var data = entity.EncrptedText;

Implementation:

Let’s proceed with the practical implementation by starting with defining encryptor and decryptor methods:

In this example, we are going to use Advanced Encryption Standard (AES) as a symmetric encryption algorithm. For .Net will use AES implementation from System.Security.Cryptography namespace.

EncriptionHelper.cs

Within this class, we define three methods:

  1. Encrypt: Converts the given text into an encrypted form, here is an explanation by row numbers:
    (7 — 12) Key and InputData Validation: Checks if Keyis provided, if not throws an exception
    (16 — 20) AES Setup: An instance of the AES algorithm (aesAlg) is created. The encryption key is set to the UTF-8 encoded bytes of the Key. The algorithm's mode is set to Electronic Codebook (ECB), and the padding mode is set to PKCS7. For more information regarding configurations see here.
    (22) Encryptor Creation: An encryptor is created using the AES algorithm’s key and vector (IV). The initialization vector is automatically generated by the AES algorithm and is used to introduce randomness into the encryption process.
    (24–30) Data Encryption: The encryption process starts by creating a MemoryStream (msEncrypt) to hold the encrypted data. A CryptoStream (csEncrypt) is then set up, which writes the encrypted data to the MemoryStream using the created encryptor.
    (33) Base64 Encoding: The encrypted byte array is then converted to a Base64-encoded string using Convert.ToBase64String to ensure that it can be safely represented as text.
  2. Decrypt: Reverts encrypted text back to its original form, here is an explanation by row numbers:
    (38–43) Key and InputData Validation: Checks if Keyis provided, if not throws an exception
    (45–18) AES Setup: An instance of the AES algorithm (aes) is created. The encryption key and the algorithm's mode are the same as those used during encryption.
    (50) Decryptor Creation: A decryptor (descriptor) is created using the AES algorithm's key and initialization vector (IV). The initialization vector must match the one used during encryption to ensure proper decryption.
    (52–55) Decrypt and Read Data: The encrypted data (dataToDecrypt) is Base64-decoded into a byte array. This byte array is then used to initialize a MemoryStream (memoryStream), which becomes the source for decryption. A CryptoStream (cryptoStream) is created to read from the memoryStream and decrypt the data using the provided decryptor.
    (57) Return Decrypted Data: The original plaintext data is read from the streamReader and returned as the result of the method.
  3. SetEncryptionKey: Initializes and sets the encryption key within the static Key variable

The third method, SetEncryptionKey, is called once to establish the encryption key (password).

Startup.cs / Program.cs

EncriptionHelper.SetEncriptionKey("1234");

To proceed, we move on to the creation of value converters. This involves crafting a class that inherits from EF Core’s generic ValueConverter class. Generic arguments of ValueConverter are source and destination types, in our case string (input value type), string (encrypted value type)

EncryptionConvertor.cs

public class EncryptionConvertor : ValueConverter<string, string>
{
public EncryptionConvertor(ConverterMappingHints mappingHints = null)
: base(x => EncryptionExtension.Encrypt(x), x => EncryptionExtension.Decrypt(x), mappingHints)
{ }
}

The ValueConverter base class necessitates the implementation of a constructor with three key arguments:

  1. convertToProviderExpression: An expression to convert the input text to the destination.
x => EncryptionExtension.Encrypt(x)

2. convertFromProviderExpression: An expression to convert the text back to the source.

x => EncryptionExtension.Decrypt(x)

3. mappingHints: Hints that are used by the type mapper during the mapping process. (We will not use it in our scenario but details can be found here.)

Set-up with 2 different approaches:

Let’s conclude by configuring the properties that require encryption. There are two approaches to achieve this:

1. Utilizing EF Core Model Configuration:

AppDbContext.cs

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var entityBuilder = modelBuilder.Entity<Entity>();

entityBuilder.Property(x => x.SensitiveInformation)
.HasConversion<EncryptionConvertor>(); // <---
}

In your AppDbContext.cs, within the OnModelCreating method, you can easily set up the encryption for specific properties using the EF Core model configuration.

2. Implementing Attribute-based Encryption:

For a more intricate but flexible approach, we can use attributes to mark properties requiring encryption. Here’s how it’s done:

EncryptPropertyAttribute.cs

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class EncryptPropertyAttribute : Attribute
{

}

We’ve introduced an EncryptProperty attribute, which can be applied only to properties, isn't allowed to be used multiple times on one property and is not inherited.

ModelPropertyEncrypterExtension.cs

public static class ModelPropertyEncrypterExtension
{
public static void UseEncryption(this ModelBuilder modelBuilder)
{
// Instantiate the EncryptionConverter
var converter = new EncryptionConvertor();

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(string) && !IsDiscriminator(property))
{
var attributes = property.PropertyInfo?.GetCustomAttributes(typeof(EncryptPropertyAttribute), false);
if (attributes != null && attributes.Any())
{
property.SetValueConverter(converter); // <---
}
}
}
}
}

// A helper function to ignore EF Core Discriminator
private static bool IsDiscriminator(IMutableProperty property)
{
return property.Name == "Discriminator" || property.PropertyInfo == null;
}
}

The UseEncryption method is an extension method on the modelBuilder. It iterates through all entities and identifies string properties marked with the EncryptProperty attribute, and applies the EncryptionConverter using property.SetValueConverter(converter).

Then we also need to call the defined extension method in AppDbContext to apply the value converter to all marked properties.

AppDbContext.cs

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseEncryption();
}

Then we can mark all needed properties EncryptProperty to encrypt them.

Model.cs

...

[EncryptProperty]
public string SensitiveInformation { get; private set; }

...

In-action:

This code snippet illustrates the process of creating and retrieving 50 entities from the database.

//Create test data
for (int i = 0; i < 50; i++)
{
var entity = new Entity(sensitiveInformation: $"Sensitive data No {i}");

context.Entities.Add(entity);
}

//save entities
await context.SaveChangesAsync();


//Get created data
var entitiesFromDb = await context.Entities.ToListAsync();

foreach (var entity in entitiesFromDb)
{
Console.WriteLine(entity.SensitiveInformation);
}

When we run it we will see this information:

Running code snippet

So, what have we done here? We created entities and got them back with their properties - nothing special, right? But when we look at the table, we will see this:

Get all entities from Database

As we can see SensitiveInformation column data is encrypted everywhere.

Here is a sequence diagram of how the value converter “middleware” works in our scenario:

Sequence diagram of the value converters

And the simplified version:

Simplified diagram

Conclusion:

The implementation of EF Core value converters within the framework of clean architecture serves as a crucial strategy to ensure the security of sensitive data. By consistently integrating these converters, we create a robust safeguard against potential vulnerabilities, ensuring data protection while maintaining an organized and scalable application structure. As data sharing and storage continue to grow our applications stand resilient in the face of modern challenges.

GitHub sample project:

The whole project can be found on GitHub.

--

--