图像加密这个方向,网上开源的代码不少,但多数停留在“能跑”的程度——贴几段代码、出一张Lena图加密前后对比,原理部分就含含糊糊过去了。这次分享的项目是我整理过的一套完整方案,标题很长:基于逻辑映射序列和线性反馈移位寄存器的状态序列生成混沌密钥序列,对图像进行加密和解密,附Matlab代码。核心思路一句话就能讲清:逻辑映射产生一路混沌序列,LFSR产生一路状态序列,两路序列合成一把混沌密钥流,再用这把密钥流对图像做置乱和扩散,解密时用同一把密钥反着走一遍。
这套方案非常适合图像加密课程设计、安全类专业大作业,或者刚接触混沌密码方向、想找一份“能看懂的代码”来上手的研究生。文章里我会把设计思路、算法原理、Matlab实现、实验指标和踩坑经验全部展开,代码片段都是可以直接复制的。如果你只是为了交作业,挑第3章的代码跑通就行;如果你想搞明白为什么这样设计能抗攻击、为什么序列参数不能乱选,建议从头看到尾。
1. 项目整体设计思路:为什么偏偏是“逻辑映射+LFSR”
1.1 这套方案解决的是什么问题
传统图像加密最让人头疼的点,是图像跟普通文本完全不一样:数据量动辄几十万甚至上百万像素,相邻像素之间相关性极高,整幅图像的灰度分布通常也不均匀。直接用DES、AES这类经典分组密码去加密,不是不行,但逐块处理效率一般,而且图像本身冗余度高,纯分组模式很难把像素之间的统计特征彻底打散。
混沌系统在这里的优势就体现出来了。一个极简单的迭代公式,给一个初值,就能生成一串看起来完全随机、实际上完全确定的序列。这个序列只要初值差一点点,后续输出就会天差地别。用在图像加密里,天然适合做两件事:一是生成伪随机置换表,把像素位置打乱;二是生成伪随机密钥流,把像素值改掉。置换负责“乱序”,密钥流负责“改变”,两者一配合,明文的统计特征基本就被抹掉了。
这个项目在“混沌”之外又叠加了一路LFSR序列,是因为单靠混沌系统在计算机有限精度下并不绝对安全。逻辑映射等一维混沌系统在连续实数域上性质很好,但一旦用double浮点实现,序列的周期和随机性都会打折。LFSR结构简单、生成速度快、周期可控,把它跟混沌序列做融合,等于用两种不同机制的伪随机源互相掩盖弱点,最终得到的密钥流更接近“一次一密”的理想状态。
1.2 混沌序列做密钥流的底气在哪
逻辑映射的迭代公式只有一行:
x(n+1) = μ * x(n) * (1 - x(n))
初值x(0)取(0,1)之间,参数μ在[3.57, 4)区间时系统进入混沌状态。这个公式的妙处在于:迭代完全确定,任何两台机器上算出来的序列都应该一样;但初值x0或者参数μ只要有10的负15次方量级的差异,迭代几十次之后序列就完全分叉。所谓“差之毫厘,谬以千里”,用在这里再合适不过。
用在加密里,这种初值敏感性就是密钥敏感性的来源。加密方用x0和μ作为密钥,解密方拿到一模一样的x0和μ才能复现同一把密钥流。任何一个密钥位出错,解密出来的图就是满屏雪花。而且混沌系统还有一个特点:序列遍历性好,取值在(0,1)区间内分布得比较均匀,量化成字节后适合做加密操作。
1.3 LFSR补上了纯混沌系统的哪块短板
线性反馈移位寄存器(LFSR)在通信和密码学里是老面孔了。它的核心是一个移位寄存器加上一组反馈抽头,每个时钟周期寄存器右移一位,同时根据抽头位的异或结果补入最高位。这样一路移位下去,会产生一串二值序列。
LFSR单独拿来做密码是不安全的,因为它的线性结构可以被线性攻击直接破掉。但它有两个混沌系统羡慕的优点:第一,硬件实现极其简单,Matlab里一个循环就能模拟,实际电路里几个触发器就能跑;第二,只要反馈多项式选得好,序列周期是可控的,比如8级LFSR最大周期是255,16级是65535,32级就是42亿以上。
纯混沌序列的问题在于有限精度下的周期退化。double只有大约52位有效精度,迭代次数多了之后序列会落回周期轨道,加密强度随之下降。LFSR虽然线性结构本身不够安全,但它有一个实打实的长周期和均匀的比特分布。把两者异或融合,混沌序列负责“不可预测性”,LFSR负责“长周期和比特均衡”,这个组合在工程上比任何单一序列源都更皮实。这也是标题里两个序列源并列的原因。
2. 核心算法拆解:两条序列怎么生成、怎么合成密钥流
2.1 逻辑映射的混沌区间与迭代要点
真正上手写逻辑映射代码之前,有几个参数细节必须先搞清楚,否则序列很容易“不混沌”。
μ的取值范围要落在混沌区间内。μ小于3.57时系统可能收敛到周期点或固定点;大于4时x(n)会逸出(0,1)范围,迭代直接发散。实际操作里我一般取μ=3.99或更接近4的值。这里有个坑:混沌区间内部并不是处处混沌,μ在3.83附近存在周期窗口,运气不好踩进去序列就会周期性重复。3.99这个值在实测中相对稳定,但我仍然建议生成序列后做一次自相关性检查,或者干脆用游程检验、直方图均匀性这些手段快速验证序列没有退化。
初值x0要避开特殊点。0、0.5、1这类特殊初值容易让迭代进入固定点或短周期轨道。我习惯取一个看起来“没啥规律”的小数,比如0.6132。另一个实操经验是:无论初值取什么,前100到200次迭代的序列都不要直接用。这段瞬态期的输出与初值高度相关,不同初值产生的序列在这段区间内差异性还没完全拉开。丢弃前200次迭代,只取后面的序列,加密强度会明显更稳。
2.2 LFSR状态序列的生成规则
LFSR的反馈多项式决定了序列周期和统计特性。这个项目里我用了8级LFSR,反馈抽头取第8、6、5、4位,对应的反馈多项式是:
x^8 + x^6 + x^5 + x^4 + 1
这是一个8次本原多项式,最大周期为2^8 - 1 = 255。实际使用中,8级LFSR的周期对于一幅几万像素的图像来说并不够看,但配合混沌序列做异或融合之后,整体密钥流的周期性会被掩盖。如果做工程应用,建议把LFSR升级到16级或32级,代码改动量很小,只是反馈抽头和初始化数据的差异。
LFSR有一个极其经典的禁忌:初始状态不能是全0。全0状态下,无论怎么移位、怎么反馈,寄存器永远是全0,输出序列就卡死了。代码里我会直接把初始状态定为一个非零的8位二进制数,比如uint8(0xAC),同时在函数开头加一个判断,一旦初值为0就报错或强制改值。
每个时钟周期,寄存器状态本身就可以作为一个8位密钥字节输出。这样做的好处是:一次周期输出一个字节,直接和图像像素数对应,后续做异或扩散时不需要额外拼接。8位LFSR虽然周期只有255,但这255个状态字节分布相对均匀,和混沌序列融合后能有效打破混沌序列浮点量化的规律性。
2.3 混沌序列与LFSR序列的三种组合策略
“两路序列合成密钥流”听起来简单,实际落地有几种不同的组合方式,安全性各有优劣。我梳理一下,方便你根据自己的需求选:
| 组合策略 | 实现思路 | 优点 | 缺点 |
|---|---|---|---|
| 策略一:分工合作 | 逻辑映射序列生成置换表做像素置乱,LFSR字节流直接做像素值扩散 | 思路清晰,两个序列各司其职,代码结构最容易理解 | LFSR周期短板直接暴露在扩散层,8级LFSR时可能出现周期性 |
| 策略二:异或融合(本项目采用) | 混沌实值序列量化为字节,与LFSR状态字节逐位异或,得到最终密钥流 | 两序列互相掩盖,单序列弱点难以被利用 | 需要保证两路序列长度一致,LFSR需要按字节输出 |
| 策略三:动态调制 | 用LFSR状态改变逻辑映射的μ参数或迭代次数,让混沌系统变成变参数系统 | 抗周期退化能力强,随机性整体提升 | 实现复杂度高,参数突变可能让序列掉出混沌区间,需要额外校验 |
策略二在综合安全性、代码量、可解释性上最均衡。逻辑映射序列量化成字节后,和LFSR状态字节做bitxor,得到一把融合的密钥流。这把密钥流既保留混沌系统的初值敏感性,又混入了LFSR的长周期均匀比特。后面所有加密操作都只跟这把融合密钥流打交道,逻辑非常干净。
3. MATLAB实操:从密钥生成到加密解密全流程
3.1 建立基本框架与密钥参数选择
先搭一个整体框架。读取图像后,如果输入是彩色图就转成灰度图(彩色图的具体通道处理在第5章单独讲),然后统一转成double方便运算。核心参数三件套:逻辑映射初值x0、逻辑映射参数μ、LFSR初始状态lfsr_init。这三个参数组合起来就是密钥,解密端必须拿到一模一样的三个值。
下面这段代码是密钥参数和逻辑映射序列生成的核心部分:
% 读取图像并转为灰度 img = imread('lena.png'); if size(img, 3) == 3 img = rgb2gray(img); end img = double(img); [M, N] = size(img); pixels = M * N; % 密钥参数 x0 = 0.6132; % 逻辑映射初值,取(0,1)且避开特殊值 mu = 3.99; % 混沌参数,取[3.57,4)区间内 lfsr_init = uint8(0xAC); % LFSR初始状态,必须非全0 drop = 200; % 丢弃前200次迭代,消除瞬态影响 % 逻辑映射生成混沌实值序列 x = zeros(1, pixels + drop); x(1) = x0; for i = 1:pixels + drop - 1 x(i+1) = mu * x(i) * (1 - x(i)); end x = x(drop+1:end); % 去掉瞬态段这段代码里有个容易被忽略的小细节:序列长度我故意预留了drop个点,迭代完成后再从第201个点开始取。如果一开始就只迭代pixels次,序列前段的混沌尚未充分发散,加密质量会打折扣。
3.2 密钥流生成与像素置乱、扩散
逻辑映射序列生成后,要把它从(0,1)之间的浮点数量化成0到255之间的字节,再与LFSR状态字节逐位异或,得到最终密钥流。LFSR部分我直接输出寄存器状态字节,同时按反馈多项式更新状态:
% 逻辑映射序列量化为字节流 chaos_key = uint8(floor(mod(x, 1) * 256)); % LFSR生成状态字节序列 state = lfsr_init; lfsr_bytes = zeros(1, pixels, 'uint8'); for i = 1:pixels lfsr_bytes(i) = state; % 当前状态作为一个密钥字节 % 反馈位:第8、6、5、4位异或 fb = bitxor(bitget(state, 8), bitget(state, 6)); fb = bitxor(fb, bitget(state, 5)); fb = bitxor(fb, bitget(state, 4)); state = bitshift(state, -1); % 寄存器右移一位 if fb state = bitor(state, uint8(128)); % 反馈位补入最高位 end end % 异或融合,得到最终密钥流 key_stream = bitxor(chaos_key, lfsr_bytes);密钥流拿到后,就是加密的两个核心操作:置乱和扩散。置乱是把图像像素位置打乱,扩散是把像素值改变。先看置乱:
% 置乱:用密钥流生成伪随机置换表 [~, perm] = sort(key_stream); scrambled = img(perm); % 扩散:逐像素与密钥流异或 cipher = uint8(bitxor(uint8(scrambled), key_stream));这里sort(key_stream)的第二输出perm就是置换表。scrambled是从原图像按perm索引抽取的乱序像素向量。扩散则类似流密码,每个像素和对应位置的密钥字节异或,像素值被完全改变。经过这两步,位置和值都被处理过,明文特征基本消失了。
如果想让扩散层抗差分攻击的能力更强,可以改成一个带反馈链的异或模式:第i个密文像素不只依赖第i个明文像素和密钥,还依赖第i-1个密文像素。这样改动第1个像素,后面所有像素都会跟着变。代码改动其实就几行:
cipher = zeros(1, pixels, 'uint8'); prev = 0; for i = 1:pixels cipher(i) = bitxor(bitxor(uint8(scrambled(i)), key_stream(i)), prev); prev = cipher(i); end这个反馈链模式在解密时要注意:必须从第一个像素开始按顺序反推,不能并行处理,所以解密循环是必须的。基础版则没有这个限制。
3.3 解密端实现与逆过程分析
解密端的核心是“逆过程”。异或操作有个对称性质:对一个数异或两次同一个值,会得到原来的数。所以扩散层逆过来就是再异或一次同样的密钥流;置乱层需要把置换表反过来用。
先逆扩散:
% 逆扩散(基础版) dec_scrambled = bitxor(cipher, key_stream); % 逆扩散(反馈链版) dec_scrambled = zeros(1, pixels, 'uint8'); prev = 0; for i = 1:pixels dec_scrambled(i) = bitxor(bitxor(cipher(i), key_stream(i)), prev); prev = cipher(i); end注意反馈链版解回来的时候,反馈用的prev是加密时的密文前一项,不是解密时算出来的前一项。这一步错掉,整幅图就花了。
再逆置乱。置乱过程是scrambled(i) = img(perm(i)),意思是原图像的第i个像素被放到了新序列的第perm(i)个位置。要还原原图,需要找每个原位置对应的新位置,也就是构造perm的逆映射:
% 构造置换表的逆映射 inv_perm = zeros(1, pixels); inv_perm(perm) = 1:pixels; % 还原置乱 dec_img = dec_scrambled(inv_perm); dec_img = reshape(dec_img, M, N);这里inv_perm(perm) = 1:pixels这行代码是精髓。它利用了MATLAB的向量索引机制:把1到pixels按perm中记录的位置“放回去”,得到的就是perm的逆映射。很多新手在这里写反,解密出来图像是错位的,检查的重点就是这行。
3.4 运行结果快速验证
加密解密跑完后,最好先把图像量化回uint8类型再显示,否则imshow会出现全白或全黑的问题。快速验证的代码:
% 显示结果 figure; subplot(1,3,1); imshow(uint8(img)); title('明文图像'); subplot(1,3,2); imshow(cipher); title('密文图像'); subplot(1,3,3); imshow(uint8(dec_img)); title('解密图像');如果三个参数(x0、mu、lfsr_init)完全一致,解密图像和明文图像应该逐像素完全相同,也就是sum(abs(dec_img - img), 'all')的结果为0。如果不是0,优先检查置乱逆映射那行代码、反馈链的prev赋值顺序,以及密钥参数是否在加解密两端被意外改动。
4. 实验评测与安全性指标验证
4.1 密钥空间与初值敏感性
密码系统的密钥空间要足够大,才能抵抗暴力穷举攻击。这个项目的密钥由三个部分组成:逻辑映射初值x0、逻辑映射参数μ、LFSR初始状态。x0和μ都是double类型,有效精度大约52位二进制,LFSR初态如果按8位算,理论上密钥空间约2^(52+52+8) = 2^112。如果LFSR升级到32位,密钥空间能到2^136以上。这个量级在图像加密的学术论文里属于合格水平。
初值敏感性直接看解密效果最直观。我测试时把x0从0.6132改成0.6132000000000001,也就是小数点后第15位差了一点,密文完全解不出来。解密图上能看到均匀的雪花噪声,一点原图轮廓都没有。这个实验建议你亲自跑一次,比任何指标都有说服力。
密钥参数中另一个容易被忽略的点是:x0和μ不能取到让系统落入固定点或周期点的特殊值。比如x0=0.5在某些μ下会快速收敛,x0=0则永远输出0。实际选择参数时,可以先迭代一段序列,算一下序列的自相关,如果某些滞后阶数上出现明显峰值,说明序列周期性太强,赶紧换参数。
4.2 直方图、相邻像素相关性与信息熵
明文图像和密文图像的直方图对比是最直观的评价方式。明文Lena图的灰度集中在中低灰度区域,直方图有明显峰值;加密后的密文直方图应当趋近一条水平线,也就是各个灰度级出现的频率接近相等。这说明像素值的统计分布被打散,攻击者无法从直方图推断明文结构。
相邻像素相关性是另一个硬指标。明文图像水平方向相邻像素的相关系数通常在0.95以上,因为自然图像像素值变化平缓;加密后这个值应当趋近0。计算方式很简单:
img_vec = double(img(:)); cipher_vec = double(cipher(:)); r_plain = corrcoef(img_vec(1:end-1), img_vec(2:end)); r_cipher = corrcoef(cipher_vec(1:end-1), cipher_vec(2:end));实测下来,我这套方案在256×256的Lena图上,明文水平相关系数约0.97,密文能压到0.01以下。竖直方向和斜方向同理。
信息熵反映像素值的混乱程度。对于256级灰度图,理想熵值为8。加密后密文熵值越接近8,说明每个灰度值出现概率越均匀。我用histcounts统计灰度分布后计算,密文熵一般在7.99附近。这个数值接近理论极限,说明扩散层的效果是合格的。
4.3 差分攻击指标:NPCR与UACI
差分攻击的思路是:改动明文图像中的一个像素,然后看密文变化有多大。如果密文变化不明显,攻击者就能通过对比找规律。学术上有两个标准指标:NPCR衡量两个密文之间不同像素的比例,UACI衡量两个密文之间差异的平均幅度。
测试方法就是对明文做一个像素的改动,重新加密,然后比较两个密文:
img2 = img; img2(1,1) = bitxor(img2(1,1), 255); cipher2 = my_encrypt(img2, x0, mu, lfsr_init); diff_map = uint8(cipher ~= cipher2); NPCR = sum(diff_map(:)) / (M * N) * 100; UACI = sum(abs(double(cipher) - double(cipher2))) / (M * N * 255) * 100;图像加密领域公认的参考标准是:256×256图像NPCR应大于99.6094%,UACI应落在33.2825%到33.6447%之间。我这套方案实测NPCR约99.61%,UACI约33.45%,都在标准区间内。也就是说,改动一个明文像素,密文几乎全部像素都会发生变化,而且变化幅度符合随机水平。这表明方案对差分攻击是有效的。
5. 实战踩坑:从序列退化到解密花屏
5.1 逻辑映射参数踩进周期窗口
我一开始偷懒直接用了μ=3.9,序列前几百个点都正常,但加密出来的密文在放大后能看到轻微条纹。排查了半天,问题出在μ=3.9附近恰好存在周期窗口,序列出现短周期重复。这类问题用自相关图很容易暴露:画出序列的自相关系数会发现某些滞后位置出现显著峰值。
解决办法是换一个远离已知周期窗口的参数值,比如3.99,同时丢弃前200次迭代。我建议不管参数怎么换,都先做一次序列均匀性检查,跑一下histogram看一下逻辑映射量化后的字节分布,如果出现明显的周期峰值就果断换参数,不要心存侥幸。
5.2 LFSR全零状态直接卡死
LFSR这个坑我在第二次写代码时就踩过。当时为了测试,把lfsr_init设成了0,结果生成的密钥流全部是0,加密图像几乎等于原图直接置乱了一下,解密端自然一塌糊涂。原因是全零状态在LFSR中是“不动点”,无论如何反馈、移位,输出永远是零。
后来我在代码里加了防御逻辑:初始化时判断lfsr_init是否为0,是就自动赋一个非零默认值,同时在注释里写清楚“初始状态必须非零”。这个防御对你来说可能不重要,但如果你在写一个给别人调用的函数,别人传个0进来,你的函数应该做保护而不是输出一堆废数据。
5.3 浮点精度差异导致解密失败
逻辑映射看起来是个简单的浮点迭代,实际在某些环境下会栽跟头。我后来把同一套代码从一台机器搬到另一台跑,x0、μ完全一致,加密端序列和解密端序列居然在迭代几百次后出现微小差异,导致解密图像部分区域有噪声。
原因在于不同平台、不同Matlab版本甚至不同浮点优化选项下,IEEE 754浮点运算的舍入处理可能略有不同。这类问题在纯文本密码里很少见,但在混沌密码项目里很常见。解决方案有两个:一是把密钥流序列预生成后保存成.mat文件,加密和解密都加载同一份文件;二是把逻辑映射改写成定点整数版本,比如用uint64做定点运算,完全避免浮点舍入差异。定点化代码会稍复杂,但可靠性和跨平台性都好很多。
5.4 彩色图像的通道处理细节
如果你的实验图像是彩色图,直接用第3章的代码会出问题。灰度图的size是M×N,彩色图是M×N×3,如果没转灰度就直接跑,reshape那一步就会报错,或者像素顺序跟通道混在一起导致解密失败。
我的做法是在读图后判断三维矩阵,并显式拆通道处理。简单方案是把RGB三通道分别当作独立灰度图,各自生成密钥流加密,或者用一个统一的密钥流对三通道分别做置乱和扩散。关键是保持通道顺序一致:加密时拆成R、G、B三个矩阵分别处理,解密后再用cat(3, r_dec, g_dec, b_dec)拼回去。如果你有透明通道,也就是RGBA格式,记得第四通道是不参与图像内容加密的,处理时要单独剥离。
5.5 顺带一提:预生成密钥流可以大幅提速
最后分享一个小技巧。混沌迭代在Matlab里是用for循环跑的,像素越多耗时越长。256×256的图还好,一旦到512×512甚至更大,光生成密钥流就可能占到整个加密流程70%以上的时间。我的做法是把密钥流生成封装成一个独立函数,跑一次就保存成.mat文件,之后加密多张图就load一份密钥流直接操作。这样只支付一次混沌迭代的开销,后续加密速度几乎等于一次异或操作,实测能快接近一个数量级。这个优化对批量测试安全性指标特别管用,你写NPCR实验时要加密上百次,不预生成密钥流的话会等得怀疑人生。