加密算法
所谓对称加密算法,通过密钥将明文加密成密文,并且再通过同一个密钥将密文解密成明文,相对于非对称加密算法速度快效率高,对于明文文本越长效率优势越大。
常见的对称加密算法有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是最简单的一种分组模式,简单地将明文拆成数据块后,每个数据块单独加密解密。如果两个数据块内容相同,那么这两个数据块加密后的密文段也是完全一样,容易受到重放攻击以及密文篡改,因此安全度不高。
CBC
CBC引入了一个新的变量初始向量IV,一般是一个随机数,用于在第一块明文数据块加密前对数据块做XOR运算,之后每一块明文数据块加密前都与前一块的密文数据块做XOR运算。
由于每一块数据块的计算结果都与上一块数据块有关联,因此即使有两个相同的数据块,计算出的密文段也不同,解决了ECB的安全问题。但因此,CBC计算只能串行处理,效率不如ECB。
CTR
CTR同样引入了一个新的变量初始随机数,每一个数据块计算时先对随机数+1,然后再参与加密解密运算,因此即使有两个相同的数据块,计算出的密文段也不同,解决了ECB的安全问题。并且每个数据块仅与随机数有联系,因此可以并行处理。
填充算法
对于块加密算法来说,不是对一次性对整个明文进行加密计算,而是拆分成若干个固定长度(加密算法的分组长度)的数据块,然后对各个数据块进行加密计算,因此明文的字节长度必须是分组长度的整数倍数。如果明文的字节长度不是分组长度的整数倍数,则需要一种填充的机制,在加密前将明文字节长度填充成分组长度的整数倍数,并且在解密后正确移除填充的字节。
常见的填充算法有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
|
public static byte[] padding7(byte[] src, int cipherBlockSize) { 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; }
public static byte[] padding5(byte[] src, int cipherBlockSize) { 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
|
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";
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";
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 { String key = "1234567890123456"; String value = "HelloWorld";
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 { String key = "1234567890123456"; String iv = "9876543210987654"; String value = "HelloWorld";
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 { String key = "1234567890123456"; String iv = "9876543210987654"; String value = "HelloWorld";
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()));
|