Introduction
Recently I was asked to encrypt several sections of a web.config file. But our configuration resides in sections that are implemented in external config files referenced from within the web.config, with the configSource
attribute.
There is a lot of help on the web, about how to encrypt or decrypt either with a tool or programmatically a section, but all these methods that are basically the same, simply fail to process the external files without even producing an error message.
Background
During the development of the project before I got involved, all configuration was stored in the AppSettings
and ConnectionStrings
sections or within custom sections, always in the web.config file. For maintenance and easy configuration when I was involved, I decided to split and organize all the above configuration values in various sections either custom or based on the NameValue
template.
I additionally decided that those sections should be implemented in external files, referenced from the web.config file. I don't believe that there is a point in me explaining the benefits of the above differences, but for me an additional flexibility for creating automated deployment procedures as always good.
I always like core development so everything I create is usually located in framework projects that provide a clean and out of the box functionality for other developers in the same organization. Because the framework code has grown a lot, I created a stripped down version project that contains the source files needed for this article only. I'm saying this because the complexity of the project may seem a little odd, but keep in mind that several parts are used for other functions that are not visible here.
Help on the tools or the API to encrypt or decrypt configuration files can be easily found by Googling for it.
Few Words About the Code
The code that does the actual encryption and decryption is located with the BaseConfigEncryption
that will hopefully be able to manage either web.config files or app.config files. At present, only web.config files are supported, the functionality of which is provided by its child class WebConfigEncryption
.
The EncryptionStartParameters
is a generic based class that inherits the functionality of handling the string
[]
args parameters of a main
function. Basically, the idea is to create a typed instance of what was asked, through the command argument list. It also handles arguments when the process is being executed as a Smart Client.
In order to encrypt or decrypt a config, you need the path of the application that has the targeted config file, the encryption provider for example DataProtectionConfigurationProvider
and the sections that we wish to encrypt. The same is required for decryption, but no provider is needed.
Arguments Examples
Sample examples of arguments are:
Keep in mind that the command line arguments handler is not designed to handle arguments as the several DOS commands and other tools do, but that is not the subject here.
Implementation
BaseConfigEncryption
does all the stuff with help from some other helper functions.
First of all, we must load the config file. This is a little tricky but overridable GetConfiguration
does the job returning a Configuration
object, thus the base class does not need to know if it is processing web or normal application configuration.
protected override System.Configuration.Configuration GetConfiguration()
{
WebConfigurationFileMap wcfm = new WebConfigurationFileMap();
wcfm.VirtualDirectories.Add("/",
new VirtualDirectoryMapping(base.RootPath, true, "web.config"));
return WebConfigurationManager.OpenMappedWebConfiguration(wcfm, "/");
}
At this point, the configuration has been loaded and we must encrypt or decrypt it.
The idea is this. Regardless of whether we are encrypting or decrypting, for each section that is required, we check whether that section is implemented within the root config file, such as web.config or within an external file.
foreach (string sectionName in sections)
{
ConfigurationSection section = configuration.GetSection(sectionName);
if (!String.IsNullOrEmpty(section.SectionInformation.ConfigSource))
{
this.sectionsWithExternalFiles.Add(sectionName);
}
}
So first business in order is to find all sections that are implemented in external files and populate them in sectionsWithExternalFiles
. For each of these sections, the idea I had was to copy the implementation from the external file in the root config file. This is done with the function MoveSectionsFromExternalFile
that iterates MoveSectionFromExternalFile
.
private void MoveSectionsFromExternalFile()
{
if (this.sectionsWithExternalFiles.Count == 0)
{
return;
}
XDocument xDocument = XDocument.Load(ConfigFilePath);
XElement xRoot = xDocument.Root;
string nameSpace = xDocument.Root.GetDefaultNamespace().NamespaceName;
foreach (string sectionName in this.sectionsWithExternalFiles)
{
MoveSectionFromExternalFile(xDocument.Root.Element
(XName.Get(sectionName, nameSpace)), sectionName);
}
xDocument.Save(ConfigFilePath);
ClearXmlNS(ConfigFilePath);
}
private void MoveSectionFromExternalFile(XElement xSection,string sectionName)
XAttribute xConfigSource=xSection.Attribute("configSource");
string configFile = xConfigSource.Value;
this.externalFiles.Add(sectionName,configFile);
XDocument xConfigFile = XDocument.Load(Path.Combine(RootPath, configFile));
xConfigSource.Remove();
xSection.Add(xConfigFile.Root.Attributes());
xSection.Add(xConfigFile.Root.Elements());
ClearXmlNS(xSection);
}
During the above process, the externalFiles
is populated with the section name and file name pair that will be used later for the same but reverse procedure.
At this point, we have a web.config file that has been merged with all the needed external files so the only thing to do is use the API mentioned and found by Google search engine.
For each section, we check whether it is encrypted or not and based on whether we are encrypting or decrypting, we call the appropriate command.
foreach (string sectionName in sections)
{
ConfigurationSection section = configuration.GetSection(sectionName);
switch (String.IsNullOrEmpty(protectionProvider))
{
case false:
if (!section.SectionInformation.IsProtected)
{
section.SectionInformation.ProtectSection(protectionProvider);
}
break;
case true:
if (section.SectionInformation.IsProtected)
{
section.SectionInformation.UnprotectSection();
}
break;
}
}
After that, we just have to move the section information from the root config file to the external files and reapply the configSource
attribute for these sections. To do this, we call MoveSectionsToExternalFile
that iterates MoveSectionToExternalFile
for each section.
private void MoveSectionsToExternalFile()
{
if (this.sectionsWithExternalFiles.Count == 0)
{
return;
}
XDocument xDocument = XDocument.Load(ConfigFilePath);
XElement xRoot = xDocument.Root;
string nameSpace = xDocument.Root.GetDefaultNamespace().NamespaceName;
foreach (string sectionName in this.sectionsWithExternalFiles)
{
MoveSectionToExternalFile(xDocument.Root.Element
(XName.Get(sectionName, nameSpace)), sectionName);
}
xDocument.Save(ConfigFilePath);
ClearXmlNS(ConfigFilePath);
}
private void MoveSectionToExternalFile(XElement xSection,string sectionName)
{
string configFile = this.externalFiles[sectionName];
XDocument xConfigFile = XDocument.Load(Path.Combine(RootPath, configFile));
xConfigFile.Root.RemoveAll();
xConfigFile.Root.Add(xSection.Attributes());
xConfigFile.Root.Add(xSection.Elements());
ClearXmlNS(xConfigFile.Root);
string configFilePath = Path.Combine(RootPath, configFile);
xConfigFile.SaveWithoutHeader(configFilePath);
ClearXmlNS(configFilePath);
xSection.RemoveAll();
xSection.Add(new XAttribute(XName.Get("configSource"), configFile));
}
All section transfers are one with the help of LINQ TO XML which is far better than the previous mechanism of accessing XML documents. The only trouble I got was with the namespaces in various nodes that were embedded in the edited XML. I believe it had something to do with the namespace declaration in the original config file.
After some search, I solved this problem by telling each element that its namespace name was empty and that left me with an xmlns=""
in the saved files. I then loaded the files, and replaced these literals with nothing and everything was ok. The functions that did this cleaning up are:
private void ClearXmlNS(XElement xElement)
{
foreach (XElement e in xElement.DescendantsAndSelf())
{
if (e.Name.Namespace != XNamespace.None)
{
e.Name = XNamespace.None.GetName(e.Name.LocalName);
}
}
}
private void ClearXmlNS(string filePath)
{
string text = File.ReadAllText(filePath);
text = text.Replace("xmlns=\"\"", "");
File.WriteAllText(filePath,text);
}
If you do not do the cleanup, then the namespaces are encrypted and the transparent decryption of the System.Configuration
throws an exception. It does so even with the blank namespace names. I know this may not be the best way, but I couldn't find a better way in the short time I had.
Points of Interest
During the development of this tool, there are some assumptions made.
- Because we have a specific pattern in naming the files according to the section names, there may be a problem if that pattern is not followed. I believe that it won't matter, but since I had that in mind while creating the tool, it is best for you to know that.
- Custom encryption providers are not supported, because I was not interested in them. I was creating a tool solely based on our project needs. In the future if a requirement is made, possibly it will be added.
- The tool has been tested only for the
DataProtectionConfigurationProvider
provider. - The tool has a
/?
argument that produces a help. The same help is produced if the arguments are not valid.
History
- Version1
- Article created on 5th March of 2010