4. Prefix Encryption 方法

Prefix Encryption,说白了就是把整个域拆成若干个小块,每个小块单独加密,然后像搭积木一样拼回去。我在做金融卡号加密那会儿,就靠它来处理可变长度的 PAN 数据。

4.1 基本思想

你想想看,常规的 FPE 算法(比如 FF1)虽然好用,但每次都要把整个明文当作一个整体来构造 Feistel 网络。如果明文很长,计算开销会直线上升。Prefix Encryption 的想法很简单:

  • 把整个消息分成若干固定长度的“前缀” + 末尾可能较短的“尾巴”;
  • 对每一个前缀分别加密,加密时带上已经加密的前面部分作为 tweak(或者叫链式向量);
  • 最后一节(尾巴)如果长度不够,就用前一个加密结果截取或填充。

本质上是一种“链式加密”思路。我记得最早看到它在磁盘加密领域(比如 XTS 模式)的影子,后来被移植到 FPE 场景。这样做的好处是:加密时只需要在固定长度上做 FE(Feistel)回合,不需要为变长域单独设计算法。

4.2 实现步骤

我习惯按下面几步来写 Prefix Encryption 的实现。假设我们要对 plaintext(由字符集为 Alphabet)加密,每次加密固定块大小 block_len。

  1. 划分块:将明文从前往后切成若干块,每块长度 block_len,最后一块可能不足。
  2. 初始化向量:使用一个全 0 或密钥衍生的初始向量 IV。
  3. 逐个加密:对于第 i 块,取上一块的密文(或 IV 作为第一个 tweak)与当前块拼接作为 FE 输入,用密钥 K 加密得到当前块密文。
  4. 处理最后一块:如果最后一块长度 < block_len,则从上一个密文中截取相应位数,用 XOR 或模运算掩码调整。
  5. 拼接输出:将所有密文块按顺序拼接,得到最终密文。

下面是一个用 Python 写的简单示意(基于 FF1 作为底层 FE):

from fpe import FF1
import math

def prefix_encrypt(key, plaintext, alphabet, block_len=8):
    # 划分块
    n = len(plaintext)
    blocks = [plaintext[i:i+block_len] for i in range(0, n, block_len)]
    cipher_blocks = []
    prev_cipher = "0" * block_len  # 初始向量
    ff1 = FF1(key, alphabet)

    for i, blk in enumerate(blocks):
        # 构造 tweak:前一块密文(或 IV)
        tweak = prev_cipher
        # 对当前块加密:输入 = tweak + blk
        inp = tweak + blk
        out = ff1.encrypt(inp)
        # 取前半作为当前块密文
        cipher_blk = out[:len(blk)]
        cipher_blocks.append(cipher_blk)
        prev_cipher = cipher_blk

    return ''.join(cipher_blocks)
⚠ 坑我踩过: 千万别把最后一块的 tweak 搞错。如果最后一块长度小于 block_len,有些实现会直接把整个 tweak 扔掉,只加密最后一块+部分tweak。这样会导致解密时前后不匹配。我建议统一用前一块密文的全长,最后一块加密时输入长度对齐即可。

4.3 优缺点分析

先聊优点:

优点 说明
计算效率高 每个块长度固定,FE 轮次恒定。尤其适合硬件实现或流式加密。
支持任意长度 不用像 FF1 那样必须规定最小长度(通常国际标准要求 2 字符以上)。Prefix 可以处理 1 字符、3 字符等。
可并行 如果采用独立 tweak(类似 CTR 模式),每个块可并行加密。我团队曾用 GPU 加速实现,吞吐量翻了 4 倍。

再看缺点:

缺点 说明
链式依赖 如果某个块加密出错,后面所有块都废了。而且解密时也依赖顺序,不能随机访问。
安全性稍弱 因为前缀是直接暴露在前一块密文上的,如果某一块泄露了 tweak,攻击者可以利用已知明文攻击(KPA)推部分密钥信息。相比 FF1 的全局统一 Feistel,风险略高。
最后一块处理麻烦 不同实现方式(截断、padding、XOR)会导致兼容性问题。我见过两个系统因为最后一块处理方式不同而互不解密。

4.4 适用场景

说实话,Prefix Encryption 非常适合以下场景:

  • 流式数据:比如实时语音或视频加密,不需要等整条消息到齐。
  • 弱实时性要求高的系统:比如 IoT 设备上处理短报文,每次只加密几十个字符。
  • 金融交易日志:日志条目很长,但每条内部结构一致(固定字段)。Prefix 可以只对关键字段加密,其它字段保持明文。
💡 我的经验: 如果明文长度很短(比如 1-4 字符),不要用 Prefix。因为块太小,FE 回合数没法做够,安全性会下降。这时候我更推荐用 FF1/FF3 直接整个加密。

4.5 Prefix Encryption 核心流程

我画了一张流程图,方便你理解整个过程。注意看数据流和 tweak 的传导关系。

Prefix Encryption 加密流程 明文 M 划分块 块1 块2 块3 块4(尾) IV (000…) FE加密(块1) FE加密(块2) FE加密(块3) FE加密(块4) 密文 C 明文 划分 IV 加密块 密文

4.6 安全性建议

我个人总结的安全三条红线:

  • 永远不要重用相同的 IV 加密相同的明文前缀(否则会有 ECB 的类问题)。
  • 块长度至少 8 个字符(对于数字字母集),否则 FE 内部轮数不够。
  • 最后一块处理必须明确约定 padding 方式,建议用 ISO 10126 或 PKCS7 种可逆方案。

嗯,Prefix Encryption 就这些。它在 FPE 工具箱里算是一把“快刀”,但要注意别切到自己。我当年在一个支付系统里误用了固定 IV,结果两个相同卡号的密文竟然一毛一样…… 从那以后,IV 生成逻辑我再也不敢偷懒了。