FF1 算法:NIST 标准详解与实战

FF1 是目前业界最成熟的 FPE 方案。我自己的项目中,大部分敏感数据保护场景都优先选它。为什么?因为它是 NIST SP 800-38G 标准,经过严格的安全性分析,不用自己造轮子。

说白了,FF1 就是个双调结构的 Feistel 网络。你想想看,它怎么保证密文跟明文格式一致?核心就一句话:在同一个字符集里做置换,不改变长度,不引入非法字符。

算法核心:Feistel 网络 + 域调整

FF1 的结构其实不复杂。它把输入分成左右两半,然后反复做「将右半部分代入轮函数,结果与左半部分异或」这个操作。轮函数里使用了 AES 或 TDES 分组密码,再结合一个叫「数字化中间值」的技巧,保证输出整数在正确的数值范围内。

我在实现时最头疼的是那个「域调整」步骤。说白了,就是每次加密前要根据当前轮数和 tweak 计算一个调整值,再把它塞进分组密码里。这个环节搞错了,密文根本解不回来。

参数设置:五个核心参数

调用 FF1 需要五个参数,少了哪个都不行。我列个表方便你对照:

参数含义典型值我踩过的坑
radix字符集基数10(数字)、36(字母数字)银行卡号用 10,别设成 16
minlen最小明文长度2 或 4太小的长度会降低安全性
maxlen最大明文长度根据场景定,如 19必须 ≥ minlen + 2
key对称加密密钥16/24/32 字节 (AES-128/192/256)别用弱密钥!
tweak附加调整值0~7 字节的二进制数据长度固定后不可随意更改

我的经验:radix 一旦定下来就不要变。tweak 可以当作「应用场景号」,比如同一个数据库里多个表,我用不同 tweak 来区分,这样明文相同但密文不同。

使用示例:Python 实现

我一般用 ff1 这个 Python 库,简单直接。但记得自行实现字符集的编码解码。下面是一个处理 16 位数字账号的例子:

from ff1 import FF1
from Crypto.Cipher import AES

# 参数准备
key = b'\x01\x23\x45\x67\x89\xab\xcd\xef' * 2  # AES-128
tweak = b'\x00\x00\x00\x00'
radix = 10
min_len = 2
max_len = 19

ff1 = FF1(key=key, tweak=tweak, radix=radix, min_len=min_len, max_len=max_len)

plaintext = "1234567890123456"   # 16位数字
ciphertext = ff1.encrypt(plaintext)
decrypted   = ff1.decrypt(ciphertext)

print(f"明文:{plaintext}")
print(f"密文:{ciphertext}")
print(f"解密:{decrypted}")

注意:radix=10 时,输入必须是纯数字字符串。我曾误传了字母,程序直接崩溃。后来我加了校验,不在参数里解决的坑,就提前拦住。

我曾经犯过的错:最开始我用同一个 key 处理多个业务表,结果两个表里有相同明文,密文也相同,被安全审计发现。后来加了不同的 tweak(比如表名哈希),问题解决。

FF1 加密流程详解

下图是我自己画的一张核心流程图,概括了 FF1 的一次加密调用:

FF1加密核心流程 输入明文 X 分割为 A || B 迭代 r = 0..9 轮函数:AES + 数字域调整 生成中间值,与右半部分异或 交换左右并合并 输出密文 Y 10轮 每轮使用不同 轮常数 + tweak

嗯,这里要注意:图中展示的是 10 轮迭代,但实际轮数取决于 maxlen 的具体值。标准规定至少 10 轮,但 maxlen 大于 2^10 时需要增加。我一般直接取 16 轮确保安全。

避坑指南:我亲手踩过的三个坑

  • tweak 长度固定:FF1 的 tweak 必须是 0~7 字节。我当初以为可以无限长,结果库报错说 tweak 超出范围。后来改用 hash 截断解决。
  • 字符集转化:FF1 内部用整数运算,外面传字符串。你自己写 str_to_int 和 int_to_str 时,必须保证一一对应。我试过数字前补零的问题,解密后变短了,后来强制固定长度。
  • 密钥管理:别把 key 硬编码在代码里。建议用 KMS 或者环境变量,最起码也得用 getpass 读取。我曾经在 Git 仓库里不小心提交了测试 key,被同事发现。

性能与安全性权衡

FF1 的计算量主要来自轮函数里的分组密码调用。10 轮就要调用 10 次 AES。我测过,用 Python 处理 16 位数字,每秒大概能跑 2000 次加密。如果你需要更高吞吐,建议用 C/C++ 实现,或切换到 ff3(但安全性争议较大)。

记得吗?我在那个支付系统的项目中,所有卡号走 FF1 加密,每秒上万笔交易。后来换用了 OpenSSL 引擎加速 AES,延迟降低了 70%。

重点总结:FF1 的核心优势是标准化、安全边界清晰。你只要把 radix、minlen、maxlen、key、tweak 五个参数设置合理,其余交给算法就行。我在多个合规审计中,FF1 方案一次过关。