目录

0.书接上文

1.兼容 51

2.密钥长度单位的转换

3.定义数据块的大小

4.定义加密轮数

5.有限域

总结一下

拓展:为什么 8 位二进制00011011对应多项式x⁸ + x⁴ + x³ + x + 1?

第一步:二进制与多项式的基础对应

第二步:为什么会多一个x⁸项?

6.子密钥表

( 1) static

( 2) xdata

( 3) unsigned char

( 4) g_roundKeyTable

( 5) [4*Nb*(Nr+1)]

总结

7.Sbox

一、Sbox 的核心功能:“搅乱” 数据,制造非线性

二、Sbox 的结构:两步运算形成的固定映射表

第一步:求 GF (2⁸) 上的乘法逆元

第二步:仿射变换(Affine Transformation)

最终:固定的映射表

三、sbox 的生成

1.有限域乘法

2.逆元

3.反射变换函数

4.生成sbox

总结

8.数据循环左移

1.作用

2.输出示例

9.批量异或两组数据

批量异或在 AES 中的核心应用:轮密钥加(AddRoundKey)

一、轮密钥加(AddRoundKey)的核心定义

二、轮密钥加在 AES 加密流程中的具体作用

三、轮密钥加的安全性意义

四、批量异或的实现函数:xor_bytes

10.轮密钥加(AddRoundKey)

一、核心定义与执行流程

二、数学表示

三、核心作用

11.篇幅原因,下文链接


0.书接上文

【AES加密】AES加密算法流程全解析

【AES加密】AES加密算法实现(附代码):1 AES头文件的书写

1.兼容 51

/*
* #ifndef __C51__:
__C51__ 是 Keil C51 编译器(专门用于 8051 系列单片机的编译器)在编译时自动定义的宏,
用于标识当前处于 C51 编译环境。#ifndef __C51__ 的意思是:
如果当前不是 C51 编译环境(即未定义__C51__宏),则执行下面的代码。
*/
#ifndef __C51__
    //在普通 C 编译器(如 PC 端的 GCC、MSVC)中,这些关键字不存在。因此这里将它们定义为 “空宏”
    #define code
    #define data
    #define idata
    #define xdata
    #define pdata
    //在普通 C 环境中,没有专门的 “布尔类型”,因此用unsigned char(无符号字符型,1 字节)
    //来模拟布尔类型BOOL(通常 0 表示假,非 0 表示真)。
    typedef unsigned char BOOL;
#else
    typedef bit BOOL;
#endif
  • code、data、idata、xdata、pdata 是 C51 编译器特有的存储类型关键字(用于指定变量在单片机内存中的存储位置,如code表示程序存储区 ROM,data表示内部数据区 RAM 等)。在普通 C 编译器(如 PC 端的 GCC、MSVC)中,这些关键字不存在。因此这里将它们定义为 “空宏”,目的是:当代码中使用这些关键字时(如code unsigned char buf[] = "abc";),在非 C51 环境下编译时会忽略这些关键字,避免编译报错。

  • typedef unsigned char BOOL;:在普通 C 环境中,没有专门的 “布尔类型”,因此用unsigned char(无符号字符型,1 字节)来模拟布尔类型BOOL(通常 0 表示假,非 0 表示真)。

当处于 C51 编译环境时(定义了C51),bit是 C51 特有的位类型(仅占 1 个二进制位,取值 0 或 1),更适合表示布尔值(节省单片机有限的内存)。因此这里将BOOL定义为bit类型。

这段代码通过条件编译,让同一套代码既能在 8051 单片机的 C51 环境下编译(使用特有的bit类型和存储关键字),也能在普通 C 环境下编译(忽略存储关键字,用unsigned char模拟布尔类型),实现了代码的跨环境兼容性。

#define data、#define idata、#define xdata、#define pdata:这些也是宏定义,分别用于标识不同类型的数据存储:

  • data:表示将变量存储在片上 RAM 的直接寻址区,即 8051 的内部 RAM 的前 128 字节(地址范围为 0x00-0x7F)。

  • idata:表示将变量存储在片上 RAM 的间接寻址区,通常是 8051 的内部 RAM 的后 128 字节(地址范围为 0x80-0xFF)。

  • xdata:这个关键字用于指示变量存储在外部数据存储器(如外部 RAM)中。

  • pdata:表示变量存储在 8051 的片外 RAM 的分页区,即外部 RAM 的一个特定分页区域,通常是 256 字节的页内存。

2.密钥长度单位的转换

/*
* Nk表示AES密钥的长度(单位是字,4字节/字)
* 假设AES_KEY_LENGTH 为128,192或256,则Nk分别为4,6,8
*/
#define Nk (AES_KEY_LENGTH /32)

3.定义数据块的大小

/*
*Nb表示数据块的大小(单位是字),固定为4.这是AES的固定标准,AES处理的是4X4的字节块。
*/
#define Nb  4

4.定义加密轮数

// Nr 表示AES的轮数,取决于密钥长度。通过#if 语句确定 AES_KEY_LENGTH 值为128、192或256时的轮数。
#if    AES_KEY_LENGTH == AES_KEY_LENGTH_128
    #define Nr  10
#elif  AES_KEY_LENGTH == AES_KEY_LENGTH_192
    #define Nr  12
#elif  AES_KEY_LENGTH == AES_KEY_LENGTH_256
    #define Nr  14
#else
    #error AES_KEY_LENGTH must be 128, 192 or 256 BOOLS!
#endif

Nr 不适合直接使用枚举确定的原因主要在于 AES 算法的实现要求 Nr 的值依赖于编译时的宏定义 AES_KEY_LENGTH,这使得轮数值在编译时就必须根据密钥长度来确定。

5.有限域

/*
* BPOLY 用于AES中有限域(GF(2^8))运算。其值0x1B 表示GF(2^8)中的不可约多项式
*(x^8+X^4+X^3+X+1)的低8位。
在AES的 MixColumns 操作中,有限域多项式 BPOLY 被用作乘法的模,确保运算在8位内循环。
*/
#define BPOLY 0x1B

我们知道 AES 是一种加密算法,核心是对数据(比如一段文字、一张图片)做各种复杂的数学变换,让明文变成乱码(密文)。这些变换里,有一步很关键:对 8 位二进制数(比如10110011这种 8 位数字)做 “乘法”。

但这里的 “乘法” 不是我们平时学的整数乘法,而是一种特殊的 “有限域乘法”。你可以理解为:这些 8 位数字生活在一个 “有限的小圈子” 里(专业名叫GF(2^8)),圈子里的规则是 “任何运算结果都不能超过 8 位”—— 就像钟表上的数字永远在 1-12 之间,超过了就 “绕回去”。

但是问题来了:乘法很容易 “出圈”

比如两个 8 位数字相乘,结果可能变成 9 位、10 位(就像 12 点的钟表上,11 点加 2 点不能是 13 点,得绕回 1 点)。这时候就需要一个 “规矩” 来把超范围的结果 “拉回” 8 位里。

这个 “规矩” 就是不可约多项式,而0x1B就是 AES 里规定的这个 “规矩”。

那0x1B具体是个什么 “规矩”?

0x1B是十六进制,转成二进制是 0001 1011,但因为是 8 位的 “圈子”,完整的规矩其实是一个多项式:x⁸ + x⁴ + x³ + x + 1(可以理解为一种 “减法公式”)。

当两个 8 位数字相乘结果超过 8 位时,就用这个多项式 “减一减”(专业叫 “取模”),直到结果变回 8 位。比如:

  • 假设相乘后得到一个 9 位的数,用0x1B对应的规则一处理,就会变成 8 位,刚好留在 “圈子” 里。

为什么非得是0x1B,而不是其他数?

主要有三个原因:

  1. 大家得统一规矩加密和解密必须用同一个 “规矩”,否则解密时就认不出密文了。AES 是国际标准,必须规定一个唯一的 “规矩” 让全世界遵守,0x1B就是被选中的那个。

  2. 计算起来方便0x1B对应的多项式x⁸ + x⁴ + x³ + x + 1,在硬件(比如芯片)和软件里实现起来特别简单。就像选工具时,肯定选顺手的那一个,0x1B就是那个 “顺手的工具”。

  3. 安全性经过验证密码算法的核心是 “难破解”。0x1B经过大量测试,用它做出来的 AES 加密,破解难度非常高。如果换一个多项式,可能会出现漏洞,让加密变脆弱。

总结一下

0x1B就像 AES 里的一把 “尺子”:

  • 作用是保证 8 位数字相乘后不会 “出圈”,始终在 8 位范围内运算;

  • 选它是因为全世界统一用(标准)、算起来方便(效率高)、加密够安全(难破解)。

如果没有这个 “尺子”,AES 的运算就会乱套,加密解密也无法统一,更谈不上安全了。

拓展:为什么 8 位二进制00011011对应多项式x⁸ + x⁴ + x³ + x + 1?

第一步:二进制与多项式的基础对应

在有限域 GF (2⁸) 中,一个 8 位二进制数可以对应一个 “8 次以内的多项式”,规则是:

  • 二进制的每一位(从右到左,即最低位到最高位)对应多项式中x⁰(x 的 0 次方,即 1)、x¹、x²……x⁷的系数。

  • 某一位是 “1”,就表示多项式包含这一项;是 “0” 就不包含。

比如00011011(8 位)的多项式应该是x⁴ + x³ + x + 1

第二步:为什么会多一个x⁸项?

因为0x1B的作用是 “处理乘法溢出”—— 当两个 8 位二进制数相乘时,结果可能超过 8 位(比如变成 9 位),这时候需要用 “模运算” 把结果 “砍回” 8 位。

模运算的本质是:如果结果超过 8 位(即出现了x⁸项,因为 8 位二进制的最高位是x⁷),就用x⁸ + x⁴ + x³ + x + 1这个多项式来 “替换”x⁸项。

具体来说:假设乘法结果里有x⁸,根据多项式规则,x⁸ = x⁴ + x³ + x + 1(因为x⁸ + x⁴ + x³ + x + 1 = 0 → 移项得x⁸ = x⁴ + x³ + x + 1)。这样一来,x⁸就被换成了低次项(最高x⁴),结果自然就变回 8 位以内了。

所以总结:0x1B的 8 位二进制是00011011,它直接对应x⁴ + x³ + x + 1;但因为它的作用是处理 “超过 8 位的x⁸项”,所以完整的不可约多项式要加上x⁸,即x⁸ + x⁴ + x³ + x + 1。

6.子密钥表

// AES子密钥表,当密钥长度为128位时,占用176字节空间
static xdata unsigned char g_roundKeyTable[4*Nb*(Nr+1)];

AES 加密的核心过程是对数据进行多轮(“Round”)变换,每一轮都需要一个特定的 “子密钥”(轮密钥)来参与运算。这条语句就是定义一个数组,专门用来存放所有轮的子密钥,相当于一个 “密钥表”。

( 1) static

表示这个数组是 “静态变量”,作用域仅限当前 C 文件(不能被其他文件的代码直接访问),避免了变量名冲突,也让代码结构更清晰。保证被多次调用之间维持其值,而不是每次函数调用时重新初始化,对对于需要多个函数调用之间维持状态的变量是有用的。

( 2) xdata

这是 8051 单片机(C51 编译器)特有的关键字,指定数组的存储位置 ——外部数据存储器(片外 RAM)。AES 的轮密钥表需要较大的空间(比如 128 位密钥时要存 176 字节),而 8051 单片机的内部 RAM(data/idata)通常很小(比如只有 128 字节),放不下,所以用xdata放到外部 RAM 中。

( 3) unsigned char

数组的元素类型是 “无符号字符型”,每个元素占 1 字节(8 位)。AES 的子密钥本质上是 8 位的字节数据,用unsigned char刚好能存储,且符合加密中对字节级操作的需求。

( 4) g_roundKeyTable

数组名,字面意思是 “轮密钥表”(g_通常表示全局变量,roundKey指轮密钥,Table表示表),直观体现了数组的用途 —— 存放所有轮的子密钥。

( 5) [4*Nb*(Nr+1)]

数组的大小,这是最关键的部分,和 AES 的算法参数直接相关:

  • Nb:AES 中 “数据块大小” 的参数(以 “32 位字” 为单位)。AES 的明文 / 密文数据块固定是 128 位(16 字节),128 位 = 4 个 32 位字(32*4=128),所以Nb=4。

  • Nr:AES 的 “轮数”,由密钥长度决定:

    • 128 位密钥 → Nr=10轮;

    • 192 位密钥 → Nr=12轮;

    • 256 位密钥 → Nr=14轮。

  • 计算逻辑:每一轮需要4*Nb个字节的子密钥(因为每轮操作对应一个 4×4 的字节矩阵,刚好4*Nb=16字节),而 AES 的轮数是Nr,但初始化时还需要额外 1 轮的密钥(第 0 轮),所以总轮数是Nr+1。

  • 例如 128 位密钥时:4*Nb*(Nr+1) = 4*4*(10+1) = 176字节,和注释 “占用 176 字节空间” 完全对应。

总结

这条代码定义了一个 “轮密钥表数组”,专门用来存放 AES 加密中所有轮(包括初始轮)的子密钥。通过xdata指定存储在外部 RAM 以节省内部空间,大小由 AES 的块大小(Nb)和轮数(Nr)动态确定,确保能适配不同密钥长度(128/192/256 位)的需求。

7.Sbox

//加密用的SBox
static code const unsigned char sBox[256]= {
        // 0     1     2     3     4     5     6     7     8     9     a     b     c     d     e     f
        0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, // 0
        0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, // 1
        0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, // 2
        0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, // 3
        0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, // 4
        0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, // 5
        0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, // 6
        0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, // 7
        0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, // 8
        0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, // 9
        0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, // a
        0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, // b
        0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, // c
        0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, // d
        0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, // e
        0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 };// f
//解密用的SBox
const unsigned char  invsBox[256] = {
        // 0     1     2     3     4     5     6     7     8     9     a     b     c     d     e     f
        0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, // 0
        0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, // 1
        0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, // 2
        0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, // 3
        0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, // 4
        0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, // 5
        0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, // 6
        0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, // 7
        0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, // 8
        0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, // 9
        0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, // a
        0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, // b
        0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, // c
        0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, // d
        0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, // e
        0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d };// f  

在 AES 加密算法中,Sbox(Substitution Box,字节替换表) 是最核心的 “非线性变换” 部件,相当于一个 “密码本”,负责把输入的 8 位字节(0-255 之间的数)映射成另一个 8 位字节,目的是让明文和密文之间的关系变得极其复杂,难以被破解。

一、Sbox 的核心功能:“搅乱” 数据,制造非线性

AES 的加密过程本质是对数据做一系列变换(比如移位、混合、与密钥结合),但如果这些变换都是 “线性的”(比如简单的加减乘),攻击者很容易通过数学规律反推明文或密钥。

Sbox 的作用就是提供非线性变换:输入的微小变化(比如某一位从 0 变 1)会导致输出完全不同,就像 “蝴蝶效应”,让数据的关联性被彻底打乱。这种非线性是 AES 安全性的关键 —— 没有它,AES 很容易被 “线性攻击” 破解。

二、Sbox 的结构:两步运算形成的固定映射表

Sbox 不是随便设计的,而是通过严格的数学运算生成的固定 8×8 表格(256 个输入对应 256 个输出)。它的构造分两步,核心是 “有限域运算” 和 “仿射变换”:

第一步:求 GF (2⁸) 上的乘法逆元

首先,对输入的 8 位字节(记为b)做 “乘法逆元” 运算:

  • 如果b不是 0(即非零字节),计算它在有限域 GF (2⁸) 中的 “逆元”(类似普通数学中 “倒数” 的概念,比如 2 的倒数是 0.5,因为 2×0.5=1)。在 GF (2⁸) 中,每个非零字节b都有唯一的逆元b⁻¹,满足b × b⁻¹ = 1(这里的乘法是有限域内的多项式乘法,模之前说的 0x1B 多项式)。

  • 如果b是 0,则逆元仍为 0(特殊处理,避免无意义运算)。

第二步:仿射变换(Affine Transformation)

为了进一步增强非线性,对第一步得到的逆元再做一次 “仿射变换”。这一步是线性变换 + 常数偏移,可以理解为 “按固定规则打乱比特位并加一个固定值”。

具体来说,仿射变换的数学表达是:对 8 位字节的每一位y_i(输出),由第一步结果的 8 位x_j(输入)通过如下公式计算:y_i = x_i + x_(i+1) + x_(i+2) + x_(i+3) + x_(i+4) + c_i

其中:

  • 加法是 “异或”(GF (2) 中的加法,1+1=0,1+0=1);

  • i+1等超过 7 时循环(比如i=7时,i+1是 0);

  • c_i是固定的常数向量,对应二进制1100011(即十六进制 0x63),用于增加随机性。

最终:固定的映射表

经过上面两步运算后,每个输入字节(0-255)都会对应一个唯一的输出字节。这些映射关系被预先计算好,做成一个 8×32 的表格(或 256 个元素的数组),就是 Sbox。

例如:

  • 输入 0x00 → 经过逆元(0x00)和仿射变换 → 输出 0x63;

  • 输入 0x01 → 逆元是 0x01(因为 1×1=1),仿射变换后 → 输出 0x7c;

  • 所有 AES 实现都用同一个 Sbox(标准规定),确保加密解密的一致性。

三、sbox 的生成

除了使用现成的sbox,我们也可以根据sbox的原理自己写一个生成sbox的函数:

1.有限域乘法

#include <stdio.h>

// 定义AES中GF(2^8)有限域的不可约多项式,十六进制0x1B对应多项式x⁸ + x⁴ + x³ + x + 1
#define AES_MOD 0x11B  

/**
 * @brief 计算两个8位字节在GF(2^8)有限域中的乘法
 * @param a 乘数(8位无符号整数,范围0-255)
 * @param b 乘数(8位无符号整数,范围0-255)
 * @return 乘积结果(8位无符号整数,经过模AES_MOD处理,确保在GF(2^8)范围内)
 * 原理:模拟二进制乘法的分步计算,结合有限域的模运算规则(超过8位时用AES_MOD修正)
 */
unsigned char gmul(unsigned char a, unsigned char b) {
    unsigned char p = 0;   // 初始化乘积结果为0(GF(2^8)中加法的初始值)
    while (b) {            // 循环处理乘数b的每一个二进制位(直到b的所有位都处理完)
        // 如果b的最低位为1(即当前位需要参与乘法),则将a累加到结果p中
        // GF(2^8)中的"加法"等价于异或(XOR),因为1+1=0,1+0=1,符合有限域加法规则
        if (b & 1) {
            p ^= a;  
        }
        // 将a左移1位(等价于乘以x,即多项式乘法中次数加1)
        // 若a的最高位(第7位,对应0x80)为1,左移后会产生第8位(溢出)
        // 此时需要用不可约多项式AES_MOD进行模运算,通过异或AES_MOD消除溢出位,确保结果仍为8位
        a = (a << 1) ^ ((a & 0x80) ? AES_MOD : 0); 
        // 将b右移1位,处理下一个二进制位(从低位到高位依次处理)
        b >>= 1;
    }
    return p;  // 返回最终的乘积结果(已确保在GF(2^8)范围内)
}

可能会有疑问:

Q: 这里面的AES_MODE为什么要定义为0x11B,因为a为unsigned char 类型就是8位,左移一位也会截断,异或的时候AES_MODE 最高的那一位不是没用吗?

A: AES_MOD的最高位不是没用的。在GF的乘法运算中,AES_MOD作为不可约的多项式,其所有位都参与了运算。当a的最高位为1时,与AES_MOD进行异或操作,确保了运算结果的正确性和在GF内。

2.逆元

// 使用暴力搜索找到 GF(2^8)中的逆元
unsigned char ginv(unsigned char x) {
    if (x == 0) return 0; //0 没有逆元
    for (unsigned char i = 1; i < 256; i++) {
        if (gmul(x, i) == 1) {
            return i;
        }
    }
    return 0; // 不应该到达此行
}

3.反射变换函数

// 仿射变换函数:AES中Sbox生成的第二步操作,用于增强非线性特性
// 输入:有限域GF(2^8)中元素的乘法逆元(8位字节x)
// 输出:经过仿射变换后的8位字节(Sbox的最终输出之一)
// 原理:通过循环移位实现线性变换,叠加固定常数偏移,进一步打乱输入字节的比特关系
unsigned char affineTransform(unsigned char x)
{
    // 计算仿射变换结果,由三部分组成:
    // 1. 原始输入x
    // 2. 四次循环移位的结果(分别循环左移1/2/3/4位),通过异或组合实现线性变换
    // 3. 固定常数偏移0x63(二进制01100011,AES标准规定)
    unsigned char result = x 
        ^ ((x << 1) | (x >> 7))  // 循环左移1位:左移1位后,用原最高位(x>>7)补充最低位
        ^ ((x << 2) | (x >> 6))  // 循环左移2位:左移2位后,用原高2位(x>>6)补充低2位
        ^ ((x << 3) | (x >> 5))  // 循环左移3位:左移3位后,用原高3位(x>>5)补充低3位
        ^ ((x << 4) | (x >> 4))  // 循环左移4位:左移4位后,用原高4位(x>>4)补充低4位
        ^ 0x63;                  // 叠加固定常数偏移(AES标准规定的仿射变换常数)
    
    return result & 0xFF;  // 确保结果为8位(虽然运算后天然为8位,此操作增强代码健壮性)
}

4.生成sbox

void generate_aes_sbox(unsigned char sbox[256]) {
    for (int i = 0; i < 256; i++) {
        unsigned char x= ginv(i); //计算逆元素
        sbox[i] =affineTransform(x); // 应用仿射变换
    }
    sbox[0]=0x63; // S-Box[0] 的特例处理
}

// 生成 AES 逆 S-Box
void generate_aes_inverse_sbox(const unsigned char sbox[256],unsigned char inv_sbox[256])
 {  
    for (int i = 0; i < 256; i++) 
    {
        inv_sbox[sbox[i]] = i;
     }
}

int main() {
    unsigned char sbox[256];
    unsigned char inv_sbox[256];

    // 生成 AES 加密的 S-Box
    generate_aes_sbox(sbox);
    printf("AES S-Box: \n");
    for (int i = 0; i < 256; i++) {
        printf("0x%02X", sbox[i]);
        if ((i + 1) % 16 == 0) printf("\n");
        else printf(", ");
    }

    // 生成 AES 解密的逆 S-Box
    generate_aes_inverse_sbox(sbox, inv_sbox);
    printf("AES Inverse S-Box: \n");
    for (int i = 0; i < 256; i++) {
        printf("0x%02X", inv_sbox[i]);
        if ((i + 1) % 16 == 0) printf("\n");
        else printf(", ");
    }
    return 0;
}

总结

Sbox 的功能是通过非线性映射 “搅乱” 数据,打破明文和密文的简单关联;其结构是通过 “有限域逆元 + 仿射变换” 生成的固定表格,既保证了安全性(难以破解映射关系),又保证了效率(直接查表即可,无需实时计算)。它是 AES 中 “混淆” 思想的核心体现 —— 让攻击者无法通过密文反向推导出明文或密钥的规律。

8.数据循环左移

1.作用

在 AES 中,数据的旋转(rotation_word)是为了增强加密的安全性,具体原因如下:

  • 混淆性:旋转操作可以增加数据的复杂性,使得输出与输入之间的关系更加难以预测,从而增强加密的安全性。

  • 防止线性攻击:通过将字节的位置打乱,旋转操作有助于防止某些线性攻击,这些攻击利用输入和输出之间的线性关系来破解密钥。

  • 提高扩散性:旋转操作可以增加输入数据对输出结果的影响范围,使得一个字节的改变可以影响到多个字节。这种扩散性是加密算法的一个重要特性,有助于确保即使只改变一个输入比特,输出也会发生显著变化。

  • 组合其他变换:旋转是 AES 变换中的一部分,通常与其他变换(如替代、混合列等)结合使用,以形成更强的加密效果。

rotation_word 函数的作用是将一个 4 字节(32 位)数据进行字节循环左移操作。也就是说,它将输入的 4 字节数据向左旋转 1 字节。

2.输出示例

以下是rotation_word 函数的一个示例输入和输出:

  • 初始输入:[0xB1, 0xB2, 0xB3,0xB4](十六进制)

  • 执行旋转后:[0xB2, 0xB3,0xB4, 0xB1]

/*
* 函数名:    rotation word
* 描述:     定义一个静态函数,旋转输入的字(4个字节)
* 输入参数:  pWord -- 要右移的4字节数据。
* 输出参数:  pWord -- 右移后的4字节数据。
* 返回值:    无。
*/
static void rotation_word(unsigned char *pWord) {
    //检查输入指针是否为空,避免潜在的空指针解引用错误
    if (pWord == NULL)return;//参数检查,若为空则返回
    
    // 保存第一个字节到临时变量
    unsigned char temp = pWord[0];
    
    // 将字的每个字节向前移动一个位置
    pWord[0]=pWord[1];//第一个字节变为第二个字节
    pWord[1]=pWord[2];// 第二个字节变为第三个字节
    pWord[2]=pWord[3];//第三个字节变为第四个字节
    
    // 将原本的第一个字节放到最后一个位置
    pWord[3] = temp;// 第四个字节变为原来的第一个字节
}

9.批量异或两组数据

批量异或在 AES 中的核心应用:轮密钥加(AddRoundKey)

批量异或操作在 AES 加密算法中的核心作用,是实现关键步骤轮密钥加(AddRoundKey) —— 这是 AES 中唯一直接将明文 / 中间加密数据与密钥结合的操作,也是保障加密安全性的核心环节。

一、轮密钥加(AddRoundKey)的核心定义

轮密钥加的本质是通过XOR(异或) 操作,将 AES 中的 “中间数据载体” 与 “轮密钥” 强制结合,核心要素如下:

  • 状态(State):AES 中存储中间加密数据的载体,是一个 4×4 字节矩阵(共 16 字节,对应 AES 固定的 128 位数据块大小);

  • 轮密钥:由原始密钥通过 “AES 密钥扩展” 算法生成的子密钥,每一轮加密对应一个独立的轮密钥,长度与状态一致(16 字节);

  • 操作逻辑:将状态矩阵的每个字节,与轮密钥的对应字节逐一执行异或操作,实现 “数据 - 密钥” 的融合。

二、轮密钥加在 AES 加密流程中的具体作用

轮密钥加贯穿 AES 加密的全流程,分三个关键阶段,逐步将密钥影响扩散到数据中:

1.初始轮密钥加:首次注入密钥信息

加密流程的第一步,将原始明文数据块(初始状态)与 “第一个轮密钥”(密钥扩展生成的首个子密钥)执行异或操作。这是明文与密钥的第一次直接结合,标志着数据从 “明文态” 进入 “加密中间态”。

2.每轮密钥加:持续扩散密钥影响

AES 加密包含多轮变换(128 位密钥对应 10 轮、192 位对应 12 轮、256 位对应 14 轮),每一轮的最后一步,都会将当前的中间状态与 “该轮专属轮密钥” 执行异或操作。通过每轮的异或,密钥的影响会持续渗透到中间数据中,避免数据与密钥的关联被轻易破解。

3.最终轮密钥加:生成最终密文

在最后一轮加密变换结束后,会执行最终一次轮密钥加,此时的异或结果即为最终的加密密文。这一步完成了 “数据 - 密钥” 的最终融合,确保密文中携带完整的密钥影响,且无法脱离密钥单独还原。

三、轮密钥加的安全性意义

轮密钥加是 AES 安全性的核心支撑,关键价值在于:

  1. 唯一的 “数据 - 密钥直接结合点”:AES 的其他步骤(如字节替换、行移位、列混合)均是对数据的独立变换,只有轮密钥加能直接将密钥信息注入数据;

  2. 扩散密钥影响:通过多轮异或,密钥的每一位都会逐步扩散到数据的每一位,打破 “局部数据与局部密钥” 的简单关联,增强数据的混淆性;

  3. 异或的天然安全性:异或操作具有 “可逆性”(同一数据与同一密钥异或两次可还原原始数据),既保证加密 / 解密的对称性,又能确保 “无密钥时无法还原数据”。

四、批量异或的实现函数:xor_bytes

轮密钥加的核心逻辑通过xor_bytes函数实现,该函数用于对两组批量数据执行逐字节异或,参数与功能对应如下:

函数核心逻辑:遍历两组数据的每一个字节,对pData1[i]与pData2[i]执行异或操作,结果覆盖到pData1(或单独存储),最终完成 “状态与轮密钥的结合”,实现轮密钥加步骤。

/**
 * @brief 批量逐字节异或两组数据
 * 
 * 功能说明:将 pData2 中每个字节与 pData1 中对应位置的字节执行异或(XOR)操作,
 * 异或结果直接覆盖存储到 pData1 中,实现数据的逐字节混淆或合并。
 * 典型应用场景:加密算法中的“轮密钥加”(如AES的AddRoundKey步骤)、数据简单混淆等。
 * 
 * @param[out/in] pData1 第一组数据的指针(双重角色):
 *                       - 输入:作为异或操作的原始数据之一;
 *                       - 输出:存储最终的异或结果(会覆盖原始值)。
 * @param[in]     pData2 第二组数据的指针:仅作为异或操作的输入数据,内容不会被修改。
 * @param[in]     nCount 需异或的数据字节长度:指定两组数据中参与异或的字节总数(需确保两组数据长度均不小于此值)。
 * 
 * @note 1. 内存安全:调用者必须确保 pData1 和 pData2 指向的内存空间有效,且至少包含 nCount 个字节,
 *            否则会导致内存访问越界,引发程序异常。
 *       2. 空指针检查:本函数未对 pData1、pData2 是否为 NULL 进行判断,若传入空指针会导致运行时错误,
 *            调用前需自行确保指针非空。
 *       3. 作用域:函数被声明为 static,仅在当前 .c 文件内可见,无法被其他文件调用。
 */
 static void xor_bytes(unsigned char *pData1, const unsigned char *pData2, unsigned char nCount)
 {
     unsigned char i;  // 循环计数变量,用于遍历每一个字节
     
     // 循环遍历所有字节(从第0个到第nCount-1个)
     for (i = 0; i < nCount; i++)
     {
         // 核心操作:将 pData1[i] 与 pData2[i] 异或,结果直接存回 pData1[i]
         // 异或特性:a ^ b ^ b = a,因此若需还原数据,可再次调用本函数(传入相同的 pData2 和 nCount)
         pData1[i] ^= pData2[i];
     }
 }

10.轮密钥加(AddRoundKey)

一、核心定义与执行流程

轮密钥加(AddRoundKey)是 AES 加密算法中唯一直接将数据与密钥结合的核心步骤,通过逐字节异或(XOR) 操作,将 “中间数据载体” 与 “轮密钥” 强制融合,以此增强数据的混淆性和加密安全性。

其核心前提是:轮密钥并非原始密钥,而是由原始密钥通过 “AES 密钥扩展算法” 生成的子密钥(不同轮次对应不同轮密钥),且轮密钥长度与 AES 数据块长度一致(固定 16 字节,对应 128 位数据块,存储为 4×4 字节矩阵)。

轮密钥加贯穿 AES 加密全流程,分三个关键阶段执行:

1.初始轮密钥加(加密第一步)原始明文数据块(首次被称为 “状态”,即 4×4 字节矩阵)与 “第一个轮密钥” 执行逐字节异或,这是明文与密钥的第一次直接结合,标志数据从 “明文态” 进入 “加密中间态”。

2.每轮轮密钥加(每轮加密最后一步)AES 加密包含多轮变换(128 位密钥对应 10 轮、192 位对应 12 轮、256 位对应 14 轮),每一轮变换的最后一步,都会将当前的 “中间状态” 与 “该轮专属轮密钥” 逐字节异或。此步骤确保每一轮的中间结果都融入新的密钥信息,避免数据与密钥的关联被轻易破解。

3.最终轮密钥加(生成密文)最后一轮加密变换结束后,执行最终一次轮密钥加,此时的异或结果即为最终的 AES 密文,完成 “数据 - 密钥” 的最终融合。

二、数学表示

轮密钥加的计算逻辑简洁且高效,核心是 “状态” 与 “轮密钥” 对应位置字节的逐位异或(XOR) 操作,数学表达式如下:状态[i][j] = 状态[i][j] ⊕ 轮密钥[i][j]

  • 符号说明:⊕ 代表逐位异或操作(二进制位规则:0⊕0=0、0⊕1=1、1⊕0=1、1⊕1=0);

  • 维度说明:状态和轮密钥均为 4×4 字节矩阵(i表示行索引 0-3,j表示列索引 0-3),操作时严格对应矩阵的同一位置字节;

  • 特点:异或操作具有 “可逆性”—— 若对结果再次与同一轮密钥异或,可还原原始数据(这是 AES 解密时能复用轮密钥加逻辑的核心原因)。

三、核心作用

轮密钥加是 AES 安全性的关键支撑,其核心价值体现在两点:

1.唯一的 “数据 - 密钥直接结合点”AES 的其他步骤(如字节替换 SubBytes、行移位 ShiftRows、列混淆 MixColumns)均是对 “状态” 的独立数据变换,仅轮密钥加能直接将密钥信息注入数据,确保加密结果与原始密钥强关联 —— 无密钥则无法还原数据。

2.扩散密钥信息,增强混淆性通过多轮次的异或操作,原始密钥的每一位信息会逐步扩散到 “状态” 的每一个字节中,打破 “局部数据对应局部密钥” 的简单关联。最终生成的密文中,每一位都融合了密钥和明文的双重信息,大幅提升攻击者的破解难度(需同时突破数据变换和密钥扩散的双重逻辑)。

/**
 * @brief 执行AES加密中的“轮密钥加”操作
 * 
 * 功能说明:将中间状态数据与当前轮的子密钥进行逐字节异或(XOR)操作,
 * 异或结果直接覆盖存储到状态数据中,实现“状态-子密钥”的融合。
 * 此操作是AES中唯一直接结合数据与密钥的步骤,固定处理16字节数据(4×4字节矩阵)。
 * 
 * @param[in,out] pState 指向状态数据的指针(4×4字节矩阵):
 *                       - 输入:当前轮的中间状态数据;
 *                       - 输出:异或后更新的状态数据(覆盖原始值)。
 * @param[in]     pRoundKey 指向当前轮子密钥的指针(4×4字节矩阵):
 *                       子密钥由原始密钥经扩展生成,仅作为异或输入,内容不被修改。
 * 
 * @note 1. 数据长度:固定为4×Nb字节(AES中Nb=4,即16字节),与AES数据块大小一致;
 *       2. 指针安全:函数未对pState、pRoundKey进行空指针检查,调用者需确保指针非空且
             指向有效内存(至少16字节);
 *       3. 实现依赖:内部通过调用xor_bytes函数完成逐字节异或操作。
 */
static void AddRoundKey(unsigned char *pState, const unsigned char *pRoundKey)
{
    // 调用批量异或函数,处理4×Nb字节(16字节)数据,结果存入pState
    xor_bytes(pState, pRoundKey, 4 * Nb);
}

/**
 * @brief 轮密钥加操作的宏定义形式
 * 
 * 说明:通过宏展开直接调用xor_bytes函数,实现与AddRoundKey函数完全相同的功能。
 * 相比函数形式,宏定义可节省4字节的data存储空间(适合资源受限的嵌入式环境,如8051单片机)。
 * 
 * @param pState  同AddRoundKey函数的pState参数(状态数据指针)
 * @param pRoundKey 同AddRoundKey函数的pRoundKey参数(子密钥指针)
 */
 //AddRoundKey的宏形式,比函数形式可以节省4字节的data数据
#define AddRoundKey(pState, pRoundKey) \
    xor_bytes((pState), (pRoundKey), 4 * Nb)

宏形式相当于的内联函数,在调用这个函数的地方直接展开,避免了函数调用时形参的压栈开销。

11.篇幅原因,下文链接

【AES加密】AES加密算法实现(附代码):2 AES工具函数的书写(下)

更多推荐