发布日期 : 12/6/2004 | 更新日期 : 12/6/2004
James McCaffrey
本文假设您熟悉 C# 和位操作。
下载本文的代码: AES.exe (143KB)
摘要
高级加密标准 (AES) 是美国标准与技术研究院针对电子数据的加密所制定的规范,它将要成为公认的数字信息(包括财务数据、电信数据和政府数据)加密方法。本文概述了 AES 并解释了它所使用的算法。本文还包括一个完整的 C# 实现以及 .NET 数据加密的示例。在阅读完本文后,您将能够使用 AES 对数据进行加密,测试基于 AES 的软件,并在自己的系统中使用 AES 加密方法。
本页内容
AES 算法概述
GF(28) 中的域加法和乘法
密钥扩展
C# 中的 AES 类构造函数
C# 中的 AES Cipher 方法
C# 中的 AES InvCipher 方法
使用 AES 类
实现替换方法
小结
美国标准与技术研究院 (NIST) 于 2002 年 5 月 26 日制定了新的高级加密标准 (AES) 规范。在本文中,我将提供用 C# 编写的 AES 的工作实现,并将完整解释到底什么是 AES 以及代码如何工作。我将向您展示如何使用 AES 来加密数据,并扩展此处给出的代码以开发商用质量的 AES 类。我还将解释如何以及为何将 AES 加密合并到软件系统中,以及如何测试基于 AES 的软件。
请注意,本文中提供的代码以及基于本文的任何其他实现都受制于适用的联邦加密模块出口控制(有关确切的规章,请参阅 Commercial Encryption Export Controls)。
AES 是一种可用来保护电子数据的新型加密算法。特别是,AES 是可以使用 128、192 和 256 位密钥的迭代式对称密钥块密码,并且可以对 128 位(16 个字节)的数据块进行加密和解密。与使用密钥对的公钥密码不同的是,对称密钥密码使用同一个密钥来对数据进行加密和解密。由块密码返回的加密数据与输入数据具有相同的位数。迭代式密码使用循环结构来针对输入数据反复执行排列和置换运算。图 1 显示了操作中的 AES,它使用 192 位密钥对 16 个字节的数据块先后进行加密和解密。
图 1 某个数据
AES 是旧式数据加密标准 (DES) 的后续标准。DES 于 1977 年被批准为联邦标准,它在 1998 年之前一直是可行的,从那时起,硬件、软件和密码分析学理论的组合发展允许将用 DES 加密的消息在 56 小时内解密。在那以后,已经针对用 DES 加密的数据成功进行了许多其他攻击,现在,DES 已过了其有用的生存期。
在 1999 年下半年,由研究员 Joan Daemen 和 Vincent Rijmen 创建的 Rijndael(读为“rain doll”)算法被 NIST 选作最符合安全性、实现效率、多功能性和简单性等设计准则的提议。尽管 AES 和 Rijndael 这两个术语有时会互换使用,但它们截然不同。AES 正日益成为加密各种形式的电子数据的实际标准,这些数据包括用于商业应用程序(如银行和金融交易、电信以及私有和联邦信息)中的数据。
AES 算法概述
AES 算法基于排列和置换运算。排列是对数据重新进行安排,置换是将一个数据单元替换为另一个。AES 使用几种不同的方法来执行排列和置换运算。为了说明这些方法,让我们演示一个具体的示例,该示例使用图 1 中显示的数据执行 AES 加密。
将使用索引数组对下面的 128 位值进行加密:
00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
192 位密钥值是:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
图 2 Sbox在 AES 构造函数被调用时,会初始化加密方法将使用的两个表。第一个表是名为 Sbox 的置换盒。它是一个 16 × 16 矩阵。图 2 显示了 Sbox 的前五行和前五列。在幕后,加密例程提取密钥数组,并使用它来生成图 3 中所示的名为 w[] 的“密钥次序表”。
图 3 密钥次序表在 w[] 中,前 Nk (6) 行基于初始密钥值(0x00 到 0x17),其余各行是从种子密钥生成的。变量 Nk 用 32 位字来表示种子密钥的大小。在我以后介绍 AES 实现时,您将确切了解 w[] 的生成方式。要点在于,现在可以使用许多密钥,而不是只使用一个密钥。这些新密钥被称作轮回密钥,以便将它们与初始的种子密钥区分开。
图 2
图 3
图 4
图 4 StateAES 加密例程首先将 16 字节的输入数组复制到名为 State 的 4×4 字节矩阵中(请参阅图 4)。该 AES 加密算法名为 Cipher 并针对 State[] 执行运算,可在伪代码(请参阅图 5)中描述。
加密算法执行预处理步骤(在规范中称为 AddRoundKey)。AddRoundKey 使用密钥次序表的前四行针对 State 矩阵执行逐字节 XOR 运算,并使用轮回密钥表 w[c,r] 针对 input State[r,c] 执行 XOR 运算。
例如,如果 State 矩阵的第一行存放字节 { 00, 44, 88, cc },密钥次序表的第一列是 { 00, 04, 08, 0c },则 State[0,2] 的新值是将 State[0,2] (0x88) 与 w[2,0] (0x08) 执行
XOR 运算的结果,即 0x80:
1 0 0 0 1 0 0 0
0 0 0 0 1 0 0 0 XOR
1 0 0 0 0 0 0 0
AES 加密算法的主循环针对 State 矩阵执行四种不同的运算,在规范中,这些运算的名称分别为 SubBytes、ShiftRows、MixColumns 和 AddRoundKey。AddRoundKey 运算与先前的 AddRoundKey 基本相同,唯一的区别在于当 AddRoundKey 每次被调用时,会使用密钥次序表的下四行。SubBytes 例程是置换操作,它提取 State 矩阵中的每个字节,并置换由 Sbox 表确定的新字节。例如,如果 State[0,1] 的值为 0x40,而且您希望查找它的置换值,则可以在 State[0,1] (0x40) 获取值,并让 x 等于 left digit (4),让 y 等于 right digit (0)。然后将 x 和 y 用作 Sbox 表的索引,以查找置换值,如图 2 所示。
ShiftRows 是排列运算,它将 State 矩阵中的字节向左旋转位移。图 6 显示了 ShiftRows 如何作用于 State[]。State 的行 0 向左旋转位移 0 个位置,行 1 向左旋转位移 1 个位置,行 2 向左旋转位移 2 个位置,行 3 向左旋转位移 3 个位置。
图 6
图 6 针对 State 运行 ShiftRowsMixColumns 运算是置换运算,它是 AES 算法最难懂的部分。它将每个字节替换为字节列中值的数学域加法和乘法的结果。我将在下一节中详细解释特殊域的加法和乘法。
假设 State[0,1] 的值是 0x09,列 1 中的其他值是 0x60、0xe1 和 0x04,那么,State[0,1] 的新值将按如下方式显示:
State[0,1] = (State[0,1] * 0x01) +
(State[1,1] * 0x02) +
(State[2,1] * 0x03) +
(State[3,1] * 0x01)
= (0x09 * 0x01) + (0x60 * 0x02) + (0xe1 * 0x03) +
(0x04 * 0x01)
= 0x57
加法和乘法是特殊的数学域运算,而不是通常的整数加法和乘法。
在一个执行 Nr 次的循环中调用四个运算(SubBytes、ShiftRows、MixColumns 和 AddRoundKey),Nr 等于给定密钥大小的循环次数减去 1。加密算法使用的循环次数是 10、12 或 14,具体值取决于种子密钥的大小是 128、192 还是 256 位。在本例中,
因为 Nr 等于 12,所以这四个运算被调用 11 次。在该迭代运算完成之后,加密算法在将 State 矩阵复制到输出参数之前,最终调用 SubBytes、ShiftRows 和 AddRoundKey。
AES 加密算法总共有四个核心运算。AddRoundKey 使用从种子密钥值生成的轮回密钥来替换 4 个字节的组。SubBytes 使用置换表来替换单个字节。ShiftRows 通过旋转 4-字节行来对 4 个字节的组进行排列。MixColumns 使用域的加法和乘法组合来置换字节。
返回页首
GF(28) 中的域加法和乘法
正如您已经看到的那样,除了 MixColumns 例程以外,AES 加密算法使用相当简单的置换和排列方法。MixColumns 例程使用特殊的加法和乘法。由 AES 使用的加法和乘法基于数学域理论。特别是,AES 基于名为 GF(28) 的域。
GF(28) 域包括一组从 0x00 到 0xff(共 256 个)的值以及加法和乘法,因而得出 (28)。GF 代表伽罗华域,它以创建域理论的数学家命名。GF(28) 有一个特征就是,加法或乘法运算的结果必须位于集合 {0x00 ...0xff} 中。尽管域理论相当深奥,但是 GF(28) 加法的最终结果非常简单:GF(28) 加法只是 XOR 运算。
但是,GF(28) 中的乘法却比较复杂。正如您将在后来的 C# 实现中看到的那样,AES 加密和解密例程需要知道如何乘以仅有的七个常数 0x01、0x02、0x03、0x09、0x0b、0x0d 和 0x0e。因此,我不会泛泛地介绍 GF(28) 乘法理论,而是只针对这七种特殊情况
对其进行介绍。
在 GF(28) 中与 0x01 相乘是特殊的,它与普通算术中的乘以 1 相对应且工作方式也相同 — 任何值与 0x01 相乘都等于它本身。
现在,让我们看看与 0x02 相乘会怎样。正如加法的情况一样,理论是深奥的,但是最终结果却相当简单。如果被乘的值小于 0x80,那么,乘法结果只是向左位移 1 位的值。如果被乘的值大于或等于 0x80,那么,乘法结果将是向左位移 1 位后再与 0x1b 值执行 XOR 运算的值。这将防止出现“域溢出”,并使乘法结果落在范围内。
一旦建立了与 GF(28) 中的 0x02 的加法和乘法,就可以使用它们来定义与任何常数的乘法。要与 GF(28) 中的 0x03 相乘,可以将 0x03 分解为 2 的幂和加法。要将任意字节 b 与 0x03 相乘,会看到 0x03 = 0x02 + 0x01。因此:
b * 0x03 = b * (0x02 + 0x01)
= (b * 0x02) + (b * 0x01)
因为您知道如何与 0x02 和 0x01 相乘以及如何执行加法,所以上述运算可以完成。同样,要将任意字节 b 与 0x0d 相乘,则可以执行以下运算:
b * 0x0d = b * (0x08 + 0x04 + 0x01)
= (b * 0x08) + (b * 0x04) + (b * 0x01)
= (b * 0x02 * 0x02 * 0x02) + (b * 0x02 * 0x02) + (b * 0x01)
加密和解密算法中 AES MixColumns 例程所需的其他乘法遵循同样的一般模式,如下所示:
b * 0x09 = b * (0x08 + 0x01)
= (b * 0x02 * 0x02 * 0x02) + (b * 0x01)
b * 0x0b = b * (0x08 + 0x02 + 0x01)
= (b * 0x02 * 0x02 * 0x02) + (b * 0x02) + (b * 0x01)
b * 0x0e = b * (0x08 + 0x04 + 0x02)
= (b * 0x02 * 0x02 * 0x02) + (b * 0x02 * 0x02) + (b * 0x02)
总之,GF(28) 中的加法是 XOR 运算。GF(28) 中的乘法简化成加法以及与 0x02 相乘,其中,与 0x02 相乘是有条件的 1 位左移。AES 规范包含许多有关 GF(28) 中运算的其他信息。
返回页首
密钥扩展
AES 加密和解密算法使用从种子密钥的字节数组生成的密钥次序表。AES 规范将这称作 KeyExpansion 例程。实质上,从初始密钥生成多个密钥(而非使用单个密钥)会大大增加位的扩散。尽管理解 KeyExpansion 不是非常困难,但这也是 AES 算法中较为复杂
的一部分。在高级伪代码中,KeyExpansion 例程的外观如下所示:
KeyExpansion(byte[] key, byte[][4] w)
{
copy the seed key into the first rows of w
for each remaining row of w
{
use two of the previous rows to create a new row
}
}
“use two of the previous rows to create a new row”例程使用两个子例程(RotWord 和 SubWord),以及一个名为 Rcon 的常数表(用于“循环常数”)。让我们看一看这三项中的每一项,然后返回到整个 KeyExpansion 例程。
RotWord 例程非常简单,它接受 4 字节的数组并将它们向左旋转位移 1 位。因为轮回次序表 w[] 有四列,所以 RotWord 会将一行 w[] 向左旋转位移。请注意,由 KeyExpansion 使用的 RotWord 函数与由加密算法使用的 ShiftRows 例程非常相似,唯一的区别就是它作用于密钥次序表 w[] 的单行,而不是整个加密状态表 State[]。
SubWord 例程使用置换表 Sbox,针对密钥次序表 w[] 的给定行执行逐字节置换。KeyExpansion 运算中的置换与加密算法中的置换完全相同。要被置换的输入字节分成 (x,y) 对,该对用作置换表 Sbox 的索引。例如,置换 0x27 将导致 x = 2,y = 7,而且 Sbox[2,7] 将返回 0xcc。
KeyExpansion 例程使用名为循环常数表的数组 Rcon[]。这些常数有 4 个字节,每个字节都与密钥次序表的一行相匹配。AES KeyExpansion 例程需要 11 个循环常数。您可以在图 7 中查看这些常数。
每个循环常数最左边的字节是 GF(28) 域中 2 的幂。查看它的另一种方法是,注意到每个值都是上一个值乘以 0x02,正如在上一节(讨论 GF(28) 中的乘法)中介绍的那样。请注意,如前所述,0x80 × 0x02 = 0x1b 是 0x80 向左移 1 位,然后与 0x1b 执行 XOR 运算。
现在,让我们更深入地了解一下 KeyExpansion 中的循环。在比以前更详细的伪代码中,循环如下所示:
for (row = Nk; row < (4 * Nr+1); ++row)
{
temp = w[row-1]
if (row % Nk == 0)
temp = SubWord(RotWord(temp)) xor Rcon[row/Nk]
else if (Nk == 8 and row % Nk == 4)
temp = SubWord(temp)
w[row] = w[row-Nk] xor temp
}
如果暂时忽略 if 子句,那么您将看到,密钥次序表 w[] 的每一行都是上一行与行 Nk(4、6 或 8,具体值取决于密钥大小)执行 XOR 运算后的结果。if 条件的第一部分使用与循环常数的 SubWord、RotWord 和 XOR 结果,修改密钥次序表的每个第四、第六或第八行,具体情况取决于密钥的大小是 128、192 还是 256 位。条件的第二部分将修改 12、20、28 行等(即每八行)或 256 位密钥,以便向密钥次序表中添加其他可变性。
让我们看一看 KeyExpansion 如何开始使用本文开头提供的示例。种子密钥是 192-位/6-字的值:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17
密钥次序字节表 w[] 的尺寸为:4 列,Nb × (Nr + 1) 等于 4 × (12 + 1) 行或 52 行。KeyExpansion 例程将种子密钥中的值复制到密钥次序字节表 w[] 的第一行。因为我的种子密钥是 192 位(24 个字节),而且 w[] 表总是有 4 列,所以,在本例中,KeyExapansion 会将种子密钥复制到 w[] 的前 6 行中。现在,让我们看一下 KeyExpansion 例程如何填充密钥次序表的其余部分。在我的示例中,因为行 0 到行 5
是用种子密钥值填充的,所以计算的第一行是行 6:
temp = w[row-1] = 14 15 16 17
条件 (row % Nk == 0) 为真,因此使用第一个 RotWord 子例程:
temp = 15 16 17 14
随后使用 SubWord:
temp = 59 47 f0 fa
然后与 Rcon[row / Nk] = Rcon[6 / 6] = 01 00 00 00 执行 XOR 运算:
temp = 58 47 f0 fa
接下来,与 w[row-Nk] = w[6-6] = 00 01 02 03 执行 XOR 运算,生成如下结果:
w[6] = 58 46 f2 f9
该过程针对密钥次序表 w[] 中的其余所有行重复。
总之,AES 加密和解密的一个重要部分就是从初始种子密钥生成多个循环密钥。这个 KeyExpansion 算法生成一个密钥次序表,并按照类似于有关加密和解密算法的方式使用置换和排列运算。
返回页首
C# 中的 AES 类构造函数
既然我已经检查了 AES 加密算法的所有组件,我将在 C# 中实现它。AES 算法的正式规范包含在联邦信息处理标准公告 197 中。我决定将我的实现尽可能接近地基于它,但是,我很快发现该规范更像是理论文档,而非实现指南。要将该正式规范用作资源,我使用了标准公告中使用的变量名(尽管它们像“Nr”和“w”一样相当隐晦)。
我的设计使用九个数据成员和一个枚举类型,如下所示:
public enum KeySize { Bits128, Bits192,
Bits256 };
private int Nb;
private int Nk;
private int Nr;
private byte[] key;
private byte[,] Sbox;
private byte[,] iSbox;
private byte[,] w;
private byte[,] Rcon;
private byte[,] State;
因为密钥大小只能是 128、192 或 256 位,所以它是枚举类型的绝佳候选:
public enum KeySize { Bits128, Bits192, Bits256 };
该规范文档通常使用字节作为其基本存储单元,但是使用 4-字节字作为两个重要数据成员的大小。两个成员 Nb 和 Nk 分别用字表示块大小和密钥大小。Nr 表示循环次数。块大小总是为 16 个字节(或者 128 位,对于 AES 来说是 4 个字),因此,它可能已被声明为常数。根据枚举参数 KeySize 的值,向密钥大小赋值 4、6 或 8。AES 算法通过许多循环进行迭代,以增加加密数据的复杂程度。循环次数是 10、12 或 14 且基于密码分析学理论。它直接取决于密钥大小。
在设计类接口时,我喜欢反向工作。设想从应用程序调用构造函数和方法。使用该方法,我已经决定希望按如下方式实例化 AES 对象:
Aes a = new Aes(the key size, the seed key)
我按如下方式调用了加密和解密例程:
a.Cipher(plainText, cipherText);
a.InvCipher(cipherText, decipheredText);
我选择了有点拗口的方法名 Cipher 和 InvCipher,因为它们用在 AES 规范文档中。下面是 AES 类构造函数的代码:
public Aes(KeySize keySize, byte[] keyBytes)
{
SetNbNkNr(keySize);
this.key = new byte[this.Nk * 4];
keyBytes.CopyTo(this.key, 0);
BuildSbox();
BuildInvSbox();
BuildRcon();
KeyExpansion();
}
该构造函数首先通过调用 helper 方法 SetNbNkNr(如图 8 所示)来设置 Nb、
Nk 和 Nr 的值。如果您注重效率,则可以将该代码直接放在构造函数中,以避免因方法调用产生开销。
接着,必须将传入构造函数的字节复制到类域变量中。密钥用其他类域进行声明,它通过执行如下代码来获取其值:
this.key = new byte[this.Nk * 4];
keyBytes.CopyTo(this.key, 0);
我决定使用构造函数中的 helper 私有方法 BuildSbox 和 BuildInvSbox 来调用对置换表 Sbox[] 和 iSbox[] 的初始化。现在,Sbox[] 和 iSbox[] 分别是密钥扩展例程以及 Cipher 和 InvCipher 方法所必需的,因此,我可以将 Sbox[] 的初始化和 KeyExpansion 方法的调用放在 Cipher 和 InvCipher 方法中,但是将它们放在构造函数中会生成更简洁的代码结构。sBox[] 在图 9 中进行填充。用来填充 iSbox[] 的代码是类似的。该代码构造得具有可读性。正如您将在以后看到的那样,对于这种为 Sbox 和 iSbox 表提供值的方法,有一个令人惊讶的替换方法。
在构造函数中声明密钥次序表 w[]、循环常数表 Rcon[] 和状态矩阵 State[],并使用 helper 私有方法向 Rcon[] 和 w[] 赋值,这些操作对我来说似乎是组织它们的最佳方法,但这主要是风格问题。图 7 显示了用来填充循环常数表 Rcon 的代码。
让我们回想一下,Rcon[] 中每一行的左侧字节是 GF(28) 中 2 的幂,因此,该表可通过使用类似如下的代码以计算方式来构建:
newVal = prevVal * 0x02;
AES 构造函数最终构建密钥次序表 w[],这是在 KeyExpansion 方法(请参阅图 10)中完成的。该代码相当简单。规范文档使用假想的 4-字节字数据类型。由于 C# 没有这样的类型,因此它是用 4 字节数组模拟的。在使用运算符 new 向密钥次序表 w[] 分配空间以后,w[] 中的前 Nk(4、6 或 8)行将从传入到构造函数的种子 key[] 数组获取值:
this.w[row,0] = this.key[4*row];
this.w[row,1] = this.key[4*row+1];
this.w[row,2] = this.key[4*row+2];
this.w[row,3] = this.key[4*row+3];
对 2 字节执行的 XOR 运算在该代码中一起出现多次。它需要在 byte 和 int 之间来回进行一些转换,这是由于未针对 C# byte 类型定义 XOR 运算符 ^。例如,使用
temp[0] = (byte)( (int)temp[0] ^ (int)this.Rcon[row/Nk,0] );
而不使用:
temp[0] = temp[0] ^ this.Rcon[row/Nk,0];
KeyExpansion 方法有条件地调用私有方法 SubWord 和 RotWord,以便维持与规
范的命名一致性。同样,因为 C# 中没有字类型,所以,我使用 4 字节数组实现了一个字类型。SubWord 和 RotWord 的代码相当简单,通过在本文附带的 AesLib 源代码中检查它,您应当容易理解它。
有点复杂的部分是在 SubWord 中查找置换值。让我们回想一下,为了查找置换值,需要将输入字节分成两部分:最左边的 4 位和最右边的 4 位。对于给定字节,用 >> 运算符右移 4 位会生成 x 索引,与 0000 1111 执行逻辑 AND 将生成 y 值。使用比实际代码稍长但更易读的形式,我可以执行类似如下的代码
int x = word[0] >> 4;
int y = word[0] & 0x0f;
byte substitute = this.Sbox[x,y];
result[0] = substitute;
而不是我用过的代码:
result[0] = this.Sbox[ word[0] >> 4, word[0] & 0x0f ];
总之,AES 构造函数接受大小为 128、192 或 256 位的密钥,以及一个字节数组种子密钥值。构造函数为加密算法的输入块大小、种子密钥大小和循环次数赋值,并将种子密钥复制到名为 key 的数据成员。构造函数还构造四个表:两个由加密和解密方法使用的置换表,一个循环常数表,一个由轮回密钥组成的密钥次序表。
返回页首
C# 中的 AES Cipher 方法
图 11 显示了 Cipher 方法的代码。它确实非常简单,因为它将大部分工作都移交给私有方法 AddRoundKey、SubBytes、ShiftRows 和 MixColumns。
Cipher 方法首先将纯文本输入数组复制到状态矩阵 State[] 中。在对
AddRoundKey 进行初始调用以后,Cipher 方法进行迭代,迭代次数比总的循环次数少一次。正如在规范中描述的那样,在最后一次循环时,省略了对 MixColumns 的调用。
图 12 显示了 AddRoundKey 和 SubBytes 私有方法的代码。AddRoundKey 方法需要知道它位于哪个循环,以便它可以正确引用密钥次序表数组 w[] 的四行。请注意,State[r,c] 与 w[c,r](而非 w[r,c])执行 XOR 运算。SubBytes 方法使用与
KeyExpansion 方法中相同的“右移 4 位”和“用 0x0f 掩码”的方法,从输入字节提取索引。
图 13 显示了 ShiftRows 方法的代码。让我们回想一下,ShiftRows(叫做 RotateRows 可能更好)将 row[0] 向左旋转位移 0 个位置,将 row[1] 向左旋转位移 1 个位置,依此类推。
在将 State[] 复制到 temp[] 矩阵中以后,用以下代码执行位移:
this.State[r, (c + r) % Nb ] = temp[r,c];
这会利用 % 运算符来环绕行。
MixColumns 方法(请参阅图 14)使用 GF(28) 加法和乘法获取每个字节,并将其置换为字节列中所有其他值的线性组合。用于乘法的常数系数基于域理论,它们是 0x01、0x02 或 0x03。给定列 c 的置换是:
State[0,c] = 0x02 * State[0,c] + 0x03 * State[1,c] + 0x01 * State[2,c] +
0x01 * State[3,c]
State[1,c] = 0x01 * State[0,c] + 0x02 * State[1,c] + 0x03 * State[2,c] +
0x01 * State[3,c]
State[2,c] = 0x01 * State[0,c] + 0x01 * State[1,c] + 0x02 * State[2,c] +
0x03 * State[3,c]
State[3,c] = 0x03 * State[0,c] + 0x01 * State[1,c] + 0x01 * State[2,c] +
0x02 * State[3,c]
这些表达式已经有点长,因此,我决定编写 helper 私有函数,以返回 GF(28) 与 0x01、0x02 和 0x03 的乘积。Helper 函数非常短。例如,用于将字节 b 与 0x03 进行域乘的代码如下所示:
return (byte) ( (int)gfmultby02(b) ^ (int)b );
正如我以前讨论的那样,与 0x02 相乘是所有 GF(28) 乘法的基本运算。我调用了 gfmultby02 方法,并使用与规范中相同的方法名来遵守我的约定;规范调用该例程 xtime。
Cipher 方法针对其输入反复应用四个运算,以生成加密的输出结果。AddRoundKey 使用从单个初始种子密钥派生的多个轮回密钥来置换字节。SubBytes 使用置换表中的值来置换字节。ShiftRows 通过平移字节行来排列字节,MixColumns 通过使用列值的域加法和乘法来置换字节。
返回页首
C# 中的 AES InvCipher 方法
AES 解密算法的基本假定非常简单:要对加密块进行解密,只需按相反的顺序取消每个运算。尽管这是基本概念,但是仍有几个细节需要处理。
AES 规范将解密例程命名为 InvCipher,而非替换名称 Decipher 或 Decrypt。这是对 AES 后面的数学运算的反映,它们所基于的是反向数学运算。
如果您将该代码与 Cipher 代码进行比较,将会发现它基本符合您的预期,但是有两个例外。首先,InvCipher 方法中反向方法调用(如 InvSubBytes)的顺序不完全是 Cipher 方法中相应调用(如 SubBytes)的反向。第二,InvCipher 调用 AddRoundKey 方法(而非 InvAddRoundKey 方法)。请注意,InvCipher 算法使用密钥次序表,但是从编号更高的索引开始,并向下工作到行 0。
InvSubBytes、InvShiftRows 和 InvMixColumns 方法的代码密切反映了相关 SubBytes、ShiftRows 和 MixColumns 方法的代码。InvSubBytes 方法与 SubBytes 方法基本相同,唯一的区别在于它使用反向置换表 iSbox[],而不使用 Sbox[] 表。
正如您可能猜到的那样,iSbox[] 只是取消由 Sbox[] 执行的任何映射。例如,如果您的字节 b 等于 0x20,并在 Sbox[] 中查找它的置换值,就会获得 0xb7。如果您在 iSbox[] 中查找 0xb7 的置换值,就会获得 0x20。
同样,InvShiftRows 方法取消 ShiftRows 方法 — row[0] 右移 0 个位置,row[1] 右移 1 个位置,row[2] 右移 2 个位置,row[3] 右移 3 个位置。
InvMixColumns 方法以一种不明显的方式取消 InvMixColumns 方法的工作。让我们回想一下,MixColumns 将 state 矩阵中的每个字节替换为初始字节列中字节的线性组合,系数是 0x01、0x02 和 0x03。域理论再次发挥作用。其结果是,反向运算是类似的,但是使用与 0x09、0x0b、0x0d 和 0x0e 的相乘,如下所示:
State[0,c] = 0x0e * State[0,c] + 0x0b * State[1,c] + 0x0d * State[2,c] +
0x09 * State[3,c]
State[1,c] = 0x09 * State[0,c] + 0x0e * State[1,c] + 0x0b * State[2,c] +
0x0d * State[3,c]
State[2,c] = 0x0d * State[0,c] + 0x09 * State[1,c] + 0x0e * State[2,c] +
0x0b * State[3,c]
State[3,c] = 0x0b * State[0,c] + 0x0d * State[1,c] + 0x09 * State[2,c] +
0x0e * State[3,c]
正如对于 MixColumns 方法一样,我决定编写专用的 helper 函数,而不是扩展已有的内联长表达式或者编写通用的 helper 乘法函数。让我向您介绍一下如何编写将任何字节 b 与常数 0x0e(14,基数是 10)相乘的函数。与任何数字一样,数字 14 可以表示为 2 的幂和。在本例中,14 等于 2 + 4 + 8。由于 4 等于 2 的平方,8 等于 2 的立方,因此,您可以将 14 表示为 2 + 22 + 23。请记住,加法只是 GF(28) 中的 XOR (^),由于我已经有了 gfmultby02 函数,因此我可以使用它来获得结果:
return (byte)( (int)gfmultby02(gfmultby02(gfmultby02(b))) ^ /* 23 + */
(int)gfmultby02(gfmultby02(b)) ^ /* 22 + */
(int)gfmultby02(b) ); /* 2 */
由 AES 加密算法使用的所有运算都是可逆转的,因此,解密算法实质上会对加密算法执行的所有运算进行反向。
返回页首
使用 AES 类
在 C# 中实现的 AES 的特征之一就是其简单性。请考虑图 15 中的代码,我使用它来生成图 1 中显示的输出结果。在声明了 16-字节纯文本输入和 24-字节(192-位)种子密钥的硬编码值以后,会对 AES 对象进行初始化,Cipher 加密方法会将纯文本加密为密码文本,然后密码文本会通过使用 InvCipher 解密为纯文本,非常简洁,非常简单。
因为 AES 对象作用于字节数组,所以您可以很方便地修改它,让它作用于其他 .NET 数据类型。我构造了一个基于 Windows? 的小型演示应用程序,并让它接受单个 8-字符(16-字节)字符串,然后对它进行加密和解密。图 16 显示了示例的运行过程。
图 16 加密演示
因为加密和解密例程需要知道用户已经指定的密钥大小,所以我将它声明为类范围内的变量,如下所示:
private Aes.KeySize keysize;
请注意,种子密钥不是由用户指定的。该演示通过向构造函数提供虚拟的 new byte[16] 来使用全部由零字节组成的“空密钥”。因为种子密钥也将全部初始化为零,所以虚拟参数的大小无关紧要。用空密钥进行加密和解密,可以方便有效地防止对数据随意进行外部检查。使用 System.Text 中的 Encoding.Unicode.GetBytes 和
Encoding.Unicode.GetString 方法,可以在 .NET 字符串和字节数组之间进行非常方便
的转换。
返回页首
实现替换方法
现在,让我们看一下本文提供的 AES 实现的一些重要变体、本文所提供代码的可能扩展以及针对 AES 的密码分析学攻击。
就我曾经使用过的任何代码来说,AES 算法有重要的替换实现方法。这为什么很重要?AES 将适用于广泛的系统;从具有微小内存容量的智能卡到大型多处理器主机系统。在许多情况下,性能是至关重要的,内存或处理资源有时是有限的。AES 中几乎所有的例程都可以进行修改,以便在牺牲内存的情况下优化性能,反之亦然。例如,向置换表 Sbox[] 赋予 256 个值似乎相当简单。但是,这些值基于 GF(28) 理论,其结果是,这些值可以通过编程方式生成。对于反向置换表和循环常数表同样如此。
对于替换实现来说,另一个有趣的可能是由 Cipher 和 InvCipher 方法使用的 GF(28) 乘法。我的实现先后对与 0x02 相乘的基本函数以及六个调用 gfmultby02 的其他函数进行编码。另一个可能性将是编写一个通用的乘法函数并使用它,而不是像我以前那样实现七个单独的函数。另一个极端是,您可以使用一个完整的乘积表,该表由全部 256 个可能的字节值与 0x01、0x02、0x03、0x09、0x0b、0x0d 和 0x0e 相乘组成。实现 GF(28) 乘法的另一种方法就是,将它作为两个 256-字节数组(通常被称作 alog[] 和 log[])的查找功能来实现,这是由于它们基于 GF(28) 的某种类似于对数的属性。
尽管本文提供的 AES 类完全能够对任何形式的 .NET 数据进行加密,但是您可能希
望考虑以多种方法来扩展它。首先,由于本文的重点在于清楚地解释 AES,因此,所有的错误检查都省略了。以我的经验,向类似于该 AES 类的类中添加适当数量的错误检查功能将使源代码的大小增至三倍。因为 AES 使用如此之多的数组,所以,应当执行许多索引绑定检查。例如,本文提供的构造函数甚至不检查种子密钥参数的大小。
您还可以考虑通过添加更多的功能来扩展这个 AES 类。最明显的开始位置将是,添加多个可对基本 .NET 数据类型(如 System.String 和 System.Int32)进行加密和解密的方法。更有力的扩展将是实现加密的流类。
AES 有多安全?这是一个难以回答的问题,但是一般的意见是,它是现有的最安全的加密算法。到目前为止,AES 比任何其他加密算法经过了更多的审查。攻击 AES 的唯一有效方法就是通过强力生成所有可能的密钥,就这一点来说,无论是在理论上和还是在实践上,AES 都被认为是“安全的”。对于 256 位的密钥大小,在一定的时间(在现有的最快系统上甚至需要数年)内,任何已知的强力攻击都无法破坏 AES。
请注意,对于 AES 密码来说,最有可能成功的攻击来自可允许进行所谓的计时攻击的弱实现。攻击者使用不同的密钥,并精确计量加密例程所需的时间。如果在对加密例程进行编码时有点粗心,以至于执行时间取决于密钥值,那么,有可能推导出有关密钥的信息。在 AES 中,由于域乘法的原因,这最有可能出现在 MixColumns 例程中。针对该攻击有两个安全措施:插入虚拟指令,以便所有的乘法都需要相同数量的指令;如我已经描述的那样,将域乘法作为查找表来实现。
对于 AES 来说,有许多可能的实现,尤其是使用查找表(而非计算)。本文中提供的基本 AES 类可用于对任何形式的 .NET 数据进行加密和解密,也可扩展为具有附加功能的类。
返回页首
小结
新的 AES 无疑将取代 DES,成为对所有形式的电子信息进行加密的实际标准。如果不针对所有可能的 256 位密钥使用强力搜索,任何已知的密码分析学攻击都无法对 AES 密码文本进行解密,就这一点来说,用 AES 加密的数据是牢不可破的。
我发现,在 Microsoft?.NET Framework 中实现 AES 类时有一个主要障碍,那就是,正式规范文档是从数学家的观点(而非软件开发人员的观点)来编写的。特别是,该规范假设读者非常熟悉 GF(28) 域,它省去了一些有关 GF(28) 乘法的关键细节,而这些细节又是正确实现 AES 所必需的。在这里,我已尽力揭开 AES(尤其是围绕 GF(28) 域乘法)的神秘面纱。
Microsoft 和第三方供应商以 .NET Framework 库的形式广泛地提供 AES 加密只是一个时间问题。但是,出于多种原因,掌握此代码还是很有价值的。该实现特别简单,并且具有很低的资源开销。另外,如果访问并了解源代码,将使您能够自定义 AES 类,并且更有效地使用它的任何实现。
在任何人的软件设计和开发过程中,安全不再是马后炮。AES 是重要的预先考虑事项,使用并了解它将大大增强软件系统的可靠性和安全性。
有关相关文章,请参阅:
Security: Protect Private Data with the Cryptography Namespaces of the .NET
Framework
Exploring Kerberos, the Protocol for Distributed Security in Windows 2000
有关背景信息,请参阅:
The Design of Rijndael: AES - The Advanced Encryption Standard by Joan Daemen and Vincent Rijmen. (Springer-Verlag, 2002)
Announcing the Advanced Encryption Standard (AES): Federal Information Processing Standards Pub 197
James McCaffrey 就职于 Volt Information Sciences Inc.,他为 Microsoft 的软件工程师管理技术培训。他曾经作为承包商致力于几个 Microsoft 产品(包括 Internet Explorer 和 MSN Search)方面的工作。您可以通过 jmccaffrey@volt.com 或 v-jammc@microsoft.com 与他联系。
转到原英文页面
返回页首
因篇幅问题不能全部显示,请点此查看更多更全内容