一、FPE 概述:为什么要给数据“穿同一件衣服”

咱们先从一个小故事讲起。

几年前我接手过一个金融项目,需求很简单——对信用卡号加密。我顺手用了 AES-256,心里还挺得意,觉得安全强度够了。结果一上线,数据库报错了。为什么?密文长度变了,字符集也变了,原本 16 位数字的卡号,加密后变成了 32 位十六进制字符串,下游系统全都是按 16 位定长字段设计的,直接崩了。

那之后我就记住了:在很多业务场景里,你不能改变数据的格式。这就是 FPE(Format-Preserving Encryption)存在的意义。

1.1 什么是 FPE?一句话说清楚

FPE 的全称是 Format-Preserving Encryption,翻译过来就是“格式保持加密”。它的核心要求很简单:密文和明文的格式完全一致。长度相同、字符集相同、甚至连校验位(比如信用卡号的 Luhn 校验)都能保持不变。

通俗定义:加密后得到的东西,看起来和加密前一样 —— 比如输入是 16 位数字,输出也是 16 位数字;输入是 18 位身份证号,输出也是 18 位身份证号。

你想想看,这听起来像是个“约束条件下的加密”。传统加密算法(AES、DES)的目标是输出均匀随机的比特串,根本不在乎可读性。但 FPE 在密码学安全性和格式约束之间找了一个平衡点。

1.2 为什么非要保持格式?三个真实痛点

  • 数据库改造成本太高:表结构已经固定,字段类型、长度、索引都绑死了。换 AES 加密,你得改表结构、改应用程序、改数据迁移脚本。我在银行项目里算过,一次全字段加密改造,光 DBA 审计就要两周。
  • 业务逻辑依赖格式:信用卡号的后四位、身份证号的出生日期、社保号的校验码……很多业务系统直接解析这些字段的特征。一旦格式变了,校验、展示、对账全都会报错。
  • 合规要求明文格式:某些监管场景(比如 PCI DSS)要求加密存储,但查询时仍需要按明文格式展示部分信息(比如“尾号 1234”)。如果用 AES 加密整个字段,你就没法只解密后四位。

1.3 历史背景:FPE 是怎么一步步走到台前的?

最早系统性地提出 FPE 概念,我记得是 2002 年左右,Black 和 Rogaway 那篇。当时他们给出了两个经典构造:FF1 和 FF2。简单的说,就是基于 Feistel 网络,把分组密码包装成能在任意基数(数字、字母、自定义字符集)上运行的置换。

后来 NIST 在 2016 年正式发布了 SP 800-38G 标准,规定了 FF1 和 FF3 两种模式。国内也陆续有行业标准跟进,比如金融行业的 JR/T 0197。可以说,FPE 已经从学术研究走向了工程落地。

我个人的习惯:去看 FPE 相关的 RFC 和 NIST 文档时,先看例子。标准文档里给的测试向量能帮你快速验证实现是否正确。我当年自己写 FPE 实现时,就靠这些测试向量揪出来一个字符集对齐的 bug。

FPE 发展时间线(简化版)
年份事件说明
2002Black & Rogaway 提出 FF1、FF2 构造
2008Bellare 等提出 FF3更快的 Feistel 变种
2016NIST SP 800-38G官方标准化 FF1、FF3
2017NIST 更新版本修复 FF3 的安全性弱点
2019国内金融标准JR/T 0197 发布

1.4 典型应用场景:FPE 都在哪里发光?

我这些年接触过的项目里,FPE 应用最集中的就是三大领域:

  • 信用卡号(PAN)加密:PCI DSS 要求存储时不能以明文保存完整卡号。但很多交易流水、对账文件、客服系统都需要基于卡号后四位做匹配。FPE 完美解决:加密后仍是 16 位数字,后四位与明文保持一致(如果只加密前 12 位保留后 4 位的话,但更严格的做法是整体加密再分段暴露)。
  • 身份证号加密:18 位数字 + 字母 X,格式固定。如果用 AES,你会得到一串乱码,没法入库。FPE 加密后还是 18 位,业务系统零改动。
  • 手机号 / 邮箱局部脱敏:虽然 FPE 可以全字段加密,但更常见的用法是做“可逆脱敏”。比如手机号中间四位加密后仍是数字,可以满足“仅运营可见”的权限控制。

注意:FPE 不是万能药。它保持的是“格式”,不是“语义”。加密后的卡号虽然还是数字,但可能不再通过 Luhn 校验(除非你专门选择支持校验位保持的模式)。我曾经踩过这个坑,上线后交易系统提示卡号非法,排查了半天才发现是校验位变了。

1.5 与标准加密的比较:一张表看明白

为了让大家更直观地理解差异,我画了一张对比表:

维度标准加密(AES、DES)格式保持加密(FPE)
输出格式任意二进制(通常 base64 再编码)与输入完全一致(长度、字符集)
安全性强度标准随机置换,强安全与 AES 同级别(底层就是 AES)
性能快(硬件加速普遍)稍慢(涉及模运算和循环)
系统改造影响大(字段、索引、程序都得改)小(几乎零改造)
适用场景通用数据加密特定格式约束字段(卡号、ID 等)
标准化程度成熟(多种标准)较新(FF1/FF3 为主)

说白了,标准加密和 FPE 是互补关系。我通常的做法是:能改表的用 AES,不能改表的用 FPE。两者在安全性上没有本质差别——FPE 底层依然可以调用 AES 作为轮函数,只是外面包了一层 Feistel 网络来做格式保持。

1.6 FPE 核心框架图

下面这个 SVG 图展示了 FPE 加密的宏观流程。你可以看到,输入数据先被转化为域内的数值,然后经过 Feistel 结构的多次迭代,最后再映射回原始格式输出。

FPE 加密核心流程 FPE 格式保持加密核心框架 明文 (数字/字母/字符集) 格式 → 数值映射 (Radix 转换) Feistel 迭代 轮函数 = AES tweak 调整 多次循环 数值 → 格式映射 密文 格式与明文相同 关键特性:长度不变、字符集不变、可逆 底层依赖 AES 或 SM4 等标准分组密码 典型场景:信用卡号、身份证号、手机号等格式敏感字段 系统改造量几乎为零,只需替换加密接口

SVG 图解读:从左到右依次是明文输入 → 把自定义字符集(比如十进制数字)映射到整数域 → 在整数域上执行 Feistel 置换(轮函数通常是 AES-128) → 再把整数域映射回原始字符集 → 输出格式与明文一模一样的密文。整个过程是确定性的,只要密钥和 tweak 相同,同样的明文永远得到同样的密文。

1.7 避坑指南:我的三个“血泪教训”

光讲理论不够,我把实战中踩过的坑分享出来,希望你们别重蹈覆辙。

  1. 字符集定义要精确:FPE 要求你明确“格式”的字符集。比如信用卡号是 [0-9] 共 10 个字符,长度 16。但有些字段里有分隔符(比如 1234-5678-9012-3456),你得决定是加密整个串(含短横)还是只加密数字部分。我以前图省事把短横也放进字符集,结果加密后短横位置变了,对账系统直接死了。
  2. tweak 不能重复:FF1 和 FF3 都需要一个 tweak(调整值),类似于初始向量。在同一个密钥下,如果两个不同字段使用相同的 tweak,会泄露部分信息。我的习惯是用“表名 + 字段名 + 记录 ID”作为 tweak 的一部分,保证唯一性。
  3. 性能预估得做足:FPE 因为要做多次 Feistel 迭代和模运算,比裸 AES 慢 3-10 倍。你以为只是换了个接口,结果在高并发场景下 CPU 直接打满。建议上线前做一次压力测试。