数学基础:分组密码、Feistel网络、模运算

说实话,这一章刚接触FF1的人容易跳过。觉得都是基础,没什么好讲的。
但我得提醒你:FF1的整个安全模型,就建立在这三根柱子上。哪一根理解不透,后面调BUG调到怀疑人生。

我自己也是踩过坑的。有次把一个分组密码的输出直接当成了FF1的中间值,忘了Feistel网络里还要做左右交换。结果加密出来的密文格式是对了,但完全解不回去。你想想,这种问题多尴尬。

1. 分组密码:加密的最小砖块

分组密码,说白了就是一把钥匙,一把数据块,输出一个同样长的密文块。
比如AES-128,输入16字节明文,16字节密钥,输出16字节密文。就这么简单一个函数:E(K, P) = C。

在FF1里,分组密码不是直接加密你的电话号码(那会破坏格式),而是作为Feistel网络的“轮函数”。它管的是内部那个短块的加密,而不是整个明文。

我建议:如果你以前只把AES当成“加密文件”的工具,现在要换一个视角——它就是一个高强度的伪随机置换。你给它一点输入,它给你一点看起来随机的输出。FF1正是借用这个“随机性”来把数字分散开。

常见的分组密码有AES、SM4、DES。FF1标准里明确支持AES和SM4。
不过内部算法其实可以替换,只要它是个安全的PRP就行。我在项目里只用过AES-128,因为硬件加速方便,性能也好。

2. Feistel网络:分而治之的框架

Feistel网络是很多分组密码的骨架。它的核心思想是:把数据切成左右两半,每次只加密一半,另一半直接换位。

Feistel一轮的结构:
L' = R
R' = L ⊕ F(R, K)

这个结构有个巨大的好处:解密和加密几乎一样,只是轮密钥顺序反过来。硬件实现起来非常友好。

FF1使用的Feistel网络和DES不太一样。DES是16轮,每轮左右都是32位。FF1的轮数是10轮(对于数字域),而且左右两边的长度可以不相等。官方标准里叫它为“平衡”和“不平衡”两种情况。

为什么非要Feistel呢?
因为我们需要一个可逆的变换,又要保持数据格式(数字串)。直接拿分组密码加密,输出是二进制,格式全坏了。而Feistel网络提供了一条路径:只要轮函数是任意函数(不一定可逆),整个网络就是可逆的。这样我们就能在数字域上做“加密”但保持长度和字符集。

我曾经在一个项目里,觉得轮数少一点速度更快,就把FF1的10轮改成了6轮。结果安全审计没过,说轮数不足会让差分攻击有可乘之机。标准定的10轮不是随便定的,是经过安全论证的。别自作聪明去减轮数。

3. 模运算:数字域上的算术

模运算,数学上就是 a mod n,表示a除以n的余数。
在FF1里,它用来把二进制加密结果映射回数字空间。

举个例子:你的电话号码是11位十进制数字,那整个空间的大小就是 1011。但分组密码输出的是一串二进制位,我们需要把这个二进制数“压”进 [0, 1011 - 1] 这个范围里。怎么压?用模运算:value mod 1011。

但注意,这里有一个坑:模运算不是一一映射的,它会丢失信息。如果二进制值的范围刚好是模数的整数倍,那没问题。但通常不是,所以FF1标准里专门有一套“数值转换”和“约减”机制,保证不会因为模运算导致偏差。说白了就是用拒绝采样或者加一个偏移。

另外,我们还会用到模加((a + b) mod n)和模乘((a * b) mod n)。在FF1的轮函数里,会先计算一个中间数,然后把这个数加到右半部分(也是在同一个模域里)。加减乘除全在同一个模数下做,进出都保证值域正确。

一句话:分组密码提供确定性随机性,Feistel网络提供可逆框架,模运算提供格式保持。三者缺一不可。

三者关系图解

下面这张图概括了FF1算法中这三个基础组件的协作关系。你可以理解为:上层是Feistel网络,左右分治;轮函数由分组密码充当;输出之后用模运算调整范围,保证落在合法数字集里。

FF1 数学基础关系图 分组密码 (PRP) Feistel 网络 模运算 (mod) 提供高安全 伪随机置换 保证可逆性 保持数据长度 数值范围映射 格式保持关键 FF1 算法核心 作为轮函数 输出处理

基础对照表

下面这个表帮你快速对比三者的作用,尤其注意在FF1中的角色差异:

基础组件 数学本质 在FF1中的职责 我踩过的坑
分组密码 F : K × {0,1}^n → {0,1}^n 充当Feistel轮函数,提供不可预测性 用了ECB模式直接加密数字串——格式没保持,还暴露了模式漏洞
Feistel网络 Li+1=Ri, Ri+1=Li⊕F(Ri) 将任意长度数字串分成左右,保证可逆 左右长度不一致时忘了补位,结果下一轮计算错位
模运算 a mod n , (a+b) mod n 将二进制加密结果约束到数字值域内 直接取模导致分布偏差,后来用了标准里的拒绝采样才修正

一点个人体会

数学基础这部分,说穿了就三个概念。但真正把这三个概念组合起来做出合格的FF1实现,需要你理解它们之间的“接口”。分组密码输出的是字节,Feistel需要的是比特,模运算需要的是整数——转换步骤一步都不能错。

我个人的习惯是,先画一张和上面类似的架构图,标清楚每个接口的数据类型和长度,然后再写代码。这样能避免大半的BUG。

嗯,基础就聊到这儿。这些概念后面每一章都会用到,尤其是第三章讲数值转换的时候,你会看到模运算怎么具体干活。