Leveraging the DPAPI to encrypt sensitive configuration settings

It is very common that an application will need to store settings in a configuration file and then later retrieve the value of those settings.

Quite often, one or more of these settings will be sensitive in nature; such as API keys, database connection strings, or perhaps mail server passwords.

Protecting sensitive data is a major concern for many applications and this concern should apply not only to data but also to the credentials used to access the data which tend to be stored in configuration files.

In this article, I explain what the DPAPI is and how it can be leveraged to encrypt sensitive configuration file settings on a Windows device.

DPAPI?

If you’ve never heard of the DPAPI, let me clarify what it is before going any further.

DPAPI is an acronym for Data Protection Application Programming Interface.

So loosely speaking the DPAPI is an API which is all about protecting (encrypting) data.

The DPAPI has been around since the days of Windows 2000 (battle-tested!) in different forms and solves the hard problem of generating and storing the cryptographic keys required to encrypt and decrypt data.

The major quandary when encrypting data has always been where to store the encryption key.

The truth of the matter is that client devices cannot store a key/secret value in complete safety. However, the DPAPI can help us greatly as it allows us to delegate the responsibility for the management of the primary encryption key to the OS.

There are two main approaches when encrypting data using the DPAPI. You can choose to use one of the following two ‘Scopes’.

  • Current User
  • Local Machine

These Data Protection Scopes both offer strong protection for sensitive data, but they differ in how the encryption key is generated.

‘Current User’ Scope is the most secure method of encrypting data since the encryption key is based on the password of the logged-in user. It will only be possible to decrypt the data (when not logged into the OS) if you can establish the user’s password.

‘Local Machine’ Scope uses a key that is specific to the machine and therefore any data that is encrypted in this manner can be decrypted across different user profiles. This is very useful whenever you want your application to work across different user accounts without needing to reconfigure settings for each account.

You can read more about the nature and usage of the DPAPI within the Microsoft Docs.

Using the DPAPI

Now let’s see how we can use the DPAPI from a .NET project to encrypt and decrypt data.

All of the code used in this article works in both .NET and .NET Core. However, if you’re using .NET Core, you’ll need to install the System.Security.Cryptography.ProtectedData NuGet package in order for the code below to compile.

Note that I have created a sample project on GitHub and the helper methods from the following two sub-sections can be found within a class named EncryptionProvider.

Encryption

First of all, let’s look at how to encrypt some text using the DPAPI.

/// <summary>
/// Encrypts the specified clear text.
/// </summary>
/// <param name="clearText">The clear text to encrypt</param>
/// <param name="entropy">Optional entropy key</param>
/// <returns>The encrypted text</returns>
public static string Encrypt(string clearTextbyte[] entropy)
{
    if (clearText == nullthrow new ArgumentNullException(nameof(clearText));
 
    byte[] clearBytes     = Encoding.UTF8.GetBytes(clearText);
    byte[] encryptedBytes = ProtectedData.Protect(clearBytesentropyDataProtectionScope.LocalMachine);
 
    return Convert.ToBase64String(encryptedBytes);
}

The Encrypt method defined above takes the clear text that you want to encrypt as a parameter and converts that into a UTF8 byte array. The bytes are then passed to the static Protect method of the ProtectedData class (this is the .NET wrapper class for accessing the DPAPI) which returns a new byte array containing the encrypted data. Lastly, the encrypted bytes are converted to a Base64 string.

By using UTF8 encoding when generating the bytes to encrypt, we ensure that all of the text is encoded correctly whenever languages which have extended character sets are being used.

By using Base64 encoding for the final encrypted text, we ensure that we are able to store the encrypted data in a consistent manner, without any special characters which could interfere with serialization or other data storage concerns.

You’ll notice that in the example I am using LocalMachine Scope so that the encrypted data can be accessed across different user profiles. You can also specify CurrentUser Scope if you only want it to be possible for the data you are encrypting to be decrypted by the currently logged in user.

Additionally, you may be wondering what the entropy parameter is for. The ‘entropy’ value is optional but if it is not specified, it will be possible for other applications running on the same machine to decrypt the data you’ve encrypted if they call into the DPAPI. By specifying an entropy value, which is essentially a secondary key, you can mitigate against this possibility and provide some assurance that the data is not only protected from humans but also from other system applications.

Decryption

Next, let’s look at how to decrypt some text using the DPAPI.

/// <summary>
/// Decrypts the specified clear text.
/// </summary>
/// <param name="encryptedText">The encrypted text to decrypt</param>
/// <param name="entropy">Optional entropy key</param>
/// <returns>The decrypted text</returns>
public static string Decrypt(string encryptedTextbyte[] entropy)
{
    if (encryptedText == nullthrow new ArgumentNullException(nameof(encryptedText));
    
    byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
    byte[] clearBytes     = ProtectedData.Unprotect(encryptedBytesentropyDataProtectionScope.LocalMachine);
 
    return Encoding.UTF8.GetString(clearBytes);
}

The decryption code is very similar to the encryption code. It is essentially doing the inverse of each operation; converting the Base64 encrypted text to a byte array, passing this to the static Unprotect method of the ProtectedData class and then converting the returned bytes into a UTF8 string.

Again I am using the LocalMachine Data Protection Scope and passing along the entropy value.

If the specified entropy value does not match the entropy which was used when the data was encrypted, a decryption error will be thrown.

Encrypted settings

Now that we have an understanding of how to utilise the DPAPI to encrypt and decrypt data, let’s look at how we can use the encryption logic we’ve created to encrypt configuration file settings.

Note that within the sample project on GitHub you can find the helper methods from the following sub-sections within a class named EncryptionSettings (unless specified otherwise).

App.config

Before looking at the code let’s create a configuration file which will be used to store the encrypted settings.

In .NET Core, it is common practice to use an appsettings.json file. However, in my example, I’m going to use an XML-based App.config file which will work with Framework support in both .NET and in .NET Core (with the help of the System.Configuration.ConfigurationManager NuGet package).

Below are the contents of an example ‘App.config’ file which can be used to store encrypted settings.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="encryptedSettings" type="System.Configuration.AppSettingsSection" />
    </configSections>
    <appSettings>
        <add key="MyPlainSetting" value="This is plain text."/>
    </appSettings>
    <encryptedSettings>
        <add key="MyEncryptedSetting" value="This text will be encrypted."/>
    </encryptedSettings>
</configuration>

The above XML is a standard .NET configuration file with a configuration root element.

A custom ‘encryptedSettings’ section is defined within the configSections element. If you are already familiar with .NET configuration files, you’ll notice that the type has been set to ‘AppSettingsSection’ and therefore this custom section will work in the same manner as a standard ‘appSettings’ section.

Within the custom encryptedSettings element I have defined a single encrypted setting key and value which will be accessed from the sample code.

Getting encrypted settings

Now that we have the App.config file set up, we can access the settings defined within the ‘encryptedSettings’ configuration section from our code, as follows.

/// <summary>
/// Gets a name/value collection of encrypted settings.
/// </summary>
/// <returns><see cref="NameValueCollection"/></returns>
private static NameValueCollection GetEncryptedSettings() => 
ConfigurationManager.GetSection("encryptedSettings"as NameValueCollection;

Note that I recommend using a constant value for the sectionName parameter which is passed to the GetSection method, as per my sample project on GitHub.

The above method will return a NameValueCollection object which is essentially a key-value list of the encrypted settings, where the setting values can be accessed by their key name.

Determining encryption status

It is important that our code is able to differentiate plain values from encrypted values.

/// <summary>
/// Determines if a given string of text is encrypted.
/// </summary>
/// <param name="text">The text to check</param>
/// <returns>True if the text is encrypted, otherwise false</returns>
public static bool IsEncrypted(string text=> 
text.StartsWith("CipherValue:"StringComparison.OrdinalIgnoreCase);

Note that I recommend using a constant value for the ‘CipherValue:’ string used above, as per the sample project on GitHub.

The above method simply checks if the specified text starts with the encrypted text prefix identifier that I’ve chosen.

Encrypting settings

Now let’s look at how to encrypt a configuration file setting.

The helper method below can be used to encrypt a specific setting within the ‘encryptedSettings’ section of the App.config file.

/// <summary>
/// Encrypts the value for the specified key.
/// </summary>
/// <param name="key">The key/name of the encrypted setting</param>
/// <param name="value">The value to set for the encrypted setting</param>
private static void EncryptSetting(string keyobject value)
{
    // Open the configuration file and set the encrypted value for the specified setting key.
    Configuration configuration          = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
    AppSettingsSection encryptedSection  = configuration.GetSection("encryptedSettings"as AppSettingsSection;
    encryptedSection.Settings[key].Value = "CipherValue:" + EncryptionProvider.Encrypt(Convert.ToString(value), Settings.Entropy);
 
    // Save changes to the configuration file.
    configuration.Save(ConfigurationSaveMode.Modified);
 
    // Refresh the Encrypted Settings section within the configuration 
    // file to update the configuration details in-memory.
    ConfigurationManager.RefreshSection("encryptedSettings");
}

Note that I recommend using constant values for the string values used above, as per the sample project on GitHub. Settings.Entropy is a byte array containing a demo key for the entropy value.

In short, the above code opens the application configuration (App.config) file and gets the ‘encryptedSettings’ section. The specified setting is then accessed by its key and the value is set to the text ‘CiperValue:’ (so that we can identify encrypted data later) plus the encrypted string. The configuration file is then saved and the ‘encryptedSettings’ section is refreshed to make sure we have the latest setting values in memory whenever they are next accessed from our code.

We can create a public method to set an encrypted setting, as per the code below.

/// <summary>
/// Sets the encrypted value for the specified encrypted setting key.
/// </summary>
/// <param name="key">The key/name of the encrypted setting</param>
/// <param name="value">The value to set for the encrypted setting</param>
public static void Set(string keyobject value=> 
EncryptSetting(keyvalue);

The code simply passes the key and value parameters to the EncryptSetting method.

Decrypting settings

Next, let’s look at how to decrypt a configuration file setting.

/// <summary>
/// Gets the decrypted value for the specified encrypted setting key.
/// Automatically encrypts the setting if it is not already encrypted.
/// </summary>
/// <param name="key">The key/name of the encrypted setting</param>
/// <returns>The decrypted value as a string</returns>
public static string Get(string key)
{
    string value = GetEncryptedSettings()[key];
 
    // There's no need to decrypt/encrypt empty values.
    if (string.IsNullOrEmpty(value)) return value;
 
    if (EncryptionProvider.IsEncrypted(value))
    {
        // Get the encrypted data from the value (i.e. strip out the 'CipherValue:' prefix).
        value = value.Substring("CipherValue:".Length, value.Length - "CipherValue:".Length);
 
        // Decrypt the data.
        return EncryptionProvider.Decrypt(valueSettings.Entropy);
    }
    else
    {
        // If the setting is not already encrypted, encrypt it before returning the decrypted data.
        EncryptSetting(keyvalue);
 
        return value;
    }
}

Note that I recommend using constant values for the string values used above, as per my sample project on GitHub. Settings.Entropy is a byte array containing a demo key for the entropy value.

The code above gets the value of the specified setting using the GetEncryptedSettings method we defined earlier to access the setting by its key.

If the value of the setting is empty then there is nothing to decrypt or encrypt so we simply return the value.

Next, we check if the value is already encrypted.

If the value is encrypted we get a substring from the text, stripping out the ‘CipherValue:’ constant which is used to indicate that the value is encrypted. The value is then decrypted and returned as a string.

If the value is not already encrypted we encrypt it and then return it as a string.

Demonstration

Now let’s demonstrate how we can use the methods defined within the EncryptedSettings class to retrieve and store encrypted configuration file settings.

First up, let’s get the value of an encrypted setting.

var decryptedValue = EncryptedSettings.Get("MyEncryptedSetting");

The value returned in my example is as follows.

This text will be encrypted.

The first time we get a value for a setting, the EncryptedSettings class takes care of ensuring that the setting has been encrypted before returning its value.

The value of the setting in the App.config file (in the bin\Debug folder) will look something like the following.

CipherValue:AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAFUKo5cc1ykqZo1WjU/k3aAQAAAACAAAAAAAQZgAAAAEAACAAAAA6FrAIRERnB2ujYPREpMQIbeqAPsZxRiKgZkaSeMEasAAAAAAOgAAAAAIAACAAAABT9TRdNuytiJFmoKV9IEFYFDySDZum7i1HiBcZX1c8ziAAAABDFRtfPBjYQEqP4Ate0E0DkhYZrpOqTsK/5o2v5f2YekAAAAAkBJiI1CwnC1YfMbfnvo0TEc0i+V/CwHSDNLwl1rZ42TbST3rnquWWk8vEgFD+gGfqqbDcgDyV9ZSWJSrwgCoO

Now on to setting the value of an encrypted setting.

EncryptedSettings.Set("MyEncryptedSetting""This is the new encrypted text.");

As you can see from the above code, to update an encrypted setting it is simply a case of passing the appropriate encrypted setting key and the new value to store.

Summary

In summary, we can use the DPAPI to do the heavy lifting for us and take care of the difficult problem of taking care of the storage of cryptographic keys.

The .NET Framework provides a convenient way of using the DPAPI via the ProtectedData class.

There are plenty of possibilities to further improve or alter the code included in this article to suit your specific needs.

For example, you may wish to change the Data Protection Scope from ‘LocalMachine’ to ‘CurrentUser’, or perhaps abstract the concept of the ‘EncryptionProvider’ further by introducing an interface so that different methods of encryption can be supported.

You can view and download the code used in this article from the sample repository on GitHub.

Comments

This site uses Akismet to reduce spam. Learn how your comment data is processed.