MATLAB实现AES与CMAC:轮函数拆解、子密钥生成及验证
2026/9/15 4:19:32 网站建设 项目流程

简介:一套基于MATLAB开发的AES加解密与CMAC认证实现代码,面向密码学学习者、算法开发者和需要轻量级加密认证实验的研究人员。资源覆盖AES完整加解密流程,并实现基于AES的CMAC消息认证码生成,从密钥扩展、S盒替换到轮运算、GF乘法均有独立函数模块,便于快速理解块密码工程实现。包内共16个文件,全部为.m源脚本,压缩包仅7KB,代码紧凑无冗余。从文件名即可看出:KeyExpansion.m负责密钥调度,AESjiami.m与AESjiemi.m实现加解密主流程,CMAC.m完成认证标签构造,其他如bitxor128、Sbox、lunyunsuan等辅助函数支持位运算、置换与轮变换,模块划分清晰,便于调试复用。目前已有505人学习浏览。借助这套源码,读者可对照标准流程验证AES结果,掌握CMAC基于子密钥处理消息分组的实现细节,也能作为课程设计或安全协议仿真的基础,并为进一步算法优化与二次开发提供起点。

1. 为什么要在MATLAB里自己写一套AES和CMAC

在MATLAB里做通信协议仿真时,经常要面对一个尴尬:数据链路层的完整性校验需要AES加密和CMAC认证,但系统库里的AES实现要么依赖额外工具箱,要么是Java/Python接口,很难直接嵌入Simulink的S-Function或者自定义的帧处理流程。于是我自己整理了一套纯MATLAB实现的AES加解密和基于AES的CMAC认证代码,把S盒生成、密钥扩展、轮函数、128位位移和伽罗华域乘法都拆成了独立文件,方便逐条调试和验证。这套代码适合三类人:一是需要在MATLAB里做密码学教学演示的,二是做通信仿真需要加入安全层的,三是想彻底搞懂每个AES轮函数底层运算细节的。下面从AES的轮函数开始,把每个文件对应的算法环节拆开讲清楚。

2. AES轮函数拆解:从S盒到各轮运算的MATLAB实现

2.1 文件结构与数据流

打开压缩包后可以看到,AES部分的代码按标准算法流程拆成了多个单文件函数。核心入口是AESjiami.mAESjiemi.m,分别负责加密和解密。内部调用的辅助函数如下表所示。

文件作用算法阶段
Sbox.m生成AES标准S盒SubBytes
Sbox_ni.m生成逆S盒InvSubBytes
KeyExpansion.m密钥扩展,生成全部轮密钥轮密钥调度
GFchengfa.mGF(2^8)有限域乘法MixColumns/InvMixColumns
lunyunsuan_1_9.m第1轮到第9轮轮函数加密轮
lunyunsuan_10.m第10轮轮函数(无列混合)加密末尾轮
lunyunsuan_1_9_ni.m解密时前9轮逆运算解密轮
lunyunsuan_10_ni.m解密时末尾轮逆运算解密入口

在实际调用时,加密流程是先对128位状态矩阵做一次初始的AddRoundKey,然后循环执行前9轮轮函数,最后用第10轮收尾。这段流程在AESjiami.m内部体现为对上述函数的顺序调用。

% 假设明文和密钥都是 1x16 的 uint8 行向量 plain = uint8([0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]); key = uint8(zeros(1,16)); cipher = AESjiami(plain, key); % 解密并验证还原 restored = AESjiemi(cipher, key); isequal(plain, restored)

这里有一个容易忽略的约定:AES标准处理128位数据块,所以输入向量长度必须是16个字节。AESjiami.m内部会把1x16的向量reshape成4x4的列优先矩阵,这与FIPS-197文档中的状态矩阵定义一致。如果你从别的语言移植过AES代码,会发现很多实现将明文拼成行优先的1x16数组,结果对不上,问题往往就出在reshape方向上。

2.2 S盒与逆S盒的生成逻辑

Sbox.m的作用是生成AES的替换表。标准S盒是通过有限域GF(2^8)乘法逆运算加仿射变换得到的,但大多数工程实现不会每次运行时现场计算,而是直接预生成256字节的查找表。Sbox.m大概率返回的是1x256或256x1的uint8向量,具体维度需要你在MATLAB里size(Sbox())确认一下。

验证S盒正确性的一个快速方法是查表首项:标准AES的S盒满足Sbox(0) = 0x63Sbox(1) = 0x7c。如果对不上,说明表中字节序可能按高位在前排列,也可能是生成时仿射矩阵的位序写反了。同样,Sbox_ni.m生成的逆S盒满足Sbox_ni(Sbox(i)) = i。写一行断言即可验证:

sb = Sbox(); assert(sb(1) == 0x63, 'S盒首项错误'); for i = 0:255 assert(Sbox_ni(sb(i+1)+1) == i, 'S盒与逆S盒不互逆'); end

注意MATLAB下标从1开始,所以查表时要把字节值加1。这套代码里Sbox_ni.m应该采用的是同样的索引约定,否则在解密时会得到错乱的状态矩阵。

2.3 密钥扩展与轮函数调用顺序

KeyExpansion.m负责把16字节(AES-128)的种子密钥扩展成176字节的轮密钥。标准做法是先将种子密钥排成4个字(每个字4字节),然后通过RotWord、SubWord和Rcon逐字生成剩余40个字。在MATLAB实现中,返回格式通常是44x4的uint8矩阵,或者按轮数组织的元胞数组,每轮包含4个字。

轮函数部分,lunyunsuan_1_9.mlunyunsuan_10.m的区别在于:前9轮依次执行SubBytes、ShiftRows、MixColumns、AddRoundKey,而第10轮跳过了MixColumns。如果你在解谜题时发现最后一块密文总是差几个字节,优先检查是否在最后一轮错误地混入了列混合运算。

w = KeyExpansion(key); % 假定返回 44x4 uint8 轮密钥矩阵 % 第1轮调用示例(示意,不代表真实接口) % state = lunyunsuan_1_9(state, w(1:4,:)); % 第10轮调用示例 % state = lunyunsuan_10(state, w(41:44,:));

这里的lunyunsuan_1_9_ni.mlunyunsuan_10_ni.m对应解密过程。注意解密轮密钥的使用顺序与加密相反,且InvMixColumns需要用到GFchengfa.m提供的GF(2^8)乘法。在调试时,可以在每次AddRoundKey前打印state矩阵,和Python的aes库中间结果对比,很快就能定位是字节代换还是移位出了问题。我自己遇到过最隐蔽的坑是GFchengfa.m在处理0x01乘以某数时返回了0,原因是有限域乘法的初始累加器被错误清零,这会导致MixColumns后的一半字节错乱。

3. CMAC认证:子密钥生成与消息分块处理

3.1 CMAC算法与NIST SP 800-38B的关系

CMAC(Cipher-based Message Authentication Code)是专门基于分组密码设计的消息认证码算法,标准文档为NIST SP 800-38B。它解决了早期CBC-MAC在变长消息下的安全缺陷,核心思路是用两个子密钥K1和K2处理最后一个数据块,从而区分完整块和带填充的块。

算法过程可以概括为四步:

  1. 用全零块作为输入,用AES加密得到L。
  2. 由L左移1位生成K1,如果L的最高位为1,则异或常量Rb(AES-128对应0x87)。
  3. 由K1左移1位生成K2,同样根据最高位决定是否异或Rb。
  4. 对消息分块:如果最后一块是完整块,用K1;否则在末尾填充0x80后继续补0,并使用K2。

在MATLAB实现中,由于不存在原生的128位数据类型,所有子密钥生成操作必须拆解为对16字节数组进行位移、位测试和异或。

3.2 128位运算工具函数在工作中的作用

压缩包里的bitget128.mbitshift128.mbitxor128.mGFchengfa128.m就是为CMAC服务的底层工具。它们的配合关系如下表所示。

函数作用CMAC中的用途
bitshift128.m对128位数组整体左移1位生成K1和K2
bitget128.m提取128位数组中指定比特位判断左移前的最高位是否置1
bitxor128.m对两个128位数组按位异或异或常量Rb及消息块
GFchengfa128.m128位域上的乘法运算理论上可替代移位异或实现乘以x

这里需要注意,GFchengfa128.m的名字看起来和AES列混合里的GFchengfa.m很像,但两者作用域完全不同。AES列混合是GF(2^8)域上的8位乘法,而CMAC子密钥生成用到的左移和异或其实是GF(2^128)域上的乘以x运算。我在第一次看代码时误以为GFchengfa128.m是做GHASH的,后来读注释才发现它就是在实现K1、K2的移位异或逻辑。如果你要修改子密钥生成方式,务必区分这两个函数。

3.3 生成K1、K2并完成CMAC计算

下面是基于这套工具函数生成子密钥的示意代码,注意所有数组都约定为1x16的uint8行向量。

function [K1, K2] = genSubKey(key) L = AESjiami(zeros(1,16,'uint8'), key); K1 = bitshift128(L); if bitget128(L, 128) == 1 Rb = uint8([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x87]); K1 = bitxor128(K1, Rb); end K2 = bitshift128(K1); if bitget128(K1, 128) == 1 Rb = uint8([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x87]); K2 = bitxor128(K2, Rb); end end

这里的bitget128(L, 128)取的是L的最高比特位,对应128位整数的最左侧位。由于MATLAB没有统一的位序定义,你需要在真实代码里确认bitget128的第二个参数是字节索引还是比特索引,否则会拿到完全错误的高位标记。bitshift128则必须确保溢出位丢失,如果实现时用了普通的数值左移,可能会让超出128位的部分回卷,导致K1完全错误。

在CMAC主循环里,对消息的处理方式是先将消息按16字节切块,然后逐块异或到状态值上,再用AES加密。最后一块根据是否完整选择K1或K2完成异或:

function tag = CMAC(key, msg) msg = msg(:)'; remainder = mod(numel(msg), 16); if remainder == 0 blocks = reshape(msg, 16, [])'; blocks(end,:) = bitxor128(blocks(end,:), K1); else padded = [msg, uint8(zeros(1, 16-remainder))]; padded(remainder+1) = 0x80; blocks = reshape(padded, 16, [])'; blocks(end,:) = bitxor128(blocks(end,:), K2); end X = zeros(1,16,'uint8'); for i = 1:size(blocks,1) X = AESjiami(bitxor128(X, blocks(i,:)), key); end tag = X; end

这段代码省略了子密钥的局部计算,但展示了CMAC最关键的分块边界处理。注意一个工程细节:如果消息长度恰好是16字节的整数倍,最后一块仍然使用K1,但不需要做任何填充。如果消息为空,则视作一个不完整块,需要构造一个“0x80后跟15个0”的填充块并使用K2。很多初学CMAC的人会把空消息错当完整块,导致fake tag与标准测试向量对不上。

4. 加解密与CMAC的调用验证

4.1 用NIST测试向量验证AES加解密

不论代码是自己写的还是下载的,第一步永远是跑标准测试向量。NIST FIPS-197附录C给出的示例:密钥为000102030405060708090a0b0c0d0e0f,明文为00112233445566778899aabbccddeeff,加密密文应为69c4e0d86a7b0430d8cdb78070b4c55a

在MATLAB中手动构造字节数组的代码可以写成:

key = uint8(0:15); plain = uint8([0x00 0x11 0x22 0x33 0x44 0x55 0x66 0x77 ... 0x88 0x99 0xaa 0xbb 0xcc 0xdd 0xee 0xff]); cipher = AESjiami(plain, key); expected = uint8(sscanf('69c4e0d86a7b0430d8cdb78070b4c55a', '%2x')'); isequal(cipher, expected)

如果最后一位校验不通过,先不要怀疑算法,去检查AESjiami.m返回的矩阵形状。常见情况是函数返回4x4的列优先矩阵,而你把它当成了1x16行向量。此时需要在isequal前加一次reshape(cipher, 1, 16),并确认数据布局是列优先还是行优先。另一个容易被忽视的点是uint8double的混用:如果密钥扩展或S盒查表中出现了double类型的0~255整数,MATLAB会自动转为double,然后再被赋值给uint8数组时可能发生精度丢失,尤其当涉及位运算时,结果会完全错乱。

4.2 CMAC正确性验证与常见错误排查

CMAC的验算可以借助NIST SP 800-38B的测试向量。例如密钥为2b7e151628aed2a6abf7158809cf4f3c,空消息的CMAC应为bb1d6929e95937287fa37d129b756746。对应代码验证如下:

key = uint8(sscanf('2b7e151628aed2a6abf7158809cf4f3c', '%2x')'); msg = uint8([]); tag = CMAC(key, msg); expectedTag = uint8(sscanf('bb1d6929e95937287fa37d129b756746', '%2x')'); isequal(tag, expectedTag)

如果这个测试没过,按下面表格检查,通常是3个地方出错。

错误现象可能原因检查方法
空消息的tag错误空消息被当作完整块处理确认CMAC.m中若消息为空则直接构造填充块并使用K2
非对齐消息的tag错误补位时把0x80写到了末尾而不是有效数据后打印填充后的最后一个块,应该是有效字节后紧跟0x80,再补0
所有消息的tag都错子密钥生成时bitshift128把最高位溢出回卷单独验证L = AES(0^128)后,K1是否等于L左移1位且无循环移位

调过这些坑后,再处理真实报文就有底气了。另外提醒一点,CMAC的输入消息长度单位是字节,不是位。如果你把消息按位处理,记得先转成字节数组,否则填充逻辑会全部失效。

4.3 字节序陷阱:MATLAB的位运算与C/C++的差异

如果你之前用C语言写AES,移植到MATLAB时最容易栽在字节序上。C语言的整型左移会保留字节内部位序,而MATLAB的数组处理通常以字节数组为单位,bitshift128.m如果使用bitshift函数对每个元素单独移位,就需要手动处理跨字节的进位。我见过一个实现是先用de2bi把整个128位变成二进制向量,移位后再拼回字节,虽然慢但思路清晰。如果你要做性能优化,考虑用位操作组合:

function out = bitshift128(in) % 将16字节转成1x128的逻辑数组,左移1位并丢到第128位 bits = zeros(1,128,'uint8'); for i = 1:16 bits((i-1)*8+1 : i*8) = bitget(in(i), 8:-1:1); end bits = circshift(bits, [0 -1]); % circshift会把最高位移到最低位,实际上应该使用索引截断 % 正确做法:bits = [bits(2:end), 0]; % 上面两行仅示意,真正实现要避免循环移位 end

这种按位实现虽然正确,但循环开销很大。如果想提速,可以用typecast把uint8数组转为uint64对高低两半分别移位,再手动处理跨边界进位。不过对于仿真用的CMAC,单次计算在毫秒量级,按位实现完全够用。

5. 把CMAC接到你的协议帧里:防重放与长度编码技巧

当你已经能正确计算AES-CMAC后,自然想把它用于实际数据保护。一个常见做法是在消息帧末尾附加16字节的CMAC标签,接收方对“消息+密钥”重新计算并比对。这里有个安全细节:裸的CMAC不抵抗重放攻击,攻击者可以截获一组合法的消息和标签,之后原样重发。因此在协议设计时,需要把消息序号或时间戳纳入CMAC计算范围,让重放报文无法通过认证。

以你手上的这套MATLAB代码为例,可以在调用CMAC前拼接一个8字节序号字段。为了保证字节序一致,我建议统一按大端编码序号:

function msgWithSeq = addSeq(msg, seq) seqBytes = uint8([bitand(bitshift(seq, -56), 255), ... bitand(bitshift(seq, -48), 255), ... bitand(bitshift(seq, -40), 255), ... bitand(bitshift(seq, -32), 255), ... bitand(bitshift(seq, -24), 255), ... bitand(bitshift(seq, -16), 255), ... bitand(bitshift(seq, -8), 255), ... bitand(seq, 255)]); msgWithSeq = [seqBytes, msg]; end

然后tag = CMAC(key, msgWithSeq),接收方也按相同顺序拼接。这样才能让CMAC同时提供完整性、真实性以及防重放能力。

还有个进阶技巧:为了避免消息长度变化导致弱密钥或边界歧义,很多工程实现会在CMAC计算前把所有长度信息也编码进去。比如先计算整个消息的字节长度,并把长度值作为前8字节参与认证。这样做即使消息尾部有可变的填充字段,也能保证不同消息不会因为尾部字节恰好对齐而被认成同一认证标签。你可以在CMAC.m入口处增加长度前置逻辑,而不是直接对原始消息计算。

最后分享一个调试技巧:不要直接比较整个tag,而是分别打印CMAC中间过程里的X值,与Pythoncryptography库或OpenSSL的cmac命令输出的逐块中间结果比对。特别是在修改了bitshift128GFchengfa128后,单独验证K1、K2是否符合NIST文档的预期,再进入主循环。这样能把问题定位控制在子密钥阶段,而不是在上层协议里排查半天。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询