对称加密-拓展

9/21/2023 Java

摘要

JDK:1.8.0_202

# 一:PBE

PBE(Password Based Encryption,基于口令加密)算法是一种基于口令的加密算法,特点是没有秘钥的概念,信息交互双方事先拟定好口令即可。单纯的口令很容易通过穷举攻击方式破译,所以PBE也加入了 “盐” 的概念。PBE 算法是对称加密算法的综合性算法,常见算法如 PBEWithMD5AndDES,该算法使用了MD5和DES算法构建PBE算法。

JDK8支持 PBEWithMD5AndDESPBEWithMD5AndTripleDESPBEWithSHA1AndDESedePBEWithSHA1AndRC2_40PBEWithSHA1AndRC2_128PBEWithSHA1AndRC4_40PBEWithSHA1AndRC4_128PBEWithHmacSHA1AndAES_128PBEWithHmacSHA224AndAES_128PBEWithHmacSHA256AndAES_128PBEWithHmacSHA384AndAES_128PBEWithHmacSHA512AndAES_128PBEWithHmacSHA1AndAES_256PBEWithHmacSHA224AndAES_256PBEWithHmacSHA256AndAES_256PBEWithHmacSHA384AndAES_256PBEWithHmacSHA512AndAES_256,其中算法名称中包含 Hmac 的需要配和初始化向量使用。

不需要指定初始化向量的PBE算法族(PBEWithMD5AndDESPBEWithMD5AndTripleDESPBEWithSHA1AndDESedePBEWithSHA1AndRC2_40PBEWithSHA1AndRC2_128PBEWithSHA1AndRC4_40PBEWithSHA1AndRC4_128)例子:

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

    @Test
    public void test1() throws Exception {
        String value = "helloWorld";
        System.out.println("待加密值:" + value);
        // 加密算法
        String algorithm = "PBEWithSHA1AndDESede";
        // 转换模式
        String transformation = "PBEWithSHA1AndDESede";
        // 密码(口令)
        String password = "74Q9gacAnPZZXyiifxdA";
        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
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

算法名称包含Hmac的PBE算法需要指定初始化向量,比如PBEWithHmacSHA1AndAES_128:

@Test
public void test2() throws Exception {
    String value = "helloWorld";
    System.out.println("待加密值:" + value);
    // 加密算法
    String algorithm = "PBEWithHmacSHA1AndAES_128";
    // 转换模式
    String transformation = "PBEWithHmacSHA1AndAES_128";
    // 密码(口令)
    String password = "74Q9gacAnPZZXyiifxdA";
    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));
}
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

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

# 二:加密模式

对称加密算法分为:序列密码(流密码)加密分组密码(块密码)加密 两种。

  • 流密码:是对信息流中的每一个元素(一个字母或一个比特)作为基本的处理单元进行加密;
  • 块密码:是先对信息流分块,再对每一块分别加密。

之前介绍的都属于块密码加密。不同的算法侧重点不同,有的强调效率,有的强调安全,有的强调容错性。根据数据加密时每个加密区块间的关联方式来区分,可以分为4种加密模式:

  • 电子密码本模式(Electronic Code Book,ECB)
  • 密文链接模式(Cipher Book Chaining,CBC)
  • 密文反馈模式(Cipher Feed Back,CFB)
  • 输出反馈模式(Output Feed Back,OFB)

AES 标准除了推荐上述4种工作模式外,还推荐了一种新的工作模式—计数器模式(Counter,CTR)。这些工作模式可适用于各种分组密码算法。

# 2.1 ECB

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

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

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

# 2.2 CBC

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

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

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

# 2.3 CFB

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

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

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

# 2.4 OFB

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

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

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

# 2.5 CTR

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

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

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

# 三:填充模式

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

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

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

# 四:加密、填充模式实战

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

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

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

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

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

    @Test
    public void test1() throws Exception {
        String value = "helloWorld";
        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
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

将转换模式改为 AES/CBC/PKCS5Padding 时,程序输出抛出 java.security.InvalidKeyException: Parameters missing 异常。因为该模式需要指定初始化向量,将代码修改为:

@Test
public void test2() throws Exception {
    String value = "helloWorld";
    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
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

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

# 五:手动指定秘钥

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

@Test
public void test3() throws Exception {
    String algorithm = "AES";
    String transformation = "AES/CTR/PKCS5Padding";
    String key = "ZP578BV7JnZ3XdQjLdaDXnhVhBYQ1JRM7CPu9PyMSCg=";
    String encrypt = "WRNzQckpM1mOWg==";
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 六:小结

对于对称性算法总结:

算法 秘钥长度(位) 工作模式 填充模式 初始化向量秘钥长度(字节)
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)

# 七:参考文献

最后更新: 9/23/2023, 3:55:03 PM