Using RSA to encrypt large data files in C#

Introduction

A utility in C# to use public/private key encryption of data inside large text files, before sending them over a secure connection such as SSL. As I soon found out when playing around with this technique, RSA as a means of assymetric encryption has a drawback, and that is in the size of the text you are encrypting – anything other than a piddling amount of text resulting in a ‘Bad length’ type of Exception. This is because asymmetric encryption is designed only for encrypting data smaller than it’s key size.

Overcoming the limitation of RSA

In practice RSA is used to exchange a private secret key between communicating end users that is then used to symmetrically encrypt/decrypt the large data. In other words, in order to successfully maintain asymmetry in encrypting arbitrarily large data, we combine both approaches:

Encryption:

i. Generate a random key of the length required for symmetrical encryption technique such as AES/Rijndael or similar.

ii. Encrypt your data using AES/Rijndael using that random key generated in part i.

iii. Use RSA encryption to asymmetrically encrypt the random key generated in part i.

Publish (eg write to a file) the outputs from parts ii. and iii.: the AES-encrypted data and the RSA-encrypted random key.

Decryption:

i. Decrypt the AES random key using your private RSA key.

In this example, the private RSA key is stored as an XML file:

<?xml version="1.0" encoding="utf-8" ?>
<RSAKeyValue>
  <Modulus>
    mNn/rFCaSoWN1QwgFhTJH8/ZESQWg1Od+5orjHF5umzy6iherkA6xaS66KecFeWGl/raeCKVxDGx25PtLWNI+oJLQ2GOlwM0I4mROqItS9kMhTH2M8uO8qtPKGHgGCU6F9x+ECa/0Gz6pmCVQwi+YVBO3evbsOkDIWz6VXVyzMM=
  </Modulus>
  <Exponent>
    AQAB
  </Exponent>
  <P>
    zctkkvfL3iKaFGG6pUdwyz5wFSUs3foQ86mN1CqQa3xo1SJmaep6WoOE2Hl3HptiY85NM2wKxPKtp0mkeQVsaQ==
  </P>
  <Q>
    viQg34B5PGp9xIGKOdOiV7VRsQugXNW+ZfjBfYBsE8obiQtce53QnnClgch6KM/kv5s3bxKgh7LV07DFM2J6Sw==
  </Q>
  <DP>
    K9YPbl7qRj8Iox7OKza0iBacuWRZ0k7aHY0YcQFAEiVGD2BsgFM6DN3HBnWZMiPXKXtgZnu1L46h/uho6H6HQQ==
  </DP>
  <DQ>
    NK8wCJ3BefML3BoEodc5IVJVS1gsW+zBr+GIQ20FBUq37HYgbwQgXPZbdaWF668G8+xfJMCliFQOGXTef0lnFw==
  </DQ>
  <InverseQ>
    MlFTqSgrmdBJo+ka1V8EQGedonzzvchKdUPiORdr3NLXD5xeDzk+8N8U9PkIJgyPE/rtFTSwIQAZla03duCF/A==
  </InverseQ>
  <D>
    kvcAOokhYMe60H6hFzoTC5BIEJAXSVwLiY/5kUbGGPaKNZRtPLOrDr/NqscFb5RJ7jUW++2c/JAfh5VatYpB7obJPPz2A/ljK/1LtgJCsd58PuGPYoeIEAiG3goUViog4c7QnQc4wAsH9RYL5I6yPXevaUL9uthWtveo7udVoFE=
  </D>
</RSAKeyValue>

ii. Decrypt the original data using the decrypted AES random key

Encryption / decryption using asymmetric algorithm RSA

The following class is for encrypting, decrypting smaller amounts of data with public and private keys using the asymmetric algorithm RSA and is heavily influenced by the example code given at this site.

// <summary>
// 1. From http://www.csharpdeveloping.net/Snippet/how_to_encrypt_decrypt_using_asymmetric_algorithm_rsa
// An asymmetric algorithm to publically encrypt and privately decrypt text data.
// </summary>

namespace Crypto
{
    using System;
    using System.Security.Cryptography;
    using System.Text;

    public static class RSA
    {
        /// <summary>
        /// The padding scheme often used together with RSA encryption.
        /// </summary>
        private static bool _optimalAsymmetricEncryptionPadding = false;

        /// <summary>
        /// Converts the RSA-encrypted text into a string
        /// </summary>
        /// <param name="text">The plain text input</param>
        /// <param name="publicKeyXml">The RSA public key in XML format</param>
        /// <param name="keySize">The RSA key length</param>
        /// <returns>The the RSA-encrypted text</returns>
        public static string Encrypt(string text, string publicKeyXml, int keySize)
        {
            var encrypted = EncryptByteArray(Encoding.UTF8.GetBytes(text), publicKeyXml, keySize);

            return Convert.ToBase64String(encrypted);
        }

        /// <summary>
        /// Gets and validates the RSA-encrypted text as a byte array
        /// </summary>
        /// <param name="data">The plain text in byte array format</param>
        /// <param name="publicKeyXml">The RSA public key in XML format</param>
        /// <param name="keySize">The RSA key length</param>
        /// <returns>The the RSA-encrypted byte array</returns>
        private static byte[] EncryptByteArray(byte[] data, string publicKeyXml, int keySize)
        {
            if (data == null || data.Length == 0)
            {
                throw new ArgumentException("Data are empty", "data");
            }

            int maxLength = GetMaxDataLength(keySize);

            if (data.Length > maxLength)
            {
                throw new ArgumentException(String.Format("Maximum data length is {0}", maxLength), "data");
            }

            if (!IsKeySizeValid(keySize))
            {
                throw new ArgumentException("Key size is not valid", "keySize");
            }

            if (String.IsNullOrEmpty(publicKeyXml))
            {
                throw new ArgumentException("Key is null or empty", "publicKeyXml");
            }

            using (var provider = new RSACryptoServiceProvider(keySize))
            {
                provider.FromXmlString(publicKeyXml);
                return provider.Encrypt(data, _optimalAsymmetricEncryptionPadding);
            }
        }

        /// <summary>
        /// Converts the RSA-decrypted text into a string
        /// </summary>
        /// <param name="text">The plain text input</param>
        /// <param name="publicKeyXml">The RSA public key in XML format</param>
        /// <param name="keySize">The RSA key length</param>
        /// <returns>The the RSA-decrypted text</returns>
        public static string Decrypt(string text, string publicAndPrivateKeyXml, int keySize)
        {
            var decrypted = DecryptByteArray(Convert.FromBase64String(text), publicAndPrivateKeyXml, keySize);
            return Encoding.UTF8.GetString(decrypted);
        }

        /// <summary>
        /// Gets and validates the RSA-decrypted text as a byte array
        /// </summary>
        /// <param name="data">The plain text in byte array format</param>
        /// <param name="publicKeyXml">The RSA public key in XML format</param>
        /// <param name="keySize">The RSA key length</param>
        /// <returns>The the RSA-decrypted byte array</returns>
        private static byte[] DecryptByteArray(byte[] data, string publicAndPrivateKeyXml, int keySize)
        {
            if (data == null || data.Length == 0)
            {
                throw new ArgumentException("Data are empty", "data");
            }

            if (!IsKeySizeValid(keySize))
            {
                throw new ArgumentException("Key size is not valid", "keySize");
            }

            if (String.IsNullOrEmpty(publicAndPrivateKeyXml))
            {
                throw new ArgumentException("Key is null or empty", "publicAndPrivateKeyXml");
            }

            using (var provider = new RSACryptoServiceProvider(keySize))
            {
                provider.FromXmlString(publicAndPrivateKeyXml);
                return provider.Decrypt(data, _optimalAsymmetricEncryptionPadding);
            }
        }

        /// <summary>
        /// Gets the maximum data length for a given key
        /// </summary>       
        /// <param name="keySize">The RSA key length</param>
        /// <returns>The maximum allowable data length</returns>
        public static int GetMaxDataLength(int keySize)
        {
            if (_optimalAsymmetricEncryptionPadding)
            {
                return ((keySize - 384) / 8) + 7;
            }
            return ((keySize - 384) / 8) + 37;
        }

        /// <summary>
        /// Checks if the given key size if valid
        /// </summary>       
        /// <param name="keySize">The RSA key length</param>
        /// <returns>True if valid; false otherwise</returns>
        public static bool IsKeySizeValid(int keySize)
        {
            return keySize >= 384 &&
                   keySize <= 16384 &&
                   keySize % 8 == 0;
        }
    }
}

Encryption / decryption using symmetric algorithm AES256

For symmetric encryption of the data as described previously, I use an implementation of Rijndael’s algorithm. An excellent implementation that I have observed to work for my own purposes can be found here, that I have heavily reproduced in the following code:

// <summary>A symmetric key algorithm used to encrypt/decrypt text data to AES256 standard.
// using Rijndael's ("Rin-dal") algorithm
// </summary>

namespace Crypto
{
    using System;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;

    public class Rijndael
    {
        // Constants used in our AES256 (Rijndael) Encryption / Decryption
        const string initVector = "@1B2c3D4e5F6g7H8";     // Must be 16 bytes
        const string passPhrase = "Pas5pr@se";            // Any string
        const string saltValue = "s@1tValue";             // Any string
        const string hashAlgorithm = "SHA1";              // Can also be "MD5", "SHA1" is stronger
        const int passwordIterations = 2;                 // Can be any number, usually 1 or 2       
        const int keySize = 256;                          // Allowed values: 192, 128 or 256


        private static string randomKeyText = string.Empty;

        /// <summary>
        /// Convert random key byte array into a ASCII string
        /// </summary>       
        /// <param name="bytes">Random key byte array to be converted.</param>    
        /// <returns>Random key formatted as a string</returns>       
        static string GetString(byte[] bytes)
        {
            char[] chars = new char[bytes.Length / sizeof(char)];
            System.Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length);
            return new string(chars);
        }

        /// <summary>
        /// Convert random key ASCII string into a byte array 
        /// </summary>       
        /// <param name="randomKeyText">Random key string to be converted.</param>    
        /// <returns>Random key formatted as a byte array</returns>       
        static byte[] GetBytes(string randomKeyText)
        {
            byte[] bytes = new byte[randomKeyText.Length * sizeof(char)];
            System.Buffer.BlockCopy(randomKeyText.ToCharArray(), 0, bytes, 0, bytes.Length);
            return bytes;
        }

        /// <summary>
        /// Get the random key string
        /// </summary>            
        /// <returns>Random key formatted as a string</returns>       
        public static string GetRandomKeyText()
        {
            return randomKeyText;
        }

        /// <summary>
        /// Encrypts text using Rijndael symmetric key algorithm and returns base64-encoded result.
        /// </summary>       
        /// <param name="plainText">Plain text data to be encrypted.</param>    
        /// <returns>Encrypted value formatted as a base64-encoded string.</returns>
        public static string Encrypt(string plainText)
        {
            // Convert init vector / salt value ASCII strings into byte arrays          
            // If strings include Unicode characters, use Unicode, UTF7, or UTF8 encoding
            byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
            byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);

            // Convert our plain text into a byte array
            // Assume plain text can contains UTF8 characters
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);

            // Using the specified hash algorithm, create a password from the specified passphrase 
            // and salt value from which we will derive the key
            // Password creation can be done in one or more iterations
            PasswordDeriveBytes password = new PasswordDeriveBytes(
                                                            passPhrase,
                                                            saltValueBytes,
                                                            hashAlgorithm,
                                                            passwordIterations);

            // Use  password to generate random bytes for encryption key. 
            // Specify the size of the key in bytes (instead of bits)
            // Set string equivalent of the random key byte array.
            byte[] keyBytes = password.GetBytes(keySize / 8);
            randomKeyText = GetString(keyBytes);

            // Create uninitialized Rijndael encryption object.
            RijndaelManaged symmetricKey = new RijndaelManaged();

            // Set encryption mode to Cipher Block Chaining            
            symmetricKey.Mode = CipherMode.CBC;

            // Generate encryptor from the key bytes and initialization vector. 
            // Key size will be based on the number of key bytes
            ICryptoTransform encryptor = symmetricKey.CreateEncryptor(
                                                             keyBytes,
                                                             initVectorBytes);

            // Define memory stream to contain encrypted data
            MemoryStream memoryStream = new MemoryStream();

            // Define cryptographic stream (always use Write mode for encryption)
            CryptoStream cryptoStream = new CryptoStream(
                                                    memoryStream,
                                                    encryptor,
                                                    CryptoStreamMode.Write);

            // Start and finish encrypting.
            cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
            cryptoStream.FlushFinalBlock();

            // Convert encrypted data from a memory stream into a byte array
            byte[] cipherTextBytes = memoryStream.ToArray();

            // Close memory and cryptographic streams
            memoryStream.Close();
            cryptoStream.Close();

            // Convert encrypted data into a base64-encoded string and return 
            return Convert.ToBase64String(cipherTextBytes);
        }

        /// <summary>
        /// Decrypts text using Rijndael symmetric key algorithm.
        /// </summary>
        /// <param name="cipherText">Base64-formatted text value.</param>
        /// <param name="keyBytes">The public key in byte array format</param>       
        /// <param name="initVector">Vector required to encrypt 1st block of text data, exactly 16 bytes long</param>      
        /// <returns>Decrypted UTF8-encoded string value</returns>       
        public static string Decrypt(string cipherText, string randomKeyText)
        {
            // Convert strings defining encryption key characteristics into byte
            // arrays. Let us assume that strings only contain ASCII codes.
            // If strings include Unicode characters, use Unicode, UTF7, or UTF8
            // encoding.
            byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);

            // Convert our ciphertext into a byte array.
            byte[] cipherTextBytes = Convert.FromBase64String(cipherText);

            // Create uninitialized Rijndael encryption object.
            RijndaelManaged symmetricKey = new RijndaelManaged();

            // It is reasonable to set encryption mode to Cipher Block Chaining
            // (CBC). Use default options for other symmetric key parameters.
            symmetricKey.Mode = CipherMode.CBC;

            // Generate decryptor from existing key bytes and initialization vector. 
            // Key size will be defined based on the number of the key bytes
            byte[] keyBytes = GetBytes(randomKeyText);
            ICryptoTransform decryptor = symmetricKey.CreateDecryptor(
                                                             keyBytes,
                                                             initVectorBytes);

            // Define memory stream which will be used to hold encrypted data
            MemoryStream memoryStream = new MemoryStream(cipherTextBytes);

            // Define cryptographic stream (always use Read mode for encryption)
            CryptoStream cryptoStream = new CryptoStream(memoryStream,
                                                          decryptor,
                                                          CryptoStreamMode.Read);

            // We don't know what the size of decrypted data will be, so allocate buffer 
            // long enough to hold ciphertext plaintext is never longer than ciphertext
            byte[] plainTextBytes = new byte[cipherTextBytes.Length];

            // Start decrypting.
            int decryptedByteCount = cryptoStream.Read(
                                                    plainTextBytes,
                                                    0,
                                                    plainTextBytes.Length);

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

            // Convert decrypted data into a string. 
            // Let us assume that the original plaintext string was UTF8-encoded.
            string plainText = Encoding.UTF8.GetString(
                                                    plainTextBytes,
                                                    0,
                                                    decryptedByteCount);

            // Return decrypted string  
            return plainText;
        }
    }
}

I took the classes described above, using them to create Encryptor and Decryptor utility classes, containing APIs with which to fully perform the necessary steps needed to encrypt/decrypt large data files, as described above. For presentation reasons I do not describe them here, but they (along with the whole Visual Studio project) are available from the download given below.

As a means of demonstrating how these classes and techniques work, I list the following text outputs given at each of the stages of the encryption and decryption stages in the code:

1. Original text:

"I have of late - but wherefore I know not - lost all my mirth,
forgone all custom of exercises;
and indeed it goes so heavily with my disposition that this goodly frame,
the earth, seems to me a sterile promontory;
this most excellent canopy, the air, look you, this brave o'erhanging firmament,
this majestical roof fretted with golden fire,
why, it appears no other thing to me than a foul and pestilential congregation of vapours.
What a piece of work is a man! how noble in reason! how infinite in faculty!
in form and moving how express and admirable!
in action how like an angel! in apprehension how like a god!
the beauty of the world! the paragon of animals!
And yet to me, what is this quintessence of dust?
man delights not me: no, nor woman neither."

2. Random key used to encrypt the original text:

챼޻ﰾṦ챻챰籶❠⨘뜱湋驴礉

3. AES256-Encrypted original text:

The Encrypt utility class is then used to AES256-encrypt the raw text data:

VbB9lfxlQq1QYqYwG0E9WgHT9yLbY7ZAMtVDf0IFN4nXnyGfICPrLTREDt8icCrF4aX
72Vrm+B8ynOn3lLDBIAraWNyc7+jJXqmyUxfaHbVP0R3vqee0dblkG8sC/PzkLZ21db
yoiXxp9e2UZdeK1lTDjAHAf5rLR3YF/eYBz6avNvkvxWbnWb6AhPsobDAPxfYU8U2RN
Fnm0Az7PFN3GeAEeI2U7/pLnYqwE5mWqsdLIw6fplhuNE3xDGwIsMlVUrgAZZHnvaoD
KLNmLfxCyh3na3vuw6E/0AlUAuzE5zYjXm5u/Nv96eU1cKXhJKBSb3zMBUByy0VAMdE
X+KD14NBpr2PEvUHeiUv6EOcuc1YZwgaDiweFP22dkQMuqPFQrdJsEutVzlhSBtLvcf
JvSuOLlPSn0BsR5/SSDoESME84cpUDRfJDfUo2nFYxKKsEAJQy/cmet69IpIhOJvaZr
76UIAjrF4kArYlhMgDY92k+PZqbABL4yticTtbJSWMUOUSRIZqtAH+BlmShJordTNq2
ChfM+IdV6M+Zd7tW4uOdbl/YZ5GLhf39ZSu2csSVr0G1o/rmrRMsxDIuxMcfpkp/gxO
ou26mLUuscakFoceQBNToWVGDRYzndVADGFwbkAdPIGi4taFmM/JECEHmrWnRXd7d7f
PlmDHhdwxS4MVpqW3MNE5c8kNrUU6cT08evYJnNPUBcJEfL3Y4J5yS/7tmlmLyVvy1o
7neCB6r4Dx+lGCjjqfma4XaUGHPPRYy7mKl+9MdXvwv+IaZBfn+cTPv2659408ol+WI
s7f2HoryhRXE3IpGuCAnFMtdEQnSiTNfPgg0Hd+4/NqiJJOQ9ueBsGrwNoplPJZDkN8
bWhnLQSLXEmgWpGSVZH32Rc5ld7YOA/onlXW1d8WlaXyPCXvh0m+5e4IU4DAVo/7+dQ
Junkpk390+UknGcKqIUA+duoIFjtPWZgVN+Kxq7Oi5TB8ffUAE9Rp1yXI9qv4uMbWkG
R2lOF6OzLAcmePXsFCIoUNAzRzuJVY7gNvWgxI+kN+S7fFkguW5Nl26O7bpnww=

4. RSA-encrypted random key:

And the Encrypt facility is also used to RSA-encrypt the random key used in the AES256-enryption of the original text:

euledXShX+rPYC2uTKlbFFK9wY1yHzIkRSiW6yb7PJg2EL08omo5KucuuaYMiUhcxI/
BCiCQq3RblWXISIQkjJ/fomj42o4BUoNud9bbJ7iPCCn+1xEFuSpIonXwjg5iQJpV95
nhh+ca7ycd93o1gFjOypQUD59A8ULxcjJbNcY=

5. RSA-decrypted random key:

At the client side, or at whichever entity is allowed to hold the private key, the Decryptor utility class is used to RSA-decrypt the random key, giving us the original random key we started with:

챼޻ﰾṦ챻챰籶❠⨘뜱湋驴礉

6. AES256-decrypted original text:

Finally, now that we have the original random key, we use it to AES256-decrypt the original raw text. Even though we use a symmetric algorithm here, we preserve the asymmetry as this AES256-decrption is not possible without first having RSA-decrypted the original random key.

Decrypted text:  "I have of late - but wherefore I know not - lost all my mirth,
forgone all custom of exercises;
and indeed it goes so heavily with my disposition that this goodly frame,
the earth, seems to me a sterile promontory;
this most excellent canopy, the air, look you, this brave o'erhanging firmament,
this majestical roof fretted with golden fire,
why, it appears no other thing to me than a foul and pestilential congregation of vapours.
What a piece of work is a man! how noble in reason! how infinite in faculty!
in form and moving how express and admirable!
in action how like an angel! in apprehension how like a god!
the beauty of the world! the paragon of animals!
And yet to me, what is this quintessence of dust?
man delights not me: no, nor woman neither."

Purchase the Visual Studio 2010 C# project demonstrating these concepts from here, a very simple to use console application:

Visual Studio 2010 console application

Comments and feedback welcome.