Fededim.Extensions.Configuration.Protected implements a custom ConfigurationBuilder and a custom ConfigurationProvider defining a custom tokenization tag which whenever found inside a configuration value decrypts the enclosed encrypted data using a pluggable encryption/decryption provider (a default one based on ASP.NET Core Data Protection API is provided).
Introduction
On October 2023, I posted an article ProtectedJson on CodeProject about an improved ConfigurationSource and ConfigurationProvider for JSON files which allowed partial or full encryption of configuration values using Data Protection API. Some comments came through and one that was not so “meaningful” had a question about whether my package also supported environment variables.
Even though we could wonder why someone ever wants to encrypt/decrypt environment variables, this question gave me an epiphany: Can what I had already done for JSON files also be extended to other configuration sources? After a small proof of concept project after work, the answer was yes and after some rework, I published a new package Fededim.Extensions.Configuration.Protected which is, most of all, an improvement and an extension of ProtectedJson to support encryption/decryption of configuration values stored inside ANY configuration source.
Key Features
- Encrypt partially or fully a configuration value
- Works with any existing and (hopefully) future ConfigurationSource and ConfigurationProvider (successfully tested with framework’s builtin providers like CommandLine, EnvironmentVariables, JSON, XML, and InMemoryCollection)
- Transparent in-memory decryption of encrypted values without almost any additional line of code
- Supports a global configuration and an eventual custom override for any ConfigurationSource
- Supports almost any NET framework (net6.0, netstandard2.0 and net462)
- Pluggable easily into any existing NET / NET Core project
- Supports automatic re-decryption on configuration reload if underlying IConfigurationProvider supports it
- Supports per configuration value encryption derived subkey (called “subpurposes”)
- Supports pluggable encryption/decryption with different providers implementing a standard interface IProtectProvider(since version 1.0.12, keep in mind that implementing a secure and robust encryption/decryption provider requires a deep knowledge of security!).
Background
ASP.NET Configuration is the standard .NET Core way of storing application configuration data through hierarchical key-value pairs inside a variety of configuration sources (usually JSON files, but also environment variables, XML files, in memory dictionaries, command line parameters, or any custom provider you would like to implement). While .NET Framework used a single source (usually, an XML file which was intrinsically more verbose), .NET Core can use multiple ordered configuration sources, which get “merged” allowing the concept of overriding the value of a key in a configuration source with the same one present in a subsequent configuration source. This is useful because, in software development, there are usually multiple environments (Development, Integration, PRE-Production, and Production) and each environment has its own custom settings (for example, API endpoints, database connection strings, different configuration variables, etc.). In .NET Core, this management is straightforward, in fact, you usually have two JSON files.
- appsettings.json: which contains the configuration parameters common to all environments.
- appsettings.<environment name>.json: which contains the configuration parameters specific to the particular environment.
ASP.NET Core apps usually configure and launch a host. The host is responsible for app startup, configuring dependency injection and background services, configuring logging, lifetime management, and obviously configuring application configuration. This is done mainly in two ways.
Implicitly, by using one of the framework-provided methods like WebApplication.CreateBuilder or Host.CreateDefaultBuilder (usually called inside the Program.cs source file) which substantially do:
- Read and parse command line arguments
- Retrieve environment name respectively from ASPNETCORE_ENVIRONMENT and DOTNET_ENVIRONMENT environment variable (set either in the operating system variables or passed directly in the command line with –environment argument).
- Read and parse two JSON configuration files named appsettings.json and appsettings.<environment name>.json.
- Read and parse the environment variables.
- Call the delegate Action of ConfigureAppConfiguration where you can configure the app configuration through the IConfigurationBuilder parameter.
Explicitly by instantiating the ConfigurationBuilder class and using one of the provided extension methods.
- AddCommandLine: to request the parsing of command line parameters (either by — or – or /)
- AddJsonFile: to request the parsing of a JSON file specifying whether it is mandatory or optional and whether it should be reloaded automatically whenever it changes on filesystem.
- AddXmlFile: to request the parsing of an XML file specifying whether it is mandatory or optional and whether it should be reloaded automatically whenever it changes on filesystem.
- AddEnvironmentVariables: to request the parsing of environment variables etc.
In essence, every Add<xxxx> extension method adds a ConfigurationSource to specify the source of key-value pairs (CommandLine, Json File, Environment Variables, etc.) and an associated ConfigurationProvider used to load and parse the data from the source into the Providers list of IConfigurationRoot interface which is returned as a result of the Build method on ConfigurationBuilderclass as you can see the picture below.
(Inside configuration. Providers, there are six sources: CommandLineConfigurationProvider, two JsonConfigurationProvider for both appsettings.json and appsettings.<environment name>.json, an XMLConfigurationProvider for the XML file appsettings.xml, a MemoryConfigurationProvider for the provided dictionary and finally EnvironmentVariableConfigurationProvider for the environment variables).
As I wrote earlier, the order in which the Add<xxxx> extension methods are called is important because when the IConfigurationRoot class retrieves a key value, it uses the GetConfiguration method which cycles the Providers list in a reversed order trying to return the first one which contains the queried key, thus simulating a “merge” of all configuration sources (LIFO order, Last In First Out).
Using the Code
You find all the source code on my Github repository, the code is based on .NET 6.0 and Visual Studio 2022. Inside the solution file, there are five projects, two about the older ProtectedJson and three about this new Protected package, let’s talk about the last four.
- Fededim.Extensions.Configuration.Protected: Essentially this package implements the core logic to integrate with Microsoft ASP.NET Core Configuration API, delegating all the encryption/encryption code to a pluggable external provider implementing the interfaces IProtectProvider (dealing with the core logic of encryption/decryption) and IProtectProviderConfigurationData, an abstract class for specifying the configuration options and plugging the providers into ProtectedConfigurationBuilder, the enabling class for decryption of any configuration value stored inside any configuration source. Other types implemented by this package are ProtectedConfigurationProvider (the main class actually responsible for decryption of configuration values), ConfigurationBuilderExtensions which contains the extension methods for IConfigurationBuilder interface (WithProtectedConfigurationOptions used to specify a particular configuration which applies only to a specific ConfigurationSource) and ProtectFileOptions/ProtectFileProcessors (XmlProtectFileProcessor, JsonProtectFileProcessor, JsonWithCommentsProtectFileProcessor, RawProtectFileProcessor) used to read, decode, encrypt and re-encode source files. This package has been published on NuGet.Org. A standard provider based on Microsoft Data Protection API is provided by the companion package Fededim.Extensions.Configuration.Protected.DataProtectionAPI, you can reference just this package in your project if you do not plan to develop another encryption/decryption provider (again keep in mind that implementing a secure and robust encryption/decryption provider requires a deep knowledge of security!).
- Fededim.Extensions.Configuration.Protected.DataProtectionAPI: This is the standard Microsoft Data Protection API encryption/decryption provider which implements the interface IProtectProvider and the abstract class IProtectProviderConfigurationData respectively with its classes DataProtectionAPIProtectProvider and DataProtectionAPIProtectConfigurationData. Again this package has been published on NuGet.Org.
- Fededim.Extensions.Configuration.Protected.DataProtectionAPITest: This a xUnit test project which tests thoroughly the two above packages in order to improve the reliability and the code quality. It creates sample data for all ConfigurationSources provided by MS .NET (a JSON file, a XML file, environment variables, an in-memory dictionary and command line arguments) containing a 2*fixed set of keys (100000), one in plaintext with random datatype and value and another with the same value but encrypted. It loads then the sample data with ProtectedConfigurationBuilder in order to decrypt it and tests that all plaintext values are the same as those that have been decrypted.
- Fededim.Extensions.Configuration.Protected.ConsoleTest: This is a console application which shows how to use ProtectedConfigurationBuilder by reading and parsing six encrypted bespoke configuration sources and converting them to a strongly type class called AppSettings. The decryption happens flawlessly and automatically without almost any line of code, let’s see how.
To use the automatic decryption feature, you have just to replace the call new ConfigurationBuilder() with a call to new ProtectedConfigurationBuilder() passing to it the configuration of the pluggable encryption/decryption provider. After having done that, you can add any existing configuration source by using the standard methods like AddCommandLine, AddJsonFile, AddXmlFile, AddInMemoryCollection, AddEnvironmentVariables or even future configuration sources since this package should support all of them as long as the implementation of the GetChildKeys of Microsoft.Extensions.Configuration.ConfigurationProvider does not change (keep reading below to understand the reason). The constructor for ProtectedConfigurationBuilder takes just one parameter, a configuration class derived from the IProtectProviderConfigurationData, an abstract class (implemented by one of the available providers) which is used for specifying the configuration options and plugging the providers into ProtectedConfigurationBuilder.There are four common fundamental parameters which must be specified by every provider.
ProtectedRegex: It is a regular expression which specifies the tokenization tag which encloses the encrypted data to be decrypted; it must define a named group called protectedData (and optionally two additional groups called subPurposePattern and subPurpose for specifying a per configuration value subkey). If null, this parameter assumes the default value.
The above regular expression essentially searches in a lazy way (so it can retrieve all the occurrences inside a value) for any string matching the pattern ‘Protected:{<subPurpose>}:{<encrypted data>}’ and extracts the <encrypted data> substring storing it inside a group named protectedData. There is also an optional part called <subPurposePattern> (made up of :{<subPurpose>}) which allows to specify per configuration value encryption derived subkey (called “subpurposes”, idea borrowed originally from Data Protection API). If you do not like this tokenization, you can replace it with any other one you prefer by crafting a regular expression with the constraint that it extracts the <encrypted data> substring in a group called protectedData and the <subPurposePattern> and <subPurpose> substrings in two groups called respectively subPurposePattern and subPurpose.
- ProtectRegex: It is a regular expression which specifies the tokenization tag which encloses the data to be encrypted; again it must define a named group called this time protectData(and optionally two additional groups called subPurposePattern and subPurpose for specifying a per configuration value subkey). If null, this parameter assumes the default value (e.g. Protect:{<subPurpose>}:{<data to be encrypted>})
- DefaultProtectedReplaceString: It is a string expression used to transform the plaintext tokenization into the encrypted tokenization (e.g. from Protect:{<subPurpose>}:{<data to be encrypted>} into Protected:{<subPurpose>}:{<encrypted data>}). It contains two placeholders ${subPurposePattern} and ${protectedData} which gets substituted respectively with the subPurposePattern (if present) and the encrypted data. If null, this parameter assumes the default value.
IProtectProvider: this is a standard interface which must be implemented by the pluggable provider providing the encryption/decryption services.
By default a standard encryption/decryption provider based on Microsoft Data Protection API is provided in the companion package Fededim.Extensions.Configuration.Protected.DataProtectionAPI. Its configuration class implementing IProtectProviderConfigurationData is called DataProtectionAPIProtectConfigurationData whose constructor takes these additional inherent parameters besides the above three common ProtectedRegex,ProtectRegexand DefaultProtectedReplaceString ones.
- dataProtectionServiceProvider: This is an IServiceProvider interface needed to instance the IDataProtectionProvider of Data Protection API in order to decrypt the data. This parameter is mutually exclusive to the next one.
- dataProtectionConfigureAction: This is an Action<IDataProtectionBuilder> used to configure the Data Protection API in standard NET Core. Again, this parameter is mutually exclusive to the previous one.
- purposeString: used to specify explicitly a purpose string to use for encryption. Data Protection API supports multiple encryption keys which are derived from the configured master key and strictly connected to one or more purpose strings passed to the CreateProtector API.
- keyNumber: this parameter is an alternative to purposeString for convenienceProtectedConfigurationBuilder.ProtectedConfigurationBuilderKeyNumberPurpose method)
The dataProtectionServiceProvider and dataProtectionConfigureAction parameters are somewhat a drawback because they represent a reconfiguration of another dependency injection for instantiating the IDataProtectionProvider needed to decrypt the data.
In fact, in a standard NET Core application, usually, the dependency injection is configured after having read and parsed the configuration file (so all configuration sources and providers do not use DI), but in this case, I was compelled since the only way to access Data Protection API is through DI. Moreover, when configuring the dependency injection, the parsed configuration usually gets binded to a strongly typed class by using services.Configure<<strongly typed settings class>>(configuration) so it’s a dog chasing its tail (for decrypting configuration you need DI, for configuring DI, you need the configuration parsed in order to bind it to a strongly typed class). The only solution I came up with for now is reconfiguring a second DI IServiceProvider just for the Data Protection API and use it inside ProtectedConfigurationProvider. To configure the second DI IServiceProvider, you have two options.
- You create it by yourself (by instantiating a ServiceCollection and calling AddDataProtection on it)
- You let ProtectedConfigurationProvider create it for you by passing a dataProtectionConfigureAction parameter. In this case, in order to avoid duplicated code, the configuration of Data Protection API can be performed inside a common private method called ConfigureDataProtection, e.g.
Here, I chose to use AES 256 symmetric encryption with HMAC SHA256 as a digital signature function. Moreover, I ask to store the master encryption key metadata (key, iv, hash algorithm, etc.) in an XML file inside the Keys folder of the console app (by default, keys are stored in a particular location according to MS key management documentation, note that all these APIs are provided by default by the Data Protection API).
So when you start the app for the first time, the Data Protection API creates automatically the master encryption key and stores it in the Keys folder, in the following runs, it loads it from this XML file. This configuration however is not the best approach from the security viewpoint because the key is stored in plain text, if you want to encrypt the master key at rest, you can use the ProtectKeysWithDpapi extension method (works only in Windows and in this case, it would be encrypted with Windows DPAPI) or ProtectKeysWithCertificate to encrypt it with a certificate installed on the computer. Please note that even though you can use different encryption keys in the Keys folder, there is just one master key from which all encryption keys are derived using the purpose string crafted from either the keyNumberparameter or the purposeStringparameter specified either in the ProtectedConfigurationBuilder or in the WithProtectedConfigurationOptions extension method.
In the console application, I add the six configuration sources in the following order to exemplify the merging feature of ASP.NET Core Configuration and also the use of encrypted values.
- AddCommandLine: to add the command line arguments.
- AddJsonFile: to add the two json files appsetting.json and appsettings.development.json, the second one has the reloadOnChange flag set to true in order to allow the reload of json file whenever it changes on the filesystem.
If you look at the ConnectionStrings section of appsetting.json, there are three keys.
If you look at Nullable section of appsetting.development.json, you can find some interesting keys.
Well, chiefly, all the ConfigurationProviders convert initially any ConfigurationSource into a Dictionary<String,String> in their Load method (please see the property Data of the framework ConfigurationProvider base abstract class, the Load method also flattens all the hierarchical path to the key into a string separated by a colon, so for example Nullable->Int becomes Nullable:Int). Only later, this dictionary gets converted and binded to a strongly typed class.
The decryption process of ProtectedConfigurationProvider happens in the middle, so it’s transparent for the user and moreover is available on any simple variable type (DateTime, bool, etc.). For now, the full encryption of a whole array is not supported, but you can however encrypt a single element converting the array to an array of strings (have a look at DoubleArray key, note also that in this key it is exemplified the use of per configuration value subkey, in fact the array contains twice the same 3.14 value [“Protect:{3.14}” and “Protect:{%customSubPurpose%}:{3.14}”] but it is encrypted with two different keys and different salt.
- PlainTextConnectionString: As the name states, it contains a plaintext connection string.
- PartiallyEncryptedConnectionString: As the name states, it contains a mixture of plain text and multiple Protect:{<data to encrypt>} tokenization tags. On every run, these tokens get automatically encrypted and replaced with the Protected:{<encrypted data>} token after the call to the extension method IProtectProviderConfigurationData.ProtectFiles.
- FullyEncryptedConnectionString: As the name states, it contains a single Protect:{<data to encrypt>} token spanning the whole connection string which gets totally encrypted after the first run.
- Int, DateTime, Double, Bool: These keys contain respectively an integer, a datetime, a double, and a boolean but they are all stored as a string using a single Protect:{<data to encrypt>} tag. Hey, wait, how is this possible?
- AddXmlFile: to add the XML file appsettings.xml
- AddInMemoryCollection: to add an in-memory dictionary
- AddEnvironmentVariables: to add environment variables
The main code of the Protected.ConsoleTest console application is,
The above code is quite simple and commented, if you launch it in Debug mode, it will automatically break into most significant points by using Debugger.Break().
The first one happens after all six configuration sources values have been encrypted by replacing the default tokenization tag Protect:{<data to encrypt>} with Protected:{<encrypted data>} (for files, please check those inside bin/Debug/net6.0 folder and not inside the solution dir which remain unchanged).
- CommandLine arguments args gets encrypted in encryptedArgs variable by using the provided extension method IProtectProviderConfigurationData.ProtectConfigurationValue.
- The appsettings.json* files have been backed up in a .bak file and encrypted by using the provided extension method IProtectProviderConfigurationData.ProtectFiles.
- The appsettings.xml file has been backed up in a .bak file and has been encrypted by using the provided extension method IProtectProviderConfigurationData.ProtectFiles.
- The environment variables have got encrypted by using IProtectProviderConfigurationData.ProtectEnvironmentVariables.
The second one is the one actually showing the use of ProtectedConfigurationBuilder, if you watch in the debugger the appSettings strongly typed class, you will notice that it magically and automatically contains the decrypted values with the right data type even though the encrypted keys are always stored in configuration sources as strings. Please note that I used IOptionsMonitor<AppSettings> instead of IOptions<AppSettings> because I wanted to test also the autodecryption feature on reload of a configuration file whenever it changes on filesystem.
The third one, the fifth one, the seventh one, etc. is inside the IOptionsMonitor.OnChange event and happens just after having updated the “Int” property with a random encrypted integer inside appsettings.development.json file (take note of this value, you will need it again in the next breakpoint), you can see that the appSettingsReloaded variable contains a different value from the one of appSettings variable.
The fourth one, the sixth one, the eighth one, etc. is after IOptionsMonitor.OnChange event and happens after re-assignment to appSettings variable of the current strongly typed configuration class from IOptionsMonitor, you can check that appSettings contains the same new value for “Int” property as that of appSettingsReloaded variable at previous breakpoint.
So summing up in order to use this package, we had just to use to replace the call new ConfigurationBuilder() with a call to new ProtectedConfigurationBuilder(), pass the Data Protection API configuration and an eventual custom tokenization tag, and everything works flawlessly in a transparent way. Moreover, all the decryption happens in memory and nothing is stored on disk for any reason.
Implementation Details
I explain here the main points of the implementation.
Code inside Fededim.Extensions.Configuration.Protected package
Extensions methods defined inside ConfigurationBuilderExtensions.
ProtectFiles is an extension method of IProtectProviderConfigurationDatawhich gets called and scans all files according to the supplied extension search pattern inside the supplied path for Protect:{<data to encrypt>} tokens, encrypts enclosed data, performs the replacement with Protected:{<encrypted data>} and saves back the file after having created an optional backup of the original file with the .bak extension. Again, if you do not like the default tokenization regular expression, you can pass your own one with the constraint that it must extract the <data to encrypt> substring in a group called protectData and optionally the <subPurposePattern> and <subPurpose> substrings in two groups called respectively subPurposePattern and subPurpose.
The format of input files is decoded, encrypted and re-encoded according to the processor specified in the public static property ConfigurationBuilderExtensions.ProtectFilesOptions, by default three processors are provided, two for JSON files (e.g. \\ becomes \, etc., see JsonProtectFileProcessor and JsonWithCommentsProtectFileProcessor class in ProtectFileProcessors.cs) and one for XML files (e.g. > becomes >, etc., see XmlProtectFileProcessor class again in ProtectFileProcessors.cs) and one for RAW text files (no decoding is done, see RawProtectFileProcessor class again in ProtectFileProcessors.cs). You can add as many processors as you wish by adding records to the ConfigurationBuilderExtensions.ProtectFilesOptionslist specifying a filenameRegexwhich if matched applies the associated ProtectFileProcessor, e.g. a class that must implement the IProtectFileProcessorinterface (see below). This interface has essentially just one method ProtectFilewhich essentially takes in input 3 parameters passed to it by the ConfigurationBuilderExtensions.ProtectFiles method.
- rawFileText: this is the raw input file as a string
- protectRegex: this is the configured protected regex which must be matched in file configuration values in order to choose whether to encrypt or not the data.
- protectFunction:
this is the protect function that encrypts data taking the plaintext data as input and producing encrypted base64 string as output.
This ProtectFile method must decode, encrypt, and re-encode the input file and return it as a string as final output. Note that all the file processors are processed in FIFO (First-In First-Out) order so keep this in mind when configuring your additional handlers.
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line)
ProtectEnvironmentVariables is an extension method of IProtectProviderConfigurationDatawhich encrypts the environment variables with the same criteria.
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line)
ProtectConfigurationValue is an extension method of IProtectProviderConfigurationDatawhich encrypts a string, an IEnumerable<string>, a string[] or a Dictionary<string,string> with the same criteria. The ultimate method responsible for the actual encryption is ProtectConfigurationValueInternal (it is the only private method since static classes do not have protected members)
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line)
WithProtectedConfigurationOptions: It is an extension method of IConfigurationBuilderwhich allows to override the Data Protection or tokenization tag configuration for a particular ConfigurationSource (e.g., the last one added). Note that this method is a little bit hacky: I was not able to change the return type of ProtectedConfigurationBuilder.Add, otherwise, the IConfigurationBuilder interface wouldn't be implemented; thus WithProtectedConfigurationOptions extends the standard IConfigurationBuilder interface converts it to the IProtectedConfigurationBuilder interface, and calls the ProtectedConfigurationBuilder.WithProtectedConfigurationOptions method, if instead, the provided IConfigurationBuilder is not an instance of IProtectedConfigurationBuilder it raises an exception remembering to replace the new ConfigurationBuilder instantiation with the new ProtectedConfigurationBuilder. This method takes as input just one parameter: an IProtectProviderConfigurationData class configured properly which overrides the global configuration specified in the ProtectedConfigurationBuilderconstructor.
ProtectedConfigurationBuilder implements the IConfigurationBuilder interface like the ConfigurationBuilder framework class (from which part of the implementation is borrowed), the main difference is the Build method which elementally proxies through composition the IConfigurationProvider returned from the IConfigurationSource.Build method by passing it as a constructor parameter to the core class responsible for in-memory transparent decryption: ProtectedConfigurationProvider. It also performs the merge between the custom configuration specified for the IConfigurationSource being converted into IConfigurationProvider (if you want to know how check the ProtectProviderConfigurationData.Merge static method) and the global configuration specified in the ProtectedConfigurationBuilder constructor.
IProtectProviderConfigurationData This class is an abstract class for specifying the configuration options and plugging the providers into ProtectedConfigurationBuilder.
As we can see the IProtectProviderConfigurationData fundamentally contains four properties and one method.
ProtectedRegex: It is a regular expression which specifies the tokenization tag which encloses the encrypted data to be decrypted; it must define a named group called protectedData(and optionally two additional groups called subPurposePatternand subPurposefor specifying a per configuration value subkey). If null, this parameter assumes the default value.
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line)
- The above regular expression essentially searches in a lazy way (so it can retrieve all the occurrences inside a value) for any string matching the pattern 'Protected:{<subPurpose>}:{<encrypted data>}' and extracts the <encrypted data> substring storing it inside a group named protectedData. There is also an optional part called <subPurposePattern> (made up of :{<subPurpose>}) which allows specifying per configuration value encryption derived subkey (called "subpurposes", an idea borrowed originally from Data Protection API). If you do not like this tokenization, you can replace it with any other one you prefer by crafting a regular expression with the constraint that it extracts the <encrypted data> substring in a group called protectedDataand the <subPurposePattern> substrings in two groups called respectivelysubPurposePattern andsubPurpose.
- ProtectRegex: It is a regular expression that specifies the tokenization tag that encloses the data to be encrypted; again it must define a named group called this time protectData(and optionally two additional groups called subPurposePattern and subPurpose for specifying a per configuration value subkey). If null, this parameter assumes the default value (e.g. Protect:{<subPurpose>}:{<data to be encrypted>}).
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line)
DefaultProtectedReplaceString: It is a string expression used to transform the plaintext tokenization into the encrypted tokenization (e.g. from Protect:{<subPurpose>}:{<data to be encrypted>} into Protected:{<subPurpose>}:{<encrypted data>}). It contains two placeholders ${subPurposePattern} and ${protectedData} which gets substituted respectively with the subPurposePattern (if present) and the encrypted data. If null, this parameter assumes the default value.
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line).
ProtectProvider:a class implementing the IProtectProvider interface for specifying a pluggable logic for encryption/decryption. This interface is made up of three methods.
- Encrypt: which takes a plain text string and returns an encrypted base64 string.
- Decrypt: which takes a base64 encrypted string and returns the plain text string
- CreateNewProviderFromSubkey: for supporting the per configuration value encryption derived subkey. It takes a subkey string parameter which should be used as an encryption subkey to create a new derived IProtectProviderinterface.
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line).
- CheckConfigurationIsValid: it is a base virtual (e.g. overrideable) method that basically checks that the IProtectProviderConfigurationData configuration is valid, if not it raises an exception with the details. It is called every time you add an IProtectProviderConfigurationData to ProtectedConfigurationBuilder, either globally in the constructor or locally with the extension method WithProtectedConfigurationOptions. Moreover, it is also called during ProtectProviderConfigurationData.Merge to check that the resulting configuration is valid when performing encryption with IProtectProviderConfigurationData.Protect... methods.
- ProtectedConfigurationProvider: This class performs the actual transparent decryption of encrypted configuration values stored inside any existing configuration source. It should also support even future configuration sources as long as the implementation of the GetChildKeys of Microsoft.Extensions.Configuration.ConfigurationProvider does not change (I principally use it to enumerate all possible configuration keys). Essentially, it takes in its constructor as input the IConfigurationProvider that needs to be decrypted and acts as a proxy:
It redefines the Load method by calling after input IConfigurationProvider.Load the method responsible for the actual decryption DecryptChildKeys. This method initially used the standard method GetChildKeys offered by the IConfigurationProvider interface to enumerate all the existing configuration keys and decrypt them by matching the ProtectedRegex stored inside ProtectProviderConfigurationDataand using the Decrypt method of ProtectProvider. If the subPurposegroup is present in the regex, a temporary derived ProtectProvider is created using CreateNewProviderFromSubkeymethod and used to perform decryption. With version 1.0.15 of the package I added a faster method for decrypting the child keys, it uses reflectionjust to retrieve the key-value dictionary inside the ConfigurationProvider.Data-protected property (reflection should be avoided as much as possible, but in this case, it is needed since the property is not public, moreover it is only used to retrieve only the dictionary, not the key-values entries). The speed increase is around 3000 times (yes 3k times!) and it should be supported by all Microsoft provided ConfigurationProviders (e.g. XmlConfigurationProvider, JsonConfigurationProvider, MemoryConfigurationProvider, CommandLineConfigurationProvider, EnvironmentVariablesConfigurationProvider) . This method is hacky but it is used in a safe mode, e.g. if the IConfigurationProviderobject being decrypted is not an instance ofConfigurationProvider class and the class does not contain a property named Data the old slower recursive method is used.
(markdown bugfix
bullet needed to re-indent the text at the right position after a code block since it always starts at the beginning of the line)
It creates its own ReloadToken if the underlying IConfigurationProvider supports it, it returns it in the GetReloadToken method and finally it registers a callback to the input IConfigurationProvider reload token using the framework static utility method ChangeToken.OnChange class in order to be notified of any configuration change re-performing the decryption in order to support to automatic decryption of values on configuration reload.
Code inside Fededim.Extensions.Configuration.Protected.DataProtectionAPI package
DataProtectionAPIProtectConfigurationData: This class chiefly stores either the global configuration or the ConfigurationSource specific one for Data Protection API and the eventual custom tokenization regular expressions. It also sets up another dependency injection provider in the main constructor (see above for the reason) and it provides two overloads one for key number and one for purposeString. Besides these two main constructors, there are several other overloads provided for usability.
DataProtectionAPIProtectProvider is the implementation of the IProtectProviderinterface using the Microsoft Data Protection API (e.g. calls IDataProtector.Unprotect for Decrypt, IDataProtector.Protect for Encrypt, IDataProtector.CreateProtector for CreateNewProviderFromSubkey).
Points of Interest
I think that the idea of specifying the custom tag through a regex is very witty because it gives every user the flexibility they need to customize the tokenization tag.
It's strange that MS did not plan a method or a property inside IConfigurationProvider to enumerate all possible keys of a provider,probably (and hopefully), it will be added in future releases so I could avoid using the nonefficient recursive GetChildKeys method to enumerate all keys.
If you wonder whether you can use this package in your project and it will still be working in the future, I can underline that the only critical point is the enumeration of all configuration keys which is now done by using the GetChildKeys method. Even if its implementation could be changed by Microsoft, it will always provide what its name states, e.g., the child keys of a configuration key. And even in the remote case that the GetChildKeys method will be removed from the IConfigurationProvider interface, you can always access all configuration keys by casting the interface to the ConfigurationProviderbase class and access the protected Data property through reflection (please see the comment in CreateProtectedConfigurationProvider method, basically all configuration providers derive from this class), so I am quite confident that the package will be working for many years.
In addition since version 1.0.12, the package allows pluggable encryption/decryption in case you want for whatever reason (alleged Microsoft hidden backdoors, open source product aficionado, etc.) not to use the default provider Fededim.Extensions.Configuration.Protected.DataProtectionAPI based on Microsoft Data Protection API.
Furthermore since version 1.0.16 of Fededim.Extensions.Configuration.Protected I implemented the safe casting of the IConfigurationProvider being decrypted to the ConfigurationProvider base class in order to make the child keys enumeration process unbelievably fast. On top of that I have recently added a xUnit test project in order to improve the reliability and the software quality of these two packages: on my personal laptop (Intel I9-13900K based) I have successfully tested it with 2100000 random keys inside five tests cases, each one dealing with the framework provided ConfigurationProviders (CommandLineConfigurationProvider, EnvironmentVariablesConfigurationProvider, MemoryConfigurationProvider, JsonConfigurationProvider [two passes, one using JsonProtectFileProcessor and one using JsonWithCommentsProtectFileProcessor], XmlConfigurationProvider). For example, a test case on the JsonConfigurationProvider generated a plain-text file with a total size of 60MB and an encrypted file with a total size of 91MB, the test ended in around 10 seconds for generating the random JSON file, encrypting it, decrypting it using the ProtectedConfigurationBuilder(in order to decrypt 250k encrypted values this step took around 5 seconds in .Net462 and around 3 seconds in net6.0 which is faster) and checking that every decrypted key was equal to the plaintext one. Moreover, the whole set of five test cases was repeated for 1000 iterations (Test Explorer Run Until Failure, unluckily it is not available for the whole suite of tests, so I had to do it separately), both for net462 (total runtime 705 minutes) and net6.0 (total runtime 424 minutes) without raising any error