04_AES算法原理

AES算法原理

一、AES简单介绍

1. AES算法特点
  • 在一个分组长度内,明文的长度变化,AES的加密结果长度都一样
  • 密钥长度128、192、256三种,如果少1个十六进制数,会在前面补0
  • 分组长度为128比特位,也就是16字节
  • 密文长度与填充后的明文长度有关,一般是16字节的倍数
2. Rijndael算法

Rijndael算法也叫AES算法,Rijndael算法本身分组长度可变,但是定为AES算法后,分组长度只有128一种

3. 运算单位

DES当中是针对比特位进行操作的,而AES中是针对字节进行操作的

4. 轮运算
  • AES128:10轮运算
  • AES192:12轮运算
  • AES256:14轮运算

二、AES原理

​ 这里分析的是AES128的分组加密。

1. 明文处理

img

上图就是一个分组当中的轮运算,首先先与密钥进行一个\(\oplus\)异或运算,接着是9轮的循环运算,分别是SubBytes (字节代换)ShiftRows (行变换)MixColumns (列混合)AddRoundKey (轮密钥加\(\oplus\))。最后第十轮少了一个列混合,经过第十轮的运算之后Ciphertext(密文)。

​ 下面来分析一下这个明文轮运算当中的一些运算的细节。

SubBytes (字节代换)

img

​ 字节代换如上图所示,直接取出一个字节,然后高4bit作为s和的行索引,低四位作为s盒的列索引。取出对应S盒当中的数值然后替换。这一步非常的简单就是一个查表的过程,就不赘述了。

ShiftRows (行变换)

​ 紧接着就是行变换了,行变换就是对每一行进行循环位移操作。具体来说就是:

  • 第一行不变
  • 第二行循环左移1位
  • 第三行循环左移2位
  • 第四行循环左移3位

整个变换过程如下图所示:
img

MixColumns (列混合)

​ 列混淆是对矩阵每一列进行线性变化,通过矩阵乘法实现,步骤如下图:
img

要注意的是,这里的列变换,一旦矩阵乘法当中出现溢出就要\(\oplus 0x1B\) , 而这个矩阵乘法的运算也有点不同,使用了异或代替了乘法之后的加法,乘法当中也是使用多轮的异或而不是简单的加法。

AddRoundKey (轮密钥加\(\oplus\))

​ 接着就是轮密钥异或了
img

这个就是简单的异或,然后进行9轮轮运算,最后在进行第十轮运算就得出密文了

2. 子密钥生成
密钥扩展

​ 传入16字节种子密钥初始化,得到\(W_0-W_3\)作为种子密钥,然后将种子密钥扩展得到剩下的\(W_4 - W_{43}\)。扩展的规则如下:

  • 如果i%4!=0\(W_i = W_{i-1} \oplus W_{i-4}\)
  • 如果i%4==0,则\(W_i = T(W_{i-1}) \oplus W_{i-4}\)

T函数流程:循环左移1个字节、s盒替换、与Rcon异或,如下图所示:
img

框选部分就是循环左移之后查表得出的值。接着与Rcon(i)进行异或。

​ 最终可以得到44组密钥,然后每一轮的addRoundKey使用4组。

三、AES源码分析

这里大致分析一下各个工作部分的代码:

1. aesEncrypt
// 参数: 密钥,密钥长度, 明文, 密文,明文长度
int aesEncrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *pt, uint8_t *ct, uint32_t len) {
    AesKey aesKey;
    uint8_t *pos = ct;
    const uint32_t *rk = aesKey.eK;  //解密密钥指针
    uint8_t out[BLOCKSIZE] = {0};
    uint8_t actualKey[16] = {0};
    uint8_t state[4][4] = {0};
    if (NULL == key || NULL == pt || NULL == ct) {
        printf("param err.\n");
        return -1;
    }
    if (keyLen > 16) {
        printf("keyLen must be 16.\n");
        return -1;
    }
    if (len % BLOCKSIZE) {
        printf("inLen is invalid.\n");
        return -1;
    }

    memcpy(actualKey, key, keyLen);
    keyExpansion(actualKey, 16, &aesKey);  // 秘钥扩展

    for (int i = 0; i < len; i += BLOCKSIZE) {	// 明文分
        loadStateArray(state, pt);				// 明文转成4 x 4矩阵

        addRoundKey(state, rk);					// 轮秘钥加
        for (int j = 1; j < 10; ++j) {
            rk += 4;
            subBytes(state);   					// 字节替换
            shiftRows(state);  					// 行移位
            mixColumns(state); 					// 列混合
            addRoundKey(state, rk); 			// 轮秘钥加
        }

        subBytes(state);    					// 字节替换
        shiftRows(state);  						// 行移位
        addRoundKey(state, rk + 4); 			// 轮秘钥加

        storeStateArray(state, pos);			// 矩阵转一维数组

        pos += BLOCKSIZE;  // 加密数据内存指针移动到下一个分组
        pt += BLOCKSIZE;   // 明文数据指针移动到下一个分组
        rk = aesKey.eK;    // 恢复rk指针到秘钥初始位置
    }
    return 0;
}

加密函数大致流程如上。

2. keyExpansion密钥扩展
int keyExpansion(const uint8_t *key, uint32_t keyLen, AesKey *aesKey) {
    if (NULL == key || NULL == aesKey) {
        printf("keyExpansion param is NULL\n");
        return -1;
    }
    if (keyLen != 16) {
        printf("keyExpansion keyLen = %d, Not support.\n", keyLen);
        return -1;
    }

    uint32_t *w = aesKey->eK;  //加密秘钥
    uint32_t *v = aesKey->dK;  //解密秘钥
    for (int i = 0; i < 4; ++i) {
        LOAD32H(w[i], key + 4 * i);
    }
    for (int i = 0; i < 10; ++i) {
        w[4] = w[0] ^ MIX(w[3]) ^ rcon[i];
        w[5] = w[1] ^ w[4];
        w[6] = w[2] ^ w[5];
        w[7] = w[3] ^ w[6];
        w += 4;
    }
    w = aesKey->eK + 44 - 4;
    for (int j = 0; j < 11; ++j) {
        for (int i = 0; i < 4; ++i) {
            v[i] = w[i];
        }
        w -= 4;
        v += 4;
    }
    return 0;
}

​ 首先通过LOAD32H将二维数组矩阵,当中的每一列取出,获取\(W_0-W_3\)LOAD32H是一个宏,定义如下:

#define LOAD32H(x, y) \
  do { (x) = ((uint32_t)((y)[0] & 0xff)<<24) | ((uint32_t)((y)[1] & 0xff)<<16) | \
             ((uint32_t)((y)[2] & 0xff)<<8)  | ((uint32_t)((y)[3] & 0xff));} while(0)

就是将矩阵的每一列拼成一个DWORD的数值。

​ 接着就是for循环10轮,搞定剩下的\(W_{4}\)~\(W_{43}\),注意这里面的循环迭代的是w指针,所以计算的索引不变,因为相对偏移不变。只要索引是4就要额外处理,MIX作用是先字节替换再循环左移,同样也是一个宏,具体定义如下:

#define MIX(x) (((S[BYTE(x, 2)] << 24) & 0xff000000) ^ ((S[BYTE(x, 1)] << 16) & 0xff0000) ^ \
                ((S[BYTE(x, 0)] << 8) & 0xff00) ^ (S[BYTE(x, 3)] & 0xff))
// 取从低位开始的第n个字节
#define BYTE(x, n) (((x) >> (8 * (n))) & 0xff)

​ 最后就是给解密的扩展密钥赋值,解密时使用的就是加密密钥的逆序。

3. loadStateArray数组转矩阵
int loadStateArray(uint8_t (*state)[4], const uint8_t *in) 
{
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            state[j][i] = *in++;
        }
    }
    return 0;
}

就是一个简单的二维数组赋值。

4. addRoundKey轮密钥加
int addRoundKey(uint8_t (*state)[4], const uint32_t *key) {
    uint8_t k[4][4];
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            k[i][j] = (uint8_t) BYTE(key[j], 3 - i); 
            state[i][j] ^= k[i][j];
        }
    }
    return 0;
}

这里先使用BYTEDWORD类型的key取出一个个对应的字节,然后再进行异或

5. subBytes字节替换
int subBytes(uint8_t (*state)[4]) {
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            state[i][j] = S[state[i][j]]; //直接使用原始字节作为S盒数据下标
        }
    }
    return 0;
}

字节替换就直接使用了原始字节作为S盒数据的下标,这个方法与分开左右两边查表是等效的。

6. shiftRows行变换
int shiftRows(uint8_t (*state)[4]) {
    uint32_t block[4] = {0};
    for (int i = 0; i < 4; ++i) {
        LOAD32H(block[i], state[i]);			// 先拼成32bit数据
        block[i] = ROF32(block[i], 8 * i);
        STORE32H(block[i], state[i]);
    }
    return 0;
}

行变化先将数组的每一行拼成一个32bit的值,然后再进行循环左移i个字节,也就是\(8 \times i\)个比特位。然后使用STORE32存储回state当中。

7. mixColumns列混淆
int mixColumns(uint8_t (*state)[4]) {
    uint8_t tmp[4][4];
    uint8_t M[4][4] = {{0x02, 0x03, 0x01, 0x01},
                       {0x01, 0x02, 0x03, 0x01},
                       {0x01, 0x01, 0x02, 0x03},
                       {0x03, 0x01, 0x01, 0x02}};

    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            tmp[i][j] = state[i][j];
        }
    }
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {  //伽罗华域加法和乘法
            state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j])
                          ^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
        }
    }
    return 0;
}

这里就是进行的伽罗华域加法和乘法,然后GMul当中还有溢出检测:

uint8_t GMul(uint8_t u, uint8_t v) {
    uint8_t p = 0;
    for (int i = 0; i < 8; ++i) {
        if (u & 0x01) {  
            p ^= v;
        }
        int flag = (v & 0x80);
        v <<= 1;
        if (flag) {
            v ^= 0x1B; /* x^8 + x^4 + x^3 + x + 1 */
        }
        u >>= 1;
    }
    return p;
}

​ 这里的矩阵乘法(伽罗华域加法和乘法),可以理解为,原来乘法就是多次加法,但是在这里面加法变成了异或,每轮循环要左移一位,如果溢出,则需要\(\oplus 0x1B\)

​ 那么到这里大致AES整个过程了,这里只分析的是ECB模式下的,一个分组的加密过程。其他加密模式都可以在这个基础上推广。

四、查表法实现AES加密

原文: AES对称密码算法介绍(2)——查表法软件实现

​ AES每一轮当中有四层结构,加密过程分别是字节代换层(Byte Substitution Layer)、ShiftRows层、MixColumn层和密钥加法层(Key Addition Layer)

S盒是一个有限集,而且是根据输入进行映射,S盒之后的矩阵计算,每个元素其实也有着一个映射关系,而对于查表法的实现就是对前三层操作的合并。

​ 而在轮运算当中,字节代换循环左移其实是可以交换顺序的。对于列混淆当中有如下的矩阵公式:

\[\begin{bmatrix} 2 & 3 & 1 & 1 \\ 1 & 2 & 3 & 1 \\ 1 & 1 & 2 & 3 \\ 3 & 1 & 1 & 2 \end{bmatrix} \cdot \begin{bmatrix} B_0 & B_4 & B_8 & B_{12}\\ B_1 & B_5 & B_9 & B_{13}\\ B_2 & B_6 & B_{10} & B_{14}\\ B_3 & B_7 & B_{11} & B_{15} \end{bmatrix} = \begin{bmatrix} C_0 & C_4 &C_8 & C_{12}\\ C_1 & C_5 &C_9 & C_{13}\\ C_2 & C_6 &C_{10} & C_{14}\\ C_3 & C_7 &C_{11} & C_{15} \end{bmatrix} \]

这里的B0就是经过字节代换循环左移之后的矩阵。然后根据矩阵的乘法可以将列向量拆开,变成这样:

\[\begin{bmatrix} B_0 \\ B_1 \\ B_2 \\ B_3 \end{bmatrix} \cdot \begin{bmatrix} 2 & 3 & 1 & 1 \\ 1 & 2 & 3 & 1 \\ 1 & 1 & 2 & 3 \\ 3 & 1 & 1 & 2 \end{bmatrix} = \begin{bmatrix} C_0 \\ C_1 \\ C_2 \\ C_3 \end{bmatrix} \\ = \begin{bmatrix} 2 \\ 1 \\ 1 \\ 3 \end{bmatrix} B_0 + \begin{bmatrix} 3 \\ 2 \\ 1 \\ 1 \end{bmatrix} B_1 + \begin{bmatrix} 1 \\ 3 \\ 2 \\ 1 \end{bmatrix} B_2 + \begin{bmatrix} 1 \\ 1 \\ 3 \\ 2 \end{bmatrix} B_3 \]

那么上面的等式就能够继续推广成这样:

\[\begin{bmatrix} 2 \\ 1 \\ 1 \\ 3 \end{bmatrix} S(A_0) + \begin{bmatrix} 3 \\ 2 \\ 1 \\ 1 \end{bmatrix} S(A_5) + \begin{bmatrix} 1 \\ 3 \\ 2 \\ 1 \end{bmatrix} S(A_{10}) + \begin{bmatrix} 1 \\ 1 \\ 3 \\ 2 \end{bmatrix} S(A_{15}) = \begin{bmatrix} C_0 \\ C_1 \\ C_2 \\ C_3 \end{bmatrix} \]

注意:这里的A0->A5是已经循环左移之后的排序。所以这三个步骤可以根据这个公式变成从\(A_0\)-->\(C_0\)的一个映射,也就能够使用查表来实现了。接着将这个查的表称为T盒,而加密过程总共有4个T表:

\[T_{e_0}(A_x) = \begin{bmatrix} 2 \\ 1 \\ 1 \\ 3 \end{bmatrix} S(A_x) ,\qquad T_{e_1}(A_x) = \begin{bmatrix} 3 \\ 2 \\ 1 \\ 1 \end{bmatrix} S(A_x) ,\qquad T_{e_2}(A_x) = \begin{bmatrix} 1 \\ 3 \\ 2 \\ 1 \end{bmatrix} S(A_x) ,\qquad T_{e_3}(A_x) = \begin{bmatrix} 1 \\ 1 \\ 3 \\ 2 \end{bmatrix} S(A_x) \]

最终的轮操作可以化简成下面的公式:

\[\begin{aligned} &\begin{bmatrix} D_0 \\ D_1 \\ D_2 \\ D_3 \end{bmatrix} = T_{e_0}(A_0) + T_{e_1}(A_5) + T_{e_2}(A_{10}) +T_{e_3}(A_{15})+W_{k0} \\\\ &\begin{bmatrix} D_4 \\ D_5 \\ D_6 \\ D_7 \end{bmatrix} = T_{e_0}(A_4) + T_{e_0}(A_9) + T_{e_0}(A_{14}) +T_{e_0}(A_3)+W_{k1}\\\\ &\begin{bmatrix} D_8 \\ D_9 \\ D_{10} \\ D_{11} \end{bmatrix} = T_{e_0}(A_8) + T_{e_0}(A_{13}) + T_{e_0}(A_2) +T_{e_0}(A_7)+W_{k2} \\\\ &\begin{bmatrix} D_{12} \\ D_{13} \\ D_{14} \\ D_{15} \end{bmatrix} = T_{e_0}(A_{12}) + T_{e_0}(A_1) + T_{e_0}(A_6) +T_{e_0}(A_{11})+W_{k3} \\ \end{aligned} \]

接下来给出的实例代码是相邻两轮的操作,使用C/C++编写:

void aes_encrypt_rounds(uint32_t *wa, uint32_t *wb, const uint32_t *keys, int rounds) {
  
    uint32_t wa0 = wa[0], wa1 = wa[1], wa2 = wa[2], wa3 = wa[3];
    uint32_t wb0 = wb[0], wb1 = wb[1], wb2 = wb[2], wb3 = wb[3];

    for (int i = 1; i < (rounds / 2); ++i) {
        // --- 偶数轮 (Even-number rounds) ---
        wa0 = TE0[wb0 >> 24] ^ TE1[(wb1 >> 16) & 0xFF] ^ TE2[(wb2 >> 8) & 0xFF] ^ TE3[wb3 & 0xFF] ^ keys[8 * i];
        wa1 = TE0[wb1 >> 24] ^ TE1[(wb2 >> 16) & 0xFF] ^ TE2[(wb3 >> 8) & 0xFF] ^ TE3[wb0 & 0xFF] ^ keys[8 * i + 1];
        wa2 = TE0[wb2 >> 24] ^ TE1[(wb3 >> 16) & 0xFF] ^ TE2[(wb0 >> 8) & 0xFF] ^ TE3[wb1 & 0xFF] ^ keys[8 * i + 2];
        wa3 = TE0[wb3 >> 24] ^ TE1[(wb0 >> 16) & 0xFF] ^ TE2[(wb1 >> 8) & 0xFF] ^ TE3[wb2 & 0xFF] ^ keys[8 * i + 3];

        // --- 奇数轮 (Odd-number rounds) ---
        wb0 = TE0[wa0 >> 24] ^ TE1[(wa1 >> 16) & 0xFF] ^ TE2[(wa2 >> 8) & 0xFF] ^ TE3[wa3 & 0xFF] ^ keys[8 * i + 4];
        wb1 = TE0[wa1 >> 24] ^ TE1[(wa2 >> 16) & 0xFF] ^ TE2[(wa3 >> 8) & 0xFF] ^ TE3[wa0 & 0xFF] ^ keys[8 * i + 5];
        wb2 = TE0[wa2 >> 24] ^ TE1[(wa3 >> 16) & 0xFF] ^ TE2[(wa0 >> 8) & 0xFF] ^ TE3[wa1 & 0xFF] ^ keys[8 * i + 6];
        wb3 = TE0[wa3 >> 24] ^ TE1[(wa0 >> 16) & 0xFF] ^ TE2[(wa1 >> 8) & 0xFF] ^ TE3[wa2 & 0xFF] ^ keys[8 * i + 7];
    }

    // 将结果写回指针 (如果需要在函数外使用)
    wa[0] = wa0; wa[1] = wa1; wa[2] = wa2; wa[3] = wa3;
    wb[0] = wb0; wb[1] = wb1; wb[2] = wb2; wb[3] = wb3;
}

但是要值得注意的是,最后一轮没有MixColumn操作,所以这一轮要额外使用S盒操作一下,不过在openssl当中,加密过程的S盒也融入了T表当中。有了加密过程解密过程也可以按照这种方式扩展了。

posted @ 2025-12-12 11:43  x0rrrr  阅读(0)  评论(0)    收藏  举报