03/23/21

How to encrypt data in .NET Core applications on macOS without DPAPI

This article talks about data encryption in a .NET Core application on macOS. It assumes that you've heard of DPAPI, a popular approach to encrypting data on Windows, and that you want to use it on macOS. Using DPAPI on macOS isn't possible, and the article presents a modern approach to data encryption on macOS.

We assume that you have a .NET Core application that you want to run on macOS and that the application should encrypt the user’s data.
Why do we mention DPAPI? It’s simple, and it’s the default way to encrypt the data for Windows applications, so it’s worth having a quick look at.

Windows - Data Protection API

DPAPI has several advantages:

  • It provides a simple interface to “protect” and “unprotect” the data without fiddling with cryptography algorithms
  • It can encrypt the data to be accessible only to the user who runs the applications so that another user wouldn’t be able to decrypt the data

Time to get our hands dirty. Let's see the code!

using System;
using System.Security.Cryptography;
using System.Text;

namespace MacOsDataEncryption
{
    class Program
    {
        static void Main(string[] args)
        {
            string plaintextSecret = "Something so secret that we can't put it here";
            byte[] rawSecretBytesToProtect = Encoding.UTF8.GetBytes(plaintextSecret);

            byte[] protectedSecretBytes = ProtectedData.Protect(rawSecretBytesToProtect, null, DataProtectionScope.CurrentUser);

            byte[] unprotectedSecretBytes = ProtectedData.Unprotect(protectedSecretBytes, null, DataProtectionScope.CurrentUser);
        }
    }
}

Install the System.Security.Cryptography.ProtectedData platform extension from NuGet, and run the code.
Ooops, ProtectedData.Protect throws a System.PlatformNotSupportedException saying that Windows Data Protection API (DPAPI) is not supported on this platform. What can we do about this?

macOS - ASP.NET Core Data Protectio

.NET provides a modern cross-platform solution for encryption - ASP.NET Core Data Protection, distributed as a NuGet package.
Despite the package having ASP.NET in the name, it’s not a mandatory requirement and you can use the package in any .NET Core application.

ASP.NET Core Data Protection provides an API similar to DPAPI:

static void Main(string[] args)
{
   IServiceCollection serviceCollection = new ServiceCollection();
   ConfigureServices(serviceCollection);
   IDataProtector dataProtector = serviceCollection
       .BuildServiceProvider()
       .GetDataProtector(purpose: "MacOsEncryption");
  
   string plaintextSecret = "Something so secret that we can't put it here";
  
   byte[] rawSecretBytesToProtect = Encoding.UTF8.GetBytes(plaintextSecret);
   byte[] protectedSecretBytes = dataProtector.Protect(rawSecretBytesToProtect);

   byte[] unprotectedSecretBytes = dataProtector.Unprotect(protectedSecretBytes);
}

We’d like to configure the scope of encryption to be similar to DataProtectionScope.CurrentUser in DPAPI, so we’ll configure the Data Protector to store keys in the user's application data folder.

private static void ConfigureServices(IServiceCollection serviceCollection)
{
   string dataProtectionKeysDirectory = Path.Combine(
       Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
       "MacOsEncryption-Keys");
  
   serviceCollection.AddDataProtection()
       .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysDirectory));
}

We’ve now gotten DataProtector.Protect and DataProtector.Unprotect to work, but how does ASP.NET Core Data Protection encrypt and decrypt your data?
Where does it store the key to decrypt the data, and how does it protect the key itself from hackers?

ASP.NET Core Data Protection key management

Let’s dive deeper to answer the questions above, and inspect the ~/.local/share/MacOsEncryption-Keys folder - it contains the ASP.NET Core Data Protection key.

key-aeeb2d84-d1b0-43e4-aa63-372f2c327104.xml:


<key id="aeeb2d84-d1b0-43e4-aa63-372f2c327104" version="1">
  <creationDate>2021-02-27T20:12:30.465209Z</creationDate>
  <activationDate>2021-02-27T20:12:30.410391Z</activationDate>
  <expirationDate>2021-05-28T20:12:30.410391Z</expirationDate>
  <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
    <descriptor>
      <encryption algorithm="AES_256_CBC" />
      <validation algorithm="HMACSHA256" />
      <masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
        <!-- Warning: the key below is in an unencrypted form. -->     
        <value>vxjPleL7WMLlVz/kJMDYJmUA+5qRUAlj64XWQeUXYrsxP3h3Gkma21bFMnDRyErXGYXoEBVoU70G69pjeQ5cdg==</value>
      </masterKey>
    </descriptor>
  </descriptor>
</key>

ASP.NET Core Data Protection uses the key to decrypt protected data, but it doesn’t protect the key itself.
On Windows, ASP.NET Core Data Protection encrypts the key using DPAPI. Since DPAPI isn’t available on macOS, the key is unencrypted and stored as plaintext.
If a hacker or another user steals the key, they would be able to decrypt the application data.

Fortunately, ASP.NET Core Data Protection provides developers with multiple ways to encrypt the keys at rest.

Using an X.509 certificate to encrypt the key at rest is a viable option for a desktop application.

How to encrypt ASP.NET Core Data Protection key at-rest using X.509 certificate

We assume you don’t have an X.509 certificate, so we’ll create a self-signed one:

static X509Certificate2 CreateSelfSignedDataProtectionCertificate(string subjectName)
{
   using (RSA rsa = RSA.Create(2048))
   {
       CertificateRequest request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256,
           RSASignaturePadding.Pkcs1);
       X509Certificate2 certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddYears(1));
       return certificate;
   }
}

Install the self-signed certificate as non-exportable to prevent stealing:

static void InstallCertificateAsNonExportable(X509Certificate2 certificate)
{
   byte[] rawData = certificate.Export(X509ContentType.Pkcs12, password: "");
  
   using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
   {
       store.Certificates.Import(rawData, password: "", keyStorageFlags: X509KeyStorageFlags.PersistKeySet);
   }
}

The X509KeyStorageFlags.PersistKeySet flag saves the certificate into the login KeyChain on macOS. This way, the application can access the certificate and decrypt the data on the next run.

Make sure that the application uses the same certificate between runs:

static X509Certificate2 SetupDataProtectionCertificate()
{
   string subjectName = "CN=ASP.NET Core Data Protection Certificate";
   using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadOnly))
   {
       X509Certificate2Collection certificateCollection = store.Certificates.Find(X509FindType.FindBySubjectName,
           subjectName,
           // self-signed certificate won't pass X509 chain validation
           validOnly: false);
       if (certificateCollection.Count > 0)
       {
           return certificateCollection[0];
       }

       X509Certificate2 certificate = CreateSelfSignedDataProtectionCertificate(subjectName);
       InstallCertificateAsNonExportable(certificate);
       return certificate;
   }
}

Finally, configure ASP.NET Core Data Protection to encrypt the keys at rest using the certificate:

private static void ConfigureServices(IServiceCollection serviceCollection)
{
   string dataProtectionKeysDirectory = Path.Combine(
       Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
       "MacOsEncryption-Keys");
   X509Certificate2 dataProtectionCertificate = SetupDataProtectionCertificate();

   serviceCollection.AddDataProtection()
       .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysDirectory))
       .ProtectKeysWithCertificate(dataProtectionCertificate);
}

Ensure that the ASP.NET Core Data Protection key is encrypted at rest

Remove the plaintext key, and run the application to re-create the key and encrypt it with the X.509 certificate.
Let’s inspect the new key:

<?xml version="1.0" encoding="utf-8"?>
<key id="43205b1d-7488-4ce9-b181-292becd03d9c" version="1">
  <creationDate>2021-02-28T12:37:39.824539Z</creationDate>
  <activationDate>2021-02-28T12:37:39.771816Z</activationDate>
  <expirationDate>2021-05-29T12:37:39.771816Z</expirationDate>
  <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
    <descriptor>
      <encryption algorithm="AES_256_CBC" />
      <validation algorithm="HMACSHA256" />
      <encryptedSecret decryptorType="Microsoft.AspNetCore.DataProtection.XmlEncryption.EncryptedXmlDecryptor, Microsoft.AspNetCore.DataProtection, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60" xmlns="http://schemas.asp.net/2015/03/dataProtection">
        <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">
          <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
          <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
            <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
              <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
              <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                <X509Data>
                  <X509Certificate>MIIC5zCCAc+gAwIBAgIJAKQ2/UREcTEYMA0GCSqGSIb3DQEBCwUAMDMxMTAvBgNVBAMTKEFTUC5ORVQgQ29yZSBEYXRhIFByb3RlY3Rpb24gQ2VydGlmaWNhdGUwHhcNMjEwMjI4MTIzNjM5WhcNMjIwMjI4MTIzNzM5WjAzMTEwLwYDVQQDEyhBU1AuTkVUIENvcmUgRGF0YSBQcm90ZWN0aW9uIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAubbJ7ULbZhX5sllxUIq/UDbLuHIWJflO/jbnc8tyxHoTg3ZJe/icYZh4OteFa7bEtxqUHhL1RXo2AS2URdfuhD9aS/Q8LSw/1JGRVJRT86stl/YeN7tjLXPihDMPcIHU+HAgcDE6/l0Y7o9H//+6J43Q+aEqjcFry7LZU3Ulx4CxOlhpehNAMQtSxpS8q3uhJtKEF2g5lfNeIHA+XgmLEndON4D/XtheCuFYoB5unAV9B0ZE6bcbycBrWl+p6jc+aTH3f8lbXzNspLSdf01wp/eTR87LhO3jSVDtUA22mVssDurqBBTowbc/95y+mo2n3+vKROitKQLz+2QbFuc7MQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAFJlRTwkSMQlj3yZWH1wSUE913i/a3PiUmTSM/FSiV1cDB1DNtKYkasseC/WxLM16bqHNuHAtDz+gvhO3hu8oILhJfTRiYekWg/xu0noFfsVcSArjcvdedRNRAE30bJhrDhdDI8c+7abKWp1WKJU/vh9AO0mQNAWJtQMzZMcVAcYcZmxkJlZ758kFrI5VQGMchwOnfTeAq9Au+vmNpEGQn8H8D72Ao2Az8d+YGLXyxkyZrQRrCNquJghyP1R2gGY54/PQubzF5A9bEQLpmudG0GE2Tmzj2Ixl5LplipVvEC38dsNDrtvYO6roNnh3adGV4sDsRXzdBnho22QlH490X</X509Certificate>
                </X509Data>
              </KeyInfo>
              <CipherData>
                <CipherValue>siyvFcISJU3Z0+N4EwAYAZeRl+0c8P6Y9uge7qHW+qaVsYEeugIT37hvZjfJM6Hcm7lsnLPxpExOJLVKguIAOdzYjOHFX/sZyU87UBGLUEUAOgpUFt7iVOU3PJu06tVhz22tWiEixlXnsmZDOIXoK+GsT1q3SOJQx6Q9L5jfGFEDbUu4YW4jP3dDcRZaCxKDg5T/1YqBv2rvFkNClvFH/drhc/lBqoqMe5aM5ulEtCVRLCsXGPPEHGk/seJ4btBQ+6YsOYyCv6uSsptS1PiEKTxbjozLyUMfQzIcQE5jmCGtBVq56/tLYuMOV53uXkLDMQULqtLn14ln9Uyq5XgsaA==</CipherValue>
              </CipherData>
            </EncryptedKey>
          </KeyInfo>
          <CipherData>
            <CipherValue>qOCALG5O0/EVHHSXLgy/0WPtLsKHLbeLQ9Kg/YpzrgBenFYtPpmSPGu6C20Fg4N4h9w0ju2ohP/iLM/9s/meAbVxalXpllPVUPjuzHenYLiR/eiKlq5thkhbnjEYvASVCcWKToOGRBbMlEsXvZgbjSAH/dQSdOhtORAlmsNHCUDomM1t5CA42MB7ckf/vfcOBOtvfwZNkBp5zPn+bYj4YNNjzoXv6j9Lew5dQNsHNiknwqZmEYpH3+B2XbMv+u+gNGi9CRgWZu2qJ530Wz+heVjyv3oYbkW45cHkjA+WC8Nn2rKdZgS7VzhYMa7OfHwv/aZMkbVqsbiqGc8GnJ1o5aQfOGJF/021BCenALTYg+cadQOfSi2tYqUtNsEzBde5RZ/BR7HqjHJCQKuDzOjeqg==</CipherValue>
          </CipherData>
        </EncryptedData>
      </encryptedSecret>
    </descriptor>
  </descriptor>
</key>

Voila, the key is encrypted and safe to use!

Security analysis

Offline attack

Since the key is not exportable, a hacker couldn’t steal the key and X.509 certificate to decrypt the data.

Online attack

Hackers could reverse engineer the application, write code that emulates the decryption mechanism of your application, and somehow run it for the target user.
Fortunately, macOS has built-in protection from this kind of attack.
When your application imports the X.509 certificate into the KeyChain, macOS adds the application into the certificate ACL.
When any other application tries to access this certificate, macOS would warn the user that an unauthenticated application is trying to access the certificate in the KeyChain, and will prompt the user to allow or deny the access.

Conclusion

ASP.NET Core Data Protection is a modern way to implement encryption, and you can use it to create a cross-platform application that runs on Windows and macOS.