1

I'm trying to replicate an existing C# application, part of which encrypts some data using a key.

Simple example:

using System;
using System.Security.Cryptography;
using System.Text;

public class Program {
  private static string xmlKey = "<RSAKeyValue><Modulus>{REDACTED MODULUS}</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";

  public static void Main() {
    RSACryptoServiceProvider cipher = new RSACryptoServiceProvider();
    cipher.FromXmlString(xmlKey);

    Console.WriteLine("KeyExchangeAlgorithm: " + cipher.KeyExchangeAlgorithm);

    byte[] input = Encoding.UTF8.GetBytes("test");
    byte[] output = cipher.Encrypt(input, true);
    Console.WriteLine("Output: " + Convert.ToBase64String(output));
  }
}

Which outputs:

KeyExchangeAlgorithm: RSA-PKCS1-KeyEx
Output: {THE ENCRYPTED OUTPUT}

I've replicated this in Java with the following, but while it runs ok, the downstream system can't decrypt the data, so I've done something wrong

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.Cipher;

public class Program {
    // I tried "RSA/ECB/PKCS1Padding" but got "java.security.NoSuchAlgorithmException: RSA/ECB/PKCS1Padding KeyFactory not available at java.base/java.security.KeyFactory.<init>(KeyFactory.java:138)"
    private static final String ALGORITHM = "RSA";

    private static final String MODULUS = "{REDACTED MODULUS}";
    private static final String EXPONENT = "AQAB";

    // Converted from XML using
    // https://superdry.apphb.com/tools/online-rsa-key-converter
    private static final String PEM_KEY = "{REDACTED PEM KEY}";

    public static void main(final String[] args) throws Exception {
        final Cipher cipher = Cipher.getInstance(ALGORITHM);
        final PublicKey key = KeyFactory.getInstance(ALGORITHM).generatePublic(getX509Key());
        cipher.init(Cipher.ENCRYPT_MODE, key);

        System.out.println("Algorithm: " + cipher.getAlgorithm());

        final byte[] input = "test".getBytes(StandardCharsets.UTF_8);
        final byte[] output = cipher.doFinal(input);
        System.out.println("Output: " + Base64.getEncoder().encodeToString(output));
    }

    private static KeySpec getRSAKey() throws Exception {
        return new RSAPublicKeySpec(base64ToInt(MODULUS), base64ToInt(EXPONENT));
    }

    private static BigInteger base64ToInt(final String str) {
        return new BigInteger(1, Base64.getDecoder().decode(str.getBytes()));
    }

    private static KeySpec getX509Key() throws Exception {
        return new X509EncodedKeySpec(Base64.getDecoder().decode(PEM_KEY));
    }
}

Can anyone advise what I've done wrong, please?

7
  • 1
    'the downstream system can't decrypt the data, so I've done something wrong' - we need more info. It fails? How are you trying to decrypt? What is teh error? Commented Mar 1, 2022 at 21:57
  • @ErmiyaEskandary what info do you need? Commented Mar 1, 2022 at 21:57
  • ...I tried "RSA/ECB/PKCS1Padding" but get a "not found" exception... I find that hard to believe, that transformation has been available for every version of Java I'm aware of for the last 20 years. Commented Mar 1, 2022 at 22:53
  • @PresidentJamesK.Polk I tried on two different PCs, both using Java 17. Not really sure what to say! Commented Mar 1, 2022 at 23:42
  • The algorithm string for Cipher.getInstance() is not the same as the algorithm string for KeyFactory.getInstance(). This is why it's critical to include the actual exception stacktrace copied and pasted into the question. Commented Mar 1, 2022 at 23:44

1 Answer 1

3

Since in RSACryptoServiceProvider#Encrypt() a true is passed in the 2nd parameter, OAEP is used as padding and SHA-1 for the OAEP and the MGF1 digest, i.e. the decryption is to be performed in Java e.g. with:

import java.nio.charset.StandardCharsets;
import java.security.spec.MGF1ParameterSpec;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
...
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-1", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
cipher.init(Cipher.ENCRYPT_MODE, key, oaepParameterSpec);
byte[] ciphertext = cipher.doFinal("test".getBytes(StandardCharsets.UTF_8));
System.out.println(Base64.getEncoder().encodeToString(ciphertext));

Note that X509EncodedKeySpec() expects a DER encoded X.509/SPKI key, i.e. PEM_KEY must not contain the PEM encoded key, but only the Base64 encoded body (i.e. without header, without footer and without line breaks).

Note also that OAEP is a probabilistic padding, i.e. the ciphertext is different for each encryption even for the same input data. For this reason, the ciphertexts of both codes will not match, even with identical input data, which is not a malfunction.


A test is possible with the following C# code:

using System;
using System.Security.Cryptography;
using System.Text;
...
string xmlKeyPriv = "<private key in XML format>";
RSACryptoServiceProvider cipherDec = new RSACryptoServiceProvider();
cipherDec.FromXmlString(xmlKeyPriv);

byte[] ciphertext = Convert.FromBase64String("<Base64 encoded ciphertext>");
byte[] decrypted = cipherDec.Decrypt(ciphertext, true);
Console.WriteLine(Encoding.UTF8.GetString(decrypted));

This code decrypts the ciphertext of the C# code as well as the ciphertext of the Java code.


EDIT:
Regarding the key import in the Java code mentioned in your comment: As explained above, PEM_KEY contains only the Base64 encoded body of the PEM key without line breaks. Apart from that, the key import matches your code:

import java.security.KeyFactory;
import java.security.PublicKey;
...
private static String PEM_KEY = "MIIBIjANB...IDAQAB"
...
PublicKey key = KeyFactory.getInstance("RSA").generatePublic(getX509Key());

Regarding the exception NoSuchAlgorithmException: RSA/ECB/PKCS1Padding KeyFactory not available: This is thrown if the padding is specified in addition to the algorithm when the KeyFactory object is created, e.g. if RSA/ECB/PKCS1Padding is passed instead of RSA in getInstance(). In contrast, padding should be specified when instantiating the Cipher object, e.g. RSA/ECB/PKCS1Padding should be passed instead of RSA. If only the algorithm is specified here, a provider-dependent default value would be used for the padding, which should be avoided. Note that PKCS1Padding denotes PKCS#1 v1.5 padding and does not correspond to OAEP used in the C# code.

Sign up to request clarification or add additional context in comments.

3 Comments

thanks, I hadn't seen the nuance of the boolean parameter in the C# example. I see you've referenced key but I can't see how this should be declared? Specifically I'm making the X509EncodedKeySpec, but in the KeyFactory I'm not sure that I'm using the right algorithm - for RSA it's not working for sure.
@Jakg - As described in my first comment, PEM_KEY only needs to contain the Base64 encoded body of the PEM key without line breaks. Apart from that, the key import matches your code, see my modified answer.
@Jakg - the Java code can be run online at jdoodle: jdoodle.com/iembed/v0/nU5 and the generated ciphertext can be decrypted online here at .NET Fiddle using the C# code: dotnetfiddle.net/sPadkb.

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.