摘要加密算法一般是用来保证数据的完整性和一致性,上一篇文章中提到了两个常用的摘要算法分别是SHA和MD5。在这篇里面,一起来学习一下一个常用的对称加密算法AES,它的主要作用是保证私密信息不被泄漏。对于这个应该都很熟悉,只要使用到对称加密,相信百分八十以上都是用AES。
接下来将会根据其不同的组成部分、特性、使用以及存在的坑做一下详细的说明。
本篇文章借鉴于小灰公众号文章:
什么是AES算法?(整合版)
1. 使用场景
在数据加密传输的时候经常会用到AES,接收方可以根据秘钥对密文进行解密获取其内容。使用步骤如下:
- 数据输出方通过秘钥”123456”将一段明文数据加密处理
- 将密文通过网络发送给接收方
- 接收方通过解密获得明文数据
如图示意:
了解AES在什么时候用到,下面来看一下AES重要的组成部分。
2. 重要组成部分:秘钥
秘钥是非常重要的组成部分,也是决定数据安全性的重要部分。在AES中秘钥的长度有三种分别是128、192、256,所以AES算法又被分为AES128、AES192、AES256。秘钥的长度决定计算的复杂程度和安全性。一般使用到AES128即可。(使用后两种需要做特殊处理,后面有坑)
在AES加密的时候,并不是把整个AES加密成一个密文串,而是根据秘钥的长度,将明文切分成若干个明文块进行加密,得到若干个密文块,然后对密文块进行组装,最终形成一个密文串。当秘钥长度是128bit的时候,就会将明文切分成若干个128bit的明文块。对应192bit、256bit秘钥长度,也是类似切分。
示意图如下:
需要注意的问题:
秘钥不等于传入的key,因为这个key的长度是有要求的。如果是使用AES128,秘钥的长度必须是128bit,那么key的长度必须是128bit,在正常的过程中,很少能设置秘钥刚好长度是128bit,这个时候应该怎么办呢?在java中提供了生成秘钥的类。可以通过不同长度的key最终生成符合需求的秘钥。实现如下:
KeyGenerator generator = KeyGenerator.getInstance("AES");//指定加密方式
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");//指定SecureRandom的加密方式
secureRandom.setSeed("123456".getBytes());//将传入的key作为种子传入
generator.init(128, secureRandom);//指定长度
return generator.generateKey();//最终生成秘钥
更好的说法是将传入的key理解为秘钥种子。当然要是能做到长度刚刚好符合要求,称为秘钥也不是不可以。
3. 重要组成部分:填充
填充,是一个很重要的概念,因为不同的明文长度变化很大,不可能保证长度刚好是128、192、256的整数倍。这个时候就需要对不满足长度的明文块进行填充,保证长度符合要求。在AES中有Nopadding、PKCS5Padding、ISO10126Padding三种方式,其中PKCS5Padding是默认的方式。下面了解一下。
3.1 NoPadding
从名字就看的出来,不填充,要求明文就是指定长度的整数倍。这种模式基本用不到,对传入明文长度要求太高。可用性不高,知道即可。
3.2 PKCS5Padding
这种模式是默认的模式。当最后一个明文块长度不满足要求的时候,会以缺少长度的值来填充。比如切分长度按照128bit来算,最后一个明文块的长度是112bit,缺少16bit,对应的字节数是2,此时后面2个字节都是用2来填充。如下图所示:
3.3 ISO10126Padding
此模式也会根据实际缺少的部分进行填充,但不是填充固定值而是随机值,最后一位是填充的位数。如下所示:
总共填充4位,最后一位值是4,其他三个是随机值。
注意点:这里填充的过程需要注意,除了NoPadding外,其他两种填充模式,即使出现明文长度是对应要求长度的整数倍,也会再填充一个明文块。(从秘钥的那张示意图就可以看出来,明文是4倍的128bit,但是在图中总共画了5个明文块)
4. 重要组成部分:工作模式
工作模式总共有5种,但是常被使用到的就ECB和CBC两种模式。其中ECB是默认的工作模式,相对于效率最高,一般在开发中使用的比较多的还有一种是CBC,效率比ECB低一点,但是比ECB安全。
4.1 5种模式
- ECB:电码本模式(Electronic Codebook Book)
- CBC:密码分组链接模式(Cipher Block Chaining)
- CTR:计算器模式(Counter)
- CFB:密码反馈模式(Cipher Feedback)
- OFB:输出反馈模式(Output Feedback)
4.2 ECB(电码本模式)
多个明文块分别和秘钥进行加密操作,最终生成多个密文块,明文块组装成对应的密文信息。在加密的过程是并发操作,不会互相干扰,加密过程用时比较长。加密示意图如下:
优缺点:
- 并行加密,加密速度快,效率高
- 相同明文块加密结果相同,安全性差
4.3 CBC(密码分组链接模式)
在CBC模式下会引入一个初始向量的概念,先看一下其加密流程图如下:
在加密的过程中,将前一个密文块和当前明文块做异或操作,将操作的结果进行加密得到对应的密文块,以此类推。但是当加密第一个明文块的时候,前面没有对应的密文块与其进行异或操作,这个时候就需要引入一个初始向量,作为与第一个明文块做异或操作的值。整个加密过程都是依赖上一个加密结果,所以只能串行,无法并行操作。
优缺点:
- 安全性高
- 引入初始向量,加密过程复杂度高
- 整个加密过程无法并行计算,效率低
5. 代码使用、坑和解决方案
5.1 代码实现
首先来看一下我自己对AES的封装,JDK自带的加密方法是有的,但是不够简洁,每次加密都要写一大串代码,因此在使用的时候必须做一下封装,作为一个工具类存在。下面看一下代码:
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.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 填充方式:PKCS5Padding和NoPadding,默认使用PKCS5Padding,慎用NoPadding,因为用NoPadding要求传入的加密原文长度是16的倍数,条件台苛刻
* 工作模式:ECB和CBC,建议使用CBC,因为CBC加密更安全
* 长度:128,256,192,一般使用128即可
*/
public class AesUtils {
private final static String KEY = "123456";
private final static String IV = "654321";
private final static String ALGORITHM = "AES";
public static String encode(String content) throws Exception {
return encode(content, PaddingModel.PKCS5, WorkModel.CBC, Length.AES128);
}
public static String decode(String content) throws Exception {
return decode(content, PaddingModel.PKCS5, WorkModel.CBC, Length.AES128);
}
public static String encode(String content, PaddingModel paddingModel, WorkModel workModel, Length length) throws Exception {
Cipher cipher = cipher(paddingModel, workModel);
if (WorkModel.CBC.equals(workModel)) {
cipher.init(Cipher.ENCRYPT_MODE, keySpec(length), parameterSpec(length));
} else {
cipher.init(Cipher.ENCRYPT_MODE, keySpec(length));
}
byte[] bytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(bytes);
}
public static String decode(String secret, PaddingModel paddingModel, WorkModel workModel, Length length) throws Exception {
Cipher cipher = cipher(paddingModel, workModel);
if (WorkModel.CBC.equals(workModel)) {
cipher.init(Cipher.DECRYPT_MODE, keySpec(length), parameterSpec(length));
} else {
cipher.init(Cipher.DECRYPT_MODE, keySpec(length));
}
byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(secret));
return new String(bytes, StandardCharsets.UTF_8);
}
private static Cipher cipher(PaddingModel paddingModel, WorkModel workModel) throws Exception {
return Cipher.getInstance(ALGORITHM + "/" + workModel.name() + "/" + paddingModel.getName());
}
private static SecretKeySpec keySpec(Length length) throws Exception {
return new SecretKeySpec(keySpecBytes(length), ALGORITHM);//真正使用的秘钥
}
private static IvParameterSpec parameterSpec(Length length) throws Exception {
return new IvParameterSpec(ivSpecBytes(length));
}
private static byte[] keySpecBytes(Length length) throws Exception {
return secretKey(SourceType.KEY, length).getEncoded();
}
private static byte[] ivSpecBytes(Length length) throws Exception {
return secretKey(SourceType.IV, length).getEncoded();
}
private static SecretKey secretKey(SourceType sourceType, Length length) throws Exception {
KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);
/*SecureRandom secureRandom = new SecureRandom(IV.getBytes());
if (SourceType.KEY.equals(sourceType)) secureRandom = new SecureRandom(KEY.getBytes());*/
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(IV.getBytes());
if (SourceType.KEY.equals(sourceType)) secureRandom.setSeed(KEY.getBytes());
generator.init(length.getValue(), secureRandom);
return generator.generateKey();
}
private enum SourceType {
KEY, IV
}
}
测试使用的main方法:
public static void main(String[] args) throws Exception {
String encode = encode("6543217865432178", PaddingModel.PKCS5, WorkModel.CBC, Length.AES128);
System.out.println(encode);
System.out.println(decode(encode, PaddingModel.PKCS5, WorkModel.CBC,Length.AES128));
}
具体代码里的内容就不做解释了,其中使用到自定义的枚举,这列也具体贴出来,文末会给出源码地址。
5.2 坑和解决方案
在调试过程有遇到两个坑。第一个是AES支持的位数问题,因为Java本身只支持128,导致使用192或者256位的时候报错(异常信息:java.security.InvalidKeyException: Illegal key size)。具体解决方案参考下面这篇博客。
java.security.InvalidKeyException: Illegal key size
第二个坑就是根据key(秘钥种子)生成符合位数要求的秘钥时出现的,SecureRandom
中使用的PRNG是根据当前环境提供的PRNG获取的,由于不同的电脑,可能会出现PRNG不同,又或者提供的PRNG不支持,就会导致出现异常问题,不能正常的加解密。在调试过程,代码在win10系统下正常,同样的代码在mac中就会抛出异常。
通过了解一下SecureRandom
中获取PRNG的过程可以知道,首先它是自动获取本地提供的PRNG,如果获取不到就是用默认的PRNG,也就是SHA1PRNG。所以为了避免不同系统,不同环境出现问题,这里统一在构建SecureRandom
的时候就指定PRNG为SHA1PRNG即可解决。
这个问题可能会导致在本地运行正常,进入到测试或者生产环境出现异常,个人还是要重视一下的。
6. 源代码
码云(gitee):https://gitee.com/itcrud/itcrud-note/tree/master/itcrud-note-1-6