Basic two-way encryption in .NET

There are lots cases when we need to encrypt and then decrypt data using .NET. A great example is if we want to store something secure in a cookie. We write the encrypted data to the web-response and decrypt it again again later from the next web-request.

I’ve been using the following algorithm for two-way encryption for a number of years now. I found it on the web a long time ago, so unfortunately don’t have a link to the original source any more. If anyone can help me here that would be appreciated.

First, let’s start with an interface as the chances are we’re going to need to inject encryption capabilities into other objects using dependency injection:

namespace Encryption
{
    public interface IEncryptor
    {
        string Encrypt(string text);
        string Decrypt(string encryptedText);
    }
}

The implementation for this is as follows:

using System;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.IO;
using System.Globalization;
using System.Runtime.InteropServices;

namespace Encryption
{
    public class Encryptor : IEncryptor
    {
        private readonly SecureString _password;

        public Encryptor(SecureString password)
        {
            _password = password;
        }

        public string Encrypt(string text)
        {
            return Encrypt(text, GetDefaultSalt());
        }

        public string Encrypt(string text, string salt)
        {
            if (text == null)
            {
                return null;
            }

            RijndaelManaged rijndaelCipher;
            byte[] textData;
            ICryptoTransform encryptor;

            using (rijndaelCipher = new RijndaelManaged())
            {
                var secretKey = GetSecretKey(salt);

                // First we need to turn the input strings into a byte array.
                textData = Encoding.Unicode.GetBytes(text);

                // Create a encryptor from the existing secretKey bytes.
                // We use 32 bytes for the secret key. The default Rijndael 
                // key length is 256 bit (32 bytes) and then 16 bytes for the 
                // Initialization Vector (IV). The default Rijndael IV length is 
                // 128 bit (16 bytes).
                encryptor = rijndaelCipher.CreateEncryptor(secretKey.GetBytes(32), secretKey.GetBytes(16));
            }

            MemoryStream memoryStream;
            byte[] encryptedData;

            // Create a MemoryStream that is going to hold the encrypted bytes:
            using (memoryStream = new MemoryStream())
            {
                // Create a CryptoStream through which we are going to be processing 
                // our data. CryptoStreamMode.Write means that we are going to be 
                // writing data to the stream and the output will be written in the 
                // MemoryStream we have provided.
                using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                {
                    // Start the encryption process.
                    cryptoStream.Write(textData, 0, textData.Length);

                    // Finish encrypting.
                    cryptoStream.FlushFinalBlock();

                    // Convert our encrypted data from a memoryStream into a byte array.
                    encryptedData = memoryStream.ToArray();

                    // Close both streams.
                    memoryStream.Close();
                    cryptoStream.Close();
                }
            }

            // Convert encrypted data into a base64-encoded string.
            // A common mistake would be to use an Encoding class for that.
            // It does not work, because not all byte values can be
            // represented by characters. We are going to be using Base64 encoding.
            // That is designed exactly for what we are trying to do.
            var encryptedText = Convert.ToBase64String(encryptedData);

            // Return encrypted string.
            return encryptedText;
        }

        public string Decrypt(string encryptedText)
        {
            return Decrypt(encryptedText, GetDefaultSalt());
        }

        public string Decrypt(string encryptedText, string salt)
        {
            if (encryptedText == null)
            {
                return null;
            }

            RijndaelManaged rijndaelCipher;
            byte[] encryptedData;
            ICryptoTransform decryptor;

            using (rijndaelCipher = new RijndaelManaged())
            {
                var secretKey = GetSecretKey(salt);

                // First we need to turn the input strings into a byte array.
                encryptedData = Convert.FromBase64String(encryptedText);

                // Create a decryptor from the existing SecretKey bytes.
                decryptor = rijndaelCipher.CreateDecryptor(secretKey.GetBytes(32), secretKey.GetBytes(16));
            }

            MemoryStream memoryStream;
            byte[] unencryptedData;
            int decryptedDataLength;

            using (memoryStream = new MemoryStream(encryptedData))
            {
                // Create a CryptoStream. Always use Read mode for decryption.
                using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                {
                    // Since at this point we don't know what the size of decrypted data
                    // will be, allocate the buffer long enough to hold EncryptedData;
                    // DecryptedData is never longer than EncryptedData.
                    unencryptedData = new byte[encryptedData.Length];

                    // Start decrypting.
                    try
                    {
                        decryptedDataLength = cryptoStream.Read(unencryptedData, 0, unencryptedData.Length);
                    }
                    catch
                    {
                        throw new CryptographicException("Unable to decrypt string");
                    }                    

                    cryptoStream.Close();
                    memoryStream.Close();
                }
            }

            // Convert decrypted data into a string.
            var decryptedText = Encoding.Unicode.GetString(unencryptedData, 0, decryptedDataLength);

            // Return decrypted string.  
            return decryptedText;
        }

        private PasswordDeriveBytes GetSecretKey(string salt)
        {
            // We are using salt to make it harder to guess our key
            // using a dictionary attack.
            var encodedSalt = Encoding.ASCII.GetBytes(salt);

            var valuePointer = IntPtr.Zero;
            try
            {
                // The Secret Key will be generated from the specified
                // password and salt.
                valuePointer = Marshal.SecureStringToGlobalAllocUnicode(_password);
                return new PasswordDeriveBytes(Marshal.PtrToStringUni(valuePointer), encodedSalt);
            }
            finally
            {
                Marshal.ZeroFreeGlobalAllocUnicode(valuePointer);
            }            
        }

        private string GetDefaultSalt()
        {
            return _password.Length.ToString(CultureInfo.InvariantCulture);
        }
    }
}

This can then be used as follows:

using System;
using System.Security;

namespace Encryption
{
    public static class Program
    {
        public static void Main()
        {
            using (var password = GetPassword())
            {
                var encryptor = new Encryptor(password);

                const string text = "Hello World!";

                var encryptedText = encryptor.Encrypt(text);
                var decryptedText = encryptor.Decrypt(encryptedText);

                Console.WriteLine(encryptedText);
                Console.WriteLine(decryptedText);

                Console.ReadLine();
            }
        }

        private static SecureString GetPassword()
        {
            var password = new SecureString();

            password.AppendChar((char)80); // P
            password.AppendChar((char)97); // a
            password.AppendChar((char)115); // s
            password.AppendChar((char)115); // s
            password.AppendChar((char)119); // w
            password.AppendChar((char)111); // o
            password.AppendChar((char)114); // r
            password.AppendChar((char)100); // d
            password.AppendChar((char)49); // 1

            return password;
        }
    }
}

Note that we store the password using the SecureString class. This makes it harder to read the string from the memory while the program is running in the event that the host machine is hacked. Also, as I’m super-paranoid, I write to the SecureString using ASCII values converted to chars to prevent any readable values from appearing in string tables in the compiled code.

When using the Encryptor class you also have the option to add “salt”. If no salt is supplied a default based on the length of the password is used. Salt is effectively an additional piece of information you must supply in order to decrypt information, which becomes part of the decryption password. It is recommended that you use salt wherever possible as it makes it much harder for a hacker to decrypt sets of data using brute-force attacks.

Happy encrypting!

5 thoughts on “Basic two-way encryption in .NET

  1. I realize this is old, but there is a huge flaw here:
    // The Secret Key will be generated from the specified
    // password and salt.
    return new PasswordDeriveBytes(_password.ToString(), salt);

    SecureString.ToString() will always just return the type name (i.e. “System.Security.SecureString”), and NOT the string value held within! You must use Marshal.SecureStringToGlobalAllocUnicode() followed by Marshal.PtrToStringUni() to actually retrieve the contents of the SecureString.

    • Great catch. Thanks jtradke! I used to implement this with a standard string class until one of our code checking tools pointed out that this is bad practice due to memory being readable by hackers. I switched implementations without fully testing. Apologies all!

    • I’ve resolved that issue in the code. Let me know what you think. I’ve also added the overloads needed for users to be able to supply their own salt, which I think was a necessary addition. Thanks again for pointing out my error. 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *