Show Changes Show Changes
Edit Edit
Print Print
Recent Changes Recent Changes
Subscriptions Subscriptions
Lost and Found Lost and Found
Find References Find References
Rename Rename
Search

History

4/14/2010 5:31:21 PM
List all versions List all versions
How To Store Secrets On A Machine
.

This has got to be one of the most frequently asked questions I get when I teach security classes: "How should I store my connection strings on the Web server?" It doesn't always take that exact form, but a lot of people out there need to store sensitive data on Web servers and other often attacked machines. It's a tricky problem with no perfect answers.

Here's the deal. Imagine a Web server that needs a password to connect to some back end machine running on some other platform where Kerberos authentication isn't an option. The server process will need to read that password at some point, and therein lies the problem. Any data that the server process can read can be read by an attacker who compromises the server process. For example, if the attacker can run arbitrary code in the server process, he can read the secret.

So why don't we just encrypt the password so the attacker will see only ciphertext if he goes looking for it? (That's usually the second question). You've got to remember that encryption algorithms never eliminate secrets. They're designed to take big secrets (like e-mail messages, documents, etc.) and compress them into small secrets, which we call keys. But there's still a secret! It's the key. And if the server program can read the key, the attacker can read it. You haven't gotten rid of the secret by encrypting it; you've only pushed the problem back a bit.

The first thing you should try to do is eliminate the secret if at all possible. By using integrated security with SQL Server, you can avoid having to store passwords in your connection strings, for example. This should be your first avenue of defense!

If you can't eliminate the secret, then protect it using defense in depth (WhatIsThePrincipleOfDefenseInDepth). You know you'll never have perfect protection (not even close to it), but you want to put up every roadblock possible between your adversary and your secret. So don't do something silly like store the secret in a file that's sitting in a virtual directory on a Web server (web.config comes to mind). Web servers have been known to accidentally allow files to be downloaded because of bugs. For example, connection strings in classic ASP pages could be stolen in the past by pointing a Web browser to page.asp::$DATA instead of page.asp. This fooled IIS into thinking that the request was for a static file because .asp::$DATA wouldn't match anything in its script map. But the suffix ::$DATA has special meaning to the operating system: It indicates the default NTFS stream for the file, which is what you get when you read the contents of the file normally. In other words, asking the file system for page.aspx::$DATA is the same as asking it for the contents of page.aspx. Thus IIS would serve up the source of the ASP page instead of interpreting it as a script. There have been lots of shenanigans like this over the years, but most folks would agree that you're better off storing sensitive files outside of any virtual directory on a Web server. Even better, keep sensitive files on a different partition then where your virtual directories reside.

You should consider protecting secrets using the Data Protection API (DPAPI). This consists of a couple of Win32 functions that allow you to encrypt (CryptProtectData) and decrypt (CryptUnprotectData) data using keys controlled by the system. DPAPI also provides integrity protection via a MAC (WhatIsCIA), so if the data is tampered with by someone who doesn't know the machine key, the unprotect function will fail.

Using DPAPI, you can encrypt data with a user's login credentials (which means you need to decrypt the data in the same security context in which it was encrypted), or you can encrypt the data using the machine's credentials. If you encrypt with the user’s credentials, when the user is not logged in to the machine, her key is not present on the machine at all, which is fantastic! But when you’re storing secrets that need to be accessed by a server that runs 24/7, since the server is going to be logged in all the time anyway, you may as well use the machine’s credentials instead, because that’ll make administration easier. For example, if your server runs as Network Service, you don’t want the administrator to have to encrypt secrets while running as Network Service. So my examples here will use the machine’s credentials.

Note that DPAPI only works if you decrypt the data on the same machine on which it was encrypted, so be careful how you use it in a load-balanced environment. For example, if one machine encrypts the data and stores it in a database, don't expect another machine to be able to read and decrypt it. And remember, an attacker who can run code on the machine can call these functions as easily as you can! All we're doing here is ensuring that, if the attacker reads a file with secret data in it, he gets ciphertext instead of plaintext. Either he needs to get root access to the machine and compromise the machine’s DPAPI key, or he needs to be able to run code on the machine to call the decryption function.1 We're not making it impossible, but we're putting a protection countermeasure in his way to slow down the attack (WhatIsACountermeasure).

DPAPI is wrapped by the .NET Framework version 2.0, but for those of you using 1.x, I’ve written a class in Managed C++ called Secret to help, and I've shown the two static methods it exposes in Figure 70.1.

 String* Secret::EncryptWithMachineKey(
    String* plaintext,
    String* additionalEntropy) {


    if (0 == plaintext) {
        throw new ArgumentException(S"plaintext required");
    }
    if (0 == additionalEntropy ||
        0 == additionalEntropy->Length) {
        throw new ArgumentException(
            S"additionalEntropy required");
    }
    const wchar_t __pin* pszPlaintext =
        PtrToStringChars(plaintext);
    DATA_BLOB dataIn = {
        plaintext->Length * sizeof(wchar_t),
        (BYTE*)pszPlaintext };


    const wchar_t __pin* pszAdditionalEntropy =
        PtrToStringChars(additionalEntropy);
    DATA_BLOB entropy = {
        additionalEntropy->Length * sizeof(wchar_t),
        (BYTE*)pszAdditionalEntropy };


    DATA_BLOB dataOut;
    if (!CryptProtectData(&dataIn, L"secret_data", &entropy,
                          0, 0,
                          CRYPTPROTECT_LOCAL_MACHINE |
                          CRYPTPROTECT_UI_FORBIDDEN,
                          &dataOut)) {
        throwWin32Exception(L"CryptProtectData");
    }


    Byte ciphertext[] = new Byte[dataOut.cbData];
    Byte __pin* p = &ciphertext[0];
    CopyMemory(p, dataOut.pbData, dataOut.cbData);
    LocalFree(dataOut.pbData);


    return Convert::ToBase64String(ciphertext);
 }


 String* Secret::DecryptWithMachineKey(
    String* base64EncodedCiphertext,
    String* additionalEntropy) {


    if (0 == base64EncodedCiphertext) {
        throw new ArgumentException(
            S"base64EncodedCiphertext required");
    }
    if (0 == additionalEntropy ||
        0 == additionalEntropy->Length) {
        throw new ArgumentException(
            S"additionalEntropy required");
    }
    Byte ciphertext[] = Convert::FromBase64String(
        base64EncodedCiphertext);
    Byte __pin* pCiphertext = &ciphertext[0];
    DATA_BLOB dataIn = {
        ciphertext->Length,
        (BYTE*)pCiphertext };


    const wchar_t __pin* pszAdditionalEntropy =
        PtrToStringChars(additionalEntropy);
    DATA_BLOB entropy = {
        additionalEntropy->Length * sizeof(wchar_t),
        (BYTE*)pszAdditionalEntropy };


    DATA_BLOB dataOut;
    if (!CryptUnprotectData(&dataIn, 0, &entropy, 0, 0,
        CRYPTPROTECT_UI_FORBIDDEN, &dataOut)) {
        throwWin32Exception(L"CryptUnprotectData");
    }


    String* plaintext = new String(
        (wchar_t*)dataOut.pbData,
        0, dataOut.cbData / sizeof(wchar_t));
    LocalFree(dataOut.pbData);


    return plaintext;
 }

Figure 70.1: Wrapping DPAPI in Managed C++

This class always uses the machine key (which is usually a good tradeoff between deployability and security). Here's an example of its usage from a C# program:

 using System;
 using KBC.WindowsSecurityUtilities;


 class TestSecret {
  static void Main(string[] args) {
    if (args.Length != 1) {
      Console.WriteLine("usage: testsecret secret");
      return;
    }
    string plaintext = args[0];
    string ciphertext =
      Secret.EncryptWithMachineKey(plaintext, "TestSecret");
    Console.WriteLine("Encrypted string: {0}", ciphertext);
    plaintext =
      Secret.DecryptWithMachineKey(ciphertext, "TestSecret");
    Console.WriteLine("Decrypted string: {0}", plaintext);
  }
 }

One thing that this class does not address is erasability. This is something that managed environments like the .NET Framework and Java aren't very good about handling. How do you erase a string once you're done with the secret data that it holds? Wait for the garbage collector to collect it? Even then there's no guarantee it will be overwritten anytime soon. You could store the secret in a byte array and then overwrite the byte array when you're done, but what if a garbage collection occurs, and the byte array is compacted (i.e., copied to another location in memory)? And this says nothing about paging: I don't know of a way to lock managed heap memory so it's not paged out to a swapfile. Finally, a byte array is useless if the function that needs the data expects a string, and this is where we usually get nailed. Have you ever seen a constructor for SqlConnection that accepts a connection string in the form of a byte array? I certainly haven't. The only ones who can help us here are the .NET Framework team, I'm afraid. This is an area that needs improved support by the runtime.

Another thing this class doesn't address is where the ciphertext should be stored once it's encrypted. Your best bet is to put the ciphertext in a file or registry key that won't be easily accessible to an attacker, and put a strong DACL (WhatIsAnAccessControlList) on it. By strong, I mean one that allows administrators to read and write the secret, and allows the account your server runs under to read the secret.

Secrets in ASP.NET configuration files

ASP.NET takes this approach for the few secrets that it allows in its configuration files. From an administrative command prompt, you run a tool called aspnet_setreg to encrypt a secret and tuck it away in the registry. Here's an example:

 aspnet_setreg -k:SOFTWARE\MyApp\MySecret -p:"Attack at dawn"

The tool prints out instructions on what to do next, but suffice it to say that there is now a registry key (HKLM/SOFTWARE/MyApp/MySecret/ASPNET_SETREG) that holds a value named "password" which contains the ciphertext for "Attack at dawn". You can now replace a secret in your ASP.NET configuration file with the following string: "HKLM/SOFTWARE/MyApp/MySecret/ASPNET_SETREG,password", and ASP.NET will know what to do: it'll read the ciphertext from that key, then use DPAPI to decrypt it using the machine key. Of course this only works for keys that ASP.NET knows about:

 <identity userName='...' password='...' />
 <processModel userName='...' password='...' />
 <sessionState stateConnectionString='...' sqlConnectionString='...' />

This mechanism isn't a bad one to emulate if you need to manage your own secrets. I've heard rumors that a feature like this is slated to be generalized for use on any section of an XML configuration file in VS.NET 2005. As for today, check out knowledge base article 329290 for more details on obtaining and using aspnet_setreg.exe.

The DataProtection class

Version 2.0 of the .NET Framework introduces a class called DataProtection that wraps DPAPI. It's simple to use; in fact, it looks almost exactly like the wrapper class I provided above. I've shown an example in figure 70.2.

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


 class Program {
  const string applicationEntropy = "Some application secret";
  static void Main() {
    string secret = "Attack at dawn";
    Console.WriteLine("Encrypting: {0}", secret);
    string base64Ciphertext = Encrypt(secret);
    Console.WriteLine("Decrypting: {0}", base64Ciphertext);
    Console.WriteLine("Result: {0}", Decrypt(base64Ciphertext));
  }
  static string Encrypt(string plaintext) {
    byte[] encodedPlaintext = Encoding.UTF8.GetBytes(plaintext);
    byte[] encodedEntropy = Encoding.UTF8.GetBytes(
      applicationEntropy);


    byte[] ciphertext = ProtectedData.Protect(encodedPlaintext,
      encodedEntropy, DataProtectionScope.LocalMachine);


    return Convert.ToBase64String(ciphertext);
  }
  static string Decrypt(string base64Ciphertext) {
    byte[] ciphertext = Convert.FromBase64String(base64Ciphertext);
    byte[] encodedEntropy = Encoding.UTF8.GetBytes(
      applicationEntropy);


    byte[] encodedPlaintext = ProtectedData.Unprotect(ciphertext,
      encodedEntropy, DataProtectionScope.LocalMachine);


    return Encoding.UTF8.GetString(encodedPlaintext);
  }
 }

Figure 70.2: Using DPAPI from C# (.NET Framework v2.0)

The output from this application is shown below. You might be surprised by the size of the ciphertext. Please realize that encrypting data doesn't cause it to get bigger. The reason the ciphertext is so long is because DPAPI is doing more than just encrypting: it's also integrity protecting the data (item 58). So the ciphertext includes a message authentication code (MAC) along with some other metadata that DPAPI needs.

 Encrypting: Attack at dawn
 Decrypting: AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAbcJjHJOz8kOjJ+hqZRZHS
 gQAAAACAAAAAAADZgAAqAAAABAAAABXEBvjoNiqmbvOsn5M56dpAAAAAASAAACgAA
 AAEAAAAM2yg+TTDbC1DFcjO9kKE1QQAAAAGa+tMkvYVFo3W6eaDfuDqRQAAAAdo4n
 0OtQqpUOdhx7A6gIWBqSBgw==
 Result: Attack at dawn

1 less obvious would be the attacker obtaining a "ghosted" image of the operating system, which would have the same machine key!

PortedBy KevinKenny

PluralsightTraining

Keith's first book-in-a-wiki. If you would like to read the book online or order a physical copy to throw at annoying coworkers, surf to the HomePage. Please note that due to overwhelming wikispam, this particular wiki is no longer editable.

About FlexWiki.

Recent Topics