Java 使用 RSA 进行加密、解密、签名和验签
RSA(Rivest-Shamir-Adleman)算法是一种非对称加密算法,广泛用于数据加密和数字签名领域。它是由 Ron Rivest、Adi Shamir 和 Leonard Adleman 于 1977 年共同提出的。
RSA 算法常用于如下场景:
- 公钥加密,私钥解密
- 私钥加密,公钥解密(不推荐)
- 私钥签名,公钥验签
生成密钥对
通过 Java java.security
包下的工具类可以生成 RSA 公钥和私钥。
package cn.springdoc.demo.test;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
/**
* @author springdoc.cn
* 生成 RSA 密钥对
*/
public class Main {
public static void main(String[] args) throws Exception {
// 初始化 Key 生成器,指定算法类型为 RSA
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// 密钥长度为 2048 位
keyPairGenerator.initialize(2048);
// 生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取公钥
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
// 获取私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
System.out.println("公钥:" + Base64.getEncoder().encodeToString(rsaPublicKey.getEncoded()));
System.out.println("私钥:" + Base64.getEncoder().encodeToString(rsaPrivateKey.getEncoded()));
}
}
如上,通过 KeyPairGenerator.getInstance("RSA");
创建 RSA 密钥对生成器,然后通过 initialize
方法指定 RSA 的密钥长度为 2048 位。注意,RSA 密钥长度位数必须是 512 的倍数,最小为 512,密钥长度越长,越安全(难破解),但是 密钥的生成、加密、解密都会更加的耗时。一般来说,目前最常见、安全的 RSA 密钥长度为 2048 位。
最后生成 KeyPair
密钥对,然后从密钥对中获取到 公钥 和 私钥。
执行 main 方法,生成的密钥对如下:
公钥:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAibDslV9dzQDmRfabaOCozwX6Qhp++sInt+qZ0aE/2Kd3byU1MivYJNIznf41Cz/Rd4IuvdJR/d9YDbMghiWpHg8WMeM9gTGtADJ3NIWceAOdYJsUbjPTsZ0xrxZlTflS8xC86W9GAlM36tF5tVu44xfc2nloVDVQ5zv5dbw7VisME0kobIi0/eLHYYiv5IRzYHhgiXwcO6iZ70xZaSm/+P6+Nl0QOOR+jsmEU5zBnflUCgRLlAbTOf7m3n5/DbQm1W+wyUxIFt/74jC+aWqoug7+AD43SobZ7DXfAoovOhRtUKZK5ir42lYwklFpZoBH6Zg3wLDPdsPg1YsjTOcf+QIDAQAB
私钥:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCJsOyVX13NAOZF9pto4KjPBfpCGn76wie36pnRoT/Yp3dvJTUyK9gk0jOd/jULP9F3gi690lH931gNsyCGJakeDxYx4z2BMa0AMnc0hZx4A51gmxRuM9OxnTGvFmVN+VLzELzpb0YCUzfq0Xm1W7jjF9zaeWhUNVDnO/l1vDtWKwwTSShsiLT94sdhiK/khHNgeGCJfBw7qJnvTFlpKb/4/r42XRA45H6OyYRTnMGd+VQKBEuUBtM5/ubefn8NtCbVb7DJTEgW3/viML5paqi6Dv4APjdKhtnsNd8Cii86FG1QpkrmKvjaVjCSUWlmgEfpmDfAsM92w+DViyNM5x/5AgMBAAECggEAAbdr65zJvZGCTWL6ov8R+6q2aPaNmLCs8BUDn+Jjul60FXWgSSTUx/i63gBaRu6fvN6pmIIztXeAUINoJ2P2zMIhY7PSg3bMElGOvN/hiHl7D8Y/JfSNcgxknysnTiaKiy8tO7fAZq0E6G9+Fe2zy1jzzuFulIYpoQhVXste7f8Ms6XWfbpH6daNimN8h+JTKKHecBaLX5vlL8U6BI/4382GCu2gQ+mn3aII2PdpHkET3AQy+8lIFYq0tuigPFPaflVbOtkqPIG3gTjdfkPgi18N/0g5s6XpVPbifu7l7xhPqHqJMvWVB+g5cnJ9vku/nT1nlks4iQOp5FQcWQlCAQKBgQC2eXwxAWYtXORWTNU5SjyrcnUQzq6YeWBDyxWjDHWAMWIcgDFj5mPJ2lmjnXdXusJc4UFdv1EGD65bgODIp/PJhYDsAB1kuCls7IlfCPt7Mabbswpj30Db+d6ZEUPrEBQf2FMcqiEnD87BiQI2AMvqsfacZF+py26ctKFdWE5MOQKBgQDBK/ZItcgWKXb1pIQsibfxnhop1DBiz1vT5Wp65LKLrcsN/HaASSCUVxetovOQ2NAnYmXd2uTX384dMLOZ5eFSSWZJ5FH0tOWOWh7YSSGHYk4C3KSiQ5mpEeunIqiydmEz14UaVr2uOvBKyqkR77vpmRMXOZikf7dfFBC6oQ7xwQKBgFlMSoKQ9LfuZa/2GcLXmaktciyveLIVdA65K/WG+1mo0SKxeXoyHVODD6sKPIUqWEOr2JlQLw6QTftprpeD2cIdG2JjC/9mQJ+VggSIGMnJGqcZj1QgxVThixXNZTd7vt12t/WnLGI5Ui1UJ0jWSvSn8s/GLyqg65i0rONJe8fBAoGASKsPFT89LAkhNTtUdaTBS+WcFgw6v5EeDFKLgfuypxUUBAjrU0svNF6nC6z3T5AgjhBYHeOQnx0UXqrJhaWS4++0yStZIWND0A9a9yZbtKBolG8Ih/pCPfX93nwNVkVuP6Bd9BCZfoexiZE/lP3IhRIOunfyUj+xbdUQjgS0qIECgYEAgoxH74SDvmRZPFWPdmUGjhZAE1MtkTDgYfwFu/hM1XDlu1+j51cihV1SDl/FOzM6XEg9d8OgVKgDv5xLTHb9mP+4ZF7H4M/meYa+M6JfALVA4nOWknbb5V3XOBPwBiEJx0WS+Ch1p1rJU4tp7QoT8tOcU4FuTewAH4BvJZJSFAU=
你可以看到,私钥要长得多。而且,每次生成的密钥对内容都是不一样的。
公钥加密,私钥解密
使用公钥加密数据,私钥解密。这是最常见的用法。
package cn.springdoc.demo.test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
/**
* @author springdoc.cn
* RSA 公钥加密,私钥解密
*/
public class Main {
/**
* 加密
*
* @param key KEY
* @param in 输入参数
* @param out 输出加密后的密文
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws IOException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
public static void encode(Key key, InputStream in, OutputStream out) throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException {
// 最大的加密明文长度
final int maxEncryptBlock = 245;
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] buffer = new byte[maxEncryptBlock];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(cipher.doFinal(buffer, 0, len));
}
}
/**
* 解密
*
* @param key KEY
* @param in 输入参数
* @param out 输出解密后的原文
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws IOException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
public static void decode(Key key, InputStream in, OutputStream out) throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException, IOException, IllegalBlockSizeException, BadPaddingException {
// 最大的加密明文长度
final int maxDecryptBlock = 256;
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] buffer = new byte[maxDecryptBlock];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(cipher.doFinal(buffer, 0, len));
}
}
public static void main(String[] args) throws Exception {
// 生成 RSA 密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥和私钥
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
// 要加密的原文
byte[] content = "你好 springdoc.cn".getBytes(StandardCharsets.UTF_8);
System.out.println("原文:" + new String(content, StandardCharsets.UTF_8));
// 加密后的密文
ByteArrayOutputStream encryptedout = new ByteArrayOutputStream();
// 公钥加密
encode(rsaPublicKey, new ByteArrayInputStream(content), encryptedout);
System.out.println("加密后的密文:" + Base64.getEncoder().encodeToString(encryptedout.toByteArray()));
// 解密后的原文
ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream();
// 私钥解密
decode(rsaPrivateKey, new ByteArrayInputStream(encryptedout.toByteArray()), decryptedOut);
System.out.println("解密后的原文:" + new String(decryptedOut.toByteArray(), StandardCharsets.UTF_8));
}
}
encode
方法用于加密数据,参数 Key
可以指定公钥或者是私钥,InputStream
表示原文,最后加密的结果会输出到 OutputStream
中。
考虑到要加密的原文可能会很大,因此通过 InputStream
流进行分段加密。对于 2048 位的密钥(使用 PKCS#1 v1.5 填充方案,填充开销通常为11字节)来说,分段加密的最大长度为 2048 / 8 - 11 = 245
字节。
decode
方法用于解密数据,参数 Key
可以指定公钥或者是私钥,InputStream
表示 加密后的密文,解密后的结果输出到 OutputStream
中。这里也采用了分段解密的方式,对于 2048 位的密钥来说,分段解密的最大长度为 2048 / 8 = 256
字节。
最后,在 main
方法中,按照上一节的方式生成 RSA 密钥对。
首先使用私钥加密原文,获取到密文后,再用公钥解密密文。获取到原文。
执行 main 方法,输出如下:
原文:你好 springdoc.cn
加密后的密文:YBMWdc94oLhpDgpDFc+HMSUl7eKBkriueGUDYrkiS4F5avd1on3VfQS1791MMhq1znwTjlgNFSB79jqjhcxIEKlsLU90tMko2FmAQQwR5VD3uxCuMxgWSLawS6DscSDKvXXIxKHVQvEihlnzEzqJqV20cbu5WwUeoFP3UIzPi5+wLX/99wKlRpG9BjYVN+jirCfDxyfDgpUnK0UK/IfChDd3MBZDmN0MuAZPcMa6TjAtrCsjqDCymM+2W+daqyDJnX529Wk6E3sV08Y9mvCAd+UmP9fTYpzrVwbbhy4QODOSdQ/Z6xr7vp/2sbRbnVr0iU9rX7oLIYjMTZT2uR9lZg==
解密后的原文:你好 springdoc.cn
如上,成功地加密解密了数据,
RSA 算法的执行速度较慢,尤其是对于大型密钥和数据。因此,在实际应用中,通常使用 RSA 算法来加密对称密钥(如 AES),然后使用对称加密算法加密实际的数据,以提高效率。
私钥加密,公钥解密
还是使用上节中的 main 方法,只需要调换一下加密、解密的密钥即可。
// 生成 RSA 密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥和私钥
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
// 要加密的原文
byte[] content = "你好 springdoc.cn".getBytes(StandardCharsets.UTF_8);
System.out.println("原文:" + new String(content, StandardCharsets.UTF_8));
// 加密后的密文
ByteArrayOutputStream encryptedout = new ByteArrayOutputStream();
// 私钥加密 1️⃣
encode(rsaPrivateKey, new ByteArrayInputStream(content), encryptedout);
System.out.println("私钥加密后的密文:" + Base64.getEncoder().encodeToString(encryptedout.toByteArray()));
// 解密后的原文
ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream();
// 公钥解密 2️⃣
decode(rsaPublicKey, new ByteArrayInputStream(encryptedout.toByteArray()), decryptedOut);
System.out.println("公钥解密后的原文:" + new String(decryptedOut.toByteArray(), StandardCharsets.UTF_8));
执行 main 方法,输出如下:
原文:你好 springdoc.cn
私钥加密后的密文:Gp3eD0u3OAHeJlyx0nH/1uiRBqFwlWH4OqH3Xl1IfVD40Q5lb5m7zAZ7Ml9yOtQiOxrzaM5Fxs9fGSUJ+yMXCOs3yQEdmSMMuO6eorsJ6Re74QD/UlF0IyuSJyyg4I7ejmI5e2JL0Vx72RAilYAMqIStMStHjXI6qCDlujxP1TicxW70bCutqvC/zjy4QshK9cJ8k1rngooZdjJsC1V+GR5sDbsTdqnx19eRAzuMAM+irPxDJpjBh0Cmgfr2pjMBKzt/akU16ctwmedu9+FlAVXdh/ZojheBAcSMQcsRQ3aKdAOQEeKM+o+9X2LdV3N5ZNIr3dqARfSVnKkkNlHIDQ==
公钥解密后的原文:你好 springdoc.cn
私钥加密、公钥解密的方式在加密通信中不推荐使用,原因如下:
- 安全性:私钥加密、公钥解密方式容易受到中间人攻击(Man-in-the-Middle Attack)。在这种攻击中,攻击者可以截获公钥,将其替换为自己的公钥,并将解密和加密操作重定向到自己的密钥上。这样,攻击者就可以轻松地获取敏感信息。
- 效率:私钥加密、公钥解密的过程相对于公钥加密、私钥解密更加耗时。私钥加密通常需要进行较大的计算量,因为私钥的长度通常比公钥更长。这会导致加密和解密操作的延迟增加。
- 密钥管理:私钥是加密系统中的重要组成部分,其安全性至关重要。在私钥加密、公钥解密的方式中,需要确保私钥的安全存储和传输,以防止未经授权的访问。相比之下,公钥可以公开传播,并不需要像私钥那样严格保护。
所以,常见的做法是使用公钥加密、私钥解密的方式来确保加密通信的安全性。这种方式可以保证加密过程在公共网络上进行,而私钥只保留在通信的接收方,从而更好地保护了敏感信息的安全性。
私钥签名,公钥验签
私钥签名和公钥验签是一种常见的数字签名机制,用于验证消息的完整性和身份认证。
- 私钥签名
- 发送方使用其私钥对消息进行签名。具体过程为先对消息进行哈希运算,然后使用私钥对哈希值进行加密。这样产生的密文就是数字签名。
- 私钥签名的关键是私钥的保密性,只有私钥的持有者才能生成有效的签名。
- 公钥验签
- 接收方收到消息和相应的数字签名。
- 接收方使用发送方的公钥对数字签名进行解密,得到签名的哈希值。
- 接收方对接收到的消息进行哈希运算,得到消息的哈希值。
- 最后,接收方比较解密得到的签名哈希值与计算得到的消息哈希值是否一致。
- 如果两个哈希值相同,说明签名有效,消息没有被篡改,并且发送方的身份得到了验证。
私钥用于生成签名,公钥用于验证签名。由于私钥是私密的,只有签名者可以生成有效的签名,而公钥是公开的,任何人都可以使用公钥验证签名。
数字签名可以提供消息的完整性和身份认证,确保消息在传输过程中没有被篡改,并且发送方是可信的。它在许多领域中被广泛应用,如电子邮件、网络通信和数字证书等。
package cn.springdoc.demo.test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
/**
* @author springdoc.cn
* RSA 私钥签名、公钥验签
*/
public class Main {
/**
* 私钥签名
*
* @param key 私钥
* @param algorithm 算法
* @param in 输入数据
* @return 签名
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws IOException
* @throws SignatureException
*/
public static byte[] sign(RSAPrivateKey key, String algorithm, InputStream in)
throws InvalidKeyException, NoSuchAlgorithmException, IOException, SignatureException {
Signature signature = Signature.getInstance(algorithm);
signature.initSign(key);
byte[] buffer = new byte[4096];
int len = 0;
while ((len = in.read(buffer)) != -1) {
signature.update(buffer, 0, len);
}
return signature.sign();
}
/**
* 公钥验签
*
* @param key 公钥
* @param algorithm 算法
* @param in 输入数据
* @param sign 签名
* @return 签名是否符合
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
* @throws IOException
*/
public static boolean validate(RSAPublicKey key, String algorithm, InputStream in, byte[] sign)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, IOException {
Signature signature = Signature.getInstance(algorithm);
signature.initVerify(key);
byte[] buffer = new byte[4096];
int len = 0;
while ((len = in.read(buffer)) != -1) {
signature.update(buffer, 0, len);
}
return signature.verify(sign);
}
public static void main(String[] args) throws Exception {
// 生成 RSA 密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥和私钥
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
// 要签名的原文
byte[] content = "你好 springdoc.cn".getBytes(StandardCharsets.UTF_8);
// 使用私钥对数据进行签名
byte[] sign = sign(rsaPrivateKey, "SHA256WithRSA", new ByteArrayInputStream(content));
System.out.println("数字签名:" + Base64.getEncoder().encodeToString(sign));
// 使用公钥验证签名
boolean valid = validate(rsaPublicKey, "SHA256WithRSA", new ByteArrayInputStream(content), sign);
System.out.println("验签结果:" + valid);
}
}
sign
方法使用 RSA 私钥 key
对数据 in
进行签名并返回,签名使用的算法通过 algorithm
参数指定。
validate
方法则使用 RSA 公钥 key
校验数据 in
的签名 sign
是否合法,同样,通过 algorithm
参数指定和签名时一样的算法。
最后,在 main 方法中,先根据第一节中的方法生成 RSA 密钥对。
然后调用 sign
方法,使用私钥对原文 content
进行签名,获取签名结果。
得到签名后,再调用 validate
方法,使用公钥验证原文 content
的签名是否合法。
这里的签名和验签均使用同一算法:SHA256WithRSA
。
执行 main 方法,输出如下:
数字签名:TxUJGyj4EtQUiZ8xk6OY4lDxfwhrHrBi0yROolNfO5KBfOLVj28w9t4s52exOL9SneWE5SRaYg0eIoGBCmwBziaLR7OPGwdnSgH8hij0TvPlNgrAfyWWkD0Rc9EZ0lHDVvcIbhbQZ0Aq8N3Qd7dPi7eScAVdZiu/oA6ALhTg0Zjr220CZqT5HQpK9z+aIVJR79wceexovbYqnJ+WYpPNIs0M4aVo5MA4uM/LkwcktOk9PfNQXogjgY7xOvrn/jAQ+x2uWcs5Lo1PQhwIg/Vc6J5zc3v7ieNnV2itAuQCtakjQpovNcaVs5zbq5Qe3Tk+F0serI98LArcVXes4zqTig==
验签结果:true
验签时,如果 签名 或者是 原文 有任何改动,那么签名就会验证失败。