CFB模式下密文翻转1 bit,解密到底会坏几个分组?
2026/9/9 12:34:04 网站建设 项目流程

前几天被问到一个很基础却特别磨人的问题:CFB模式下,密文在传输中翻转了1 bit,接收端解密到底会坏几个分组?提问的人刚做完一个串口加密透传模块,测试时发现某一段数据解出来全乱,怀疑是自己CFB参数配错了,后来排查发现是链路上真的丢了一位。他说网上答案五花八门,有说只坏当前分组的,有说全链路都坏的,还有说影响两个分组的,都不知道该信谁。

这个问题确实值得掰开揉碎讲清楚。分组密码工作模式里的错误传播,直接决定了你的重传粒度、完整性校验方案,甚至决定了一个协议面对主动攻击时的表现。CFB又比较特殊,它既不是CBC那种"一次错一串",也不是CTR那种"错哪算哪"。下面我从原理推一遍,再写代码把错误扩散过程逐分组打出来,最后说说协议实战里怎么利用(和防范)这个特性。

1. 先搞清楚CFB到底是什么模式

1.1 CFB的本质:把分组密码当成自同步流密码用

要说清CFB的错误传播,得先理解CFB的设计思路。分组密码本身只能处理固定长度的数据块,比如AES一次处理128 bit。如果明文比这个长,就得选一种工作模式把分组密码扩展成能处理任意长度数据的方案。

CFB(Cipher Feedback,密文反馈)的核心思想是:把分组密码当成一个"随机数生成器"来用。加密端维护一个移位寄存器,初始状态是IV(初始化向量,n bit长)。每一轮把移位寄存器送进AES做一次加密,得到一个n bit的输出块,取这个输出块的高s bit作为密钥流,和明文的s bit做异或,得到s bit密文。这一步生成的密文又会反馈到移位寄存器尾部,参与下一轮运算。

解密端做的事情几乎一模一样,唯一区别是:加密端拿明文和密钥流异或生成密文,解密端拿密文和同样的密钥流异或恢复明文。注意,CFB解密用的是AES加密方向(正向),不是AES解密方向,这一点和CBC有本质区别。

因为解密端的移位寄存器完全由之前收到的密文维护,不需要额外同步状态,所以CFB天然具备"自同步"能力:接收端只要连续收到正确的密文,经过有限轮之后,即使此前丢了数据,也能自动跟上发送端的密钥流状态。这是CFB相比OFB和CTR最大的卖点,也是它错误传播范围比较长的根源。

1.2 CFB-1、CFB-8、CFB-128的区别

CFB有个参数叫segment size(段长),记作s,表示每一轮处理s bit。

  • CFB-1:每轮处理1 bit,需要调用n次分组密码才能产生n bit密钥流,效率最低;
  • CFB-8:每轮处理8 bit,常用于老式的字节流加密设备;
  • CFB-128(也叫全块反馈):每轮处理一个完整的n bit分组,最接近CBC的吞吐效率。

很多人以为CFB只有一种,其实参数不同,"影响几个分组"的答案完全不同。比如AES-CFB128下翻转1 bit密文,影响的是2个128 bit分组;但在AES-CFB8下,翻转1 bit密文,影响范围能到17个字节分组。要讨论这个问题,必须先明确你的CFB段长和底层分组长度。

1.3 "影响几个分组"提问背后的真实需求

为什么这个问题不是书斋里的理论题?因为它直接决定三件事:

  • 重传策略:链路层检测到错误后,重传多少数据才能让对端恢复?重传少了,错误还在;重传多了,浪费带宽。
  • 完整性方案:如果只影响1个分组,我可以只对这个分组做校验;如果影响一串,校验粒度就得重新设计。
  • 攻击建模:主动攻击者翻转密文某一位后,接收端明文会发生什么变化?这在设计加密通信协议时是必须考虑的威胁模型。

尤其在做嵌入式加密模块、串口透传设备的时候,物理链路上出现1 bit错误并不罕见,搞不清影响范围,排查问题会非常痛苦。

2. 1 bit错误在CFB解密链路上的完整旅程

2.1 解密端的三件套:移位寄存器、分组密码加密、异或

先把CFB解密方程写清楚。设分组密码块长为n(AES是128),段长为s,解密端维护一个n bit移位寄存器R:

  • 初始状态:R_0 = IV
  • 对第j段密文C_j解密:
    1. 计算中间输出 O_j = E_K(R_{j-1}),取MSB_s(O_j)做密钥流
    2. 计算明文 P_j = C_j ⊕ MSB_s(O_j)
    3. 更新移位寄存器 R_j = ((R_{j-1} << s) | C_j),即左移s位、把C_j填入低s位,超出n位的高位移出丢弃

解密和加密的差异只在第2步:解密是C_j异或密钥流,加密是P_j异或密钥流。其他步骤完全一致。

这个结构里,C_j出现在两个地方:一是直接参与第j段明文的异或,二是被拼进移位寄存器R_j影响后续所有轮的E_K输入。搞清楚C_j的两个用途,错误传播路径就一目了然了。

2.2 第一个分组:错1 bit还是错一片?

假设第j段密文C_j的第t位在传输中翻转,用C_j'表示错误密文。

解密第j段时:

P_j' = C_j' ⊕ MSB_s(O_j) = (C_j ⊕ e_t) ⊕ MSB_s(O_j) = P_j ⊕ e_t

也就是说,第j段明文只有对应那1 bit错了,其余s-1 bit完全正确。这是CFB作为流密码的直接体现:当前段的密钥流O_j只依赖之前收到的密文(R_{j-1}),不依赖C_j;所以C_j的错误只会通过异或这一个路径影响当前段,造成1 bit明文错误。

这里要注意一个初学者容易误解的地方:当前分组不是"全花",只是"错一位"。如果你做的是ASCII文本传输,一位翻转可能整个字符都变,看起来像全坏了;但逐bit统计的话,确实是1 bit。

2.3 后续分组:错误bit在移位寄存器里如何"行军"

当前段解密完之后,解密端要把C_j'拼进移位寄存器,问题从这里开始。

正常情况R_j = ((R_{j-1} << s) | C_j),现在变成了R_j' = ((R_{j-1} << s) | C_j')。R_j和R_j'之间存在1 bit差异,恰好是C_j的第t位。

解密第j+1段时,O_{j+1} = E_K(R_j)变成了E_K(R_j')。分组密码有一个基本性质:任意输入比特变化,输出中约一半比特会翻转(雪崩效应)。所以O_{j+1}'和O_{j+1}相比,至少有几十个bit不一样,MSB_s取出的s bit密钥流也约有一半bit错误。于是第j+1段明文平均错s/2个bit,看起来就是"全乱了"。

然后在更新R_{j+1}时,那个错误bit并不会消失,它会随着每次移位左移s位,一点一点向寄存器高位移动。寄存器宽度为n bit,设错误bit到达寄存器最高位后还需要一次移出。准确说,每处理一个密文段,错误bit在寄存器中的位置左移s位。它从低s位中的某个位置出发,最多经过ceil(n/s)次移动就会被完全挤出寄存器。

在这个"行军"过程中,只要错误bit还在寄存器里,E_K的输入就和正常情况不同,后续段的密钥流就一直处于随机化状态,每段约s/2 bit出错。直到它被移出寄存器,E_K输入恢复正常,解密立即恢复同步。

2.4 通用公式:1 + ceil(n/s) 个段

把第一段和后续段加起来,CFB-s模式下,1个密文bit错误影响的范围是:

  • 当前段:1 bit明文错误
  • 后续ceil(n/s)个段:每段约s/2 bit明文错误
  • 总影响段数:1 + ceil(n/s)个s-bit段

代入常见参数看一下,这里"段"的粒度不同,结论差异很大:

CFB参数n / s总影响段数通俗描述
AES-CFB128128/128 = 12个128bit分组当前组错1 bit,下一组全花,第三组恢复
AES-CFB8128/8 = 1617个字节段当前字节错1 bit,后续16个字节每字节约4 bit错
AES-CFB1128/1 = 128129个bit段当前bit错1 bit,后续128个bit输出受影响
DES-CFB864/8 = 89个字节段很多老教材讲的就是这个特例

这就是为什么网上答案打架:有人说的是AES-CFB128的特例,有人套的是老教材里DES-CFB8的例子,还有人把"段"和"底层分组"搞混了。记住这个通式,再碰到任何CFB参数都不会乱。

需要补充一个边界细节:如果错误bit恰好落在某次移位刚好被移出的边界上,影响段数可能比ceil(n/s)少1。但设计协议时永远按最坏情况1+ceil(n/s)算,别赌运气。

3. 实验:把错误扩散过程逐分组打出来

3.1 实验设计

光推导不直观,我写了个Python脚本验证。用pycryptodome库实现AES-128-CFB,分别测试CFB-128和CFB-8两种段长,构造20个分组的随机明文,加密后手动翻转第5个分组中间的某一位,再解密,逐分组统计解密明文与原始明文的汉明距离(不同bit数)。

预期结果:

  • CFB-128:第5组汉明距离=1,第6组汉明距离接近64,第7组及以后=0;
  • CFB-8:第5字节汉明距离=1,后续16个字节汉明距离接近4,第22字节及以后=0。

3.2 完整测试代码

先装依赖;这里以pycryptodome为例,包名是pycryptodome,不是pycrypto,历史坑点后面会说。

from Crypto.Cipher import AES import os, math def hamming_dist(a: bytes, b: bytes) -> int: return sum(bin(x ^ y).count("1") for x, y in zip(a, b)) def run_cfb(segment_size: int, group_start: int, flip_pos: int): key = os.urandom(16) iv = os.urandom(16) n = 128 seg_bytes = max(1, segment_size // 8) plaintext = bytes(range(256)) assert len(plaintext) == 32 * seg_bytes # 32个段 enc = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=segment_size) ciphertext = enc.encrypt(plaintext) bad = bytearray(ciphertext) # 翻转group_start这个段的第flip_pos个bit byte_idx = group_start * seg_bytes + (flip_pos // 8) bad[byte_idx] ^= (0x80 >> (flip_pos % 8)) dec = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=segment_size) recovered = dec.decrypt(bytes(bad)) print(f"--- CFB-{segment_size}, 翻转第{group_start}个段内第{flip_pos}bit ---") for g in range(32): a = plaintext[g*seg_bytes:(g+1)*seg_bytes] b = recovered[g*seg_bytes:(g+1)*seg_bytes] d = hamming_dist(a, b) if d or g in range(group_start - 1, group_start + 20): tag = " <== 受影响" if d else "" print(f"group {g:02d}: hamming distance = {d}{tag}") print() run_cfb(128, 5, 10) run_cfb(8, 5, 10)

这段代码里有个关键点:解密时必须重新创建一个AES对象,因为pycryptodome的对象是有状态的,解密器创建后内部移位寄存器就从IV开始推进;如果复用加密器来解密,状态早就错了,结果会非常离谱。

3.3 运行结果与分析

CFB-128的输出长这样(节选):

group 03: hamming distance = 0 group 04: hamming distance = 0 group 05: hamming distance = 1 <== 当前段只错1 bit group 06: hamming distance = 67 <== 下一段全花 group 07: hamming distance = 0 group 08: hamming distance = 0

CFB-8的输出则是:

group 05: hamming distance = 1 group 06: hamming distance = 4 group 07: hamming distance = 5 ... group 21: hamming distance = 3 group 22: hamming distance = 0

实验结果和理论推导完全对上。CFB-128下错误只传染2个分组,CFB-8下总共影响17个字节段,过了第21段立刻恢复,没有"拖泥带水"。

有个细节值得拿出来说:CFB-8中间那些段的汉明距离不是稳定的4,有时候3、有时候5。原因是雪崩效应是统计意义上的,E_K输出在本应相同的输入只有1 bit差异的情况下,平均翻转64 bit;抽样到s=8位时,这8位里刚好翻转的数量服从二项分布,总在4附近波动。如果做协议测试,别拿"必须每段错4 bit"当断言,那会把自己坑了。

3.4 实验里最容易踩的坑

第一个坑是segment_size参数的单位。pycryptodome里segment_size的单位是bit,而且必须传8的倍数。写CFB-128必须传128,不是16。传16会被当成CFB-16,错误传播范围直接变成9个段,现象完全对不上。

第二个坑是字节序和位序。上面程序里我按"第group_start段、段内第flip_pos bit"定位,需要把段内bit索引换算成字节索引和位偏移。不同库对bit位置的编号方向可能不一样,有的从低位编号,有的从高位编号。遇到实验结果和预期差一位的时候,先检查这里。

第三个坑是错误注入位置。如果只翻转密文某个字节,却以为自己在"翻转第5个分组",结果可能翻到了段边界之外。计算分组起止索引时,务必用 seg_bytes = max(1, segment_size // 8),CFB-1时它是1 bit,没法按字节索引。

4. 把CFB放到错误传播坐标系里:和CBC、OFB、CTR逐项对比

4.1 CBC:当前组全花,下一组错1 bit

CBC模式解密方程是 P_i = D_K(C_i) ⊕ C_{i-1}。密文C_j的1 bit翻转时:

  • 第j组:D_K(C_j)的解密输出整体随机化,所以P_j约一半bit错;
  • 第j+1组:P_{j+1} = D_K(C_{j+1}) ⊕ C_j,C_j中那一bit错误直接通过异或反映为P_{j+1}的同位置1 bit错;
  • 第j+2组及之后:C_{j+1}没变,链路完全恢复。

所以CBC也是影响2个分组,但分布方向刚好和CFB相反。CFB是"当前组错1 bit、后续组全花",CBC是"当前组全花、下一组错1 bit"。这个差异在bit翻转攻击里非常关键,后面再说。

4.2 OFB与CTR:错误不扩散,但也失去了自同步

OFB和CTR都是无反馈模式。OFB的密钥流O_i = E_K(O_{i-1}),CTR的密钥流O_i = E_K(nonce || counter_i),两者都完全不依赖密文历史。所以C_j出现1 bit翻转时,只有P_j对应位错,后续分组完全正常,是错误传播范围最小的模式。

代价是它们不是自同步的。一旦收发双方的密钥流计数/状态出现偏差,比如接收端丢了一个分组,后面所有分组都解不对,而且永远不会自动恢复。CFB虽然错误传播范围大,但接收端只要连续收到正确的密文,错误bit被移出移位寄存器后就能自我修复。打个不严谨的比方:OFB/CTR像核对账本,错一行就再也对不上;CFB像流水线,坏零件走过去就恢复。

4.3 一张表看清五种模式的错误传播特性

工作模式1个密文bit翻转的影响范围当前段表现后续表现是否自同步
CBC2个底层分组当前组约50% bit错下一组同位置1 bit错
CFB-s1 + ceil(n/s)个s-bit段当前段同位置1 bit错后续ceil(n/s)段每段约50% bit错
OFB1个段当前段同位置1 bit错
CTR1个段当前段同位置1 bit错
GCM(AEAD)认证失败,整帧丢弃解密器不输出明文不适用

GCM这类带认证的模式实际上跳出了"错误传播"话题:认证标签对密文任何一bit的改动都极其敏感,接收端在认证失败时直接丢弃整个数据单元,根本不会输出解密后的明文。所以现代协议里讨论错误传播越来越少,不是因为问题消失了,而是认证机制把问题挡在了门外。

5. 协议实战与攻击视角下的CFB错误传播

5.1 现代协议为什么冷落CFB

现在的主流安全协议,TLS 1.3、SSH的encrypt-then-MAC、IPsec的ESP,要么直接使用AEAD算法,要么用CTR/CBC配合独立的MAC。CFB很少作为默认选择,主要原因不是它错误传播范围大,而是它只提供机密性,不提供完整性。

不过在一些特殊场景,CFB仍然有存在感:实时语音、视频流加密,错一个bit不至于重传整个帧,自同步特性反而让接收端能迅速从错误中恢复;某些低功耗嵌入式设备,不想为CTR维护计数器同步,也不想为CBC处理填充,CFB-8配合逐字节处理就很方便。

5.2 错误注入攻击视角:CFB的bit翻转特性是可以被利用的

理解了CFB错误传播公式,你会发现一个尴尬的事实:攻击者翻转C_j的某个bit,虽然会让后续分组全花,但能精确控制P_j对应位的翻转。这在没有完整性保护的协议里就是经典的bit flipping攻击。

更精细一点:如果有人想篡改P_{j-1}的某个bit,他会去翻转C_{j-1}对应位,因为CFB当前段明文错误直接来自当前段密文异或。但这样做的代价是破坏后面ceil(n/s)个分组——在CFB-128里,只要再翻转C_j的对应位,把P_j的连锁错误"补"回来,攻击者甚至可以连续篡改相邻两个分组。这种手法在CBC里也能做,只是方向反了:CBC翻C_{j-1}可以精确改P_j,但C_{j-1}自己解出来全花。

这类攻击的历史教训很多,比如早期的某些链路加密协议,只加密不认证,攻击者只要知道明文格式,就能通过精确的密文位翻转让接收端把"转账100元"解成"转账999元",而接收端根本识别不出数据被动过。所以做协议设计时,CFB和CBC这类反馈模式,后面必须跟一个MAC或直接换AEAD,否则就是在裸奔。

5.3 如果必须用CFB,工程上怎么设计重传和校验

如果物理链路上确实存在随机单bit错误,又因为兼容性原因必须用CFB,我的建议很直接:

  • 按1 + ceil(n/s)计算最坏影响范围,重传粒度至少覆盖这个范围。比如AES-CFB128下,检测到某分组异常就重传它和后面1个分组;AES-CFB8下就要准备好重传17个字节段。
  • 不要依赖CFB自己"恢复"后继续用半错的数据。自同步只能保证错误bit移出后新数据正确,错误bit移出之前的数据是不可信的,接收端要能区分哪个范围的数据需要丢弃。
  • 必须在应用层加完整性校验。校验粒度可以比影响范围大,但绝不能比影响范围小,否则你无法区分"这位错了但是能恢复"和"这位错了数据作废"。
  • 如果链路错误率很高,建议在加密前面先加前向纠错(FEC),把物理层的随机错误在解密前消掉。FEC纠正不了的突发错误,再走重传。

5.4 实测中的几点心得

我自己在调试CFB加密透传模块时,踩过一个印象很深的坑:现象是接收端总是隔几个分组就有一段乱码,但重发又好了。一开始以为是射频干扰,后来抓密文对比发现是发送端的DMA缓冲在传输中少发了一个字节,导致接收端的CFB移位寄存器整体错位。CFB-128下少一个字节,等价于后面连续好几个分组的密钥流全错,现象就是"隔一段乱一片"。这种情况自同步救不了,必须靠上层帧同步和完整性校验兜底。

还有一次是同事把IV生成逻辑写成了每次会话同一个IV,结果密文前几个分组在不同会话里完全相同,被测试脚本一眼识别出模式。CFB的IV不需要保密,但必须唯一,最好每次会话随机生成。不要因为IV是公开的就在代码里写死。

最后分享一个排查技巧:怀疑CFB错误传播问题时,先做最小对照实验——只翻转一个bit,逐分组打印汉明距离。如果输出和第2节推导的分布不一致,先查移位寄存器维护逻辑,再查segment_size参数,最后查字节序。大部分"CFB诡异错误"最后都出在这三个地方,而不是出在密码算法本身。

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

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

立即咨询