使用RSA非对称加密算法进行加解密
1、非对称加密算法的加密方式
对称加密算法使用相同的密钥进行加密,安全性依赖于密钥的保密性,而非对称加密算法则是使用一对密钥(公钥 和 私钥)来进行加解密,公钥(publicKey)加密,私钥(privateKey)解密,通常用于两个系统之间的交互。
常见的非对称加密算法:RSA、ECC(椭圆曲线加密)、DSA(数字签名算法)等
A系统生成一对密钥(公钥A 和 私钥A),B系统生成一对密钥(公钥B 和 私钥B)。然后,两个系统分别提供给对方,自己系统的公钥。这样做,即使公钥被捕获也没用,因为解密是通过私钥解密的。而私钥只有自己系统才知道。
交换结果如下:
| A系统 | B系统 |
| 公钥A | 公钥B |
| 私钥A | 私钥B |
| 公钥B(publicKey-B) | 公钥A(publicKey-A) |
B系统要发送加密信息给A系统时,必须使用公钥A进行加密,然后把加密信息发送给A系统。A系统再通过私钥A将获取到的加密信息进行解密。
A系统回复B系统消息,A系统需使用公钥B将返回信息进行加密,B系统拿到返回的加密信息之后,通过私钥B将返回信息进行解密。
1.0 位(bit)、字节(byte)、字符之间的关系
1字节(byte) = 8bit,但1字符并不一定占用1字节,取决于字符编码。
ASCII编码方式下,每个字符占用 1 字节,包括英文字母、数字、标点符号及一些控制字符。
UTF-8编码方式下,字符长度不固定。ASCII 范围内的字符仍占用1字节,中文字符占用3字节。
GBK编码方式下,中文字符占2字节
2、定义RSA加密工具类
该工具类主要用途:
①生成一对密钥(公钥和私钥)
②提供加解密的方法
2.1 初版RSA工具类(存在数据量过大时,加解密失败)
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
private static String pubKey;
private static String priKey;
/**
* 生成RSA密钥对
*/
public static void generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
//可以选择1024, 2048, 4096 等
keyGen.initialize(2048);
//生成一对密钥,保存在KeyPair中
KeyPair pair = keyGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) pair.getPublic();
pubKey = Base64.getEncoder().encodeToString(publicKey.getEncoded());
System.out.println("随机生成的公钥:\n" + pubKey);
RSAPrivateKey privateKey = (RSAPrivateKey) pair.getPrivate();
priKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
System.out.println("随机生成的私钥:\n" + priKey);
}
/**
* 使用公钥加密
*
* @param data 明文信息
* @param publicKey 公钥
* @return
* @throws Exception
*/
public static String encrypt(String data, String publicKey) throws Exception {
byte[] decode = Base64.getDecoder().decode(publicKey);
//需要以字节数组的形式,导入已有公钥
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decode);
RSAPublicKey rsaPub = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, rsaPub);
//执行加密操作
byte[] encryptedBytes = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* 使用私钥解密
*
* @param encryptData 加密信息
* @return
* @throws Exception
*/
public static String decrypt(String encryptData, String privateKey) throws Exception {
byte[] decode = Base64.getDecoder().decode(privateKey);
//导入已有私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decode);
RSAPrivateKey rsaPri = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, rsaPri);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptData));
return new String(decryptedBytes,"UTF-8");
}
public static void main(String[] args) {
RSAUtil rsaUtil = new RSAUtil();
try {
//生成密钥对
generateKeyPair();
StringBuilder sb = new StringBuilder();
//构建一个大于245字节的测试数据
for (int i = 0; i < 200; i++) {
sb.append(i);
}
String data = sb.toString();
String encryptedData = rsaUtil.encrypt(data, pubKey);
System.out.println("加密信息: \n" + encryptedData);
String decryptedText = rsaUtil.decrypt(encryptedData, priKey);
System.out.println("解密信息: \n" + decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
}
启动运行会提示:加密数据不能超过245字节

原因是使用RSA加密算法的时候,密钥长度为2048位,默认的填充方案为PKCS1,而明文的字节长度超过了245字节就会报错,至于为什么,代码的注释上会解释。
2.2 工具类2.0版(采用分段加密,解决数据量过大,加解密失败问题)
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSAUtil {
/**
* RSA分段加密明文大小
* 密钥为2048位,对应256字节
* <p>
* Cipher.getInstance("RSA") 等价于 RSA/ECB/PKCS1Padding
* 填充模式:PKCS#1 v1.5 占用 11个字节
* <p>
* 分段加密明文 应小于 256 - 11,即为245字节
* <p>
* 而分段加密明文大小设置为117字节,是为了兼容曾经密钥长度为1024位的情况
* <p>
* 1024位 对应 128字节,减去填充模式占用字节(11字节),就等于117字节
*/
private static int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA密文块大小(MAX_DECRYPT_BLOCK = 密钥长度/8)
* <p>
* 密钥长度(keySize)
* 2048位(bit) = 256字节(byte)
* <p>
* 1024位(bit) = 128字节(byte)
* <p>
* <p>
* RSA的规范,密文大小 必须严格等于 密钥长度,如果不等于这个值,会导致分片失败
*/
private static int MAX_DECRYPT_BLOCK = 256;
private static String pubKey;
private static String priKey;
/**
* 生成RSA密钥对
*/
public static void generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
//密钥长度可以选择 1024位、2048位、4096位
keyGen.initialize(2048);
//生成一对密钥,保存在KeyPair中
KeyPair pair = keyGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) pair.getPublic();
pubKey = Base64.getEncoder().encodeToString(publicKey.getEncoded());
System.out.println("随机生成的公钥:\n" + pubKey);
RSAPrivateKey privateKey = (RSAPrivateKey) pair.getPrivate();
priKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
System.out.println("随机生成的私钥:\n" + priKey);
}
/**
* 使用公钥加密
*
* @param data 明文信息
* @param publicKey 公钥
* @return
* @throws Exception
*/
public static String encrypt(String data, String publicKey) throws Exception {
byte[] decode = Base64.getDecoder().decode(publicKey);
//需要以字节数组的形式,导入已有公钥
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decode);
RSAPublicKey rsaPub = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpec);
Cipher cipher = Cipher.getInstance("RSA");
System.out.println("实际算法:" + cipher.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, rsaPub);
int dataLength = data.getBytes().length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
//如果不想写死,也可以用这种方式获取【分段加密明文大小】
// MAX_ENCRYPT_BLOCK = rsaPub.getModulus().bitLength() / 8 - 11;
// System.out.println("明文最大字节数:" + (rsaPub.getModulus().bitLength() / 8 - 11));
int offset = 0;
byte[] cache;
//对数据分段加密,当偏移量offset等于明文字节长度,就说明已经全部分段加密完成,退出循环
while (offset < dataLength) {
//min方法取最小值,如果最小值是 MAX_ENCRYPT_BLOCK,说明需要进行分组
int inputLen = Math.min(MAX_ENCRYPT_BLOCK, dataLength - offset);
//分割执行加密
cache = cipher.doFinal(data.getBytes(), offset, inputLen);
//合并加密块(每个加密块cache肯定等于当前的密钥长度,即256字节)
out.write(cache, 0, cache.length);
offset += inputLen;
}
byte[] encryptedData = out.toByteArray();
out.close();
return Base64.getEncoder().encodeToString(encryptedData);
}
/**
* 使用私钥解密
*
* @param encryptData 加密信息
* @return
* @throws Exception
*/
public static String decrypt(String encryptData, String privateKey) throws Exception {
//将私钥通过base64编码转为字节数组
byte[] privateKeyDecode = Base64.getDecoder().decode(privateKey);
//导入已有私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyDecode);
RSAPrivateKey rsaPri = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, rsaPri);
//将加密信息通过base64编码转为字节数组
byte[] dataBytes = Base64.getDecoder().decode(encryptData.getBytes("UTF-8"));
int dataLength = dataBytes.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
// 对加密块进行解密
while (offset < dataLength) {
cache = cipher.doFinal(dataBytes, offset, MAX_DECRYPT_BLOCK);
out.write(cache, 0, cache.length);
offset += MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return new String(decryptedData, "UTF-8");
}
public static void main(String[] args) {
RSAUtil rsaUtil = new RSAUtil();
try {
//生成密钥对
generateKeyPair();
StringBuilder sb = new StringBuilder();
//构建一个大于245字节的测试数据
for (int i = 0; i < 1111; i++) {
sb.append(i);
}
String data = sb.toString();
String encryptedData = rsaUtil.encrypt(data, pubKey);
System.out.println("加密信息: \n" + encryptedData);
String decryptedText = rsaUtil.decrypt(encryptedData, priKey);
System.out.println("解密信息: \n" + decryptedText);
} catch (Exception e) {
e.printStackTrace();
}
}
}
以2048位密钥长度(即256字节)为例:
| 操作阶段 | 填充模式 | 长度约束 | 是否受填充模式影响 |
| 加密之前 |
PSCK#1 (11字节) |
明文的字节长度 <= 密钥字节数(256字节) - 填充开销(11字节) | 受影响 |
| 解密输入 |
PSCK#1 (11字节) |
密文块 = 密钥的字节长度 | 不受影响 |
不同的填充模式,占用的字节不同,这里不花过多篇幅解释。
由于每个密文块都是256字节长度,所以合并起来的密文字节长度肯定是256的整数倍。
3、关键方法解释
Cipher的doFinal(byte[] input, int inputOffset, int inputLen) 方法

input: 输入字节数组,表示要进行最终转换的数据源。
inputOffset: 整数类型的偏移量,指定了输入数组中开始读取的位置。这意味着实际参与运算的是从该位置起始的一段子数组。
inputLen: 表示需要被处理的有效长度,即从inputOffset开始往后数inputLen个字节长度,这部分的连续字节,要参与到加/解密的操作中。
4、为什么AES不需要分段进行加解密?
因为AES对称加密算法,属于分组加密算法,本身就支持数据分块处理,其工作模式如CBC,会自动进行分组迭代加密,配合PKCS7填充模式,保证数据完整性。
RSA属于块加密算法,要求需要加密的明文必须作为整体进行处理,但明文的字节长度又受到密钥长度的影响(2048位密钥一次最多只能支持加密245字节长度的明文),所以需要分段加密。
AES设计目标就是针对大数据,数据长度通过分组迭代处理,也就没有限制。
RSA设计是用于小数据安全(如加密AES的密钥),直接加密大数据,其耗时会大大增加。
5、拓展--混合加密
如果觉得RSA分段加密麻烦,还可以使用AES+RSA进行混合加密。
首先,用AES将数据进行加密,然后RSA只需要加密AES的Key就行了,而AES的Key就相对较短,AES128只有16字节、AES192只有24字节、AES256只有32字节。
然后,解密就用RSA的私钥解密,拿到AES的Key,最后,在用AES解密,拿到真正的数据。
更多推荐


所有评论(0)