AES算法简介
AES的全称是Advanced Encryption Standard,意思是高级加密标准。 AES密码分组大小和密钥大小可以为128位、192位和256位。然而AES只要求分组大小为128位。本文只对分组大小128位,密钥长度也为128位的Rijndael算法进行分析。密钥长度为192位和256位的处理方式和128位的处理方式类似,只不过密钥长度每增加64位,算法的循环次数就增加2轮,128位循环10轮、192位循环12轮、256位循环14轮。
AES算法使用逻辑就是:发送方将要发送的明文数据X使用秘钥K进行AES加密后会得到密文Y,将密文进行网络传输,接受方在收到密文Y后使用秘钥K进行AES解密后技能得到明文X,这样即使密文Y在网络上传输时被截获了,没有秘钥也难以破解其真实意思。

AES算法相关数学知识
在AES算法中的MixColumn层中会用到伽罗瓦域中的乘法运算,而伽罗瓦域的运算涉及一些数学知识如下:
素域:
有限域有时也称伽罗瓦域,它指的是由有限个元素组成的集合,在这个集合内可以执行加、减、乘和逆运算。而在密码编码学中,我们只研究拥有有限个元素的域,也就是有限域。域中包含元素的个数称为域的阶。只有当m是一个素数幂时,即m=pn(其中n为正整数是p的次数,p为素数),阶为m的域才存在。p称为这个有限域的特征。也就是说,有限域中元素的个数可以是11(p=11是一个素数,n=1)、可以是81(p=3是一个素数,n=4)、也可以是256(p=2是一个素数,n=8).....但有限域的中不可能拥有12个元素,因为12=2·2·3,因此12也不是一个素数幂。有限域中最直观的例子就是阶为素数的域,即n=1的域。的元素可以用整数0、1、...、p-1l来表示。域的两种操作就是模整数加法和整数乘法模p。加上p是一个素数,整数环Z表示为GF(p),也成为拥有素数个元素的素数域或者伽罗瓦域。GF(p)中所有的非零元素都存在逆元,GF(p)内所有的运算都是模p实现的。
素域内的算数运算规则如下:
(1)加法和乘法都是通过模p实现的;
(2)任何一个元素a的加法逆元都是由a+(a的逆元)=0 mod p得到的;
(3)任何一个非零元素a的乘法逆元定义为a·a的逆元=1。
例:在素域GF(5)={0、1、2、3、4}中,2的加法逆元为3,这是因为2+(3)=5,5mod5=0,所以2+3=5mod5=0。2的乘法逆元为3,这是因为2·3=6,6mod5=1,所以2·3=6mod5=1。(在很多地方a的加法逆元用-a表示,a的乘法逆元用1/a表示)
注:GF(2)是一个非常重要的素域,也是存在的最小的有限域,由于GF(2)的加法,即模2加法与异或(XOR)门等价,GF(2)的乘法与逻辑与(AND)门等价,所以GF(2)对AES非常重要。
扩展域:
如果有限域的阶不是素数,则这样的有限域内的加法和乘法运算就不能用模整数加法和整数乘法模p表示。而且m>1的域被称为扩展域,为了处理扩展域,我们就要使用不同的符号表示扩展域内的元素,使用不同的规则执行扩展域内元素的算术运算。
在扩展域GF(2^m)中,元素并不是用整数表示的,而是用系数为域GF(2)中元素的多项式表示。这个多项式最大的度(幂)为m-1,所以每个元素共有m个系数,在AES算法使用的域GF(2^8)中,每个元素A∈GF(28)都可以表示为:

注意:在域GF(28)中这样的多项式共有256个,这256个多项式组成的集合就是扩展域GF(28)。每个多项式都可以按一个8位项链的数值形式存储:

像x7、x6等因子都无需存储,因为从位的位置就可以清楚地判断出每个系数对应的幂。
扩展域GF(2m)内的加减法:
在AES算法中的密钥加法层中就使用了这部分的知识,但是不是很明显,因为我们通常把扩展域中的加法当作异或运算进行处理了,因为在扩展域中的加减法处理都是在底层域GF(2)内完成的,与按位异或运算等价。假设A(x)、B(x)∈GF(2m),计算两个元素之和的方法就是:

而两个元素之差的计算公式就是:

注:在减法运算中减号之所以变成加号,这就和二进制减法的性质有关了。从上述两个公式中我们发现在扩展域中加法和减法等价,并且与XOR等价(异或运算也被称作二进制加法)。
扩展域GF(2^m)内的乘法
扩展域的乘法主要运用在AES算法的列混淆层(Mix Column)中,也是列混淆层中最重要的操作。我们项要将扩展域中的两个元素用多项式形式展开,然后使用标准的多项式乘法规则将两个多项式相乘:

注:通常在多项式乘法中C(x)的度会大于m-1,因此需要对此进行化简,而化简的基本思想与素域内乘法情况相似:在素域GF(p)中,将两个整数相乘得到的结果除以一个素数,化简后的结果就是最后的余数。而在扩展域中进行的操作就是:将两个多项式相乘的结果除以一个不可约多项式,最后的结果就是最后的余数。(这里的不可约多项式大致可以看作一个素数)

例:

AES算法原理
AES算法主要有四种操作处理,分别是密钥加法层(也叫轮密钥加,英文Add Round Key)、字节代换层(SubByte)、行位移层(Shift Rows)、列混淆层(Mix Column)。而明文x和密钥k都是由16个字节组成的数据(当然密钥还支持192位和256位的长度,暂时不考虑),它是按照字节的先后顺序从上到下、从左到右进行排列的。而加密出的密文读取顺序也是按照这个顺序读取的,相当于将数组还原成字符串的模样了,然后再解密的时候又是按照4·4数组处理的。AES算法在处理的轮数上只有最后一轮操作与前面的轮处理上有些许不同(最后一轮只是少了列混淆处理),在轮处理开始前还单独进行了一次轮密钥加的处理。在处理轮数上,我们只考虑128位密钥的10轮处理。接下来,就开始一步步的介绍AES算法的处理流程了。

AES算法流程图:

密钥加法层
在密钥加法层中有两个输入的参数,分别是明文和子密钥k[0],而且这两个输入都是128位的。k[0]实际上就等同于密钥k,具体原因在密钥扩展生成中进行介绍。我们前面在介绍扩展域加减法中提到过,在扩展域中加减法操作和异或运算等价,所以这里的处理也就异常的简单了,只需要将两个输入的数据进行按字节异或操作就会得到运算的结果。
图示:


1 int AddRoundKey(unsigned char(*PlainArray)[4], unsigned char(*ExtendKeyArray)[44], unsigned int MinCol)
2 {
3 int ret = 0;
4
5 for (int i = 0; i < 4; i++)
6 {
7 for (int j = 0; j < 4; j++)
8 {
9 PlainArray[i][j] ^= ExtendKeyArray[i][MinCol + j];
10 }
11 }
12
13 return ret;
14 }
字节代换层
字节代换层的主要功能就是让输入的数据通过S_box表完成从一个字节到另一个字节的映射,这里的S_box表是通过某种方法计算出来的,具体的计算方法在此不做详述,我们主需要知道如何使用S_box结果即可。S_box表是一个拥有256个字节元素的数组,可以将其定义为一维数组,也可以将其定义为16·16的二维数组,如果将其定义为二维数组,读取S_box数据的方法就是要将输入数据的每个字节的高四位作为第一个下标,第四位作为第二个下标,略有点麻烦。这里建议将其视作一维数组即可。逆S盒与S盒对应,用于解密时对数据处理,我们对解密时的程序处理称作逆字节代换,只是使用的代换表盒加密时不同而已。
S盒

逆S盒

加密图示:


1 //S盒
2 const unsigned char S_Table[16][16] =
3 {
4 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
5 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
6 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
7 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
8 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
9 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
10 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
11 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
12 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
13 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
14 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
15 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
16 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
17 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
18 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
19 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
20 };
21
22 //字节代换
23 int Plain_S_Substitution(unsigned char *PlainArray)
24 {
25 int ret = 0;
26
27 for (int i = 0; i < 16; i++)
28 {
29 PlainArray[i] = S_Table[PlainArray[i] >> 4][PlainArray[i] & 0x0F];
30 }
31
32 return ret;
33 }
34
35
36 //逆S盒
37 const unsigned char ReS_Table[16][16] =
38 {
39 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
40 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
41 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
42 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
43 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
44 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
45 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
46 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
47 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
48 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
49 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
50 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
51 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
52 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
53 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
54 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
55 };
56 //逆字节代换
57 int Cipher_S_Substitution(unsigned char *CipherArray)
58 {
59 int ret = 0;
60
61 for (int i = 0; i < 16; i++)
62 {
63 CipherArray[i] = ReS_Table[CipherArray[i] >> 4][CipherArray[i] & 0x0F];
64 }
65
66 return ret;
67 }
行位移——ShiftRows
行位移操作最为简单,它是用来将输入数据作为一个4·4的字节矩阵进行处理的,然后将这个矩阵的字节进行位置上的置换。ShiftRows子层属于AES手动的扩散层,目的是将单个位上的变换扩散到影响整个状态当,从而达到雪崩效应。在加密时行位移处理与解密时的处理相反,我们这里将解密时的处理称作逆行位移。它之所以称作行位移,是因为它只在4·4矩阵的行间进行操作,每行4字节的数据。
加密时:保持矩阵的第一行不变,第二行向左移动8Bit(一个字节)、第三行向左移动2个字节、第四行向左移动3个字节。
解密时:保持矩阵的第一行不变,第二行向右移动8Bit(一个字节)、第三行向右移动2个字节、第四行向右移动3个字节。
正向行位移图解:

1 //这里将char二维数组强制转换成int一维数组处理int ShiftRows(unsigned int *PlainArray)
2 {
3 int ret = 0;
4
5 //第一行 不移位
6 //PlainArray[0] = PlainArray[0];
7
8 //第二行 左移8Bit
9 PlainArray[1] = (PlainArray[1] >> 8) | (PlainArray[1] << 24);
10
11 //第三行 左移16Bit
12 PlainArray[2] = (PlainArray[2] >> 16) | (PlainArray[2] << 16);
13
14 //第四行 左移24Bit
15 PlainArray[3] = (PlainArray[3] >> 24) | (PlainArray[3] << 8);
16
17 return ret;
18 }
逆向行位移图解:


1 //这里将char二维数组强制转换成int一维数组处理
2 int ReShiftRows(unsigned int *CipherArray)
3 {
4 int ret = 0;
5
6 //第一行 不移位
7 //CipherArray[0] = CipherArray[0];
8
9 //第二行 右移8Bit
10 CipherArray[1] = (CipherArray[1] << 8) | (CipherArray[1] >> 24);
11
12 //第三行 右移16Bit
13 CipherArray[2] = (CipherArray[2] << 16) | (CipherArray[2] >> 16);
14
15 //第四行 右移24Bit
16 CipherArray[3] = (CipherArray[3] << 24) | (CipherArray[3] >> 8);
17
18 return ret;
19 }
列混淆——MixColumn
列混淆子层是AES算法中最为复杂的部分,属于扩散层,列混淆操作是AES算法中主要的扩散元素,它混淆了输入矩阵的每一列,使输入的每个字节都会影响到4个输出字节。行位移子层和列混淆子层的组合使得经过三轮处理以后,矩阵的每个字节都依赖于16个明文字节成可能。其中包含了矩阵乘法、伽罗瓦域内加法和乘法的相关知识。
在加密的正向列混淆中,我们要将输入的4·4矩阵左乘一个给定的4·4矩阵。而它们之间的加法、乘法都在扩展域GF(28)中进行,所以也就可以将这一个步骤分成两个部分进行讲解:
1) 正向列混淆
正向列混淆的原理图如下:

根据矩阵的乘法可知,在列混淆的过程中,每个字节对应的值只与该列的4个值有关系。此处的乘法和加法都是定义在GF(28)上的,需要注意如下几点:
1) 将某个字节所对应的值乘以2,其结果就是将该值的二进制位左移一位,如果原始值的最高位为1,则还需要将移位后的结果异或00011011;
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) 逆向列混淆
逆向列混淆的原理图如下:

由于:

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

1 //列混淆左乘矩阵
2 const unsigned char MixArray[4][4] =
3 {
4 0x02, 0x03, 0x01, 0x01,
5 0x01, 0x02, 0x03, 0x01,
6 0x01, 0x01, 0x02, 0x03,
7 0x03, 0x01, 0x01, 0x02
8 };
9
10 int MixColum(unsigned char(*PlainArray)[4])
11 {
12 int ret = 0;
13 //定义变量
14 unsigned char ArrayTemp[4][4];
15
16 //初始化变量
17 memcpy(ArrayTemp, PlainArray, 16);
18
19 //矩阵乘法 4*4
20 for (int i = 0; i < 4; i++)
21 {
22 for (int j = 0; j < 4; j++)
23 {
24 PlainArray[i][j] =
25 MixArray[i][0] * ArrayTemp[0][j] +
26 MixArray[i][1] * ArrayTemp[1][j] +
27 MixArray[i][2] * ArrayTemp[2][j] +
28 MixArray[i][3] * ArrayTemp[3][j];
29 }
30 }
31
32 return ret;
33 }
我们发现在矩阵乘法中,出现了加法和乘法运算,我们前面也提到过在扩展域中加法操作等同于异或运算,而乘法操作需要一个特殊的方式进行处理,于是我们就先把代码中的加号换成异或符号,然后将伽罗瓦域的乘法定义成一个有两个参数的函数,并让他返回最后计算结果。于是列混淆的代码就会变成下面的样子:

1 const unsigned char MixArray[4][4] =
2 {
3 0x02, 0x03, 0x01, 0x01,
4 0x01, 0x02, 0x03, 0x01,
5 0x01, 0x01, 0x02, 0x03,
6 0x03, 0x01, 0x01, 0x02
7 };
8
9 int MixColum(unsigned char(*PlainArray)[4])
10 {
11 int ret = 0;
12 //定义变量
13 unsigned char ArrayTemp[4][4];
14
15 //初始化变量
16 memcpy(ArrayTemp, PlainArray, 16);
17
18 //矩阵乘法 4*4
19 for (int i = 0; i < 4; i++)
20 {
21 for (int j = 0; j < 4; j++)
22 {
23 PlainArray[i][j] =
24 GaloisMultiplication(MixArray[i][0], ArrayTemp[0][j]) ^
25 GaloisMultiplication(MixArray[i][1], ArrayTemp[1][j]) ^
26 GaloisMultiplication(MixArray[i][2], ArrayTemp[2][j]) ^
27 GaloisMultiplication(MixArray[i][3], ArrayTemp[3][j]);
28 }
29 }
30 return ret;
31 }
接下来我们就只用处理伽罗瓦域乘法相关处理了,由于前面介绍过相关概念,所以代码就不在此进行讲解了,大家可以参考下方的代码注释进行理解:

1 ///////////////////////////////////////////////////////////////
2 //功能: 伽罗瓦域内的乘法运算 GF(128)
3 //参数: Num_L 输入的左参数
4 // Num_R 输入的右参数
5 //返回值:计算结果
6 char GaloisMultiplication(unsigned char Num_L, unsigned char Num_R)
7 {
8 //定义变量
9 unsigned char Result = 0; //伽罗瓦域内乘法计算的结果
10
11 while (Num_L)
12 {
13 //如果Num_L最低位是1就异或Num_R,相当于加上Num_R * 1
14 if (Num_L & 0x01)
15 {
16 Result ^= Num_R;
17 }
18
19 //Num_L右移一位,相当于除以2
20 Num_L = Num_L >> 1;
21
22 //如果Num_R最高位为1
23 if (Num_R & 0x80)
24 {
25 //左移一位相当于乘二
26 Num_R = Num_R << 1; //注:这里会丢失最高位,但是不用担心
27
28 Num_R ^= 0x1B; //计算伽罗瓦域内除法Num_R = Num_R / (x^8(刚好丢失最高位) + x^4 + x^3 + x^1 + 1)
29 }
30 else
31 {
32 //左移一位相当于乘二
33 Num_R = Num_R << 1;
34 }
35 }
36 return Result;
37 }
在解密的逆向列混淆中与正向列混淆的不同之处在于使用的左乘矩阵不同,它与正向列混淆的左乘矩阵互为逆矩阵,也就是说,数据矩阵同时左乘这两个矩阵后,数据矩阵不会发生任何变化。
正向列混淆处理

逆向列混淆

加解密验证

加密部分讲解完毕,最后应该注意要将密文结果从矩阵形式还原成字符串形式输出!
AES密钥扩展

子密钥的生成是以列为单位进行的,一列是32Bit,四列组成子密钥共128Bit。生成子密钥的数量比AES算法的轮数多一个,因为第一个密钥加法层进行密钥漂白时也需要子密钥。密钥漂白是指在AES的输入盒输出中都使用的子密钥的XOR加法。子密钥在图中都存储在W[0]、W[1]、...、W[43]的扩展密钥数组之中。k1-k16表示原始密钥对应的字节,而图中子密钥k0与原始子密钥相同。在生成的扩展密钥中W的下标如果是4的倍数时(从零开始)需要对异或的参数进行G函数处理。扩展密钥生成有关公式如下:

1 1<= i <= 10 2 1<= j <= 3 3 w[4i] = W[4(i-1)] + G(W[4i-1]); 4 w[4i+j] = W[4(i-1)+j] + W[4i-1+j];

函数G()首先将4个输入字节进行翻转,并执行一个按字节的S盒代换,最后用第一个字节与轮系数Rcon进行异或运算。轮系数是一个有10个元素的一维数组,一个元素1个字节。G()函数存在的目的有两个,一是增加密钥编排中的非线性;二是消除AES中的对称性。这两种属性都是抵抗某些分组密码攻击必要的。


1 //用于密钥扩展 Rcon[0]作为填充,没有实际用途
2 const unsigned int Rcon[11] = { 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36 };
3
4
5 int Key_S_Substitution(unsigned char(*ExtendKeyArray)[44], unsigned int nCol)
6 {
7 int ret = 0;
8
9 for (int i = 0; i < 4; i++)
10 {
11 ExtendKeyArray[i][nCol] = S_Table[(ExtendKeyArray[i][nCol]) >> 4][(ExtendKeyArray[i][nCol]) & 0x0F];
12 }
13
14 return ret;
15 }
16
17
18 int G_Function(unsigned char(*ExtendKeyArray)[44], unsigned int nCol)
19 {
20 int ret = 0;
21
22 //1、将扩展密钥矩阵的nCol-1列复制到nCol列上,并将nCol列第一行的元素移动到最后一行,其他行数上移一行
23 for (int i = 0; i < 4; i++)
24 {
25 ExtendKeyArray[i][nCol] = ExtendKeyArray[(i + 1) % 4][nCol - 1];
26 }
27
28 //2、将nCol列进行S盒替换
29 Key_S_Substitution(ExtendKeyArray, nCol);
30
31 //3、将该列第一行元素与Rcon进行异或运算
32 ExtendKeyArray[0][nCol] ^= Rcon[nCol / 4];
33
34 return ret;
35 }
36
37
38 int CalculateExtendKeyArray(const unsigned char(*PasswordArray)[4], unsigned char(*ExtendKeyArray)[44])
39 {
40 int ret = 0;
41
42 //1、将密钥数组放入前四列扩展密钥组
43 for (int i = 0; i < 16; i++)
44 {
45 ExtendKeyArray[i & 0x03][i >> 2] = PasswordArray[i & 0x03][i >> 2];
46 }
47
48 //2、计算扩展矩阵的后四十列
49 for (int i = 1; i < 11; i++) //进行十轮循环
50 {
51 //(1)如果列号是4的倍数,这执行G函数 否则将nCol-1列复制到nCol列上
52 G_Function(ExtendKeyArray, 4*i);
53
54 //(2)每一轮中,各列进行异或运算
55 // 列号是4的倍数
56 for (int k = 0; k < 4; k++)//行号
57 {
58 ExtendKeyArray[k][4 * i] = ExtendKeyArray[k][4 * i] ^ ExtendKeyArray[k][4 * (i - 1)];
59 }
60
61 // 其他三列
62 for (int j = 1; j < 4; j++)//每一轮的列号
63 {
64 for (int k = 0; k < 4; k++)//行号
65 {
66 ExtendKeyArray[k][4 * i + j] = ExtendKeyArray[k][4 * i + j - 1] ^ ExtendKeyArray[k][4 * (i - 1) + j];
67 }
68 }
69 }
70
71 return ret;
72 }
AES解密流程图

至此,AES算法基础部分介绍完毕!
相关补充知识
欧几里得算法:
两个正整数r0和r1的gcd表示为gcd(r0, r1),它指的是可以被r0和r1同时整除的最大正整数,例如gcd(21, 6)=3。对与较小的整数而言gcd就是将两个整数进行因式分解,并找出最大的公因子。
例:r0=84,r1=30,因式分解:r0=2·2·3·7;r1=2·3·5;gcd的结果就是:gcd(30,84)=2·3=6。
gcd(r0,r1)=gcd(r0-r1,r1)其中假设r0>r1,并且两个数均是正整数。证明:gcd(r0,r1)=g,由于g可以同时被r0、r1整除,则可以记作r0=g·x、r1=g·y,其中x>y,并且x和y互为素数。所以得到:gcd(r0,r1)=gcd(r0-r1,r1)=gcd(g·(x-y),g·y)=g=gcd(ri,0)。
例:r0=973,r1=301,gcd的计算方式为:


1 //输入两个正整数r0>r1,输出计算结果
2 int gcd(int r0, int r1)
3 {
4 int r=0;
5 while(r1 != 0)
6 {
7 r = r0 % r1;
8 r0 = r1;
9 r1 = r;
10 }
11 return r0;
12 }
扩展欧几里得算法:
扩展欧几里得算法主要的应用不是为例计算最大公因子,它的在密码学中主要的作用是为了计算乘法逆元,乘法逆元在公钥密码学中占有着举足轻重的地位。当然,除了扩展欧几里得算法(EEA)除了可以计算gcd,它还可以计算以下形式的线性组合:
其中s和t都表示整型系数。关于如何计算这两个系数的推到过程这里就不介绍了,我们只给出最后的公式结论:
介绍完这些公式,我们来看看乘法的逆元是怎么计算的吧。假设我们要计算r1 mod r0的逆元,其中r1 < r0。我们前面提到过乘法的逆元计算公式为a*b=1 mod p,b就是a mod p的乘法逆元,也就是gcd(p,
a)=1的情况。下才存在乘法逆元。则s·r0+t·r1=1=gcd(r0,r1),将这个等式执行模r0计算可得:
例:计算12 mod 67,12的逆元,即gcd(67,12)=1
注:通常情况下不需要计算系数S,而且实际中一般也用不上它,另外结果t可能是一个负数,这种情况下就必须把t加是r0让人的结果为正,因为t=(t+r0) mod r0。

1 int EEA(int r0, int r1)
2 {
3 int mod = r0;
4 int r = 0;
5 int t0 = 0;
6 int t1 = 1;
7 int t = t1;
8 int q = 0;
9
10 //0不存在乘法逆元
11 if (r1 == 0)
12 {
13 return 0;
14 }
15
16 while (r1 != 1)
17 {
18 q = r0 / r1;
19
20 r = r0 - q * r1;
21
22 t = t0 - q * t1;
23
24 r0 = r1;
25 r1 = r;
26 t0 = t1;
27 t1 = t;
28 }
29
30 //结果为负数
31 if (t < 0)
32 {
33 t = t + mod;
34 }
35
36 return t;
37 }
如果要想计算伽罗瓦域内乘法的逆元,函数的输入r0就是GF(2^8)的不可约多项式p(x),r1就是域元素a(x),然后通过EEA计算多项式t(x)得到a(x)的乘法逆元。只不过在上方给出的EEA代码略有不同,因为在伽罗瓦域中多项式都是在GF(2)上进行加减运算的,也就是说上面的加号和减号都要换成异或运算符,同时乘法和除法也有要进行适当的调整,转变成多项式乘法和除法。否则结果会出现偏差。
伽罗瓦域的扩展欧几里得算法:

1 //获取最高位
2 int GetHighestPosition(unsigned short Number)
3 {
4 int i = 0;
5 while (Number)
6 {
7 i++;
8 Number = Number >> 1;
9 }
10 return i;
11 }
12
13
14 //GF(2^8)的多项式除法
15 unsigned char Division(unsigned short Num_L, unsigned short Num_R, unsigned short *Remainder)
16 {
17 unsigned short r0 = 0;
18 unsigned char q = 0;
19 int bitCount = 0;
20
21 r0 = Num_L;
22
23 bitCount = GetHighestPosition(r0) - GetHighestPosition(Num_R);
24 while (bitCount >= 0)
25 {
26 q = q | (1 << bitCount);
27 r0 = r0 ^ (Num_R << bitCount);
28 bitCount = GetHighestPosition(r0) - GetHighestPosition(Num_R);
29 }
30 *Remainder = r0;
31 return q;
32 }
33
34
35
36 //GF(2^8)多项式乘法
37 short Multiplication(unsigned char Num_L, unsigned char Num_R)
38 {
39 //定义变量
40 unsigned short Result = 0; //伽罗瓦域内乘法计算的结果
41
42 for (int i = 0; i < 8; i++)
43 {
44 Result ^= ((Num_L >> i) & 0x01) * (Num_R << i);
45 }
46
47 return Result;
48 }
49
50
51 int EEA_V2(int r0, int r1)
52 {
53 int mod = r0;
54 int r = 0;
55 int t0 = 0;
56 int t1 = 1;
57 int t = t1;
58 int q = 0;
59
60 if (r1 == 0)
61 {
62 return 0;
63 }
64
65 while (r1 != 1)
66 {
67 //q = r0 / r1;
68 q = Division(r0, r1, &r);
69
70 r = r0 ^ Multiplication(q, r1);
71
72 t = t0 ^ Multiplication(q, t1);
73
74 r0 = r1;
75 r1 = r;
76 t0 = t1;
77 t1 = t;
78 }
79
80 if (t < 0)
81 {
82 t = t ^ mod;
83 }
84
85 return t;
86 }
生成S盒的过程:
AES的S盒具有非常强的代数结构,它是经过两个步骤计算而来的:

我们已经了解的逆元的计算过程,接下来只剩下了仿射映射过程了。
S盒的仿射映射:
仿射映射这个名词听起来有点高深莫测的感觉,不过在我的理解上,它就是一个计算过程。S盒的仿射映射也比较简单,主要就是运用到了矩阵乘法,不过这个矩阵是Bit矩阵。先上一下计算方法:

注意仿射映射所有的计算都是基于GF(2)上的。我们从计算过程上发现,输入数据A的逆元B在仿射映射中被展开成了一个8·1的矩阵,最上方是LSB,然后左乘了一个8·8的Bit矩阵,后加上了0x63展开的8·1矩阵,由于是基于GF(2)的,所以需要进行mod 2操作,最终的结果才是输出数据C。可能有些同学还是看不懂这张图,那我们就以输入数据为A=0x7为例,手动计算一下这个结果C:
1、计算乘法逆元:
将以下两个参数带入EEA_V2()函数可以得到A的逆元:

最终结果为:B=EEA(A)=0xD1
2、仿射映射(重点):
我们将上述结果B拆成Bit带入第二个矩阵得:


1 c[0] = (1*1) ^ (0*0) ^ (0*0) ^ (0*0) ^ (1*1) ^ (1*0) ^ (1*1) ^ (1*1) ^ 1 = 1 2 c[1] = (1*1) ^ (1*0) ^ (0*0) ^ (0*0) ^ (0*1) ^ (1*0) ^ (1*1) ^ (1*1) ^ 1 = 0 3 c[2] = (1*1) ^ (1*0) ^ (1*0) ^ (0*0) ^ (0*1) ^ (0*0) ^ (1*1) ^ (1*1) ^ 0 = 1 4 c[3] = (1*1) ^ (1*0) ^ (1*0) ^ (1*0) ^ (0*1) ^ (0*0) ^ (0*1) ^ (1*1) ^ 0 = 0 5 c[4] = (1*1) ^ (1*0) ^ (1*0) ^ (1*0) ^ (1*1) ^ (0*0) ^ (0*1) ^ (0*1) ^ 0 = 0 6 c[5] = (0*1) ^ (1*0) ^ (1*0) ^ (1*0) ^ (1*1) ^ (1*0) ^ (0*1) ^ (0*1) ^ 1 = 0 7 c[6] = (0*1) ^ (0*0) ^ (1*0) ^ (1*0) ^ (1*1) ^ (1*0) ^ (1*1) ^ (0*1) ^ 1 = 1 8 c[7] = (0*1) ^ (0*0) ^ (0*0) ^ (1*0) ^ (1*1) ^ (1*0) ^ (1*1) ^ (1*1) ^ 0 = 1
最后得到的仿射映射的结果为:C=0xC5
查看S盒,验证结果正确!

1 unsigned char ByteImage(int imput)
2 {
3 unsigned char Result = 0;
4
5 for (int i = 0; i < 8; i++)
6 {
7 Result ^= (((imput >> i) & 1) ^ ((imput >> ((i + 4) % 8)) & 1) ^ ((imput >> ((i + 5) % 8)) & 1) ^ ((imput >> ((i + 6) % 8)) & 1) ^ ((imput >> ((i + 7) % 8)) & 1)) << i;
8 }
9
10 Result = Result ^ 0x63;
11
12 return Result;
13 }
生成逆S盒的过程:

可以发现逆S盒是先进行逆仿射映射,然后才计算乘法逆元的,这也是与S盒生成的不同之处,而逆仿射映射与仿射映射的结构是相同的,只不过8·8Bit矩阵的数值不同,最后异或的那个数字不是0x63而是0xA0。

附件:AES算法演示动画
