RSA前端js加密 后端java解密

Yuyang 前端小白🥬

什么是RSA加密

RSA加密是一种非对称加密算法,其具体实现如下:
1、选择两个不相等的质数p和q,计算n=pq
2、计算n的欧拉函数φ(n)=(p-1)
(q-1)
3、选择一个整数e,1<e<φ(n),且e与φ(n)互质
4、计算d, 使得(d*e)modφ(n)=1
5、公钥是(n, e),私钥是(n, d)
6、加密时,将明文m^e mod n,解密时,将密文c^d mod n

例如公钥(n, e) = (33, 3),私钥(n, d) = (33, 7),明文m = 24,加密后的密文c = 24^3 mod 33 = 24,解密后的明文m = 24^7 mod 33 = 24

p = 11, q = 3, n = 33, φ(n) = 20, e = 3, d = 7

前端js加密

1
2
npm install jsencrypt
npm install node-forge

生成公钥和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import forge from 'node-forge'
// 生成RSA密钥对
const { privateKey, publicKey } = forge.pki.rsa.generateKeyPair(1024);

// 转换公钥和私钥为PEM格式字符串
const publicKeyPem = forge.pki.publicKeyToPem(publicKey);
const privateKeyPem = forge.pki.privateKeyToPem(privateKey);

console.log('Public Key:', publicKeyPem);
console.log('Private Key:', privateKeyPem);

// 生成的公钥和私钥
// 公钥
// -----BEGIN PUBLIC KEY-----
// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCjXq96Iz4+TShUSGkPO2LpCWue
// C56FQoEFGZqx/7F6JIqLdF8qA773iPk1qbqSbtYPzu3lCOXWxDl99bI+aIKEJPDp
// cGM4QbMwDhYN2WgUHbnaAPJOLpCvf2S+COG3HRQMarMvTX2PVujIhi44EfOPPJ5f
// lf6KAddjsiCBi85x7wIDAQAB
// -----END PUBLIC KEY-----
// 私钥
// -----BEGIN RSA PRIVATE KEY-----
// MIICXAIBAAKBgQCjXq96Iz4+TShUSGkPO2LpCWueC56FQoEFGZqx/7F6JIqLdF8q
// A773iPk1qbqSbtYPzu3lCOXWxDl99bI+aIKEJPDpcGM4QbMwDhYN2WgUHbnaAPJO
// LpCvf2S+COG3HRQMarMvTX2PVujIhi44EfOPPJ5flf6KAddjsiCBi85x7wIDAQAB
// AoGAUfw9gcTYAroD6DAikSNTYvF4UWsxVVznlKCh0nwoG8zkvlFbRKF2n5Dcx8Jy
// v/PhdzN4jX0mot38oCrCFbGq9UoohUYo0tKKroWjV6Tz2Cbfv/9/GGi6I3nrn409
// hh9LzyRoIddLRaBBL91bZZ7OKF9G98Ya27y3teUM1MfX5uECQQDcD6dtZsNTjqFm
// haWKlcvEB3z/+brgwMFU53g0PLhfCTD7dXfh83Sc9zcrMDMYAnM6bDBTKrMet6LV
// M33OmyURAkEAvgzf6luLFHlQZ8T8CpPRMOcfYrn6qnVBeZ5FsQx+Le84rqk3Tcv+
// mXqhjJia49J2n6qlSRyiY5loos6A64Qm/wJASY5p9mWNEJbyWCSACuy7KZEfNSiy
// UIHdnZWpVjydnBakasj/A2WvKvBvXl5EFdhrz7FuDb6OFMxo/z6w5KhzIQJAZVHB
// 0lRpikm+twPKhu0VOmgNUHnsDBIlDOx3JROO9XgylNM726rkmfhJxgXZlZmviiHU
// qmysmkLACQPksSqsCwJBAJVEcG3gonm0BGmZiMDmwwP0M7grYTOUHDAVDJV1u1cB
// hYoV6mr5ZDBOMMIMroIHorcuYdqEKfiV8ulHc8hxBmo=
// -----END RSA PRIVATE KEY-----

前端加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Encrypt from 'jsencrypt';

const publulicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCjXq96Iz4+TShUSGkPO2LpCWue
C56FQoEFGZqx/7F6JIqLdF8qA773iPk1qbqSbtYPzu3lCOXWxDl99bI+aIKEJPDp
cGM4QbMwDhYN2WgUHbnaAPJOLpCvf2S+COG3HRQMarMvTX2PVujIhi44EfOPPJ5f
lf6KAddjsiCBi85x7wIDAQAB
-----END PUBLIC KEY-----`;
const encryptor = new Encrypt();
encryptor.setPublicKey(publulicKey);
const data = {
password: '123456',
}
const encrypted = encryptor.encrypt(JSON.stringify(data));
console.log("🚀 ~ encrypted:", encrypted)

// 🚀 ~ encrypted: HFzmziwImUXYOfVDy2RnlFm73rjWRQ/sacZ/hV7/XEATCaIjQdTbIPeH3iy+Kc6rIRJRp42LKMWLGq0x2pBdHauCEbJeKDGF3t3RXKk+AM/CFsODv3rXwXt4ArDMkhrtDg7ha6Zgr4jIqvkhauV9hVrB34RUK4jRBw8gBO9rmoM=

后端解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import org.springframework.stereotype.Service;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import javax.crypto.Cipher;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
@Service
public class EncryptService {
private static final int RSA_KEY_SIZE = 1024;
private static final int CHUNK_SIZE = RSA_KEY_SIZE / 8;

public void testRsa(String encryptedData){
System.out.println(encryptedData);
Security.addProvider(new BouncyCastleProvider());
try {
String privateKeyStr = " -----BEGIN RSA PRIVATE KEY-----\n" +
" MIICXAIBAAKBgQCjXq96Iz4+TShUSGkPO2LpCWueC56FQoEFGZqx/7F6JIqLdF8q\n" +
" A773iPk1qbqSbtYPzu3lCOXWxDl99bI+aIKEJPDpcGM4QbMwDhYN2WgUHbnaAPJO\n" +
" LpCvf2S+COG3HRQMarMvTX2PVujIhi44EfOPPJ5flf6KAddjsiCBi85x7wIDAQAB\n" +
" AoGAUfw9gcTYAroD6DAikSNTYvF4UWsxVVznlKCh0nwoG8zkvlFbRKF2n5Dcx8Jy\n" +
" v/PhdzN4jX0mot38oCrCFbGq9UoohUYo0tKKroWjV6Tz2Cbfv/9/GGi6I3nrn409\n" +
" hh9LzyRoIddLRaBBL91bZZ7OKF9G98Ya27y3teUM1MfX5uECQQDcD6dtZsNTjqFm\n" +
" haWKlcvEB3z/+brgwMFU53g0PLhfCTD7dXfh83Sc9zcrMDMYAnM6bDBTKrMet6LV\n" +
" M33OmyURAkEAvgzf6luLFHlQZ8T8CpPRMOcfYrn6qnVBeZ5FsQx+Le84rqk3Tcv+\n" +
" mXqhjJia49J2n6qlSRyiY5loos6A64Qm/wJASY5p9mWNEJbyWCSACuy7KZEfNSiy\n" +
" UIHdnZWpVjydnBakasj/A2WvKvBvXl5EFdhrz7FuDb6OFMxo/z6w5KhzIQJAZVHB\n" +
" 0lRpikm+twPKhu0VOmgNUHnsDBIlDOx3JROO9XgylNM726rkmfhJxgXZlZmviiHU\n" +
" qmysmkLACQPksSqsCwJBAJVEcG3gonm0BGmZiMDmwwP0M7grYTOUHDAVDJV1u1cB\n" +
" hYoV6mr5ZDBOMMIMroIHorcuYdqEKfiV8ulHc8hxBmo=\n" +
" -----END RSA PRIVATE KEY-----";

PrivateKey privateKey = getPrivateKeyFromPem(privateKeyStr);
String decryptedData = decryptData(encryptedData, privateKey);

// 将解密后的 Base64 编码字符串转换回原始的 JSON 字符串
// byte[] decodedBytes = Base64.getDecoder().decode(decryptedData);
// String jsonString = new String(decodedBytes, StandardCharsets.UTF_8);

// System.out.println("Decrypted Data: " + jsonString);
System.out.println("Decrypted Data: " + decryptedData);
// System.out.println(java.net.URLDecoder.decode(decryptedData ,"UTF-8"));
} catch (Exception e) {
e.printStackTrace();
}
}

private static PrivateKey getPrivateKeyFromPem(String pemString) throws Exception {
// PemReader pemReader = new PemReader(new StringReader(pemString));
// PemObject pemObject = pemReader.readPemObject();
// byte[] keyBytes = pemObject.getContent();
// pemReader.close();
//
// PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
// KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// return keyFactory.generatePrivate(keySpec);
String privateKeyPEM = pemString
.replace("-----BEGIN RSA PRIVATE KEY-----", "")
.replace("-----END RSA PRIVATE KEY-----", "")
.replaceAll("\\s", ""); // 删除所有空格和换行

byte[] encoded = Base64.getDecoder().decode(privateKeyPEM);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return keyFactory.generatePrivate(keySpec);
}

private static String decryptData(String encryptedData, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);

byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData);

// Ensure the buffer is large enough to hold all decrypted chunks
byte[] decryptedBytes = new byte[encryptedBytes.length];
int decryptedLength = 0;

for (int i = 0; i < encryptedBytes.length; i += CHUNK_SIZE) {
int chunkLength = Math.min(CHUNK_SIZE, encryptedBytes.length - i);
byte[] chunk = new byte[chunkLength];
System.arraycopy(encryptedBytes, i, chunk, 0, chunkLength);
byte[] decryptedChunk = cipher.doFinal(chunk);
System.arraycopy(decryptedChunk, 0, decryptedBytes, decryptedLength, decryptedChunk.length);
decryptedLength += decryptedChunk.length;
}

return new String(decryptedBytes, 0, decryptedLength, StandardCharsets.UTF_8);
}
}

存在的问题 当前端解密信息超长时jsencrypt库无法分段加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 将数据替换为以下数据即会显示Message too long for RSA 原因:RSA 加密算法对单个消息块的长度有限制。这是因为 RSA 加密使用公钥对消息进行加密,而消息的长度不能超过密钥的长度减去填充字节数。因此对于 1024 位(128 字节)的 RSA 密钥,最大消息长度为:128 - 11 - 3 = 128 - 14 = 117

const data = {
code: 200,
result: {
timestamp: 1572321851823,
inter1: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter2: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter3: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter4: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter5: [
"123123123",
"123123123",
"123123123",
"123s123123",
"123123123",
],
inter6: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
stream: {},
caton: {},
card: [],
},
};

解决方法使用encryptlong库

1
npm install encryptlong

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import JSEncrypt from 'encryptlong';
const publulicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCjXq96Iz4+TShUSGkPO2LpCWue
C56FQoEFGZqx/7F6JIqLdF8qA773iPk1qbqSbtYPzu3lCOXWxDl99bI+aIKEJPDp
cGM4QbMwDhYN2WgUHbnaAPJOLpCvf2S+COG3HRQMarMvTX2PVujIhi44EfOPPJ5f
lf6KAddjsiCBi85x7wIDAQAB
-----END PUBLIC KEY-----`;
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publulicKey);
// const data = {
// password: '123456',
// }
let data = {
code: 200,
result: {
timestamp: 1572321851823,
inter1: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter2: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter3: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter4: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter5: [
"123123123",
"123123123",
"123123123",
"123s123123",
"123123123",
],
inter6: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
stream: {},
caton: {},
card: [],
},
};
const encrypted = encryptor.encryptLong(JSON.stringify(data));
console.log("🚀 ~ encrypted:", encrypted)

后端可以解析 but 当内容过长且出现中文时后端就会解析失败😭 例如加密输入改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
let data = {
code: 200,
result: {
timestamp: 1572321851823,
name: "宇阳",
inter1: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter2: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter3: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter4: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
inter5: [
"123123123",
"123123123",
"123123123",
"123s123123",
"123123123",
],
inter6: [
"123123123",
"123123123",
"123123123",
"123123123",
"123123123",
],
stream: {},
caton: {},
card: [],
},
};

后端解析报错

image-20240804234409275

原因排查 源码解析

image-20240805001758724

PKCS#1 v1.5 填充方案

PKCS#1 v1.5 是一种填充方案,用于确保 RSA 加密的安全性和一致性。它在加密前对数据进行填充,以确保填充后的数据长度与 RSA 密钥长度相匹配。具体填充格式如下:

1
0x00 || 0x02 || PS || 0x00 || D

代码中体现

image-20240805001918947

中文问题:

image-20240805003508983

image-20240805003620646

当出现中文时由于其占3个字符 所以会导致传入的ba数组出现负索引的情况 导致后续解密失败

https://www.npmjs.com/package/jsencrypt-ext

使用这位大哥改进的代码就可以,但是我在使用其打包构建后的文件script导入仍有问题。