I’ve migrated quite a bit of my .NET code to non-Windows platforms, this past year. As I’ve done so, I’ve re-discovered the fact that DPAPI, which is what I’ve traditionally used for data protection, doesn’t really come in non-Windows flavors. Looking around, I discovered that ASP.NET has something they call ‘Data Protection’. It’s not exactly DPAPI, but it seems like it could be a workable alternative, for me. I thought I might blog about my attempts to come to terms with the ASP.NET Data Protection library, and then demonstrate some of the code I came up with, in the process.
My goal for DPAPI has always been to protect certain fields in local configuration files. By ‘certain fields’, I mean things like passwords, connection strings – anything likely to be picked up by a bad actor. Because the files are local and are never seen outside the box (a basic assumption on my part), the strength of the encryption doesn’t need to be terribly strong, just strong enough to keep honest folks honest, as the saying goes. My thinking has always been, if a hacker gains access to my server’s file system, the fact that I used DPAPI to protect the connection string to my database is probably the least of my worries.
So, as I turn from DAPI to the ASP.NET Data Protection library (I’ll call it DPL from here on in … I’m lazy, give me a break) I’ll continue with the assumption that whatever code I write to integrate with DPL, will provide me with “good enough”, but not perfect, encryption for my local data.
Let’s start with the class I came up with, to wrap the DPL in a way that works for me. I started with this class:
public class DataProtector : SingletonBase<DataProtector>, IDataProtector { protected IDataProtectionProvider Provider { get; } protected IDataProtector Protector { get; } protected string Purpose { get; } [DebuggerStepThrough] private DataProtector() { Purpose = "1A4DF30A-28F2-49A8-8324-F0118A671B6A"; var localAppData = string.Empty; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { localAppData = Environment.GetEnvironmentVariable( "LOCALAPPDATA" ); } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { localAppData = Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? Path.Combine( Environment.GetEnvironmentVariable("HOME"), ".local", "share" ); } if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { localAppData = Path.Combine( Environment.GetEnvironmentVariable("HOME"), "Library", "Application Support" ); } var destFolder = Path.Combine( localAppData, Purpose ); Provider = DataProtectionProvider.Create( new DirectoryInfo(destFolder), options => { options.SetDefaultKeyLifetime( TimeSpan.MaxValue.Subtract(TimeSpam.FromDays(1)) ); options.SetApplicationName(Purpose); }); Protector = Provider.CreateProtector( nameof(DataProtector) ); } [DebuggerStepThrough] public byte[] Protect(byte[] plaintext) { Guard.Instance().ThrowIfNull(plaintext, nameof(plaintext)); return Protector.Protect(plaintext); } [DebuggerStepThrough] public byte[] Unprotect(byte[] protectedData) { Guard.Instance().ThrowIfNull(protectedData, nameof(protectedData)); return Protector.Unprotect(protectedData); } [DebuggerStepThrough] public IDataProtector CreateProtector(string purpose) { Guard.Instance().ThrowIfNullOrEmpty(purpose, nameof(purpose)); var result = Provider.CreateProtector( Purpose, purpose ); return result; } }
This class derives from SingletonBase
(which is part of my CG.Core
NUGET package). The way SingletonBase
works, a user can create a singular instance, and access that instance, through the appropriately named Instance
method. That method isn’t part of this code listing. Just know that the private constructor on DataProtector
isn’t a typo, and it will be called when the user calls the Instance
method.
The private constructor begins by checking to determine which OS the code is running on. Depending on that check, we then create a path to a reasonable location for storing our crypto keys. The keys themselves are encrypted by the DPL, so we really don’t need to worry about those files, too much. Once we have the path for the keys, we then call to the DataProtectionProvider.Create
method, which creates a DPL provider for us. There are loads of options we could have added, at this point, but all we really need, for my workflow, are long lived keys and a predictable name for our DPL objects. So, we only add those two bits of information into the DataProtectionProvider.Create
call. What we get from that effort is an instance of IDataProtectionProvider
that we then use to create our IDataProvider
instance. That IDataProvider
instance is the object we really care about. We store both objects into protected properties, on the class, for later use.
The DataProtector
class implements the IDataProtector
interface, which comes from DPL. The class is actually a wrapper around the internal data protector that we created in the constructor. That means the implementation of the rest of the class is trivial. Still, I’ll provide the entire listing, above, for completeness.
So I now have a singleton object that creates an instance of IDataProtector
. At this point you may be wondering, why didn’t I simply use the IServiceCollection.AddDateProtection
method, that comes with DPL, to register an IDataProtector
object with the ASP.NET dependency container, and simply use it as a service? Great question! Here’s why I did that:
First, some of my projects aren’t websites, and some of those projects don’t use Microsoft dependency injection library. For those scenarios, I needed a simple way to tie into DPL, without introducing Microsoft DI, and my DataProtector
does that.
Second, Microsoft’s DI implementation has a small window of time, when the Startup class is trying to create / configure the service collection, where I seem to end up needing the very service collection that I’m trying to build, before I have it completely built. It’s a nasty loop, and there’s no clear cut way to work around it.
Here’s a simple example of what I mean. Let’s start with this:
public class Startup { public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddDataProtection(); // This sets up the DPL service. } }
From the snippet above, we’re inside the Startup
class, in the ConfigureServices
method, and we’ve called AddDataProtection
, to register DPL as a service. This means we can now use the DI container to create an IDataProtectionProvider
object, and from that, an IDataProtector
object. Once we have the IDataProtector
object, we can then call the Unprotect
method to decrypt any encrypted text, right? So, let’s say we want to decrypt something from the configuration. Like this:
public class Startup { public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddDataProtection(); // This sets up the DPL service. var sp = services.BuildServiceProvider(); // We have to call this var dpp = sp.GetRequiredService<IDataProtectionProvider>(); // To create this vap dp = dpp.CreateProvider(); // So we have this var unprotected = dp.Unprotect(Configuration["Test"]); // To do this } }
The snippet above works, but the problem is the call to BuildServiceProvider
. We have to do that in order to get a service provider, so we can create service instances. But, that call has a side effect – it creates an instance of every singleton. That’s a problem because ASP.NET itself is going to call that same method in order to create the service provider it needs to function properly. That means we could, potentially, have multiple instances of singletons in the DI container.
So, in order to work around this, using my simple example, I would probably do something like this:
public class Startup { public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddDataProtection(); // This sets up the DPL service. // This no longer requires the DPL from above. var unprotected = DataProtector.Instance().Unprotect( Configuration["Test"] ); } }
The snippet above no longer even uses the DPL that we registered, previously, with Microsoft’s DI library. In fact, if we only registered DPL like that in order to unprotect our “Test” configuration key/value, then we could actually drop the call to AddDataProtection
, if we wanted to.
This example is admittedly simple. Then again, I’m trying to prove a point that there are times when we need DI services, in ASP.NET, while we’re in the process of setting up our DI services. There may be other ways I could have worked around the call to BuildServiceProvider
, but this is the one I chose.
Now though, I can integrate data protection with the startup classes that I’ve already written, and not have to worry about getting stuck in the endless DI startup loop. Those data protection services will run on multiple operating systems as well, so I’m no longer stuck with running only on Windows servers. I can also use my approach in situations where there are no DI services available, which is nice because, believe it or not, not everything is a website.
Photo by engin akyurt on Unsplash