2. FPE基本原理:保形加密与保序加密的区别
聊FPE(保留格式加密),你首先得明白一件事——它不是为了“加密”而生的,而是为了解决一个很现实的问题:加密之后的数据格式变了,下游系统直接罢工。
我刚开始接触数据脱敏时,遇到一个典型场景:要对信用卡号做脱敏,但前端系统死活不认16位以外的数字。传统的AES加密会输出一堆乱码,根本没法用。后来我才发现,FPE内部其实分了两条技术路线——保形加密和保序加密。二者虽然都叫FPE,但解决的问题和设计思路截然不同。
2.1 保形加密(Format‑Preserving Encryption)
保形加密的目标很简单:密文和明文的格式完全相同。比如输入一个16位的信用卡号,输出还是一个16位的数字串;输入一个合法的电子邮件地址,输出仍然符合xxx@xxx.xxx的结构。至于大小顺序?完全不关心。
我记得在做一个支付系统的测试数据脱敏时,系统要求脱敏后的卡号必须通过Luhn算法校验。当时我试了好几个方案,最后用FF1算法(一种保形加密标准)实现了——密文看起来完全像真实卡号,连校验位都自动对齐。保形加密的核心原理是:把明文映射到同一个有限字符集上的一个伪随机排列。说白了,就是用Feistel网络或者循环分组,在保持字符集和长度不变的前提下,完成置换。
保形加密的特点
- 输出格式与输入完全相同(长度、字符集、边界符)
- 安全性依赖分组密码(如AES),具备可证明安全
- 不可逆(单向函数),但支持解密恢复明文
- 常用于敏感数据脱敏、格式严苛的遗留系统
2.2 保序加密(Order‑Preserving Encryption)
保序加密走的是另一条路。它的核心目标是:密文的大小关系与明文完全一致。如果明文A < 明文B,那么加密后的密文A < 密文B。至于输出格式是不是跟原来一样?不一定,很多时候密文就是一段二进制串,长度可能会变。
我曾在数据库加密项目中遇到过这种需求:业务需要直接在密文上做范围查询(比如查询年龄在20到30之间的记录)。如果用了保形加密,只能全表解密再过滤,性能直接崩溃。保序加密恰好解决了这个问题——它本质上是一个单调递增的映射函数。最简单的实现就是把明文按数值区间映射到密文空间,但安全级别很低。工业界常用的是基于线性分流或随机保序编码的方案,比如Boldyreva构造。
我曾经踩过的坑
有一次我图省事,直接用了简单的线性映射来做保序加密——把明文乘以一个固定系数再加一个偏移。看起来顺序保住了,但攻击者只要拿到几个明密文对,就能猜出密钥。后来我才发现,真正的保序加密必须引入随机化或噪声,同时保证单调性不破。而保形加密就没有这个纠结。选型时一定想清楚你到底要“保形”还是“保序”,这两个东西不能混为一谈。
2.3 两者的核心区别
| 对比维度 | 保形加密 | 保序加密 |
|---|---|---|
| 核心目的 | 保持输出格式与输入相同 | 保持输出数值顺序与输入相同 |
| 输出格式 | 固定长度、固定字符集 | 通常为二进制串,长度可变 |
| 顺序关系 | 不保持(密文无序) | 严格保持(密文有序) |
| 安全性 | 可证明安全(如FF1/FF3达到PRP安全) | 语义安全较弱,一般只达到“选择明文攻击下的不可区分” |
| 查询能力 | 只能精确匹配,不能直接范围查询 | 支持直接在密文上进行范围查询、排序 |
| 典型算法 | FF1、FF3、VAES3 | Boldyreva OPE、CLWW、mOPE |
| 应用场景 | 信用卡、身份证、手机号脱敏 | 数据库加密、范围检索、排序索引 |
嗯,这里要注意:保序加密因为泄露了大小顺序,信息量比保形加密多,所以理论上安全强度会打折扣。你如果既要求格式不变又要求顺序保持,那就得用复合方案——比如先保形再保序,或者反过来。但这样会导致复杂度飙升。
2.4 应用场景与选型建议
- 纯保形需求:你只需要格式不变,不需要做任何大小比较。比如脱敏后的数据用于展示、打印、接口对接。用FF1或FF3。
- 纯保序需求:你需要在加密数据上做范围查询、B+树索引、排序聚合。用Boldyreva OPE或者更先进的mOPE(可调节的保序加密)。
- 二者都不能满足的情况:如果你既需要格式不变又需要顺序查询……我个人建议重新考虑设计。很多时候可以把格式和顺序拆开处理:一个字段存保形加密的结果用于展示,另一个字段存保序加密的结果用于查询,两列分开维护。
我从项目里学到的经验
我习惯在选型前先画一个决策树:问自己三个问题——1)下游系统对格式有没有硬依赖?2)查询语句里是否需要BETWEEN、ORDER BY或MIN/MAX?3)能否接受密文长度变化?然后对着上表对号入座,基本不会选错。