FF1算法详解:NIST标准、参数域、加密与解密流程

咱们今天聊的FF1算法,是格式保留加密里最硬核的一个角色。它是NIST SP 800-38G标准里明确规定的算法,说白了就是官方认证的“正规军”。我最初接触FF1时,是在一个信用卡号加密的项目里。

当时客户要求密文也是16位数字,长度不能变,格式不能变。我那会儿就在想,这玩意儿得多么巧妙才能做到?后来研究透FF1,才拍大腿叫绝。

1. NIST SP 800-38G 标准概述

这个标准是个什么来头?简单说,NIST把FF1、FF2、FF3都收进了SP 800-38G。但说实话,FF2现在基本没人用了,FF3也有安全漏洞被指出了。

FF1是目前最靠谱的。 这里面有个故事——我有一阵子在做支付系统的PCI DSS合规,审计方明确要求只能用NIST批准的算法。FF1就是他们的首选。

核心要点:FF1基于Feistel网络结构,它的安全性建立在AES之上。你想想看,一个基于AES的算法,底子是硬的。

2. 参数域与密钥要素

FF1的参数比一般加密算法要复杂。为什么?因为它要保留格式,所以必须知道自己要处理什么类型的“符号”。

参数名 含义 示例值
radix 字符集的基数 10(数字)、26(字母)、36(字母+数字)
minLen / maxLen 输入明文的最小/最大长度 minLen=6, maxLen=30
key 加密密钥(128/192/256位) 0xE4F3...(32字节Hex)
tweak 可变长度“调谐值” 通常为8字节,但可以更长

radix这个词老有学员问——radix=10就是十进制数字,radix=36就是0-9加上A-Z。嗯,这里要注意,FF1的radix必须满足 radixminLen ≥ 100,否则会报错。我刚开始还踩过这个坑,传了个radix=2,结果直接异常。

个人建议:tweak是个好东西。它相当于一把“第二钥匙”。你在不同的业务域里面用同一个主密钥加密,tweak不一样,出来的密文就不一样。我在做多租户系统时,就用租户ID当tweak。

3. FF1加密流程详解

讲加密流程前,咱们先看一张结构图。这张图画的是FF1的Feistel网络,说白了就是“分两半,轮流折腾”。

FF1算法加密流程图(Feistel结构) 输入明文 X(长度 n) A(左半部分) B(右半部分) 迭代轮函数 F (AES加密 + 取模运算) 左右交换(Feistel标准操作) 输出密文 Y(长度保持 n) 注意 1. radix决定字符集大小 2. tweak参与每轮加密 3. 迭代次数固定10轮

加密流程的核心步骤是这样的:

  1. 分割:把明文X分成左右两半,A和B。分割点 u = floor(n/2)。n为偶数时两边等长,奇数时左边少一个字符。
  2. 迭代计算:进行10轮(这是标准规定的数字,不是随便写的)计算。每轮里,用AES加密当前右半部分B、当前轮数、tweak,生成一个伪随机数。
  3. 更新B和A:新B = 旧A,新A = 旧B 加上 那个伪随机数(对radixu取模)。
  4. 输出:10轮结束后,把A和B拼起来,就是密文Y。

你想想看,这个过程是不是跟“搓麻绳”很像?你搓过来、搓过去,最后还是原始长度,但里面的纤维全乱了。

我曾经见过一个项目把这个迭代轮数改成6轮,想“提高性能”。结果做出来之后,密文格式是保留了,但安全强度跟纸糊的一样。不要自作聪明改轮数。

4. FF1解密流程

解密和加密正好反过来。这是Feistel网络的一个绝妙特性:解密就是加密的逆操作,不需要写另一套解密函数!

解密时你只需要:

  • 输入密文Y,分割成A和B(和加密时一样的分割规则)。
  • 从第10轮反着往第1轮走,使用相同的轮函数F。
  • 每一步的操作和加密一样,只是顺序颠倒了。
  • 最后输出明文X。

说白了,加密和解密的核心代码可以共用一套轮函数。这一点在工程上特别爽——你只要测试好一个方向,另一个方向自然就对了。我个人的习惯是写代码时直接把加解密写成一个正反开关,省事。

5. 代码示例(Python)

咱们来个简单的FF1加密和解密。我用的是python的ff1库,这个库是NIST认证过的。

from ff1 import FF1

# 定义参数
key = b'\xE4\xF3\x20...'  # 128位密钥(实际生产用安全的密钥生成)
tweak = b'\x12\x34\x56\x78\x9A\xBC\xDE\xF0'  # 8字节tweak
radix = 10  # 十进制数字
plaintext = "1234567890123456"  # 16位卡号

# 创建FF1实例
cipher = FF1(
    key=key,
    tweak=tweak,
    radix=radix
)

# 加密
ciphertext = cipher.encrypt(plaintext)
print(f"密文:{ciphertext}")  # 输出:16位数字

# 解密
decrypted = cipher.decrypt(ciphertext)
print(f"解密:{decrypted}")   # 输出:1234567890123456

注意看,密文也是16位数字。这就是格式保留的魔力——你在做研发时,下游系统什么都不用改,直接存密文就行。

6. 工程避坑要点

我这些年踩过不少坑,总结出下面几条。你写代码时一定要记得:

  • 长度校验不能省:FF1有minLen和maxLen。输入长度超出范围会抛异常。最好在业务层提前拦下来。
  • radix要合法:radix必须大于等于2,且 radixminLen ≥ 100。否则算法初始化都过不了。
  • tweak不要重复:同一个tweak + 同一个密钥,加密同样的明文,出来的密文一样。这就泄密了。我的做法是tweak里带上时间戳或随机数。
  • 性能预估:FF1每笔加密大约0.1~0.5毫秒(取决于长度)。你要处理高并发的话,记得做压测。

好了,FF1的核心内容就到这。你把它理解成“一个带格式的Feistel网络”,然后记住参数域、轮函数、加解密流程,基本就能应付大部分实战场景了。