0

I have created xml key with c#:

RSACryptoServiceProvider RSA = new RSACryptoServiceProvider (2048);
string str = RSA.ToXmlString (true);
System.IO.File.WriteAllText (@"c:\path\to\keypair.xml", str);

Then using this online tool, I converted XML key to pem private key. Then using open SSL, I extracted the public key from the private key:

openssl rsa -in ./private.pem -pubout -out public.pem 

Using the public.pem I am encrypting the secret string in Node:

const encryptWithRsa = (secretStringToEncrypt) => {
  const publicKeyPath = path.join(appRoot, `${process.env.PUBLIC_KEY_PATH}`);
  const publicKeyString = fs.readFileSync(publicKeyPath, 'utf8');
  const buffer = Buffer.from(secretStringToEncrypt, 'utf8');
  const encrypted = crypto.publicEncrypt({
    key: publicKeyString,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
    oaepHash: "sha256",
  }, buffer);
  const encryptedString = encrypted.toString('base64');
  console.log("encryptWithRsa -> encryptedString", encryptedString);
  return encryptedString;
};

Then I am trying to decrypt the string in c#:

public string RSADecrypt (string encryptedStringFromNode) {
            string keyPair = System.IO.File.ReadAllText (@"c:\path\to\keypair.xml");
            using var csp = new RSACryptoServiceProvider ();
            ImportKey (csp, keyPair);
            var decrypted = csp.Decrypt (Convert.FromBase64String (encryptedStringFromNode), true);
            return Encoding.UTF8.GetString (
                decrypted
            );
        }

        public void ImportKey (RSA rsa, string xmlKey) {
            var parameters = new RSAParameters ();

            var xmlDoc = new XmlDocument ();
            xmlDoc.LoadXml (xmlKey);

            if (xmlDoc.DocumentElement.Name.Equals ("RSAKeyValue")) {
                foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes) {
                    var value = ToByteArray (node.InnerText);
                    switch (node.Name) {
                        case nameof (parameters.Modulus):
                            parameters.Modulus = value;
                            break;
                        case nameof (parameters.Exponent):
                            parameters.Exponent = value;
                            break;
                        case nameof (parameters.P):
                            parameters.P = value;
                            break;
                        case nameof (parameters.Q):
                            parameters.Q = value;
                            break;
                        case nameof (parameters.DP):
                            parameters.DP = value;
                            break;
                        case nameof (parameters.DQ):
                            parameters.DQ = value;
                            break;
                        case nameof (parameters.InverseQ):
                            parameters.InverseQ = value;
                            break;
                        case nameof (parameters.D):
                            parameters.D = value;
                            break;
                    }
                }
            } else {
                throw new Exception ("");
            }
            rsa.ImportParameters (parameters);
        }

        public byte[] ToByteArray (string input) {
            return string.IsNullOrEmpty (input) ? null : Convert.FromBase64String (input);
        }

But when I try to decrypt the string which is encrypted by Node.js, I get below error:

    Exception has occurred: CLR/Internal.Cryptography.CryptoThrowHelper.WindowsCryptographicException
    An exception of type 'Internal.Cryptography.CryptoThrowHelper.WindowsCryptographicException' occurred in 
System.Security.Cryptography.Csp.dll but was not handled in user code: 'The parameter is incorrect.'

However, if I try to encrypt with the same public key on this online tool and then decrypt with my C# code, it works and I get the original unencrypted string.

Is there any issue with the Padding I am using for the RSA encryption in Node.js?

4
  • 1
    That's a lot of painful XML processing. Why not just use RSA.FromXMLString() in ImportKey? Commented Oct 8, 2020 at 11:35
  • 2
    The RSACryptoServiceProvider supports besides PKCS#1v1.5 only OAEP with SHA1. In the NodeJS code OAEP with SHA256 is used, therefore decryption with the C# code fails. To solve the problem, SHA1 can be used in the NodeJS Code. Alternatively, an RSA implementation that supports SHA256 can be applied in the C# code. Here RSACng would be a possibility, provided this is available in the .NET version you are using. Since both sides also support PKCS#1 v1.5, this is another alternative (see Michael Fehr's answer). Commented Oct 8, 2020 at 15:16
  • Which line of code is causing the exception? Commented Oct 8, 2020 at 21:12
  • @Topaco - using RSA_PKCS1_OAEP_PADDING with sha1 solved the issue. Commented Oct 11, 2020 at 14:40

1 Answer 1

4

First - sorry for being too lazy to analyze your code to find out what is the real reason for failure. Most times it is the padding that does not match between platforms and it is always a good programming technique to define parameters even if they are called "standard" or "default" because they may change in the future.

In my example I'm using the PKCS1 padding for RSA encryption that is available on both platforms.

Second: I wanted to use online compiler for demonstration, so I'm using fixed / stringed public and private RSA keys. Please note that I'm using an *UNSECURE RSA keypair with 512 bit key length (just to shorten the key strings) and yes - the keys are sample keys.

Third: both solutions are for educational purpose only because they do not have any exception handling.

Fourth: both solution provide a "full round" means both do an encryption and a decryption, the C#-program does a second decryption with an encrypted string coming from the Node.JS-Solution.

This is the output of the Node.JS-program (run it here: https://repl.it/@javacrypto/RSAEncryptionNodeJstoC):

enc: QPCE4WtNPLFGjWvwpN/JTITdr2k9IhGrsohuW0yPue4dQ2Cv63i+LlohmjsSUcnaiB/zbeItGDIx3s11ayBIUA==
dec: The quick brown fox jumps over the lazy dog

If you run the C#-version you will get this output (run it here: https://repl.it/@javacrypto/RSADecryptionCfromNodeJs-1)

encryptedData: jnAyBvjvjG/NXc/HUmfrTks3SWgeYz3HEksPvk9D9eH/DF0PK6nZu36wTAeZ/fHr3v8dWYwXtT8nXZgvokJJYg==
decryptedData: The quick brown fox jumps over the lazy dog
original Data: VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
*** enc string from NodeJs ***
decryptedData: The quick brown fox jumps over the lazy dog

Node.JS code:

const crypto = require('crypto')

// SECURITY WARNING: these sample keys are UNSECURE 512 bit long RSA keys
const publicKey = `-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJn4LYaoLyuT+pD9NKI8wOvPrRjMIxGn
HbqIxUrpGsyj162fEOV4836oCZg0N8HFnt4Vivdjt8/7ZgLjeOygNGUCAwEAAQ==
-----END PUBLIC KEY-----`;

const privateKey = `-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAmfgthqgvK5P6kP00
ojzA68+tGMwjEacduojFSukazKPXrZ8Q5XjzfqgJmDQ3wcWe3hWK92O3z/tmAuN4
7KA0ZQIDAQABAkEAmCpGy/rxS08e5iXn26LRQvvm5UfyLKMNZWmAGk2QF8cRGFB7
dds/SI9wGTC9xyOoF4N2kWzYdLx+dYbR9lqwbQIhAPFQkWqNIWZiL+YFRhS6jg/+
5+k0CpXpAPelNUFc4pijAiEAo1bchWA5ddDd59FED37QcrakoTXNoxRspFtsLDKE
p1cCIQC6AXRVQTvBg2WQU/hU+geO5Nk1tFuEefm46atHGqW/KQIgXoGggC9Hr8pU
yo9DIPAP7X+Ny5TU0Vm87m/TK9Ni+2sCIGKjhxD/HYAr8+UPJ4aUIHayn3ZZo5DQ
BcShe496ZtPS
-----END PRIVATE KEY-----`;

function encrypt(toEncrypt, relativeOrAbsolutePathToPublicKey) {
  //const absolutePath = path.resolve(relativeOrAbsolutePathToPublicKey)
  //const publicKey = fs.readFileSync(absolutePath, 'utf8')
  const buffer = Buffer.from(toEncrypt, 'utf8')
  var constants = require("constants");
  const encrypted = crypto.publicEncrypt(
  {"key":publicKey, padding:constants.RSA_PKCS1_PADDING}, buffer)
  return encrypted.toString('base64')
}

function decrypt(toDecrypt, relativeOrAbsolutePathtoPrivateKey) {
  //const absolutePath = path.resolve(relativeOrAbsolutePathtoPrivateKey)
  //const privateKey = fs.readFileSync(absolutePath, 'utf8')
  const buffer = Buffer.from(toDecrypt, 'base64')
  var constants = require("constants");
  const decrypted = crypto.privateDecrypt(
    {
      key: privateKey.toString(),
      passphrase: '',
      padding:constants.RSA_PKCS1_PADDING,
    },
    buffer,
  )
  return decrypted.toString('utf8')
}

const enc = encrypt('The quick brown fox jumps over the lazy dog')
console.log('enc:', enc)
const dec = decrypt(enc)
console.log('dec:', dec)

C# code:

using System;
using System.Security.Cryptography;
using System.Text;
using System.Collections.Generic;

class RSACSPSample {
    static void Main() {

        try {
            ASCIIEncoding ByteConverter = new ASCIIEncoding();
            string dataString = "The quick brown fox jumps over the lazy dog";

            // Create byte arrays to hold original, encrypted, and decrypted data.
            byte[] originalData = ByteConverter.GetBytes(dataString);
            byte[] encryptedData;
            byte[] decryptedData;

      // SECURITY WARNING: these sample keys are UNSECURE 512 bit long RSA keys
            // get private and public key
            var publicKey = "<RSAKeyValue><Modulus>mfgthqgvK5P6kP00ojzA68+tGMwjEacduojFSukazKPXrZ8Q5XjzfqgJmDQ3wcWe3hWK92O3z/tmAuN47KA0ZQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
            var privateKey = "<RSAKeyValue><Modulus>mfgthqgvK5P6kP00ojzA68+tGMwjEacduojFSukazKPXrZ8Q5XjzfqgJmDQ3wcWe3hWK92O3z/tmAuN47KA0ZQ==</Modulus><Exponent>AQAB</Exponent><P>8VCRao0hZmIv5gVGFLqOD/7n6TQKlekA96U1QVzimKM=</P><Q>o1bchWA5ddDd59FED37QcrakoTXNoxRspFtsLDKEp1c=</Q><DP>ugF0VUE7wYNlkFP4VPoHjuTZNbRbhHn5uOmrRxqlvyk=</DP><DQ>XoGggC9Hr8pUyo9DIPAP7X+Ny5TU0Vm87m/TK9Ni+2s=</DQ><InverseQ>YqOHEP8dgCvz5Q8nhpQgdrKfdlmjkNAFxKF7j3pm09I=</InverseQ><D>mCpGy/rxS08e5iXn26LRQvvm5UfyLKMNZWmAGk2QF8cRGFB7dds/SI9wGTC9xyOoF4N2kWzYdLx+dYbR9lqwbQ==</D></RSAKeyValue>";

            RSACryptoServiceProvider RSAalg = new RSACryptoServiceProvider(512); // SECURITY WARNING: 512 is UNSECURE
            RSAalg.PersistKeyInCsp = false;
            // encrypt with xml-public key
      RSAalg.FromXmlString(publicKey);
            encryptedData = RSAalg.Encrypt(originalData, false);
            string encryptedDataBase64 = Convert.ToBase64String(encryptedData);
            Console.WriteLine("encryptedData: " + encryptedDataBase64);

            // decrypt with xml-private key
            RSAalg.FromXmlString(privateKey);
            decryptedData = RSAalg.Decrypt(encryptedData, false);
            Console.WriteLine("decryptedData: " + ByteArrayToString(decryptedData));
            Console.WriteLine("original Data: " + Convert.ToBase64String(originalData));

      Console.WriteLine("*** enc string from NodeJs ***");

      string b64string = "QPCE4WtNPLFGjWvwpN/JTITdr2k9IhGrsohuW0yPue4dQ2Cv63i+LlohmjsSUcnaiB/zbeItGDIx3s11ayBIUA==";
      encryptedData = Convert.FromBase64String(b64string);
      decryptedData = RSAalg.Decrypt(encryptedData, false);
            Console.WriteLine("decryptedData: " + ByteArrayToString(decryptedData));
        }
        catch(ArgumentNullException) {
            Console.WriteLine("error in data en-/decryption");
        }
    }
    
  private static string ByteArrayToString(byte[] arr)
  {
    System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding();
    return enc.GetString(arr);
  }
}
Sign up to request clarification or add additional context in comments.

2 Comments

RSAalg.Decrypt(encryptedData, false); you are setting OEAP padding to false. If you set it to true, it will fail to decrypt nodejs encrypted string.
You got it - the padding makes the difference! As explained in my answer I'm working with PKCS1-padding on Javascript- and on C#-side. If you want other paddings follow the advice given by @Topaco in his comment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.