I have a requirement to store some information in the database encrypted (not hashed, and I'm not talking about passwords here), but still be able to read that information in memory to validate certain scenarios (hence the encrypting, not hashing).
I want a clean easy way to identify which columns should be stored as encrypted in the database because they may change in the future. With that said, I was wondering if the following approach would work with the ApplicationDbContext provided by IdentityProvider in MVC5 and Entity Framework 6. Would there be any caveats to watch out for? Or is this even a good approach to the Idea? If not, any guidance would be appreciated.
- To define columns (code first) I created an attribute called "StoreSecurelyAttribute" that can be applied to different properties in the Code-First model.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class StoreSecurelyAttribute : Attribute { }
- I can then apply this attribute to any column that needs to be stored securely (only strings supported for now).
public class UserProfileInfo
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
[Required, StoreSecurely]
public string SomePersonalInformation { get; set; }
}
- Then in my ApplicationDbContext Constructor I add the following:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext() : base("DefaultConnection", false)
{
var ctx = ((IObjectContextAdapter) this).ObjectContext;
ctx.ObjectMaterialized += OnObjectMaterialized;
ctx.SavingChanges += OnSavingChanges;
}
void OnSavingChanges(object sender, EventArgs e)
{
foreach (var entry in ((ObjectContext) sender).ObjectStateManager.GetObjectStateEntries(EntityState.Added |
EntityState.Modified))
{
foreach (var propInfo in entry.Entity.GetType().GetProperties()
.Where(prop => prop.PropertyType == typeof(string) && Attribute.IsDefined(prop, typeof(StoreSecurelyAttribute))))
{
var plainTextValue = propInfo.GetValue(entry.Entity) as string;
// TODO: encrypt using injected encryption provider
var encryptedValue = Encrypt(plainTextValue);
propInfo.SetValue(entry.Entity, encryptedValue);
}
}
}
void OnObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
{
foreach (var propInfo in e.Entity.GetType().GetProperties()
.Where(prop => prop.PropertyType == typeof(string) && Attribute.IsDefined(prop, typeof(StoreSecurelyAttribute))))
{
var encryptedValue = propInfo.GetValue(e.Entity) as string;
// TODO: decrypt using injected encryption provider
var plainTextValue = Decrypt(encryptedValue);
propInfo.SetValue(e.Entity, plainTextValue);
}
}
public override int SaveChanges()
{
// Hold onto them before their state changes and they're no longer "added" or "modified"
var entries = ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified).ToList();
// Go Ahead and save...
var result = base.SaveChanges();
// After saving to db, we want our local hydrated object to be "correct" so... decrypt...
foreach (var entry in entries)
{
foreach (var propInfo in entry.Entity.GetType().GetProperties()
.Where(prop => prop.PropertyType == typeof(string) && Attribute.IsDefined(prop, typeof(StoreSecurelyAttribute))))
{
var encryptedValue = propInfo.GetValue(entry.Entity) as string;
var plainTextValue = Decrypt(encryptedValue);
propInfo.SetValue(entry.Entity, plainTextValue);
}
}
return result;
}
// ... snip ...
}
Update: I know this is a lot of reflection and could be slow -- but we're not going to get too crazy on encrypting and decrypting fields and the ones we do encrypt, are not hit "on every request."