分组密码(Block Cipher)是现代对称密码学的基石。无论是 AES、SM4 还是历史上的 DES,它们的核心能力都相同——接收一个固定长度的明文块和一把密钥,输出一个等长的密文块。以 AES-128 为例,它只能加密恰好 128 位(16 字节)的数据。然而,现实世界中的消息几乎不可能恰好是 16 字节:一封电子邮件可能有数千字节,一个数据库字段可能只有 3 字节,一次 TLS 记录可能长达 16384 字节。
如何将一个只能处理 16 字节的”积木块”组装成能加密任意长度消息的完整系统?这正是工作模式(Mode of Operation)要解决的问题。工作模式定义了分组密码如何被反复调用以处理超过单个块长度的数据,它决定了加密方案的安全性、性能特征与容错能力。选择错误的工作模式,即使底层分组密码无懈可击,整个系统也可能不堪一击。
本文将全面解析五种经典工作模式——ECB、CBC、CTR、OFB 和 CFB,剖析它们的设计原理、安全性论证、已知攻击和工程实践。我们将看到,工作模式的选择绝非细枝末节,而是密码系统设计中最容易犯错、代价最高的决策之一。
一、为什么需要工作模式
分组密码的本质局限
一个分组密码 \(E_K: \{0,1\}^n \to \{0,1\}^n\) 本质上是一个密钥索引的置换族(Keyed Family of Permutations)。给定密钥 \(K\),\(E_K\) 是从 \(n\) 位字符串到 \(n\) 位字符串的双射。这意味着分组密码在数学上等价于一本巨大的查找表——对于每个可能的 \(n\) 位输入,恰好映射到一个唯一的 \(n\) 位输出。
这个定义立即暴露了三个根本问题。第一,长度限制:明文必须恰好是 \(n\) 位,不能多也不能少。第二,确定性:相同的密钥和明文总是产生相同的密文,这意味着裸的分组密码不可能满足语义安全(Semantic Security)的要求——攻击者只需观察到两段相同的密文,就知道对应的明文也相同。第三,缺乏完整性保护:分组密码只提供保密性(Confidentiality),不提供任何关于消息是否被篡改的保证。
工作模式正是为了解决前两个问题而设计的——它将分组密码从一个固定长度的确定性置换,扩展为一个能处理任意长度消息的概率性加密方案(Probabilistic Encryption Scheme)。第三个问题——完整性——则需要消息认证码(MAC)或认证加密(AEAD)来解决,我们将在后续文章中详细讨论。
NIST SP 800-38A
1981 年,NIST 的前身 NBS(National Bureau of Standards)在 FIPS 81 中首次标准化了四种 DES 工作模式:ECB、CBC、OFB 和 CFB。2001 年,NIST 发布 SP 800-38A,为 AES 重新定义了这四种模式并新增了 CTR 模式。此后,SP 800-38 系列又陆续定义了 CCM(SP 800-38C)、GCM(SP 800-38D)等认证加密模式。理解经典的五种模式是理解一切后续发展的基础。
形式化框架
在形式化框架中,一个工作模式可以被视为一个三元组 \((\text{KeyGen}, \text{Enc}, \text{Dec})\):
- \(\text{KeyGen}\):生成密钥 \(K\)(通常直接复用底层分组密码的密钥生成)。
- \(\text{Enc}(K, M)\):输入密钥和任意长度的明文 \(M\),输出密文 \(C\)(通常还包含一个随机的初始化向量 IV 或计数器 nonce)。
- \(\text{Dec}(K, C)\):输入密钥和密文,恢复明文 \(M\)。
正确性要求 \(\text{Dec}(K, \text{Enc}(K, M)) = M\) 对所有合法的 \(K\) 和 \(M\) 成立。安全性则需要通过形式化的安全模型来定义,最基本的标准是选择明文攻击下的不可区分性(IND-CPA),我们将在第八节详细讨论。
二、ECB 模式
原理
电子密码本模式(Electronic Codebook,ECB)是最简单也最直觉的工作模式。它的做法简单到几乎不值得称为”模式”——将明文按块大小分割,每块独立加密:
\[C_i = E_K(P_i), \quad i = 1, 2, \ldots, t\]
解密同样逐块独立进行:
\[P_i = D_K(C_i), \quad i = 1, 2, \ldots, t\]
其中 \(P_i\) 和 \(C_i\) 分别是第 \(i\) 个明文块和密文块,\(E_K\) 和 \(D_K\) 分别是分组密码的加密和解密函数。如果最后一个明文块不满整块长度,需要进行填充(Padding)。
ECB 模式最突出的优点是简单性和并行性。加密和解密都可以完全并行化——每个块的处理独立于其他所有块,这使得 ECB 在硬件实现中可以达到极高的吞吐量。此外,ECB 支持随机访问——可以独立地加密或解密任意位置的块,无需处理之前或之后的块。
企鹅问题
然而,ECB 模式有一个致命的缺陷:它是确定性的(Deterministic)。相同的明文块在相同的密钥下总是产生相同的密文块。这意味着明文中的模式(Pattern)会完整地保留在密文中。
这个问题最著名的可视化演示被称为”ECB 企鹅”(ECB Penguin)。如果我们用 ECB 模式加密一张位图图像(Bitmap),由于图像中相同颜色的像素区域会产生相同的密文块,加密后的图像仍然保留了原始图像的轮廓。用 CBC 或 CTR 等随机化模式加密同一张图像,结果则看起来像完全随机的噪声。
以下 Python 代码演示了这个问题:
"""ECB 企鹅演示:展示 ECB 模式如何泄露明文结构"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import os
def ecb_encrypt(key: bytes, plaintext: bytes) -> bytes:
"""使用 AES-ECB 加密,要求明文长度是 16 的倍数"""
cipher = Cipher(algorithms.AES(key), modes.ECB())
enc = cipher.encryptor()
return enc.update(plaintext) + enc.finalize()
def cbc_encrypt(key: bytes, plaintext: bytes) -> bytes:
"""使用 AES-CBC 加密(随机 IV),用于对比"""
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
enc = cipher.encryptor()
return iv + enc.update(plaintext) + enc.finalize()
key = os.urandom(16)
# 构造具有重复模式的"图像数据"——模拟企鹅图像
# 用重复的块模拟图像中大面积相同颜色的区域
block_white = b'\xff' * 16 # 白色区域
block_black = b'\x00' * 16 # 黑色区域
block_gray = b'\x80' * 16 # 灰色区域
# 模拟一幅简单的"图像":白-白-黑-黑-灰-白-白-黑-黑-灰(重复模式)
image_data = (block_white * 2 + block_black * 2 + block_gray) * 4
ecb_ciphertext = ecb_encrypt(key, image_data)
cbc_ciphertext = cbc_encrypt(key, image_data)
# 分析 ECB 密文中的重复块
ecb_blocks = [ecb_ciphertext[i:i+16] for i in range(0, len(ecb_ciphertext), 16)]
unique_ecb = len(set(ecb_blocks))
# CBC 密文的前 16 字节是 IV,跳过后再分块
cbc_ct_only = cbc_ciphertext[16:]
cbc_blocks = [cbc_ct_only[i:i+16] for i in range(0, len(cbc_ct_only), 16)]
unique_cbc = len(set(cbc_blocks))
print(f"明文块总数: {len(ecb_blocks)}")
print(f"ECB 唯一密文块数: {unique_ecb} (仅 {unique_ecb} 种,模式完全暴露)")
print(f"CBC 唯一密文块数: {unique_cbc} (几乎全部唯一,模式被隐藏)")
print()
# 直观展示:打印前 10 个 ECB 密文块的十六进制前 8 字节
print("ECB 密文块(前 8 字节):")
for i, blk in enumerate(ecb_blocks[:10]):
print(f" 块 {i:2d}: {blk[:8].hex()}")
print("\nCBC 密文块(前 8 字节):")
for i, blk in enumerate(cbc_blocks[:10]):
print(f" 块 {i:2d}: {blk[:8].hex()}")运行此代码会清楚地展示:ECB 模式下 20 个密文块中只有 3 种不同的块(对应白、黑、灰三种”颜色”),明文的结构完全暴露;而 CBC 模式下所有密文块几乎都不相同,明文结构被完全隐藏。
为什么 ECB 几乎永远不合适
ECB 模式不满足最基本的安全定义 IND-CPA(选择明文攻击下的不可区分性)。攻击者只需提交两条不同的消息——一条由相同的块重复组成,另一条由不同的块组成——然后检查密文中是否有重复的块,就能以概率 1 区分这两条消息的加密。
在实际应用中,ECB 模式的使用几乎总是一个严重的安全缺陷。已知的真实案例包括:Adobe 在 2013 年数据泄露事件中被发现使用 ECB 模式加密用户密码,由于大量用户使用相同的密码(如”123456”),对应的密文也完全相同,攻击者无需破解加密就能识别出最常用的密码。
ECB 唯一合理的用途是加密单个块长度的随机数据——例如用 AES-ECB 加密一个 128 位的随机密钥。在这种特殊情况下,由于输入本身是随机的且恰好一个块,ECB 的确定性不会泄露任何有用的信息。但即便如此,使用其他模式也不会有任何额外的性能开销,因此在实践中没有理由选择 ECB。
三、CBC 模式
原理
密码块链接模式(Cipher Block Chaining,CBC)通过引入块间依赖来消除 ECB 的确定性缺陷。其核心思想是在加密每个明文块之前,先将其与前一个密文块进行异或(XOR)运算:
\[C_0 = IV\] \[C_i = E_K(P_i \oplus C_{i-1}), \quad i = 1, 2, \ldots, t\]
解密过程则是:
\[P_i = D_K(C_i) \oplus C_{i-1}, \quad i = 1, 2, \ldots, t\]
其中 \(IV\)(Initialization Vector,初始化向量)是一个与块等长的随机值,必须在加密前随机生成并与密文一起传输。\(IV\) 不需要保密,但在 CBC 模式中必须是不可预测的(Unpredictable)——这一点比仅仅”不重复”的要求更强,我们稍后会看到预测 IV 带来的攻击。
加密的串行性与解密的并行性
CBC 加密是严格串行的——\(C_i\) 的计算依赖于 \(C_{i-1}\),因此必须从第一个块开始逐块处理。这在需要高吞吐量的场景中是一个显著的性能劣势。
然而,CBC 解密是可以并行化的。观察解密公式 \(P_i = D_K(C_i) \oplus C_{i-1}\),恢复 \(P_i\) 只需要 \(C_i\) 和 \(C_{i-1}\),这两个值在解密开始之前就已经完全已知。因此,所有块的 \(D_K(C_i)\) 可以并行计算,然后各自与 \(C_{i-1}\) 异或即可。
IV 的要求
CBC 模式对 IV 的要求是不可预测性(Unpredictability),而非仅仅唯一性。这一点在历史上曾被忽视,并导致了真实的攻击。
如果 IV 是可预测的(例如使用计数器或时间戳作为 IV),攻击者可以利用这一点来执行选择明文攻击。具体来说,如果攻击者能预测下一次加密使用的 \(IV'\),并且已经观察到之前某次加密的 \(IV\) 和 \(C_1 = E_K(P_1 \oplus IV)\),那么攻击者可以构造一个特殊的明文 \(P' = P_{\text{guess}} \oplus IV' \oplus IV\),使得 \(E_K(P' \oplus IV') = E_K(P_{\text{guess}} \oplus IV)\),从而验证自己对 \(P_1\) 的猜测是否正确。这正是 BEAST 攻击的核心原理。
在实践中,CBC 模式的 IV 应当使用密码学安全的伪随机数生成器(CSPRNG)产生。绝不应使用固定值、计数器或任何可预测的值。
错误传播
CBC 模式具有有限的错误传播特性。如果密文块 \(C_i\) 在传输过程中发生比特翻转:
- \(P_i\) 的恢复将完全错误(因为 \(D_K\) 对损坏的 \(C_i\) 的输出是不可预测的随机值)。
- \(P_{i+1}\) 的对应比特位将被翻转(因为 \(P_{i+1} = D_K(C_{i+1}) \oplus C_i\),\(C_i\) 的比特错误直接通过异或传播到 \(P_{i+1}\))。
- \(P_{i+2}\) 及后续块不受影响。
这种”一块完全错误,下一块对应位翻转”的特性在某些场景下反而成为攻击面——攻击者可以通过精确修改 \(C_i\) 的特定比特来控制 \(P_{i+1}\) 的对应比特,这就是比特翻转攻击(Bit-Flipping Attack)的基础。
四、CBC 的攻击面
CBC 模式的历史是密码学从理论安全走向实际安全的一部生动教材。尽管 CBC 在理论上满足 IND-CPA(前提是 IV 不可预测且使用安全的分组密码),但它在实际部署中暴露出一系列严重的漏洞,这些漏洞几乎都与 CBC 模式本身的结构特性——特别是其与填充方案的交互——有关。
Padding Oracle 攻击(Vaudenay 2002)
2002 年,瑞士密码学家 Serge Vaudenay 在 EUROCRYPT 2002 上发表了一篇里程碑式的论文,揭示了 CBC 模式与填充验证之间的致命交互。这篇论文的核心洞察极为精妙:如果解密系统在填充验证失败时返回与解密成功时不同的错误信息,攻击者可以利用这个信息差来逐字节恢复整个明文,而无需知道密钥。
攻击的原理如下。假设攻击者截获了一段 CBC 密文 \(IV \| C_1\)(为简化,只考虑一个密文块),并且可以向一个”填充预言机”(Padding Oracle)提交任意密文并得知填充是否正确。设 \(I = D_K(C_1)\) 是密文块解密后但尚未异或 IV 之前的中间值,则 \(P_1 = I \oplus IV\)。
攻击者构造一个伪造的 IV’,使得 \(I \oplus IV'\) 的最后一个字节等于 \(\texttt{0x01}\)(PKCS#7 填充中表示”一字节填充”的值)。攻击者不知道 \(I\) 的最后一个字节,因此需要遍历 \(IV'\) 最后一个字节的所有 256 种可能值。当填充预言机报告填充正确时,攻击者知道 \(I_{16} \oplus IV'_{16} = \texttt{0x01}\),从而推算出 \(I_{16} = \texttt{0x01} \oplus IV'_{16}\),进而恢复 \(P_{1,16} = I_{16} \oplus IV_{16}\)。
一旦知道了最后一个字节,攻击者可以继续攻击倒数第二个字节:设置 \(IV'\) 的最后一个字节使 \(I_{16} \oplus IV'_{16} = \texttt{0x02}\),然后遍历倒数第二字节的 256 种可能值,寻找使填充 \(\texttt{0x02 0x02}\) 正确的值。以此类推,每恢复一个字节平均只需 128 次查询,恢复整个 16 字节块只需约 \(128 \times 16 = 2048\) 次查询。
以下是 Padding Oracle 攻击的概念演示:
"""CBC Padding Oracle 攻击概念演示"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding as sym_padding
import os
class PaddingOracle:
"""模拟一个存在填充预言机漏洞的服务端"""
def __init__(self, key: bytes):
self._key = key
def encrypt(self, plaintext: bytes) -> bytes:
"""加密并返回 IV || 密文"""
padder = sym_padding.PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv))
enc = cipher.encryptor()
ct = enc.update(padded) + enc.finalize()
return iv + ct
def check_padding(self, iv_and_ct: bytes) -> bool:
"""
漏洞所在:返回填充是否正确的布尔值。
在真实系统中,这可能表现为不同的 HTTP 状态码、
不同的错误消息、或不同的响应时间。
"""
iv = iv_and_ct[:16]
ct = iv_and_ct[16:]
cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv))
dec = cipher.decryptor()
padded_pt = dec.update(ct) + dec.finalize()
# 验证 PKCS#7 填充
try:
unpadder = sym_padding.PKCS7(128).unpadder()
unpadder.update(padded_pt) + unpadder.finalize()
return True
except ValueError:
return False
def padding_oracle_attack(oracle: PaddingOracle, iv_and_ct: bytes) -> bytes:
"""
利用 Padding Oracle 逐字节恢复明文。
仅演示对单个密文块(IV + 1 block)的攻击。
"""
iv = bytearray(iv_and_ct[:16])
ct = iv_and_ct[16:32] # 只攻击第一个块
intermediate = bytearray(16) # D_K(C_1) 的值
recovered = bytearray(16)
# 从最后一个字节开始逐字节恢复
for byte_index in range(15, -1, -1):
pad_value = 16 - byte_index # 目标填充值
# 设置已知字节的 IV',使解密后的填充值正确
forged_iv = bytearray(16)
for k in range(byte_index + 1, 16):
forged_iv[k] = intermediate[k] ^ pad_value
# 遍历当前字节的所有可能值
for guess in range(256):
forged_iv[byte_index] = guess
test_data = bytes(forged_iv) + bytes(ct)
if oracle.check_padding(test_data):
# 需要排除误报:当 byte_index < 15 时可能有多个有效填充
if byte_index < 15:
# 修改前一个字节来确认
check_iv = bytearray(forged_iv)
check_iv[byte_index - 1] ^= 0x01
if not oracle.check_padding(bytes(check_iv) + bytes(ct)):
continue
intermediate[byte_index] = guess ^ pad_value
recovered[byte_index] = intermediate[byte_index] ^ iv[byte_index]
break
return bytes(recovered)
# 演示
key = os.urandom(16)
oracle = PaddingOracle(key)
secret_message = b"Attack at dawn!" # 15 字节,将被填充为 16 字节
iv_and_ct = oracle.encrypt(secret_message)
print(f"原始消息: {secret_message}")
print(f"密文长度: {len(iv_and_ct)} 字节 (16 IV + 16 密文)")
print()
recovered = padding_oracle_attack(oracle, iv_and_ct)
# 去除 PKCS#7 填充
pad_len = recovered[-1]
if 1 <= pad_len <= 16:
recovered_msg = recovered[:-pad_len]
else:
recovered_msg = recovered
print(f"恢复的消息: {recovered_msg}")
print(f"攻击成功: {recovered_msg == secret_message}")POODLE 攻击(2014)
2014 年 10 月,Google 安全团队发现了 POODLE(Padding Oracle On Downgraded Legacy Encryption)攻击。POODLE 的精妙之处在于它利用了 SSL 3.0 协议中 CBC 填充验证的一个设计缺陷:SSL 3.0 的填充方案只验证最后一个填充字节的值,不检查其他填充字节的内容。
这个看似微小的差异导致了灾难性的后果。在 PKCS#7 填充中,如果填充长度为 \(n\),则最后 \(n\) 个字节都必须是 \(n\);但在 SSL 3.0 中,只有最后一个字节需要是 \(n-1\)(零索引),前面的填充字节可以是任意值。这意味着攻击者有 \(1/256\) 的概率使随机修改的密文通过填充验证。
POODLE 可以逐字节解密 SSL 3.0 保护的通信内容。虽然每解密一个字节平均需要 256 次请求,但在浏览器场景中,攻击者可以利用 JavaScript 发起大量 HTTPS 请求,使攻击在几分钟内完成。POODLE 的发现直接终结了 SSL 3.0——各大浏览器在数周内全部禁用了 SSL 3.0 支持。
BEAST 攻击(2011)
BEAST(Browser Exploit Against SSL/TLS)攻击由 Thai Duong 和 Juliano Rizzo 在 2011 年发表,它利用了 TLS 1.0 中 CBC 模式 IV 处理的一个缺陷。
在 TLS 1.0 及更早版本中,CBC 模式的 IV 不是随机生成的,而是使用上一个记录的最后一个密文块作为下一个记录的 IV。这意味着在发送下一个记录之前,攻击者已经知道将要使用的 IV,从而可以执行前文所述的可预测 IV 攻击。
BEAST 的实际影响是攻击者可以在中间人(Man-in-the-Middle)位置逐字节恢复 HTTPS 会话中的加密内容,例如 HTTP Cookie。这个攻击是 TLS 1.1 将 IV 改为每条记录独立随机生成的直接原因。
比特翻转攻击
比特翻转攻击(Bit-Flipping Attack)利用 CBC 解密的结构特性:修改 \(C_{i-1}\) 的第 \(j\) 个比特会导致 \(P_i\) 的第 \(j\) 个比特被翻转(虽然 \(P_{i-1}\) 会被完全破坏)。如果攻击者知道 \(P_i\) 的部分内容,就可以精确地将已知的字节修改为任意值。
一个经典的例子是修改加密的 HTTP
请求。如果攻击者知道某个加密块中包含
admin=0,可以通过翻转前一个密文块的对应位将其改为
admin=1。虽然前一个明文块会变成乱码,但如果那个块恰好在一个不被检查的字段中,攻击就成功了。
CBC 为何正在被淘汰
上述攻击有一个共同的根源:CBC 模式只提供保密性,不提供完整性。攻击者可以任意修改密文,解密系统无法检测到篡改,而修改后的解密行为(无论是填充错误、应用层错误还是正常处理)向攻击者泄露了关于明文的信息。
这就是为什么现代密码学实践正全面转向认证加密(AEAD)——将保密性和完整性绑定在一起,任何对密文的篡改都会导致认证标签验证失败,从而在解密之前就被拒绝。TLS 1.3 已经完全移除了对 CBC 模式的支持,只保留了 AEAD 模式(AES-GCM 和 ChaCha20-Poly1305)。
五、CTR 模式
原理
计数器模式(Counter Mode,CTR)的设计思路与 CBC 截然不同——它将分组密码变成一个流密码(Stream Cipher)。CTR 不使用分组密码来直接加密明文,而是用分组密码加密一系列递增的计数器值来生成密钥流(Keystream),然后将密钥流与明文异或:
\[O_i = E_K(\text{nonce} \| \text{ctr}_i)\] \[C_i = P_i \oplus O_i\]
解密过程完全相同:
\[P_i = C_i \oplus O_i\]
其中 \(\text{nonce}\) 是一个一次性数值(Number used Once),\(\text{ctr}_i\) 是从某个初始值开始递增的计数器。\(\text{nonce}\) 和 \(\text{ctr}\) 的拼接必须构成 \(n\) 位的块输入(对 AES 而言是 128 位)。常见的划分方式是高 96 位作为 nonce,低 32 位作为计数器,允许单个 nonce 加密最多 \(2^{32}\) 个块(约 64 GB)的数据。
完全并行性
CTR 模式的一个重要优势是加密和解密都可以完全并行化。每个密钥流块 \(O_i = E_K(\text{nonce} \| \text{ctr}_i)\) 的生成完全独立于其他块——给定 nonce 和初始计数器值,任何块的密钥流都可以直接计算,无需先计算之前的块。这使得 CTR 模式在多核处理器和硬件加速器上可以达到极高的性能。
CTR 模式还天然支持随机访问——要解密第 \(i\) 个块,只需计算 \(E_K(\text{nonce} \| (ctr_0 + i))\) 然后异或密文块即可,无需处理之前的任何块。这在磁盘加密等需要随机访问的场景中非常有价值。
此外,CTR 模式不需要分组密码的解密函数 \(D_K\)。加密和解密使用完全相同的操作——生成密钥流然后异或。这意味着实现者只需实现分组密码的加密方向,可以减少代码量和硬件面积。对于像 AES 这样加密方向有硬件指令支持(如 Intel AES-NI)的密码,这也意味着解密可以利用同样的硬件加速。
Nonce 管理:CTR 的阿喀琉斯之踵
CTR 模式的安全性完全依赖于一个绝对要求:同一密钥下,nonce 绝不能重复。如果两段密文 \(C\) 和 \(C'\) 使用了相同的密钥和 nonce,攻击者可以计算:
\[C_i \oplus C'_i = (P_i \oplus O_i) \oplus (P'_i \oplus O_i) = P_i \oplus P'_i\]
密钥流 \(O_i\) 被抵消,攻击者直接得到两段明文的异或——这就是经典的”两次本”(Two-Time Pad)问题。如果明文具有足够的冗余(如自然语言文本或结构化数据),攻击者可以利用统计分析或已知明文技术恢复双方的完整明文。
nonce 重复的灾难在历史上多次发生。最臭名昭著的例子是 Sony 的 PlayStation 3(PS3)代码签名系统:它使用 ECDSA 签名算法(内部使用类似 CTR 的 nonce 机制),但错误地对所有签名使用了相同的 nonce,导致私钥被 fail0verflow 团队直接计算出来,使得任何人都可以签署和运行自制软件。虽然这是签名而非加密的场景,但根本原因相同——nonce 重复导致秘密信息泄露。
在实践中,nonce 管理策略通常有两种:一是使用全局计数器(确保单调递增,但需要可靠的持久存储和多节点间的协调);二是使用足够长的随机 nonce(如 96 位随机值,在 \(2^{48}\) 次加密以内碰撞概率可忽略——这基于生日攻击界)。
以下是 CTR 模式的实现演示:
"""CTR 模式实现演示"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import os
import struct
def ctr_encrypt_manual(key: bytes, nonce: bytes, plaintext: bytes) -> bytes:
"""
手动实现 CTR 模式,展示其内部工作原理。
nonce: 12 字节 (96 位)
计数器: 4 字节 (32 位),从 1 开始
"""
assert len(nonce) == 12, "nonce 必须是 12 字节"
block_size = 16
ciphertext = bytearray()
# 逐块生成密钥流并异或
num_blocks = (len(plaintext) + block_size - 1) // block_size
for i in range(num_blocks):
# 构造计数器块:nonce (12 字节) || counter (4 字节,大端序)
counter_block = nonce + struct.pack('>I', i + 1)
# 用 AES-ECB 加密计数器块生成密钥流块
cipher = Cipher(algorithms.AES(key), modes.ECB())
enc = cipher.encryptor()
keystream_block = enc.update(counter_block) + enc.finalize()
# 提取当前明文块
start = i * block_size
end = min(start + block_size, len(plaintext))
pt_block = plaintext[start:end]
# 异或生成密文(最后一块可能不满 16 字节,无需填充)
for j in range(len(pt_block)):
ciphertext.append(pt_block[j] ^ keystream_block[j])
return bytes(ciphertext)
def ctr_decrypt_manual(key: bytes, nonce: bytes, ciphertext: bytes) -> bytes:
"""CTR 解密与加密完全相同"""
return ctr_encrypt_manual(key, nonce, ciphertext)
# 演示
key = os.urandom(16)
nonce = os.urandom(12)
message = "分组密码的工作模式是密码系统设计的关键组成部分。".encode('utf-8')
print(f"明文: {message.decode('utf-8')}")
print(f"明文长度: {len(message)} 字节")
print()
# 手动实现
ct_manual = ctr_encrypt_manual(key, nonce, message)
pt_manual = ctr_decrypt_manual(key, nonce, ct_manual)
print(f"手动 CTR 加密后: {ct_manual[:32].hex()}...")
print(f"手动 CTR 解密后: {pt_manual.decode('utf-8')}")
print()
# 使用标准库验证
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce + b'\x00\x00\x00\x01'))
enc = cipher.encryptor()
ct_lib = enc.update(message) + enc.finalize()
print(f"标准库 CTR 加密: {ct_lib[:32].hex()}...")
print(f"两种实现一致: {ct_manual == ct_lib}")
print()
# 演示 nonce 重用的灾难
message2 = "这是另一条使用相同 nonce 加密的消息——大忌!".encode('utf-8')
ct1 = ctr_encrypt_manual(key, nonce, message)
ct2 = ctr_encrypt_manual(key, nonce, message2)
# 攻击者计算两段密文的异或
min_len = min(len(ct1), len(ct2))
xor_result = bytes(a ^ b for a, b in zip(ct1[:min_len], ct2[:min_len]))
# 这等于两段明文的异或——密钥流被完全抵消
pt_xor = bytes(a ^ b for a, b in zip(message[:min_len], message2[:min_len]))
print(f"ct1 XOR ct2 == pt1 XOR pt2: {xor_result == pt_xor}")
print("nonce 重用使密钥流被抵消,攻击者可恢复明文!")CTR 的安全性边界
CTR 模式的安全性存在一个基于生日界(Birthday Bound)的根本限制。当使用同一密钥加密约 \(2^{n/2}\) 个块时(对 AES 而言是 \(2^{64}\) 个块,约 256 EB),不同的计数器输入产生相同输出的概率开始显著上升——这本质上是因为真正的随机函数和伪随机置换在输出大量样本后可以被区分。
在实践中,这意味着单个 AES 密钥在 CTR 模式下不应加密超过约 \(2^{64}\) 个块的数据。对于大多数应用场景这个限制绰绰有余,但对于需要处理海量数据的场景(如大规模数据库加密),密钥轮换策略是必要的。
六、OFB 与 CFB 模式
OFB 模式
输出反馈模式(Output Feedback Mode,OFB)是另一种将分组密码转化为流密码的工作模式。它的密钥流生成方式与 CTR 不同——不是加密递增的计数器,而是迭代地加密前一个密钥流块:
\[O_0 = IV\] \[O_i = E_K(O_{i-1}), \quad i = 1, 2, \ldots, t\] \[C_i = P_i \oplus O_i\]
密钥流的生成是一个确定性的迭代过程,完全由密钥 \(K\) 和初始化向量 \(IV\) 决定。由于密钥流的生成不依赖于明文或密文,它可以在明文到达之前预先计算——这在某些硬件实现中是有用的特性。
OFB 模式与 CTR 模式共享许多特性:加密和解密相同(都是密钥流与数据异或)、不需要分组密码的解密函数、最后一块不需要填充。然而,OFB 有一个关键劣势——密钥流的生成是严格串行的,\(O_i\) 依赖于 \(O_{i-1}\),因此无法并行化。
OFB 模式的 IV 要求是唯一性——同一密钥下不应重复使用相同的 IV。与 CBC 不同,OFB 不要求 IV 不可预测。但是,IV 重用在 OFB 中同样会导致灾难性的后果——与 CTR 的 nonce 重用完全相同,两段密文的异或会暴露两段明文的异或。
OFB 模式还有一个微妙的周期性(Periodicity)问题。由于分组密码是一个置换,密钥流序列 \(O_1, O_2, \ldots\) 最终必然进入循环。对于 \(n\) 位块,循环的期望长度约为 \(2^{n-1}\)(对 AES 为 \(2^{127}\)),在实践中不太可能成为问题。但在理论分析中,CTR 模式不存在这个问题,因此通常被认为优于 OFB。
CFB 模式
密码反馈模式(Cipher Feedback Mode,CFB)与 OFB 类似,但密钥流的生成取决于密文而非前一个密钥流块:
\[O_i = E_K(C_{i-1}), \quad C_0 = IV\] \[C_i = P_i \oplus O_i\]
解密过程为:
\[O_i = E_K(C_{i-1})\] \[P_i = C_i \oplus O_i\]
CFB 的加密是串行的(\(C_i\) 依赖于 \(C_{i-1}\)),但解密可以并行化——与 CBC 类似,解密只需要当前密文块和前一个密文块,两者在解密开始前都已知。
CFB 的自同步特性
CFB 模式最独特的属性是自同步(Self-Synchronizing)特性。如果密文流中丢失或插入了若干字节,接收方会经历短暂的同步丢失期(持续一个块长度),之后自动恢复同步并正确解密后续内容。这是因为 CFB 的密钥流只依赖于当前密文块之前的一个块——一旦接收方缓冲了一个完整的正确密文块,后续的解密就会自动回到正轨。
这个特性在某些通信场景中非常有价值,例如在不可靠的信道上传输数据时,偶尔的字节丢失不会导致整个后续通信不可读。然而在现代应用中,传输层协议(如 TCP)已经保证了可靠的字节流传输,CFB 的自同步特性的实用价值大大降低。
错误传播对比
三种流密码模式(CTR、OFB、CFB)对密文错误的反应各有不同:
- CTR:密文的一个比特错误只影响对应明文的一个比特。不多也不少,因为密文和密钥流是逐位异或的关系。
- OFB:与 CTR 相同——一个密文比特错误只影响对应的一个明文比特。这是因为密钥流的生成完全独立于密文。
- CFB:一个密文比特错误会影响两个区域——当前块中对应的一个比特(通过异或),以及下一个完整块的所有比特(因为损坏的密文块会被送入 \(E_K\) 产生一个不正确的密钥流块)。这与 CBC 的错误传播模式非常相似。
需要强调的是,上述错误传播特性不能被用作完整性保护的替代品。能够检测到错误和能够安全地抵抗主动篡改是完全不同的安全目标——后者需要消息认证码(MAC)或认证加密模式。
七、填充方案
为什么需要填充
ECB、CBC 和某些变体模式要求明文长度是块大小的整数倍。当明文不满足这个条件时,需要在最后一个块中添加额外的字节使其达到块大小——这就是填充(Padding)。填充看起来是一个微不足道的实现细节,但正如 Padding Oracle 攻击所展示的,它可以成为严重安全漏洞的根源。
PKCS#7
PKCS#7 填充(也常被称为 PKCS#5 填充,后者实际上是 PKCS#7 的 8 字节块特化版本)是最广泛使用的填充方案。其规则极为简单:如果需要填充 \(n\) 个字节(\(1 \le n \le \text{block\_size}\)),则所有填充字节的值都是 \(n\)。
例如,对于 16 字节的块大小:
- 需要 1 字节填充:
... 01 - 需要 2 字节填充:
... 02 02 - 需要 5 字节填充:
... 05 05 05 05 05 - 如果明文恰好是块大小的整数倍,则增加一个完整的填充块:
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
最后一条规则(总是添加填充,即使明文长度已经对齐)确保了解填充的无歧义性——接收方总是可以通过检查最后一个字节的值来确定填充长度。
PKCS#7 的验证规则是:读取最后一个字节的值 \(v\),检查最后 \(v\) 个字节是否都等于 \(v\),且 \(1 \le v \le \text{block\_size}\)。任何不符合此规则的情况都是无效填充。正是这个验证过程成为了 Padding Oracle 攻击的切入点。
ISO/IEC 7816-4
ISO/IEC 7816-4 填充(也称为”位填充”或”ISO
填充”)使用不同的策略:首先在明文末尾添加一个
0x80 字节(即二进制
10000000),然后用零字节填充到块大小。
例如:... 80 00 00 00 00
ISO
填充的优势是概念更简单——去填充时只需从末尾向前扫描跳过零字节,然后移除
0x80
字节。它同样总是至少添加一个字节(0x80),即使明文已经对齐。
零填充
零填充(Zero Padding)是最简单的方案——用零字节填充到块大小。然而,零填充有一个致命缺陷:它无法区分原始明文末尾的零字节和填充的零字节。如果明文本身以一个或多个零字节结尾,去填充会错误地移除这些字节。因此零填充只能在明文长度已知(或通过其他机制传递)的情况下使用,不适合通用场景。
密文窃取(Ciphertext Stealing)
密文窃取(Ciphertext Stealing,CTS)是一种巧妙的技术,可以在不增加密文长度的情况下处理不完整的最后一块。其核心思想是将倒数第二个密文块的部分内容”借”给最后一块。
以 CBC-CTS 为例,假设最后一个明文块 \(P_t\) 只有 \(m\) 字节(\(m < n\)):
- 正常加密 \(P_{t-1}\) 得到完整的密文块 \(C'_{t-1}\)。
- 取 \(C'_{t-1}\) 的前 \(m\) 字节,将 \(P_t\) 用这 \(m\) 字节填充到完整块后进行加密,得到 \(C_{t-1}\)。
- 输出的密文是 \(C_1, \ldots, C_{t-2}, C_{t-1}, C'_{t-1}[0:m]\)。
CTS 的优势是密文长度与明文长度完全相同(不包括 IV),没有任何膨胀。这在存储空间敏感的场景(如磁盘扇区加密)中非常有价值。XTS-AES(IEEE 1619 标准的磁盘加密模式)就使用了密文窃取技术。
填充为何是漏洞的温床
填充之所以危险,根本原因在于它在解密过程中引入了一个额外的验证步骤——去填充和填充正确性检查。这个验证步骤可以通过多种渠道向攻击者泄露信息:
- 显式错误消息:服务端返回”填充无效”和”MAC 验证失败”两种不同的错误。
- 时间差异:填充检查失败立即返回,而 MAC 验证失败需要先计算 MAC,两者的响应时间不同。
- 行为差异:填充有效时继续处理请求(可能触发应用层错误),填充无效时立即拒绝。
对策是”先认证后解密”(Authenticate-then-Decrypt)或更好的”加密并认证”(Encrypt-then-MAC / AEAD)——在尝试解密和去填充之前先验证密文的完整性标签。如果标签验证失败,直接拒绝,不进行任何解密操作。CTR 模式天然不需要填充——这是它相对于 CBC 的又一个优势。
八、安全性分析与 IND-CPA
IND-CPA 的定义
选择明文攻击下的不可区分性(Indistinguishability under Chosen-Plaintext Attack,IND-CPA)是对称加密方案最基本的安全定义。它通过一个”安全游戏”(Security Game)来形式化:
- 挑战者(Challenger)随机生成密钥 \(K\)。
- 攻击者(Adversary)可以请求挑战者加密任意数量的自选明文(这模拟了攻击者在实际场景中可能获得的加密能力)。
- 攻击者提交两条等长的消息 \(M_0\) 和 \(M_1\)。
- 挑战者随机选择 \(b \in \{0, 1\}\),加密 \(M_b\) 并将密文返回给攻击者。
- 攻击者可以继续请求加密其他明文。
- 攻击者输出对 \(b\) 的猜测 \(b'\)。
如果对于任何计算上可行(Computationally Feasible)的攻击者,\(\Pr[b' = b] - 1/2\) 的优势都可以忽略不计(Negligible),则该加密方案是 IND-CPA 安全的。
直觉上,IND-CPA 安全意味着密文不会泄露关于明文的任何有用信息——攻击者即使可以获得任意明文的加密,也无法区分两条消息中哪一条被加密了。
为什么 ECB 不满足 IND-CPA
ECB 模式不满足 IND-CPA 的证明非常简单。攻击者选择 \(M_0 = P \| P\)(两个相同的块)和 \(M_1 = P \| P'\)(两个不同的块),其中 \(P \ne P'\)。收到挑战密文 \(C_1 \| C_2\) 后,攻击者只需检查 \(C_1\) 是否等于 \(C_2\):如果相等,输出 \(b' = 0\);否则输出 \(b' = 1\)。这个攻击的优势为 1(完美区分),远非可忽略。
CTR 模式的 IND-CPA 安全性
CTR 模式的 IND-CPA 安全性可以归约到底层分组密码的伪随机置换(PRP)安全性。证明的核心思路如下:
定理:如果 \(E\) 是一个安全的伪随机置换(PRP),则 CTR 模式在使用不重复 nonce 的条件下是 IND-CPA 安全的。
证明思路:
第一步(从 PRP 到随机函数):假设 \(E_K\) 被替换为一个真正的随机置换 \(\pi\),即密钥流块变为 \(\pi(\text{nonce} \| i)\)。由于 nonce 不重复,所有对 \(\pi\) 的查询输入都不同。对于一个随机置换,不同的输入给出的输出是均匀随机且互不相同的。
第二步(安全性论证):当密钥流是真正的随机字节时,CTR 加密的效果等同于一次性密码本(One-Time Pad)——密文 \(C_i = P_i \oplus O_i\) 中每个 \(O_i\) 都是均匀随机的,因此密文的分布独立于明文。攻击者无法从密文中获得关于明文的任何信息,其 IND-CPA 优势为零。
第三步(归约):如果存在一个能以不可忽略的优势攻破 CTR 模式 IND-CPA 安全性的攻击者 \(\mathcal{A}\),那么可以构造一个区分器 \(\mathcal{D}\),利用 \(\mathcal{A}\) 来区分 \(E_K\) 和真正的随机置换。\(\mathcal{D}\) 模拟 CTR 模式的加密过程,将对分组密码的调用替换为对其预言机的查询。如果预言机是 \(E_K\),\(\mathcal{A}\) 看到的是真实的 CTR 密文;如果预言机是随机置换,\(\mathcal{A}\) 看到的是一次性密码本加密的结果。\(\mathcal{A}\) 的优势直接转化为 \(\mathcal{D}\) 区分 PRP 和随机置换的优势。
生日界限制
上述证明中隐藏了一个重要的细节——PRP/PRF 转换引理(PRP/PRF Switching Lemma)。随机置换和随机函数在查询次数 \(q\) 较小时几乎不可区分,但当 \(q\) 接近 \(2^{n/2}\) 时(其中 \(n\) 是块大小),区分优势开始显著增长(约为 \(q^2 / 2^n\))。
这个生日界(Birthday Bound)限制意味着:使用 128 位块大小的 AES,CTR 模式在同一密钥下加密约 \(2^{64}\) 个块后,安全性开始退化。具体来说,IND-CPA 的安全余量会降低到可能不够安全的水平。这被称为”数据限制”(Data Limit),是所有基于 128 位分组密码的模式共有的约束。
对于 64 位块大小的分组密码(如 3DES 的 TDEA),这个限制更加紧迫——约 \(2^{32}\) 个块(约 32 GB)后安全性就开始退化。Sweet32 攻击(2016)正是利用了这一点来攻击仍在使用 3DES 的 TLS 连接。
CBC 模式的 IND-CPA 安全性
CBC 模式同样可以被证明为 IND-CPA 安全的,前提是 IV 是均匀随机且不可预测的。证明的思路与 CTR 类似——先将分组密码替换为随机置换,然后论证密文分布与随机值不可区分。但 CBC 的证明更加复杂,因为加密过程的链式结构使得分析更加精细。
CBC 的安全界同样受生日攻击限制,但具体的界比 CTR 更紧一些。这也是为什么 NIST 建议 CBC-AES 在同一密钥下加密不超过 \(2^{48}\) 个块——比 CTR 的建议更保守。
九、模式选择指南
综合对比
下表总结了五种经典工作模式的关键特性对比:
| 特性 | ECB | CBC | CTR | OFB | CFB |
|---|---|---|---|---|---|
| 加密并行化 | 是 | 否 | 是 | 否 | 否 |
| 解密并行化 | 是 | 是 | 是 | 否 | 是 |
| 随机访问 | 是 | 否 | 是 | 否 | 否 |
| 需要填充 | 是 | 是 | 否 | 否 | 否 |
| IV/Nonce 要求 | 无 | 随机且不可预测 | 唯一 | 唯一 | 唯一且不可预测 |
| IND-CPA 安全 | 否 | 是 | 是 | 是 | 是 |
| 密文膨胀 | 填充 | IV + 填充 | Nonce | IV | IV |
| 需要解密函数 | 是 | 是 | 否 | 否 | 否 |
| 错误传播 | 仅当前块 | 当前块损坏 + 下一块位翻转 | 仅对应位 | 仅对应位 | 对应位 + 下一块全损 |
模式选型速查表
为方便工程师在实际项目中快速决策,下表从更偏实用的维度对五种经典模式进行速查汇总:
| 模式 | 加密可并行? | 随机访问? | 错误传播范围 | 推荐使用场景 |
|---|---|---|---|---|
| ECB | 是 | 是 | 仅当前块 | 几乎不应使用。唯一合理场景:加密单个独立块(如密钥包装的内部步骤) |
| CBC | 否 | 仅解密 | 当前块 + 下一块 1 位翻转 | 遗留系统兼容;必须配合 MAC(Encrypt-then-MAC);IV 须密码学随机 |
| CTR | 是 | 是 | 仅对应位 | 现代首选。GCM/CCM/SIV 的底层基础;nonce 必须全局唯一 |
| OFB | 否 | 否 | 仅对应位 | 需要流式加密且无法使用 CTR 时的备选;IV 须唯一 |
| CFB | 否 | 否 | 对应位 + 下一块全损 | 需要自同步特性的特殊场景;IV 须唯一且不可预测 |
笔者认为,CTR 模式的简洁性是一种具有欺骗性的简洁。从数学上看,CTR 模式不过是「对计数器加密后异或明文」,几行伪代码即可实现——但正是这种极简设计将全部安全责任转嫁给了一个看似微小的要求:nonce 绝不能重复。在 GCM 中,一次 nonce 重用不仅泄露明文的异或(与二次密码本问题相同),还会立即暴露 GHASH 的认证密钥,使攻击者可以伪造任意消息。现实中,nonce 管理失败导致的安全事故远比任何模式设计缺陷更为频繁——从 2016 年的 KRACK 攻击(WPA2 协议中 nonce 被强制重置)到各种分布式系统中计数器回绕或状态丢失,CTR/GCM 的脆弱点几乎总是在 nonce 上。一个值得深思的现象是:我们用更简单的模式替代了 CBC 的复杂性,却只是将复杂性从算法层面推移到了运维层面——nonce 的全局唯一性保证在分布式、多节点、可能崩溃重启的现实环境中,本身就是一个非平凡的工程问题。这也是 SIV 模式(Synthetic IV)和 XChaCha20(192-bit nonce)等设计出现的根本动力。
一个经常被忽视的观点是,如此众多的分组密码工作模式(ECB、CBC、CTR、OFB、CFB、GCM、CCM、SIV……)的存在本身就是一种设计失败的标志。它意味着密码学社区在三十多年间从未找到一种在所有场景下都明确最优的模式,因而不得不将选择权——以及犯错的机会——交给每一个应用开发者。一个不了解密码学的工程师面对这些缩写,可能随机选择
CBC(因为它最常出现在教科书中),却不知道自己已经打开了填充预言机攻击的大门。现代共识”只用
AEAD”(GCM 或 ChaCha20-Poly1305)花了大约三十年才形成——从
1976 年 CBC 的提出到 2007 年 GCM 的标准化,再到 2018 年 TLS
1.3 将非 AEAD
模式全部移除。在这个窗口期内发生了多少因模式误选导致的漏洞,恐怕无人能完整统计。密码学
API
设计的终极目标不应该是”提供所有选项让开发者自行判断”,而应该是”只暴露一个正确的选项”——libsodium
的 crypto_aead_xchacha20poly1305_ietf
正是这种哲学的体现。
现代实践:为什么 CTR 胜出
从上表可以清楚地看到,CTR 模式在几乎所有维度上都优于其他经典模式:完全并行化、支持随机访问、不需要填充、不需要解密函数、IND-CPA 安全。CTR 唯一的”劣势”——对 nonce 唯一性的严格要求——在实践中可以通过合理的 nonce 管理策略来满足。
这就是为什么几乎所有现代认证加密模式都建立在 CTR 之上:
- GCM(Galois/Counter Mode):CTR 加密 + GHASH 认证,是 TLS 1.2 和 1.3 中最广泛使用的密码套件。
- CCM(Counter with CBC-MAC):CTR 加密 + CBC-MAC 认证,广泛用于 Wi-Fi(WPA2/WPA3)和蓝牙。
- SIV(Synthetic IV):CTR 加密 + PRF 派生 IV,提供 nonce 误用抵抗(Nonce Misuse Resistance)。
- ChaCha20-Poly1305:ChaCha20 流密码(本质上是一种广义的 CTR 模式)+ Poly1305 认证,是 GCM 的主要替代方案。
从保密到认证加密的范式转变
五种经典工作模式都只提供保密性(Confidentiality),不提供完整性(Integrity)或真实性(Authenticity)。在真实的密码系统中,仅有保密性是远远不够的——CBC 的攻击历史已经充分证明了这一点。
现代密码学的共识是:永远不要单独使用加密模式,始终使用认证加密(Authenticated Encryption with Associated Data,AEAD)。AEAD 将保密性和完整性绑定在一个不可分割的操作中,确保任何对密文的篡改都会被检测到。
如果由于遗留系统的限制必须使用 CBC 等非 AEAD 模式,则必须在加密之上添加独立的 MAC(如 HMAC),并且必须采用”先加密后认证”(Encrypt-then-MAC)的组合顺序——先计算密文的 MAC,验证 MAC 通过后才进行解密。“先认证后加密”(MAC-then-Encrypt)和”同时认证和加密”(Encrypt-and-MAC)这两种替代方案在理论上都不如 Encrypt-then-MAC 安全,已知导致过实际的攻击。
密钥轮换与数据限制
无论选择哪种模式,单个密钥的使用都有数据量上限。这个上限由生日界决定:
- AES-128/256(128 位块):约 \(2^{64}\) 个块 ≈ 256 EB。在 GCM 模式下,NIST 建议更保守的 \(2^{32}\) 个块(约 64 GB)的限制,因为 GCM 的安全性退化在接近生日界时比纯 CTR 更快。
- 3DES(64 位块):约 \(2^{32}\) 个块 ≈ 32 GB。在实际中,Sweet32 攻击表明即使在这个限制之内也可能不够安全。
密钥轮换(Key Rotation)是应对数据限制的标准做法——定期或在加密一定量数据后更换密钥。TLS 1.3 通过 KeyUpdate 消息支持在会话中途轮换密钥,正是为了应对这个问题。
选择总结
对于新系统的设计,选择建议非常简单:
- 首选 AEAD 模式:AES-256-GCM 或 ChaCha20-Poly1305,取决于硬件是否支持 AES-NI。
- 如果需要 nonce 误用抵抗:使用 AES-GCM-SIV 或 AES-SIV。
- 如果必须使用非 AEAD 模式:选择 CTR + HMAC(Encrypt-then-MAC),绝不使用 ECB。
- 如果必须使用 CBC(遗留系统):确保先验证 MAC 再解密,使用常量时间(Constant-Time)的填充检查,IV 必须密码学随机。
分组密码的工作模式是连接理论密码学与工程实践的关键桥梁。从 ECB 的直观但不安全,到 CBC 的广泛部署但攻击频发,再到 CTR 的优雅高效和 AEAD 的全面保护——这段演进史告诉我们,密码学中的魔鬼往往不在算法本身,而在于算法被组合和使用的方式。理解工作模式,是理解现代密码系统安全性的必经之路。
密码学百科系列 · 第 08 篇