Simple string encryption for the EPiServer Dynamic Data Store

This is part of the short dynamic data store article series I mentioned before. The layer mentioned may be found in Simple EPiServer Dynamic Data Store (DDS) layer and was mainly created to ease unit testing of DDS code.

Since there may be a need to store sensitive information in the DDS I have extended this DdsService with encryption functionality (the actual encryption is not described here).

Automatically encrypt/decrypt strings working with EPiServer’s Dynamic Data Store

So first we need a way of telling our service which properties it should encrypt when writing data, and which will be needed to decrypt when retrieving it. For this I created a simple Attribute with property target.

RequireEncryptionAttribute.cs

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

Note that this attribute will only support string types, if you use it on properties with other return types it will be ignored. This is because we will be storing the encrypted value as a string in the same property as we are encrypting.

If we wanted to be able to encrypt other types, like for instance int or MyObject, it would probably be possible to keep all the property types in the entity as strings, and then add a true type sort of setting to the attribute. But that would require a bit more reflection and type casting, and since it was out of my scope, it has been omitted.

The usage of the attribute will be as in the example Dynamic Data Store Entity class below.

public class EncryptTestDdsEntity : BaseData
{
  [RequireEncryption]
  public string WillBeEncrypted01 { get; set; }
    
  // Will not be encrypted since non string properties are ignored.
  [RequireEncryption]
  public int WillNotBeEncryptedInt { get; set; }
    
  [RequireEncryption]
  public String WillBeEncrypted02 { get; set; }
    
  // Will not be encrypted since it lacks the attribute.
  public string WillNotBeEncryptedString { get; set; }
}

So the actual service class then, first let’s have a look at the support methods and fields. I have removed all logging to make the code less noisy.

EncryptedDdsService.cs

public class EncryptedDdsService<T> : DdsService<T> where T : BaseData
{
  private readonly IEncryptionService _encryptionService;

  public EncryptedDdsService(IEncryptionService encryptionService)
  {
    _encryptionService = encryptionService ?? throw new ArgumentNullException(nameof(encryptionService));
  }
  private readonly PropertyInfo[] _properties = typeof(T).GetProperties()
        .Where(p => Attribute.IsDefined(p, typeof(RequireEncryptionAttribute)))
        .Where(p => typeof(string).IsAssignableFrom(p.PropertyType))
        .ToArray();
  
  private bool RequireEncyption(string propertyName)
  { 
    return _properties.Any(p => p.Name.Equals(propertyName));
  }

The first two things we need is a list of all the relevant PropertyInfo objects (the properties that will need our attention), and an easy way of finding out if a named property is supposed to be encrypted (used for Find later on).

To create the PropertyInfo array, just get all the PropertyInfos from the Type in question and keep all those that have the attribute, and return strings. The RequireEncryption method just checks to see if there is a PropertyInfo with the proper name in this array.

So how do we encrypt and decrypt our data then? I have created two methods for this.

private T EncryptValuesIn(T item)
{
  if (_properties.Length <= 0)
  {
    return item;
  }
  foreach (var info in _properties)
  {
    var clear = info.GetValue(item);
    if (clear == null)
    {
      continue;
    }
    var encrypted = _encryptionService.Encrypt(clear.ToString());
    info.SetValue(item, encrypted);
  }
  return item;
}

private T DecryptValuesIn(T item)
{
  if (_properties.Length <= 0)
  {
    return item;
  }
  foreach (var info in _properties)
  {
    var encrypted = info.GetValue(item);
    if (encrypted == null) 
    {
      continue;
    }
    if (!_encryptionService.TryDecrypt(encrypted.ToString(), out var clear))
    {
      continue;
    }
    info.SetValue(item, clear);
  }
  return item;
}

The methods are rather similar in how they work. They both takes an entity object (T item) and updates its values with either the encrypted version or the clear string, depending on method. The methods will both skip properties that are not supposed to be handled as per the attribute and string rules described above.

Using the encryption and decryption methods when getting data from, or writing data to the Episerver DDS is quite easy. It’s just a matter of passing the entity object to either method before or after it passes through the real DdsService.

public override Guid Save(T item)
{
  item = EncryptValuesIn(item);
  return base.Save(item);
}

public override T Load(Guid id)
{
  var item = base.Load(id);
  DecryptValuesIn(item);
  return item;
}

public override IOrderedQueryable GetAll()
{
  return base.GetAll().Select(i => DecryptValuesIn(i)).OrderBy(i => 0);
}

public override T[] LoadAll()
{
  var items = base.LoadAll();
  foreach (var item in items)
  {
    DecryptValuesIn(item);
  }
  return items;
}

The only place where you would really need to do anything different is in the implementation of the Find method. In this case you have a propertyName and the clear text value (the decrypted one) as parameters into the method. You will not be able to search the Episerver DDS with this value since it would try to match a decrypted string with it’s encrypted match.

public override T[] Find(string propertyName, object value)
{
  if (RequireEncyption(propertyName))
  {
    value = _encryptionService.Encrypt(value.ToString());
  }
  var items = base.Find(propertyName, value);
  foreach (var item in items)
  {
    DecryptValuesIn(item);
  }
  return items;
}

This is where the RequireEncryption method that we created earlier comes into play. Simply pass the property name to it to find out if we need to do any work, and if we do, just encrypt the value before using it to find the Episerver DDS Entity.

You may have noticed that this Find only takes a single property into account, but it’s possible to pass an entire dictionary with criteria. The way you’d implement that follows the same pattern.

Note that if you’re moving a database between environments, such as getting a fresh one from production to stage, in order to have reasonable test data, then you would need to consider the encryption as well. Will the staging environment be able to decrypt production values? Should it?