Java 对称加密算法

Java 对称加密算法

对称加密算法加密和解密使用的是同一份秘钥,解密是加密的逆运算。对称加密算法加密速度快,密文可逆,一旦秘钥文件泄露,就会导致原始数据暴露。对称加密的结果一般使用Base64算法编码,便于阅读和传输。JDK8支持的对称加密算法主要有DES、DESede、AES、Blowfish,以及RC2和RC4等。不同的算法秘钥长度不同,秘钥长度越长,加密安全性越高。

DES

DES(Data Encryption Standard,数据加密标准)算法是对称加密算法领域中的典型算法,DES算法秘钥较短,以现在计算机的计算能力,DES算法加密的数据在24小时内可能被破解。所以DES算法已经被淘汰,建议使用AES算法,不过这里还是简单了解下。

JDK8仅支持56位长度的DES秘钥,下面举个JDK8实现DES加密的例子:

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
import org.junit.Test;

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 Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
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));
}
}

转换模式transformation这里先设置为和加密算法一样,下面介绍了加密模式和填充模式后再作说明。

上面步骤看着挺多,其实可以总结为如下几步:

  1. 生成加密秘钥;

    1.1. 通过KeyGenerator生成一个指定位数的秘钥;

    1.2. 通过上面生成的秘钥实例化算法对应的秘钥材料KeySpec;

    1.3. 使用秘钥材料通过秘钥工厂SecretKeyFactory生成算法秘钥SecretKey。

  2. 通过转换模式实例化Cipher;

  3. 指定Cipher模式和秘钥,进行加解密操作。

运行结果如下:

1
2
3
4
待加密值:mrbird's blog
DES秘钥:LO/CCEC8y8E=
DES加密结果:hRZdN6f+6yj/lhPWUPsayA==
DES解密结果:mrbird's blog

如果在生成秘钥的时候,不指定为56位,则会抛出java.security.InvalidParameterException: Wrong keysize: must be equal to 56异常。

DESede

作为DES算法的一种改良,DESede算法(也成为3DES,三重DES)针对其秘钥长度偏短和迭代次数偏少等问题做了相应改进,提高了安全强度,但同时也造成处理速度较慢、秘钥计算时间加长、加密效率不高的问题。所以这里还是简单了解下,实际还是推荐用AES。

JDK8支持112位或168位长度的DESede秘钥,下面举个JDK8实现DESede加密的例子:

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
import org.junit.Test;

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 Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
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));
}
}

过程和DES一致,区别仅在于使用的加密算法为DESede,秘钥长度指定为112,秘钥材料对象为DESedeKeySpec类。

上面程序输出如下:

1
2
3
4
待加密值:mrbird's blog
DESede秘钥:1fFb+F1wboVGGTSPNIBJ+9XxW/hdcG6F
DESede加密结果:WVg9w6exyRREqY1vNyRI+Q==
DESede解密结果:mrbird's blog

如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Wrong keysize: must be equal to 112 or 168异常。

AES

AES(AdvancedEncryption Standard,高级数据加密标准)算法支持128位、192位和256位的秘钥长度,加密速度比DES和DESede都快,至今还没有被破解的报道。经过验证,目前采用的AES算法能够有效抵御已知的针对DES算法的所有攻击方法,如部分差分攻击、相关秘钥攻击等。AES算法因秘钥建立时间短、灵敏性好、内存需求低等优点,在各个领域得到广泛的研究与应用。

JDK8支持128位、192位和256位长度的AES秘钥,下面举个JDK8实现AES加密的例子:

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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
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));
}
}

过程和上面例子大体一致,唯一区别是使用AES算法无需指定特定类型的秘钥材料,直接用SecretKeySpec对象即可。

程序输出如下:

1
2
3
4
待加密值:mrbird's blog
AES秘钥:vMj1fnhYdIGpTfxpEeKaynG879ARZ5r2AcfBpf+yoRs=
AES加密结果:GCmIlqEJIBlBRw/474OanQ==
AES解密结果:mrbird's blog

如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256异常。

RC2、RC4

RC2和RC4算法也可以用于替换DES算法,特点是秘钥长度较为灵活,RC2和RC4的秘钥长度范围为40到1024位。

JDK8支持RC2和RC4算法:

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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "RC2";
// 转换模式
String transformation = "RC2";
// --- 生成秘钥 ---
// 实例化秘钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
// 初始化秘钥长度
keyGenerator.init(666);
// 生成秘钥
SecretKey secretKey = keyGenerator.generateKey();
// 生成秘钥材料
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), algorithm);
System.out.println("RC2秘钥:" + 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("RC2加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("RC2解密结果:" + new String(decrypt));
}
}

程序输出如下:

1
2
3
4
待加密值:mrbird's blog
RC2秘钥://wHmkfa/TpQqeard5RD8h6Gdd1YJQZbFT4pUsssRfitpz1YkhJfCN/J/hjR0Q1HmVReJaSPCNDqP+GUBlvnWi7zsoKW35O6R5W/R0Bgnjh/qVsu
RC2加密结果:+XL70sqXz/myi1JRiLVbCw==
RC2解密结果:mrbird's blog

如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Key length for RC2 must be between 40 and 1024 bits异常。

RC4读者可以自己玩一玩。

Blowfish

Blowfish算法也可以用于替换DES,Blowfish算法的秘钥长度范围为32到448位,并且必须为8的倍数。

JDK8支持Blowfish算法:

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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
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));
}
}

程序运行结果:

1
2
3
4
待加密值:mrbird's blog
Blowfish秘钥:d3ReFylxI2OI7n3qgPFViQ==
Blowfish加密结果:nnH2mqVZhhhegBwAuqecLw==
Blowfish解密结果:mrbird's blog

如果指定不合法的秘钥长度,程序将抛出java.security.InvalidParameterException: Keysize must be multiple of 8, and can only range from 32 to 448 (inclusive)异常。

PBE

PBE(Password Based Encryption,基于口令加密)算法是一种基于口令的加密算法,特点是没有秘钥的概念,信息交互双方事先拟定好口令即可。单纯的口令很容易通过穷举攻击方式破译,所以PBE也加入了“盐”的概念。

PBE算法是对称加密算法的综合性算法,常见算法如PBEWithMD5AndDES,该算法使用了MD5和DES算法构建PBE算法。JDK8支持PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40、PBEWithSHA1AndRC2_128、PBEWithSHA1AndRC4_40、PBEWithSHA1AndRC4_128、PBEWithHmacSHA1AndAES_128、PBEWithHmacSHA224AndAES_128、PBEWithHmacSHA256AndAES_128、PBEWithHmacSHA384AndAES_128、PBEWithHmacSHA512AndAES_128、PBEWithHmacSHA1AndAES_256、PBEWithHmacSHA224AndAES_256、PBEWithHmacSHA256AndAES_256、PBEWithHmacSHA384AndAES_256和PBEWithHmacSHA512AndAES_256,其中算法名称中包含Hmac的需要配和初始化向量使用。

不需要指定初始化向量的PBE算法族(PBEWithMD5AndDES、PBEWithMD5AndTripleDES、PBEWithSHA1AndDESede、PBEWithSHA1AndRC2_40、PBEWithSHA1AndRC2_128、PBEWithSHA1AndRC4_40、PBEWithSHA1AndRC4_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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "PBEWithSHA1AndDESede";
// 转换模式
String transformation = "PBEWithSHA1AndDESede";
// 密码(口令)
String password = "mrbird@qq.com";
System.out.println("PBE口令:" + password);
// 迭代次数
int count = 99;

// 实例化安全随机数
SecureRandom secureRandom = new SecureRandom();
// 生成盐
byte[] salt = secureRandom.generateSeed(8);
System.out.println("盐值:" + Base64.getEncoder().encodeToString(salt));
// 通过密码生成秘钥材料
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
// 实例化秘钥工厂
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
// 生成秘钥
SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
// 实例化PBE参数材料
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, count);

// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("PBE加密结果:" + Base64.getEncoder().encodeToString(encrypt));

// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("PBE解密结果:" + new String(decrypt));
}
}

程序输出如下:

1
2
3
4
5
待加密值:mrbird's blog
PBE口令:mrbird@qq.com
盐值:e2vd/Or8i3U=
PBE加密结果:TZx40AyeTP9PZyF6iP3C8A==
PBE解密结果:mrbird's blog

算法名称包含Hmac的PBE算法需要指定初始化向量,比如PBEWithHmacSHA1AndAES_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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "PBEWithHmacSHA1AndAES_128";
// 转换模式
String transformation = "PBEWithHmacSHA1AndAES_128";
// 密码(口令)
String password = "mrbird@qq.com";
System.out.println("PBE口令:" + password);
// 迭代次数
int count = 99;

// 实例化安全随机数
SecureRandom secureRandom = new SecureRandom();
// 生成盐
byte[] salt = secureRandom.generateSeed(8);
System.out.println("盐值:" + Base64.getEncoder().encodeToString(salt));
// 通过密码生成秘钥材料
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
// 实例化秘钥工厂
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithm);
// 生成秘钥
SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
// 创建初始化向量
IvParameterSpec iv = new IvParameterSpec("123456789abcdefg".getBytes());
// 实例化PBE参数材料
PBEParameterSpec pbeParameterSpec = new PBEParameterSpec(salt, count, iv);

// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 初始化
cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("PBE加密结果:" + Base64.getEncoder().encodeToString(encrypt));

// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKey, pbeParameterSpec);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("PBE解密结果:" + new String(decrypt));
}
}

通过IvParameterSpec类创建初始化向量,创建初始化向量的秘钥必须为16字节,这里为123456789abcdefg,实例化PBE参数材料的时候通过构造参数传入初始化向量。

程序输出如下:

1
2
3
4
5
待加密值:mrbird's blog
PBE口令:mrbird@qq.com
盐值:UJwEggkfM88=
PBE加密结果:l+HGXLUHEQN+uKmNuIzqzA==
PBE解密结果:mrbird's blog

加密模式

对称加密算法分为:序列密码(流密码)加密,分组密码(块密码)加密两种。流密码是对信息流中的每一个元素(一个字母或一个比特)作为基本的处理单元进行加密,块密码是先对信息流分块,再对每一块分别加密。

上面介绍的这些都属于块密码加密。不同的算法侧重点不同,有的强调效率,有的强调安全,有的强调容错性。根据数据加密时每个加密区块间的关联方式来区分,可以分为4种加密模式:电子密码本模式(Electronic Code Book,ECB)、密文链接模式(Cipher Book Chaining,CBC)、密文反馈模式(Cipher Feed Back,CFB)、输出反馈模式(Output Feed Back,OFB)。AES标准除了推荐上述4种工作模式外,还推荐了一种新的工作模式—计数器模式(Counter,CTR)。这些工作模式可适用于各种分组密码算法。

ECB

ECB模式加解密过程如下图所示:

QQ20200821-090430@2x

明文分为若干块,每次加密均产生独立的密文分组,每组的加密结果不会对其他分组产生影响,相同的明文加密后对应产生相同的密文。

  • 优点:可并行操作,没有误差传递(因为每个密文都是独立加密来的);
  • 缺点:如果明文重复,则对应的密文也会重复,对明文进行主动攻击的可能性较高;
  • 用途:适合加密秘钥、随机数等短数据。例如,安全地传递DES秘钥,ECB是最合适的模式。

CBC

CBC模式加解密过程如下图所示:

QQ20200821-154612@2x.png

明文分为若干块,每次加密前,明文块都和前一个明文块加密后的内容进行异或处理,然后再用秘钥加密。因为第一个明文块没有可以用来异或处理的密文块,所以我们需要提供一个初始化向量来替代。

  • 优点:密文链接模式加密后的密文上下文关联,对明文的主动攻击的可能性较低;
  • 缺点:不能并行加密,如果在加密过程中发生错误,则错误将被无限放大,导致加密失败。并且需要提供初始化向量;
  • 用途:可加密任意长度的数据;适用于计算产生检测数据完整性的消息认证码Mac。

CFB

CFB模式加解密过程如下图所示:

QQ20200821-094022@2x

明文分为若干块,每次加密前,先将前一个密文块使用秘钥加密,加密结果和当前明文块异或处理得到密文块。同样的,需要为第一个明文块加密提供初始化向量。

  • 优点:和CBC类似;
  • 缺点:和CBC类似;
  • 用途:因错误传播无界,可用于检查发现明文密文的篡改。

OFB

OFB模式加解密过程如下图所示:

QQ20200821-094947@2x

过程和CFB类似,区别在于OFB第一次使用秘钥对初始化向量进行加密(结果为A),加密结果和明文块异或处理得到密文块,下一次操作时候,不是使用秘钥加密前一个密文块,而是使用秘钥加密A的结果再和明文块异或处理,得到当前密文块。

  • 优点:和CFB类似;
  • 缺点:不利于并行计算;对明文的主动攻击是可能的,安全性较CFB差;
  • 用途:适用于加密冗余性较大的数据,比如语音和图像数据。

CTR

CTR模式加解密过程如下图所示:

QQ20200821-100139@2x

CTR含义是计数器模式,所以它维护了一个递增的计数器。秘钥加密计数器,结果和明文块异或得到密文块,依次类推。

  • 优点:可以并行操作,安全性和CBC一样好;
  • 缺点:没有错误传播,因此不易确保数据完整性;
  • 用途:适用于各种加密应用。

填充模式

当需要按块处理的数据, 数据长度不符合块处理需求时, 按照一定的方法填充满块长的规则。如果不填充,待加密的数据块长度不符合要求时程序会抛出异常。

JDK8中主要支持NoPadding和PKCS5Padding填充模式。

  1. NoPadding:不填充;
  2. PKCS5Padding:数据块的大小为8位, 不够就补足。

加密、填充模式实战

在了解了加密模式和填充模式后,我们回头看前面代码中的transformation参数,实例化Cipher对象的时候需要指定transformation转换模式,转换模式主要有两种格式:

  1. 算法;
  2. 算法/加密模式/填充模式。

下面就AES算法来实践不同的加密、填充模式。

当转换模式为AES/ECB/PKCS5Padding时:

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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "AES";
// 转换模式
String transformation = "AES/ECB/PKCS5Padding";
// --- 生成秘钥 ---
// 实例化秘钥生成器
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));
}
}

程序运行解果:

1
2
3
4
待加密值:mrbird's blog
AES秘钥:xCwGw2d/cflibDRY4oQtjNEgBN5tIrxOBtt/31jAeN8=
AES加密结果:kjFz9IMO2HAOFgExXMsBjg==
AES解密结果:mrbird's blog

将转换模式改为AES/CBC/PKCS5Padding时,程序输出抛出java.security.InvalidKeyException: Parameters missing异常。

因为该模式需要指定初始化向量,将代码修改为:

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
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String value = "mrbird's blog";
System.out.println("待加密值:" + value);
// 加密算法
String algorithm = "AES";
// 转换模式
String transformation = "AES/CBC/PKCS5Padding";
// --- 生成秘钥 ---
// 实例化秘钥生成器
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()));

// 初始化向量,123456789abcdefg初始化向量秘钥,16字节
IvParameterSpec iv = new IvParameterSpec("123456789abcdefg".getBytes());
// 实例化密码对象
Cipher cipher = Cipher.getInstance(transformation);
// 设置模式(ENCRYPT_MODE:加密模式;DECRYPT_MODE:解密模式)和指定秘钥
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
// 加密
byte[] encrypt = cipher.doFinal(value.getBytes());
System.out.println("AES加密结果:" + Base64.getEncoder().encodeToString(encrypt));
// 解密
// 设置为解密模式
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
byte[] decrypt = cipher.doFinal(encrypt);
System.out.println("AES解密结果:" + new String(decrypt));
}
}

程序输出如下:

1
2
3
4
待加密值:mrbird's blog
AES秘钥:xbqvwRKSrAtMsKOxzkbJqG0aMgMQwTC7TF5jkch1M5I=
AES加密结果:jksbHzoAA/K0sNVW1+RUOw==
AES解密结果:mrbird's blog

将转换模式改为AES/CBC/NoPadding时,程序抛出javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes异常。因为mrbird’s blog不是16字节的倍数。

更多对称加密算法和加密模式填充模式的组合可以自己尝试下,或者参考下面的算法总结。

手动指定秘钥

在使用对称加密算法加解密的时候,秘钥一般是双方事先约定好的,假如现在有AES秘钥/a3nqjFFtkWNmeX5HzmMtzCziOThbAnEHoiTA4JquAI=,AES密文y9YMlnX7TE4qF0dDIQ==,转换模式为AES/CTR/PKCS5Padding,初始化向量秘钥为123456789abcdefg,如何通过秘钥和密文进行解密呢,可以参考下面的代码(即演示如何手动指定秘钥并解密):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.junit.Test;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Demo {

@Test
public void test() throws Exception {
String algorithm = "AES";
String transformation = "AES/CTR/PKCS5Padding";
String key = "/a3nqjFFtkWNmeX5HzmMtzCziOThbAnEHoiTA4JquAI=";
String encrypt = "y9YMlnX7TE4qF0dDIQ==";
String ivKey = "123456789abcdefg";

Cipher cipher = Cipher.getInstance(transformation);
SecretKeySpec secretKeySpec = new SecretKeySpec(Base64.getDecoder().decode(key), algorithm);
IvParameterSpec iv = new IvParameterSpec(ivKey.getBytes());
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, iv);
byte[] decrypt = cipher.doFinal(Base64.getDecoder().decode(encrypt));
System.out.println("AES解密结果:" + new String(decrypt));

输出如下:

1
AES解密结果:mrbird's blog

算法总结

对上面这些算法进行总结:

算法 秘钥长度(位) 工作模式 填充模式 初始化向量秘钥长度(字节)
DES 56 ECB、CBC、CFB、OFB、CTR等 NoPadding、 PKCS5Padding、 ISO10126Padding 8
DESede 112、168 ECB、CBC、CFB、OFB、CTR等 NoPadding、 PKCS5Padding、 ISO10126Padding 8
AES 128、192、256 ECB、CBC、CFB、OFB、CTR等 NoPadding、 PKCS5Padding、 ISO10126Padding 16
RC2 40~1024 ECB、CBC、CFB、OFB、CTR等 NoPadding、 PKCS5Padding、 ISO10126Padding 8
RC4 40~1024 ECB NoPadding
Blowfish 32~448,8的倍数 ECB、CBC、CFB、OFB、CTR等 NoPadding、 PKCS5Padding、 ISO10126Padding 8
PBE CBC PKCS5Padding 16(带Hmac)

《Java加密与解密的艺术》读书笔记