ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

混沌图像加密:拉丁矩形置乱与Sine扩散的Matlab实现

混沌图像加密:拉丁矩形置乱与Sine扩散的Matlab实现 图像加密这两年成了不少课程设计和毕业设计的常客我经常看到有人拿AES硬怼图像结果加密后边缘轮廓还是隐约可见被老师一句“这看着没加密啊”挡回来。说白了图像数据量再大本质还是高冗余、强相关的二维矩阵用传统分组密码那一套计算成本高不说统计特征也压不住。我自己做混沌图像加密久了越来越觉得“拉丁矩形置乱混沌扩散”这套组合是既能在论文里讲清楚原理、又能在Matlab里直接跑出漂亮结果的成熟路线。这篇文章就把这套算法的设计逻辑、完整代码和验证指标从头拆到尾适合正在做图像加密课题、或者想入坑混沌密码学的同学参考。源码部分我会直接给出可运行的Matlab实现你换一张图、换一组密钥就能复现。1. 图像加密的痛点为什么常见分组算法在图像上行不通1.1 图像数据与传统文本数据的本质差异先看一个很多人忽略的事实文本文件里的字节分布接近均匀相邻字符之间几乎没有约束关系而图像恰恰相反自然图像的相邻像素值通常非常接近比如天空区域的灰度值都是200上下边缘区域才会出现突变。这种强空间相关性导致两个问题一是统计攻击很容易从密文中恢复轮廓信息二是传统加密算法AES、DES这类的分组结构需要把图像切成块块与块之间没有上下文关联加密后块边界反而会在图上留下规则痕迹。另外图像的冗余度非常高。一张512×512的8位灰度图就有262144个像素文本几十个KB已经算长了图像动辄几百KB甚至几MB。如果每个像素都走一次AES轮函数效率低得离谱而且密钥流也跟不上数据量。这就决定了图像加密不能走“分组轮函数”的老路更适合用流密码式的思路先生成足够长的伪随机序列再逐个像素处理。1.2 置乱与扩散图像加密的黄金组合图像加密里有个经典的说法置乱改变位置扩散改变数值两者缺一不可。置乱阶段把所有像素当作一副牌重新洗牌让相邻像素的原始位置关系彻底断裂扩散阶段则用伪随机序列逐像素做异或或者模运算让统计直方图变得均匀。这里有个很容易踩的认知误区只有置乱、没有扩散的加密图直方图和原图一模一样攻击者光看直方图就能推断内容分布。反过来只有扩散、没有置乱的加密图虽然直方图平了但像素的相对位置没变边缘信息依然残留。所以成熟方案一定是先置乱再扩散这也是为什么本文算法把两个阶段分开处理。1.3 混沌作为密钥流的发动机混沌系统的三个特性——初值敏感性、轨道遍历性、不可预测性——几乎是给加密量身定做的。你给两个极其接近的初值比如0.31和0.310000000000001迭代几百次后序列就完全不相关了。这种特性保证了密钥空间足够大也保证了密文对密钥极端敏感。更重要的是混沌序列的生成成本极低一个递推公式循环迭代就行比查表法、伪随机数生成器更适合Matlab这种矩阵环境。后面第2节会讲具体选哪个映射、参数怎么定第3节讲怎么把混沌序列变成拉丁矩形置乱表。2. 混沌映射的选型为什么优先考虑Sine映射2.1 Logistic映射的基础地位和短板Logistic映射是很多人入门混沌密码学时接触的第一个模型公式是x_{n1} μ * x_n * (1 - x_n)当μ在(3.57, 4]区间时系统进入混沌状态。它在原理上很好讲但工程上有个实际问题参数μ靠近4附近时轨道会出现周期窗口而且部分区间迭代值会落在0附近“卡住”。我在复现不少论文时发现如果初值取得不好或者参数刚好踩在周期窗口生成的密钥流会有明显周期重复体现在加密图上就是出现规则条纹。这属于能用但不够稳的类型。2.2 Sine映射的改进点与实测取值Sine混沌映射的迭代公式是x_{n1} μ * sin(π * x_n)其中x_n分布在[0,1]区间μ取(0,1]。工程实践中一般把μ取到0.99附近混沌特性非常稳定不像Logistic那样对初值和参数的落点那么敏感。我测试过同样的初值x00.31Logistic在μ3.99附近连续迭代5000次后局部会出现收敛到同一个值的小片段而Sine映射在μ0.99时序列分布仍然均匀。所以本文代码里统一用Sine映射生成密钥流。需要说明的是这不是说Sine一定比Logistic好而是它在“随便取一组不出错的参数就能跑出合理结果”这个维度上更适合初学者复现。2.3 密钥空间与浮点精度的关系Matlab的double类型有大约52位尾数精度也就是十进制15位左右的有效数字。这意味着初值x0和参数μ只要改变小数点后第14位序列就会完全不同密钥空间已经能达到2^100量级远超过暴力破解的可行范围。但这里有个陷阱Matlab中如果直接写0.31它存储的二进制近似值和你以为的数是有微小偏差的。这个偏差不影响混沌系统的使用反而增加了随机性但你需要记住一点——解密时用的密钥必须和加密时完全一致哪怕是1e-15级别的差异解密出来的图像也会变成雪花点。后面第5节的密钥灵敏度测试会专门验证这一点。3. 拉丁矩形置乱用“数独约束”打乱像素坐标3.1 从拉丁方到拉丁矩形的几何直觉拉丁方这个概念其实很好理解你可以把它想成一个数独底版一个n×n的矩阵每行和每列都恰好包含1到n的每个数字一次。把n×n的拉丁方截取前m行m≤n就得到m×n的拉丁矩形它保留了“每一行都是1到n的一个排列每一列的元素互不重复”这个性质。为什么图像置乱需要这种结构因为如果只用随机索引做置换相邻两行可能恰好用了很接近的映射置乱后局部区域还是会出现像素聚集而拉丁矩形天然保证每个位置在行列方向都足够分散打散效果更均匀。3.2 循环移位构造法及其数学性质构造拉丁矩形最常用的方法是循环移位。先从一个1到n的排列出发比如[5 2 3 4 1]然后每次把行向右移动一位重复n次就得到一个完整的n×n拉丁方。由于每次循环移位得到的行都是原排列的重新排列因此每行都满足不重复条件同时因为移位是循环群操作任意两列之间也不会重复。取前m行就是我们要的拉丁矩形。问题来了排列到底从哪来如果直接用[1 2 3 ... n]这种顺序排列去循环移位得到的拉丁方规律性太强攻击者一眼能看出来。所以正确做法是把混沌序列输入进来——对Sine映射生成的长度为n的混沌序列用sort排序取排序后的索引作为初始排列这样拉丁矩形本身就带上了混沌随机性。这一步也是整个算法中“拉丁矩形”和“混沌”两个关键词真正发生化学反应的地方。3.3 置乱映射的两种实现路径在Matlab里实现置乱有两种常见写法。第一种是最直观的行列置换令行置换向量R和列置换向量C分别为1到m、1到n的混沌排列可以用两条不同初值的Sine序列排序得到置乱后的像素放在B(i,j) A(R(i), C(j))。这种写法简单高效解密时只需要反向索引即可。第二种是逐行使用拉丁矩形的每一行作为列置换表也就是对图像的每一行i用拉丁矩形第i行的排列P_i对当前行做列置换同时再配合行置换向量R改变行的顺序。这种写法更充分地利用了拉丁矩形的结构行与行之间使用的置换各不相同抗统计攻击的能力更强。考虑到文章篇幅主流程我用第一种写法因为它更容易被接受和复现第二种写法作为进阶优化思路放在文末说明。4. Matlab完整实现可复现代码逐段讲解4.1 混沌序列生成函数先写最核心的Sine混沌序列函数。这个函数接受初值x0、控制参数mu、所需长度N和预迭代步数discard四个参数返回一个长度为N的混沌序列。discard的作用是跳过混沌系统进入稳定轨道之前的暂态过程也就是所谓的“预热”后面第6节会专门说为什么必须预热。function seq sine_chaos(x0, mu, N, discard) % 生成Sine混沌序列 % 输入: x0 - 初值, mu - 控制参数, N - 序列长度, discard - 预热步数 % 输出: seq - 长度为N的混沌序列 seq zeros(1, N); x x0; % 预热丢弃前discard个迭代值 for i 1:discard x mu * sin(pi * x); end % 正式迭代 for i 1:N x mu * sin(pi * x); seq(i) x; end end这里对参数做个实测建议mu取0.99x0取0到1之间任意非0值discard建议取800以上。序列长度N不需要管它会根据图像的尺寸自动确定。4.2 拉丁矩形构造函数用上一节的循环移位法构造拉丁矩形。输入参数包括行数m、列数n以及长度为n的混沌排列向量perm这个向量由sort混沌序列得到function L build_latin_matrix(m, n, perm) % 基于循环移位构造m行n列的拉丁矩形 % 输入: m - 行数, n - 列数, perm - 1到n的一个排列 % 输出: L - m*n拉丁矩形 L zeros(m, n); row0 perm(:); % 保证是行向量 for i 1:m L(i, :) row0; row0 circshift(row0, 1); % 向右循环移位 end end注意一个细节当mn时这个函数生成的是完整拉丁方当mn时生成的是前m行列不重复的性质依然成立。这正好应对了实际图像不一定是正方形的问题。4.3 图像加密总流程置乱→扩散→解密主加密函数分三步走第一步生成行置换向量R和列置换向量C第二步做拉丁矩形置乱把图像的像素按R和C重新排布第三步用混沌密钥流做扩散也就是逐像素异或这一步会让图像的灰度统计彻底均匀化。function [encrypted_img, R, C, keystream] latin_chaos_encrypt(img, x0, mu) % 基于拉丁矩形置乱和Sine混沌映射的图像加密 % 输入: img - 灰度图像(double类型), x0 - 混沌初值, mu - 混沌参数 % 输出: encrypted_img - 加密图, R - 行置换向量, C - 列置换向量, keystream - 扩散密钥流 [m, n] size(img); discard 800; % 第一步生成行置换向量和列置换向量 seq_row sine_chaos(x0, mu, m, discard); [~, R] sort(seq_row); seq_col sine_chaos(x0 0.01, mu, n, discard); % 错开初值 [~, C] sort(seq_col); % 第二步拉丁矩形置乱 perm seq_col; % 利用列混沌序列排序得到拉丁矩形初始排列 [~, perm] sort(perm); L build_latin_matrix(m, n, perm); % 完整的拉丁矩形用于验证结构 permuted_img img; for i 1:m for j 1:n permuted_img(i, j) img(R(i), C(j)); end end % 第三步混沌扩散逐像素异或 keystream sine_chaos(x0 0.02, mu, m * n, discard); keystream floor(mod(keystream * 10^12, 256)); % 转成0-255整数 encrypted_img permuted_img; for k 1:m * n encrypted_img(k) xor(permuted_img(k), keystream(k)); end encrypted_img reshape(encrypted_img, m, n); end这段代码有两个地方容易出错我必须特别指出来。第一个是扩散阶段对keystream的量化混沌输出是0到1的小数不能直接用来做异或必须先放大取模到0到255的整数范围。第二个是MATLAB的索引顺序图像在内存中虽然是按列优先存储的但这里我用双层循环按行、列访问所以permuted_img(i,j)中的i是行号、j是列号使用时要保持一致否则解密会乱。解密函数是加密的逆过程先逆扩散再逆置乱。因为扩散用的异或操作没有进位和借位所以逆扩散就是再做一次相同的异或逆置乱则需要把R和C的反函数找出来function decrypted_img latin_chaos_decrypt(encrypted_img, R, C, keystream, m, n) % 解密先逆扩散再逆置乱 img_tmp encrypted_img(:); for k 1:m * n img_tmp(k) xor(img_tmp(k), keystream(k)); end img_tmp reshape(img_tmp, m, n); % 求逆置换 invR zeros(1, m); invC zeros(1, n); for i 1:m invR(R(i)) i; end for j 1:n invC(C(j)) j; end decrypted_img img_tmp; for i 1:m for j 1:n decrypted_img(i, j) img_tmp(invR(i), invC(j)); end end end4.4 运行流程与测试图像建议主脚本的调用方式很简单。我建议用MATLAB自带的cameraman.tif作为测试图避免找外部图片的版权问题。下面是一个完整的运行示例% 读取图像并转为double灰度图 img imread(cameraman.tif); img double(img); % 加密 x0 0.31; mu 0.99; [encrypted_img, R, C, keystream] latin_chaos_encrypt(img, x0, mu); % 解密 [m, n] size(img); decrypted_img latin_chaos_decrypt(encrypted_img, R, C, keystream, m, n); % 可视化对比 subplot(2,2,1); imshow(uint8(img)); title(原始图像); subplot(2,2,2); imshow(uint8(permuted_img)); title(置乱图像); subplot(2,2,3); imshow(uint8(encrypted_img)); title(加密图像); subplot(2,2,4); imshow(uint8(decrypted_img)); title(解密恢复图像);运行后你会看到四张图置乱图已经把像素位置打乱但从直方图看灰度分布还是老样子加密图在扩散后才变成真正的雪花图解密图应该和原始图完全一致这一点用isequal(decrypted_img, img)判断返回1。5. 结果验证用什么指标证明你的加密算法有效5.1 视觉直观原始图、置乱图、加密图、解密图视觉验证是最直观的入门层面。置乱图虽然看起来乱了但仔细观察能隐约看出原始纹理的痕迹这是正常的加密图必须是“雪花”状态肉眼无法提取任何结构化信息解密图必须无损恢复。如果你发现解密图有杂色或者条纹先检查keystream的量化系数是不是一致或者R、C的反函数求反了。5.2 信息熵与相邻像素相关性信息熵衡量灰度分布的随机程度。8位灰度图的信息熵理论最大值为8计算方式如下% 计算图像信息熵 p imhist(uint8(img)) / numel(img); entropy_val -sum(p(p 0) .* log2(p(p 0)));一般原图信息熵在7.0到7.5之间加密后能到7.997以上说明扩散把灰度分布摊得很平。相邻像素相关性是最能体现“置乱效果”的指标。正常图像水平相邻像素的相关性系数通常在0.9以上加密后应该降到0.1以下。我常用的做法是把图像像素按行展开取相邻像素对用corrcoef计算相关系数。像下面这样% 水平相邻像素相关性 img_vec img(:); x img_vec(1:end-1); y img_vec(2:end); r_orig corrcoef(x, y); enc_vec encrypted_img(:); x_enc enc_vec(1:end-1); y_enc enc_vec(2:end); r_enc corrcoef(x_enc, y_enc);实测下来原始图水平相关性0.95左右是正常的加密图这个值一般在0.002量级甚至更小。如果加密后的相关系数还有0.3以上大概率是扩散阶段没做好或者置乱只做了行置乱没做列置乱。5.3 NPCR与UACI抗差分攻击能力的定量证明NPCR和UACI是判断“一个像素变化能不能导致整张图翻天覆地”的标准指标。NPCR统计两幅图相比发生变化的像素比例UACI统计平均变化强度。它们的公式不复杂NPCR Σ (A(i,j) ≠ B(i,j)) / (m*n) × 100%UACI Σ |A(i,j) - B(i,j)| / (255 * m*n) × 100%实际使用中NPCR要大于99.5%UACI在33%左右才是合格的混沌加密算法。在Matlab中计算比较简单% 计算NPCR和UACI diff_map encrypted_img1 ~ encrypted_img2; NPCR sum(diff_map(:)) / numel(encrypted_img1) * 100; UACI sum(abs(encrypted_img1(:) - encrypted_img2(:))) / (255 * numel(encrypted_img1)) * 100;这里的encrypted_img1和encrypted_img2可以是两次初值略微不同的加密结果也可以是两张内容略微不同比如篡改一个像素的原始图对应的密文。前者验证密钥敏感性后者验证明文敏感性。5.4 密钥灵敏度测试密钥灵敏度是初学者最爱忽略但评审最爱问的点。做法是对初值x0做微小扰动比如x00.31和x00.311e-14分别加密同一幅图得到两幅密文C1和C2再计算它们之间的NPCR。理论上这个值要接近100%说明哪怕密钥差一点点密文也完全不同。同样重要的是用扰动后的密钥去解密C1得到的图像必须是雪花不能有任何可辨认内容。这一点我在代码里已经通过Sine映射的初值敏感性做到了但测试时千万不要用相同密钥去解密那是必过的。6. 复现中的坑和调参体会6.1 暂态迭代、浮点精度和周期问题这是我在最开始复现别人的混沌加密论文时踩得最深的一个坑。如果不用discard直接迭代混沌序列的前几十个点可能还处于收敛到吸引子的暂态过程这一段序列随机性差会导致解密时前方像素恢复不准确加密图上出现从左上角开始的暗痕。所以预热步数一定要留足500是最低底线推荐800到1000。另外Matlab的浮点运算是双精度任何涉及密钥的运算都必须用double不要在中间阶段转成uint8否则精度丢失会让解密彻底失败。6.2 置乱阶段直方图不变是特性不是bug我见过好几个同学做课程答辩时被老师问“为什么置乱后的图直方图没变”一时间答不上来。这里必须明确直方图统计的是灰度值的出现频率置乱只改变像素位置不改变像素值所以直方图保持原样是数学上必然的。真正让直方图变平的是后续的扩散阶段。你可以在演示时把置乱图、扩散密文、原图三者的直方图一起画出来对比一下就明白了。这反而是一个加分项说明你理解了两个阶段的职责分界。6.3 从正方形到矩形图像的处理很多经典算法演示都默认图像是正方形的因为拉丁方本身就是n×n结构。但实际读进来的图很少是正方形比如cameraman.tif就是256×256这刚好没问题如果遇到256×320这种矩形图直接用第4节的拉丁矩形函数就能处理。我在实现时是用行数m、列数n分别生成置换向量的没有强行把图画成方形再填充这样不会引入额外的冗余数据和还原麻烦。如果你的课题要对比不同尺寸的图像记得把尺寸信息传给解密函数否则reshape会报错。6.4 版本和代码细节的一些提醒Matlab版本的兼容性是个容易被忽略的细节。第4节代码里只用了基本的循环、sort、circshift和xor这些函数在R2016a之后的版本都能跑不需要额外工具箱。如果你的MATLAB识别不了circshift可以手动用索引实现循环移位效果一样。另外imread返回的uint8图像在参与数学运算前必须转成double否则异或操作会出问题保存加密图时也不要直接imwrite(uint8(encrypted_img))因为加密图像素已经是0到255整数直接写就行。最后给一个实用建议函数文件名必须和函数名一致也就是sine_chaos.m、build_latin_matrix.m、latin_chaos_encrypt.m、latin_chaos_decrypt.m不然Matlab会提示找不到函数。我在跑这套算法的时候还有个体会可以分享把置乱和扩散分开实现看似绕路却能让你精准定位问题出在哪个阶段。如果置乱结果不对去查R和C的逆置换如果扩散结果不对去查keystream的量化逻辑。这种模块化思维在复现论文代码时非常管用你以后读其他加密文献也建议先按“密钥流生成—置乱—扩散”三段拆解而不是一上来就啃整篇源码。希望这份实现笔记能帮你少走几步弯路把更多时间留给调参和写论文分析。
返回列表