这一节我们来看AES是如何对混淆后的数据实施加密的。为了便于理解这个过程,我们先假设原始数据和密钥的长度,都是128位,也就是AES-128加密算法。

其实,最终的加密过程很简单,就是把密钥和混淆后的结果进行xor运算(也就是GF(2)上的加法运算)就好了。但在AES里,这个过程会执行多次,每一次叫做一轮加密,并且每一轮使用的密钥都是根据上一轮的密钥变换而来的。因此,只要我们搞清楚密钥的生成算法,就能理解AES全部的加密/解密流程了。

密钥扩展的轮数

那么,这个密钥的扩展是如何完成的呢?同样,我们用一些图结合代码来理解。首先是原始的128位密钥,一共16个字节:

其中:k0-k15表示原始key中的每一个字节,我们把它们4字节分成一组,计作W0-W3。每一轮密钥的扩展用一张图表示,就是这样的:

先计算W4,我们要把W3经过一个函数g处理之后,与W0异或;然后用下面的方式计算W5-W7

W4 = g(W3) xor W0
W5 = W4 xor W1
W6 = W5 xor W2
W7 = W6 xor W3

把得到的W4-7再组合起来,就是第一轮加密使用的密钥了。把这个计算过程写成更一般的形式,就是这样的:

W(4i)   = g(W(4i-1)) xor W(4i-4)
W(4i+1) = W(4i)      xor W(4i-3)
W(4i+2) = W(4i+1)    xor W(4i-2)
W(4i+3) = W(4i+2)    xor W(4i-1)

其中,i=1, 2, 3, ..., 10,这是因为之前我们说过,128位的密钥,要执行10轮加密。

密钥自身的混淆

理解了这个过程之后,剩下的问题,就是刚才说过的函数g了,它做了什么呢?我们还是用一张图来表示:

其中,b0-b3表示输入的4个字节,也就是之前的图中我们看到的W3。然后,先把W3向左移动一个字节,再把得到的结果,根据之前的SBox进行SubBytes替换。替换之后,把得到的结果的第一个字节,和图中的RC数组进行xor操作,就可以得到第二个密钥中的W5。就这样,每一轮加密密钥中的W(4i)组,都采用同样的计算方法。

至于这个RC是怎么算出来的,我们就不展开说了,反正它不会变,当成常量处理就行。大家要想了解它的算法,可以参考这里并且,RC[0]在AES加密中是没意义的,第一轮加密使用的值是RC[1]

扩展密钥算法的实现

了解了扩展密钥算的算法之后,我们来看它的实现过程:

#define Nb 4
#define Nk 4        // The number of 32 bit words in a key.
#define Nr 10       // The number of rounds in AES Cipher.

void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key) {
  unsigned i, j, k;
  uint8_t tempa[4]; // Used for the column/row operations

  // The first round key is the key itself.
  for (i = 0; i < Nk; ++i)
  {
    RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
    RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
    RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
    RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
  }
}

这里:

  • Nb表示原始数据以4字节为单位的长度;
  • Nk表示以4字节为单位的密钥长度;
  • Nr表示加密执行的轮数;
  • 参数Key表示原始密钥;
  • 参数RoundKey用于返回生成的新密钥;

在上面的代码里,执行的是初始化阶段,RoundKey的前16个字节(也就是W0-W3),是原始密钥。

然后,定义一个执行Nb * Nr次的循环,每循环一次,就生成密钥中的一个W(i)Nr轮加密一共需要Nb * NrW。在这个循环的一开始,先读出W(i-1)组四个字节的值:

void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key) {
  /// ...

  // All other round keys are found from the previous round keys.
  for (i = Nk; i < Nb * (Nr + 1); ++i) {
    // Initial round
    {
      k = (i - 1) * 4;
      tempa[0] = RoundKey[k + 0];
      tempa[1] = RoundKey[k + 1];
      tempa[2] = RoundKey[k + 2];
      tempa[3] = RoundKey[k + 3];
    }
  }
}

如果i的值是4的倍数,就要执行函数g变换,这个变换里,要先向左移动一个字节:

// All other round keys are found from the previous round keys.
for (i = Nk; i < Nb * (Nr + 1); ++i)
{
  // Initial round
  // ...

  if (i % Nk == 0)
  {
    // RotWord
    {
      const uint8_t u8tmp = tempa[0];
      tempa[0] = tempa[1];
      tempa[1] = tempa[2];
      tempa[2] = tempa[3];
      tempa[3] = u8tmp;
    }
  }
}

对移动后的结果做SubBytes替换:

// Initial round
// ...
if (i % Nk == 0)
{
  // RotWord
  // ...

  // SubWord
  {
    tempa[0] = getSBoxValue(tempa[0]);
    tempa[1] = getSBoxValue(tempa[1]);
    tempa[2] = getSBoxValue(tempa[2]);
    tempa[3] = getSBoxValue(tempa[3]);
  }
}

再把变换后的第一个字节,和RC常量数组进行xor计算:

if (i % Nk == 0)
{
  // RotWord

  // SubWord

  tempa[0] = tempa[0] ^ Rcon[i/Nk];
}

至此,W(i-1)这一组的g变换就完成了。接下来,根据上图的规则生成W(i)这一组的4字节数据。并且,如果i不是4的倍数,也直接执行这里的变换就好了:

for (i = Nk; i < Nb * (Nr + 1); ++i)
{
  /// ...

  if (i % Nk == 0)
  {
    // RotWord

    // SubWord

    // xor
  }

  j = i * 4; k = (i - Nk) * 4;
  RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0];
  RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1];
  RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2];
  RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3];
}

这样,密钥扩展的流程就完成了。此时RoundKey中包含的,就是扩展出来的密钥。真正的加密工作之前我们说过了,就是把数据和密钥进行xor,因此这一步的实现就很简单了:

void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey) {
  uint8_t i,j;
  for (i = 0; i < 4; ++i) {
    for (j = 0; j < 4; ++j) {
      (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j];
    }
  }
}

其中:

  • round表示轮数;
  • state表示上一轮加密后的结果;
  • RoundKey表示之前生成好的扩展密钥;

密钥长度带来的变化

在这一节最后,我们来说说AES-192和AES-256这两种加密算法。它们的数据加密流程和AES128是完全一样的。唯一的差别就是在扩展密钥时的方法和执行加密的轮数不同。

AES-192

对于AES-192来说,密钥的长度是24字节,要加密12轮,也就是Nk = 6; Nr=12。但用于加密的数据仍旧是16字节,即Nb = 4

如上图所示,此时,密钥的扩展就变成了每6字节为单位,而要应用函数gW组,也变成了6的倍数。并且,初始轮的密钥,以及第一轮密钥的前8个字节,都直接来自原始密钥。

AES-256

最后,我们来说说AES-256,它的密钥长度为32字节,要加密14轮,也就是Nk = 8; Nr = 14。要加密的数据同样是16字节,即Nb = 4。既然Nk = 8,不难想象,W组的g变换要8的倍数一组执行了。但除此之外,W组每4组,还要额外进行另外一次变换h,把这个过程用一张图表示,就是这样的:

相比函数gh就简单多了,只要把作为输入的4字节做一次SubBytes替换就好了。因此,在刚才实现的KeyExpansion函数里,当使用AES-256算法的时候,生成最终的密钥之前,还要添加一段额外的代码:

for (i = Nk; i < Nb * (Nr + 1); ++i)
{
  /// ...

  if (i % Nk == 0)
  {
    // RotWord

    // SubWord

    // xor
  }

  #if defined(AES256) && (AES256 == 1)
    if (i % Nk == 4)
    {
      // h
      {
        tempa[0] = getSBoxValue(tempa[0]);
        tempa[1] = getSBoxValue(tempa[1]);
        tempa[2] = getSBoxValue(tempa[2]);
        tempa[3] = getSBoxValue(tempa[3]);
      }
    }
#endif

  // Generate RoundKey
}

What's next?

至此,AES加密算法的计算细节,我们就都说完了。但看到这里,相信你心里一直都有一个疑问,既然AES只能针对定长数据进行加密,它是如何处理任意长度数据的呢?下一节,我们就来实现最终的加密算法,并了解AES这种分组密码的几种不同工作模式的实现。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们