Encrypting Configuration Settings in .NET 2.0

This article describes a utility called ProtectConfig that can be used to Encrypt/Decrypt .NET 2.0 configuration files using the ProtectSection (https://msdn.microsoft.com/en-us/library/system.configuration.sectioninformation.protectsection.aspx) API. The source code of the utility is available here:

Here's what the ProtectConfig utility looks like:

image

If you have ever worked on a large project that involved accessing a database server or other network resources, you will have realised at some stage a lot of potentially sensitive information (credentials, settings etc.) end up inside the configuration files. Ideally, you would want to store all this information in a secure data store. Some information however, such as the database connection strings, are best placed in the configuration file. Thankfully, .NET Framework 2.0 ships with a great utility called aspnet_regiis.exe that can be used to encrypt configuration settings in a web.config file. The encryption can be based on the RSA provider or Windows' own Data Protection API (DPAPI). The RSA provider also allows you to tie the encryption to a user certificate so that you can use the same encrypted data across multiple hosts (provided each of them has the correct certificate in its certificate store). More information about aspnet_regiis.exe can be found here: https://msdn.microsoft.com/en-us/library/ms998280.aspx.

What a lot of people don't realise is that it is possible to encrypt the settings of any .NET 2.0 (or upwards) application, not just web sites and web applications. What lies behind the aspnet_regiis utility is the ProtectSection method (https://msdn.microsoft.com/en-us/library/system.configuration.sectioninformation.protectsection.aspx) of the SectionInformation class. This method lets you encrypt a configuration section belonging to a .NET configuration file based on a given protection provider. At the moment there are two protection providers (RSA and DPAPI) available out of the box although there is nothing stopping you from writing one.

Recently, I needed to encrypt a large set of configuration files for a client. We decided to go with the DPAPI provider because we considered it secure enough (it is what Windows machines use to protect logged on user credentials) and we also wanted to tie the encrypted configuration to the machine it was running on. The configuration files to be encrypted were a mixture of background services, web services and web sites. I realised that we would have to provide the deployment team a way to encrypt all these confguration files. My first thought was to go with aspnet_regiis. However, seeing the large number of files and the discomfort the deployment team expressed with a command-line tool I decided to write something that would make their life and mine a bit easier. This was where ProtectConfig was born.

At the moment, ProtectConfig uses DPAPI to encrypt settings and encrypts the appSettings, connectionStrings, identity and authorization sections. It is smart enough to detect the presence of these sections in the configuration file and then process them. It can also be used to decrypt settings when needed. The really nice thing about ProtectConfig though is that it can process a folder full of .config files (recursively or non-recursively). This makes it easy to encrypt/decrypt all the files on a host in one go.

Lets take a quick look at the code. The meat of the application lies in two methods: _EncryptConfig and _DecryptConfig. Here's what _EncryptConfig looks like:

private void _EncryptConfig(string fileName)
{
if (String.IsNullOrEmpty(fileName) == false && File.Exists(fileName))
{
lbLog.Items.Add("Analyzing " + fileName);

XmlDocument configDom = new XmlDocument();
configDom.Load(fileName);
bool hasConfig = false;
bool hasAppSettings = false;
bool hasConnectionStrings = false;
bool hasIdentity = false;
bool hasAuthorization = false;
bool hasSystemWeb = false;

XmlNode node = configDom.SelectSingleNode("/*[local-name()='configuration']");
if (node != null)
{
lbLog.Items.Add(fileName + " has a <configuration> section.");
hasConfig = true;
}
else
{
lbLog.Items.Add(fileName + " has no <configuration> section. This file will not be processed.");
}

if (hasConfig)
{
node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='appSettings']");
if (node != null)
{
lbLog.Items.Add("Found appSettings");
hasAppSettings = true;
}

node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='connectionStrings']");
if (node != null)
{
lbLog.Items.Add("Found connectionStrings");
hasConnectionStrings = true;
}

node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']");
if (node != null)
{
hasSystemWeb = true;

XmlNode innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='identity']");
if (innerNode != null)
{
lbLog.Items.Add("Found identity");
hasIdentity = true;
}

innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='authorization']");
if (innerNode != null)
{
lbLog.Items.Add("Found authorization");
hasAuthorization = true;
}
}

ExeConfigurationFileMap map = new ExeConfigurationFileMap();
map.ExeConfigFilename = fileName;
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

if (hasAppSettings)
{
if (config.AppSettings.SectionInformation.IsProtected)
{
lbLog.Items.Add("appSettings is already protected!");
}
else
{
lbLog.Items.Add("Encrypting appSettings");
config.AppSettings.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
}
}

if (hasConnectionStrings)
{
if (config.ConnectionStrings.SectionInformation.IsProtected)
{
lbLog.Items.Add("connectionStrings is already protected!");
}
else
{
lbLog.Items.Add("Encrypting connectionStrings");
config.ConnectionStrings.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
}
}

if (hasSystemWeb)
{
ConfigurationSectionGroup systemwebgroup = config.SectionGroups["system.web"];

if (hasIdentity)
{
ConfigurationSection section = systemwebgroup.Sections["identity"];
if (section.SectionInformation.IsProtected)
{
lbLog.Items.Add("identity is already protected!");
}
else
{
lbLog.Items.Add("Encrypting identity");
section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
}
}

if (hasAuthorization)
{
ConfigurationSection section = systemwebgroup.Sections["authorization"];
if (section.SectionInformation.IsProtected)
{
lbLog.Items.Add("authorization is already protected!");
}
else
{
lbLog.Items.Add("Encrypting authorization");
section.SectionInformation.ProtectSection("DataProtectionConfigurationProvider");
}
}
}

lbLog.Items.Add("Done encrypting information. Saving file " + config.FilePath);

config.Save();

lbLog.Items.Add("File " + config.FilePath + " saved to disk.");
}
}
else
{
lbLog.Items.Add("Can't find the file " + fileName);
}
}

The algorithm itself is quite simple. The method first loads up a configuration file in an XmlDocument and checks what sections it has available. After that, armed with this information it goes about encrypting each of these sections using the ProtectSection method of the SectionInformation class. Notice the argument to the ProtectSection method. This argument (DataProtectionConfigurationProvider) is what tells the method to use DPAPI to encrypt the section. To use the RSA provider, simply use the string RsaProtectedConfigurationProvider as argument. To learn how to create and export an RSA key container for use with ProtectSection, see https://msdn.microsoft.com/en-us/library/2w117ede.aspx.

The code also makes use of the IsProtected property to ensure that it isn't encrypting a configuration section that is already encrypted. The _DecryptConfig method basically reverses the whole process and uses UnprotectSection to decrypt the configuration. Here's what it looks like:

private void _DecryptConfig(string fileName)
{
if (String.IsNullOrEmpty(fileName) == false && File.Exists(fileName))
{
lbLog.Items.Add("Analyzing " + fileName);

XmlDocument configDom = new XmlDocument();
configDom.Load(fileName);
bool hasConfig = false;
bool hasAppSettings = false;
bool hasConnectionStrings = false;
bool hasIdentity = false;
bool hasAuthorization = false;
bool hasSystemWeb = false;

XmlNode node = configDom.SelectSingleNode("/*[local-name()='configuration']");
if (node != null)
{
lbLog.Items.Add(fileName + " has a <configuration> section.");
hasConfig = true;
}
else
{
lbLog.Items.Add(fileName + " has no <configuration> section. This file will not be processed.");
}

if (hasConfig)
{
node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='appSettings']");
if (node != null)
{
lbLog.Items.Add("Found appSettings");
hasAppSettings = true;
}

node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='connectionStrings']");
if (node != null)
{
lbLog.Items.Add("Found connectionStrings");
hasConnectionStrings = true;
}

node = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']");
if (node != null)
{
hasSystemWeb = true;

XmlNode innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='identity']");
if (innerNode != null)
{
lbLog.Items.Add("Found identity");
hasIdentity = true;
}

innerNode = configDom.SelectSingleNode("/*[local-name()='configuration']/*[local-name()='system.web']/*[local-name()='authorization']");
if (innerNode != null)
{
lbLog.Items.Add("Found authorization");
hasAuthorization = true;
}
}

ExeConfigurationFileMap map = new ExeConfigurationFileMap();
map.ExeConfigFilename = fileName;
Configuration config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

if (hasAppSettings)
{
if (config.AppSettings.SectionInformation.IsProtected)
{
lbLog.Items.Add("Decrypting appSettings");
config.AppSettings.SectionInformation.UnprotectSection();
}
else
{
lbLog.Items.Add("appSettings is not encrypted!");
}
}

if (hasConnectionStrings)
{
if (config.ConnectionStrings.SectionInformation.IsProtected)
{
lbLog.Items.Add("Decrypting connectionStrings");
config.ConnectionStrings.SectionInformation.UnprotectSection();
}
else
{
lbLog.Items.Add("connectionStrings is not encrypted!");
}
}

if (hasSystemWeb)
{
ConfigurationSectionGroup systemwebgroup = config.SectionGroups["system.web"];

if (hasIdentity)
{
ConfigurationSection section = systemwebgroup.Sections["identity"];
if (section.SectionInformation.IsProtected)
{
lbLog.Items.Add("Decrypting identity");
section.SectionInformation.UnprotectSection();
}
else
{
lbLog.Items.Add("identity is not encrypted!");
}
}

if (hasAuthorization)
{
ConfigurationSection section = systemwebgroup.Sections["authorization"];
if (section.SectionInformation.IsProtected)
{
lbLog.Items.Add("Decrypting authorization");
section.SectionInformation.UnprotectSection();
}
else
{
lbLog.Items.Add("authorization is not encrypted!");
}
}
}

lbLog.Items.Add("Done decrypting information. Saving file " + config.FilePath);

config.Save();

lbLog.Items.Add("File " + config.FilePath + " saved to disk.");
}
}
else
{
lbLog.Items.Add("Can't find the file " + fileName);
}
}

The really great thing about encrypting your configuration using ProtectSection is that you don't need to change the code of your application at all. The code for ProtectConfig is available here: