Encrypting and Decrypting Data Using X.509 Certificates

posted on 11/02/09 at 08:24:08 pm by Joel Ross

LockOver the past few days, I've been working to change our build process and make it a little more flexible for our needs. As part of it, I was asked to look into how we encrypt our data, and see if we could come up with a better way to do it. Right now, we use DPAPI to encrypt information. It works great, but because the encryption is specific to a given machine, it means that encrypted values on one machine can't be decrypted on another machine. If we ever moved to a web farm, we'd be in trouble.

So I started looking at different options for how we could do it. The recurring solution I saw pointed to RSA encryption using X.509 certificates. Since you have the option to export your private keys for these certificates, you can share them across machines and decrypt across a web farm. And all the other side needs is the public key to encrypt the values.

I'm not going to into details about how to get the certificate or export the keys, but there are great resources for creating a test certificate (follow steps 1-3) and for importing and exporting them. These are the two links I used to get my machine ready. Note that in a production environment, you probably want to look at getting a certificate from a trusted certificate authority.

So now you've got your certificate set up, and you want to encrypt something. Now what? Well, don't do what I did my first time implementing it! I went along happily encrypting and decrypting passwords and everything was great. I even rolled the changes out to our team. Then I realized that there's a limit to how much text you can encrypt using this method. Apparently, for a 1024 bit certificate, it's 117 characters. Basically, the formula is (number of bits) / 8 - 11. So a 512 bit key will encrypt up to 53 characters. I don't know enough about encryption to adequately explain why this is, but a few searches turn this up over and over, and my own testing proves this out.

So what to do now? We have strings that can be over 117 characters. After talking it over with a co-worker, I realized that the correct way to go about this is to actually use a symmetric encryption, like AES. But that presents a problem, because both sides need to know the encryption key - a secret that everyone knows!  And if we were to use the same key over and over, the encryption could eventually be compromised. So we need to change up the key every time. But how will the other side know the key? That's where the certificate encryption comes in. We encrypt the key and send both the encrypted key and the message to the recipient. The other side grabs the encrypted key, decrypts it using the certificate, and then can decrypt the actual message. From my understanding, this is how SSL works, as well as PGP.

Theory's good, but code is better, right? So how do we do this? First, let's look at how AES encryption works. There's a class in System.Security.Cryptography called RijndaelManaged that implements AES for us. I created a class that wraps the complexity of using it. The constructor takes in an instance of the RijndaelManaged class, and uses that to both encrypt and decrypt data. First, the encryption method.

   1: public string Encrypt(string plainText)
   2: {
   3:     MemoryStream msEncrypt = null;
   4:     CryptoStream csEncrypt = null;
   5:     StreamWriter swEncrypt = null;
   6:  
   7:     try
   8:     {
   9:         var encryptor = _aesAlgorithm.CreateEncryptor(_aesAlgorithm.Key, _aesAlgorithm.IV);
  10:  
  11:         msEncrypt = new MemoryStream();
  12:         csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);
  13:         swEncrypt = new StreamWriter(csEncrypt);
  14:  
  15:         swEncrypt.Write(plainText);
  16:     }
  17:     finally
  18:     {
  19:         if (swEncrypt != null)
  20:             swEncrypt.Close();
  21:         if (csEncrypt != null)
  22:             csEncrypt.Close();
  23:         if (msEncrypt != null)
  24:             msEncrypt.Close();
  25:  
  26:         if (_aesAlgorithm != null)
  27:             _aesAlgorithm.Clear();
  28:     }
  29:  
  30:     return Convert.ToBase64String(msEncrypt.ToArray());
  31: }

The encryption uses the instance filed _aesAlgorithm to perform the encryption, which actually produces a byte array. I then convert that to a string, since that's what I want.

Decrypting data is essentially the same process, but reversed.

   1: public string Decrypt(string cipherText)
   2: {
   3:     MemoryStream msDecrypt = null;
   4:     CryptoStream csDecrypt = null;
   5:     StreamReader srDecrypt = null;
   6:  
   7:     string plaintext;
   8:  
   9:     try
  10:     {
  11:         ICryptoTransform decryptor = _aesAlgorithm.CreateDecryptor(_aesAlgorithm.Key, _aesAlgorithm.IV);
  12:  
  13:         msDecrypt = new MemoryStream(Convert.FromBase64String(cipherText));
  14:         csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
  15:         srDecrypt = new StreamReader(csDecrypt);
  16:  
  17:         plaintext = srDecrypt.ReadToEnd();
  18:     }
  19:     finally
  20:     {
  21:         if (srDecrypt != null)
  22:             srDecrypt.Close();
  23:         if (csDecrypt != null)
  24:             csDecrypt.Close();
  25:         if (msDecrypt != null)
  26:             msDecrypt.Close();
  27:  
  28:         if (_aesAlgorithm != null)
  29:             _aesAlgorithm.Clear();
  30:     }
  31:  
  32:     return plaintext;
  33: }

Now, let's look at the certificate side of things. I did the same thing here too - a class that wraps it and make things easier. But instead of having an algorithm for how the encryption should work (like RijndaelManaged is), this class takes an identifier for the certificate. I used the Thumbprint from it, since that was already in place in other areas for our system, and was a unique value I could grab. So, for this class, I pass in just the thumb print.

Encrypting is pretty simple with a certificate.

   1: public string Encrypt(string plainText)
   2: {
   3:     var signatureCertificate = GetCertificate();
   4:     var rsaProvider = (RSACryptoServiceProvider)signatureCertificate.PublicKey.Key;
   5:     return Convert.ToBase64String(rsaProvider.Encrypt(plainText, false));
   6: }

The complications to the process reside in the GetCertificate() method. Once you have the certificate, encrypting is straight forward. Note that you only need the public key to encrypt - so you can pass around your certificate without the private key and they can encrypt data, but not decrypt it. Only holders of the private key will be able to decrypt it.

Decrypting is just as simple.

   1: public string Decrypt(string cipherText)
   2: {
   3:     var signatureCertificate = GetCertificate();
   4:     var rsaProvider = (RSACryptoServiceProvider)signatureCertificate.PrivateKey;
   5:     return Encoding.ASCII.GetString(rsaProvider.Decrypt(Convert.FromBase64String(cipherText), false));
   6: }

Note here that it's using the private key from the certificate to do the decryption.

The heart of using this method is in actually getting the certificate. To do that, we have a method that retrieves it by it's thumbprint.

   1: private X509Certificate2 GetCertificate()
   2: {
   3:     var x509Store = new X509Store(StoreLocation.LocalMachine);
   4:     x509Store.Open(OpenFlags.ReadOnly);
   5:  
   6:     foreach (var storeCertificate in x509Store.Certificates)
   7:     {
   8:         if (_thumbprint.ToLower() == storeCertificate.Thumbprint.ToLower())
   9:         {
  10:             return storeCertificate;
  11:             break;
  12:         }
  13:     }
  14:     
  15:     return null;
  16: }

By the way, the actual code is a little more, umm, robust for our purposes, but I eliminated the noise in favor of clarity.

We have the pieces in place for this to work, and now we just need to pull it all together. Again, I made a class that wraps the whole process and makes it easier to use.

First, the encryption part:

   1: public string Encrypt(string plainText)
   2: {
   3:     var aesAlgorithm = new RijndaelManaged();
   4:     var aesDataProtector = new AesDataProtector(aesAlgorithm);
   5:  
   6:     var rsaDataProtected = new RsaDataProtector(_thumbprint);
   7:  
   8:     var keyCipher =  Convert.ToBase64String(rsaDataProtector.Encrypt(Convert.ToBase64String(aesAlgorithm.Key)));
   9:     var ivCipher = Convert.ToBase64String(rsaDataProtector.Encrypt(Convert.ToBase64String(aesAlgorithm.IV)));
  10:     var textCipher = aesDataProtector.Encrypt(plainText);
  11:  
  12:     return keyCipher + ivCipher + textCipher;
  13: }

There's a lot going on here. But here's the gist of it. Get an AES encryption implementation, give it a key and initialization vector (IV. Honestly, I'm not totally clear on how that's used, but it's required and needed later to decrypt the string), encrypt those values using the certificate, encrypt the message with AES, then send them all along for the ride.

Decrypting is similar, except we have to do things in a certain order. First, we have to split the message up into it's three parts. Then we can decrypt the key and IV using the certificate, create our AES implementation using those values, and from there, we can decrypt the message.

   1: public string Decrypt(string cipherText)
   2: {
   3:     var rsaDataProtector = new RsaDataProtector(_thumbprint);
   4:  
   5:     var keyEnd = 172;
   6:     var ivEnd = 344;
   7:  
   8:     var keyString = cipherText.Substring(0, keyEnd);
   9:     var ivString = cipherText.Substring(keyEnd, ivEnd - keyEnd);
  10:     var keyBytes = Convert.FromBase64String(keyString);
  11:     var ivBytes = Convert.FromBase64String(ivString);
  12:  
  13:     var aesAlgorith = new RijndaelManaged
  14:                           {
  15:                               Key = rsaDataProtector.Decrypt(keyBytes),
  16:                               IV = rsaDataProtector.Decrypt(ivBytes)
  17:                           };
  18:  
  19:     var aesDataProtector = new AesDataProtector(aesAlgorith);
  20:     return aesDataProtector.Decrypt(cipherText.Substring(ivEnd, cipherText.Length - ivEnd));
  21: }

Most of this is basic string manipulation. Again, for simplicity, the keyEnd and ivEnd values are hardcoded (and work with a 1024 bit certificate), but in my app, they are determined by the location of terminator values. I'll leave that part as an exercise for the reader. But note that encrypted values are typically terminated by an equals sign.

And that's pretty much it. The nice thing about this is that the only thing both sides need to know are details about the certificate. And if someone just wants to send you an encrypted value, you give them your public key, and that's it. No exchange of secrets at all!

Photo courtesy of AMagill

Categories: Development, C#