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?
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.