🔐 "Encrypt it Like it's Hot!" - Encrypting Marked Columns in .NET Core 🎉
We’ll explore how to mark specific columns for encryption and save them securely using .NET Core Entity Framework. This approach allows sensitive data to be encrypted when stored in the database, ensuring data security with minimal changes to your existing database structure.
For this demonstration, we'll be using Database-First approach, but the same principles apply if you’re using Code-First.
📌 Setting up the User Class
Let’s start with a basic User table. Here, we want to encrypt the PhoneNumber
field for security. When stored in the database, this field will be encrypted, making it unreadable without proper decryption.
public partial class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; } // This will be encrypted.
public bool IsDeleted { get; set; }
public bool? IsAdmin { get; set; }
public virtual ICollection<UserRoles> UserRoles { get; set; }
}
To achieve this, we’ll create a partial class for User
that inherits from a common BaseEntity
. This base class will allow us to handle common properties and changes for all entities. 🌐
🛠 Defining the BaseEntity
The BaseEntity
class provides a common CreatedTime
property for all entities, which isn’t mapped to the database.
using System;
using System.ComponentModel.DataAnnotations.Schema;
namespace Core
{
public abstract class BaseEntity
{
[NotMapped]
public DateTime CreatedTime { get; set; } = DateTime.Now;
}
}
Now, let’s create a partial User
class that inherits from BaseEntity
to keep shared properties consistent.
public partial class User : BaseEntity
{
}
🔒 Marking Properties for Encryption
To indicate which properties should be encrypted, we’ll define a custom attribute named EncryptData
.
using System;
namespace DB.PartialEntities
{
[AttributeUsage(AttributeTargets.Property)]
public class EncryptData : Attribute
{
}
}
Note: We’ll use metadata to avoid compilation errors when applying this attribute to existing columns.
Defining Metadata for User Class
To mark the PhoneNumber
property with [EncryptData]
, we need a metadata class:
public class UserMetaData
{
[EncryptData]
public string PhoneNumber { get; set; }
}
Then, we link the UserMetaData
class with the User
entity to apply the [EncryptData]
attribute on PhoneNumber
:
[MetadataType(typeof(UserMetaData))]
public partial class User : BaseEntity
{
}
💾 Creating a Global Repository
Let’s create a repository pattern to manage our database operations, including encryption for marked fields. The IRepository
interface will define the Insert()
method:
using System.Linq;
namespace Repository
{
public interface IRepository<T> where T : BaseEntity
{
IQueryable<T> Table { get; }
void Insert(T entity);
}
}
Implementing the GeneralRepository
The GeneralRepository
class implements Insert()
to handle encryption for any field marked with [EncryptData]
.
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Core;
using Core.Security;
namespace Repository
{
public class GeneralRepository<T> : IRepository<T> where T : BaseEntity
{
private readonly DbContext _context;
private DbSet<T> _entities;
private readonly IEncryption _encryption;
public GeneralRepository(DbContext context, IEncryption encryption)
{
_context = context;
_entities = context.Set<T>();
_encryption = encryption;
}
public void Insert(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
// Retrieve metadata for any properties marked with [EncryptData]
var metadataTypes = entity.GetType().GetCustomAttributes(true)
.OfType<MetadataTypeAttribute>().ToArray();
foreach (var metadata in metadataTypes)
{
var properties = metadata.MetadataClassType.GetProperties();
foreach (var pi in properties)
{
if (Attribute.IsDefined(pi, typeof(DB.PartialEntities.EncryptData)))
{
var currentValue = _context.Entry(entity).Property(pi.Name).CurrentValue?.ToString();
if (!string.IsNullOrEmpty(currentValue))
{
_context.Entry(entity).Property(pi.Name).CurrentValue = _encryption.EncryptText(currentValue);
}
}
}
}
_entities.Add(entity);
_context.SaveChanges();
}
public IQueryable<T> Table => _entities;
}
}
Here’s a breakdown of the key parts:
Insert(T entity)
: Encrypts properties marked with[EncryptData]
.metadata.MetadataClassType.GetProperties()
: Retrieves properties in the metadata class._encryption.EncryptText(...)
: Encrypts and sets the property’s value before saving.
🔑 Defining the Encryption Interface
Define the methods for encrypting and decrypting in the IEncryption
interface.
namespace Core.Security
{
public interface IEncryption
{
string EncryptText(string text, string key = "");
string DecryptText(string text, string key = "");
}
}
Implementing the Encryption Class
The Encryption
class implements the methods from IEncryption
and performs the actual encryption/decryption.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
namespace Core.Security
{
public class Encryption : IEncryption
{
private readonly IOptions<AppConfig> _config;
public Encryption(IOptions<AppConfig> config)
{
_config = config;
}
public string EncryptText(string text, string key = "")
{
if (string.IsNullOrEmpty(text)) return string.Empty;
key ??= _config.Value.SecretKey;
using var provider = new TripleDESCryptoServiceProvider
{
Key = Encoding.ASCII.GetBytes(key.Substring(0, 16)),
IV = Encoding.ASCII.GetBytes(key.Substring(8, 8))
};
var data = Encoding.Unicode.GetBytes(text);
using var ms = new MemoryStream();
using var cs = new CryptoStream(ms, provider.CreateEncryptor(), CryptoStreamMode.Write);
cs.Write(data, 0, data.Length);
cs.FlushFinalBlock();
return Convert.ToBase64String(ms.ToArray());
}
public string DecryptText(string text, string key = "")
{
if (string.IsNullOrEmpty(text)) return string.Empty;
key ??= _config.Value.SecretKey;
var buffer = Convert.FromBase64String(text);
using var provider = new TripleDESCryptoServiceProvider
{
Key = Encoding.ASCII.GetBytes(key.Substring(0, 16)),
IV = Encoding.ASCII.GetBytes(key.Substring(8, 8))
};
using var ms = new MemoryStream(buffer);
using var cs = new CryptoStream(ms, provider.CreateDecryptor(), CryptoStreamMode.Read);
using var sr = new StreamReader(cs, Encoding.Unicode);
return sr.ReadToEnd();
}
}
}
The Encryption
class handles encryption with EncryptText()
and decryption with DecryptText()
, using TripleDES for security.
🎉 Conclusion
In this article, we implemented encrypted saving for specified columns using Entity Framework. By marking properties with [EncryptData]
, we ensured sensitive data like phone numbers are stored securely. The repository layer manages encryption seamlessly, allowing any marked columns to be automatically encrypted before saving.
This approach helps secure sensitive data in any project, offering flexibility and security with minimal changes to your codebase.