FF1 算法详解:参数、步骤与安全约束归纳总结

FF1 是目前应用最广泛的格式保持加密算法,没有之一。我最早接触它是在一个信用卡号脱敏项目里,需求是「加密后长度不变、字符集不变,还要能反向解密」。当时调研了一圈,最后选了 NIST SP 800-38G 里定义的 FF1。今天咱们就把它拆开揉碎了讲——参数、加密步骤、还有那些容易踩坑的安全约束。

1. 核心参数:你得先认清这五个

FF1 的参数长得有点啰嗦,但每个都不能少。我在一次方案评审里见过有人把 tweak 长度设成 0,结果合规检查直接不通过。下面这张表帮你理清脉络:

参数 类型 说明 我踩过的坑
radix 整数 (≥2) 字符集基。数字用10,字母用26,字母数字混合用36 基设错会导致格式校验失败,我记得一个短信验证码项目用了16进制却设了radix=10,解密后全是乱码
密钥K 字节串,128/192/256位 AES 密钥。长度决定了安全强度 千万别用随机数生成替代密钥派生,我见过有人直接把用户密码当K传进去,立马被安全审计打回
tweak T 字节串 (0..2^64-1 字节) 类似于 IV,提供上下文隔离 长度不够时用 0x00 补齐,但 tweak 本身可为空——不过我个人建议永远别用空 tweak,否则同一密钥加密相同明文会得出相同密文,数据泄露风险极大
minlen 整数 (≥2) 最小明文长度。通常设为2 有次生产上明文长度为1,FF1直接抛异常,因为算法要求 n ≥ minlen
maxlen 整数 (≤2^32-1) 最大明文长度。受内存和性能限制 处理超长字段时(比如身份证号18位)没问题,但若达到几百上千字符,每个分组迭代会变慢,建议分段处理
关键点: radix、tweak 和 minlen 必须事前约定好,解密端一旦传错,结果全部不可逆。我通常会在配置中心用 JSON Schema 把这三个参数固化下来,上线前做 100 组正向+逆向测试。

2. 加密步骤:一个迭代的 Feistel 结构

FF1 本质上是一个经过调整的 Feistel 网络。你想想看,一个 Feistel 结构把明文分成左右两半,一轮一轮地做非线性变换——FF1 在此基础上加了 tweak 和 radix 的适配。步骤拆解如下:

  1. 分拆明文:将长度为 n 的明文(数字串或字符串)拆成左半 A(长度 u = floor(n/2))和右半 B(长度 v = n - u)。
  2. 计算中间值:用 PRF(key, tweak + A_integer) 生成一个伪随机数流,转成 radix 进制表示,长度等于 v。
  3. 更新右半:将上一步的伪随机数串与 B 按位模加(radix 下的加法),得到新的 B'。
  4. 交换左右:将 A 和 B' 交换,作为下一轮的输入。
  5. 重复 10 轮:FF1 固定迭代 10 轮。每轮使用不同的 round counter 参与 PRF 计算。
  6. 最终输出:将最后一轮的 A 和 B 拼接成加密后的字符串。

解密步骤刚好反过来——轮函数用同样的 PRF,但加减法互换。说白了就是把加密流程倒着跑一遍。我曾在一个安全 SDK 里实现过两端自动切换,核心逻辑不超过 30 行。

我的开发习惯: 在代码注释里用“S1 → S2 → ... → S6”标出以上六个阶段,这样同事 review 时一眼就能看出哪段对应哪个 Feistel 轮次。

3. 安全约束:这些红线不能碰

FF1 虽然经过了标准化,但使用不当照样会翻车。我记得有一篇专门指出了 FF1 在特定 radix 和 tweak 组合下的攻击面。下面是几条必须遵守的约束:

  • 最小明文长度 ≥ 2:算法内部依赖左右拆分,如果 n=1 则无法执行 Feistel 结构。NIST 标准直接规定 minlen 至少为 2。
  • tweak 应具备唯一性:在同一个密钥下,尽量保证每个密文使用不同的 tweak。例如可以用记录 ID + 时间戳拼接。我曾经遇到过线上一个缓存键冲突,就是因为多个加密记录用了同一个静默 tweak(全是空字节),蜜文全部重复,排查了很久才发现。
  • 禁止直接使用明文作为 tweak:这样做会破坏 FPE 的安全性基础,攻击者很容易构造选择明文攻击。
  • Radix 上限 2^16:官方建议 radix ≤ 65536,否则内部算术运算可能溢出或性能骤降。一般场景下我们只用 10/16/36/62 这几个常用值。
  • 密钥周期管理:FF1 的加密速度较慢(比 AES-GCM 慢一个数量级),所以密钥复用次数需要控制。我建议单个密钥加密的记录数不超过 10^8 条,并定期轮换。
⚠ 我曾经踩过的大坑: 在实现FF1的C库时,忽略了模加法中的进位处理。结果加密完美,但解密时某些边界字符(例如 radix=10 下数字 '9' 加 '1' 后应为 '0' 并进位到前一位)没有正确处理进位传递,导致解密后部分字符偏移了一位。从那以后,我要求每个FF1实现都必须通过 NIST 官方测试向量全量验证,一条都不能少。
FF1 算法核心流程 (Feistel 结构简化图) 明文 (n) radix, tweak minlen, maxlen 拆分 A (u) B (v) u = floor(n/2) PRF (AES) key + round B' = (B + PRF_out) mod radix 交换 A ↔ B' 重复 10 轮 密文 (n 同长) 10 轮循环
// FF1 加密的伪代码(简化)
function FF1_Encrypt(key, tweak, radix, plaintext):
    n = len(plaintext)
    u = floor(n/2)
    v = n - u
    A = plaintext[0:u]
    B = plaintext[u:n]
    
    for i in range(10):
        // 生成伪随机数流(转为 radix 进制,长度 v)
        prf_output = AES_CMAC(key, tweak + int_to_bytes(A) + i)
        prf_radix = bytes_to_radix(prf_output, radix, v)
        // 模加法
        B_new = (B + prf_radix) mod radix
        // 交换
        A, B = B_new, A
    
    return A + B
总结: FF1 的参数决定了它能在哪些场景下安全使用,步骤清晰但实现细节(模加法、边界处理)容易被忽略。记住那条安全规则:tweak 需要随机化、密钥不重用、minlen 不小于 2。我每次做 FF1 的集成测试都会用 NIST 官方测试向量跑一遍,确保加密解密对 10 万条以上数据无误。