Search This Blog

Saturday, March 6, 2010

More Configuration Protected Data Encryption

I'm back again with a new enhancement to my configuration encryption approach.  This time I wanted to tackle the ability to encrypt the values in the <appsettings> block but still have all the key names readable.

Since this ties to the connection strings, I decided a little refactoring was in order.  I created a base class to contain all the shared functionality:


using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;

namespace ProtectedConfiguration
{
[PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
public abstract class ProtectedConfigurationProviderBase : ProtectedConfigurationProvider
{
#region members

// Provider name
private string pName;

// Create Entropy To salt the process aka 'Magic Salt'
private readonly byte[] entropy = Encoding.Unicode.GetBytes("magicsalt");

#endregion






#region Overrides of ProviderBase

//
// ProviderBase.Name
//
public override string Name
{
get { return pName; }
}

#endregion




#region Overrides of ProtectedConfigurationProvider

//
// ProviderBase.Initialize
//
public override void Initialize(string name, NameValueCollection config)
{
pName = name;
base.Initialize(name, config);
}

#endregion



#region Methods

/// <summary>
/// Encrypt the passed string
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
protected string EncryptData(string data)
{
//convert to byte array
byte[] valBytes = Encoding.ASCII.GetBytes(data);


// Use DPAPI to Encrypt
byte[] encryptedData = ProtectedData.Protect
(valBytes,
entropy, DataProtectionScope.LocalMachine);

//convert to base64 and wrap with xml tags
return Convert.ToBase64String(encryptedData);
}


/// <summary>
/// Decrypt the passed string
/// </summary>
/// <param name="encryptedData"></param>
/// <returns></returns>
protected string DecryptData(string encryptedData)
{
string password = encryptedData;

//convert to encrypted byte array
byte[] valBytes = Convert.FromBase64String(password);


// Decrypt using DPAPI
byte[] decryptedData = ProtectedData.Unprotect
(valBytes, entropy, DataProtectionScope.LocalMachine);

var encoding = new ASCIIEncoding();

//convert to ascii
return encoding.GetString(decryptedData);
}

#endregion
}
}


I also made some changes to my ConnectionStringProtectedConfigurationProvider.  I made it db agnostic, so any connection string, SqlServer, Oracle, DB2, etc. will work as long as it has the word "Password" in it.  Of course I also changed it to inherit my base class above:

using System;
using System.Data.Common;
using System.Security.Permissions;
using System.Xml;
using System.Security.Cryptography;
using System.Text;
using System.Collections.Specialized;
using System.Configuration;


namespace ProtectedConfiguration
{
public sealed class ConnectionStringProtectedConfigurationProvider : ProtectedConfigurationProviderBase
{
#region Overrides of ProtectedConfigurationProvider

/// <summary>
/// Encrypts the passed <see cref="T:System.Xml.XmlNode" /> object from a configuration file.
/// </summary>
/// <returns>
/// The <see cref="T:System.Xml.XmlNode" /> object containing encrypted data.
/// </returns>
/// <param name="node">The <see cref="T:System.Xml.XmlNode" /> object to encrypt.</param>
public override XmlNode Encrypt(XmlNode node)
{
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;

XmlNodeList nodeList = node.SelectNodes("/connectionStrings/add");
if (nodeList != null)
{
for (int i = 0; i < nodeList.Count; i++)
{
XmlAttribute attribute = nodeList[i].Attributes["connectionString"];
DbConnectionStringBuilder builder = new DbConnectionStringBuilder
{ConnectionString = attribute.Value};
if (builder.ContainsKey("Password") && !string.IsNullOrEmpty(builder["Password"].ToString()))
{
builder["Password"] = EncryptData(builder["Password"].ToString());
attribute.Value = builder.ToString();
}
}
}
doc.LoadXml("<EncryptedData>" + node.InnerXml +"</EncryptedData>");

return doc.DocumentElement;
}

/// <summary>
/// Decrypts the passed <see cref="T:System.Xml.XmlNode" /> object from a configuration file.
/// </summary>
/// <returns>
/// The <see cref="T:System.Xml.XmlNode" /> object containing decrypted data.
/// </returns>
/// <param name="encryptedNode">The <see cref="T:System.Xml.XmlNode" /> object to decrypt.</param>
public override XmlNode Decrypt(XmlNode encryptedNode)
{
//get each <add> in the connection string block
XmlNodeList nodeList = encryptedNode.SelectNodes("/EncryptedData/add");

//start building the output xml string
StringBuilder output = new StringBuilder();
output.Append("<connectionStrings>");

if (nodeList != null)
{
for (int i = 0; i < nodeList.Count; i++)
{
//get the text for the connection string
XmlAttribute attribute = nodeList[i].Attributes["connectionString"];

//use DbConnectionStringBuilder to parse
DbConnectionStringBuilder builder = new DbConnectionStringBuilder
{ConnectionString = attribute.Value};
if (builder.ContainsKey("Password") && !string.IsNullOrEmpty(builder["Password"].ToString()))
{
//decrypt it
builder["Password"] = DecryptData(builder["Password"].ToString());
attribute.Value = builder.ToString();
}
//add the connection string with decrypted password to output
output.Append(nodeList[i].OuterXml);
}
}
//add closing tag
output.Append("</connectionStrings>");

XmlDocument xmlDoc = new XmlDocument();
xmlDoc.PreserveWhitespace = true;

xmlDoc.LoadXml(output.ToString());

return xmlDoc.DocumentElement;
}

#endregion
}
}


And on to the AppSettings, nothing too fancy, at least after you've seen the above it's really just more of the same:


using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Security.Permissions;
using System.Xml;
using System.Security.Cryptography;
using System.Text;
using System.Collections.Specialized;
using System.Configuration;

namespace ProtectedConfiguration
{
class AppSettingsProtectedConfigurationProvider : ProtectedConfigurationProviderBase
{
#region members

private List<string> keys = new List<string>();

#endregion



#region Overrides of ProtectedConfigurationProvider

/// <summary>
/// Stores the list of keys that need encrypted
/// </summary>
/// <param name="name"></param>
/// <param name="config"></param>
public override void Initialize(string name, NameValueCollection config)
{
keys = new List<string>(config["keys"].Split(','));
base.Initialize(name, config);
}

/// <summary>
/// Encrypts the passed <see cref="T:System.Xml.XmlNode" /> object from a configuration file.
/// </summary>
/// <returns>
/// The <see cref="T:System.Xml.XmlNode" /> object containing encrypted data.
/// </returns>
/// <param name="node">The <see cref="T:System.Xml.XmlNode" /> object to encrypt.</param>
public override XmlNode Encrypt(XmlNode node)
{
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;

XmlNodeList nodeList = node.SelectNodes("/appSettings/add");
foreach (XmlNode keyvaluepair in nodeList)
{
XmlAttribute key = keyvaluepair.Attributes["key"];
XmlAttribute value = keyvaluepair.Attributes["value"];

if (keys.Contains(key.Value))
{
value.Value = EncryptData(value.Value);

}
}
doc.LoadXml("<EncryptedData>" + node.InnerXml + "</EncryptedData>");

return doc.DocumentElement;
}

/// <summary>
/// Decrypts the passed <see cref="T:System.Xml.XmlNode" /> object from a configuration file.
/// </summary>
/// <returns>
/// The <see cref="T:System.Xml.XmlNode" /> object containing decrypted data.
/// </returns>
/// <param name="encryptedNode">The <see cref="T:System.Xml.XmlNode" /> object to decrypt.</param>
public override XmlNode Decrypt(XmlNode encryptedNode)
{
//start building the output xml string
StringBuilder output = new StringBuilder();
output.Append("<appSettings>");

//get each <add> in the appSettings block
XmlNodeList nodeList = encryptedNode.SelectNodes("/EncryptedData/add");

foreach (XmlNode keyvaluepair in nodeList)
{
XmlAttribute key = keyvaluepair.Attributes["key"];
XmlAttribute value = keyvaluepair.Attributes["value"];

if (keys.Contains(key.Value))
{
value.Value = DecryptData(value.Value);

}
output.Append(keyvaluepair.OuterXml);
}
output.Append("</appSettings>");

XmlDocument xmlDoc = new XmlDocument();
xmlDoc.PreserveWhitespace = true;

xmlDoc.LoadXml(output.ToString());

return xmlDoc.DocumentElement;
}

#endregion
}
}


You also have to modify the web.config to tell it what assembly to use for the encryption and decryption. There are 2 entries here. One for connection strings, and one for appSettings. Notice in the AppSettingsProtectedConfigurationProvider there is an attribute named keys. It lists the keys in the appSettings, by name, in a comma seperated list that should be encrypted.

<configProtectedData>
<providers>
<add name="ConnectionStringProtectedConfigurationProvider" type="ProtectedConfiguration.ConnectionStringProtectedConfigurationProvider,ProtectedConfiguration"/>
<add name="AppSettingsProtectedConfigurationProvider" type="ProtectedConfiguration.AppSettingsProtectedConfigurationProvider,ProtectedConfiguration" keys="testkey1,testkey2"/>
</providers>
</configProtectedData>

And last, but not least, to do the encryption, you will need to generate a strong name key, and give your assembly a strong name. Once that is done, you can add it to the GAC and use aspnet_regiis.exe to encrypt your web config.
There is an alternative, do it with code. I added this to the global.asax.cs:


protected void Application_Start(object sender, EventArgs e)
{
/**************************************
* Verify connection string is encrypted
* ***********************************/

//get the application path so we can open the config file for writing
string path = HttpContext.Current.Request.ApplicationPath;
//open the config file
Configuration config = WebConfigurationManager.OpenWebConfiguration(path);
//get the connection string section
ConfigurationSection configurationSection;

configurationSection = config.GetSection("connectionStrings");
//If it's not encrypted, encrypt it.
if (configurationSection != null && !configurationSection.SectionInformation.IsProtected)
{
configurationSection.SectionInformation.ProtectSection("ConnectionStringProtectedConfigurationProvider");
configurationSection.SectionInformation.ForceSave = true;

config.Save(ConfigurationSaveMode.Full);

}

configurationSection = config.GetSection("appSettings");
//If it's not encrypted, encrypt it.
if (configurationSection != null && !configurationSection.SectionInformation.IsProtected)
{
configurationSection.SectionInformation.ProtectSection("AppSettingsProtectedConfigurationProvider");
configurationSection.SectionInformation.ForceSave = true;

config.Save(ConfigurationSaveMode.Full);
}


}

No comments: