常见对称加密原理以及应用

加密算法

所谓对称加密算法,通过密钥将明文加密成密文,并且再通过同一个密钥将密文解密成明文,相对于非对称加密算法速度快效率高,对于明文文本越长效率优势越大。

常见的对称加密算法有AES、DES、3DES等,其中DES由于密钥长度低容易被暴力破解,因此安全性相对较低已经不推荐使用。而3DES则是DES的升级版,安全性有所提升,但依然不如AES,因此推荐安全性更高的AES加密算法。

算法 算法类型 密钥长度 分组长度 安全性
AES 块密码算法 128/192/256比特 128比特 安全
DES 块密码算法 56比特 64比特 不安全
3DES 块密码算法 128/168比特 64比特 安全

分组模式

AES、DES、3DES都是块密码算法,即运算加密解密时不是一次性将整个明文/密文文本进行运算,而是拆成固定长度的数据块后对每个数据块进行运算。

由于文本被拆分成若干个数据块,对于超过一个数据块的场景需要定义数据块之间的拆分和组装方式,即分组模式,场景的分组模式如下表:

分组 是否需要填充 安全性
ECB 不安全
CBC 安全
CTR 安全

ECB

ECB是最简单的一种分组模式,简单地将明文拆成数据块后,每个数据块单独加密解密。如果两个数据块内容相同,那么这两个数据块加密后的密文段也是完全一样,容易受到重放攻击以及密文篡改,因此安全度不高。

image

image

CBC

CBC引入了一个新的变量初始向量IV,一般是一个随机数,用于在第一块明文数据块加密前对数据块做XOR运算,之后每一块明文数据块加密前都与前一块的密文数据块做XOR运算。

由于每一块数据块的计算结果都与上一块数据块有关联,因此即使有两个相同的数据块,计算出的密文段也不同,解决了ECB的安全问题。但因此,CBC计算只能串行处理,效率不如ECB。

image

image

CTR

CTR同样引入了一个新的变量初始随机数,每一个数据块计算时先对随机数+1,然后再参与加密解密运算,因此即使有两个相同的数据块,计算出的密文段也不同,解决了ECB的安全问题。并且每个数据块仅与随机数有联系,因此可以并行处理。

image

image

填充算法

对于块加密算法来说,不是对一次性对整个明文进行加密计算,而是拆分成若干个固定长度(加密算法的分组长度)的数据块,然后对各个数据块进行加密计算,因此明文的字节长度必须是分组长度的整数倍数。如果明文的字节长度不是分组长度的整数倍数,则需要一种填充的机制,在加密前将明文字节长度填充成分组长度的整数倍数,并且在解密后正确移除填充的字节。

常见的填充算法有PKCS#7和PKCS#5,而实际上两者的处理逻辑是相同的,只是PKCS#5只能处理分组长度为8字节的数据,而PKCS#7可以处理分组长度是1-255任意长度字节的数据。从这个角度看,PKCS#5是PKCS#7的子集。

PKCS#7很简单,实现代码如下:

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
/**
* PKCS#7填充实现
* @param src 明文
* @param cipherBlockSize 加密算法分组长度
*/
public static byte[] padding7(byte[] src, int cipherBlockSize) {
// PKCS#7的分组最大为255字节
int blockSize = Math.min(255, cipherBlockSize);
int padNum = blockSize - src.length % blockSize;
byte[] dst = new byte[src.length + padNum];
System.arraycopy(src, 0, dst, 0, src.length);
for (int i = 0; i < padNum; i++) {
dst[src.length + i] = (byte) padNum;
}
return dst;
}

/**
* PKCS#5填充实现
* @param src 明文
* @param cipherBlockSize 加密算法分组长度
*/
public static byte[] padding5(byte[] src, int cipherBlockSize) {
// PKCS#5的分组固定为8字节
final int blockSize = 8;
if (cipherBlockSize != blockSize) {
throw new RuntimeException("block length not support");
}
int padNum = blockSize - src.length % blockSize;
byte[] dst = new byte[src.length + padNum];
System.arraycopy(src, 0, dst, 0, src.length);
for (int i = 0; i < padNum; i++) {
dst[src.length + i] = (byte) padNum;
}
return dst;
}

如果数据需要填充n个字节,则在数据后添加n个字节长度的字节,并且填充的每个字节的值为n。另外,如果数据长度刚好是分组长度的整数倍时,依然还需要填充,填充长度为分组长度。

1
2
3
4
0x01
0x02 0x02
0x03 0x03 0x03
0x04 0x04 0x04 0x04
  • 分组长度8字节时数据5字节,需要填充3个字节:
1
2
3
Hello
0x48 0x65 0x6c 0x6c 0x6f
0x48 0x65 0x6c 0x6c 0x6f 0x03 0x03 0x03
  • 分组长度8字节时数据8字节,本应不需要填充,但实际上再额外填充8字节:
1
2
3
EchoEcho
0x45 0x63 0x68 0x6f 0x45 0x63 0x68 0x6f
0x45 0x63 0x68 0x6f 0x45 0x63 0x68 0x6f 0x08 0x08 0x08 0x08 0x08 0x08 0x08 0x08

==AES算法的分组长度超过8字节,因此不能使用PKCS#5,但有些程序使用AES算法时若指定PKCS#5时并未报错,实际内部依然使用了PCKS#7。==

代码实现

常见的填充算法有PCKS#5和PKCS#7,但JDK的语法中存在PKCS5Padding却不存在PKCS7Padding。查阅官方文档Cipher以及Cipher Algorithm Padding后确认语法中确实不存在PKCS7Padding

JDK中PKCS#7命名问题无从考证,但可以确定的是,JDK中语法上的PKCS5Padding实际上就是PKCS#7的实现,而官方文档Cipher也显示了这一点:

Every implementation of the Java platform is required to support the following standard Cipher transformations with the keysizes in parentheses:

  • AES/CBC/NoPadding (128)
  • AES/CBC/PKCS5Padding (128)
  • AES/ECB/NoPadding (128)
  • AES/ECB/PKCS5Padding (128)
  • DES/CBC/NoPadding (56)
  • DES/CBC/PKCS5Padding (56)
  • DES/ECB/NoPadding (56)
  • DES/ECB/PKCS5Padding (56)
  • DESede/CBC/NoPadding (168)
  • DESede/CBC/PKCS5Padding (168)
  • DESede/ECB/NoPadding (168)
  • DESede/ECB/PKCS5Padding (168)
  • RSA/ECB/PKCS1Padding (1024, 2048)
  • RSA/ECB/OAEPWithSHA-1AndMGF1Padding (1024, 2048)
  • RSA/ECB/OAEPWithSHA-256AndMGF1Padding (1024, 2048)

DES-ECB

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
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class DESECBUtils {

public static String encrypt(String key, String data) throws Exception {
Cipher cipher = initCipher(key, Cipher.ENCRYPT_MODE);
byte[] encryptedByte = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encryptedByte);
}

public static String decrypt(String key, String data) throws Exception {
Cipher cipher = initCipher(key, Cipher.DECRYPT_MODE);
byte[] decryptedByte = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedByte);
}

private static Cipher initCipher(String key, int encryptMode) throws Exception {
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(encryptMode, new SecretKeySpec(key.getBytes(), "DES"));
return cipher;
}

public static void main(String[] args) throws Exception {
String key = "12345678";
String value = "HelloWorld";

// 16进制的加密参数key
System.out.println("key hex: " + Hex.encodeHexString(key.getBytes()));

// 加密
String encrypt = encrypt(key, value);
System.out.println("encrypt: " + encrypt);
// 解密
String decrypt = decrypt(key, encrypt);
System.out.println("decrypt: " + decrypt);
}

}
1
2
3
key hex: 3132333435363738
encrypt: 8SBfruuCs9qh0WiTqhkSbg==
decrypt: HelloWorld

OpenSSL命令行调用:

1
2
3
4
5
6
7
[root@VM_0_10_centos ~]# echo -n HelloWorld > in.txt
[root@VM_0_10_centos ~]# openssl enc -des-ecb -in in.txt -out encrypt.txt -K 3132333435363738 -a
[root@VM_0_10_centos ~]# cat encrypt.txt
8SBfruuCs9qh0WiTqhkSbg==
[root@VM_0_10_centos ~]# openssl enc -des-ecb -in encrypt.txt -out decrypt.txt -K 3132333435363738 -a -d
[root@VM_0_10_centos ~]# cat decrypt.txt
HelloWorld

DES-CBC

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
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class DESCBCUtils {

public static String encrypt(String key, String iv, String data) throws Exception {
Cipher cipher = initCipher(key, iv, Cipher.ENCRYPT_MODE);
byte[] encryptedByte = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encryptedByte);
}

public static String decrypt(String key, String iv, String data) throws Exception {
Cipher cipher = initCipher(key, iv, Cipher.DECRYPT_MODE);
byte[] decryptedByte = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedByte);
}

private static Cipher initCipher(String key, String iv, int encryptMode) throws Exception {
Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "DES");
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
cipher.init(encryptMode, keySpec, ivSpec);
return cipher;
}

public static void main(String[] args) throws Exception {
String key = "12345678";
String iv = "87654321";
String value = "HelloWorld";

// 16进制的加密参数key,iv
System.out.println("key hex: " + Hex.encodeHexString(key.getBytes()));
System.out.println("iv hex: " + Hex.encodeHexString(iv.getBytes()));

// 加密
String encrypt = encrypt(key, iv, value);
System.out.println("encrypt: " + encrypt);
// 解密
String decrypt = decrypt(key, iv, encrypt);
System.out.println("decrypt: " + decrypt);
}

}
1
2
3
4
key hex: 3132333435363738
iv hex: 3837363534333231
encrypt: 9r2UI+JnSrVP+WqsrFUqsg==
decrypt: HelloWorld

OpenSSL命令行调用:

1
2
3
4
5
6
7
[root@VM_0_10_centos ~]# echo -n HelloWorld > in.txt
[root@VM_0_10_centos ~]# openssl enc -des-cbc -in in.txt -out encrypt.txt -K 3132333435363738 -iv 3837363534333231 -a
[root@VM_0_10_centos ~]# cat encrypt.txt
9r2UI+JnSrVP+WqsrFUqsg==
[root@VM_0_10_centos ~]# openssl enc -des-cbc -in encrypt.txt -out decrypt.txt -K 3132333435363738 -iv 3837363534333231 -a -d
[root@VM_0_10_centos ~]# cat decrypt.txt
HelloWorld

AES-ECB

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.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class AESECBUtils {

public static String encrypt(String key, String data) throws Exception {
Cipher cipher = initCipher(key, Cipher.ENCRYPT_MODE);
byte[] encryptedByte = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encryptedByte);
}

public static String decrypt(String key, String data) throws Exception {
Cipher cipher = initCipher(key, Cipher.DECRYPT_MODE);
byte[] decryptedByte = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedByte);
}

private static Cipher initCipher(String key, int encryptMode) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(encryptMode, new SecretKeySpec(key.getBytes(), "AES"));
return cipher;
}

public static void main(String[] args) throws Exception {
// 16字节秘钥即128比特
String key = "1234567890123456";
String value = "HelloWorld";

// 16进制的加密参数key
System.out.println("key hex: " + Hex.encodeHexString(key.getBytes()));

// 加密
String encrypt = encrypt(key, value);
System.out.println("encrypt: " + encrypt);
// 解密
String decrypt = decrypt(key, encrypt);
System.out.println("decrypt: " + decrypt);
}

}
1
2
3
key hex: 31323334353637383930313233343536
encrypt: VfqXvdGjzea0rgvXO56j5g==
decrypt: HelloWorld

OpenSSL命令行调用:

1
2
3
4
5
6
7
[root@VM_0_10_centos ~]# echo -n HelloWorld > in.txt
[root@VM_0_10_centos ~]# openssl enc -aes-128-ecb -in in.txt -out encrypt.txt -K 31323334353637383930313233343536 -a
[root@VM_0_10_centos ~]# cat encrypt.txt
VfqXvdGjzea0rgvXO56j5g==
[root@VM_0_10_centos ~]# openssl enc -aes-128-ecb -in encrypt.txt -out decrypt.txt -K 31323334353637383930313233343536 -a -d
[root@VM_0_10_centos ~]# cat decrypt.txt
HelloWorld

AES-CBC

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.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AESCBCUtils {

public static String encrypt(String key, String iv, String data) throws Exception {
Cipher cipher = initCipher(key, iv, Cipher.ENCRYPT_MODE);
byte[] encryptedByte = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encryptedByte);
}

public static String decrypt(String key, String iv, String data) throws Exception {
Cipher cipher = initCipher(key, iv, Cipher.DECRYPT_MODE);
byte[] decryptedByte = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedByte);
}

private static Cipher initCipher(String key, String iv, int encryptMode) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
cipher.init(encryptMode, keySpec, ivSpec);
return cipher;
}

public static void main(String[] args) throws Exception {
// 16字节秘钥即128比特
String key = "1234567890123456";
String iv = "9876543210987654";
String value = "HelloWorld";

// 16进制的加密参数key,iv
System.out.println("key hex: " + Hex.encodeHexString(key.getBytes()));
System.out.println("iv hex: " + Hex.encodeHexString(iv.getBytes()));

// 加密
String encrypt = encrypt(key, iv, value);
System.out.println("encrypt: " + encrypt);
// 解密
String decrypt = decrypt(key, iv, encrypt);
System.out.println("decrypt: " + decrypt);
}

}
1
2
3
4
key hex: 31323334353637383930313233343536
iv hex: 39383736353433323130393837363534
encrypt: 5febt2VH5eaKXn/nrQj/aQ==
decrypt: HelloWorld

OpenSSL命令行调用:

1
2
3
4
5
6
7
[root@VM_0_10_centos ~]# echo -n HelloWorld > in.txt
[root@VM_0_10_centos ~]# openssl enc -aes-128-cbc -in in.txt -out encrypt.txt -K 31323334353637383930313233343536 -iv 39383736353433323130393837363534 -a
[root@VM_0_10_centos ~]# cat encrypt.txt
5febt2VH5eaKXn/nrQj/aQ==
[root@VM_0_10_centos ~]# openssl enc -aes-128-cbc -in encrypt.txt -out decrypt.txt -K 31323334353637383930313233343536 -iv 39383736353433323130393837363534 -a -d
[root@VM_0_10_centos ~]# cat decrypt.txt
HelloWorld

AES-CTR

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.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AESCTRUtils {

public static String encrypt(String key, String iv, String data) throws Exception {
Cipher cipher = initCipher(key, iv, Cipher.ENCRYPT_MODE);
byte[] encryptedByte = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encryptedByte);
}

public static String decrypt(String key, String iv, String data) throws Exception {
Cipher cipher = initCipher(key, iv, Cipher.DECRYPT_MODE);
byte[] decryptedByte = cipher.doFinal(Base64.decodeBase64(data));
return new String(decryptedByte);
}

private static Cipher initCipher(String key, String iv, int encryptMode) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
cipher.init(encryptMode, keySpec, ivSpec);
return cipher;
}

public static void main(String[] args) throws Exception {
// 16字节秘钥即128比特
String key = "1234567890123456";
String iv = "9876543210987654";
String value = "HelloWorld";

// 16进制的加密参数key,iv
System.out.println("key hex: " + Hex.encodeHexString(key.getBytes()));
System.out.println("iv hex: " + Hex.encodeHexString(iv.getBytes()));

// 加密
String encrypt = encrypt(key, iv, value);
System.out.println("encrypt: " + encrypt);
// 解密
String decrypt = decrypt(key, iv, encrypt);
System.out.println("decrypt: " + decrypt);
}

}
1
2
3
4
key hex: 31323334353637383930313233343536
iv hex: 39383736353433323130393837363534
encrypt: Zjh8leeiqcWH0w==
decrypt: HelloWorld

OpenSSL命令行调用:

1
2
3
4
5
6
7
[root@VM_0_10_centos ~]# echo -n HelloWorld > in.txt
[root@VM_0_10_centos ~]# openssl enc -aes-128-ctr -in in.txt -out encrypt.txt -K 31323334353637383930313233343536 -iv 39383736353433323130393837363534 -a
[root@VM_0_10_centos ~]# cat encrypt.txt
Zjh8leeiqcWH0w==
[root@VM_0_10_centos ~]# openssl enc -aes-128-ctr -in encrypt.txt -out decrypt.txt -K 31323334353637383930313233343536 -iv 39383736353433323130393837363534 -a -d
[root@VM_0_10_centos ~]# cat decrypt.txt
HelloWorld

随机密钥

阅读一些java的加解密代码实现时可能会看到如下代码,这实际上是一个密码学的随机数生成,通过指定一个随机种子后生成一个随机数,最后以随机数作为对称加密算法的密钥。

因此随后参与对称加密运算的密钥并不是最开始的程序输入值,而是随机数生成器生成的随机数secretKey.getEncoded()

1
2
3
4
5
6
KeyGenerator generator = KeyGenerator.getInstance("AES");
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed("1234567890123456".getBytes());
generator.init(secureRandom);
SecretKey secretKey = generator.generateKey();
System.out.println(Hex.encodeHexString(secretKey.getEncoded()));
>