Home » Customization » Experiments with Field Data Encryption in Sitecore

Experiments with Field Data Encryption in Sitecore

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

Last week, I came up with a design employing the decorator pattern for encrypting and decrypting Web Forms for Marketers form data. My design encapsulates and decorates an instance of Sitecore.Forms.Data.DataProviders.WFMDataProvider — the main data provider that lives in the Sitecore.Forms.Core assembly and is used by the module for saving/retrieving form data from its database — by extending and implementing its base class and interface, respectively, and delegates requests to its public methods using the same method signatures. The “decoration” part occurs before before form data is inserted and updated in the database, and decrypted after being retrieved.

Once I completed this design, I began to ponder whether it would be possible to take this one step further and encrypt/decrypt all field data in Sitecore as a whole.

I knew I would eventually have to do some research to see if this were possible. However, I decided to begin my encryption ventures by building classes that encrypt/decrypt data.

I envisioned stockpiling an arsenal of encryption algorithms. I imagined all encryption algorithms having the same public methods to encrypt and decrypt strings of data, so I created the following interface:

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);
    }
}

Since I am lightyears away from being a cryptography expert — or even considering myself a novice — I had to go fishing on the internet to find an encryption utility class in .NET. I found the following algorithm at http://www.deltasblog.co.uk/code-snippets/basic-encryptiondecryption-c/, and put it within a class that adheres to my IEncryptor interface above:

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
{
    // retrieved from http://www.deltasblog.co.uk/code-snippets/basic-encryptiondecryption-c/

    public class TripleDESEncryptor : IEncryptor
    {
        private string Key { get; set; }

        private TripleDESEncryptor(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);
            TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider();
            tripleDES.Key = UTF8Encoding.UTF8.GetBytes(key);
            tripleDES.Mode = CipherMode.ECB;
            tripleDES.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = tripleDES.CreateEncryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            tripleDES.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);
            TripleDESCryptoServiceProvider tripleDES = new TripleDESCryptoServiceProvider();
            tripleDES.Key = UTF8Encoding.UTF8.GetBytes(key);
            tripleDES.Mode = CipherMode.ECB;
            tripleDES.Padding = PaddingMode.PKCS7;
            ICryptoTransform cTransform = tripleDES.CreateDecryptor();
            byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
            tripleDES.Clear();
            return UTF8Encoding.UTF8.GetString(resultArray);
        }

        public static IEncryptor CreateNewTripleDESEncryptor(string key)
        {
            return new TripleDESEncryptor(key);
        }
    }
}

I then started wondering how I could ascertain whether data needed to be encrypted or decrypted. In other words, how could I prevent encrypting data that is already encrypted and, conversely, not decrypting data that isn’t encrypted?

I came up with building a decorator class that looks for a string terminator — a substring at the end of a string — and only encrypts the passed string when this terminating substring is not present, or decrypts only when this substring is found at the end of the string.

I decided to use a data transfer object (DTO) for my encryptor decorator, just in case I ever decide to use this same decorator with any encryption class that implements my encryptor interface. I also send in the encryption key and string terminator via this DTO:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.DTO
{
    public class DataNullTerminatorEncryptorSettings
    {
        public string EncryptionDataNullTerminator { get; set; }
        public string EncryptionKey { get; set; }
        public IEncryptor Encryptor { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Security.DTO;
using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.Encryption
{
    public class DataNullTerminatorEncryptor : IEncryptor
    {
        private DataNullTerminatorEncryptorSettings DataNullTerminatorEncryptorSettings { get; set; }

        private DataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            SetDataNullTerminatorEncryptorSettings(dataNullTerminatorEncryptorSettings);
        }

        private void SetDataNullTerminatorEncryptorSettings(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            Assert.ArgumentNotNull(dataNullTerminatorEncryptorSettings, "dataNullTerminatorEncryptorSettings");
            Assert.ArgumentNotNullOrEmpty(dataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator, "dataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator");
            Assert.ArgumentNotNullOrEmpty(dataNullTerminatorEncryptorSettings.EncryptionKey, "dataNullTerminatorEncryptorSettings.EncryptionKey");
            Assert.ArgumentNotNull(dataNullTerminatorEncryptorSettings.Encryptor, "dataNullTerminatorEncryptorSettings.Encryptor");
            DataNullTerminatorEncryptorSettings = dataNullTerminatorEncryptorSettings;
        }

        public string Encrypt(string input)
        {
            if (!IsEncrypted(input))
            {
                string encryptedInput = DataNullTerminatorEncryptorSettings.Encryptor.Encrypt(input);
                return string.Concat(encryptedInput, DataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator);
            }

            return input;
        }

        public string Decrypt(string input)
        {
            if (IsEncrypted(input))
            {
                input = input.Replace(DataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator, string.Empty);
                return DataNullTerminatorEncryptorSettings.Encryptor.Decrypt(input);
            }

            return input;
        }

        private bool IsEncrypted(string input)
        {
            if (!string.IsNullOrEmpty(input))
                return input.EndsWith(DataNullTerminatorEncryptorSettings.EncryptionDataNullTerminator);

            return false;
        }

        public static IEncryptor CreateNewDataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            return new DataNullTerminatorEncryptor(dataNullTerminatorEncryptorSettings);
        }
    }
}

Now that I have two encryptors — the TripleDESEncryptor and DataNullTerminatorEncryptor classes — I thought it would be prudent to create a factory class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Sandbox.Security.DTO;

namespace Sitecore.Sandbox.Security.Encryption.Base
{
    public interface IEncryptorFactory
    {
        IEncryptor CreateNewTripleDESEncryptor(string encryptionKey);

        IEncryptor CreateNewDataNullTerminatorEncryptor();

        IEncryptor CreateNewDataNullTerminatorEncryptor(string encryptionDataNullTerminator, string encryptionKey);

        IEncryptor CreateNewDataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Configuration;

using Sitecore.Sandbox.Security.DTO;
using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Security.Encryption
{
    public class EncryptorFactory : IEncryptorFactory
    {
        private static volatile IEncryptorFactory _Current;
        private static object lockObject = new Object();

        public static IEncryptorFactory Current
        {
            get 
            {
                if (_Current == null) 
                {
                    lock (lockObject) 
                    {
                        if (_Current == null)
                        {
                            _Current = new EncryptorFactory();
                        }
                    }
                }

                return _Current;
            }
        }

        private EncryptorFactory()
        {
        }

        public IEncryptor CreateNewTripleDESEncryptor(string encryptionKey)
        {
            return TripleDESEncryptor.CreateNewTripleDESEncryptor(encryptionKey);
        }

        public IEncryptor CreateNewDataNullTerminatorEncryptor()
        {
            string encryptionDataNullTerminator = Settings.GetSetting("Encryption.DataNullTerminator");
            string encryptionKey = Settings.GetSetting("Encryption.Key");
            return CreateNewDataNullTerminatorEncryptor(encryptionDataNullTerminator, encryptionKey);
        }

        public IEncryptor CreateNewDataNullTerminatorEncryptor(string encryptionDataNullTerminator, string encryptionKey)
        {
            DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings = new DataNullTerminatorEncryptorSettings 
            { 
                EncryptionDataNullTerminator = encryptionDataNullTerminator,
                EncryptionKey = encryptionDataNullTerminator,
                // we're going to use the TripleDESEncryptor by default
                Encryptor = CreateNewTripleDESEncryptor(encryptionKey)  
            };

            return CreateNewDataNullTerminatorEncryptor(dataNullTerminatorEncryptorSettings);
        }

        public IEncryptor CreateNewDataNullTerminatorEncryptor(DataNullTerminatorEncryptorSettings dataNullTerminatorEncryptorSettings)
        {
            return DataNullTerminatorEncryptor.CreateNewDataNullTerminatorEncryptor(dataNullTerminatorEncryptorSettings);
        }
    }
}

I decided to make my factory class be a singleton. I saw no need of having multiple instances of this factory object floating around in memory, and envisioned using this same factory object in a tool I wrote to encrypt all field data in all Sitecore databases. I will discuss this tool at the end of this article.

It’s now time to do some research to see whether I have the ability to hijack the main mechanism for saving/retrieving data from Sitecore.

While snooping around in my Web.config, I saw that the core, master and web databases use a data provider defined at configuration/sitecore/dataProviders/main:

<configuration>
	<sitecore database="SqlServer">

		<!-- More stuff here -->

		<dataProviders>
			<main type="Sitecore.Data.$(database).$(database)DataProvider, Sitecore.Kernel">
				<param connectionStringName="$(1)"/>
				<Name>$(1)</Name>
			</main>

			<!-- More stuff here -->

		<dataProviders>

			<!-- More stuff here -->

	</sitecore>
</configuration>

In my local instance, this points to the class Sitecore.Data.SqlServer.SqlServerDataProvider in Sitecore.Kernel.dll. I knew I had open up .NET Reflector and start investigating.

I discovered that there wasn’t much happening in this class at the field level. I also noticed this class used Sitecore.Data.DataProviders.Sql.SqlDataProvider as its base class, so I continued my research there.

I struck gold in the Sitecore.Data.DataProviders.Sql.SqlDataProvider class. Therein, I found methods that deal with saving/retrieving field data from an instance of the database utility class Sitecore.Data.DataProviders.Sql.SqlDataApi.

I knew this was the object I had to either decorate or override. Since most of the methods I needed to decorate with my encryption functionality were signed as protected, I had to subclass this class in order to get access to them. I then wrapped these methods with calls to my encryptor’s Encrypt(string input) and Decrypt(string input) methods:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.DataProviders;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;

using Sitecore.Sandbox.Security.Encryption;
using Sitecore.Sandbox.Security.Encryption.Base;

namespace Sitecore.Sandbox.Data.DataProviders.Sql
{
    public class EncryptionSqlDataProvider : SqlDataProvider
    {
        private static readonly IEncryptorFactory EncryptorBegetter = EncryptorFactory.Current;
        private static readonly IEncryptor Encryptor = EncryptorBegetter.CreateNewDataNullTerminatorEncryptor();

        public EncryptionSqlDataProvider(SqlDataApi sqlDataApi) 
            : base(sqlDataApi)
        {
        }

        public override FieldList GetItemFields(ItemDefinition itemDefinition, VersionUri versionUri, CallContext context)
        {
            FieldList fieldList = base.GetItemFields(itemDefinition, versionUri, context);

            if (fieldList == null)
                return null;

            FieldList fieldListDecrypted = new FieldList(); 
            foreach (KeyValuePair<ID, string> field in fieldList)
            {
                string decryptedValue = Decrypt(field.Value);
                fieldListDecrypted.Add(field.Key, decryptedValue);
            }

            return fieldListDecrypted;
        }

        protected override string GetFieldValue(string tableName, Guid entryId)
        {
            string value = base.GetFieldValue(tableName, entryId);
            return Decrypt(value);
        }

        protected override void SetFieldValue(string tableName, Guid entryId, string value)
        {
            string encryptedValue = Encrypt(value);
            base.SetFieldValue(tableName, entryId, encryptedValue);
        }

        protected override void WriteSharedField(ID itemId, FieldChange change, DateTime now, bool fieldsAreEmpty)
        {
            FieldChange unencryptedFieldChange = GetEncryptedFieldChange(itemId, change);
            base.WriteSharedField(itemId, unencryptedFieldChange, now, fieldsAreEmpty);
        }

        protected override void WriteUnversionedField(ID itemId, FieldChange change, DateTime now, bool fieldsAreEmpty)
        {
            FieldChange unencryptedFieldChange = GetEncryptedFieldChange(itemId, change);
            base.WriteUnversionedField(itemId, unencryptedFieldChange, now, fieldsAreEmpty);
        }

        protected override void WriteVersionedField(ID itemId, FieldChange change, DateTime now, bool fieldsAreEmpty)
        {
            FieldChange unencryptedFieldChange = GetEncryptedFieldChange(itemId, change);
            base.WriteVersionedField(itemId, unencryptedFieldChange, now, fieldsAreEmpty);
        }   

        private FieldChange GetEncryptedFieldChange(ID itemId, FieldChange unencryptedFieldChange)
        {
            if (!string.IsNullOrEmpty(unencryptedFieldChange.Value))
            {
                Field field = GetField(itemId, unencryptedFieldChange.FieldID);
                string encryptedValue = Encrypt(unencryptedFieldChange.Value);
                return new FieldChange(field, encryptedValue);
            }

            return unencryptedFieldChange;
        }

        private Field GetField(ID itemId, ID fieldID)
        {
            Item item = GetItem(itemId);
            return GetField(item, fieldID);
        }

        private Item GetItem(ID itemId)
        {
            return base.Database.Items[itemId];
        }

        private static Field GetField(Item owner, ID fieldID)
        {
            return new Field(fieldID, owner);
        }

        private static string Encrypt(string value)
        {
            return Encryptor.Encrypt(value);
        }

        private static string Decrypt(string value)
        {
            return Encryptor.Decrypt(value);
        }
    }
}

After creating my new EncryptionSqlDataProvider above, I had to define my own SqlServerDataProvider that inherits from it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Data.SqlServer;

using Sitecore.Sandbox.Data.DataProviders.Sql;

namespace Sitecore.Sandbox.Data.SqlServer
{
    public class EncryptionSqlServerDataProvider : EncryptionSqlDataProvider
    {
        public EncryptionSqlServerDataProvider(string connectionString)
             : base(new SqlServerDataApi(connectionString))
        {
        }
    }
}

I put in a patch reference to my new EncryptionSqlServerDataProvider in a new config file (/App_Config/Include/EncryptionDatabaseProvider.config), and defined the settings used by my encryptors:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<dataProviders>
			<main type="Sitecore.Data.$(database).$(database)DataProvider, Sitecore.Kernel">
				<patch:attribute name="type">Sitecore.Sandbox.Data.$(database).Encryption$(database)DataProvider, Sitecore.Sandbox</patch:attribute>
		       </main>
		</dataProviders>
		<settings>
			<!-- must be 24 characters long -->
			<setting name="Encryption.Key" value="4rgjvqm234gswdfd045r3f4q" />
			
			<!-- this is added to the end of encrypted data in the database -->
			<setting name="Encryption.DataNullTerminator" value="%IS_ENCRYPTED%" />
		</settings>
	</sitecore>
</configuration>

Will the above work, or will I be retreating back to the whiteboard scratching my head while being in a state of bewilderment?. Let’s try out the above to see where my fate lies.

I inserted a new Sample Item under my Home node, entered some content into four fields, and then clicked the Save button:

encryption test item

I opened up my Sitecore master database in Microsoft SQL Server Management Studio — I don’t recommend this being a standard Sitecore development practice, although I am doing this here to see if my encryption scheme worked — to see if my field data was encrypted. It was encrypted successfully:

sql

I then published my item to the web database, and pulled up my new Sample Item page in a browser to make sure it works despite being encrypted in the database:

presentation

Hooray! It works!

You might be asking yourself: how would we go about encrypting “legacy” field data in all Sitecore databases? Well, I wrote tool — the reason for creating my EncryptorFactory above — yesterday to do this. It encrypted all field data in the core, master and web databases.

However, when I opened up the desktop view of the Sitecore client, many of my tray buttons disappeared. Seeing this has lead me to conclude it’s not a good idea to encrypt data in the core database.

Plus, after having rolled back my core database to its previous state — a state of being unencrypted — I tried to open up the Content Editor in the master database. I then received an exception stemming from the rules engine — apparently not all fields in both the master and web databases should be encrypted. There must be code outside of the main data provider accessing data in all three databases.

Future research is needed to discover which fields are good candidates for having their data encrypted, and which fields should be ignored.

Advertisement

3 Comments

  1. […] my Experiments with Field Data Encryption in Sitecore article, I briefly mentioned designing a custom Web Forms for Marketers (WFFM) DataProvider that […]

  2. […] an earlier post, I walked you through how I experimented with data encryption of field values in Sitecore, and […]

Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: