Practical .NET
Decrypting (But Not Really Encrypting) Configuration Settings in ASP.NET Core
You can store encrypted values in your ASP.NET Core configuration file and seamlessly decrypt the values as you retrieve them. But there are, at least, two issues that you'll need to address.
When I started teaching the ASP.NET course I wrote for Learning Tree International, one of the first questions I was asked (and that the course didn't answer) was: "How can I have encrypted settings in the web.config file?" It's a fair question -- I was once told that 50 percent of security breaches are performed by people inside the organization who have access to the kind of information in, well, your application's configuration file. As Microsoft advises, you should "Never store passwords or other sensitive data in configuration provider code or in plain text configuration files."
So I shouldn't have been surprised when, in an early run of my ASP.NET Core for ASP.NET MVC Developers course, someone wanted to know how to work with encrypted information in ASP.NET Core's appsettings.json file ( that's the file that replaces the appsettings and connectionStrings sections of the ASP.NET web.config file).
I was surprised to discover that, even in version 3.0, there isn't a built-in decryption option just for the configuration manager (.NET Core's DataProtectionProvider provides a more general solution). I was also unable to find a third-party, open source solution. So, in the spirit of the community, here's how to create one ... with a couple of caveats that I'll discuss as they come up.
Before I begin, however, I should mention that you can, for development purposes, store sensitive information in Visual Studio's User Secrets. However, that's intended just to keep sensitive information from going into your source control system in plain text. Don't get me wrong: You should be using User Secrets, but you need a different solution for production. If your application is running in production in Azure, you should be using the Azure Key Vault. So this post is also only about on-premises solutions.
Initializing Decryption
Fundamentally, the goal is to add a decryption to the pipeline that processes your configuration file. The starting point for that is to create two classes. The first class inherits from the ConfigurationManager class that comes with ASP.NET Core and will, eventually, handle the decryption:
public class DecryptedConfiguration : ConfigurationProvider
{
}
Through the ConfigurtionProvider base class, this class also inherits the IConfigurationBuilder interface.
However, in order to support fitting your ConfigurationManager class into the configuration management pipeline, you also need a class that implements the IConfigurationSource interface. Adding that interface will cause this second class to have a Build method and, in that Build method, you must return an instance of your first class. The initial version of that class just needs to return a new instance of your DecryptedConfiguration class so, initially, it looks like this:
public class DecryptedConfigurationSource : IConfigurationSource
{
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new DecryptedConfiguration();
}
}
Decrypting Settings
I'm going to assume that you'll use AES symmetrical decryption for your encrypted value. Under that assumption, the next step is to add a constructor to the DecryptedConfiguration class to accept the two parameters that AES requires for encryption and decryption. In the code for that constructor, you use the static Create method on the Aes class to create an Aes object and then use that object to generate a decryptor class (I'm also going to generate an encryptor though, as you'll see, that's of questionable value). That decryptor/encryptor pair needs to be stored in fields so that you can use them in other methods in your DecryptedConfiguration class.
With those fields and properties in place, the DecryptedConfiguration class looks like this:
public class DecryptedConfiguration : ConfigurationProvider
{
public ICryptoTransform Decryptor;
private ICryptoTransform Encryptor;
internal DecryptedConfiguration(byte[] Key, byte[] IV)
{
Aes aes = Aes.Create();
Decryptor = aes.CreateDecryptor(Key, IV);
Decryptor = aes.CreateDecryptor(Key, IV);
}
The job of that other class you created -- the IConfigurationSource class -- is to configure your DecryptedConfiguration class while adding it to the configuration processing pipeline. So we'll now need to enhance the DecryptedConfigurationSource class to accept the AES parameters and use them when creating the DecryptionConfiguration object. The result looks like this:
public class DecryptedConfigurationSource : IConfigurationSource
{
private byte[] Key;
private byte[] IV;
public DecryptedConfigurationSource(byte[] Key, byte[] IV)
{
this.Key = Key;
this.IV = IV;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new DecryptedConfiguration(Key, IV);
}
}
I'll show how to generate those two parameters later, but their existence brings us to the first of those issues I mentioned: Where do you store those two parameters? Obviously, you'll want to use the same values each time you access your appsettings.json file, so you only want to generate the parameters once and then store them somewhere -- somewhere very safe because, if you lose them, your configuration file becomes unreadable, even to you. I'll leave that problem to you.
Decrypting/Encrypting
Now we're ready to decrypt settings. Back in the DecryptedConfiguration class, you just need to override its base class's TryGet method to decrypt values on the way out, like this:
public override bool TryGet(string key, out string value)
{
if (base.TryGet(key, out value))
{
byte[] decryptedBytes = Convert.FromBase64String(value);
byte[] textBytes = Decryptor.TransformFinalBlock(decryptedBytes, 0, decryptedBytes.Length);
value = Encoding.Unicode.GetString(textBytes);
return true;
}
return false;
}
Simple, right? But this brings us to the second issue with this design: saving values. Yes, you can override the base class's Set method to encrypt incoming values. That code looks like this:
public override void Set(string key, string value)
{
byte[] textBytes = Encoding.Unicode.GetBytes(value);
byte[] decryptedBytes = Decryptor.TransformFinalBlock(textBytes, 0, textBytes.Length);
base.Set(key, Convert.ToBase64String(decryptedBytes));
}
However, this method doesn't write those values to the appsettings.json file. As far as ASP.NET is concerned, the appsettings.json file is a read-only file. You'll need to put together some application (using the encryption/decryption code I've shown here and the same two AES parameters that you'll use with your application) to generate the encrypted values that you'll then stuff into your appsettings file.
With those caveats out of the way, you're now ready to add your decrypting class to your appsettings pipeline.
Incorporating Decryption
To use your new configuration class in the Startup class you'll need to add the following code to the Configure method, integrating it with ASP.NET's various configuration files. In the following code, I've assumed that you'll put all your decrypted data in the environment-specific aspsettings file and use the default appsettings.json file for unencrypted data.
Also in the following code, I'm finally generating the two parameters that AES needs. But, in real life you won't want to do it as I show here: This code creates a new set of the parameters every time it's run. With this code, I'd be trying to decrypt data with a set of parameters different from the ones used to encrypt the data -- that isn't going to work. You only want to generate these parameters once and store them forever.
Here's the code to create the parameters:
Aes aes = Aes.Create();
byte[] key = aes.Key;
byte[] iv = aes.IV;
And here's the code that incorporates your DecryptedConfiguration class into the configuration pipeline, using the two parameters:
IConfigurationBuilder encryptingBuilder =
new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Add(new EncryptedConfigurationSource(key, iv))
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
IConfiguration cfg = encryptingBuilder.Build();
You can now read encrypted values from the configuration file with code like this:
string ecryptedValue = cfg.GetValue("WebServices:VendorUrl");
And, for what it's worth, you can update (in memory only) those values with code like this:
cfg["WebServices:VendorUrl"] = "https://www.phvis.com";
Security is tough and I obviously haven't provided a complete solution here. It's still up to you to figure out where to store your decryption parameters and how you'll generate the encrypted values to store in appsettings.json file. So: You're welcome (sort of)!
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.