四、常见脱敏算法(二):加扰、加密、幻影(FPE)——安全性分析
脱敏算法选错,比不脱敏更危险。这话听着夸张,但我在项目里见过太多「脱敏后数据还能反推原文」的案例了。今天咱们就把加扰、加密、FPE这三类算法的安全性掰开揉碎讲透。
4.1 加扰:看起来乱了,其实很脆弱
加扰,说白了就是「假装脱敏」。它通过固定规则打乱字符顺序、替换部分字符,但保留原始数据的格式和统计特征。
- 字符乱序:比如把手机号 13812345678 变成 18236547138
- 固定替换:把名字里的「张」统一换成「王」
- 随机偏移:每个数字加一个固定值(比如+3,1变成4)
关键认知:加扰的安全性≈0。它只防肉眼,不防机器。
我参与过一家金融公司的数据审计,他们用「姓替换、随机打乱中间四位」的方式脱敏身份证号。结果呢?攻击者用统计分布一算,就把前六位地址码和后四位校验码还原了。因为打乱的是中间区位码,但前六位和后四位根本没动。
避坑指南:
- 千万别用加扰处理高敏感字段(身份证、银行卡号、社保号)
- 加扰只能用于测试数据中「非关键」字段,比如用户昵称、邮编
- 加扰后的数据不可逆转——你找不到原始值
4.2 加密:安全,但格式会变
加密大家都熟。AES-256,SM4,这才是正经的密码学算法。安全性依赖密钥,只要密钥不泄露,密文就是一堆二进制乱码。
但问题来了:加密后的数据格式变了。比如一个18位身份证号,AES加密后变成32字节的十六进制字符串。你原来的数据库字段存的是 varchar(18),现在得改成 varchar(64) 甚至更大的 blob。
我遇到过最典型的情况:一家电商平台要对用户手机号加密存储,但下游的短信营销系统需要直接读取原始号码发验证码。他们没做解密接口,而是直接把密文拷贝过去了……结果是所有营销短信都发给了不可读的乱码串。
| 算法 | 安全性等级 | 格式保持 | 性能 | 典型场景 |
|---|---|---|---|---|
| AES-256 | 极高 | 否(输出二进制/hex) | 快(硬件加速) | 数据库列级加密、传输加密 |
| SM4 | 极高(国密) | 否(输出16字节倍长) | 快 | 国内合规场景 |
| RSA-2048 | 高 | 否(输出256字节) | 慢(仅适合小数据) | 密钥交换、数字签名 |
注意:加密 ≠ 脱敏。加密是可逆的,只要拿到密钥就能恢复。在很多监管要求中(比如GDPR、个人金融信息保护),加密只能算「基本防护」,不能替代脱敏。脱敏要求的是「即使攻击者拿到密钥,也无法还原原始数据」。
我曾经和合规团队争论过这件事,后来监管现场检查时,他们明确表示「加密存储后仍然需要做脱敏处理,否则视为未脱敏」。
4.3 幻影(FPE):既要安全,又要保持格式
FPE(Format Preserving Encryption)就是来解决「加密后格式变化」的痛点。它采用保留格式的加密模式——输出数据和原始数据长度相同,字符集相同。
举个例子:信用卡号 4111 1111 1111 1111,FPE加密后仍然是16位数字,且前4位还能保留(BIN码)。这样数据库字段不用改,接口不需要适配,下游系统照常工作。
我个人习惯:评估一个字段是否用FPE,先问自己三个问题:
- 这个字段的格式是否必须保留?(比如手机号、身份证、银行卡号)
- 应用层是否有现成的解密能力?(没有就回流到下一条)
- 字段长度是不是固定且较小?(超过128位变慢)
如果都是"是",FPE首选。
FPE 的安全性到底怎么样?
FPE 的底层本质还是加密。主流实现 FF1 和 FF3-1 都基于 AES,用的是 NIST SP 800-38G 标准。安全强度等价于相同密钥长度的 AES——这是严格证明过的。
但有个坑:FPE 不能抵御「选择明文攻击」中的某些侧信道。举例:如果你把 FPE 用在了「自增ID」上(比如用户ID 10001 → FPE后还是10001),攻击者很容易通过已知明文对推断出密钥。这不是算法本身的问题,而是使用场景不当。
// 举例:Java 中使用 FF3-1 (Bouncy Castle)
import org.bouncycastle.crypto.fpe.FPEParameters;
import org.bouncycastle.crypto.fpe.FF3p1Engine;
import org.bouncycastle.crypto.params.KeyParameter;
// 初始化密钥和Tweak
byte[] key = hexStringToByteArray("2B7E151628AED2A6ABF7158809CF4F3C");
byte[] tweak = hexStringToByteArray("CBD09F18");
KeyParameter keyParam = new KeyParameter(key);
FF3p1Engine engine = new FF3p1Engine();
engine.init(true, new FPEParameters(keyParam, tweak, 10)); // radix=10 表示数字
// 加密 16 位银行卡号
String plain = "4111111111111111";
byte[] cipher = engine.processBlock(plain.getBytes(), 0, plain.length());
System.out.println(new String(cipher)); // 输出仍是16位数字
4.4 安全性对照一览
我直接画了个对比表格,方便你选型时快速对照:
| 维度 | 加扰 | 标准加密(AES) | FPE(FF1/FF3-1) |
|---|---|---|---|
| 可逆性 | 不可逆(无密钥) | 可逆(有密钥) | 可逆(有密钥+相同tweak) |
| 格式保持 | 是 | 否 | 是 |
| 抗统计攻击 | 弱(保留分布) | 强(均匀分布) | 强(密文分布与明文无关) |
| 密钥管理需求 | 无 | 需要 | 需要(+ tweak管理) |
| 典型脱敏等级 | 低(仅用于非敏感字段) | 中-高(配合其他措施) | 高(适合合规场景) |
| 性能(10万条/秒) | 极快(几乎无开销) | 快(~50μs/条) | 中等(~200μs/条,FF3-1比FF1更快) |
选型结论:
- 需要用明文原样做查询/索引 → FPE(配合保留明文索引前缀)
- 只存储不允许原样读出 → 标准加密 + KMS管理密钥
- 仅做展示脱敏(比如页面显示138****5678)→ 加扰就已经够了
4.5 一张图看懂三类算法的安全性定位
4.6 避坑清单:我的实战教训
- 不要认为「加密了就能过合规」——很多行业标准(PCI-DSS、等保三级、金融数据安全分级指南)明确要求「不可逆脱敏」或「仅暴露必要部分」,加密后等于没脱敏。
- FPE 不要用在序列号上——前面提到过,如果明文本身是连续自增ID,FPE加密后依然可能被猜出密钥。我曾经在项目中因此被DBA追着问「为什么用户ID加密后还是连续的?」
- 加扰不要用在 JSON 或 XML 字段里——你会破坏数据结构的完整性。有一次下游解析时因为少了逗号直接报错。
- 密钥和tweak要分开存储——很多团队把tweak硬编码在代码里,等于给攻击者开了一扇后门。
我的习惯:
- 对于生产环境的数据复制品(比如测试库),我优先使用 FPE 加上动态过滤规则,这样既保留了数据可用性,又能在运行时拦截敏感查询。
- 如果条件允许,我会在数据库层面做「透明脱敏网关」——应用层感知不到脱敏,数据库返回时自动替换为加扰或FPE结果。这需要 DBA 配合,但效果很干净。
4.7 小结
嗯,这一章我们聊了三种算法的「安全底牌」。加扰像层薄纱——看着挡了,一戳就破。加密是铁桶,但桶的形状变了,管线和接口都得跟着改。FPE 是定制化铁桶——既要安全又要保持原样,代价是需要多掏一份 tweak 的管理成本。
选哪个?看你的业务场景和合规要求。我自己的经验是:能用FPE优先用FPE,因为改字段长度的代价远比多维护一个tweak大得多。当然,如果你只是给报表做个展示脱敏,加扰就足够——别为了炫技引入不必要的复杂度。