摘要
JDK:1.8.0_202
# 一:前言
数据编码、数字签名、信息加密 是前后端开发都经常需要使用到的技术,应用场景包括了用户登入、交易、信息通讯、OAuth 等等,不同的应用场景也会需要使用到不同的签名加密算法,或者需要搭配不一样的 签名加密算法 来达到业务目标。下面介绍几种常见的签名加密算法和一些典型场景下的应用。主要包括:
- 数据编码:
Base64
- 散列算法(消息摘要、签名算法):
MD5/SHA/MAC
- 对称加密算法:
DES/AES/RC2/RC4
- 非对称加密算法:
RSA
# 二:数据编码
# 2.1 编码原理
Base64算法并不是加密算法,它的出现是为了解决ASCII码在传输过程中可能出现乱码的问题。Base64是网络上最常见的用于传输 8bit字节码 的可读性编码算法之一。可读性编码算法不是为了保护数据的安全性,而是为了可读性。可读性编码不改变信息内容,只改变信息内容的表现形式。Base64使用了64种字符:大写A到Z、小写a到z、数字0到9、"+"和"/"
,故得此名。
在UTF-8编码下,一个中文占3个字节;在GBK编码下,一个中文占2个字节。可以查看 汉字占多少字节
Base64编码表
索引 | 对应字符 | 索引 | 对应字符 | 索引 | 对应字符 | 索引 | 对应字符 |
---|---|---|---|---|---|---|---|
0 | A | 17 | R | 34 | i | 51 | z |
1 | B | 18 | S | 35 | j | 52 | 0 |
2 | C | 19 | T | 36 | k | 53 | 1 |
3 | D | 20 | U | 37 | l | 54 | 2 |
4 | E | 21 | V | 38 | m | 55 | 3 |
5 | F | 22 | W | 39 | n | 56 | 4 |
6 | G | 23 | X | 40 | o | 57 | 5 |
7 | H | 24 | Y | 41 | p | 58 | 6 |
8 | I | 25 | Z | 42 | q | 59 | 7 |
9 | J | 26 | a | 43 | r | 60 | 8 |
10 | K | 27 | b | 44 | s | 61 | 9 |
11 | L | 28 | c | 45 | t | 62 | + |
12 | M | 29 | d | 46 | u | 63 | / |
13 | N | 30 | e | 47 | v | ||
14 | O | 31 | f | 48 | w | ||
15 | P | 32 | g | 49 | x | ||
16 | Q | 33 | h | 50 | y |
Base64编码的过程:
- 将 字符串 转换为 字符数组;
- 将每个 字符 转换为 ASCII码;
- 将 ASCII码 转换为 8bit二进制码;
- 然后每 3个字节 为一组(一个字节为8个bit,所以每组24个bit);
- 将每组的 24个bit分为 4份,每份 6个bit;
- 在 每 6个bit 前补0,补齐 8bit(前面补0不影响数值大小);
- 然后将 每 8bit 转换为 10进制数,根据上面的Base64编码表进行转换。
上面步骤中,为什么要将每组24个bit分为4份,每份6个bit呢?因为6bit的最大值为111111,转换为十进制为63,所以6bit的取值范围为0~63,这和base64编码表长度一致。
例子
现要对 hello
这个字符串进行Base64编码,过程如下:
hello
转换为字符数组:h
、e
、l
、l
、o
;- 对应的 ASCII 码为:
104
、101
、108
、108
、111
; - 转换为 8bit 二进制数:
01101000
、01100101
、01101100
、01101100
、01101111
; - 分组,每组 24个bit(不足24个bit的用00000000补齐):
011010000110010101101100
、011011000110111100000000
; - 每组 24bit 分为4份,每份6bit:
011010
、000110
、010101
、101100
、011011 000110
、111100
、000000
; - 在 每6个bit 前补0,补齐8bit:
00011010
、00000110
、00010101
、00101100
、00011011
、00000110
、00111100
、00000000
; - 将每8bit转换为10进制数:
26
、6
、21
、44
、27
、6
、60
、0
- 从上面Base64编码表中找到十进制数对应的字符(末尾的0并不是A,而是用=等号补位):
a
、G
、V
、s
、b
、G
、8
、=
代码验证:
@Test
public void test1() {
System.out.println(Base64.getEncoder().encodeToString("hello".getBytes()));
}
2
3
4
# 2.2 URL Base64算法
Base64编码值通过URL传输会出现问题,因为Base64编码中的“+”和“/”符号是不允许出现在URL中的。同样,符号“=”用做参数分隔符,也不允许出现在URL中,根据RFC 4648中的建议,~~“”和“.”符都有可能替代“=”符号。但“”~~符号与文件系统相冲突,不能使用;如果使用“.”符号,某些文件系统认为该符号连续出现两次则为错误。所以 common codec
包下的 URL Base64 算法舍弃了填充符,使用了不定长 URL Base64 编码。
引入common codec依赖包(包含一些加解密工具类,用于增强JDK或者简化JDK相关加解密API):
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
2
3
4
5
测试:
@Test
public void test2() {
String s = "hello";
System.out.println(org.apache.commons.codec.binary.Base64.encodeBase64String(s.getBytes()));
System.out.println(org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(s.getBytes()));
}
2
3
4
5
6
# 三:散列算法
散列算法(消息摘要算法、签名算法)是单向不可逆的,无法通过加密后的散列值反推原始值,相同的内容用同样的摘要算法获得的散列值是一样的,所以常用于验证数据的完整性。该算法主要分为三大类:
- MD(Message Digest,消息摘要算法)
- MD2
- MD4
- MD5
- SHA(Secure HashAlgorithm,安全散列算法)
- SHA-1
- SHA-1的变种SHA-2系列(包含SHA-224、SHA-256、SHA-384和SHA-512)
- MAC(Message Authentication Code,消息认证码算法)综合了上述两种算法
- HmacMD5
- HmacSHA1
- HmacSHA256
- HmacSHA384
- HmacSHA512
# 3.1 MD 系列算法
MD5算法是MD系列算法的代表,由MD2、MD4等算法演变而来。无论采用哪种MD算法,结果都是32字节的16进制字符串。JDK8只支持MD2和MD5两种MD算法。
新建一个maven项目,引入common-codec依赖:
JDK8中,MD系列算法的实现是通过 MessageDigest 类来完成的,下面演示下使用JDK8原生API实现MD2和MD5加密(算法名称不区分大小写)。
import org.apache.commons.codec.binary.Hex;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MdTest {
@Test
public void test1() throws Exception {
String value = "hello";
// MD2 加密
String md2 = convert(value, "MD2");
System.out.println(md2);
System.out.println(md2.length());
// MD5 加密
String md5 = convert(value, "MD5");
System.out.println(md5);
System.out.println(md5.length());
}
/**
* MD加密 转十六进制
*
* @param value 待加密字符串
* @param algorithm 加密类型
* @return 十六进制加密内容
*/
private String convert(String value, String algorithm) throws NoSuchAlgorithmException {
// 获取 MessageDigest 实例
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
// 调用 digest 方法生成数字摘要
byte[] digest = messageDigest.digest(value.getBytes());
// 结果转换为 16 进制字符串(借助 common-codec Hex类)
return Hex.encodeHexString(digest);
}
}
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
common-codec 的 DigestUtils 也提供了MD2和MD5算法相关方法,不过他们并没有自己实现相关算法,而是对JDK原生API进行封装,使用起来更方便。
import org.apache.commons.codec.digest.DigestUtils;
public class MdTest {
@Test
public void test2() {
String value = "hello";
System.out.println(DigestUtils.md2Hex(value.getBytes()));
System.out.println(DigestUtils.md5Hex(value.getBytes()));
}
}
2
3
4
5
6
7
8
9
10
# 3.2 SHA 系列算法
SHA 算法是基于 MD4 算法实现的,作为 MD 算法的继任者,成为了新一代的消息摘要算法的代表。SHA 与 MD 算法不同之处主要在于摘要长度,SHA 算法的摘要更长,安全性更高。
SHA 算法家族目前共有 SHA-1
、SHA-224
、SHA-256
、SHA-384
和 SHA-512
五种算法,通常将后四种算法并称为 SHA-2 算法。JDK8 支持上述五种算法,其中 SHA
的写法等价于 SHA-1
。JDK8中,SHA 系列算法的实现也是通过 MessageDigest 类来完成的:
import org.apache.commons.codec.binary.Hex;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class ShaTest {
@Test
public void test1() throws NoSuchAlgorithmException {
String value = "hello";
MessageDigest m1 = MessageDigest.getInstance("SHA-1");
System.out.println(Hex.encodeHexString(m1.digest(value.getBytes())));
MessageDigest m2 = MessageDigest.getInstance("SHA-256");
System.out.println(Hex.encodeHexString(m2.digest(value.getBytes())));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
common-codec同样也提供了SHA相关算法的API:
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.Test;
public class ShaTest {
@Test
public void test2() {
String value = "hello";
String sha1Hex = DigestUtils.sha1Hex(value);
System.out.println(sha1Hex);
String sha256Hex = DigestUtils.sha256Hex(value);
System.out.println(sha256Hex);
String sha384Hex = DigestUtils.sha384Hex(value);
System.out.println(sha384Hex);
String sha512Hex = DigestUtils.sha512Hex(value);
System.out.println(sha512Hex);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3.3 MAC 系列算法
MAC 算法结合了 MD 和 SHA 算法的优势,并加入秘钥的支持,是一种更为安全的消息摘要算法。因为 MAC 算法融合了秘钥散列函数(keyed-Hash),通常也把 MAC 称为 HMAC(keyed-Hash Message Authentication Code)。
MAC 算法主要集合了 MD 和 SHA 两大系列消息摘要算法。MD 系列算法有 HmacMD2
、HmacMD4
和 HmacMD5
三种算法;SHA 系列算法有 HmacSHA1
、HmacSHA224
、HmacSHA256
、HmacSHA384
和 HmacSHA512
五种算法。
JDK8支持了 HmacMD5
、HmacSHA1
、HmacSHA224
、HmacSHA256
、HmacSHA384
和 HmacSHA512
这六种MAC算法,通过 Mac 类实现。
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class MacTest {
@Test
public void test() throws Exception {
String value = "hello";
// JDK支持 HmacMD5、HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384 和 HmacSHA512 六种算法
String algorithm = "HmacMD5";
// 初始化KeyGenerator
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 构建秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 获得秘钥
byte[] key = secretKey.getEncoded();
// 还原秘钥
SecretKeySpec secretKeySpec = new SecretKeySpec(key, algorithm);
// 打印秘钥
System.out.println(Base64.encodeBase64String(secretKeySpec.getEncoded()));
// 实例化Mac
Mac mac = Mac.getInstance(algorithm);
// 初始化Mac
mac.init(secretKeySpec);
// 获取消息摘要
byte[] bytes = mac.doFinal(value.getBytes());
// 转换为16进制
System.out.println(Hex.encodeHexString(bytes));
}
}
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
# 四:对称加密算法
对称加密算法加密和解密使用的是同一份秘钥,解密是加密的逆运算。对称加密算法加密速度快,密文可逆,一旦秘钥文件泄露,就会导致原始数据暴露。对称加密的结果一般使用 Base64 算法编码,便于阅读和传输。JDK8支持的对称加密算法主要有 DES
、DESede
、AES
、Blowfish
,以及 RC2
和 RC4
等。不同的算法秘钥长度不同,秘钥长度越长,加密安全性越高。
# 4.1 DES
DES(Data Encryption Standard,数据加密标准)算法是对称加密算法领域中的典型算法,DES 算法秘钥较短,以现在计算机的计算能力,DES 算法加密的数据在24小时内可能被破解。所以 DES 算法已经被淘汰,建议使用 AES 算法,不过这里还是简单了解下。
JDK8仅支持 56 位长度的 DES秘钥
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.util.Base64;
public class DesTest {
@Test
public void test() throws Exception {
String value = "helloWorld";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "DES";
// 转换模式
String transformation = "DES";
// --- 生成秘钥 ---
// 实例化秘钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 初始化秘钥长度
keyGenerator.init(56);
// 生成秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 实例化DES秘钥材料
DESKeySpec desKeySpec = new DESKeySpec(secretKey.getEncoded());
// 实例化秘钥工厂
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
// 生成DES秘钥
SecretKey desSecretKey = secretKeyFactory.generateSecret(desKeySpec);
System.out.println("DES秘钥:" + Base64.getEncoder().encodeToString(desSecretKey.getEncoded()));
// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
cipher.init(Cipher.ENCRYPT_MODE, desSecretKey);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("DES加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, desSecretKey);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("DES解密结果:" + new String(decrypt));
}
}
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
上面步骤总结为如下几步:
- 生成加密秘钥; 1.1 通过 keyGenerator 生成一个指定位数的秘钥; 1.2 通过上面生成的秘钥实例化算法对应的秘钥材料 KeySpec; 1.3 使用秘钥材料通过秘钥工厂 SecretKeyFactory 生成算法秘钥 SecretKey。
- 通过转换模式实例化 Cipher;
- 指定 Cipher 模式和秘钥,进行加解密操作。
如果在生成秘钥的时候,不指定为 56 位,则会抛出 java.security.InvalidParameterException: Wrong keysize: must be equal to 56
异常。
# 4.2 DESede
作为 DES 算法的一种改良,DESede 算法(也称为 3DES,三重DES)针对其秘钥长度偏短和迭代次数偏少等问题做了相应改进,提高了安全强度,但同时也造成处理速度较慢、秘钥计算时间加长、加密效率不高的问题。所以这里还是简单了解下,实际还是推荐用AES。
JDK8支持 112位或 168位长度的DESede秘钥
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import java.util.Base64;
public class DESedeTest {
@Test
public void test() throws Exception {
String value = "helloWorld";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "DESede";
// 转换模式
String transformation = "DESede";
// --- 生成秘钥 ---
// 实例化秘钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 初始化秘钥长度
keyGenerator.init(112);
// 生成秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 实例化DESede秘钥材料
DESedeKeySpec desKeySpec = new DESedeKeySpec(secretKey.getEncoded());
// 实例化秘钥工厂
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
// 生成DES秘钥
SecretKey desSecretKey = secretKeyFactory.generateSecret(desKeySpec);
System.out.println("DESede秘钥:" + Base64.getEncoder().encodeToString(desSecretKey.getEncoded()));
// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
cipher.init(Cipher.ENCRYPT_MODE, desSecretKey);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("DESede加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, desSecretKey);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("DESede解密结果:" + new String(decrypt));
}
}
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
如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Wrong keysize: must be equal to 112 or 168
异常。
# 4.3 AES
AES(AdvancedEncryption Standard,高级数据加密标准)算法支持 128 位、192 位和256 位的秘钥长度,加密速度比 DES 和 DESede 都快,至今还没有被破解的报道。经过验证,目前采用的 AES 算法能够有效抵御已知的针对 DES 算法的所有攻击方法,如部分差分攻击、相关秘钥攻击等。AES 算法因秘钥建立时间短、灵敏性好、内存需求低等优点,在各个领域得到广泛的研究与应用。
JDK8支持 128 位、192 位和256 位长度的AES秘钥
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AesTest {
@Test
public void test() throws Exception {
String value = "helloWorld";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "AES";
// 转换模式
String transformation = "AES";
// --- 生成秘钥 ---
// 实例化秘钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 初始化秘钥长度
keyGenerator.init(256);
// 生成秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 生成秘钥材料
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
System.out.println("AES秘钥:" + Base64.getEncoder().encodeToString(secretKey.getEncoded()));
// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("AES加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("AES解密结果:" + new String(decrypt));
}
}
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
如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256
异常。
# 4.4 RC2、RC4
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class TcTest {
@Test
public void test() throws Exception {
String value = "helloWorld";
convert(value, "RC2", "RC2");
System.out.println("============================");
convert(value, "RC4", "RC4");
}
/**
* 加密
*
* @param value 待加密值
* @param algorithm 加密算法
* @param transformation 转换模式
*/
private void convert(String value, String algorithm, String transformation) throws Exception {
System.out.println("待加密值:" + value);
// --- 生成秘钥 ---
// 实例化秘钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 初始化秘钥长度
keyGenerator.init(666);
// 生成秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 生成秘钥材料
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
System.out.println(algorithm + "秘钥:" + Base64.getEncoder().encodeToString(secretKey.getEncoded()));
// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println(algorithm + "加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println(transformation + "解密结果:" + new String(decrypt));
}
}
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
如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Key length for RC2 must be between 40 and 1024 bits
异常。
# 4.5 Blowfish
Blowfish 算法也可以用于替换 DES,Blowfish 算法的秘钥长度范围为 32到448位,并且必须为8的倍数。
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class BlowfishTest {
@Test
public void test() throws Exception {
String value = "helloWorld";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "Blowfish";
// 转换模式
String transformation = "Blowfish";
// --- 生成秘钥 ---
// 实例化秘钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 初始化秘钥长度
keyGenerator.init(128);
// 生成秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 生成秘钥材料
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
System.out.println("Blowfish秘钥:" + Base64.getEncoder().encodeToString(secretKey.getEncoded()));
// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("Blowfish加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("Blowfish解密结果:" + new String(decrypt));
}
}
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
如果指定不合法的秘钥长度,程序将抛出 java.security.InvalidParameterException: Keysize must be multiple of 8, and can only range from 32 to 448 (inclusive)
异常。
# 五:非对称加密算法
非对称加密和对称加密算法相比,多了一把秘钥,为双秘钥模式,一个公开称为公钥,一个保密称为私钥。遵循公钥加密私钥解密,或者私钥加密公钥解密。非对称加密算法源于DH算法,后又有基于椭圆曲线加密算法的密钥交换算法ECDH,不过目前最为流行的非对称加密算法是RSA,本文简单记录下RSA的使用。
# 5.1 RSA 算法
RSA算法是最为典型的非对称加密算法,该算法由美国麻省理工学院(MIT)的Ron Rivest、Adi Shamir和Leonard Adleman三位学者提出,并以这三位学者的姓氏开头字母命名,称为RSA算法。
RSA算法的数据交换过程分为如下几步:
- A构建RSA秘钥对;
- A向B发布公钥;
- A用私钥加密数据发给B;
- B用公钥解密数据;
- B用公钥加密数据发给A;
- A用私钥解密数据。
JDK8支持RSA算法:
算法 | 秘钥长度 | 加密模式 | 填充模式 |
---|---|---|---|
RSA | 512~16384位,64倍数 | ECB | NoPadding PKCS1Padding OAEPWithMD5AndMGF1Padding OAEPWithSHA1AndMGF1Padding OAEPWithSHA-1AndMGF1Padding OAEPWithSHA-224AndMGF1Padding OAEPWithSHA-256AndMGF1Padding OAEPWithSHA-384AndMGF1Padding OAEPWithSHA-512AndMGF1Padding OAEPWithSHA-512/224AndMGF1Padding OAEPWithSHA-512/2256ndMGF1Padding |
import javax.crypto.Cipher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
public class RsaTest {
@Test
public void test1() throws Exception {
String value = "helloWorld";
// 加密算法
String algorithm = "RSA";
// 转换模式
String transform = "RSA/ECB/PKCS1Padding";
// 实例化秘钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 初始化,秘钥长度512~16384位,64倍数
keyPairGenerator.initialize(512);
// 生成秘钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥
PublicKey publicKey = keyPair.getPublic();
System.out.println("RSA公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
// 私钥
PrivateKey privateKey = keyPair.getPrivate();
System.out.println("RSA私钥: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));
// ------ 测试公钥加密,私钥解密 ------
Cipher cipher = Cipher.getInstance(transform);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] pubEncryptBytes = cipher.doFinal(value.getBytes());
System.out.println("RSA公钥加密后数据: " + Base64.getEncoder().encodeToString(pubEncryptBytes));
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] priDecryptBytes = cipher.doFinal(pubEncryptBytes);
System.out.println("RSA私钥解密后数据: " + new String(priDecryptBytes));
// ------ 测试私钥加密,公钥解密 ------
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] priEncryptBytes = cipher.doFinal(value.getBytes());
System.out.println("RSA私钥加密后数据: " + Base64.getEncoder().encodeToString(priEncryptBytes));
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] pubDecryptBytes = cipher.doFinal(priEncryptBytes);
System.out.println("RSA公钥解密后数据: " + new String(pubDecryptBytes));
}
}
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
如果拥有RSA公钥或私钥,需要将它们还原为 PublicKey 和 PrivateKey 对象
@Test
public void test2() throws Exception {
String value = "hw";
// 加密算法
String algorithm = "RSA";
// 转换模式
String transform = "RSA/ECB/PKCS1Padding";
// RSA公钥BASE64字符串
String rsaPublicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANRWZfv5tdLJBtI8/L5uZuHpiLalxcmvwuTkVK5TUQXMrkqBdBSOcC+WFTHNXAggrJMWEopwSzgYATJd9jb6EHkCAwEAAQ==";
// RSA私钥BASE64字符串
String rsaPrivateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA1FZl+/m10skG0jz8vm5m4emItqXFya/C5ORUrlNRBcyuSoF0FI5wL5YVMc1cCCCskxYSinBLOBgBMl32NvoQeQIDAQABAkEAozGYBjYgQVWRcYm/8pglaGG1WjNENUNpdcPrNWQBdIM5Y93PFeDg+/D0fZvT+WlH53vlDLuShwZuDMUx4PqApQIhAPVafQOrhbqh36WJwav1mtkokRhW3kumlGjnbL8+xUrTAiEA3Y0m1nAUGtafDKbW6IHSq+fvntxd6WAOeTzaMqnvEAMCIBtxDX51lrVzGXKIX9L9213ifaf9P0uyy/KXv7/8I1DlAiBeMGIwjFmfx1q68Dsxge/ksahHq3wpeXLtzBcfrus5rQIhAIpifvoyqLf8MJAftohw9Lu+pTUqKc+UQSvj2SdQ3ZJv";
// ------- 还原公钥 --------
byte[] publicKeyBytes = Base64.getDecoder().decode(rsaPublicKey);
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
// ------- 还原私钥 --------
byte[] privateKeyBytes = Base64.getDecoder().decode(rsaPrivateKey);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
// ------- 测试加解密 --------
Cipher cipher = Cipher.getInstance(transform);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] pubEncryptBytes = cipher.doFinal(value.getBytes());
System.out.println("RSA公钥加密数据: " + Base64.getEncoder().encodeToString(pubEncryptBytes));
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] priDecryptBytes = cipher.doFinal(pubEncryptBytes);
System.out.println("RSA私钥解密数据: " + new String(priDecryptBytes));
}
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
# 5.2 分段加解密
RSA加解密中必须考虑到的密钥长度、明文长度和密文长度问题。明文长度需要小于密钥长度,而密文长度则等于密钥长度。因此当加密内容长度大于密钥长度时,有效的RSA加解密就需要对内容进行分段。
这是因为,RSA算法本身要求加密内容也就是明文长度m必须满足 0<m<密钥长度n
。如果小于这个长度就需要进行padding,因为如果没有padding,就无法确定解密后内容的真实长度,字符串之类的内容问题还不大,以0作为结束符,但对二进制数据就很难,因为不确定后面的0是内容还是内容结束符。而只要用到padding,那么就要占用实际的明文长度,于是实际明文长度需要减去padding字节长度。我们一般使用的padding标准有 NoPPadding
、OAEPPadding
、PKCS1Padding
等,其中PKCS#1建议的padding就占用了11个字节。
以秘钥长度为1024bits为例:
@Test
public void test3() throws Exception {
StringBuilder value = new StringBuilder();
for (int i = 0; i <= 29; i++) {
value.append("18cm");
}
System.out.println("待加密内容长度: " + value.toString().length());
// 加密算法
String algorithm = "RSA";
// 转换模式
String transform = "RSA/ECB/PKCS1Padding";
// 实例化秘钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 初始化,秘钥长度512~16384位,64倍数
keyPairGenerator.initialize(1024);
// 生成秘钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥
PublicKey publicKey = keyPair.getPublic();
System.out.println("RSA公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
// 私钥
PrivateKey privateKey = keyPair.getPrivate();
System.out.println("RSA私钥: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));
// ------ 测试公钥加密,私钥解密 ------
Cipher cipher = Cipher.getInstance(transform);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] pubEncryptBytes = cipher.doFinal(value.toString().getBytes());
System.out.println("RSA公钥加密后数据: " + Base64.getEncoder().encodeToString(pubEncryptBytes));
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] priDecryptBytes = cipher.doFinal(pubEncryptBytes);
System.out.println("RSA私钥解密后数据: " + new String(priDecryptBytes));
// ------ 测试私钥加密,公钥解密 ------
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] priEncryptBytes = cipher.doFinal(value.toString().getBytes());
System.out.println("RSA私钥加密后数据: " + Base64.getEncoder().encodeToString(priEncryptBytes));
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] pubDecryptBytes = cipher.doFinal(priEncryptBytes);
System.out.println("RSA公钥解密后数据: " + new String(pubDecryptBytes));
}
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
对于1024长度的密钥。128字节(1024bits/8)减去PKCS#1建议的padding就占用了11个字节正好是117字节。所以加密的明文长度120字节大于117字节,程序抛出了异常。
要解决这个问题,可以采用分段加密的手段。编写一个分段加解密的工具类:
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
/**
* RSA分段加解密
* 针对秘钥长度为1024bits
*/
public class RsaUtil {
// 最大加密块长度 1024/8 - 11
private static final int MAX_ENCRYPT_BLOCK = 117;
// 最大解密块长度 1024/8
private static final int MAX_DECRYPT_BLOCK = 128;
private static final String TRANSFORM = "RSA/ECB/PKCS1Padding";
/**
* 公钥加密
*
* @param publicKey 公钥
* @param value 待加密值
* @return 加密值
*/
public static String encrypt(PublicKey publicKey, String value) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Cipher cipher = Cipher.getInstance(TRANSFORM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bytes = value.getBytes();
int length = bytes.length;
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (length - offSet > 0) {
if (length - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(bytes, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(bytes, offSet, length - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
return Base64.getEncoder().encodeToString(encryptedData);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 私钥解密
*
* @param privateKey 私钥
* @param encrypt 带解密值
* @return 解密值
*/
public static String decrypt(PrivateKey privateKey, String encrypt) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Cipher cipher = Cipher.getInstance(TRANSFORM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bytes = Base64.getDecoder().decode(encrypt);
int length = bytes.length;
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (length - offSet > 0) {
if (length - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(bytes, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(bytes, offSet, length - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
return out.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
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
测试:
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;
public class RsaUtilTest {
@Test
public void test() throws Exception {
StringBuilder value = new StringBuilder();
for (int i = 0; i <= 29; i++) {
value.append("18cm");
}
System.out.println("待加密内容长度: " + value.toString().length());
// 加密算法
String algorithm = "RSA";
// 实例化秘钥对生成器
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithm);
// 初始化,秘钥长度512~16384位,64倍数
keyPairGenerator.initialize(1024);
// 生成秘钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 公钥
PublicKey publicKey = keyPair.getPublic();
System.out.println("RSA公钥: " + Base64.getEncoder().encodeToString(publicKey.getEncoded()));
// 私钥
PrivateKey privateKey = keyPair.getPrivate();
System.out.println("RSA私钥: " + Base64.getEncoder().encodeToString(privateKey.getEncoded()));
// ------ 测试公钥加密,私钥解密 ------
String pubEncrypt = RsaUtil.encrypt(publicKey, value.toString());
System.out.println("RSA公钥加密后数据: " + pubEncrypt);
String priDecrypt = RsaUtil.decrypt(privateKey, pubEncrypt);
System.out.println("RSA私钥解密后数据: " + priDecrypt);
}
}
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
# 5.3 建议
- 公钥是通过A发送给B的,其在传递过程中很有可能被截获,也就是说窃听者很有可能获得公钥。如果窃听者获得了公钥,向A发送数据,A是无法辨别消息的真伪的。因此,虽然可以使用公钥对数据加密,但这种方式还是会有存在一定的安全隐患。如果要建立更安全的加密消息传递模型,就需要AB双方构建两套非对称加密算法密钥,仅遵循“私钥加密,公钥解密”的方式进行加密消息传递;
- RSA不适合加密过长的数据,虽然可以通过分段加密手段解决,但过长的数据加解密耗时较长,在响应速度要求较高的情况下慎用。一般推荐使用非对称加密算法传输对称加密秘钥,双方数据加密用对称加密算法加解密。