AES加密

AES加密

一. 简介

1.1 背景

我们知道数据加密标准(Data Encryption Standard: DES)的密钥长度是56比特,因此算法的理论安全强度是256。但二十世纪中后期正是计算机飞速发展的阶段,元器件制造工艺的进步使得计算机的处理能力越来越强DES将不能提供足够的安全性

1997年1月2号,美国国家标准技术研究所(National Institute of Standards and Technology: NIST)宣布希望征集高级加密标准(Advanced Encryption Standard: AES),用以取代DES。AES得到了全世界很多密码工作者的响应,先后有很多人提交了自己设计的算法。最终有5个候选算法进入最后一轮:Rijndael,Serpent,Twofish,RC6和MARS。最终经过安全性分析、软硬件性能评估等严格的步骤,Rijndael算法获胜。

1.2 AES是什么?

Rijndael由比利时两位非常著名的密码学家Joan Daemen和Vincent Rijmen设计。Rijndael是一个分组密码算法族,其分组长度包括128比特、160比特、192比特、224比特、256比特,密钥长度也包括这五种长度,但是最终AES只选取了分组长度为128比特,密钥长度为128比特、192比特和256比特的三个版本。

本文主要结合AES-128进行介绍,AES-196和AES-256的思路基本一样,只是密钥扩展算法的过程会稍有不同,加解密的轮数会适当增加,但加解密的操作都是一样的。另外,本文只对AES算法的各个模块、基本原理进行介绍,旨在加深对算法流程、密码算法实现的了解。在正式软件运用中并不推荐自己编写代码,很多开源项目如Linux,OPENSSL,SRTP等都有非常高效的实现。由于数学知识的缺陷,本文不介绍算法安全性分析相关的知识,有兴趣的读者可以自行阅读相关文献。

AES是一个分组密码,属于对称密码范畴,AES算法的模块在对称密码领域特别是分组密码领域常有使用。

1.3 AES思维导图

AES思维导图


二. 算法流程

Rijndael算法是基于代换-置换网络(SPN,Substitution-permutation network)的迭代算法。明文数据经过多轮次的转换后方能生成密文,每个轮次的转换操作由轮函数定义。轮函数任务就是根据密钥编排序列(即轮密码)对数据进行不同的代换及置换等操作。

轮函数的流程

上图左侧为轮函数的流程,主要涉及4种操作:字节替代(SubBytes)、行移位(ShiftRows)、列混淆(MixColumns)和轮密钥加(AddRoundKey)。图右侧为密钥编排方案,在Rijndael中称为密钥扩展算法(KeyExpansion)。

下图给出了AES加解密的流程,从图中可以看出:

  1. 解密算法的每一步分别对应加密算法的逆操作。
  2. 加解密所有操作的顺序正好是相反的。

正是由于这几点(再加上加密算法与解密算法每步的操作互逆)保证了算法的正确性。加解密中每轮的密钥分别由种子密钥经过密钥扩展算法得到。算法中16字节的明文、密文和轮子密钥都以一个4x4的矩阵表示。AES标准算法将128位的明文,以特定次序生成一个4x4的矩阵(每个元素是一个字节,8位),即初始状态(state),经由轮函数的迭代转换之后又将作为下一轮迭代的输入继续参与运算直到迭代结束。

算法流程

Rijndael算法支持大于128位的明文分组,所以需要列数更多的矩阵来描述。Rijndael轮函数的运算是在特殊定义的有限域GF(256)上进行的。有限域(Finite Field)又名伽罗瓦域(Galois field),简单言之就是一个满足特定规则的集合,集合中的元素可以进行加减乘除运算,且运算结果也是属于此集合。更详细有有关Rijndael算法的数学描述,在此不做熬述。

2.1 字节替代

字节代替的主要功能是通过S盒完成一个字节到另外一个字节的映射,S盒的详细构造方法就不展开了,这里直接给出构造好的结果,下图(a)为S盒,图(b)为S-1(S盒的逆)。S盒用于提供密码算法的混淆性

S-BOX

Inverse S-BOX

S和S-1分别为16x16的矩阵,完成一个8比特输入到8比特输出的映射,输入的高4-bit对应的值作为行标,低4-bit对应的值作为列标。假设输入字节的值为a=a7a6a5a4a3a2a1a0,则输出值为 S[a7a6a5a4][a3a2a1a0] ,S-1的变换也同理。

例如:字节00000000B替换后的值为(S[0][0] =)63H,再通过S-1即可得到替换前的值,(S-1 [6][3] =)00H。

2.2 行移位

行移位是一个4x4的矩阵内部字节之间的置换,用于提供算法的扩散性

2.2.1 正向行移位

正向行移位用于加密,其原理图如下。其中:第一行保持不变,第二行循环左移8比特,第三行循环左移16比特,第四行循环左移24比特。

假设矩阵的名字为state,用公式表示如下:state’[i][j] = state[i][(j+i)%4] ;其中i、j属于[0,3]。

行移位

2.2.2 逆向行移位

逆向行移位即是相反的操作,即:第一行保持不变,第二行循环右移8比特,第三行循环右移16比特,第四行循环右移24比特。

用公式表示如下:state’[i][j] = state[i][(4+j-i)%4] ;其中i、j属于[0,3]。

2.3 列混淆

列混淆:利用GF(28)域上算术特性的一个代替,同样用于提供算法的扩散性

2.3.1 正向列混淆

正向列混淆的原理图如下:

列混淆原理图

根据矩阵的乘法可知,在列混淆的过程中,每个字节对应的值只与该列的4个值有关系。

此处的乘法和加法都是定义在GF(28)上的,需要注意如下几点:

  1. 将某个字节所对应的值乘以2,其结果就是将该值的二进制位左移一位,如果原始值的最高位为1,则还需要将移位后的结果异或00011011;[1]
  2. 乘法对加法满足分配率,例如:07·S0,0=(01⊕02⊕04)·S0,0= S0,0⊕(02·S0,0)(04·S0,0)
  3. 此处的矩阵乘法与一般意义上矩阵的乘法有所不同,各个值在相加时使用的是模28加法(异或运算)。

下面举一个例子,假设某一列的值如下图,运算过程如下:

运算过程

在计算02与C9的乘积时,由于C9对应最左边的比特为1,因此需要将C9左移一位后的值与(0001 1011)求异或。同理可以求出另外几个值。

2.3.2 逆向列混淆

逆向列混淆的原理图如下:

逆向列混淆原理图

由于:

由于

说明两个矩阵互逆,经过一次逆向列混淆后即可恢复原文。

2.4 轮密钥加

这个操作相对简单,其依据的原理是“任何数和自身的异或结果为0”。加密过程中,每轮的输入与轮子密钥异或一次;因此,解密时再异或上该轮的轮子密钥即可恢复。

2.5 密钥扩展算法

密钥扩展过程说明:

密钥扩展过程

  1. 将种子密钥按图(a)的格式排列,其中k0、k1、……、k15依次表示种子密钥的一个字节;排列后用4个32比特的字表示,分别记为w[0]、w[1]、w[2]、w[3];
  2. 按照如下方式,依次求解w[j],其中j是整数并且属于[4,43];
  3. 若j%4=0,则w[j]=w[j-4]⊕g(w[j-1]),否则w[j]=w[j-4]⊕w[j-1];

函数g的流程说明:

a) 将w循环左移8比特;

b) 分别对每个字节做S盒置换;

c) 与32比特的常量(RC[j/4],0,0,0)进行异或,RC是一个一维数组,其值如下。(RC的值只需要有10个,而此处用了11个,实际上RC[0]在运算中没有用到,增加RC[0]是为了便于程序中用数组表示。由于j的最小取值是4,j/4的最小取值则是1,因此不会产生错误。)

RC = {0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36}

2.6 总结

密码算法要求是可逆的,这样解密算法才能正确的恢复明文。拿AES来说,在密钥固定的情况下,明文和密文在整个输入空间是一一对应的。因此算法的各个部件也都是可逆的,再将各个部件的操作顺序设计成可逆的,密文就能正确的解密了。


二. 源码解析

暂无。


三. 实战应用

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
public class AesEncryptUtils {
//可配置到Constant中,并读取配置文件注入,16位,自己定义
private static final String KEY = "abcdefghijklmnop";

//参数分别代表 算法名称/加密模式/数据填充方式
private static final String ALGORITHMSTR = "AES/ECB/PKCS5Padding";

/**
* 加密
* @param content 加密的字符串
* @param encryptKey key值
* @return
* @throws Exception
*/
public static String encrypt(String content, String encryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), "AES"));
byte[] b = cipher.doFinal(content.getBytes("utf-8"));
// 采用base64算法进行转码,避免出现中文乱码
return Base64.encodeBase64String(b);
}

/**
* 解密
* @param encryptStr 解密的字符串
* @param decryptKey 解密的key值
* @return
* @throws Exception
*/
public static String decrypt(String encryptStr, String decryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
Cipher cipher = Cipher.getInstance(ALGORITHMSTR);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), "AES"));
// 采用base64算法进行转码,避免出现中文乱码
byte[] encryptBytes = Base64.decodeBase64(encryptStr);
byte[] decryptBytes = cipher.doFinal(encryptBytes);
return new String(decryptBytes);
}

public static String encrypt(String content) throws Exception {
return encrypt(content, KEY);
}
public static String decrypt(String encryptStr) throws Exception {
return decrypt(encryptStr, KEY);
}

public static void main(String[] args) throws Exception {
String content = "dhfkKjhsd23wefsd";
System.out.println("加密前:" + content);

String encrypt = encrypt(content, KEY);
System.out.println("加密后:" + encrypt);

String decrypt = decrypt(encrypt, KEY);
System.out.println("解密后:" + decrypt);
}
}

参考:

🔗 matt-wu/AES

🔗 密码算法详解——AES

🔗 AES加密算法的详细介绍与实现