I’ve written before about my use of Microsoft’s options pattern before. More specifically, I’ve written about my extensions of that pattern, to include validations. This time I though I would write about another extension I wrote for options – this time, for encrypting or decrypting selected properties on an options class.
Before I start though, let’s step back and look at why I would want to do that in the first place. After all, Microsoft already has the “user secrets” provider for their configuration extensions, right? Well, yes, they do, but, that’s only intended for keeping sensitive data out of source code, and so, out of TFS, or GIT repositories. After your code moves from development, to QA, or even on to production, the task of protecting sensitive data in configuration sources becomes yet another exercise left up to the developer. [Resist temptation to rant, here …]
So, what are the options, beyond “user secrets” on a developer machine? Well, quite a few, actually. For instance, there are key vaults in Azure, and key management services, in AWS. There is also the DPAPI library, if cloud based options aren’t an option, for whatever reason. There is also, of course, the option to roll your own crypto provider. That sounds like a lot of hard work though, so, I’ll avoid that possibility.
I suppose, like so many other development choices, it really boils down to the needs of the project. After all, there isn’t a clearly defined “best approach” that works in all situations and any direction we move in will have tradeoffs. Cloud based services, like Azure and AWS, usually require a paid subscription, 24×7 connectivity to the cloud, and also the willingness among developers, management, and other stakeholders, to allow sensitive data to be stored offsite. However, if you do decide go that route, Microsoft has an app configuration extension available for just that scenario. HERE is a good link to get you started.
Let me be clear, the approach I’ll outline in this blog post will NOT have the capabilities of a Cloud based key protection scheme. So, if you think Cloud services are right for you, take a look at the Microsoft provider I mentioned above, instead of the approach I’m about to go into.
For my purposes, I often have a need to protect my configuration data, on non-development machines, in a way that (1) won’t require a credit card for those pesky subscription fees, (2) will always work – even if I don’t have connectivity to the Interwebz, and (3) won’t require me to hide a password anywhere on my machine. That takes Cloud based products out of consideration. It also narrows the other options down to a solution based on Microsoft’s DPAPI library, since I’ve already said I won’t roll my own encryption library. That’s fine though, DPAPI is super easy to use and it works well enough for my needs. The only drawback, to using a DPAPI based solution, that I can think of, is that I’ll have to re-encrypt data whenever I move a configuration file to another machine. I can live with that.
Let’s go look at my code now.
I envision the cryptographic operations driven by the use of a custom attribute and, at least, two extension methods – one to encrypt, another to decrypt. Let’s start with that attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class ProtectedPropertyAttribute : Attribute { public byte[] Entropy { get; set; } public DataProtectionScope? Scope { get; set; } }
The ProtectedPropertyAttribute
class contains the Entropy
property, which can be used to add custom entropy to the cryptographic operation. It also contains the Scope
property, which can be used to specify a custom scope for the cryptographic operation. Using this attribute on your own options class is easy, that would look something like this:
class MyOptions : OptionsBase { [ProtectedProperty] public string Password{ get; set;} }
Here is a method for encrypting any string properties, on any object with public properties that are decorated with my ProtectedPropertyAttribute
attribute:
public static IConfiguration EncryptProperties( this IConfiguration configuration, object options, DataProtectionScope? dataProtectionScope = null, byte[] entropy = null ) { Guard.Instance().ThrowIfNull(configuration, nameof(configuration)) .ThrowIfNull(options, nameof(options)); var props = options.GetType().GetProperties() .Where( x => x.PropertyType.IsClass && x.PropertyType != typeof(string) ).ToList(); props.ForEach(prop => { object obj = null; try { obj = prop.GetGetMethod().Invoke( options, new object[0] ); if (null != obj) { configuration.EncryptProperties( obj, dataProtectionScope, entropy ); } } catch (Exception ex) { throw new InvalidOperationException( message: string.Format( Resources.ConfigurationExtensions_EncryptProperties, prop.Name, obj.GetType().Name ), innerException: ex ); } }); props = options.GetType().GetProperties() .Where( x => x.CanRead && x.CanWrite && x.PropertyType == typeof(string) ).ToList(); props.ForEach(prop => { try { var attr = prop.GetCustomAttributes(true) .FirstOrDefault( x => x.GetType() == typeof(ProtectedPropertyAttribute) ) as ProtectedPropertyAttribute; if (null != attr) { var unprotectedPropertyValue = prop.GetGetMethod().Invoke( options, new object[0] ) as string; if (false == string.IsNullOrEmpty(unprotectedPropertyValue)) { var unprotectedBytes = Encoding.UTF8.GetBytes( unprotectedPropertyValue ); if (null == entropy || entropy.Length == 0) { if (attr.Entropy != null) { entropy = attr.Entropy; } else { entropy = new byte[] { 4, 8, 15, 16, 23, 42 }; } } if (null == dataProtectionScope) { if (null != attr.Scope) { dataProtectionScope = attr.Scope; } else { dataProtectionScope = DataProtectionScope.LocalMachine; } } var protectedBytes = ProtectedData.Protect( unprotectedBytes, entropy, dataProtectionScope.Value ); var protectedPropertyValue = Convert.ToBase64String( protectedBytes ); prop.GetSetMethod().Invoke( options, new[] { protectedPropertyValue } ); } } } catch (Exception ex) { throw new InvalidOperationException( message: string.Format( Resources.ConfigurationExtensions_EncryptProperties, prop.Name, options.GetType().Name ), innerException: ex ); } }); return configuration; }
Let’s walk through the EncryptProperties
method. It starts by validating the incoming parameters. After that I get the type of the incoming object, using a bit of reflection, then I get a list of all the properties on that object’s class. Notice that I’m doing some filtering here so that I only return properties that are a class type, and also, not a string. Essentially, I’m only looking for properties that returns objects. I’m not looking for strings though because I’ll deal with string properties in a bit. For each property I find, I get the value of the property. If that value isn’t NULL I encrypt that property by calling the same EncryptProperties
method. In this way, I recursively walk through and encrypt any child options, as well.
After I’ve dealt with any child options, I then use reflection again to get a list of all public properties that are decorated with the ProtectedPropertyAttribute
attribute. These properties represent the values, on this options object, that we are interested in protecting. Assuming I found at least one or more decorated properties, I then iterate through that list. For each property I read the value into a byte array which, I’ll use after I do some housekeeping …
Before I use the value of the property for anything, I need to setup the DPAPI library. For that I’ll need two things: entropy, and scope. I’ll start with entropy by checking the entropy
parameter, on the method. If the caller specified entropy, I’ll use that. If not. I’ll move on to check for the Entropy
property in the associated ProtectedPropertyAttribute
attribute. Assuming I find a value there, I’ll use that. If not, I’ll supply a default entropy value.
For DPAPI scope I’ll follow much the same logic I did for entropy. I’ll start by checking to see if the protectedProtectionScope
parameter was specified by the caller. If the caller specified scope that way I’ll use that value. If not, I’ll move on to check for the Scope
property in the associated ProtectedPropertyAttribute
attribute. Assuming I find a value there, I’ll use that. If not, I’ll supply a default scope value.
After I have the entropy and scope, I pass the data I read before into the DPAPI library using a call to the ProtectedData.Protect
method. That code is the heart of the DPAPI library and results in the data getting encrypted, just like we want. After the data is encrypted, I need a way to represent those bytes, as a string. I’ll do that by base-64 encoding the now encrypted byte array. After I have the base-64 string, I then pass it pack to the options property using the corresponding setter method for that property.
Notice that, even though I hung the EncryptProperties
extension method off the IConfiguration
type, I’m making no attempt to encrypt the underlying configuration source. That’s because the IConfiguration
object I’m using most likely wraps multiple configuration providers, and, more than likely, none of those providers has any concept of how to update the data in the underlying files, or databases, or wherever its data actually comes from. Generally speaking IConfiguration
is a read-only configuration mechanism.
All of this means, after you encrypt the properties on your options object, you’ll need to figure out how to update your configuration yourself – that is, of course, if you need those properties encrypted in the underlying data source. This isn’t as big a deal as you might imagine though. Speaking for myself, I typically only have one or two protected properties in any given configuration file. So, when I need to encrypt the actual appSettings.json file for a project, I usually use my CG.Tools.QuickCrypto
tool to encrypt that data myself, then I copy it to the file manually.
The CG.Tools.QuickCrypto
tool can be downloaded, for free, HERE.
The next extension method I wrote is called DecryptProperties
. Here is what that looks like:
public static IConfiguration DecryptProperties( this IConfiguration configuration, object options, DataProtectionScope? dataProtectionScope = null, byte[] entropy = null ) { Guard.Instance().ThrowIfNull(configuration, nameof(configuration)) .ThrowIfNull(options, nameof(options)); var props = options.GetType().GetProperties() .Where(x => x.PropertyType.IsClass && x.PropertyType != typeof(string)) .ToList(); props.ForEach(prop => { object obj = null; try { obj = prop.GetGetMethod().Invoke( options, new object[0] ); if (null != obj) { configuration.DecryptProperties( obj, dataProtectionScope, entropy ); } } catch (Exception ex) { throw new InvalidOperationException( message: string.Format( Resources.ConfigurationExtensions_DecryptProperties, prop.Name, obj.GetType().Name ), innerException: ex ); } }); props = options.GetType().GetProperties() .Where(x => x.CanRead && x.CanWrite && x.PropertyType == typeof(string)) .ToList(); props.ForEach(prop => { try { var attr = prop.GetCustomAttributes( true ).OfType<ProtectedPropertyAttribute>() .FirstOrDefault(); if (null != attr) { var encryptedPropertyValue = prop.GetGetMethod().Invoke( options, new object[0] ) as string; if (!string.IsNullOrEmpty(encryptedPropertyValue)) { var encryptedBytes = Convert.FromBase64String( encryptedPropertyValue ); if (null == entropy || 0 == entropy.Length) { if (null != attr.Entropy) { entropy = attr.Entropy; } else { entropy = new byte[] { 4, 8, 15, 16, 23, 42 }; } } if (null == dataProtectionScope) { if (null != attr.Scope) { dataProtectionScope = attr.Scope; } else { dataProtectionScope = DataProtectionScope.LocalMachine; } } var unprotectedBytes = ProtectedData.Unprotect( encryptedBytes, entropy, dataProtectionScope.Value ); var unprotectedPropertyValue = Encoding.UTF8.GetString( unprotectedBytes ); prop.GetSetMethod().Invoke( options, new[] { unprotectedPropertyValue } ); } } } catch (Exception ex) { throw new InvalidOperationException( message: string.Format( Resources.ConfigurationExtensions_DecryptProperties, prop.Name, options.GetType().Name ), innerException: ex ); } }); return configuration; }
This method is almost identical to the EncryptProperties
method, so, instead of walking through each line I’ll just point out that I’m calling the ProtectedData.Unprotect
method here, instead of the ProtectedData.Protect
method – decrypting data, rather than encrypting it. Call me lazy, but, everything else is virtual the same between the two methods so I don’t feel like anyone would want me to walk through it all again.
So, how do you use this in code? Easy! Just do something like this:
void TestIt(IConfiguration cfg) { var options = new MyOptions(); cfg.Bind(options); // Any decorated properties are encrypted here. cfg.DecryptProperties(options); // Any decorated properties are plain text here. options.ThrowIfInvalid(); }
There you have it, a way to encrypt or decrypt individual properties on an options object, without resorting to Cloud based approaches, or, rolling your own crypto code.
By the way, the pattern of reading data from an IConfiguration
object, then binding that data to an options object, then decrypting any/all decorated properties on that options object, then validating the results, is something I do often enough that I’ve also written a few extension methods to deal with that, too. I’ll blog about that soon.
Photo by engin akyurt on Unsplash