AES - Advanced Encryption Standard

AES 是取代 DES 称为新标准的一种对称密码算法

AES 的选拔

AES 由 NIST (National Institute of Standards and Technology, 国家标准技术研究所) 组织公开竞选,最终所选拔的密码算法,成为美国的国家标准,即联邦信息处理标准(FIPS), 但最终成为一个世界性的标准。

AES 选拔对全世界公开,参加者提供密码算法的详细规格书、算法强度的证明和实现代码,由全世界的企业和密码学家共同完成评审。2000 年, Rijndael 算法被 NIST 从最终候选算法名单中选定成为 AES 标准。

被选为 AES 密码算法必须无条件地免费供全世界使用。

Rijndael 密码算法

两位比利时密码学家设计,也属于*分组密码算法 *(block cipher)

分组长度: 128 比特位

密钥长度: 128、192 和 256 比特

Rijndael 的加密和解密

和 DES 一样,Rijndael 算法由多轮构成,其中每一轮分为 SubBytes、ShiftRows、MixColumns 和 AddRoundKey 个步骤。并且使用了一种称为 SPN 结构。

1. SubBytes-逐字节替换

Rijndael 的分组是 128 比特位,即 16 字节。对 16 个字节进行 SubBytes 处理,就是将每个字节所代表的无符号整数值(0-255)作为索引,从一个拥有 256 个值的替换表 (S-Box, S盒) 中查找对应的值。简单来说,就是每个字节查表映射为新的值。

SubBytes

2. ShiftRows-平移行

将以 4 字节为单位的行 (row) 按照一定的规则向左平移,且每一行平移的字节数是不同的。图示为对其中一行进行处理的情形:

ShiftRows

3. MixColumns-混合列

对一个 4 字节的值进行比特运算,将其变为另一个 4 字节的值,图示为对其中一列(column)进行处理的情形:

MixColumns

4. AddRoundKey-轮密钥异或运算

将 MixColumns 的输出与轮密钥进行 XOR,图示为对其中一个字节进行处理的情形:

AddRoundKey

输入的所有比特在一轮中都会被加密,相比于 DES 的 Feistel 网络相比(每一轮只加密一半输入的比特位),加密所需的轮数更少,一般重复进行10 到 14 轮,而且其中的步骤,可以分别以字节、行和列为单位进行并行计算。

注:并行计算也是衡量密码算法的一个重要指标,并行意味着加密解密的速度块

加密的过程:

1
SubBytes  --> SbiftRows --> MixColumns --> AddRoundKey

解密,就是按照相反的顺序来进行就可以:

1
2
3
AddRoundKey --> InvMixColumns --> InvShiftRows --> InvSubBytes

* Inv 表示逆运算

Rijndael 的算法背后有着严谨的数学结构,明文到密文的过程可以全部用数学公式来表达,此外,轮密钥的计算也涉及到稍微复杂的过程,没有在这里涉及。

使用 openssl 和 JCA 进行加密解密的验证

opesnssl 加密和解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用 openssl 加密 'hello' 字符串
$ openssl enc -aes-128-cbc -e -base64 -salt -p <<< hello

salt=3631C9DA382CE487
key=421990ECB00E9A9B8372D0E46727F6AA
iv =5973EAA7D1C1D5240B78CB8A4458D663
U2FsdGVkX182McnaOCzkh7BPMUJaKwGCIc8cAGAtbNw=

# 使用 openssl 进行解密
$ openssl enc -aes-128-cbc -d -base64 -salt -p <<< U2FsdGVkX182McnaOCzkh7BPMUJaKwGCIc8cAGAtbNw=

salt=3631C9DA382CE487
key=421990ECB00E9A9B8372D0E46727F6AA
iv =5973EAA7D1C1D5240B78CB8A4458D663
hello

使用 JCA (Java Cryptography Architecture) 进行验证

openssl 会生成 8 个比特的 salt,并以 Salted__${salt} 附加在加密的密文前,一共占 16 个字节,验证如下:

1
2
3
4
$ echo -n "U2FsdGVkX182McnaOCzkh7BPMUJaKwGCIc8cAGAtbNw=" | base64 -d | hexdump -C
00000000 53 61 6c 74 65 64 5f 5f 36 31 c9 da 38 2c e4 87 |Salted__61..8,..|
00000010 b0 4f 31 42 5a 2b 01 82 21 cf 1c 00 60 2d 6c dc |.O1BZ+..!...`-l.|
00000020

所以实际加密后密文的 16 进制为:b04f31425a2b018221cf1c00602d6cdc

使用 JCA 进行解密验证:

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
public class AESCryptoTest {

public static final String AES = "AES";
public static final String AES_CBC_PKCS_5_PADDING = "AES/CBC/PKCS5Padding";

public static void main(String[] args) {
String keyHex = "421990ECB00E9A9B8372D0E46727F6AA";
String ivHex = "5973EAA7D1C1D5240B78CB8A4458D663";
String cipherTextHex = "b04f31425a2b018221cf1c00602d6cdc";

byte[] key = HexUtils.fromHexString(keyHex);
byte[] iv = HexUtils.fromHexString(ivHex);
byte[] cipherText = HexUtils.fromHexString(cipherTextHex);

byte[] clearText = aesDecrypt(key, iv, cipherText);
System.out.println(new String(clearText, StandardCharsets.UTF_8));
}

@SneakyThrows
public static byte[] aesDecrypt(byte[] key, byte[] iv, byte[] cipherText) {
// constructor a secret key
SecretKeySpec secretKey = new SecretKeySpec(key, AES);

// init algorithm parameters with iv
AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(AES);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
algorithmParameters.init(ivParameterSpec);

// init a AES cipher with key and iv
Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING);
cipher.init(Cipher.DECRYPT_MODE, secretKey, algorithmParameters);

// decrypt
return cipher.doFinal(cipherText);
}
}

//最终输出 hello