November Happy Hour will be moved to Thursday December 5th.

Daniel Ovaska
Jul 13, 2016
  5073
(5 votes)

Episerver Forms–Encrypting submitted data

Background

A nice question that popped up in the forum a couple of days ago is how to encrypt the submitted data in Episerver forms. If you collect sensitive information about your users by using forms, this is definitely a relevant question. You don’t want to be the one who loses your customers personal emails, credit card information etc if someone gets hold of your database. It has happend to plenty of websites unfortunately and it doesn’t build trust with your end users so to speak...

Encrypting the submitted data can be done in a couple of ways and I’ll demonstrate one: extending the default implementation of IPermanentStorage. This can be done easily nowadays (CMS 7+) due to Episerver nifty SOLID architecture. Fun challenge! Let's code!

Step one - Select what elements should be encrypted

First let's create a new attribute to mark what elements we want encrypted. Another option is to use some naming convention or lookup table. I'll go with the attribute this time and create a custom element for it. Let's create a simple TextBox element that uses the attribute.

[ContentType(GUID = "95A98808-4BFC-43FE-B723-4FD2F7E01234")]
[EncryptedFormsElement]
public class EncryptedTextBoxElementBlock : TextboxElementBlock
{
}

public class EncryptedFormsElementAttribute : Attribute
{

}

Now we need a view called EncryptedTextBoxElementBlock.cshtml for the textbox as well to render it.

@using EPiServer.Forms.Core
@using EPiServerSiteV9.Business
@model EPiServerSiteV9.Business.EncryptedTextBoxElementBlock

<div class="Form__Element FormTextbox" data-epiforms-element-name="@Model.FormElement.ElementName">
    <label for="@Model.FormElement.Guid">
        @{
            var label = Model.FormElement.SourceContent.Property["Label"];
        }
        @label
    </label>
    <input type="text" name="@Model.FormElement.ElementName" class="FormTextbox__Input" id="@Model.FormElement.Guid" />


    <span data-epiforms-linked-name="@Model.FormElement.ElementName" class="Form__Element__ValidationError" style="display: none;">*</span>
</div>

Step two - Extend the default implementation of IPermanentStorage

Let's use the default implementation, DdsPermanentStorage, and inherit from that and then extend it with some encryption. This class already takes care of storing everything in DDS for us. Our new implementation uses a simple CryptographyService under the hood to do the heavy lifting with encryption. We need to override both the storing method and the one that gets the data to handle the encryption/decryption. Otherwise editors will find it somewhat problematic to read submissions if we only store it encrypted. The new class also checks whether an element is decorated with the new attribute before it does it's thing.

public class EncryptedFormsDataStorage : DdsPermanentStorage
{
    private readonly ILogger _log = LogManager.GetLogger(typeof(EncryptedFormsDataStorage));
    private readonly ICryptographyService _cryptographyService;
    private Injected<EPiServer.Forms.Core.Data.Internal.DdsStructureStorage> _formStructureStorage;
    private Injected<IContentLoader> _contentLoader;

    public EncryptedFormsDataStorage(ICryptographyService cryptographyService)
    {
        _cryptographyService = cryptographyService;
    }
    public override Guid SaveToStorage(FormIdentity formIden, Submission submission)
    {
        var form = formIden.GetFormBlock();
        var elements = form.ElementsArea.FilteredItems;
        foreach (var contentAreaItem in elements)
        {
            var elementblock = _contentLoader.Service.Get<ElementBlockBase>(contentAreaItem.ContentLink);
            if (ShouldEncryptElement(elementblock))
            {
                var oldText = submission.Data[elementblock.FormElement.ElementName];
                if (oldText != null)
                {
                    submission.Data.Remove(elementblock.FormElement.ElementName);
                    var encryptedText = _cryptographyService.Encrypt(oldText.ToString());
                    submission.Data.Add(elementblock.FormElement.ElementName, encryptedText);
                }
            }
        }
        return base.SaveToStorage(formIden, submission);

    }
    public override IEnumerable<PropertyBag> LoadSubmissionFromStorage(FormIdentity formIden, DateTime beginDate, DateTime endDate, bool finalizeOnly = false)
    {
        try
        {
            var propertyBags = base.LoadSubmissionFromStorage(formIden, beginDate, endDate, finalizeOnly);
            var form = formIden.GetFormBlock();
            var elements = form.ElementsArea.FilteredItems;
            foreach (var contentAreaItem in elements)
            {
                var elementblock = _contentLoader.Service.Get<ElementBlockBase>(contentAreaItem.ContentLink);
                if (ShouldEncryptElement(elementblock))
                {
                    foreach (var propertyBag in propertyBags)
                    {
                        var oldValue = propertyBag[elementblock.FormElement.ElementName];
                        if (oldValue != null)
                        {
                            string newValue;
                            if (_cryptographyService.TryDecrypt(oldValue.ToString(), out newValue))
                            {
                                _log.Information("Decrypted text");
                                propertyBag.Remove(elementblock.FormElement.ElementName);
                                propertyBag.Add(elementblock.FormElement.ElementName, newValue);
                            }
                            else
                            {
                                _log.Error("Failed to decrypt element text");
                            }

                        }

                    }
                }
            }
            return propertyBags;
        }
        catch (Exception ex)
        {
            _log.Error("Error while decrypting", ex);
            throw;
        }
    }
    private bool ShouldEncryptElement(ElementBlockBase element)
    {
        var encryptionAttribute = Attribute.GetCustomAttribute(element.GetType(),
        typeof(EncryptedFormsElementAttribute));
        if (encryptionAttribute == null)
        {
            _log.Information("Element lacks attribute for encryption");
        }
        else
        {
            _log.Information("Element has attribute for encryption");
            return true;
        }
        return false;
    }
}

Step three - Register your implementation

We now need to tell Episerver to use our new implementation of storage for forms. This is done by configuring the structuremap container.

    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    public class DependencyResolverInitialization : IConfigurableModule
    {
        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            context.Container.Configure(ConfigureContainer);
            DependencyResolver.SetResolver(new StructureMapDependencyResolver(context.Container));
        }

        private static void ConfigureContainer(ConfigurationExpression container)
        {
            container.For<ICryptographyService>().Use<CryptographyService>();
            container.For<IPermanentStorage>().Use<EncryptedFormsDataStorage>();
            ...

Appendix

To wrap it up I'll also include my CryptographyService class. Feel free to change it to use your favorite algorithm.

public interface ICryptographyService
{
    /// <summary>
    /// Array for password salt
    /// </summary>
    byte[] PasswordKeyByteArray { get; set; }

    /// <summary>
    /// Set this to a tough to break string before trying to encrypt
    /// </summary>
    string CryptoKey { get; set; }

    byte[] Encrypt(byte[] clearData, byte[] key, byte[] iv);
    string Encrypt(string clearText);

    /// <summary>
    /// Given wrong key this may cast an exception of type CryptographicException with message "Padding is invalid and cannot be removed"
    /// Decrypt a byte array into a byte array using a key and an IV
    /// Uses Decrypt(byte[], byte[], byte[])
    /// </summary>
    /// <returns></returns>
    byte[] Decrypt(byte[] cipherData,
        byte[] key, byte[] iv);

    /// <summary>
    /// Given wrong key this may cast an exception of type CryptographicException with message "Padding is invalid and cannot be removed"
    /// Decrypt a string into a string using a password
    /// Uses Decrypt(byte[], byte[], byte[])
    /// </summary>
    /// <param name="cipherText"></param>
    /// <returns></returns>
    bool TryDecrypt(string cipherText, out string result);
}

/// <summary>
/// Remember to set crypto key before trying to encrypt / decrypt anything.
/// </summary>
public class CryptographyService : ICryptographyService
{
    public byte[] PasswordKeyByteArray { get; set; }
    public string CryptoKey { get; set; }

    public CryptographyService()
    {
        CryptoKey = "ASDLKFJALSDKFJAKDFJ";
        PasswordKeyByteArray = new byte[] {0x4b, 0x49, 0xa1, 0x6e, 0x11, 0x4d,
        0x58, 0x45, 0x76, 0x61, 0x62, 0x45, 0xa5};
    }
    //Encrypt a byte array into a byte array using a key and an IV
    public byte[] Encrypt(byte[] clearData, byte[] key, byte[] iv)
    {
        // Create a MemoryStream to accept the encrypted bytes
        var ms = new MemoryStream();
        var alg = Rijndael.Create();
        alg.Key = key;
        alg.IV = iv;
        var cs = new CryptoStream(ms,
        alg.CreateEncryptor(), CryptoStreamMode.Write);
        cs.Write(clearData, 0, clearData.Length);
        cs.Close();
        byte[] encryptedData = ms.ToArray();
        return encryptedData;
    }

    public string Encrypt(string clearText)
    {
        if (string.IsNullOrEmpty(clearText))
        {
            return clearText;
        }
        byte[] clearBytes = Encoding.Unicode.GetBytes(clearText);
        var pdb = new Rfc2898DeriveBytes(CryptoKey, PasswordKeyByteArray);
        byte[] encryptedData = Encrypt(clearBytes, pdb.GetBytes(32), pdb.GetBytes(16));
        return Convert.ToBase64String(encryptedData);

    }
    public byte[] Decrypt(byte[] cipherData,
        byte[] key, byte[] iv)
    {
        var ms = new MemoryStream();
        var alg = Rijndael.Create();
        alg.Key = key;
        alg.IV = iv;
        var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write);
        cs.Write(cipherData, 0, cipherData.Length);
        cs.Close();
        byte[] decryptedData = ms.ToArray();

        return decryptedData;
    }
    public bool TryDecrypt(string cipherText, out string result)
    {
        if (string.IsNullOrEmpty(cipherText))
        {
            result = cipherText;
            return false;
        }
        try
        {
            byte[] cipherBytes = Convert.FromBase64String(cipherText);
            var pdb = new Rfc2898DeriveBytes(CryptoKey, PasswordKeyByteArray);
            byte[] decryptedData = Decrypt(cipherBytes, pdb.GetBytes(32), pdb.GetBytes(16));
            result = Encoding.Unicode.GetString(decryptedData);
            return true;
        }
        catch (Exception)
        {
            result = null;
            return false;
        }
    }
}

Summary

Now we store our sensitive information from our users submissions encrypted. I really like the new pluggable architecture of Episerver. It makes it possible to extend almost anything.

Happy coding everyone!

Jul 13, 2016

Comments

K Khan
K Khan Jul 13, 2016 12:06 PM

Thanks for sharing! From where you find the reference "DdsPermanentStorage", its still not in documentation :)

Jul 13, 2016 12:10 PM

I think this is a really good and useful extension

I also think that extending form so that submitted data is held in an database / data store outside of the content database is really important. It would provide a more robust way of synchronising environments. (For example: Content can be synced to TEST and DEV environments without pulling over user submitted data which could potentially be sensitive). I'm presuming that IPermanentStorage is the right extension point here too.

Jul 13, 2016 12:14 PM

@Mark Yes, should be pretty easy.

Jul 13, 2016 12:49 PM

Nice :)

K Khan
K Khan Jul 13, 2016 01:09 PM

@Daniel, Cool, I like the way you approached and solved it! It is very useful.

Luc Gosso (MVP)
Luc Gosso (MVP) Dec 1, 2017 08:59 AM

Thanks Daniel, implemented this approach and wrote an evaluation about forms encryption
https://devblog.gosso.se/2017/12/encrypting-form-submissions-episerver-forms/

To get forms steps working it is important to override UpdateToStorage in DdsPermanentStorage, with this:

        public override Guid UpdateToStorage(Guid formSubmissionId, FormIdentity formIden, Submission submission)
        {
            IEnumerable source = this.LoadSubmissionFromStorage(formIden, new string[1]
            {
                formSubmissionId.ToString()
            });
            if (source == null || source.Count() == 0)
                return this.SaveToStorage(formIden, submission);


            var form = formIden.GetFormBlock();
            var elements = form.ElementsArea.FilteredItems;
            foreach (var contentAreaItem in elements)
            {
                var elementblock = _contentLoader.Service.Get(contentAreaItem.ContentLink);
                if (ShouldEncryptElement(elementblock) && submission.Data.ContainsKey(elementblock.FormElement.ElementName)/*if formstep*/)
                {
                    var oldText = submission.Data[elementblock.FormElement.ElementName];
                    if (oldText != null)
                    {
                        submission.Data.Remove(elementblock.FormElement.ElementName);
                        var encryptedText = _cryptographyService.Encrypt(oldText.ToString());
                        submission.Data.Add(elementblock.FormElement.ElementName, encryptedText);
                    }
                }
            }

            return base.UpdateToStorage(formSubmissionId, formIden, submission);
        }

Please login to comment.
Latest blogs
Adding Geolocation Personalisation to Optimizely CMS with Cloudflare

Enhance your Optimizely CMS personalisation by integrating Cloudflare's geolocation headers. Learn how my Cloudflare Geo-location Criteria package...

Andy Blyth | Nov 26, 2024 | Syndicated blog

Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog