Home » Security

Category Archives: Security

Restrict Certain Files from Being Uploaded Through Web Forms for Marketers Forms in Sitecore: an Alternative Approach

This past weekend I noticed the <formUploadFile> pipeline in Web Forms for Marketers (WFFM), and wondered whether I could create an alternative solution to the one I had shared in this post — I had built a custom WFFM field type to prevent certain files from being uploaded through WFFM forms.

After some tinkering, I came up the following class to serve as a <formUploadFile> pipeline processor:

using System;
using System.Collections.Generic;
using System.Web;

using Sitecore.Diagnostics;

using Sitecore.Form.Core.Pipelines.FormUploadFile;

namespace Sitecore.Sandbox.Form.Pipelines.FormUploadFile
{
    public class CheckMimeTypesNotAllowed
    {
        static CheckMimeTypesNotAllowed()
        {
            MimeTypesNotAllowed = new List<string>();
        }

        public void Process(FormUploadFileArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            string mimeType = GetMimeType(args.File.FileName);
            if (IsMimeTypeAllowed(mimeType))
            {
                return;
            }

            throw new Exception(string.Format("Uploading a file with MIME type {0} is not allowed", mimeType));
        }

        protected virtual string GetMimeType(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, "fileName");
            return MimeMapping.GetMimeMapping(fileName);
        }

        protected virtual bool IsMimeTypeAllowed(string mimeType)
        {
            foreach (string mimeTypeNotAllowed in MimeTypesNotAllowed)
            {
                if (AreEqualIgnoreCase(mimeTypeNotAllowed, mimeType))
                {
                    return false;
                }    
            }

            return true;
        }

        private static bool AreEqualIgnoreCase(string stringOne, string stringTwo)
        {
            return string.Equals(stringOne, stringTwo, StringComparison.CurrentCultureIgnoreCase);
        }

        private static void AddMimeTypeNotAllowed(string mimeType)
        {
            if (string.IsNullOrWhiteSpace(mimeType) || MimeTypesNotAllowed.Contains(mimeType))
            {
                return;
            }

            MimeTypesNotAllowed.Add(mimeType);
        }

        private static IList<string> MimeTypesNotAllowed { get; set; }
    }
}

The class above adds restricted MIME types to a list — these MIME types are defined in a configuration file shown below — and checks to see if the uploaded file’s MIME type is in the restricted list. If it is restricted, an exception is thrown with some information — throwing an exception in WFFM prevents the form from being submitted.

MIME types are inferred using System.Web.MimeMapping.GetMimeMapping(string fileName), a method that is new in .NET 4.5 (this solution will not work in older versions of .NET).

I then registered the class above as a <formUploadFile> pipeline processor via the following configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <formUploadFile>
        <processor patch:before="processor[@type='Sitecore.Form.Core.Pipelines.FormUploadFile.Save, Sitecore.Forms.Core']"
                   type="Sitecore.Sandbox.Form.Pipelines.FormUploadFile.CheckMimeTypesNotAllowed">
          <mimeTypesNotAllowed hint="list:AddMimeTypeNotAllowed">
            <mimeType>application/octet-stream</mimeType>
          </mimeTypesNotAllowed >
        </processor>  
      </formUploadFile>
    </pipelines>
  </sitecore>
</configuration>

In case you are wondering, the MIME type being passed to the processor in the configuration file is for executables.

Let’s see how we did. 😉

I whipped up a test form, and pulled it up in a browser:

form-for-testing

I then selected an executable:

select-executable

I saw this after an attempt to submit the form:

form-upload-exception

And my log file expressed why:

exception-in-log

I then copied a jpeg into my test folder, and selected it to be uploaded:

select-jpg

After clicking the submit button, I was given a nice confirmation message:

success-upload

There is one problem with the solution above that I would like to point out: it does not address the issue of file extensions being changed. I could not solve this problem since the MIME type of the file being uploaded cannot be determined from the Sitecore.Form.Core.Media.PostedFile instance set in the arguments object: there is no property for it. 😦

If you know of another way to determine MIME types on Sitecore.Form.Core.Media.PostedFile instances, or have other ideas for restricting certain files from being uploaded through WFFM, please share in a comment.

Advertisement

Restrict IP Access of Directories and Files in Your Sitecore Web Application Using a httpRequestBegin Pipeline Processor

Last week my friend and colleague Greg Coffman had asked me if I knew of a way to restrict IP access to directories within the Sitecore web application, and I recalled reading a post by Alex Shyba quite some time ago.

Although Alex’s solution is probably good enough in most circumstances, I decided to explore other solutions, and came up with the following <httpRequestBegin> pipeline processor as another way to accomplish this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Hosting;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Web;

namespace Sitecore.Sandbox.Pipelines.HttpRequest
{
    public class FilePathRestrictor : HttpRequestProcessor 
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!ShouldRedirect(args))
            {
                return;
            }

            RedirectToNoAccessUrl();
        }

        private bool ShouldRedirect(HttpRequestArgs args)
        {
            return CanProcess(args, GetFilePath(args)) 
                    && !CanAccess(args.Context.Request.UserHostAddress);
        }

        protected virtual string GetFilePath(HttpRequestArgs args)
        {
            if (string.IsNullOrWhiteSpace(Context.Page.FilePath))
            {
                return args.Url.FilePath;
            }

            return Context.Page.FilePath;
        }

        protected virtual bool CanProcess(HttpRequestArgs args, string filePath)
        {
            return !string.IsNullOrWhiteSpace(filePath)
                    && !string.IsNullOrWhiteSpace(RootFilePath)
                    && AllowedIPs != null
                    && AllowedIPs.Any()
                    && (HostingEnvironment.VirtualPathProvider.DirectoryExists(filePath)
                        || HostingEnvironment.VirtualPathProvider.FileExists(filePath))
                    && args.Url.FilePath.StartsWith(RootFilePath, StringComparison.CurrentCultureIgnoreCase)
                    && !string.IsNullOrWhiteSpace(args.Context.Request.UserHostAddress)
                    && !string.Equals(filePath, Settings.NoAccessUrl, StringComparison.CurrentCultureIgnoreCase);
        }

        protected virtual bool CanAccess(string ip)
        {
            Assert.ArgumentNotNullOrEmpty(ip, "ip");
            return AllowedIPs.Contains(ip);
        }

        protected virtual void RedirectToNoAccessUrl()
        {
            WebUtil.Redirect(Settings.NoAccessUrl);
        }

        protected virtual void AddAllowedIP(string ip)
        {
            if (string.IsNullOrWhiteSpace(ip) || AllowedIPs.Contains(ip))
            {
                return;
            }

            AllowedIPs.Add(ip);
        }

        private string RootFilePath { get; set; }

        private IList<string> _AllowedIPs;
        private IList<string> AllowedIPs 
        {
            get
            {
                if (_AllowedIPs == null)
                {
                    _AllowedIPs = new List<string>();
                }

                return _AllowedIPs;
            }
        }
    }
}

The pipeline processor above determines whether the IP making the request has access to the directory or file on the file system — a list of IP addresses that should have access are passed to the pipeline processor via a configuration file, and the code does check to see if the requested URL is a directory or a file on the file system — by matching the beginning of the URL with a configuration defined root path.

If the user does not have access to the requested path, s/he is redirected to the “No Access Url” which is specified in the Sitecore instance’s configuration.

The list of IP addresses that should have access to the directory — including everything within it — and the root path are handed to the pipeline processor via the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:before=" processor[@type='Sitecore.Pipelines.HttpRequest.FileResolver, Sitecore.Kernel']"
                   type="Sitecore.Sandbox.Pipelines.HttpRequest.FilePathRestrictor, Sitecore.Sandbox">
          <RootFilePath>/sitecore</RootFilePath>
          <AllowedIPs hint="list:AddAllowedIP">
            <IP>127.0.0.2</IP>
          </AllowedIPs>
        </processor>
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Since my IP is 127.0.0.1, I decided to only allow 127.0.0.2 access to my Sitecore directory — this also includes everything within it — in the above configuration file for testing.

After navigating to /sitecore of my local sandbox instance, I was redirected to the “No Access Url” page defined in my Web.config:

no-access

If you have any thoughts on this, or know of other solutions, please share in a comment.

Perform a Virus Scan on Files Uploaded into Sitecore

Last week I took notice of a SDN forum post asking whether there are any best practices for checking files uploaded into Sitecore for viruses.

Although I am unaware of there being any best practices for this, adding a processor into the <uiUpload> pipeline to perform virus scans has been on my plate for the past month. Not being able to find an open source .NET antivirus library has been my major blocker for building one.

However, after many hours of searching the web — yes, I do admit some of this time is sprinkled with idle surfing — over multiple weeks, I finally discovered nClam — a .NET client library for scanning files using a Clam Antivirus server (I setup one using instructions enumerated here).

Before I continue, I would like to caution you on using this or any antivirus solution before doing a substantial amount of research — I do not know how robust the solution I am sharing actually is, and I am by no means an antivirus expert. The purpose of this post is to show how one might go about adding antivirus capabilities into Sitecore, and I am not advocating or recommending any particular antivirus software package. Please consult with an antivirus expert before using any antivirus software/.NET client library .

The solution I came up with uses the adapter design pattern — it basically wraps and makes calls to the nClam library, and is accessible through the following antivirus scanner interface:

using System.IO;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public interface IScanner
    {
        ScanResult Scan(Stream sourceStream);

        ScanResult Scan(string filePath);
    }
}

This interface defines the bare minimum methods our antivirus classes should have. Instances of these classes should offer the ability to scan a file through the file’s Stream, or scan a file located on the server via the supplied file path.

The two scan methods defined by the interface must return an instance of the following data transfer object:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ScanResult
    {
        public string Message { get; set; }

        public ScanResultType ResultType { get; set; }
    }
}

Instances of the above class contain a detailed message coupled with a ScanResultType enumeration value which conveys whether the scanned file is clean, contains a virus, or something else went wrong during the scanning process:

namespace Sitecore.Sandbox.Security.Antivirus
{
    public enum ScanResultType
    {
        Clean,
        VirusDetected,
        Error,
        Unknown
    }
}

I used the ClamScanResults enumeration as a model for the above.

I created and used the ScanResultType enumeration instead of the ClamScanResults enumeration so that this solution can be extended for other antivirus libraries — or calls could be made to other antivirus software through the command-line — and these shouldn’t be tightly coupled to the nClam library.

I then wrapped the nClam library calls in the following ClamScanner class:

using System;
using System.IO;
using System.Linq;

using Sitecore.Diagnostics;

using nClam;

namespace Sitecore.Sandbox.Security.Antivirus
{
    public class ClamScanner : IScanner
    {
        private ClamClient Client { get; set; }

        public ClamScanner(string server, string port)
            : this(server, int.Parse(port))
        {
        }

        public ClamScanner(string server, int port)
            : this(new ClamClient(server, port))
        {
        }

        public ClamScanner(ClamClient client)
        {
            SetClient(client);
        }

        private void SetClient(ClamClient client)
        {
            Assert.ArgumentNotNull(client, "client");
            Client = client;
        }

        public ScanResult Scan(Stream sourceStream)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(sourceStream));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        public ScanResult Scan(string filePath)
        {
            ScanResult result;
            try
            {
                result = CreateNewScanResult(Client.SendAndScanFile(filePath));
            }
            catch (Exception ex)
            {
                result = CreateNewExceptionScanResult(ex);
            }

            return result;
        }

        private static ScanResult CreateNewScanResult(ClamScanResult result)
        {
            Assert.ArgumentNotNull(result, "result");
            if (result.Result == ClamScanResults.Clean)
            {
                return CreateNewScanResult("Yay! No Virus found!", ScanResultType.Clean);
            }

            if (result.Result == ClamScanResults.VirusDetected)
            {
                string message = string.Format("Oh no! The {0} virus was found!", result.InfectedFiles.First().VirusName);
                return CreateNewScanResult(message, ScanResultType.VirusDetected);
            }

            if (result.Result == ClamScanResults.Error)
            {
                string message = string.Format("Something went terribly wrong somewhere. Details: {0}", result.RawResult);
                return CreateNewScanResult(message, ScanResultType.Error);
            }

            return CreateNewScanResult("I have no clue about what just happened.", ScanResultType.Unknown);
        }

        private static ScanResult CreateNewExceptionScanResult(Exception ex)
        {
            string message = string.Format("Something went terribly wrong somewhere. Details:\n{0}", ex.ToString());
            return CreateNewScanResult(ex.ToString(), ScanResultType.Error);
        }

        private static ScanResult CreateNewScanResult(string message, ScanResultType type)
        {
            return new ScanResult
            {
                Message = message,
                ResultType = type
            };
        }
    }
}

Methods in the above class make calls to the nClam library, and return a ScanResult intance containing detailed information about the file scan.

Next I developed the following <uiUpload> pipeline processor to use instances of classes that define the IScanner interface above, and these instances are set via Sitecore when instantiating this pipeline processor — the ClamScanner type is defined in the patch configuration file shown later in this post:

using System.IO;
using System.Web;

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

using Sitecore.Sandbox.Security.Antivirus;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class ScanForViruses
    {
        public void Process(UploadArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            foreach (string fileKey in args.Files)
            {
                HttpPostedFile file = args.Files[fileKey];
                ScanResult result = ScanFile(file.InputStream);
                if(result.ResultType != ScanResultType.Clean)
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Reason: {1}", file.FileName, result.Message));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private ScanResult ScanFile(Stream fileStream)
        {
            Assert.ArgumentNotNull(fileStream, "fileStream");
            return Scanner.Scan(fileStream);
        }

        private IScanner Scanner { get; set; }
    }
}

Uploaded files are passed to the IScanner instance, and the pipeline is aborted if something isn’t quite right — this occurs when a virus is detected, or an error is reported by the IScanner instance. If a virus is discovered, or an error occurs, the message contained within the ScanResult instance is captured in the Sitecore log.

I then glued everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.Upload.ScanForViruses, Sitecore.Sandbox">
          <Scanner type="Sitecore.Sandbox.Security.Antivirus.ClamScanner">
            <param desc="server">localhost</param>
            <param desc="port">3310</param>
          </Scanner>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

I wish I could show you this in action when a virus is discovered in an uploaded file. However, I cannot put my system at risk by testing with an infected file.

But, I can show you what happens when an error is detected. I will do this by shutting down the Clam Antivirus server on my machine:

scanner-turned-off

On upload, I see the following:

tried-to-upload-file

When I opened up the Sitecore log, I see what the problem is:

log-error

Let’s turn it back on:

scanner-turned-on

I can now upload files again:

upload-no-error

If you have any suggestions on making this better, or know of a way to test it with a virus, please share in a comment below.

Restrict Certain Types of Files From Being Uploaded in Sitecore

Tonight I was struck by a thought while conducting research for a future blog post: should we prevent users from uploading certain types of files in Sitecore for security purposes?

You might thinking “Prevent users from uploading files? Mike, what on Earth are you talking about?”

What I’m suggesting is we might want to consider restricting certain types of files — specifically executables (these have an extension of .exe) and DOS command files (these have an extension of .com) — from being uploaded into Sitecore, especially when files can be easily downloaded from the media library.

Why should we do this?

Doing this will curtail the probability of viruses being spread among our Sitecore users — such could happen if one user uploads an executable that harbors a virus, and other users of our Sitecore system download that tainted executable, and run it on their machines.

As a “proof of concept”, I built the following uiUpload pipeline processor — the uiUpload pipeline lives in /configuration/sitecore/processors/uiUpload in your Sitecore instance’s Web.config — to restrict certain types of files from being uploaded into Sitecore:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Xml;

using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Pipelines.Upload;

namespace Sitecore.Sandbox.Pipelines.Upload
{
    public class CheckForRestrictedFiles : UploadProcessor
    {
        private List<string> _RestrictedExtensions;
        private List<string> RestrictedExtensions
        {
            get
            {
                if (_RestrictedExtensions == null)
                {
                    _RestrictedExtensions = new List<string>();
                }

                return _RestrictedExtensions;
            }
        }

        public void Process(UploadArgs args)
        {
            foreach(string fileKey in args.Files)
            {
                string fileName = GetFileName(args.Files, fileKey);
                string extension = Path.GetExtension(fileName);
                if (IsRestrictedExtension(extension))
                {
                    args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Files with an extension of {1} are not allowed.", fileName, extension));
                    Log.Warn(args.ErrorText, this);
                    args.AbortPipeline();
                }
            }
        }

        private static string GetFileName(HttpFileCollection files, string fileKey)
        {
            Assert.ArgumentNotNull(files, "files");
            Assert.ArgumentNotNullOrEmpty(fileKey, "fileKey");
            return files[fileKey].FileName;
        }

        private bool IsRestrictedExtension(string extension)
        {
            return RestrictedExtensions.Exists(restrictedExtension => string.Equals(restrictedExtension, extension, StringComparison.CurrentCultureIgnoreCase));
        }

        protected virtual void AddRestrictedExtension(XmlNode configNode)
        {
            if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText))
            {
                return;
            }

            RestrictedExtensions.Add(configNode.InnerText);
        }
    }
}

The class above ascertains whether each uploaded file has an extension that is restricted — restricted extensions are defined in the configuration file below, and are added to a list of strings via the AddRestrictedExtensions method — and logs an error message when a file possessing a restricted extension is encountered.

I then tied everything together using the following patch configuration file including specifying some file extensions to restrict:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <uiUpload>
        <processor mode="on" type="Sitecore.Sandbox.Pipelines.Upload.CheckForRestrictedFiles, Sitecore.Sandbox" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']">
          <restrictedExtensions hint="raw:AddRestrictedExtension">
            <!-- Be sure to prefix with a dot -->
            <extension>.exe</extension>
            <extension>.com</extension>
          </restrictedExtensions>
        </processor>
      </uiUpload>
    </processors>
  </sitecore>
</configuration>

Let’s try this out.

I went to the media library, and attempted to upload an executable:

upload-exe-dialog_001

After clicking the “Open” button, I was presented with the following:

upload-exe-error

An error in my Sitecore instance’s latest log file conveys why I could not upload the chosen file:

upload-exe-error-log

If you have thoughts on this, or have ideas for other processors that should be added to uiUpload pipeline, please share in a comment.

Encrypt Web Forms For Marketers Fields in Sitecore

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:

the-form-in-sitecore

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:

the-form-page

After clicking submit, I was given a ‘thank you’ page’:

the-form-page-confirmation

Let’s see what our field data looks like in the WFFM database:

the-form-submission-encrypted-db

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:

forms-report-encryption

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.

Enforce Password Expiration in the Sitecore CMS

I recently worked on a project that called for a feature to expire Sitecore users’ passwords after an elapsed period of time since their passwords were last changed.

The idea behind this is to lessen the probability that an attacker will infiltrate a system — or multiple systems if users reuse their passwords across different applications (this is more common than you think) — within an organization by acquiring old database backups containing users’ passwords.

Since I can’t show you what I built for that project, I cooked up another solution — a custom loggingin processor that determines whether a user’s password has expired in Sitecore:

using System;
using System.Web.Security;

using Sitecore.Diagnostics;
using Sitecore.Pipelines.LoggingIn;
using Sitecore.Web;

namespace Sitecore.Sandbox.Pipelines.LoggingIn
{
    public class CheckPasswordExpiration
    {
        private TimeSpan TimeSpanToExpirePassword { get; set; }
        private string ChangePasswordPageUrl { get; set; }

        public void Process(LoggingInArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if (!IsEnabled())
            {
                return;
            }

            MembershipUser user = GetMembershipUser(args);
            if (HasPasswordExpired(user))
            {
                WebUtil.Redirect(ChangePasswordPageUrl);
            }
        }

        private bool IsEnabled()
        {
            return IsTimeSpanToExpirePasswordSet() && IsChangePasswordPageUrlSet();
        }

        private bool IsTimeSpanToExpirePasswordSet()
        {
            return TimeSpanToExpirePassword > default(TimeSpan);
        }

        private bool IsChangePasswordPageUrlSet()
        {
            return !string.IsNullOrWhiteSpace(ChangePasswordPageUrl);
        }

        private static MembershipUser GetMembershipUser(LoggingInArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNullOrEmpty(args.Username, "args.Username");
            return Membership.GetUser(args.Username, false);
        }

        private bool HasPasswordExpired(MembershipUser user)
        {
            return user.LastPasswordChangedDate.Add(TimeSpanToExpirePassword) <= DateTime.Now;
        }
    }
}

The processor above ascertains whether a user’s password has expired by adding a configured timespan — see the configuration file below — to the last date and time the password was changed, and if that date and time summation is in the past — this means the password should have been changed already — then we redirect the user to a change password page (this is configured to be the Change Password page in Sitecore).

I wired up the custom loggingin processor, its timespan on expiring passwords — here I am using 1 minute since I can’t wait around all day 😉 — and set the change password page to be the url of Sitecore’s Change Password page:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <processors>
      <loggingin>
        <processor mode="on" type="Sitecore.Sandbox.Pipelines.LoggingIn.CheckPasswordExpiration, Sitecore.Sandbox"
					patch:before="processor[@type='Sitecore.Pipelines.LoggingIn.CheckStartPage, Sitecore.Kernel']">
          <!-- Number of days, hours, minutes and seconds after the last password change date to expire passwords -->
          <TimeSpanToExpirePassword>00:00:01:00</TimeSpanToExpirePassword>
          <ChangePasswordPageUrl>/sitecore/login/changepassword.aspx</ChangePasswordPageUrl>
        </processor>  
      </loggingin>
    </processors>
  </sitecore>
</configuration>

Let’s test this out.

I went to Sitecore’s login page, and entered my username and password on the login form:

login-page

I clicked the Login button, and was redirected to the Change Password page as expected:

change-password-page

If you can think of any other security measures that should be added to Sitecore, please share in a comment.

Prevent Sitecore Users from Using Common Dictionary Words in Passwords

Lately, enhancing security measures in Sitecore have been on my mind. One idea that came to mind today was finding a way to prevent password cracking — a scenario where a script tries to ascertain a user’s password by guessing over and over again what a user’s password might be, by supplying common words from a data store, or dictionary in the password textbox on the Sitecore login page.

As a way to prevent users from using common words in their Sitecore passwords, I decided to build a custom System.Web.Security.MembershipProvider — the interface (not a .NET interface but an abstract class) used by Sitecore out of the box for user management. Before we dive into that code, we need a dictionary of some sort.

I decided to use a list of fruits — all for the purposes of keeping this post simple — that I found on a Wikipedia page. People aren’t kidding when they say you can virtually find everything on Wikipedia, and no doubt all content on Wikipedia is authoritative — just ask any university professor. 😉

I copied some of the fruits on that Wikipedia page into a patch include config file — albeit it would make more sense to put these into a database, or perhaps into Sitecore if doing such a thing in a real-world solution. I am not doing this here for the sake of brevity.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <dictionaryWords>
      <word>Apple</word>
      <word>Apricot</word>
      <word>Avocado</word>
      <word>Banana</word>
      <word>Breadfruit</word>
      <word>Bilberry</word>
      <word>Blackberry</word>
      <word>Blackcurrant</word>
      <word>Blueberry</word>
      <word>Currant</word>
      <word>Cherry</word>
      <word>Cherimoya</word>
      <word>Clementine</word>
      <word>Cloudberry</word>
      <word>Coconut</word>
      <word>Date</word>
      <word>Damson</word>
      <word>Dragonfruit</word>
      <word>Durian</word>
      <word>Eggplant</word>
      <word>Elderberry</word>
      <word>Feijoa</word>
      <word>Fig</word>
      <word>Gooseberry</word>
      <word>Grape</word>
      <word>Grapefruit</word>
      <word>Guava</word>
      <word>Huckleberry</word>
      <word>Honeydew</word>
      <word>Jackfruit</word>
      <word>Jettamelon</word>
      <word>Jambul</word>
      <word>Jujube</word>
      <word>Kiwi fruit</word>
      <word>Kumquat</word>
      <word>Legume</word>
      <word>Lemon</word>
      <word>Lime</word>
      <word>Loquat</word>
      <word>Lychee</word>
      <word>Mandarine</word>
      <word>Mango</word>
      <word>Melon</word>
    </dictionaryWords>
  </sitecore>
</configuration>

I decided to reuse a utility class I built for my post on expanding Standard Values tokens.

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

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Utilities.StringUtilities.Base;
using Sitecore.Sandbox.Utilities.StringUtilities.DTO;

namespace Sitecore.Sandbox.Utilities.StringUtilities
{
    public class StringSubstringsChecker : SubstringsChecker<string>
    {
        private bool IgnoreCase { get; set; }

        private StringSubstringsChecker(IEnumerable<string> substrings)
            : this(null, substrings, false)
        {
        }

        private StringSubstringsChecker(IEnumerable<string> substrings, bool ignoreCase)
            : this(null, substrings, ignoreCase)
        {
        }

        private StringSubstringsChecker(string source, IEnumerable<string> substrings, bool ignoreCase)
            : base(source)
        {
            SetSubstrings(substrings);
            SetIgnoreCase(ignoreCase);
        }

        private void SetSubstrings(IEnumerable<string> substrings)
        {
            AssertSubstrings(substrings);
            Substrings = substrings;
        }

        private static void AssertSubstrings(IEnumerable<string> substrings)
        {
            Assert.ArgumentNotNull(substrings, "substrings");
            Assert.ArgumentCondition(substrings.Any(), "substrings", "substrings must contain as at least one string!");
        }

        private void SetIgnoreCase(bool ignoreCase)
        {
            IgnoreCase = ignoreCase;
        }

        protected override bool CanDoCheck()
        {
            return !string.IsNullOrEmpty(Source);
        }

        protected override bool DoCheck()
        {
            Assert.ArgumentNotNullOrEmpty(Source, "Source");

            foreach (string substring in Substrings)
            {
                if(DoesSourceContainSubstring(substring))
                {
                    return true;
                }
            }

            return false;
        }

        private bool DoesSourceContainSubstring(string substring)
        {
            if (IgnoreCase)
            {
                return !IsNotFoundIndex(Source.IndexOf(substring, StringComparison.CurrentCultureIgnoreCase));
            }

            return !IsNotFoundIndex(Source.IndexOf(substring));
        }

        private static bool IsNotFoundIndex(int index)
        {
            const int notFound = -1;
            return index == notFound;
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings)
        {
            return new StringSubstringsChecker(substrings);
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(IEnumerable<string> substrings, bool ignoreCase)
        {
            return new StringSubstringsChecker(substrings, ignoreCase);
        }

        public static ISubstringsChecker<string> CreateNewStringSubstringsContainer(string source, IEnumerable<string> substrings, bool ignoreCase)
        {
            return new StringSubstringsChecker(source, substrings, ignoreCase);
        }
    }
}

In the version of our “checker” class above, I added the option to have the “checker” ignore case comparisons for the source and substrings.

Next, I created a new System.Web.Security.MembershipProvider subclass where I am utilizing the decorator pattern to decorate methods around changing passwords and creating users.

By default, we are instantiating an instance of the System.Web.Security.SqlMembershipProvider — this is what my local Sitecore sandbox instance is using, in order to get at the ASP.NET Membership tables in the core database.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Web.Security;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Security;

using Sitecore.Sandbox.Utilities.StringUtilities;
using Sitecore.Sandbox.Utilities.StringUtilities.Base;

namespace Sitecore.Sandbox.Security.MembershipProviders
{
    public class PreventDictionaryWordPasswordsMembershipProvider : MembershipProvider 
    {
        private static IEnumerable<string> _DictionaryWords;
        private static IEnumerable<string> DictionaryWords
        {
            get
            {
                if (_DictionaryWords == null)
                {
                    _DictionaryWords = GetDictionaryWords();
                }

                return _DictionaryWords;
            }
        }

        public override string Name
        {
            get
            {
                return InnerMembershipProvider.Name;
            }
        }

        public override string ApplicationName
        {
            get
            {
                return InnerMembershipProvider.ApplicationName;
            }
            set
            {
                InnerMembershipProvider.ApplicationName = value;
            }
        }

        public override bool EnablePasswordReset
        {
            get
            {
                return InnerMembershipProvider.EnablePasswordReset;
            }
        }

        public override bool EnablePasswordRetrieval
        {
            get
            {
                return InnerMembershipProvider.EnablePasswordRetrieval;
            }
        }

        public override int MaxInvalidPasswordAttempts
        {
            get
            {
                return InnerMembershipProvider.MaxInvalidPasswordAttempts;
            }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get
            {
                return InnerMembershipProvider.MinRequiredNonAlphanumericCharacters;
            }
        }

        public override int MinRequiredPasswordLength
        {
            get
            {
                return InnerMembershipProvider.MinRequiredPasswordLength;
            }
        }

        public override int PasswordAttemptWindow
        {
            get
            {
                return InnerMembershipProvider.PasswordAttemptWindow;
            }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get
            {
                return InnerMembershipProvider.PasswordFormat;
            }
        }

        public override string PasswordStrengthRegularExpression
        {
            get
            {
                return InnerMembershipProvider.PasswordStrengthRegularExpression;
            }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get
            {
                return InnerMembershipProvider.RequiresQuestionAndAnswer;
            }
        }

        public override bool RequiresUniqueEmail
        {
            get
            {
                return InnerMembershipProvider.RequiresUniqueEmail;
            }
        }

        private MembershipProvider InnerMembershipProvider { get; set; }
        private ISubstringsChecker<string> DictionaryWordsSubstringsChecker { get; set; }

        public PreventDictionaryWordPasswordsMembershipProvider()
            : this(CreateNewSqlMembershipProvider(), CreateNewDictionaryWordsSubstringsChecker())
        {
        }
        
        public PreventDictionaryWordPasswordsMembershipProvider(MembershipProvider innerMembershipProvider, ISubstringsChecker<string> dictionaryWordsSubstringsChecker)
        {
            SetInnerMembershipProvider(innerMembershipProvider);
            SetDictionaryWordsSubstringsChecker(dictionaryWordsSubstringsChecker);
        }

        private void SetInnerMembershipProvider(MembershipProvider innerMembershipProvider)
        {
            Assert.ArgumentNotNull(innerMembershipProvider, "innerMembershipProvider");
            InnerMembershipProvider = innerMembershipProvider;
        }

        private void SetDictionaryWordsSubstringsChecker(ISubstringsChecker<string> dictionaryWordsSubstringsChecker)
        {
            Assert.ArgumentNotNull(dictionaryWordsSubstringsChecker, "dictionaryWordsSubstringsChecker");
            DictionaryWordsSubstringsChecker = dictionaryWordsSubstringsChecker;
        }

        private static MembershipProvider CreateNewSqlMembershipProvider()
        {
            return new SqlMembershipProvider();
        }

        private static ISubstringsChecker<string> CreateNewDictionaryWordsSubstringsChecker()
        {
            return CreateNewStringSubstringsChecker(DictionaryWords);
        }

        private static ISubstringsChecker<string> CreateNewStringSubstringsChecker(IEnumerable<string> substrings)
        {
            Assert.ArgumentNotNull(substrings, "substrings");
            const bool ignoreCase = true;
            return StringSubstringsChecker.CreateNewStringSubstringsContainer(substrings, ignoreCase);
        }

        private static IEnumerable<string> GetDictionaryWords()
        {
            return Factory.GetStringSet("dictionaryWords/word");
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            if (DoesPasswordContainDictionaryWord(newPassword))
            {
                return false;
            }

            return InnerMembershipProvider.ChangePassword(username, oldPassword, newPassword);
        }

        private bool DoesPasswordContainDictionaryWord(string password)
        {
            Assert.ArgumentNotNullOrEmpty(password, "password");
            DictionaryWordsSubstringsChecker.Source = password;
            return DictionaryWordsSubstringsChecker.ContainsSubstrings();
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            return InnerMembershipProvider.ChangePasswordQuestionAndAnswer(username, password, newPasswordQuestion, newPasswordAnswer);
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            if (DoesPasswordContainDictionaryWord(password))
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            return InnerMembershipProvider.CreateUser(username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status);
        }

        public override bool DeleteUser(string userName, bool deleteAllRelatedData)
        {
            return InnerMembershipProvider.DeleteUser(userName, deleteAllRelatedData);
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            return InnerMembershipProvider.FindUsersByEmail(emailToMatch, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection FindUsersByName(string userNameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            return InnerMembershipProvider.FindUsersByName(userNameToMatch, pageIndex, pageSize, out totalRecords);
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            return InnerMembershipProvider.GetAllUsers(pageIndex, pageSize, out totalRecords);
        }

        public override int GetNumberOfUsersOnline()
        {
            return InnerMembershipProvider.GetNumberOfUsersOnline();
        }

        public override string GetPassword(string username, string answer)
        {
            return InnerMembershipProvider.GetPassword(username, answer);
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            return InnerMembershipProvider.GetUser(providerUserKey, userIsOnline);
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            return InnerMembershipProvider.GetUser(username, userIsOnline);
        }

        public override string GetUserNameByEmail(string email)
        {
            return InnerMembershipProvider.GetUserNameByEmail(email);
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            InnerMembershipProvider.Initialize(name, config);
        }

        public override string ResetPassword(string username, string answer)
        {
            return InnerMembershipProvider.ResetPassword(username, answer);
        }

        public override bool UnlockUser(string userName)
        {
            return InnerMembershipProvider.UnlockUser(userName);
        }

        public override void UpdateUser(MembershipUser user)
        {
            InnerMembershipProvider.UpdateUser(user);
        }

        public override bool ValidateUser(string username, string password)
        {
            return InnerMembershipProvider.ValidateUser(username, password);
        }
    }
}

In our MembershipProvider above, we have decorated the ChangePassword() and CreateUser() methods by employing a helper method to delegate to our DictionaryWordsSubstringsChecker instance — an instance of the StringSubstringsChecker class above — to see if the supplied password contains any of the fruits found in our collection, and prevent the workflow from moving forward in changing a user’s password, or creating a new user if one of the fruits is found in the provided password.

If we don’t find one of the fruits in the password, we then delegate to the inner MembershipProvider instance — this instance takes care of the rest around changing passwords, or creating new users.

I then had to register my MemberProvider in my Web.config — this cannot be placed in a patch include file since it lives outside of the <sitecore></sitecore> element.

<configuration>
	<membership defaultProvider="sitecore" hashAlgorithmType="SHA1">
		<providers>
			<clear/>
			<add name="sitecore" type="Sitecore.Security.SitecoreMembershipProvider, Sitecore.Kernel" realProviderName="sql" providerWildcard="%" raiseEvents="true"/>
			
			<!-- our new provider -->
			<add name="sql" type="Sitecore.Sandbox.Security.MembershipProviders.PreventDictionaryWordPasswordsMembershipProvider, Sitecore.Sandbox" connectionStringName="core" applicationName="sitecore" minRequiredPasswordLength="6" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" maxInvalidPasswordAttempts="256"/>
			
			<add name="switcher" type="Sitecore.Security.SwitchingMembershipProvider, Sitecore.Kernel" applicationName="sitecore" mappings="switchingProviders/membership"/>
		</providers>
    </membership>
</configuration>

So, let’s see what the code above does.

I first tried to create a new user with a password containing a fruit.

new-user-lime

I then used a fruit that was not in the collection of fruits above, and successfully created my new user.

Next, I tried to change an existing user’s password to one that contains the word “apple”.

change-password-apples

I successfully changed this same user’s password using the word orange — it does not live in our collection of fruits above.

change-password-oranges

And that’s all there is to it. If you can think of alternative ways of doing this, or additional security features to implement, please drop a comment.

Until next time, have a Sitecoretastic day!

Rip Out Sitecore Web Forms for Marketers Field Values During a Custom Save Action

A couple of weeks ago, I architected a Web Forms for Marketers (WFFM) solution that sends credit card information to a third-party credit card processor — via a custom form verification step — and blanks out sensitive credit card information before saving into the WFFM database.

Out of the box, WFFM will save credit card information in plain text to its database — yes, the data is hidden in the form report, although is saved to the database as plain text. This information being saved as plain text is a security risk.

One could create a solution similar to what I had done in my article discussing data encryption — albeit it might be in your best interest to avoid any headaches and potential security issues around saving and holding onto credit card information.

I can’t share the solution I built a couple of weeks ago — I built it for my current employer. I will, however, share a similar solution where I blank out the value of a field containing a social security number — a unique identifier of a citizen of the United States — of a person posing a question to the Internal Revenue Service (IRS) of the United States (yes, I chose this topic since tax season is upon us here in the US, and I loathe doing taxes :)).

First, I created a custom Web Forms for Marketers field for social security numbers. It is just a composite control containing three textboxes separated by labels containing hyphens:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;

using Sitecore.Form.Core.Attributes;
using Sitecore.Form.Core.Controls.Data;
using Sitecore.Form.Core.Visual;
using Sitecore.Form.Web.UI.Controls;

namespace Sitecore.Sandbox.WFFM.Controls
{
    public class SocialSecurityNumber : InputControl
    {
        private const string Hyphen = "-";
        private static readonly string baseCssClassName = "scfSingleLineTextBorder";
        protected TextBox firstPart;
        protected System.Web.UI.WebControls.Label firstMiddleSeparator;
        protected TextBox middlePart;
        protected System.Web.UI.WebControls.Label middleLastSeparator;
        protected TextBox lastPart;

        public SocialSecurityNumber() : this(HtmlTextWriterTag.Div)
        {
            firstPart = new TextBox();
            firstMiddleSeparator = new System.Web.UI.WebControls.Label { Text = Hyphen };
            middlePart = new TextBox();
            middleLastSeparator = new System.Web.UI.WebControls.Label { Text = Hyphen };
            lastPart = new TextBox();
        }

        public SocialSecurityNumber(HtmlTextWriterTag tag)
            : base(tag)
        {
            this.CssClass = baseCssClassName;
        }

        protected override void OnInit(EventArgs e)
        {
            SetCssClasses();
            SetTextBoxeWidths();
            SetMaxLengths();
            SetTextBoxModes();
            AddChildControls();
        }

        private void SetCssClasses()
        {
            help.CssClass = "scfSingleLineTextUsefulInfo";
            title.CssClass = "scfSingleLineTextLabel";
            generalPanel.CssClass = "scfSingleLineGeneralPanel";
        }

        private void SetTextBoxeWidths()
        {
            firstPart.Style.Add("width", "40px");
            middlePart.Style.Add("width", "35px");
            lastPart.Style.Add("width", "50px");
        }

        private void SetMaxLengths()
        {
            firstPart.MaxLength = 3;
            middlePart.MaxLength = 2;
            lastPart.MaxLength = 4;
        }

        private void SetTextBoxModes()
        {
            firstPart.TextMode = TextBoxMode.SingleLine;
            middlePart.TextMode = TextBoxMode.SingleLine;
            lastPart.TextMode = TextBoxMode.SingleLine;
        }

        private void AddChildControls()
        {
            Controls.AddAt(0, generalPanel);
            Controls.AddAt(0, title);
            
            generalPanel.Controls.Add(firstPart);
            generalPanel.Controls.Add(firstMiddleSeparator);
            generalPanel.Controls.Add(middlePart);
            generalPanel.Controls.Add(middleLastSeparator);
            generalPanel.Controls.Add(lastPart);
            generalPanel.Controls.Add(help);
        }

        public override string ID
        {
            get
            {
                return firstPart.ID;
            }
            set
            {
                title.ID = string.Concat(value, "_text");
                firstPart.ID = value;
                base.ID = string.Concat(value, "_scope");
                title.AssociatedControlID = firstPart.ID;
            }
        }

        public override ControlResult Result
        {
            get
            {
                return GetNewControlResult();
            }
        }

        private ControlResult GetNewControlResult()
        {
            TrimAllTextBoxes();
            return new ControlResult(ControlName, GetValue(), string.Empty);
        }

        private string GetValue()
        {
            bool hasValue = !string.IsNullOrEmpty(firstPart.Text) 
                            && !string.IsNullOrEmpty(middlePart.Text) 
                            && !string.IsNullOrEmpty(lastPart.Text);

            if (hasValue)
            {
                return string.Concat(firstPart.Text, firstMiddleSeparator.Text, middlePart.Text, middleLastSeparator.Text, lastPart.Text);
            }

            return string.Empty;
        }

        private void TrimAllTextBoxes()
        {
            firstPart.Text = firstPart.Text.Trim();
            middlePart.Text = middlePart.Text.Trim();
            lastPart.Text = lastPart.Text.Trim();
        }
    }
}

I then registered this custom field in Sitecore. I added my custom field item under /sitecore/system/Modules/Web Forms for Marketers/Settings/Field Types/Custom:

registered-ssn-field

Next, I decided to create a generic interface that defines objects that will excise or rip out values from a given object of a certain type, and returns a new object of another type — although both types could be the same:

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

namespace Sitecore.Sandbox.Utilities.Excisors.Base
{
    public interface IExcisor<T, U>
    {
        U Excise(T source);
    }
}

My WFFM excisor will take in a collection of WFFM fields and return a new collection where the targeted field values — field values that I don’t want saved into the WFFM database — are set to the empty string.

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

using Sitecore.Form.Core.Controls.Data;
using Sitecore.Form.Core.Client.Data.Submit;

namespace Sitecore.Sandbox.Utilities.Excisors.Base
{
    public interface IWFFMFieldValuesExcisor : IExcisor<AdaptedResultList, AdaptedResultList>
    {
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Sitecore.Diagnostics;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Core.Controls.Data;

using Sitecore.Sandbox.Utilities.Excisors.Base;

namespace Sitecore.Sandbox.Utilities.Excisors
{
    public class WFFMFieldValuesExcisor : IWFFMFieldValuesExcisor
    {
        private IEnumerable<string> FieldNamesForExtraction { get; set; }

        private WFFMFieldValuesExcisor(IEnumerable<string> fieldNamesForExtraction)
        {
            SetFieldNamesForExtraction(fieldNamesForExtraction);
        }

        private void SetFieldNamesForExtraction(IEnumerable<string> fieldNamesForExtraction)
        {
            Assert.ArgumentNotNull(fieldNamesForExtraction, "fieldNamesForExtraction");
            FieldNamesForExtraction = fieldNamesForExtraction;
        }

        public AdaptedResultList Excise(AdaptedResultList fields)
        {
            if(fields == null || fields.Count() < 1)
            {
                return fields;
            }

            List<AdaptedControlResult> adaptedControlResults = new List<AdaptedControlResult>();

            foreach(AdaptedControlResult field in fields)
            {
                adaptedControlResults.Add(GetExtractValueFieldIfApplicable(field));
            }

            return adaptedControlResults;
        }

        private AdaptedControlResult GetExtractValueFieldIfApplicable(AdaptedControlResult field)
        {
            if(ShouldExtractFieldValue(field))
            {
                return GetExtractedValueField(field);
            }

            return field;
        }

        private bool ShouldExtractFieldValue(ControlResult field)
        {
            return FieldNamesForExtraction.Contains(field.FieldName);
        }

        private static AdaptedControlResult GetExtractedValueField(ControlResult field)
        {
            return new AdaptedControlResult(CreateNewControlResultWithEmptyValue(field), true);
        }

        private static ControlResult CreateNewControlResultWithEmptyValue(ControlResult field)
        {
            ControlResult controlResult = new ControlResult(field.FieldName, string.Empty, field.Parameters);
            controlResult.FieldID = field.FieldID;
            return controlResult;
        }

        public static IWFFMFieldValuesExcisor CreateNewWFFMFieldValuesExcisor(IEnumerable<string> fieldNamesForExtraction)
        {
            return new WFFMFieldValuesExcisor(fieldNamesForExtraction);
        }
    }
}

For scalability purposes and to avoid hard-coding field name values, I put my targeted fields — I only have one at the moment — into a config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<settings>
			<setting name="ExciseFields.SocialSecurityNumberField" value="Social Security Number" />
		</settings>
	</sitecore>
</configuration>

I then created a custom save to database action where I call my utility class to rip out my specified targeted fields, and pass that through onto the base save to database action class:

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

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Form.Core.Client.Data.Submit;
using Sitecore.Form.Submit;

using Sitecore.Sandbox.Utilities.Excisors;
using Sitecore.Sandbox.Utilities.Excisors.Base;

namespace Sitecore.Sandbox.WFFM.Actions
{
    public class ExciseFieldValuesThenSaveToDatabase : SaveToDatabase
    {
        private static readonly IEnumerable<string> FieldsToExcise = new string[] { Settings.GetSetting("ExciseFields.SocialSecurityNumberField") };
        
        public override void Execute(ID formId, AdaptedResultList fields, object[] data)
        {
            base.Execute(formId, ExciseFields(fields), data);
        }

        private AdaptedResultList ExciseFields(AdaptedResultList fields)
        {
            IWFFMFieldValuesExcisor excisor = WFFMFieldValuesExcisor.CreateNewWFFMFieldValuesExcisor(FieldsToExcise);
            return excisor.Excise(fields);
        }
    }
}

I created a new item to register my custom save to database action under /sitecore/system/Modules/Web Forms for Marketers/Settings/Actions/Save Actions:
registered-save-db-action

I then created my form:

built-irs-form

I set this form on a new page item, and published all of my changes above.

Now, it’s time to test this out. I navigated to my form, and filled it in as an irate tax payer might:

filled-in-irs-form

Looking at my form’s report, I see that the social security number was blanked out before being saved to the database:

irs-form-report

A similar solution could also be used to add new field values into the collection of fields — values of fields that wouldn’t be available on the form, but should be displayed in the form report. An example might include a transaction ID or approval ID returned from a third-party credit card processor. Instead of ripping values, values would be inserted into the collection of fields, and then passed along to the base save to database action class.

Experiments with Field Data Encryption in Sitecore

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.