FEATURED · 精选文章

MATLAB分组密码实战:SPN结构加密文本与图像并验证

发布时间 / 2026/9/16 14:10:29
来源 / 创域科博编辑部
栏目 / 资讯中心
MATLAB分组密码实战:SPN结构加密文本与图像并验证 简介面向文本与图像加密解密应用的新分组密码算法MATLAB代码包适合信息安全、计算机或电子信息工程等专业学生用于课程设计、期末大作业和毕业设计。代码采用参数化编程参数可方便调整注释明细附赠案例数据可直接运行便于快速验证算法效果并开展二次开发。资源共4个文件含两个M脚本程序分别对应文本加密与整体算法流程、一份PDF项目说明、一份README文档压缩包仅895KB结构紧凑兼顾代码阅读与原理理解。M脚本分段清晰便于定位加密轮函数、密钥扩展等关键模块PDF说明可辅助理解算法设计思路README提供快速运行指引。已有110人学习下载适合需要快速上手分组密码算法仿真、或在文本和图像加密场景中对照研究的初学者与进阶开发者。1. 文本和图像的加密解密最怕的不是算法复杂而是类型不统一。文本是 UTF-8 字节流图像是 uint8 像素矩阵两者在 MATLAB 里都能抽象成 0~255 的分组字节新分组密码算法要做的事就是把这两类数据放进同一个 SPN 结构。很多初学者拿到类似标题的代码包后第一反应是找现成 AES 函数结果文本能跑通图像却因为矩阵维度、填充方式和显示格式乱了套。这篇文章会从分组密码的三个核心部件讲起给出一个 16 字节分组的自定义算法再分别落到文本和图像的加密解密代码最后用雪崩效应和直方图验证它是不是真的能用。适合正在学 MATLAB 图像处理、密码学课程设计或者想自己搭一套可改可调加密流程的人。2. 设计 16 字节分组的分组密码算法MATLAB 里的 S 盒、P 盒与轮密钥扩展2.1 为什么从 SPN 结构开始而不是直接改 AES分组密码的结构大致分成两种思路Feistel 网络和 SPN。Feistel 结构每轮只处理一半数据实现简单但扩散速度偏慢SPN 是“代换-置换网络”每一轮先做非线性替换再做线性置换最后加轮密钥。AES 就是典型的 SPN但 AES 的 S 盒、行移位、列混合和密钥扩展都做了大量数学优化直接改代码很容易把轮数改坏。新分组密码算法没必要从零发明一套安全堡垒而是要把可读性放在第一位。我一般会选定 16 字节分组、128 位主密钥轮数设为 16这样每一轮的输入输出都是1×16的 uint8 向量。替换层用查表法扩散层用固定索引置换轮密钥用主密钥循环移位加常数扰动生成。这样一来整个算法只有四类操作S 盒替换、字节置换、异或轮密钥、循环移位。MATLAB 里没有指针和位运算负担用向量化代码很容易跑通。这里要先把话说明白这个结构只用于教学和验证不考虑侧信道攻击、线性分析和差分分析。如果你想把它用在真实系统里至少要把 S 盒改成密码学安全的查表轮数提高到 20 轮以上并加上符合标准的密钥扩展。2.2 生成双射 S 盒并做字节替换S 盒可以理解成一个 256 项的一维查表。输入字节 0~255输出也是 0~255而且不能有重复输出否则解密时无法还原。最简单的生成方式是固定随机种子后使用randperm。function sbox generate_sbox(seed) % generate_sbox 生成一个 256 字节双射 S 盒 % 输出 sbox 是 1x256 uint8sbox(i1) 表示字节 i 被替换成哪个值 if nargin 1 seed 2024; end rng(seed); sbox uint8(randperm(256) - 1); end这段代码利用了randperm(256)生成 0~255 的全排列再转成 uint8。因为排列不会重复所以这个 S 盒天然可逆。MATLAB 下标从 1 开始所以查询时要把输入字节加 1function out sub_bytes(in_block, sbox) % in_block 是 1x16 uint8 明文分组 % out 是替换后的 1x16 uint8 out sbox(in_block 1); endS 盒的作用是制造非线性。如果没有这一步整个加密过程就是线性异或和置换的组合攻击者可以用线性方程组直接求解密钥。每一轮都做一次 16 字节查表可以让局部字节的变化快速扩散到全分组。2.3 可逆字节置换通过固定索引表实现扩散扩散层要让“一个字节改变”影响尽可能多的字节。SPN 里常用的办法是把 16 字节看成一个 4×4 矩阵然后做行移位和列混合MATLAB 里更直观的做法是直接维护一张 16 元素的位置映射表。function out permute_bytes(in_block, P) % P 是 0 基索引表长度必须为 16 % 例如 P(1)1 表示输出第 1 字节来自输入第几个字节 out in_block(P 1); end下面给出一组可以用的 P 表P uint8([15 11 3 8 0 12 2 5 10 1 14 7 6 13 4 9]);这张表不是乱写的它保证每个位置都被映射到新位置且是双射。解密时需要一个逆置换表用反向赋值生成function invP invert_perm(P) invP zeros(1, numel(P), uint8); invP(P 1) uint8(0:numel(P)-1); endinvert_perm的核心逻辑是如果正向映射说P(i) j那么逆映射就是invP(j) i。在 MATLAB 里写成invP(P 1) uint8(0:15)一行就能完成反查。置换本身不改变字节值只改变位置因此它不能提供安全性但能让 S 盒的非线性效果在下一轮被放大。2.4 轮密钥扩展16 轮子密钥怎么从主密钥里长出来轮密钥的目标是让每一轮使用不同的 16 字节子密钥。常见做法是主密钥循环左移再混入轮号和字节位置常数。function round_keys expand_key(master_key, rounds) % master_key 是 1x16 uint8 主密钥 % rounds 是加密轮数这里固定为 16 % round_keys 是 rounds x 16 uint8 矩阵 if nargin 2 rounds 16; end round_keys zeros(rounds, 16, uint8); rk master_key(:); % 统一成行向量 for r 1:rounds rk circshift(rk, [0 -1]); % 整体循环左移 1 字节 for j 1:16 rk(j) bitxor(rk(j), uint8(mod(r * 17 j * 13, 256))); end round_keys(r, :) rk; end end参数含义是这样circshift(rk, [0 -1])把向量向左移动一位第一字节跑到末尾避免多轮之间出现重复模式。r * 17 j * 13是轮数与位置的混合扰动mod(...,256)保证结果落在 uint8 范围。实际项目中建议改用 SHA-256 或自定义哈希做密钥扩展但作为演示代码这个版本足以验证分组密码的轮函数流程。参数取值作用分组长度16 字节每次加密一个明文分组轮数16制造足够多的扩散S 盒256 项双射非线性替换置换表16 元素线性扩散轮密钥16 × 16 字节每轮异或扰动3. 文本加密解密实现字符串转字节、PKCS7 填充与 CBC 分组模式3.1 文本必须经过填充否则最后一组长短不齐分组密码要求输入长度是 16 的倍数。中英文混排时UTF-8 编码后一个汉字占 3 字节一个英文字母占 1 字节直接取字符串长度不可靠。正确做法是先用unicode2native转成字节数组。plain_bytes unicode2native(text, UTF-8); % plain_bytes 是 1xN uint8N 可能不是 16 的倍数如果最后一组只有 5 字节剩余 11 字节怎么处理常用的是 PKCS7 填充缺几个字节就补几个几。例如缺 5 字节就补[5 5 5 5 5]。解密时读到最后一个字节的值就知道要删掉多少填充。function padded pad_pkcs7(vec, blocklen) r mod(numel(vec), blocklen); if r 0 padval blocklen; padded [vec, repmat(uint8(blocklen), 1, blocklen)]; else padval blocklen - r; padded [vec, repmat(uint8(padval), 1, padval)]; end end这里有个特别容易踩的坑即使原文长度刚好是 16 的倍数也必须补满一整块 PKCS7否则解密时无法区分“最后一字节恰好是 1”和“填充了一个 1”。所以代码里r 0时补了blocklen个blocklen。3.2 用 CBC 模式加密任意长度文本而不是直接分块ECB 模式每个分组独立加密相同的明文块会产生相同的密文块图像上会留下轮廓。文本短时看不出问题但为了后面图像复用直接写成 CBC 更省事。CBC 会让每个明文分组先与前一个密文分组异或再进入分组加密函数。function enc_data encrypt_text(text, key) sbox generate_sbox(2024); P uint8([15 11 3 8 0 12 2 5 10 1 14 7 6 13 4 9]); invP invert_perm(P); rk expand_key(key, 16); plain_bytes uint8(unicode2native(text, UTF-8)); plain_bytes pad_pkcs7(plain_bytes, 16); enc_data zeros(size(plain_bytes), uint8); prev zeros(1, 16, uint8); % CBC 初始向量全 0 for i 1:16:numel(plain_bytes) block plain_bytes(i:i15); xor_block bitxor(block, prev); enc_data(i:i15) encrypt_block(xor_block, rk, sbox, P); prev enc_data(i:i15); end endencrypt_block对应 2.2 到 2.4 的轮函数组合结构如下function enc encrypt_block(block, round_keys, sbox, P) x block(:); for r 1:size(round_keys, 1) x sub_bytes(x, sbox); % 非线性替换 x permute_bytes(x, P); % 线性扩散 x bitxor(x, round_keys(r,:));% 加轮密钥 end enc x; endCBC 模式的关键点是prev被不断更新为上一个密文分组。这个变量在加密端和解密端必须完全一致否则第 2 个分组之后全部解密错误。这里初始向量只用于演示正式工程里应该随机生成并单独保存。3.3 解密时逆序遍历轮函数并剥离 PKCS7 填充解密不能简单地把加密函数反过来调用因为替换和置换都要用逆表。先求逆 S 盒function inv_sbox invert_sbox(sbox) inv_sbox zeros(1, 256, uint8); inv_sbox(sbox 1) uint8(0:255); end分组解密函数的顺序与加密相反先逆轮密钥加法再逆置换再逆 S 盒。function dec decrypt_block(block, round_keys, inv_sbox, invP) x block(:); for r size(round_keys, 1):-1:1 x bitxor(x, round_keys(r, :)); % 去掉本轮子密钥 x permute_bytes(x, invP); % 置换逆运算 x inv_sbox(x 1); % 替换逆运算 end dec x; end注意inv_sbox(x 1)的写法x 是 uint8作为下标时必须转成 double。MATLAB 会自动完成但你可以显式写成inv_sbox(double(x) 1)来避免大矩阵下标警告。完整解密文本的流程是把密文按 16 字节分组CBC 逆运算时让每个密文分组先进入decrypt_block再与上一个密文分组异或最后读取末尾填充值并裁剪。function text decrypt_text(enc_data, key) sbox generate_sbox(2024); P uint8([15 11 3 8 0 12 2 5 10 1 14 7 6 13 4 9]); invP invert_perm(P); inv_sbox invert_sbox(sbox); rk expand_key(key, 16); plain_block zeros(size(enc_data), uint8); prev zeros(1, 16, uint8); for i 1:16:numel(enc_data) current_cipher enc_data(i:i15); dec_tmp decrypt_block(current_cipher, rk, inv_sbox, invP); plain_block(i:i15) bitxor(dec_tmp, prev); prev current_cipher; end padval plain_block(end); if padval 1 || padval 16 error(填充值非法密钥或密文可能不正确); end plain_bytes plain_block(1:end - padval); text native2unicode(plain_bytes, UTF-8); end这个解密函数把填充校验放在最后一步。如果密钥错误padval很可能不是 1~16 之间的合法值直接报错比等到纯文本乱码后再排查更容易定位问题。4. 图像加密解密的 MATLAB 实现像素分块、Arnold 置乱与无损还原4.1 图像转字节分组的两种做法先选对数据形状图像在 MATLAB 里是h×w×c的 uint8 矩阵灰度图c1RGB 图c3。转成字节流有两种常见路线第一种是直接用img(:)把矩阵拉平相当于按列顺序取出所有像素第二种是先做 Arnold 置乱让像素位置被打乱再进入分组加密。对于分组密码算法本身两种方式都能跑区别在于拉平顺序。img(:)的列优先顺序会把同一列的像素放在一起如果原图存在大面积相近颜色CBC 模式会减弱这种规律但 ECB 模式仍然可能保留横向纹理。所以图片加密我默认采用 CBC而不是 ECB。图像类型矩阵形状转换方式灰度图h×w uint8img(:)得到 h×w×1 像素流RGB 图h×w×3 uint8img(:)得到 h×w×3 像素流矩形图h≠w直接按字节流分组不强制置乱正方形图hw可选 Arnold 置乱后分组4.2 图像加密完整代码CBC 分组加零填充图像本身是二进制数据不需要像文本那样保存可读字符所以填充可以直接用 0 补位解密后裁剪到原始长度。实现时需要把原始字节数保存下来。function [enc_img, meta] image_encrypt(img, key) sbox generate_sbox(2024); P uint8([15 11 3 8 0 12 2 5 10 1 14 7 6 13 4 9]); rk expand_key(key, 16); bytes img(:); % h*w*c x1 uint8 orig_n numel(bytes); r mod(orig_n, 16); if r 0 bytes [bytes; zeros(16-r, 1, uint8)]; end nblocks numel(bytes) / 16; enc_bytes zeros(numel(bytes), 1, uint8); prev zeros(1, 16, uint8); for idx 1:nblocks block bytes((idx-1)*161 : idx*16); xor_block bitxor(block, prev); enc_bytes((idx-1)*161 : idx*16) encrypt_block(xor_block, rk, sbox, P); prev enc_bytes((idx-1)*161 : idx*16); end enc_img reshape(enc_bytes, size(img)); meta struct(orig_n, orig_n, rows, size(img,1), ... cols, size(img,2), channels, size(img,3)); end这里的逻辑说明bytes先变成列向量补零后按每 16 字节一组进入encrypt_block。每个密文分组会作为下一个分组的 XOR 输入所以相同像素不会产生相同密文。meta里保存原始字节数是为了解密后能精确切掉补的 0。4.3 先用 Arnold 置乱再做分组扩散能抵抗轮廓泄露分组加密负责扩散但如果不先置乱人物照片的肤色区域在密文里仍可能表现为大致连续的像素块。Arnold 置乱是一种经典的位置置乱只适用于正方形图像。它的正向公式是function out_img arnold_permute(img, iterations) [h, w, ~] size(img); if h ~ w error(Arnold 置乱需要正方形图像); end n h; out_img img; for it 1:iterations tmp zeros(size(img), uint8); for x 0:n-1 for y 0:n-1 nx mod(x y, n) 1; ny mod(x 2*y, n) 1; tmp(nx, ny, :) out_img(x1, y1, :); end end out_img tmp; end end循环嵌套在 MATLAB 里会比较慢但 Arnold 置乱本身只做整数坐标映射n 不大时完全能接受。逆变换公式是nx mod(2*x - y, n) 1; ny mod(-x y, n) 1;逆变换的代码只是把映射方向倒过来。实际使用时先调用arnold_permute(img, 10)再调用image_encrypt解密顺序必须相反先image_decrypt再做同样 10 次逆 Arnold 变换。4.4 图像解密与无损还原的实现要点图像解密需要meta里的原始尺寸信息和原始字节数。因为加密时补的是 0不是 PKCS7所以解密函数不能读最后一个像素当作填充长度而要直接用meta.orig_n截断。function [img, plain_img] image_decrypt(enc_img, key, meta) sbox generate_sbox(2024); P uint8([15 11 3 8 0 12 2 5 10 1 14 7 6 13 4 9]); invP invert_perm(P); inv_sbox invert_sbox(sbox); rk expand_key(key, 16); enc_bytes enc_img(:); dec_bytes zeros(numel(enc_bytes), 1, uint8); prev zeros(1, 16, uint8); nblocks numel(enc_bytes) / 16; for idx 1:nblocks current_cipher enc_bytes((idx-1)*161 : idx*16); dec_tmp decrypt_block(current_cipher, rk, inv_sbox, invP); dec_bytes((idx-1)*161 : idx*16) bitxor(dec_tmp, prev); prev current_cipher; end plain_bytes dec_bytes(1:meta.orig_n); plain_img reshape(plain_bytes, meta.rows, meta.cols, meta.channels); end解密时最容易搞错的是 CBC 的prev更新prev必须是上一个密文分组而不是上一个明文分组。很多人的解密图像只有第一块是正常的后面全是雪花噪点就是这里用错了。第二个易错点是用img(:)处理彩色图后reshape时矩阵维度必须和meta完全一致meta.channels就用来保证彩色图不会变成灰度图。5. 用这三条标准验证你的 MATLAB 加密算法雪崩效应、直方图与熵值5.1 单比特密钥变化测试雪崩效应一个合格的分组密码改变密钥的任意一位密文大约应有一半的比特发生变化。下面这段代码用两把只有最低位不同 的密钥加密同一张图然后计算像素差异率。key1 uint8(ones(1, 16)); key2 key1; key2(1) bitxor(key2(1), uint8(1)); % 密钥只差 1 个比特 [enc1, ~] image_encrypt(img, key1); [enc2, ~] image_encrypt(img, key2); diff_ratio sum(enc1(:) ~ enc2(:)) / numel(enc1);理想情况下diff_ratio应该在 0.45~0.55 之间。如果经过几轮加密后差异率仍低于 0.3说明你的置换表或轮数不够扩散没有完全铺开。出现这种情况时先增加轮数再把 S 盒换成非线性更强的查表。5.2 直方图与信息熵校验加密效果加密图像的直方图应该从原来的集中分布变成近似均匀分布。MATLAB 里用histcounts统计 0~255 的像素频率[counts, edges] histcounts(enc_img(:), 0:256); p counts / sum(counts); entropy_val -sum(p(p 0) .* log2(p(p 0)));信息熵越接近 8说明每个灰度值出现得越均匀。原始灰度图通常只有 5~7 的熵加密后一般能到 7.9 以上。如果熵值偏低优先检查你加密的到底是不是已经拉平后的像素流很多人只加密了图像文件名或者某个通道那直方图自然不均匀。5.3 轮数与填充方式的取舍这个自定分组算法在 16 轮时雪崩效应已经能到 0.5 附近但真实使用建议直接调到 24 轮。轮数增加后加密时间近似线性增长一张 512×512 的灰度图在 MATLAB 里大约需要多跑零点几秒换来的是更好的安全性。填充方式上文本必须用 PKCS7图像可以用零填充加元数据截断但不能反过来把 PKCS7 用在图像上因为图像像素值本身可能以任何数字结尾读到最后一个字节是 0 时无法确认是否需要填充校验。模式选择上放弃 ECB优先 CBC后期还可以改成 CTR 模式获得并行加密能力。验证算法时不要只检查明文是否能还原还要把解密失败、错误密钥、填充非法这三种情况都跑一遍确认错误提示足够明确。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻