DataProtector

DataProtector

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