In an earlier post, I walked you through how I experimented with data encryption of field values in Sitecore, and alluded to how I had done a similar thing for the Web Forms For Marketers (WFFM) module on a past project at work.
Months have gone by, and guilt has begun to gnaw away at my entire being — no, not really, I’m exaggerating a bit — but I definitely have been feeling bad for not sharing a solution.
In order to shake feeling bad, I decided to put my nose to the grindstone over the past few days to come up with a different solution than the one I had built at work, and this post shows the fruits of that labor.
I decided to reuse the interface I had created in my older post on data encryption. I am re-posting it here for reference:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Sitecore.Sandbox.Security.Encryption.Base { public interface IEncryptor { string Encrypt(string input); string Decrypt(string input); } }
I then asked myself “What encryption algorithm should I use?” I scavenged through the System.Security.Cryptography namespace in mscorlib.dll using .NET Reflector, and discovered some classes, when used together, achieve data encryption using the RC2 algorithm — an algorithm I know nothing about:
using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using Sitecore.Diagnostics; using Sitecore.Sandbox.Security.Encryption.Base; namespace Sitecore.Sandbox.Security.Encryption { public class RC2Encryptor : IEncryptor { public string Key { get; set; } private RC2Encryptor(string key) { SetKey(key); } private void SetKey(string key) { Assert.ArgumentNotNullOrEmpty(key, "key"); Key = key; } public string Encrypt(string input) { return Encrypt(input, Key); } public static string Encrypt(string input, string key) { byte[] inputArray = UTF8Encoding.UTF8.GetBytes(input); RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider(); rc2.Key = UTF8Encoding.UTF8.GetBytes(key); rc2.Mode = CipherMode.ECB; rc2.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = rc2.CreateEncryptor(); byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length); rc2.Clear(); return System.Convert.ToBase64String(resultArray, 0, resultArray.Length); } public string Decrypt(string input) { return Decrypt(input, Key); } public static string Decrypt(string input, string key) { byte[] inputArray = System.Convert.FromBase64String(input); RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider(); rc2.Key = UTF8Encoding.UTF8.GetBytes(key); rc2.Mode = CipherMode.ECB; rc2.Padding = PaddingMode.PKCS7; ICryptoTransform cTransform = rc2.CreateDecryptor(); byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length); rc2.Clear(); return UTF8Encoding.UTF8.GetString(resultArray); } public static IEncryptor CreateNewRC2Encryptor(string key) { return new RC2Encryptor(key); } } }
As I had mentioned in my previous post on data encryption, I am not a cryptography expert, nor a security expert.
I am not aware of how strong the RC2 encryption algorithm is, or what it would take to crack it. I strongly advise against using this algorithm in any production system without first consulting with a security expert. I am using it in this post only as an example.
If you happen to be a security expert, or are able to compare encryption algorithms defined in the System.Security.Cryptography namespace in mscorlib.dll, please share in a comment.
In a previous post on manipulating field values for WFFM forms, I had to define a new class that implements Sitecore.Forms.Data.IField in Sitecore.Forms.Core.dll in order to change field values — it appears the property mutator for the “out of the box” class is ignored — and decided to reuse it here:
using System; using Sitecore.Forms.Data; namespace Sitecore.Sandbox.Utilities.Manipulators.DTO { public class WFFMField : IField { public string Data { get; set; } public Guid FieldId { get; set; } public string FieldName { get; set; } public IForm Form { get; set; } public Guid Id { get; internal set; } public string Value { get; set; } } }
Next, I created a WFFM Data Provider that encrypts and decrypts field names and values:
using System; using System.Collections.Generic; using Sitecore.Configuration; using Sitecore.Diagnostics; using Sitecore.Forms.Data; using Sitecore.Forms.Data.DataProviders; using Sitecore.Reflection; using Sitecore.Sandbox.Security.DTO; using Sitecore.Sandbox.Security.Encryption.Base; using Sitecore.Sandbox.Security.Encryption; using Sitecore.Sandbox.Utilities.Manipulators.DTO; namespace Sitecore.Sandbox.WFFM.Forms.Data.DataProviders { public class WFFMEncryptionDataProvider : WFMDataProviderBase { private WFMDataProviderBase InnerProvider { get; set; } private IEncryptor Encryptor { get; set; } public WFFMEncryptionDataProvider(string innerProvider) : this(CreateInnerProvider(innerProvider), CreateDefaultEncryptor()) { } public WFFMEncryptionDataProvider(string connectionString, string innerProvider) : this(CreateInnerProvider(innerProvider, connectionString), CreateDefaultEncryptor()) { } public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider) : this(innerProvider, CreateDefaultEncryptor()) { } public WFFMEncryptionDataProvider(WFMDataProviderBase innerProvider, IEncryptor encryptor) { SetInnerProvider(innerProvider); SetEncryptor(encryptor); } private static WFMDataProviderBase CreateInnerProvider(string innerProvider, string connectionString = null) { Assert.ArgumentNotNullOrEmpty(innerProvider, "innerProvider"); if (!string.IsNullOrWhiteSpace(connectionString)) { return ReflectionUtil.CreateObject(innerProvider, new[] { connectionString }) as WFMDataProviderBase; } return ReflectionUtil.CreateObject(innerProvider, new object[0]) as WFMDataProviderBase; } private void SetInnerProvider(WFMDataProviderBase innerProvider) { Assert.ArgumentNotNull(innerProvider, "innerProvider"); InnerProvider = innerProvider; } private static IEncryptor CreateDefaultEncryptor() { return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(GetEncryptorSettings()); } private static DataNullTerminatorEncryptorSettings GetEncryptorSettings() { return new DataNullTerminatorEncryptorSettings { EncryptionDataNullTerminator = Settings.GetSetting("WFFM.Encryption.DataNullTerminator"), InnerEncryptor = RC2Encryptor.CreateNewRC2Encryptor(Settings.GetSetting("WFFM.Encryption.Key")) }; } private void SetEncryptor(IEncryptor encryptor) { Assert.ArgumentNotNull(encryptor, "encryptor"); Encryptor = encryptor; } public override void ChangeStorage(Guid formItemId, string newStorage) { InnerProvider.ChangeStorage(formItemId, newStorage); } public override void ChangeStorageForForms(IEnumerable<Guid> ids, string storageName) { InnerProvider.ChangeStorageForForms(ids, storageName); } public override void DeleteForms(IEnumerable<Guid> formSubmitIds) { InnerProvider.DeleteForms(formSubmitIds); } public override void DeleteForms(Guid formItemId, string storageName) { InnerProvider.DeleteForms(formItemId, storageName); } public override IEnumerable<IPool> GetAbundantPools(Guid fieldId, int top, out int total) { return InnerProvider.GetAbundantPools(fieldId, top, out total); } public override IEnumerable<IForm> GetForms(QueryParams queryParams, out int total) { IEnumerable<IForm> forms = InnerProvider.GetForms(queryParams, out total); DecryptForms(forms); return forms; } public override IEnumerable<IForm> GetFormsByIds(IEnumerable<Guid> ids) { IEnumerable<IForm> forms = InnerProvider.GetFormsByIds(ids); DecryptForms(forms); return forms; } public override int GetFormsCount(Guid formItemId, string storageName, string filter) { return InnerProvider.GetFormsCount(formItemId, storageName, filter); } public override IEnumerable<IPool> GetPools(Guid fieldId) { return InnerProvider.GetPools(fieldId); } public override void InsertForm(IForm form) { EncryptForm(form); InnerProvider.InsertForm(form); } public override void ResetPool(Guid fieldId) { InnerProvider.ResetPool(fieldId); } public override IForm SelectSingleForm(Guid fieldId, string likeValue) { IForm form = InnerProvider.SelectSingleForm(fieldId, likeValue); DecryptForm(form); return form; } public override bool UpdateForm(IForm form) { EncryptForm(form); return InnerProvider.UpdateForm(form); } private void EncryptForms(IEnumerable<IForm> forms) { Assert.ArgumentNotNull(forms, "forms"); foreach (IForm form in forms) { EncryptForm(form); } } private void EncryptForm(IForm form) { Assert.ArgumentNotNull(form, "form"); Assert.ArgumentNotNull(form.Field, "form.Field"); form.Field = EncryptFields(form.Field); } private IEnumerable<IField> EncryptFields(IEnumerable<IField> fields) { Assert.ArgumentNotNull(fields, "fields"); IList<IField> encryptedFields = new List<IField>(); foreach (IField field in fields) { encryptedFields.Add(EncryptField(field)); } return encryptedFields; } private IField EncryptField(IField field) { Assert.ArgumentNotNull(field, "field"); return CreateNewWFFMField(field, Encrypt(field.FieldName), Encrypt(field.Value)); } private void DecryptForms(IEnumerable<IForm> forms) { Assert.ArgumentNotNull(forms, "forms"); foreach (IForm form in forms) { DecryptForm(form); } } private void DecryptForm(IForm form) { Assert.ArgumentNotNull(form, "form"); Assert.ArgumentNotNull(form.Field, "form.Field"); form.Field = DecryptFields(form.Field); } private IEnumerable<IField> DecryptFields(IEnumerable<IField> fields) { Assert.ArgumentNotNull(fields, "fields"); IList<IField> decryptedFields = new List<IField>(); foreach (IField field in fields) { decryptedFields.Add(DecryptField(field)); } return decryptedFields; } private IField DecryptField(IField field) { Assert.ArgumentNotNull(field, "field"); return CreateNewWFFMField(field, Decrypt(field.FieldName), Decrypt(field.Value)); } private string Encrypt(string input) { return Encryptor.Encrypt(input); } private string Decrypt(string input) { return Encryptor.Decrypt(input); } private static IField CreateNewWFFMField(IField field, string fieldName, string value) { if (field != null) { return new WFFMField { Data = field.Data, FieldId = field.FieldId, FieldName = fieldName, Form = field.Form, Id = field.Id, Value = value }; } return null; } } }
The above class employs the decorator pattern. An inner WFFM Data Provider — which is supplied via a parameter configuration node in \App_Config\Include\forms.config, and is created via magic within the Sitecore.Reflection.ReflectionUtil class — is wrapped.
Methods that save and retrieve form data in the above Data Provider decorate the same methods defined on the inner WFFM Data Provider.
Methods that save form data pass form(s) — and eventually their fields — through a chain of Encrypt methods. The Encrypt method that takes in an IField instance as an argument encrypts the instance’s field name and value, and returns a new instance of the WFFMField class using the encrypted data and the other properties on the IField instance untouched.
Similarly, a chain of Decrypt methods are called for form(s) being retrieved from the inner Data Provider — field names and values are decrypted and saved into a new instance of the WFFMField class, and the manipulated form(s) are returned.
I want to point out that the IEncryptor instance is actually an instance of DataNullTerminatorEncryptor — see my earlier post on data encryption to see how this is implemented — which decorates our RC2Encryptor. This decorating encryptor stamps encrypted strings with a special string so we don’t accidentally encrypt a value twice, and it also won’t try to decrypt a string value that isn’t encrypted.
I added a new include configuration file to hold encryption related settings — the IEncryptor’s key, and the string that will be put at the end of all encrypted data via the DataNullTerminatorEncryptor instance:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <settings> <!-- TODO: change the terminator so it does not scream "PLEASE TRY TO CRACK ME!" --> <setting name="WFFM.Encryption.DataNullTerminator" value="#I_AM_ENCRYPTED#" /> <!-- I found this key somewhere on the internet, so it must be secure --> <setting name="WFFM.Encryption.Key" value="88bca90e90875a" /> </settings> </sitecore> </configuration>
I then hooked in the encryption WFFM Data Provider in \App_Config\Include\forms.config, and set the type for the inner provider:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/"> <sitecore> <!-- There is stuff up here --> <!-- MS SQL --> <formsDataProvider type="Sitecore.Sandbox.WFFM.Forms.Data.DataProviders.WFFMEncryptionDataProvider, Sitecore.Sandbox"> <!-- No, this is not my real connection string --> <param desc="connection string">user id=(user);password=(password);Data Source=(database)</param> <param desc="inner provider">Sitecore.Forms.Data.DataProviders.WFMDataProvider, Sitecore.Forms.Core</param> </formsDataProvider> <!-- There is stuff down here --> </sitecore> </configuration>
Let’s see this in action.
I created a new WFFM form with some fields for testing:
I then mapped the above form to a new page in Sitecore, and published both the form and page.
I navigated to the form page, and filled it out:
After clicking submit, I was given a ‘thank you’ page’:
Let’s see what our field data looks like in the WFFM database:
As you can see, the data is encrypted.
Now, let’s see if the data is also encrypted in the Forms Report for our test form:
As you can see, the end-user would be unaware that any data manipulation is happening under the hood.
If you have any thoughts on this, please leave a comment.
