简介:AES加密的MATLAB实现,面向需要在MATLAB环境中完成数据加密与解密验证的工程师、科研人员及密码学初学者。资源共20个m文件,压缩包仅18KB,代码轻量且模块化,涵盖AES的核心轮变换、S盒生成、密钥扩展、加解密主流程及演示脚本,便于逐模块理解与二次开发。已有2575人学习下载,适用于课程设计、算法仿真或安全通信实验。通过该资源可直接运行演示程序,观察AES从明文到密文的完整变换过程,也可结合描述中的工作模式与轮数说明,深入学习Rijndael算法在MATLAB中的具体实现细节。 写东西这件事,最怕的不是写不出来,而是写到一半发现方向错了。AES加密在MATLAB里实现,就是这么个容易跑偏的方向。网上搜"AES matlab实现",出来的结果大多是"用工具箱一行搞定"或者"调用某某函数完成加解密"。你复制粘贴跑通了,感觉万事大吉。等真到了要把这段逻辑移植到STM32、要和别的语言联调、或者要在C语言和MATLAB之间来回比对加密结果的时候,才发现工具箱那层封装把细节全藏起来了,你连S盒怎么排、填充用的什么规则、字节序对不对都说不清。
所以这篇博文不谈怎么调包,而是老老实实从零把AES-128在MATLAB里写一遍。我自己在做上位机和嵌入式通信时吃过不少亏,也把AES的代码在MATLAB、C、Python三种环境之间来回折腾过,这里面的坑我还算有发言权。文章会给出能直接跑的代码,每个关键步骤都配了为什么这么写的解释,末了还有一份问题排查清单。适合三种人看:正在做密码学课程设计的学生、要用MATLAB做原型验证然后转C落地的嵌入式开发者,以及纯粹想把AES原理吃透的爱好者。
1. 为什么我建议你手写一遍AES,而不是直接调工具箱
1.1 工具箱省了事,也藏了事
MATLAB里确实有封装好的AES实现。某些工具箱或者第三方库里面,几行代码就能完成加密:
cipher = aes_encrypt(plain, key); % 某些库提供的现成函数用起来是真省事,但你有没有想过这几个问题:它默认用ECB还是CBC?明文不足16字节时是补零还是PKCS7?密钥和明文是行优先还是列优先?如果这些答案你都要翻一遍文档甚至看源码才知道,那就说明这个黑盒对你来说并不可控。我做STM32和上位机联调的时候,最怕听到的一句话就是"两边都用AES,怎么就对不上"。两边都是黑盒,字节序、填充规则、初始向量的处理方式但凡有一个不一样,出来的密文就完全不同。
自己写一遍AES,最大的价值不是写出一份"能跑的代码",而是把每一轮变换变成你能调试、能打印、能逐步核对的东西。换个语言、换个平台,代码逻辑直接对照着翻译过去就行,不用猜。
1.2 AES到底在算什么,这里用大白话讲一遍
AES是个分组加密算法。分组长度固定128比特,也就是16个字节。密钥有128、192、256比特三种,分别对应10轮、12轮、14轮迭代。本文只讲AES-128。
每一轮加密就做四件事:先用一个替换表(S盒)把16个字节挨个换成别的值,这步叫SubBytes;然后把4x4的状态矩阵按行做循环移位,叫ShiftRows;接着对每一列做矩阵乘法,让一个字节的变化扩散到整列,叫MixColumns;最后把这一轮的子密钥和状态矩阵做异或,叫AddRoundKey。第1到第9轮这四个操作齐上阵,最后一轮去掉MixColumns。
解密就是完全反过来,用逆S盒、逆行移位、逆列混淆,子密钥按反方向用。整个过程像个非常规整的流水线,每一步都是固定数学变换,所以特别适合用MATLAB来写——MATLAB最擅长的就是矩阵运算,这些变换用矩阵下标和位操作能写得很紧凑。
2. 动手前的准备:格式、填充、工具箱
2.1 需要的工具箱和版本
我的实现只用MATLAB基础函数,不需要额外工具箱。列一个清单:uint8、bitxor、bitshift、reshape、hex2dec。就这么几个。我测试时用的是R2021b,但这些函数在近十年的大多数版本里都有。
有一个版本相关的坑必须提醒:MATLAB对整数类型的位移和算术运算,老版本和新版本的行为不完全一致。尤其是uint8类型做加法时会饱和而不是回绕,这个细节直接导致S盒查询容易出错。比如uint8(255) + 1在MATLAB里的结果是255,不是256,因为整数算术默认饱和。所以在写查表代码时,我要么先把uint8转成double再加1,要么用double(state(:)) + 1,否则S盒最后一个字节永远查不到。
2.2 明文格式:先统一成uint8数组
AES只认字节,所以输入什么格式直接决定你第一行代码怎么写。我的建议非常粗暴:所有明文、密钥、密文,一律用uint8数组表示。如果输入是字符串,用uint8(str)转成ASCII;如果是十六进制字符串,用hex2dec进行两两转换;如果是文件,用fread读成uint8数组。这套约定看起来简单,但能省掉一半排错时间。
还有分组填充问题。AES每16字节一组,明文长度不一定是16的倍数,所以要填充。标准做法是PKCS7:缺几个字节,就补几个值为缺少数目的字节。比如明文12字节,缺4字节,就在末尾补四个0x04。如果明文恰好16字节,仍然要额外补16个0x10,因为解密端需要靠最后一个字节的值来判断"哪里是填充,哪里是真实数据"。
注意:填充规则必须和通信对方确认。很多嵌入式设备默认用ZeroPadding,而OpenSSL默认PKCS7,混着用就会在解密最后一块时出现乱码。
3. 在MATLAB里一步步把AES写出来
3.1 第一步,先把S盒和逆S盒搞定
AES的S盒是一个256字节的查找表,内容由FIPS-197标准给出。直接手动输入256个数字很容易打错,我推荐用十六进制字符串的方式,一次性贴进来,然后让MATLAB自己转:
sbox_hex = ['637c777bf26b6fc53001672bfed7ab76' ... 'ca82c97dfa5947f0add4a2af9ca472c0' ... 'b7fd9326363ff7cc34a5e5f171d83115' ... '04c723c31896059a071280e2eb27b275' ... '09832c1a1b6e5aa0523bd6b329e32f84' ... '53d100ed20fcb15b6acbbe394a4c58cf' ... 'd0efaafb434d338545f9027f503c9fa8' ... '51a3408f929d38f5bcb6da2110fff3d2' ... 'cd0c13ec5f974417c4a77e3d645d1973' ... '60814fdc222a908846eeb814de5e0bdb' ... 'e0323a0a4906245cc2d3ac629195e479' ... 'e7c8376d8dd54ea96c56f4ea657aae08' ... 'ba78252e1ca6b4c6e8dd741f4bbd8b8a' ... '703eb5664803f60e613557b986c11d9e' ... 'e1f8981169d98e949b1e87e9ce5528df' ... '8ca1890dbfe6426841992d0fb054bb16']; Sbox = uint8(hex2dec(reshape(sbox_hex, 2, []).'))';逆S盒也是标准表,同样用字符串导入:
inv_sbox_hex = ['52096ad53036a538bf40a39e81f3d7fb' ... '7ce339829b2fff87348e4344c4dee9cb' ... '547b9432a6c2233dee4c950b42fac34e' ... '082ea16628d924b2765ba2496d8bd125' ... '72f8f66486689816d4a45ccc5d65b692' ... '6c704850fdedb9da5e154657a78d9d84' ... '90d8ab008cbcd30af7e45805b8b34506' ... 'd02c1e8fca3f0f02c1afbd0301138a6b' ... '3a9111414f67dcea97f2cfcef0b4e673' ... '96ac7422e7ad3585e2f937e81c75df6e' ... '47f11a711d29c5896fb7620eaa18be1b' ... 'fc563e4bc6d279209adbc0fe78cd5af4' ... '1fdda8338807c731b11210592780ec5f' ... '60517fa919b54a0d2de57a9f93c99cef' ... 'a0e03b4dae2af5b0c8ebbb3c83539961' ... '172b047eba77d626e169146355210c7d']; InvSbox = uint8(hex2dec(reshape(inv_sbox_hex, 2, []).'))';注意,MATLAB的索引从1开始,而字节值从0开始。所以查表时一定要加1。但这里就有前面说的uint8饱和陷阱,所以查表前先转成double,稳妥。
3.2 SubBytes和ShiftRows:矩阵下标就是好使
字节代换的代码非常短:
function state = subBytes(state, sbox) state = sbox(double(state(:)) + 1); state = reshape(state, 4, 4); endstate(:)把4x4矩阵展开成列向量,查表后再reshape回4x4。这里展开方式是列优先,和AES标准按列填充状态矩阵的方式正好一致。
ShiftRows的规则是:第0行不动,第1行左移1字节,第2行左移2字节,第3行左移3字节。用矩阵索引可以写得很优雅:
function state = shiftRows(state) state(2,:) = state(2, [2 3 4 1]); state(3,:) = state(3, [3 4 1 2]); state(4,:) = state(4, [4 1 2 3]); end function state = invShiftRows(state) state(2,:) = state(2, [4 1 2 3]); state(3,:) = state(3, [3 4 1 2]); state(4,:) = state(4, [2 3 4 1]); end这里有个很容易晕的点:AES官方文档里用的是行优先的视觉表示,但状态矩阵在内存里是按列填充的。只要你在MATLAB里统一用"列代表四列状态、行代表四行状态"的方式,ShiftRows就很直观。
3.3 MixColumns:AES里最容易写错的地方
列混淆是在有限域GF(2^8)上做矩阵乘法,不是普通的整数乘法。很多新手在这里直接用了a*2,a*3,结果加密结果跟标准答案永远差一截。GF(2^8)上的乘法要先做多项式乘法,再对不可约多项式0x11B取模。落到代码里,可以写一个通用的GF乘法函数:
function out = gmul(a, b) p = uint8(0); for k = 0:7 if bitand(b, uint8(1)) p = bitxor(p, a); end hi = bitand(a, uint8(0x80)); a = bitshift(a, 1); if hi a = bitxor(a, uint8(0x1B)); end b = bitshift(b, -1); end out = p; end这段代码的思路就是不断把a左移、把b右移,b当前位为1就累加a。左移后最高位溢出时,异或0x1B完成取模。这其实是AES里最核心也最反直觉的一步。
有了gmul,MixColumns就是按列做4次运算。AES里乘数固定为1、2、3,解密时固定为9、11、13、14。可直接套用公式:
function state = mixColumns(state) for c = 1:4 a0 = state(1,c); a1 = state(2,c); a2 = state(3,c); a3 = state(4,c); state(1,c) = bitxor(bitxor(gmul(uint8(2), a0), gmul(uint8(3), a1)), bitxor(a2, a3)); state(2,c) = bitxor(bitxor(a0, gmul(uint8(2), a1)), bitxor(gmul(uint8(3), a2), a3)); state(3,c) = bitxor(bitxor(a0, a1), bitxor(gmul(uint8(2), a2), gmul(uint8(3), a3))); state(4,c) = bitxor(bitxor(gmul(uint8(3), a0), a1), bitxor(a2, gmul(uint8(2), a3))); end end对于逆MixColumns,只需把系数换成9、11、13、14,结构完全一样。
3.4 密钥扩展:把16字节密钥变成44个32位字
AES-128需要11轮子密钥,总共44个32位字。密钥扩展的核心逻辑是:每4个字一组,当前字由前一个字和前面隔4个位置的字异或得到;当索引是4的倍数时,先循环左移一个字节,再逐字节过S盒,然后第一个字节异或一个轮常量。
function w = keyExpansion(key, sbox) Nk = 4; Nr = 10; Nb = 4; Rcon = uint8([1 2 4 8 16 32 64 128 27 54]); w = zeros(4, Nb*(Nr+1), 'uint8'); w(:, 1:Nk) = reshape(key, 4, Nk); for i = Nk : Nb*(Nr+1)-1 temp = w(:, i); if mod(i, Nk) == 0 temp = temp([2 3 4 1]); % RotWord temp = sbox(double(temp) + 1); % SubWord temp(1) = bitxor(temp(1), Rcon(i/Nk)); % 轮常量异或 end w(:, i+1) = bitxor(w(:, i-Nk+1), temp); end end这里下标看着有点绕,因为FIPS文档从0开始编号,MATLAB从1开始。实际对应关系是:文档里的w[i],就是代码里的w(:, i+1)。只要记住这个偏移,其他逻辑都能直接对着标准抄。
3.5 总装:AES加密解密完整流程
轮密钥加就是异或,一行代码搞定:
function state = addRoundKey(state, w) state = bitxor(state, w); end加密一块16字节数据,按标准流程走:
function cipher = aesEncryptBlock(plain16, key, sbox) w = keyExpansion(key, sbox); state = reshape(uint8(plain16), 4, 4); state = addRoundKey(state, w(:, 1:4)); for round = 1:9 state = subBytes(state, sbox); state = shiftRows(state); state = mixColumns(state); state = addRoundKey(state, w(:, (1:4) + 4*round)); end state = subBytes(state, sbox); state = shiftRows(state); state = addRoundKey(state, w(:, 41:44)); cipher = state(:); end解密就是把操作逆过来,注意轮密钥使用的顺序和加密相反:
function plain = aesDecryptBlock(cipher16, key, sbox, invsbox) w = keyExpansion(key, sbox); state = reshape(uint8(cipher16), 4, 4); state = addRoundKey(state, w(:, 41:44)); for round = 9:-1:1 state = invShiftRows(state); state = subBytes(state, invsbox); state = addRoundKey(state, w(:, (1:4) + 4*round)); state = invMixColumns(state); end state = invShiftRows(state); state = subBytes(state, invsbox); state = addRoundKey(state, w(:, 1:4)); plain = state(:); end写完马上用官方测试向量验证。FIPS-197附录B给了一组标准测试数据:密钥是000102030405060708090a0b0c0d0e0f,明文是00112233445566778899aabbccddeeff,加密结果应该是69c4e0d86a7b0430d8cdb78070b4c55a。我把验证脚本也贴出来:
key = uint8(hex2dec(reshape('000102030405060708090a0b0c0d0e0f', 2, []).'))'; plain = uint8(hex2dec(reshape('00112233445566778899aabbccddeeff', 2, []).'))'; cipher = aesEncryptBlock(plain, key, Sbox); disp(dec2hex(cipher(:).')); % 期望输出:69 C4 E0 D8 6A 7B 04 30 D8 CD B7 80 70 B4 C5 5A back = aesDecryptBlock(cipher, key, Sbox, InvSbox); assert(isequal(back, plain));如果这个测试能过,你的AES核心逻辑就基本没问题了。
3.6 从单块到多块:补上ECB和CBC模式
上面的函数只处理16字节一块。实际加密一长串数据,需要按分组模式来。ECB模式就是把每块独立加密,简单但会泄露明文模式,真实系统不建议用。CBC模式则让每个明文块先和上一个密文块异或,再加密,所以相同的明文块会产生不同的密文块,安全性高得多。
CBC模式下,解密方向也一样,第一块需要一个初始向量IV。处理多块明文,还牵扯到PKCS7填充,我补一个完整的CBC加密脚本:
function cipher = aesEncryptCBC(plain, key, iv, sbox) plain = uint8(plain(:)); n = numel(plain); padLen = 16 - mod(n, 16); if padLen == 0 padLen = 16; end padded = [plain; repmat(uint8(padLen), padLen, 1)]; nBlocks = numel(padded) / 16; cipher = zeros(size(padded), 'uint8'); prev = uint8(iv(:)); for i = 1:nBlocks block = padded((i-1)*16+1 : i*16); x = bitxor(block, prev); enc = aesEncryptBlock(x, key, sbox); cipher((i-1)*16+1 : i*16) = enc; prev = enc; end end注意,CBC加密必须在明文端做填充,解密后要去掉填充,这一步别漏。
4. 实际应用:加密文件,以及往硬件平台移植
4.1 用MATLAB加密一个本地文件
加密文件的核心就是把文件内容读成uint8数组,然后走CBC加密,最后写出去。下面是一个完整示例:
fid = fopen('secret.dat', 'rb'); data = fread(fid, Inf, '*uint8'); fclose(fid); key = uint8(hex2dec(reshape('000102030405060708090a0b0c0d0e0f', 2, []).'))'; iv = uint8(hex2dec(reshape('101112131415161718191a1b1c1d1e1f', 2, []).'))'; cipherData = aesEncryptCBC(data, key, iv, Sbox); fid = fopen('secret.enc', 'wb'); fwrite(fid, cipherData, 'uint8'); fclose(fid);解密的时候反过来读密文文件,用CBC解密函数处理,再按原格式写回。这里要再次提醒:IV不能写死在生产系统里。IV的作用是让同一段明文配合不同IV产生不同密文,一般随机生成并和密文一起传输。实际系统里,IV或盐放在前端还是后端、由谁生成、怎么存,是个需要设计的架构问题,MATLAB原型阶段也得留出这个参数传递的接口,别写死在函数里。
4.2 和STM32、ESP32等硬件设备的联调思路
我当初做这个MATLAB实现的直接原因,就是要在STM32上做AES加密,跟上位机通信。很多嵌入式芯片有硬件AES加速器,比如STM32的AES外设,直接配置寄存器就能用。上位机用MATLAB做数据分析和界面,双方要对密文格式达成一致,至少下面几项必须统一:
- 密钥长度:128、192还是256,两边必须一致;
- 分组模式:ECB、CBC还是CTR;
- 填充方案:PKCS7还是ZeroPadding;
- IV和盐由哪一端生成、怎么传;
- 数据字节序:大端还是小端,尤其是在把32位整数转换成字节数组时。
我在联调时最常遇到的问题就是字节序。MATLAB里typecast转换完的整数字节序,和STM32里默认的小端序经常对不上。建议在协议设计阶段就明确:网络传的字节流统一大端,芯片内部转换时再转成小端。MATLAB这边用typecast(..., 'uint8')配合swapbytes,能省不少事。
5. 常见问题与排查技巧实录
5.1 加密解密没报错,但结果就是不对
这是最让人抓狂的情况。我列一个速查表,基本覆盖了我踩过的所有坑:
| 症状 | 常见原因 | 排查方向 |
|---|---|---|
| 加密结果和标准测试向量不符 | S盒字符串抄错,或查表时忘了加1 | 先跑FIPS-197标准向量,确认核心轮函数正确 |
| 解密出来前面正常,最后一小块乱码 | 填充规则不一致 | 确认PKCS7填充的加解密两侧实现一致 |
| 不同语言加密结果无法互通 | 字节序问题 | 统一字节序约定,用同一个标准向量做交叉测试 |
| CBC模式多块数据解不出来 | IV没有对齐 | 检查CBC模式里IV是"只参与第一块"还是被错误复用到了后续块 |
bitxor报维度不一致 | 分组块长度不是16字节 | 检查填充逻辑是否对块边界做了正确处理 |
uint8查表越界或结果古怪 | uint8算术饱和导致+1没生效 | 查表前统一转double:sbox(double(state(:)) + 1) |
5.2 性能问题:MATLAB跑AES太慢怎么办
MATLAB本身是解释型语言,逐轮逐字节做位运算,性能肯定比不了C语言。如果你只是做原型验证,这个速度完全够用;如果要对几百兆文件做加解密,那就不太现实了。我建议两条路:
第一,优化热点函数。在MATLAB里跑性能分析器,看看gmul是不是最耗时的函数。AES里乘数固定,可以用预先算好的乘法表替代通用gmul,比如把乘2和乘3的结果各做成一个256字节的查表数组,速度能提升不少。第二,如果数据量大,就把MATLAB当作验证工具,算法确认无误后,用C、C++或者芯片的硬件加速器来做实际加解密。MATLAB写出来的代码逻辑清晰,对照翻译成C语言并不难,这也是我推荐手写一遍的另一个原因。
5.3 移植到C语言时的三个提醒
从MATLAB移植到C语言,有三个地方特别容易栽跟头。第一个是数据类型,MATLAB的uint8在C语言里对应unsigned char,但bitxor在C里就是^,bitshift就是<<和>>,语法简单,但C语言没有默认的溢出保护,左移时要手动处理最高位。第二个是数组索引,MATLAB从1开始,C从0开始,查表时一个+1一个-1,出错的概率很高,建议在代码边上写清对应关系。第三个是内存布局,MATLAB的矩阵按列优先存储,C语言的二维数组按行优先,状态矩阵在两种语言里如果定义方式不同,很容易在行列转置上栽跟头。
我个人在实际操作中的体会是,AES这种算法,网上从来不缺能跑的代码,缺的是"能让自己改明白"的代码。花一个下午在MATLAB里把这个流程完整写一遍,远比收藏十个现成脚本有用。写完以后再用FIPS-197的标准向量卡一下,但凡能过,后面换什么语言、对接什么设备,心里都有底。最后再分享一个小技巧:调试时把每一轮的state都disp出来,跟标准中间值比对,这样能快速定位是S盒错了、行移位错了还是密钥扩展错了,比盯着最终密文瞎猜效率高得多。
本文还有配套的精品资源,点击获取