FPE 应用场景:从卡号到身份证的蜕变
上一章我们讲了格式保持加密的原理,说白了就是让加密结果看起来还是原来的格式。今天我想聊聊,这东西在真实项目里到底怎么用。我做过不少数据安全项目,FPE 的应用场景比想象中要多得多。
为什么需要 FPE?
你想想看,传统的加密方式,像 AES、DES 之类的,输出结果是一堆乱码。32个字节的 AES 输出,直接放你那18位身份证号的位置?数据库直接报错。更别提业务系统里那些对数据格式有严格要求的场景了。
我在金融安全项目中遇到过这种情况:测试环境需要真实数据,但全是明文又不行。传统加密后,业务系统直接崩了。嗯,FPE 就是解决这个尴尬的。
下面这个图,是我对这个章节的知识体系的一个抽象理解。它把 FPE 应用的三个核心领域串联了起来:
信用卡号加密:金融行业的刚需
金融行业是 FPE 最成熟的应用场景。PCI DSS 合规要求你不能明文存储信用卡主账号。但业务系统又需要对卡号进行部分展示、交易对账、风控分析。
你想想看,一张真实的信用卡号是 16 位(大多数情况),带 Luhn 校验。用 AES 加密?输出是二进制流,没办法直接存回原字段。就算把它编码成 Base64,长度膨胀,数据库要改字段,业务逻辑要改代码。这不是开玩笑的。
FPE 在这里的典型做法:使用 FF1 或 FF3-1 算法,在同一个字符集 [0-9] 内进行加密。输出还是 16 位数字,而且可以保留前 6 位(BIN 码)和后 4 位,中间部分做 FPE 加密。这叫「部分加密」,是 PCI DSS 允许的。
社会安全号码(SSN):美国特有的坑
SSN 是 9 位数字,格式通常是 xxx-xx-xxxx。空格和连字符是格式的一部分。很多美国项目的测试数据库里,SSN 是一大隐患。
我曾经遇到一个客户,他们把测试环境的 SSN 用 AES256 加密后存成乱码。好吧,测试人员没法用了。业务方要求做数据脱敏,但脱敏后的 SSN 要看起来像真实的、符合格式的号码。为什么?因为下游系统要校验格式。你脱敏出来的 SSN 如果是 000-00-0000?直接被认为无效。系统崩了。
FPE 的做法很简单:把 SSN 的 9 位数字当成一个整体,在 [0-9] 空间内加密。输入 9 位数字,输出 9 位数字。连字符要不要保留?我个人习惯是保留连字符的格式,但加密只针对纯数字部分。
| 场景 | 输入 | FPE 输出示例 | 是否保留连字符 |
|---|---|---|---|
| 生产环境 1 | 078-05-1120 | 491-62-8390 | 是 |
| 生产环境 2 | 123-45-6789 | 784-21-3956 | 是 |
| 脱敏测试 | 987-65-4321 | 532-91-4872 | 是 |
你看,输出保持了 SSN 的典型视觉特征。安全性方面,FPE 的加密强度取决于底层算法,和 AES 是同一个级别的。它不是简单的替换或掩饰。
电话号码加密:全球号码各有各的格式
电话号码是我觉得最烦的一个。不同国家格式不同。中国的 11 位纯数字,美国的 10 位(+1 国家码),英国的 11 位(含 0 开头)。
你想想看,一个全球 CRM 系统里,电话号码字段长度是 varchar(18)。你要对它做加密。如果直接 AES 加密后 Base64 编码,长度膨胀到 24 位以上。数据库字段要改。所有查询接口要改。这是大工程。
FPE 可以按国家分类处理。比如对于中国手机号,它在 [0-9] 长度 11 的空间内做 FF1 加密。对于美国号码,保留国家码 +1,对后面 10 位做加密。输出后直接存回原字段。字段长度不变。
更多场景:身份证号、护照号、邮箱地址
FPE 的应用不局限于上面三种。身份证号是典型的 18 位(中国)或 10 位(台湾),含校验规则。护照号有字母数字混合。邮箱地址是带 @ 符号的字符串。
对于邮箱地址,我倾向使用 FPE 对 @ 前面的 local part 进行加密。字符集是 [a-zA-Z0-9._%+-],长度控制好。输出还是同一个字符集。域名部分保持不变,因为要路由邮件。
下面的表格总结了几个常见场景的 FPE 参数配置:
| 数据类型 | 字符集 | 典型长度 | 推荐算法 | 需保留部分 |
|---|---|---|---|---|
| 信用卡号 | [0-9] | 16 | FF1 (AES-256) | 前6位BIN + 后4位 |
| SSN | [0-9] | 9 | FF1 (AES-256) | 无(完全加密) |
| 中国手机号 | [0-9] | 11 | FF3-1 (AES-128) | 前3位号段 |
| 身份证号 (中国) | [0-9X] | 18 | FF1 (AES-256) | 前6位地址码 + 后4位 |
| 邮箱 (local part) | [a-zA-Z0-9._%+-] | 不定 (≤ 64) | FF1 (AES-256) | @ 域名部分 |
你看,不同数据类型的 FPE 参数差异很大。字符集、长度、算法选择都有讲究。我个人建议你在做方案时先列一个类似上面的表。
代码示例:信用卡号部分加密
我写了一个简单的 Python 片段,展示对信用卡号做部分 FPE 加密。这里用的是 pyffx 这个库(一个轻量级 FPE 实现)。
from pyffx import FPEInteger
def encrypt_card_pan(pan: str, key: bytes) -> str:
"""
对信用卡号PAN进行部分FPE加密。
保留前6位BIN和后4位,加密中间部分。
"""
bin_part = pan[:6] # 前6位保留 # 发卡行标识
mid_len = len(pan) - 10
mid_plain = pan[6:-4] # 中间部分
suffix = pan[-4:] # 后4位保留
# 使用FF1算法,在0-10^len-1的范围内加密
tweak = b"my_app_card_tweak" # 同一个应用的tweak保持一致
cipher = FPEInteger(
key=key,
radix=10,
length=mid_len,
tweak=tweak
)
mid_enc = cipher.encrypt(int(mid_plain))
# 转回字符串,补齐长度
mid_enc_str = str(mid_enc).zfill(mid_len)
return bin_part + mid_enc_str + suffix
# 测试一下
pan = "4111111111111111"
key = b"sec_39char_key_"
enc = encrypt_card_pan(pan, key)
print(f"原号: {pan}")
print(f"加密: {enc}")
tweak 参数很重要。同一个 tweak 值代表同一个应用下的不同字段或不同用户。不要对所有数据用同一个 tweak,否则会有频率攻击风险。我一般用一个应用级别的 tweak 加上字段的元数据。
避坑指南:我踩过的三个坑
最后分享三个我踩过的坑:
- 逻辑校验位处理不当:信用卡的 Luhn 校验、身份证的加权校验。FPE 输出后校验值可能变。你的系统能不能接受校验失败的号码?如果不能,需要在加密后重新计算校验位。这不是 FPE 算法本身的问题,是业务系统的容忍度。
- 重复加密 vs 不可逆脱敏:FPE 可逆。如果你只是做测试脱敏,不需要可逆。用 FPE 可逆方案增加了密钥管理成本。我建议你对生产环境用可逆加密,对测试环境用不可逆的方式。
- 性能陷阱:FF1 算法处理短数据(如 6 位数字)比处理长数据慢很多。因为 FF1 内部有多次迭代。测试的时候一定要考虑并发量。我上次做金融项目时,QPS 要求 5000,FPE 成了瓶颈。后来我们换成 FF3-1 算法才解决。
好了,这一章我们主要聊了 FPE 在常见数据字段中的应用。基本上你在企业做数据隐私、合规、或测试脱敏时,都跑不出这个范围。记住两点:格式保持 + 业务容忍度。搞清楚了再动手。