是五月呀!

非对称加密与RSA算法

非对称加密

公开密钥加密使用一对非对称的密钥。一把叫做 私有密钥(private key),另一把叫做 公开密钥(public key)。
顾名思义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得。
使用公开密钥机密方式,发送密文的一方使用对方的公开密钥进行机密处理,对方收到被加密的信息后,再使用自己的私有密钥进行解密。利用这种方式,不需要发送用来解密的私有密钥,也不必担心密钥被攻击者窃听而盗走。

公钥加密

假设一下,我找了两个数字,一个是1,一个是2。我喜欢2这个数字,就保留起来,不告诉你们(私钥),然后我告诉大家,1是我的公钥。

我有一个文件,不能让别人看,我就用1加密了。别人找到了这个文件,但是他不知道2就是解密的私钥啊,所以他解不开,只有我可以用数字2,就是我的私钥,来解密。这样我就可以保护数据了。

我的好朋友x用我的公钥1加密了字符a,加密后成了b,放在网上。别人偷到了这个文件,但是别人解不开,因为别人不知道2就是我的私钥,只有我才能解密,解密后就得到a。这样,我们就可以传送加密的数据了。

私钥签名

如果我用私钥加密一段数据(当然只有我可以用私钥加密,因为只有我知道2是我的私钥),结果所有的人都看到我的内容了,因为他们都知道我的公钥是1,那么这种加密有什么用处呢?

但是我的好朋友x说有人冒充我给他发信。怎么办呢?我把我要发的信,内容是c,用我的私钥2,加密,加密后的内容是d,发给x,再告诉他解密看是不是c。他用我的公钥1解密,发现果然是c。
这个时候,他会想到,能够用我的公钥解密的数据,必然是用我的私钥加的密。只有我知道我得私钥,因此他就可以确认确实是我发的东西。
这样我们就能确认发送方身份了。这个过程叫做数字签名。当然具体的过程要稍微复杂一些。用私钥来加密数据,用途就是数字签名。

总结:

  • 公钥和私钥是成对的,它们互相解密。
  • 公钥加密,私钥解密。
  • 私钥数字签名,公钥验证。

举例

比如有两个用户Alice和Bob,Alice想把一段明文通过双钥加密的技术发送给Bob,Bob有一对公钥和私钥,那么加密解密的过程如下:

  1. Bob将他的公开密钥传送给Alice。
  2. Alice用Bob的公开密钥加密她的消息,然后传送给Bob。
  3. Bob用他的私人密钥解密Alice的消息。

上面的过程可以用下图表示,Alice使用Bob的公钥进行加密,Bob用自己的私钥进行解密。

RSA算法

RSA公钥加密算法是1977年由Ron Rivest、Adi Shamirh和LenAdleman在(美国麻省理工学院)开发的。RSA取名来自开发他们三者的名字。RSA是目前最有影响力的公钥加密算法,它能够抵抗到目前为止已知的所有密码攻击,已被ISO推荐为公钥数据加密标准。RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

密钥对与分块大小

RAS密钥对包含一个私钥和一个公钥。密钥长度指的是生成密钥对时模值的位数,主流可选:1024bit,2048bit等,位数越长,复杂度越高,但加解密越耗时。
下面以1024bit的密钥模值为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 生成公私密钥对
*/
public static Tuple<String, String> getRSAKey() {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
//设置密钥对的bit数,越大越安全,但速度减慢
generator.initialize(1024);
KeyPair keyPair = generator.generateKeyPair();
// 获取公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 获取私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 将密钥对封装为Map
return Tuple.of(Base64.encodeBase64String(publicKey.getEncoded()), Base64.encodeBase64String(privateKey.getEncoded()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}

对于长度为1024的加密key,RSA加密明文最大长度117字节,解密要求密文最大长度为128字节,所以在加密和解密的过程中需要分块进行。
为什么是明文长度最长是117字节?
RSA实际可加密的明文长度最大也是1024bits,与生成密钥时的模值等长。但是我们加密的明文可能小于1024位,就需要使用padding标示。
只要用到padding,那么就要占用实际的明文长度,于是才有117字节的说法。我们一般使用的padding标准有NoPPadding、OAEPPadding、PKCS1Padding等,其中PKCS#1建议的padding就占用了11个字节。
这样,128字节(1024bits)-减去11字节正好是117字节,但对于RSA加密来讲,padding也是参与加密的,所以,依然按照1024bits去理解,但实际的明文只有117字节了。
密文长度就是给定符合条件的明文加密出来的结果位长,这个可以确定,加密后的密文位长跟密钥的位长度是相同的,即128字节(1024bit)。

那如果明文长度超过了128字节怎么办?就需要分块加密、解密,下面会介绍。

分段加解密

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
//定义加密方式
private static final String KEY_RSA = "RSA";
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* 公钥加密
*
* @param encryptingStr 明文
* @param publicKeyStr 公钥
* @return 密文
*/
public static String encryptByPublic(String encryptingStr, String publicKeyStr) {
try {
// 将公钥由字符串转为UTF-8格式的字节数组
byte[] publicKeyBytes = Base64.decodeBase64(publicKeyStr);
// 获得公钥
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
PublicKey publicKey = factory.generatePublic(keySpec);
// 对数据加密
Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 取得待加密数据
byte[] data = encryptingStr.getBytes(StandardCharsets.UTF_8);
byte[] resultBytes = cipherDoFinal(cipher, data, MAX_ENCRYPT_BLOCK);
// 返回加密后由Base64编码的加密信息
return Base64.encodeBase64String(resultBytes);
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
return null;
}
/**
* 私钥解密
*
* @param encryptedStr 密文
* @param privateKeyStr 密钥
* @return 明文
*/
public static String decryptByPrivate(String encryptedStr, String privateKeyStr) {
try {
// 对私钥解密
byte[] privateKeyBytes = Base64.decodeBase64(privateKeyStr);
// 获得私钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory factory = KeyFactory.getInstance(KEY_RSA);
PrivateKey privateKey = factory.generatePrivate(keySpec);
// 对数据解密
Cipher cipher = Cipher.getInstance(factory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 获得待解密数据
byte[] data = Base64.decodeBase64(encryptedStr);
byte[] resultBytes = cipherDoFinal(cipher, data, MAX_DECRYPT_BLOCK);
// 返回UTF-8编码的解密信息
return new String(resultBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
LOGGER.error(e.getMessage());
}
return null;
}
/**
* 分段加解密
*
* @param cipher Cipher实例(加密或者解密)
* @param srcBytes 输入数据字节数组
* @param maxSegmentSize 最大分段大小(加密117,解密128)
* @return 处理后数据
*/
private static byte[] cipherDoFinal(Cipher cipher, byte[] srcBytes, int maxSegmentSize)
throws IllegalBlockSizeException, BadPaddingException, IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int inputLen = srcBytes.length;
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > maxSegmentSize) {
cache = cipher.doFinal(srcBytes, offSet, maxSegmentSize);
} else {
cache = cipher.doFinal(srcBytes, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * maxSegmentSize;
}
byte[] data = out.toByteArray();
out.close();
return data;
}

参考资料:
公钥,私钥和数字签名这样最好理解
java RSA加密解密实现(含分段加密)