文章目录
  1. 1. 加密算法
  2. 2. AES
    1. 2.1. AES 工作模式
    2. 2.2. AES 填充方式
  3. 3. Java 中的 AES
    1. 3.1. ECB
    2. 3.2. CBC
    3. 3.3. GCM
      1. 3.3.1. 密文和 authentication tag
  4. 4. 源码
  5. 5. 参考文档

加密算法

加密算法通常分为对称加密算法和非对称加密算法:

  • 对称加密算法(symmetric-key cryptography):加密和解密时使用相同的密钥。常用的对称加密算法有 DES、AES。
  • 非对称加密算法(asymmetric-key cryptography):加密和解密使用不同的密钥,例如公钥加密的内容只能用私钥解密,所以又称为公钥加密算法(public-key cryptography)。使用最广泛的非对称加密算法是 RSA 算法。

两者有不同的使用场景,而且经常会一起搭配起来使用,例如 SSL/TLS 协议就结合了对称加密算法和非对称加密算法。

本文主要介绍最常用的对称加密算法:AES。

AES

AES 全称 Advanced Encryption Standard,是一种对称加密算法。AES 的出现主要是用来取代 DES 加密算法,因为 AES 的安全性相对更高。

AES 使用非常广泛,可以说只要上网,无论是使用手机 APP 还是 Web 应用,几乎都离不开 AES 加密算法。因为目前大部分网站,包括手机 APP 后端接口,都已经使用 HTTPS 协议,而 HTTPS 在数据传输阶段大部分都是使用 AES 对称加密算法。

在学习 AES 之前,首先要知道以下规则:

  • AES 是一种区块加密算法,加密时会将原始数据按大小拆分成一个个区块进行加密,区块大小固定为 128 比特(即 16 字节)
  • AES 密钥长度可以是 128、192 或 256 比特(即 16、25 或 32 字节),密钥长度越长,安全性越高,而性能也就越低

AES 工作模式

AES加密算法有多种工作模式(mode of operation),如:ECB、CBC、OFB、CFB、CTR、XTS、OCB、GCM。不同的模式参数和加密流程不同,但是核心仍然是 AES 算法。

本文主要介绍 ECB、CBC、GCM 三种模式。

AES 填充方式

由于 AES 是一种区块加密算法,加密时会将原始数据按大小拆分成一个个 128 比特(即 16 字节)区块进行加密,如果需要加密的原始数据不是 16 字节的整数倍时,就需要对原始数据进行填充,使其达到 16 字节的整数倍。

常用的填充方式有 PKCS5Padding、ISO10126Padding 等,另外如果能保证待加密的原始数据大小为 16 字节的整数倍,也可以选择不填充,即 NoPadding。

Java 中的 AES

Java 中的 javax.crypto.Cipher 类提供加密和解密的功能。

创建一个 Cipher

1
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

CiphergetInstance 方法需传递一个加密算法的名称作为参数,用来创建对应的 Cipher,其格式为 algorithm/mode/padding,即 算法名称/工作模式/填充方式,例如 AES/CBC/PKCS5Padding。具体有哪些可选的加密方式,可以参考文档:

https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher

ECB

ECB 全称为电子密码本(Electronic codebook),将待加密的数据拆分成块,并对每个块进行独立加密。

ECB 加密
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
public static byte[] encryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
byte[] result = cipher.doFinal(data);
return result;
}

public static byte[] decryptECB(byte[] data, byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"));
byte[] result = cipher.doFinal(data);
return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
String data = "Hello World"; // 待加密的明文
String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节

byte[] ciphertext = encryptECB(data.getBytes(), key.getBytes());
System.out.println("ECB 模式加密结果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

byte[] plaintext = decryptECB(ciphertext, key.getBytes());
System.out.println("解密结果:" + new String(plaintext));
}

由于加密后的密文是二进制格式而非字符串,所以这里使用了 Base64 编码方式将其转换成字符串方便输出查看。输出:

1
2
ECB 模式加密结果(Base64):bB0gie8pCE2RBQoIAAIxeA==
解密结果:Hello World

要想知道上面代码的加密结果是否正确,需要找个“裁判”来判断一下,这里可以用 openssl 命令来作为这个“裁判”:

1
2
$ echo -n 'Hello World' | openssl enc -base64 -aes-128-ecb -K $(echo -n '12345678abcdefgh' | od -A n -t x1 | sed 's/ *//g')
bB0gie8pCE2RBQoIAAIxeA==

加密结果和 Java 代码加密结果一致。

需要注意,AES 密钥长度只能是 16、25 或 32 字节,如果不符合要求则会异常:

1
java.security.InvalidKeyException: Invalid AES key length

CBC 模式有一个致命的缺点,由于该模式对每个块进行独立加密,会导致同样的明文块被加密成相同的密文块,相对来说并不是非常安全。下图就是一个很好的例子:

ECB 模式

CBC

CBC 全称为密码分组链接(Cipher-block chaining),它的出现解决 ECB 同样的明文块会被加密成相同的密文块的问题。

CBC 引入了初始向量的概念(IV,Initialization Vector),第一个明文块先与 IV 进行异或后再加密,后续每个明文块先与前一个密文块进行异或后再加密。

CBC 加密
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
public static byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] result = cipher.doFinal(data);
return result;
}

public static byte[] decryptCBC(byte[] data, byte[] key, byte[] iv) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] result = cipher.doFinal(data);
return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
String data = "Hello World"; // 待加密的原文
String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节
String iv = "iviviviviviviviv"; // CBC 模式需要用到初始向量参数

byte[] ciphertext = encryptCBC(data.getBytes(), key.getBytes(), iv.getBytes());
System.out.println("CBC 模式加密结果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

byte[] plaintext = decryptCBC(ciphertext, key.getBytes(), iv.getBytes());
System.out.println("解密结果:" + new String(plaintext));
}

输出:

1
2
CBC 模式加密结果(Base64):K7bSB51+KxfqaMjJOsPAQg==
解密结果:Hello World

用 openssl 命令检查一下上面代码加密结果是否正确:

1
2
$ echo -n 'Hello World' | openssl enc -base64 -aes-128-cbc -K $(echo -n '12345678abcdefgh' | od -A n -t x1 | sed 's/ *//g') -iv $(echo -n 'iviviviviviviviv' | od -A n -t x1 | sed 's/ *//g')
K7bSB51+KxfqaMjJOsPAQg==

GCM

GCM 的全称是 Galois/Counter Mode,它是一种认证加密(authenticated encryption)算法。它不但提供了加密解密,还提供了数据完整性校验,防止篡改。

AES-GCM 模式是目前使用最广泛的模式,可以尝试抓包看一下目前主流的 https 网站,其中大部分都是基于 GCM 模式。下图是使用抓包工具 Charles 查看浏览器访问 https 网站所使用的加密算法:

抓包

可以看到浏览器一般支持 AES-GCM 和 AES-CBC 模式,最终服务器选择使用 AES-GCM。

AES-GCM 认证加密需要用到以下参数:

  • 待加密的明文
  • 密钥
  • 初始向量 IV
  • additional authenticated data (AAD),可以为空
  • authentication tag 比特位数,必须是 128、120、112、104、96 之一

代码:

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
public static byte[] encryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad, int tagLength) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(tagLength, iv));
cipher.updateAAD(aad);
byte[] result = cipher.doFinal(data);
return result;
}

public static byte[] decryptGCM(byte[] data, byte[] key, byte[] iv, byte[] aad, int tagLength) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(tagLength, iv));
cipher.updateAAD(aad);
byte[] result = cipher.doFinal(data);
return result;
}

public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
String data = "Hello World"; // 待加密的原文
String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节
String iv = "iviviviviviviviv";
String aad = "aad"; // AAD 长度无限制,可为空
int tagLength = 128; // tag 长度必须是 128、120、112、104、96 之一

byte[] ciphertext = encryptGCM(data.getBytes(), key.getBytes(), iv.getBytes(), aad.getBytes(), tagLength);
System.out.println("GCM 模式加密结果(Base64):" + Base64.getEncoder().encodeToString(ciphertext));

byte[] plaintext = decryptGCM(ciphertext, key.getBytes(), iv.getBytes(), aad.getBytes(), tagLength);
System.out.println("解密结果:" + new String(plaintext));
}

输出:

1
2
GCM 模式加密结果(Base64):1UxXmFpdUwMnpI7rh0XfmFqtdZSHTbNC/08g
解密结果:Hello World

AES-GCM 是流加密(Stream cipher)算法,所以对应的填充模式为 NoPadding,即无需填充。

由于 openssl 命令不支持 GCM 模式,所以这里不能用 openssl 命令来验证加密结果。

密文和 authentication tag

如果看过 NIST 的 GCM 文档 , 可以看到其 GCM 模式的认证加密实际上会有两个返回值:密文和 authentication tag。

The following two bit strings comprise the output data of the authenticated encryption function:

  • A ciphertext, denoted C, whose bit length is the same as that of the plaintext.
  • An authentication tag, or tag, for short, denoted T.

其中,authentication tag 是用于校验数据完整性和防篡改的:

A cryptographic checksum on data that is designed to reveal both accidental errors and the intentional modification of the data.

上面的 Java 代码中,加密方法的返回值只有一个字节数组,实际上这个返回值已经包含了密文和 authentication tag,authentication tag 是追加在密文的尾部。所以如果要想分别拿到密文和 authentication tag,需要对返回的字节数组进行拆分:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
String data = "Hello World"; // 待加密的原文
String key = "12345678abcdefgh"; // key 长度只能是 16、25 或 32 字节
String iv = "iviviviviviviviv";
String aad = "aad"; // AAD 长度无限制,可为空
int tagLength = 128; // tag 长度必须是 128、120、112、104、96 之一
int tagLengthBytes = tagLength / 8; // 比特数转字节数

byte[] result = encryptGCM(data.getBytes(), key.getBytes(), iv.getBytes(), aad.getBytes(), tagLength);

byte[] ciphertext = Arrays.copyOfRange(result, 0, result.length - tagLengthBytes); // 密文
byte[] tag = Arrays.copyOfRange(result, result.length - tagLengthBytes, result.length); // authentication tag
}

源码

https://github.com/wucao/XToolkit/blob/master/src/main/java/com/xxg/xtoolkit/AesUtil.java

参考文档

文章目录
  1. 1. 加密算法
  2. 2. AES
    1. 2.1. AES 工作模式
    2. 2.2. AES 填充方式
  3. 3. Java 中的 AES
    1. 3.1. ECB
    2. 3.2. CBC
    3. 3.3. GCM
      1. 3.3.1. 密文和 authentication tag
  4. 4. 源码
  5. 5. 参考文档