RSA Encryption of large data in C#

19,069

Solution 1

This is not how RSA encryption should be done.

RSA is all about math. What you encrypt is a number so it has to be of finite length and matching the RSA keypair length you're using. Further length limitations are imposed by the padding used (either PKCS#1 or OAEP).

If you want to encrypt large data with RSA you need to do it indirectly - i.e. use a symmetric key to encrypt the large data and encrypt this key using the RSA public key.

You can read about implementing this on my blog.

Solution 2

Okay, I've finally come up with a solution to the problem I stated in my original post. This is something I haven't thoroughly tested or anything, but something I figured out from a little trial and error process.

Here is the current code I have:

    public static string Encrypt(string dataToEncrypt, RSAParameters publicKeyInfo)
    {
        //// Our bytearray to hold all of our data after the encryption
        byte[] encryptedBytes = new byte[0];
        using (var RSA = new RSACryptoServiceProvider())
        {
            try
            {
                //Create a new instance of RSACryptoServiceProvider.
                UTF8Encoding encoder = new UTF8Encoding();

                byte[] encryptThis = encoder.GetBytes(dataToEncrypt);

                //// Importing the public key
                RSA.ImportParameters(publicKeyInfo);

                int blockSize = (RSA.KeySize / 8) - 32;

                //// buffer to write byte sequence of the given block_size
                byte[] buffer = new byte[blockSize];

                byte[] encryptedBuffer = new byte[blockSize];

                //// Initializing our encryptedBytes array to a suitable size, depending on the size of data to be encrypted
                encryptedBytes = new byte[encryptThis.Length + blockSize - (encryptThis.Length % blockSize) + 32];

                for (int i = 0; i < encryptThis.Length; i += blockSize)
                {
                    //// If there is extra info to be parsed, but not enough to fill out a complete bytearray, fit array for last bit of data
                    if (2 * i > encryptThis.Length && ((encryptThis.Length - i) % blockSize != 0))
                    {
                        buffer = new byte[encryptThis.Length - i];
                        blockSize = encryptThis.Length - i;
                    }

                    //// If the amount of bytes we need to decrypt isn't enough to fill out a block, only decrypt part of it
                    if (encryptThis.Length < blockSize)
                    {
                        buffer = new byte[encryptThis.Length];
                        blockSize = encryptThis.Length;
                    }

                    //// encrypt the specified size of data, then add to final array.
                    Buffer.BlockCopy(encryptThis, i, buffer, 0, blockSize);
                    encryptedBuffer = RSA.Encrypt(buffer, false);
                    encryptedBuffer.CopyTo(encryptedBytes, i);
                }
            }
            catch (CryptographicException e)
            {
                Console.Write(e);
            }
            finally
            {
                //// Clear the RSA key container, deleting generated keys.
                RSA.PersistKeyInCsp = false;
            }
        }
        //// Convert the byteArray using Base64 and returns as an encrypted string
        return Convert.ToBase64String(encryptedBytes);
    }

    /// <summary>
    /// Decrypt this message using this key
    /// </summary>
    /// <param name="dataToDecrypt">
    /// The data To decrypt.
    /// </param>
    /// <param name="privateKeyInfo">
    /// The private Key Info.
    /// </param>
    /// <returns>
    /// The decrypted data.
    /// </returns>
    public static string Decrypt(string dataToDecrypt, RSAParameters privateKeyInfo)
    {
        //// The bytearray to hold all of our data after decryption
        byte[] decryptedBytes;

        //Create a new instance of RSACryptoServiceProvider.
        using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
        {
            try
            {
                byte[] bytesToDecrypt = Convert.FromBase64String(dataToDecrypt);

                //// Import the private key info
                RSA.ImportParameters(privateKeyInfo);

                //// No need to subtract padding size when decrypting (OR do I?)
                int blockSize = RSA.KeySize / 8;

                //// buffer to write byte sequence of the given block_size
                byte[] buffer = new byte[blockSize];

                //// buffer containing decrypted information
                byte[] decryptedBuffer = new byte[blockSize];

                //// Initializes our array to make sure it can hold at least the amount needed to decrypt.
                decryptedBytes = new byte[dataToDecrypt.Length];

                for (int i = 0; i < bytesToDecrypt.Length; i += blockSize)
                {
                    if (2 * i > bytesToDecrypt.Length && ((bytesToDecrypt.Length - i) % blockSize != 0))
                    {
                        buffer = new byte[bytesToDecrypt.Length - i];
                        blockSize = bytesToDecrypt.Length - i;
                    }

                    //// If the amount of bytes we need to decrypt isn't enough to fill out a block, only decrypt part of it
                    if (bytesToDecrypt.Length < blockSize)
                    {
                        buffer = new byte[bytesToDecrypt.Length];
                        blockSize = bytesToDecrypt.Length;
                    }

                    Buffer.BlockCopy(bytesToDecrypt, i, buffer, 0, blockSize);
                    decryptedBuffer = RSA.Decrypt(buffer, false);
                    decryptedBuffer.CopyTo(decryptedBytes, i);
                }
            }
            finally
            {
                //// Clear the RSA key container, deleting generated keys.
                RSA.PersistKeyInCsp = false;
            }
        }

        //// We encode each byte with UTF8 and then write to a string while trimming off the extra empty data created by the overhead.
        var encoder = new UTF8Encoding();
        return encoder.GetString(decryptedBytes).TrimEnd(new[] { '\0' });

    }

As I said, I have not tested it much, other than sizes below, at and above the block-size, but it seems to be doing what it should. I am still a novice, so I would really like for you to scrutinize my code :)

Share:
19,069
Simon Langhoff
Author by

Simon Langhoff

Updated on July 19, 2022

Comments

  • Simon Langhoff
    Simon Langhoff almost 2 years

    This is my first post, so hope I haven't missed anything important. I'm doing a project in C# where I need to use public/private key encryption to encrypt a message and then send it over an SSL connection.

    I chose to use the RSACryptoService, as according to the documentation, that was the only asymmetric encryption scheme used for encrypting data. The problem is that I am having a lot of problems with this. (I wanted to do symmetric encryption, but that is not what my teacher want me to do, and according to him it should be easy to just determine a block size and then it should do all the work for you.) Well, so far no luck and I've tried some different approaches, but now I'm back to basics and trying again, this is my current code:

        public string[] GenerateKeysToStrings(string uniqueIdentifier)
        {
            string[] keys;
            using (var rsa = new RSACryptoServiceProvider(4096))
            {
                try
                {
                    string privateKey = rsa.ToXmlString(true);
                    string publicKey = rsa.ToXmlString(false);
    
                    this.pki.StoreKey(publicKey, uniqueIdentifier);
    
                    keys = new string[2];
                    keys[0] = privateKey;
                    keys[1] = publicKey;
                }
                finally
                {
                    //// Clear the RSA key container, deleting generated keys.
                    rsa.PersistKeyInCsp = false;
                }
            }
            return keys;
        }
    

    As you can see, I generate the keys and I mimmick a PKI by sending the public key to a simple class that stores it, and then the private key is written to a file (Notice that I also have another method that does the same but stores it to an array instead, just because I wanted to test and simplify things as I get No such key exceptions and sometimes cryptographic exceptions when I do it the way shown in the example, so I wanted to simplify it by simply storing the rsa.ToXmlString string, as a string in an array, but no luck.)

    Now I have an encrypt and decrypt method as follows:

        public string Encrypt(string keyString, string message)
        {
            string encryptedMessage;
            using (var rsa = new RSACryptoServiceProvider())
            {
                try
                {
                    //// Load the key from the specified path
                    var encryptKey = new XmlDocument();
                    encryptKey.Load(@"C:\Test\PrivateKeyInfo.xml");
                    rsa.FromXmlString(encryptKey.OuterXml);
    
    
                    //// Conver the string message to a byte array for encryption
                    //// var encoder = new UTF8Encoding();
                    ASCIIEncoding byteConverter = new ASCIIEncoding();
                    byte[] dataToEncrypt = byteConverter.GetBytes(message);
    
                    byte[] encryptedData = rsa.Encrypt(dataToEncrypt, false);
    
                    //// Convert the byte array back to a string message
                    encryptedMessage = byteConverter.GetString(encryptedData);
                }
                finally
                {
                    //// Clear the RSA key container, deleting generated keys.
                    rsa.PersistKeyInCsp = false;
                }
            }
            return encryptedMessage;
        }
    

    Decryption:

        public string Decrypt(string keyString, string message)
        {
            string decryptedText;
            using (var rsa = new RSACryptoServiceProvider())
            {
                try
                {
                    //// Loads the keyinfo into the rsa parameters from the keyfile
                    /*
                    var privateKey = new XmlDocument();
                    privateKey.Load(keyString);
                     */
                    rsa.FromXmlString(keyString);
    
                    //// Convert the text from string to byte array for decryption
                    ASCIIEncoding byteConverter = new ASCIIEncoding();
                    var encryptedBytes = byteConverter.GetBytes(message);
    
                    //// Create an aux array to store all the encrypted bytes
                    byte[] decryptedBytes = rsa.Decrypt(encryptedBytes, false);
    
                    decryptedText = byteConverter.GetString(decryptedBytes);
                }
                finally
                {
                    //// Clear the RSA key container, deleting generated keys.
                    rsa.PersistKeyInCsp = false;
                }
            }
            return decryptedText;
        }
    

    I know that this is a wall of text, but I hope you can help me out because I've been banging my head against the wall for so long now it's not funny :)

    The problem is, how do I go about encrypting messages with RSA (or any other public/private key encryption)

    Here is the Test client:

        public static void Main(string[] args)
        {
            PublicKeyInfrastructure pki = new PublicKeyInfrastructure();
            Cryptograph crypto = new Cryptograph();
            string[] keys = crypto.GenerateKeysToStrings("[email protected]");
    
    
            string plainText = "Hello play with me, please";
            string publicKey = crypto.GetPublicKey("[email protected]");
    
            string encryptedText = crypto.Encrypt(keys[0], plainText);
    
    
            string decryptedText = crypto.Decrypt(keys[1], encryptedText);
    
        }
    

    As I mentioned, the string arrays are there because I wanted to eliminate bad parsing error from XML documents...

    When I run the test client, if I use the private key to encrypt and public key to decrypt, I get a "Key does not exist exception" and if I do it the other way around, I get a bad data exception.

    Please help me out guys, if you know of any good guide, or can tell me how to somewhat straightfoward implement public/private key encryption on string messages, please help me out.

    I appreciate any help.

  • Simon Langhoff
    Simon Langhoff over 12 years
    I talked with my professor and at that time my point of view was that it would be better to encrypt a key, exchange the key and then use it, as base for a symmetric algorithm, like rijndael to encrypt/decrypt the message, however, he did not want us to use symmetric encryption so now I'm in a position where that is sorta off-limit for now. Performance wise, how much more time would it take to encrypt messages, 501 bytes a time (using 4096bit RSA keys) when we talked about HTTP messages containing username and password? coding block by block or not, I still have issues actually using RSA :(
  • poupou
    poupou over 12 years
    You can loop the RSA encryption/decryption to stay under the size limit (padding included) and concatenate/split the result. How much time ? that will depend on your CPU, is it an old smartphone and a new 8 core server ? ;-) but a lot longer than a correct alternative.
  • Simon Langhoff
    Simon Langhoff over 12 years
    I wish I could time it, but right now I cannot even get this to work for any data. I took a simplified version and used that, however, I still get "Key does not exist" error and all I really did was to refactor the design, from a single main method to separate methods. Maybe I am using the constructor right? The part where I import the key settings? I know that a block-cipher is 1000 times slower than a symmetric algorithm, but right now I just want it to work, no matter if I have to hardcode it so that I chop each block of data into pieces of (keysize/8 - 11 (for padding)) bytes.
  • poupou
    poupou over 12 years
    Review your code. Your Encrypt method does not use the keyString parameter and loads an XML file (with the key?) but does nothing with its content. Looks like you're encrypting with the automatically generated key (which defaults to 1024 bits) so this won't match for decryption.
  • Simon Langhoff
    Simon Langhoff over 12 years
    I think I've found out the reason why I get the key doesn't exist error, and this simply happens every time I use the private key to encrypt data. I thought it didn't matter what key you used, and please, please do correct me if I'm wrong on this, but I am guessing that since private key encryption is used to validify and not keep something secret, then the encrypt and decrypt method of the RSA CryptoService provider doesn't allow you to use the private key, instead that should be used solely for signing/verification. But that doesn't explain why I get the bad data exception with public key?
  • poupou
    poupou over 12 years
    Private key does matter - Time to read your schoolbook again ;-)
  • Simon Langhoff
    Simon Langhoff over 12 years
    Ah yes, @poupou, I'm afraid that was just a bad error on my side, I have been experimenting with loading the keys in so many different ways that it was silly, so I've updated the original post now, with the extra line that does so that I load from the xml file, other times I simply saved the key as a string and parsed it as a parameter, this would allow me to parse the string directly as it was exported, so I wouldn't eliminate any doubt of any XML parse errors or the like. sorry about the confusion.
  • Simon Langhoff
    Simon Langhoff over 12 years
    Yes I am terrible sorry about that, but I must have forgot to put it in there, but I assure you that I have made sure to load the key. I have updated the OP now. I have just tried loading the data so many times, that I must have got some of the versions mixed up and didn't see it :) My problem still persists though. The problem I am having now, is that when I use my private key to encrypt, and the public to decrypt, I get a keyDoesn'tExist exception. If I do it the other way around however, I get a "bad data" exception.
  • Simon Langhoff
    Simon Langhoff over 12 years
  • RBT
    RBT over 2 years
    Blog link is dead.