努力在 Java 中使用 C# RSA
Struggling to use C# RSA in Java
我正在尝试复制现有的 C# 应用程序,其中的一部分使用密钥加密了一些数据。
简单示例:
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));
}
}
输出:
KeyExchangeAlgorithm: RSA-PKCS1-KeyEx
Output: {THE ENCRYPTED OUTPUT}
我在Java中用下面的方法复制了这个,但是当它运行正常时,下游系统无法解密数据,所以我做错了
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));
}
}
任何人都可以告诉我做错了什么吗?
由于在RSACryptoServiceProvider#Encrypt()
中第二个参数传递了true
,OAEP被用作填充,OAEP和MGF1摘要被SHA-1使用,即解密将在Java 例如有:
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));
请注意,X509EncodedKeySpec()
需要 DER 编码的 X.509/SPKI 密钥,即 PEM_KEY
不能包含 PEM 编码密钥,而只能包含 Base64 编码正文(即没有页眉,没有页脚)并且没有换行符)。
另请注意,OAEP 是一种概率填充,即即使对于相同的输入数据,每次加密的密文也不同。因此,即使输入相同的数据,两个代码的密文也不会匹配,这不是故障。
可以使用以下 C# 代码进行测试:
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));
此代码解密C#代码的密文以及Java代码的密文。
编辑:
关于您评论中提到的 Java 代码中的密钥导入:如上所述,PEM_KEY
仅包含没有换行符的 PEM 密钥的 Base64 编码主体。除此之外,密钥导入与您的代码匹配:
import java.security.KeyFactory;
import java.security.PublicKey;
...
private static String PEM_KEY = "MIIBIjANB...IDAQAB"
...
PublicKey key = KeyFactory.getInstance("RSA").generatePublic(getX509Key());
关于异常 NoSuchAlgorithmException: RSA/ECB/PKCS1Padding KeyFactory 不可用: 如果在 KeyFactory
对象时除了算法之外还指定了填充,则会抛出此异常被创建,例如如果在 getInstance()
中传递 RSA/ECB/PKCS1Padding
而不是 RSA
。相反,在实例化 Cipher
对象时应指定填充,例如应该传递 RSA/ECB/PKCS1Padding
而不是 RSA
。如果此处仅指定算法,则填充将使用 provider-dependent 默认值,应避免这种情况。请注意,PKCS1Padding
表示 PKCS#1 v1.5 填充,并不对应于 C# 代码中使用的 OAEP。
我正在尝试复制现有的 C# 应用程序,其中的一部分使用密钥加密了一些数据。
简单示例:
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));
}
}
输出:
KeyExchangeAlgorithm: RSA-PKCS1-KeyEx
Output: {THE ENCRYPTED OUTPUT}
我在Java中用下面的方法复制了这个,但是当它运行正常时,下游系统无法解密数据,所以我做错了
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));
}
}
任何人都可以告诉我做错了什么吗?
由于在RSACryptoServiceProvider#Encrypt()
中第二个参数传递了true
,OAEP被用作填充,OAEP和MGF1摘要被SHA-1使用,即解密将在Java 例如有:
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));
请注意,X509EncodedKeySpec()
需要 DER 编码的 X.509/SPKI 密钥,即 PEM_KEY
不能包含 PEM 编码密钥,而只能包含 Base64 编码正文(即没有页眉,没有页脚)并且没有换行符)。
另请注意,OAEP 是一种概率填充,即即使对于相同的输入数据,每次加密的密文也不同。因此,即使输入相同的数据,两个代码的密文也不会匹配,这不是故障。
可以使用以下 C# 代码进行测试:
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));
此代码解密C#代码的密文以及Java代码的密文。
编辑:
关于您评论中提到的 Java 代码中的密钥导入:如上所述,PEM_KEY
仅包含没有换行符的 PEM 密钥的 Base64 编码主体。除此之外,密钥导入与您的代码匹配:
import java.security.KeyFactory;
import java.security.PublicKey;
...
private static String PEM_KEY = "MIIBIjANB...IDAQAB"
...
PublicKey key = KeyFactory.getInstance("RSA").generatePublic(getX509Key());
关于异常 NoSuchAlgorithmException: RSA/ECB/PKCS1Padding KeyFactory 不可用: 如果在 KeyFactory
对象时除了算法之外还指定了填充,则会抛出此异常被创建,例如如果在 getInstance()
中传递 RSA/ECB/PKCS1Padding
而不是 RSA
。相反,在实例化 Cipher
对象时应指定填充,例如应该传递 RSA/ECB/PKCS1Padding
而不是 RSA
。如果此处仅指定算法,则填充将使用 provider-dependent 默认值,应避免这种情况。请注意,PKCS1Padding
表示 PKCS#1 v1.5 填充,并不对应于 C# 代码中使用的 OAEP。