Using an RSA Public Key to decrypt a string that was encrypted using RSA Private Key

64,718

Solution 1

Having looked at some of the information on RSA encryption modes, it would appear that PKCS#1 v1.5 (which you're using, because you're calling Decrypt(..., false))

"...can operate on messages of length up to k - 11 octets (k is the octet length of the RSA modulus)"

(RFC 3447, emphasis mine).

Based on the error message, which indicates that your key is 128 bytes, that means that you can't perform RSA (en|de)cryption using PKCS#1 v1.5 on a message with more than 128 - 11 = 117 bytes.

Instead of encrypting your message directly using RSA, you should be using a symmetric algorithm to encrypt the body of the message, and encrypt only the symmetric encryption key using RSA. Only if your message is reasonably short (i.e. below 117 bytes for your key size) should you consider encrypting the message directly with RSA.

I have added the following, assuming that your input is Base64 encoded as you indicate in your comment below:

public string DecryptUsingPublic(string dataEncryptedBase64, string publicKey)
    {
        if (dataEncryptedBase64 == null) throw new ArgumentNullException("dataEncryptedBase64");
        if (publicKey == null) throw new ArgumentNullException("publicKey");
        try
        {
            RSAParameters _publicKey = LoadRsaPublicKey(publicKey, false);
            RSACryptoServiceProvider rsa = InitRSAProvider(_publicKey);

            byte[] bytes = Convert.FromBase64String(dataEncryptedBase64);
            byte[] decryptedBytes = rsa.Decrypt(bytes, false);

            // I assume here that the decrypted data is intended to be a
            // human-readable string, and that it was UTF8 encoded.
            return Encoding.UTF8.GetString(decryptedBytes);
        }
        catch
        {
            return null;
        }
    }

Solution 2

RSA is built into .NET: System.Security.Cryptography.RSA.

Encrypting using the public key and decrypting with the private key is one of the most common things people do with asymmetric algorithms, it allows anybody to send you something securely.

If you do it the other way: encrypt using the private key, and decrypt with the public key then it proves the message was sent by the holder of the private key. But because anyone presumably can get hold of the public key, people don't tend to encrypt the whole message, they instead just sign a hash of the data using the private key. Hence RSACryptoServiceProvider has Sign__ and Verify__ methods to do just that.

Still, there are Encrypt/Decrypt methods if your partner insists.

Saying that, I've found the Microsoft crypto classes a bit tricky to deal with and lacking in certain areas and much prefer the Bouncy Castle libraries.

Solution 3

RSA is not meant to encrypt arbitrary data, even less arbitrary data length (like @Iridium already told you). The limit depends on the padding used and using the padding is very important (enough that MS won't let you call EncryptValue and DecryptValue directly).

The right way to do this is to encrypt your string using a symmetric cipher (like AES) then encrypt the secret key using the RSA public key.

The other party will be able to decrypt the secret (AES) key using the RSA private key. Then using the key decrypt your string.

I have an old (but still up to date) blog entry on the subject that includes source code (C#).

Share:
64,718
Ruairi O'Brien
Author by

Ruairi O'Brien

@ruarfff

Updated on July 11, 2022

Comments

  • Ruairi O'Brien
    Ruairi O'Brien almost 2 years

    I know the main answer I am likely to get is why the hell would you want to do that?!

    Unfortunately despite my protests I have to do it, even though I know it makes little sense.

    I have functions written in .Net to decrypt using a private key, encrypt using a public key. I also RSA sign and verify and have a reasonable understanding of how this all work I think.

    I am now being sent a value that is RSA encrypted using a private key which I am supposed to derive a usable value by decrypting using the public key.

    I can't seem to figure out how to do this. Am I being an idiot? Is this a normal thing to do?

    I am told by the person sending me the value that this is no problem in PHP. I don't know and haven't used PHP yet. I can't find a library to do it in any of the main languages I know i.e. C++, Java, C#. The server I am working on uses .Net.

    I am hoping someone might be able help me.

    It would be great if there is some kind of reasonable solution besides begging them to change what they are doing.

    This is my method (updated from my previous bad one as pointed out by Iridium) but when I try to decrypt the value I get an exception

    "Error occurred while decoding OAEP padding."

    If I use rsa.Decrypt(bytes, false) I get a bad key exception.

    public static string DecryptUsingPublic(string dataEncrypted, string publicKey)
        {
            if (dataEncrypted == null) throw new ArgumentNullException("dataEncrypted");
            if (publicKey == null) throw new ArgumentNullException("publicKey");
            try
            {
                RSAParameters _publicKey = LoadRsaPublicKey(publicKey, false);
                RSACryptoServiceProvider rsa = InitRSAProvider(_publicKey);
    
                byte[] bytes = Convert.FromBase64String(dataEncrypted);
                byte[] decryptedBytes = rsa.Decrypt(bytes, true);
    
                ArrayList arrayList = new ArrayList();
                arrayList.AddRange(decryptedBytes);
    
               return Encoding.UTF8.GetString(decryptedBytes);
            }
            catch
            {
                return null;
            }
        }
    
        private static RSAParameters LoadRsaPublicKey(String publicKeyFilePath, Boolean isFile)
        {
            RSAParameters RSAKeyInfo = new RSAParameters();
            byte[] pubkey = ReadFileKey(publicKeyFilePath, "PUBLIC KEY", isFile);
            byte[] SeqOID = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
            byte[] seq = new byte[15];
            // ---------  Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob  ------
            MemoryStream mem = new MemoryStream(pubkey);
            BinaryReader binr = new BinaryReader(mem);    //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;
    
            try
            {
    
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();   //advance 2 bytes
                else
                    return RSAKeyInfo;
    
                seq = binr.ReadBytes(15);       //read the Sequence OID
                if (!CompareBytearrays(seq, SeqOID))    //make sure Sequence for OID is correct
                    return RSAKeyInfo;
    
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8203)
                    binr.ReadInt16();   //advance 2 bytes
                else
                    return RSAKeyInfo;
    
                bt = binr.ReadByte();
                if (bt != 0x00)     //expect null byte next
                    return RSAKeyInfo;
    
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();   //advance 2 bytes
                else
                    return RSAKeyInfo;
    
                twobytes = binr.ReadUInt16();
                byte lowbyte = 0x00;
                byte highbyte = 0x00;
    
                if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81)
                    lowbyte = binr.ReadByte();  // read next bytes which is bytes in modulus
                else if (twobytes == 0x8202)
                {
                    highbyte = binr.ReadByte(); //advance 2 bytes
                    lowbyte = binr.ReadByte();
                }
                else
                    return RSAKeyInfo;
                byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };   //reverse byte order since asn.1 key uses big endian order
                int modsize = BitConverter.ToInt32(modint, 0);
    
                byte firstbyte = binr.ReadByte();
                binr.BaseStream.Seek(-1, SeekOrigin.Current);
    
                if (firstbyte == 0x00)
                {   //if first byte (highest order) of modulus is zero, don't include it
                    binr.ReadByte();    //skip this null byte
                    modsize -= 1;   //reduce modulus buffer size by 1
                }
    
                byte[] modulus = binr.ReadBytes(modsize);   //read the modulus bytes
    
                if (binr.ReadByte() != 0x02)            //expect an Integer for the exponent data
                    return RSAKeyInfo;
                int expbytes = (int)binr.ReadByte();        // should only need one byte for actual exponent data (for all useful values)
                byte[] exponent = binr.ReadBytes(expbytes);
    
    
                RSAKeyInfo.Modulus = modulus;
                RSAKeyInfo.Exponent = exponent;
    
                return RSAKeyInfo;
            }
            catch (Exception)
            {
                return RSAKeyInfo;
            }
    
            finally { binr.Close(); }
            //return RSAparams;
    
        }
    
     private static RSACryptoServiceProvider InitRSAProvider(RSAParameters rsaParam)
        {
            //
            // Initailize the CSP
            //   Supresses creation of a new key
            //
            CspParameters csp = new CspParameters();
            //csp.KeyContainerName = "RSA Test (OK to Delete)";
    
            const int PROV_RSA_FULL = 1;
            csp.ProviderType = PROV_RSA_FULL;
    
            const int AT_KEYEXCHANGE = 1;
            // const int AT_SIGNATURE = 2;
            csp.KeyNumber = AT_KEYEXCHANGE;
            //
            // Initialize the Provider
            //
            RSACryptoServiceProvider rsa =
              new RSACryptoServiceProvider(csp);
            rsa.PersistKeyInCsp = false;
    
            //
            // The moment of truth...
            //
            rsa.ImportParameters(rsaParam);
            return rsa;
        }
    
        private static int GetIntegerSize(BinaryReader binr)
        {
            byte bt = 0;
            byte lowbyte = 0x00;
            byte highbyte = 0x00;
            int count = 0;
            bt = binr.ReadByte();
            if (bt != 0x02)     //expect integer
                return 0;
            bt = binr.ReadByte();
    
            if (bt == 0x81)
                count = binr.ReadByte();    // data size in next byte
            else
                if (bt == 0x82)
                {
                    highbyte = binr.ReadByte(); // data size in next 2 bytes
                    lowbyte = binr.ReadByte();
                    byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                    count = BitConverter.ToInt32(modint, 0);
                }
                else
                {
                    count = bt;     // we already have the data size
                }
    
            while (binr.ReadByte() == 0x00)
            {   //remove high order zeros in data
                count -= 1;
            }
            binr.BaseStream.Seek(-1, SeekOrigin.Current);       //last ReadByte wasn't a removed zero, so back up a byte
            return count;
        }
    
        private static bool CompareBytearrays(byte[] a, byte[] b)
        {
            if (a.Length != b.Length)
                return false;
            int i = 0;
            foreach (byte c in a)
            {
                if (c != b[i])
                    return false;
                i++;
            }
            return true;
        }
    

    The two methods above InitRSAProvider and LoadRsaPublicKey were gotten out of tutorials to allow PEM keys as Strings to be used with .Net.

  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Sorry I am an idiot. I got my question completely backwards. I have edited it to show what I actually meant.
  • Duncan Smart
    Duncan Smart over 12 years
    Thought so! Updated the answer.
  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Thanks very much for your answer. I can't seem to get the decrypt method to work in this case. I have updated my question to show where it is going wrong. I am probably doing something silly and will mark this as the answer as soon as I figure out what it is.
  • Duncan Smart
    Duncan Smart over 12 years
    Added a suggestion to try the Bouncy Castle libraries - they tend to inter-operate with the non-MS world a bit better.
  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Thanks for your response. I have been reading RFC 3447 all day trying to find a solution. The main problem is that I have no control over how the value is encrypted. Basically I am given a value i.e. value = BASE64[RSA_PRIVATE_ENCRYPT(some_text)] It has been left to me to figure out how to decrypt and use this value. I have tried lots of things but no joy. I have asked for a working example for decryption from the person sending me the value. If I get it I will post it here.
  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Thanks for your response. I completely agree with you. The problem is I am not encrypting the value. I have a private key and a public key. I am being sent a value that has been encrypted using the private key and now I have to figure out how to decrypt it. It may not make sense and there may not be a simple solution but I am not very experienced with this so i appreciate all comments and suggestions.
  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Will look in to Bouncy Castle. Thanks for your suggestion.
  • poupou
    poupou over 12 years
    If you stuck on the decrypting end then you have no choice (safe or not) than doing the reverse operations to get back the data. Hopefully it will not require you to use DecryptValue (it should not if the other, encrypting, party use .NET) but even then there are solutions for that :) Let us know!
  • Iridium
    Iridium over 12 years
    That isn't what your code snippet suggests, as you have no Base64 decoding. (You shouldn't be using Encoding.ASCII for this purpose either, as it's only 7-bit). If your input is Base64, you should be using Convert.FromBase64String(dataEncrypted) to get a byte array for use with the RSA functions.
  • Iridium
    Iridium over 12 years
    Whilst my original answer still applies, I have added some code to my answer that may correct your problem, based on the assumption that the input is in fact Base64 encoded as you mention in your comment above.
  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Thanks. You are right. I was supposed to be using Base64. It was a stupid mistake made as I was trying to reverse the encrypt method. I have updated my question.
  • Ruairi O'Brien
    Ruairi O'Brien over 12 years
    Finally got it working using your advice and source code from here codeproject.com/KB/security/PrivateEncryption.aspx
  • xmen
    xmen over 9 years
    I dont understand this from your blog It also make sense "security wise" because a 128 bits symmetric key is more secure than a 1024 bits asymmetric keypair (at least for RSA). Could you explain more about how AES is more secure than RSA ?
  • poupou
    poupou over 9 years
    It's not about AES or RSA themselves (too different), it's about the key length used for both (i.e. how hard to break them). IOW it should be easier to break RSA 1024 than AES 128. Suggested read security.stackexchange.com/questions/38015/…