AES算法是当前最流行的对称加密算法,也是一种分组加密算法,分组密码就是把明文分为固定长度的一组一组,每次加密一组数据,直到加密完整个明文数据。AES算法根据分组长度可以分为AES128, AES192,AES256,其所要求的秘钥长度和加密轮数也各不相同。鉴于这三种模式的算法在本质上没有区别,所以本文主要介绍AES-128(数据分组为16字节,秘钥长度为16字节,加密轮数为10轮),并给出C语言实现。
确切的说分组密码只是规定了怎么加密一组明文,如果明文数据比较长,其他的组需要怎么进行加密取决于使用何种分组密码工作模式。对于AES-128而言,每次只加密16字节长度的数据,如果明文长度为32字节话,我们很容易想到第2组16字节可以仿照第1组16字节数据进行处理,这就是最简单的分组密码工作模式ECB(电子密码本)模式,本文主要讲述AES算法实现,对于长数据也是使用这种最简单的ECB分组处理方式,更多其他分组密码工作模式,请参考另一篇文章图解分组密码五大工作模式。
前面讨论的数据长度都是16字节,或者其整倍数长度的加密算法实现,对于数据长度不是分组长度整倍数的情形,通常需要对数据进行填充,使其长度达到分组长度的整倍数再来进行加密。对于数据长度不足分组长度整倍数使用何种格式进行数据填充有多种不同的填充标准,比如在数据后面填充二进制的0x0,直到达到要求的长度,这就是ZeroPadding方式;比如数据缺少几位就填充二进制的几,例如缺少4位填充0x04 0x04 0x04 0x04,这就是PKCS7/PKCS5填充方式。本文提供的实现不涉及数据填充,假定明文数据都是16字节的整倍数长度。
AES算法主要可以分为秘钥扩展、字节替换、行移位、列混合和轮秘钥加这5个步骤。
AES-128加密流程可以使用如下伪代码表示:
AES-128加密(uint8 in[16], uint8 out[16], uint8 key[16]){
uint8 state[4,4] = in;
uint32 w[44] = KeyExpansions(key[16]);
addRoundKey(state, w[0-3]);
for (int j = 1; j < 10; ++j) {
subBytes(state);
shiftRows(state);
mixColumns(state);
addRoundKey(state, w); //w[4-7],w[8-11]…w[37-40]
}
subBytes(state);
shiftRows(state);
addRoundKey(state, w[41-44]);
out = state;
}
AES-128解密流程可以使用如下伪代码表示:
AES-128解密(uint8 in[16], uint8 out[16], uint8 key[16]){
uint8 state[4,4] = in;
uint32 w[44] = KeyExpansions(key[16]);
//此时使用的秘钥是加密时使用的秘钥的倒序
addRoundKey(state, w[41-44]);
for (int j = 1; j < 10; ++j) {
inverse-subBytes(state);
inverse-shiftRows(state);
inverse-mixColumns(state);
addRoundKey(state, w); //w[37-40], … w[8-11],w[4-7],…
}
inverse-subBytes(state);
inverse-shiftRows(state);
addRoundKey(state, w[0-3]);
out = state;
}
AES运算都是以下4x4字节表示的二维数组矩阵uint8_t state[4][4]
为一个单位,把一个连续的序列"1234567890abcdef"放在矩阵中,对应的顺序为下图所示。一定要注意 排列的顺序是竖排的,不是横排的!
示例秘钥key = “abcdefghijklmnop”={0x61, 0x62,…,0x6F,0x70}。
AES128中原始秘钥key为16字节,运算中需要11个矩阵大小的秘钥,每一列所包含的32位记为一个uint32_t W
,所以秘钥扩展一共需要生产44个列W
,即uint32_t W[44]
。
W[0-3]为直接复制的原始秘钥。
W[4-43]为扩展的秘钥。
W[n]
=
{
W[n-4]
⊕
W[n-1]
,
if n != 4的倍数
.
W[n-4]
⊕
Mix(W[n-1])
⊕
rcon[(n/4) - 1]
,
if n == 4的倍数
.
\text {W[n]}=
W[n]=⎩⎨⎧W[n-4]⊕W[n-1],W[n-4]⊕Mix(W[n-1])⊕rcon[(n/4) - 1],if n != 4的倍数.if n == 4的倍数.
Mix
(
x
)
=
SubWord
(
RotWord
(
x
)
)
\text {Mix}(x) = \text {SubWord}(\text{RotWord}(x))
Mix(x)=SubWord(RotWord(x))
RotWord()为循环左移一位,如输入0x12345678,输出0x34567812。
SubWord()为字节替换,可以参考字节替换。
rcon为轮常量异或,常量数组为:
static const uint32_t rcon[10] = {
0x01000000UL, 0x02000000UL, 0x04000000UL, 0x08000000UL, 0x10000000UL,
0x20000000UL, 0x40000000UL, 0x80000000UL, 0x1B000000UL, 0x36000000UL
};
秘钥key = “abcdefghijklmnop”,秘钥扩展后生成的扩展秘钥uint32_t W[44]为:
W[ 0-3 ] 61626364 65666768 696A6B6C 6D6E6F70
W[ 4-7 ] FFCA3258 9AAC5530 F3C63E5C 9EA8512C
W[ 8-11] 3F1B4353 A5B71663 5671283F C8D97913
W[12-15] 0EAD3EBB AB1A28D8 FD6B00E7 35B279F4
W[16-19] 311B812D 9A01A9F5 676AA912 52D8D0E6
W[20-23] 406B0F2D DA6AA6D8 BD000FCA EFD8DF2C
W[24-27] 01F57EF2 DB9FD82A 669FD7E0 894708CC
W[28-31] E1C53555 3A5AED7F 5CC53A9F D5823253
W[32-35] 72E6D856 48BC3529 14790FB6 C1FB3DE5
W[36-39] 66C1012E 2E7D3407 3A043BB1 FBFF0654
W[40-43] 46AE2121 68D31526 52D72E97 A92828C3
字节替换就是简单的查表操作,AES定义了加密用的S盒和解密用的逆S盒来进行字节替换。
S盒为256个元素的数组,即1个字节(0x00~0xff)可以表示的数量,所以进行字节替换时可以直接把该字节的值作为S盒数组的下标来进行替换。比如0x03字节替换结果为S[0x03]=0x7B,逆S盒同理。图示中
b
2
,
2
=
S
[
a
2
,
2
]
b_{2,2} = S[a _{2,2}]
b2,2=S[a2,2]。
看到有的地方把S盒定义为16x16二维数组S[16][16],字节替换时取该字节的高4位作为行下标,低4位作为列下标。这种方式因为还得对需要替换字节分别取高低位,得到结果再合并高低位,无疑把字节替换操作复杂化了。采用S[256]一维数组完全可以省去这些不必要的操作。
S盒为
unsigned char S[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
};
解密时逆字节替换就是使用逆S盒进行字节替换,逆S盒为:
unsigned char inv_S[256] = {
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
};
前面已经说过,AES运算都是基于4x4二维数组进行的。行移位操作为:第0行不移动,第1行循环左移1字节,第2行循环左移2字节,第3行循环左移3字节。
解密时逆行移位操作为:第0行不移动,第1行循环右移1字节,第2行循环右移2字节,第3行循环右移3字节。
列混合通过矩阵相乘来实现,经过移位后的矩阵左乘一个固定的矩阵,得到混淆后的矩阵,如下公式所示
上述矩阵相乘可以化简为如下表达式:
{
b
0
,
j
=
(
2
∗
a
0
,
j
)
⊕
(
3
∗
a
1
,
j
)
⊕
a
2
,
j
⊕
a
3
,
j
b
0
,
j
=
a
0
,
j
⊕
(
2
∗
a
1
,
j
)
⊕
(
3
∗
a
2
,
j
)
⊕
a
3
,
j
b
0
,
j
=
a
0
,
j
⊕
a
1
,
j
⊕
(
2
∗
a
2
,
j
)
⊕
(
3
∗
a
3
,
j
)
b
0
,
j
=
(
3
∗
a
0
,
j
)
⊕
a
1
,
j
⊕
a
2
,
j
⊕
(
2
∗
a
3
,
j
)
⎩⎪⎪⎪⎨⎪⎪⎪⎧b0,j=(2∗a0,j)⊕(3∗a1,j)⊕a2,j⊕a3,jb0,j=a0,j⊕(2∗a1,j)⊕(3∗a2,j)⊕a3,jb0,j=a0,j⊕a1,j⊕(2∗a2,j)⊕(3∗a3,j)b0,j=(3∗a0,j)⊕a1,j⊕a2,j⊕(2∗a3,j)
其中矩阵的乘法和加法并不是通常意义上的乘法和加法,而是定义在伽罗华域上的二元运算,且使用的不可约多项式为
P
(
x
)
=
x
8
+
x
4
+
x
3
+
x
+
1
P(x)=x^8+x^4+x^3+x+1
P(x)=x8+x4+x3+x+1。关于伽罗华域运算我在另一篇文章中有详细介绍《伽罗华域运算及C语言实现》。其加法为模二加法,相当于异或运算,其乘法可以使用GMul表示,则上式运算可以表示为:
{
b
0
,
j
=
GMul
(
2
,
a
0
,
j
)
∧
GMul
(
3
,
a
1
,
j
)
∧
(
a
2
,
j
)
∧
(
a
3
,
j
)
b
0
,
j
=
(
a
0
,
j
)
∧
GMul
(
2
,
a
1
,
j
)
∧
GMul
(
3
,
a
2
,
j
)
∧
(
a
3
,
j
)
b
0
,
j
=
(
a
0
,
j
)
∧
(
a
1
,
j
)
∧
GMul
(
2
,
a
2
,
j
)
∧
GMul
(
3
,
a
3
,
j
)
b
0
,
j
=
GMul
(
3
,
a
0
,
j
)
∧
(
a
1
,
j
)
∧
(
a
2
,
j
)
∧
GMul
(
2
,
a
3
,
j
)