通过RSA算法加密(解密)消息 以及 web应用前后端传递RSA消息样例工程

RSA

RSA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。

非对称加密算法需要两个密钥: 公开密钥(publicKey)和私有密钥(privateKey)。
公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;
如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。

后端(Java)秘钥生成

JDK提供了秘钥生成API,在这里需要对其进行封装,如下:

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
private static final Integer KEY_SIZE_DEFAULT = 4096;

public static class MyKeyPair {
public PrivateKey privateKey;
public PublicKey publicKey;
}

/**
* Create public/private key pair.
*
* @param keySize size of the key
* @return {@link MyKeyPair}
*/
public static synchronized MyKeyPair createKeyPair(Integer keySize) {
if (keySize == null) {
keySize = KEY_SIZE_DEFAULT;
}
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(keySize, new SecureRandom(UUID.randomUUID().toString().replaceAll("-", "").getBytes()));
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey publicKey = keyPair.getPublic();
MyKeyPair myKeyPair = new MyKeyPair();
myKeyPair.publicKey = publicKey;
myKeyPair.privateKey = privateKey;
return myKeyPair;
}

说明:

  • 参数 keySize 决定了生成的秘钥对的长度,秘钥长度越长,破解难度越大,相应的消息加解密速度也会越慢。

后端(Java)公(私)钥加(解)密

利用生成的秘钥对进行加解密操作,如下:

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/**
* Encrypt source by public key.
*
* @param sourceBytes source
* @param publicKey {@link PublicKey}
* @return encrypted data
*/
public static byte[] encryptByPublicKey(byte[] sourceBytes, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
int pageSize = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8 - 11;//该密钥能够加密的最大字节长度
List<Byte[]> bytesList = split(sourceBytes, pageSize);
List<Byte> result = new ArrayList<>();
for (Byte[] bytes : bytesList) {
result.addAll(convert(cipher.doFinal(copy(bytes))));
}
return convert(result);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
return null;
}

/**
* Encrypt source by private key.
*
* @param sourceBytes source
* @param privateKey {@link PrivateKey}
* @return encrypted data
*/
public static byte[] encryptByPrivateKey(byte[] sourceBytes, PrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
int pageSize = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8 - 11;
List<Byte[]> bytesList = split(sourceBytes, pageSize);
List<Byte> result = new ArrayList<>();
for (Byte[] bytes : bytesList) {
result.addAll(convert(cipher.doFinal(copy(bytes))));
}
return convert(result);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
return null;
}

/**
* Decrypt by public key
*
* @param encryptedSource encrypted source
* @param publicKey {@link PublicKey}
* @return source
*/
public static byte[] decryptByPublicKey(byte[] encryptedSource, PublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
int pageSize = ((RSAPublicKey) publicKey).getModulus().bitLength() / 8;//该密钥能够解密的最大字节长度
List<Byte[]> bytesList = split(encryptedSource, pageSize);
List<Byte> result = new ArrayList<>();
for (Byte[] bytes : bytesList) {
result.addAll(convert(cipher.doFinal(copy(bytes))));
}
return convert(result);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
return null;
}

/**
* Decrypt by private key.
*
* @param encryptedSource encrypt source
* @param privateKey {@link PrivateKey}
* @return source
*/
public static byte[] decryptByPrivateKey(byte[] encryptedSource, PrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
int pageSize = ((RSAPrivateKey) privateKey).getModulus().bitLength() / 8;
List<Byte[]> bytesList = split(encryptedSource, pageSize);
List<Byte> result = new ArrayList<>();
for (Byte[] bytes : bytesList) {
result.addAll(convert(cipher.doFinal(copy(bytes))));
}
return convert(result);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
return null;
}

private static List<Byte[]> split(byte[] bytes, int pageSize) {
int remain = bytes.length % pageSize;
int pages = remain != 0 ? bytes.length / pageSize + 1 : bytes.length / pageSize;
List<Byte[]> bytesList = new ArrayList<>();
Byte[] temp;
for (int page = 0; page < pages; page++) {
if (page == pages - 1 && remain != 0) {
temp = new Byte[remain];
System.arraycopy(copy(bytes), page * pageSize, temp, 0, remain);
} else {
temp = new Byte[pageSize];
System.arraycopy(copy(bytes), page * pageSize, temp, 0, pageSize);
}
bytesList.add(temp);
}
return bytesList;
}

private static Byte[] copy(byte[] bytes) {
Byte[] bytesU = new Byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
bytesU[i] = bytes[i];
}
return bytesU;
}

private static byte[] copy(Byte[] bytes) {
byte[] bytesL = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
bytesL[i] = bytes[i];
}
return bytesL;
}

private static byte[] convert(List<Byte> byteList) {
byte[] bytes = new byte[byteList.size()];
for (int i = 0; i < byteList.size(); i++) {
bytes[i] = byteList.get(i);
}
return bytes;
}

private static List<Byte> convert(byte[] bytes) {
List<Byte> byteList = new ArrayList<>(bytes.length);
for (byte b : bytes) {
byteList.add(b);
}
return byteList;
}

说明:

  • RSA算法对消息一次处理的最大字节长度是有限制的,秘钥长度越长,单次加解密的字节数越多,因此在加解密过程中,建议使用分段加解密的方式处理原始数据。在上述例程中使用 split 方法将长串数据分解成小段处理。
  • 上述方法经过公钥加密的数据需要通过私钥解密;通过私钥加密的数据需要通过公钥解密。
  • 上述方法传参,返回值数据均为字节数组格式,防止因编码方式不同出现的加解密失败情况出现,用户可以根据实际需要将字节转换成其他类型(如Base64字串)。

前端(JavaScript)公(私)钥加(解)密

前端(JavaScript)加解密使用jsencrypt库,github, npm安装如下:

1
npm install jsencrypt save

使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ----- publicKey 加密
var enc = new JSEncrypt();
enc.setPublicKey("------publicKey Base64-----");
var encryptedText = enc.encrypt("text");

// ----- publicKey 解密
var enc = new JSEncrypt();
enc.setPublicKey("------publicKey Base64-----");
var decryptTest = enc.decrypt("text");

// ----- privateKey 加密
var enc = new JSEncrypt();
enc.setPrivateKey("------privateKey Base64-----");
var encryptedText = enc.encrypt("text");

// ----- privateKey 解密
var enc = new JSEncrypt();
enc.setPrivateKey("------privateKey Base64-----");
var decryptTest = enc.decrypt("text");

说明

  • 公钥、私钥传参格式是经过base64编码的字串
  • 加密、解密返回值是经过base64编码的字串

样例工程介绍

rsa-message-transport

工程地址:
rsa-message-transport

架构

使用SpringBoot构建的微型web应用,包括一个web页面,目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
|- src
|- main
|- bin 启停脚本
|- java java源码
|- resources 应用配置文件
|- test
|- java 单元测试java源码
|- web
|- lib 前端依赖库
|- index.xml web主页
|- assembly maven工程打包配置文件
|- pom.xml maven工程配置文件

用法(依赖 maven, jdk)

构建

1
mvn clean package -Dmaven.test.skip=true

运行

1
2
3
- 解压 target/rsa-test.tar.gz
- 进入解压后目录
- 运行脚本 bin/server.sh start

访问

1
2
浏览器访问:
http://127.0.0.1:9500/

启动入口:

1
com.github.johnsonmoon.rsaencryptionanddecryption.Main

RSA秘钥对定时刷新任务

1
2
3
com.github.johnsonmoon.rsaencryptionanddecryption.task.RSAKeyRefreshTask

该任务自应用启动开始每隔十分钟重新调用方法生成新的秘钥对,并储存在应用缓存中

restful-service api

1
2
3
4
5
com.github.johnsonmoon.rsaencryptionanddecryption.controller.BaseController

该类定义了两个api:
- 获取公钥接口
- 上传加密信息,返回解密结果接口

前后端交互

交互时序图如下:
image

说明:

  • 用户填写测试字串,点击测试
  • 前端请求公钥
  • 后端返回公钥
  • 前端获取到公钥,使用公钥对测试字串进行加密
  • 前端将加密数据通过请求发送至后端
  • 后端接收到加密数据,使用私钥进行解密,并返回解密后字串
  • 前端获取后端响应,将结果显示至页面

运行展示

image