AES的加密和解密

一.什么是AES?

AES是英文Advanced Encryption Standard(高级加密标准)的缩写。AES是一种最常见的对称加密算法,对称加密算法即可以使用加密的密钥反向解密出来。AES是DES的升级版,安全性很好,理论上几乎不可能被破解。

1.AES的加解密流程

使用流程如下图

  • 明文P:没有经过加密的数据。
  • 密钥K:用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。
  • AES加密函数:设AES加密函数为E,则 C = E(K, P),其中P为明文,K为密钥,C为密文。也就是说,把明文P和密钥K作为加密函数的参数输入,则加密函数E会输出密文C。
  • 密文C:经加密函数处理后的数据。
  • AES解密函数:设AES解密函数为D,则 P = D(K, C),其中C为密文,K为密钥,P为明文。也就是说,把密文C和密钥K作为解密函数的参数输入,则解密函数会输出明文P。

在实际操作过程中,一般是通过RSA加密AES的密钥,传输到接收方,接收方解密得到AES密钥,然后发送方和接收方用AES密钥来通信。

2.密钥长度

AES为分组密码,分组密码也就是把明文分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文。在AES标准规范中,分组长度只能是128位,也就是说,每个分组为16个字节(每个字节8位)。密钥的长度可以使用128位、192位或256位。密钥的长度不同,推荐加密轮数也不同,如下表所示:

AES 密钥长度(32位比特字) 分组长度(32位比特字) 加密轮数
AES-128 4 4 10
AES-192 6 4 12
AES-256 8 4 14

例如AES-128,就是密钥的长度为128位,加密轮数为10轮。
上面说到,AES的加密公式为C = E(K,P),在加密函数E中,会执行一个轮函数,并且执行10次这个轮函数,这个轮函数的前9次执行的操作是一样的,只有第10次有所不同。也就是说,一个明文分组会被加密10轮。AES的核心就是实现一轮中的所有操作。

3.对称加密算法与非对称加密算法的区别:

  • 对称加密算法:加密和解密用到的密钥是相同的,这种加密方式加密速度非常快,适合经常发送数据的场合。缺点是密钥的传输比较麻烦。
  • 非对称加密算法:加密和解密用的密钥是不同的,这种加密方式是用数学上的难解问题构造的,通常加密解密的速度比较慢,适合偶尔发送数据的场合。优点是密钥传输方便。常见的非对称加密算法为RSA、ECC和EIGamal。

二.AES的工作模式

AES加密算法有多种工作模式,其实现细节也不相同:

  1. 电码本模式(Electronic Codebook Book (ECB));
  2. 密码分组链接模式(Cipher Block Chaining (CBC));
  3. 计算器模式(Counter (CTR));
  4. 密码反馈模式(Cipher FeedBack (CFB));
  5. 输出反馈模式(Output FeedBack (OFB))。

最常见的是ECB和CBC两种。

1.电码本模式(ECB)

ECB是最简单的加密方式,它是将整个明文分成若干段相同的小段,然后对每一小段进行加密。

  • 优点:操作简单,易于实现;分组独立,易于并行;误差不会被传送

  • 缺点:掩盖不了明文结构信息,难以抵抗统计分析攻击。

2.密码分组链接模式(CBC)

CBC是最常见的分组加密模式,它是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。

  • 优点:能掩盖明文结构信息,保证相同密文可得不同明文,所以不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL和IPSec的标准。

  • 缺点:(1)不利于并行计算;(2)传递误差——前一个出错则后续全错;(3)第一个明文块需要与一个初始化向量IV进行抑或,初始化向量IV的选取比较复杂。

    初始化IV的选取方式:固定IV,计数器IV,随机IV(只能得到伪随机数,用的最多),瞬时IV(难以得到瞬时值)

三.AES的填充模式

填充的作用是在加密前将普通文本的长度扩展到需要的长度。关键在于填充的数据能够在解密后正确的移除。
AES加密有5种填充模式:

  • PKCS7Padding(PKCS#7)
  • PKCS5Padding(PKCS#5)
  • Zero padding
  • ISO 10126
  • ANSI X.923

这些都属于字节填充,此处不再深究。

  

三.Java中使用AES

在Java中,指定加密算法的字符串是“算法/模式/填充方式”,比如“AES/ECB/PKCS5Padding”。

Java代码中使用AES需要添加额外的jar包:bcprov-jdk16-1.46.jar
或者增加如下依赖:

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>

Cipher类为加密和解密提供密码功能,它是一个引擎类,它需要通过静态工厂方法来实例化对象。

  • public static CiphergetInstance(String transformation)
  • public static Cipher getInstance(String transformation, Provider provider)

transformation是密码学参数,有“算法/模式/填充”或“算法”两种形式,例如

Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding")

或者

Cipher c = Cipher.getInstance("AES");

后一种情形没有指定加密模式和填充方式,就会使用特定提供者指定的默认模式或填充方式,比如SunJCE提供者默认ECB和PKCS5Padding作为AES和DES的默认模式和填充方式。

provider是一个可选参数,日常使用中较少用到。

getInstance工厂方法返回的对象没有进行初始化,因此在使用前必须进行初始化。

代码示例:

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.UUID;


public class AESUtil {


private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;


/**
* 加密
* CBC模式需要指定初始化向量iv,而ECB模式是不需要的
*
* @param content 二进制数据原始内容
* @param aesKeyByte 二进制密钥
* @param ivByte 二进制初始化向量
* @return 加密后的二进制数据
*/
public static byte[] encrypt(byte[] content, byte[] aesKeyByte, byte[] ivByte) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
SecretKeySpec skeySpec = new SecretKeySpec(aesKeyByte, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivParameterSpec);
return cipher.doFinal(content);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* 加密
*
* @param content 二进制数据原始内容
* @param aesKeyByte 二进制密钥
* @return 加密后的二进制数据
*/
public static byte[] encrypt(byte[] content, byte[] aesKeyByte) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
SecretKeySpec skeySpec = new SecretKeySpec(aesKeyByte, "AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
return cipher.doFinal(content);
} catch (Exception e) {
throw new RuntimeException(e);
}
}


/**
* 加密
*
* @param sourceText 需要加密的文本
* @param key 密钥
* @return 加密后的密文
*/
public static String encrypt(String sourceText, String key) {
Charset charset = DEFAULT_CHARSET;
byte[] encrypt = encrypt(sourceText.getBytes(charset), key.getBytes(charset));
return Base64.encodeBase64String(encrypt);
}

/**
* 加密
*
* @param sourceText 需要加密的明文
* @param key 密钥
* @param initVector 加密的初始化向量
* @return 加密后的密文
*/
public static String encrypt(String sourceText, String key, String initVector) {
Charset charset = DEFAULT_CHARSET;
byte[] encrypt = encrypt(sourceText.getBytes(charset), key.getBytes(charset), initVector.getBytes());
return Base64.encodeBase64String(encrypt);
}


/**
* 解密
* CBC模式需要指定初始化向量iv,而ECB模式是不需要的
*
* @param content 加密后的二进制数据
* @param aesKeyByte 二进制密钥
* @param ivByte 二进制的初始向量
* @return 解密后的二进制数据
*/
public static byte[] decrypt(byte[] content, byte[] aesKeyByte, byte[] ivByte) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
Key sKeySpec = new SecretKeySpec(aesKeyByte, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, sKeySpec, ivParameterSpec);
return cipher.doFinal(content);
} catch (Exception e) {
throw new RuntimeException(e);
}
}


/**
* 解密
*
* @param content 加密后的二进制数据
* @param aesKeyByte 二进制密钥
* @return 解密后的二进制数据
*/
public static byte[] decrypt(byte[] content, byte[] aesKeyByte) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
Key sKeySpec = new SecretKeySpec(aesKeyByte, "AES");
cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
return cipher.doFinal(content);
} catch (Exception e) {
throw new RuntimeException(e);
}
}


/**
* 解密
*
* @param sourceText 加密后的密文
* @param key 密钥
* @return 解密后的原文
*/
public static String decrypt(String sourceText, String key) {
Charset charset = DEFAULT_CHARSET;
byte[] bytes = Base64.decodeBase64(sourceText);
byte[] encrypt = decrypt(bytes, key.getBytes(charset));
return new String(encrypt, charset);
}


/**
* 根据密码生成一个128位的AES密钥
*
* @param password 密码
* @return Base64编码的AES密钥
* @throws NoSuchAlgorithmException
*/
public static String generateSecretKey(String password) throws NoSuchAlgorithmException {

//构造密钥生成器,指定AES算法
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");

//根据传入的密钥字节生成一个可信任的随机数据源
SecureRandom secureRandom = new SecureRandom(password.getBytes(DEFAULT_CHARSET));

//初始化一个128的密钥生成器
keyGenerator.init(128, secureRandom);

//生成一个原始的对称密钥
SecretKey originSecretKey = keyGenerator.generateKey();

//根据原始对称密钥生成一个AES二进制密钥
byte[] keyEncoded = originSecretKey.getEncoded();

//AES二进制密钥用Base64编码
return Base64.encodeBase64String(keyEncoded);
}


public static void main(String[] args) throws Exception {
String text = "hello world";
String password = UUID.randomUUID().toString();
String secretKey = generateSecretKey(password);
System.err.println("secretKey:" + secretKey);

//encrypt方法加密后的明文使用了Base64编码,decrypt解密时先用Base64解密
String encrypt = encrypt(text, secretKey);
System.out.println("encrypt:" + encrypt);
String decrypt = decrypt(encrypt, secretKey);
System.out.println("decrypt:" + decrypt);

System.out.println(new String(text.getBytes()));
System.out.println(new String(Base64.decodeBase64(text)));
}
}

Illegal key size or default parameters异常处理

Java中使用AES加解密时可能会出现异常:java.security.InvalidKeyException: Illegal key size or default parameters。

产生原因

因为美国的出口限制,Sun通过权限文件(local_policy.jar、US_export_policy.jar)做了相应限制,从而会导致一些问题:

  • 密钥长度上不能满足需求(如:java.security.InvalidKeyException: Illegal key size or default parameters);
  • 部分算法未能支持,如MD4、SHA-224等算法;
  • API使用起来还不是很方便;一些常用的进制转换辅助工具未能提供,如Base64编码转换、十六进制编码转换等工具。

而AES加解密又用到了这些java安全库,所以就报了Illegal key size or default parameters的异常。

解决办法

Oracle在其官方网站上提供了无政策限制权限文件(Unlimited Strength Jurisdiction Policy Files),我们只需要将其部署在JRE环境中,就可以解决限制问题。
在官方网站下载JCE无限制权限策略文件

JDK7的下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
JDK8的下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html

下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt。

切换到%JDK_Home%\jre\lib\security目录下,对应覆盖local_policy.jar和US_export_policy.jar两个文件。同时,你可能有必要在%JRE_Home%\lib\security目录下,也需要对应覆盖这两个文件。

注:JDK1.8.0_161之后的JDK版本不需要单独的无限制策略文件,所以直接升级JDK版本也是一种解决方式。

------ 本文完 ------