本文介绍ECB及CBC两种模式的加解密算法,日常使用也足够了。

# 密钥生成

/**
 * 生成SM4密钥
 * SM4密钥没有特别的要求,只要是128比特的随机数据即可
 */
@Test
public void testKeygen() {
    try {
        Security.addProvider(new BouncyCastleProvider());

        KeyGenerator keyGenerator = KeyGenerator.getInstance("SM4", BouncyCastleProvider.PROVIDER_NAME);

        keyGenerator.init(128);

        SecretKey secretKey = keyGenerator.generateKey();

        System.out.println("key: " + Hex.toHexString(secretKey.getEncoded()));
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 加解密

BouncyCastle库中实现的SM4对称加解密算法完全满足我们平时计算短数据及超大数据摘要的需求,主要使用方法就是三部曲

  1. init: 初始化
  2. update: 添加待计算数据,如果是大文件,可以循环N次update方法,将大文件一点点的添加进去
  3. doFinal: 计算最终结果

init用来确定本次是加密还是解密,本次使用的密钥及IV

update的方法定义:

byte[] update(byte[] dataBytes);
byte[] update(byte[] dataBytes, int off, int len);
1
2

主要注意调用完update方法后,可能会有结果输出,要处理结果,因为满一轮就会处理一轮。比如:

byte[] cipherBytes = cipher.update(dataBytes);
if(cipherBytes != null && cipherBytes.length > 0) {
	// 保存结果
}
1
2
3
4

同样的doFinal的返回值处理方式与update相同。

# 不填充加解密

/**
 * 测试SM4 NoPadding 加解密,CBC注意多传一个IV,IV的大小等于BlockSize
 * 注意每轮数据长度必须等于SM4 BlockSize的倍数,也即16字节的倍数
 * 可以取消System.out.println的注释,关注每一轮的输出,每满一轮就会输出数据,可以用来处理大文件
 */
@Test
public void testSM4NoPadding() {
    try {
        Security.addProvider(new BouncyCastleProvider());

        String[] rounds = new String[] {
                "SM4/ECB/NoPadding",
                "SM4/CBC/NoPadding"
        };
        String[] roundPlains = new String[] {
                "4167c4875cf2f7a2297da02b8f4ba8e0",
                "b8b08eae2876ef4e24bc7b3e95373b39246cdcce58aaf6cdaf42874369ba1ff3"
        };
        String[] roundCiphers = new String[] {
                "daf80aa49ffd52889f1ce80cd88d2370a1e81e96766eeba7b95cf8d038d4214d19ea48263076a6d3bb04a09e3f947331",
                "957900f635ddfba3c457367d7c6d7c3a9389c5ca9c0848cc222f5902222399b804f5a7b47d21576cf590043a83f68b20"
        };

        // 密钥大小为16字节
        Key sm4Key = new SecretKeySpec(Hex.decodeStrict("66c7f0f462eeedd9d1f2d46bdc10e4e2"), "SM4");
        // IV大小等于blockSize,也即16字节
        IvParameterSpec ivParameterSpec = new IvParameterSpec(Hex.decodeStrict("a9036e0289d9fa6d566cd0500807e3cb"));

        for (int i = 0; i < rounds.length; i++) {
            String alg = rounds[i];

            // NoPadding 加密
            ByteArrayOutputStream bout = new ByteArrayOutputStream(48);
            Cipher cipher = Cipher.getInstance(alg, BouncyCastleProvider.PROVIDER_NAME);
            if(alg.contains("CBC")) {
                cipher.init(Cipher.ENCRYPT_MODE, sm4Key, ivParameterSpec);
            }else {
                cipher.init(Cipher.ENCRYPT_MODE, sm4Key);
            }

            for (String plain: roundPlains) {
                byte[] cipherBytes = cipher.update(Hex.decodeStrict(plain));
                if(cipherBytes != null && cipherBytes.length > 0) {
                    // System.out.println("cipher bytes is not null, length: " + cipherBytes.length);
                    bout.write(cipherBytes);
                }
            }
            byte[] cipherBytes = cipher.doFinal();
            if(cipherBytes != null && cipherBytes.length > 0) {
                // System.out.println("cipher bytes is not null 3, length: " + cipherBytes.length);
                bout.write(cipherBytes);
            }

            cipherBytes = bout.toByteArray();
            Assert.assertArrayEquals(cipherBytes, Hex.decodeStrict(roundCiphers[i]));


            // NoPadding 解密
            bout.reset();

            cipher = Cipher.getInstance(alg, BouncyCastleProvider.PROVIDER_NAME);
            if(alg.contains("CBC")) {
                cipher.init(Cipher.DECRYPT_MODE, sm4Key, ivParameterSpec);
            }else {
                cipher.init(Cipher.DECRYPT_MODE, sm4Key);
            }
            byte[] plainBytes = cipher.update(cipherBytes, 0, 16);
            if(plainBytes != null && plainBytes.length > 0) {
                bout.write(plainBytes);
            }

            plainBytes = cipher.update(cipherBytes, 16, 32);
            if(plainBytes != null && plainBytes.length > 0) {
                bout.write(plainBytes);
            }
            plainBytes = cipher.doFinal();
            if(plainBytes != null && plainBytes.length > 0) {
                bout.write(plainBytes);
            }

            plainBytes = bout.toByteArray();
            Assert.assertArrayEquals(plainBytes, Hex.decodeStrict(roundPlains[0] + roundPlains[1]));
        }
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }
}
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

待处理的数据要正好是16字节的倍数才能使用NoPadding

# PKCS7Padding

/**
 * 测试SM4 PKCS7Padding 加解密
 * BC中PKCS5PADDING和PKCS7PADDING处理方式是一样的,数据长度不满blocksize大小时,填充至blocksize大小,正好相等时,再填充一个blocksize的大小
 * 详细的填充算法网上搜索一下,不复杂
 */
@Test
public void testSM4PKCS7Padding() {
    try {
        Security.addProvider(new BouncyCastleProvider());

        String[] rounds = new String[] {
                "SM4/ECB/PKCS7Padding",
                "SM4/CBC/PKCS7Padding"
        };

        String[] roundCiphers = new String[] {
                "609155d62e40228f3883f366e25d1ded",
                "1f011e071fa90af38e2bd150471290ad"
        };

        // 密钥大小为16字节
        Key sm4Key = new SecretKeySpec(Hex.decodeStrict("66c7f0f462eeedd9d1f2d46bdc10e4e2"), "SM4");
        // IV大小等于blockSize,也即16字节
        IvParameterSpec ivParameterSpec = new IvParameterSpec(Hex.decodeStrict("a9036e0289d9fa6d566cd0500807e3cb"));

        for (int i = 0; i < rounds.length; i++) {
            String alg = rounds[i];

            // PKCS7Padding 加密
            ByteArrayOutputStream bout = new ByteArrayOutputStream(48);
            Cipher cipher = Cipher.getInstance(alg, BouncyCastleProvider.PROVIDER_NAME);
            if(alg.contains("CBC")) {
                cipher.init(Cipher.ENCRYPT_MODE, sm4Key, ivParameterSpec);
            }else {
                cipher.init(Cipher.ENCRYPT_MODE, sm4Key);
            }
            byte[] cipherBytes = cipher.update(Hex.decodeStrict("616263"));
            if (cipherBytes != null && cipherBytes.length > 0) {
                bout.write(cipherBytes);
            }

            cipherBytes = cipher.doFinal();
            if (cipherBytes != null && cipherBytes.length > 0) {
                bout.write(cipherBytes);
            }

            cipherBytes = bout.toByteArray();
            Assert.assertArrayEquals(cipherBytes, Hex.decodeStrict(roundCiphers[i]));


            // PKCS7Padding 解密
            bout.reset();

            cipher = Cipher.getInstance(alg, BouncyCastleProvider.PROVIDER_NAME);
            if(alg.contains("CBC")) {
                cipher.init(Cipher.DECRYPT_MODE, sm4Key, ivParameterSpec);
            }else {
                cipher.init(Cipher.DECRYPT_MODE, sm4Key);
            }
            byte[] plainBytes = cipher.update(cipherBytes, 0, 16);
            if (plainBytes != null && plainBytes.length > 0) {
                bout.write(plainBytes);
            }

            plainBytes = cipher.doFinal();
            if (plainBytes != null && plainBytes.length > 0) {
                bout.write(plainBytes);
            }

            plainBytes = bout.toByteArray();
            Assert.assertArrayEquals(plainBytes, Hex.decodeStrict("616263"));
        }
    }catch (Exception ex) {
        Assert.fail(ex.getMessage());
    }
}
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