替代方案概览:加密、哈希、截断、掩码
说到令牌化的替代方案,我首先得承认:令牌化不是万能的。早年做某支付平台合规改造时,我们遇到过资金流上游系统死活接不了令牌接口的情况。当时就一句话——不上令牌,但必须合规。那怎么办?
今天聊的四个方案:加密、哈希、截断、掩码,说白了都是同一件事:让原始卡号不出现在不该出现的地方,同时尽可能保留业务可用性。
核心判断标准:
- 我需要还原原始数据吗?→ 选加密
- 我只做等值比较,不关心原文?→ 选哈希
- 我只保留最后几位,其他丢掉?→ 截断
- 我只在显示/打印时遮挡部分数字?→ 掩码
加密:可逆但不可轻用
加密是唯一可逆的替代方案。你存储了密文,业务需要时用密钥还原。这个我熟——当年给某银行的交易日志做合规改造,他们坚持“必须能查到完整卡号以便客服核账”。
我个人习惯用的对称加密算法是AES-256,配合GCM模式。
from cryptography.fernet import Fernet
# 生成一个密钥(一次性)
key = Fernet.generate_key()
cipher = Fernet(key)
# 加密
pan = "4111111111111111"
encrypted = cipher.encrypt(pan.encode())
# 解密
plain = cipher.decrypt(encrypted).decode()
注意:PCI DSS对加密有硬性要求。
- 密钥必须存储在HSM或专用密钥管理系统中,不能硬编码
- 加密后的数据仍属于“持卡人数据”,存储环境需符合CDE要求
- 定期轮换密钥,建议最多一年一次
加密的最大问题是:你保留了风险。一旦私钥泄露,所有历史数据就全完了。我曾经见过一个案例,某公司把密钥放在配置文件里,还提交到了Git仓库……嗯,那场事故处理了整整三周。
哈希:不可逆但撞库风险大
如果业务只需要“比较两笔交易是不是同一个卡号”,哈希就够了。比如防重复支付、风控建模、黑名单匹配。
但注意——不要用明文哈希。为什么?因为卡号只有16位数字,穷举空间很小。攻击者可以预先算好所有可能卡号的哈希值,这就是彩虹表攻击。
我的避坑习惯:
- 一律使用加盐哈希:
SHA-256(salt + PAN) - 盐必须足够长(至少32字节)且每张卡独立
- 哈希值仍属于持卡人数据——PCI的CDE范围没变
import hashlib, os
def hash_pan(pan, salt=None):
if salt is None:
salt = os.urandom(32)
h = hashlib.pbkdf2_hmac('sha256', pan.encode(), salt, 100_000)
return salt + h # 存储盐+哈希
你想想看,如果只用SHA-256(PAN),攻击者做一次预计算就能匹配所有卡号。加盐之后,每个卡号都要独立计算,成本上升数百万倍。
截断:最简单的减法
截断就是只保留卡号的前几位或后几位,其余丢掉。PCI DSS允许保留不超过6位且不包含完整BIN+完整账号的最后4位——说白了,你不能留着前6位和后4位,那样就是完整卡号了。
我在项目中唯一见过截断的合理场景是交易分析报表:只需要卡BIN(前6位)判断发卡行归属,完全不需要完整PAN。这时候用截断就够了。
千万别误会:
- 截断是不可逆的,数据永久丢失
- 如果业务未来可能需要完整卡号,截断会害死你
- 截断后的数据依然在《PCI DSS适用性矩阵》范围内
掩码:只遮挡不存储
掩码和上面三者完全不同。掩码不是一种存储方案,它只用于显示/打印时的实时遮挡。比如客服界面只显示“411111******1111”,但数据库里存的是完整PAN。
合规经验谈:掩码显示本身不受PCI DSS限制——因为它不存储、不传输。但你用于生成掩码的原始数据必须在CDE中保护。
def mask_pan(pan):
# 显示前6位 + 后4位,中间掩码
return pan[:6] + "******" + pan[-4:]
print(mask_pan("4111111111111111"))
# 输出: 411111******1111
有一次审计,对方问我们客服系统API返回的完整PAN为什么没有走加密传输——我解释说“这只是掩码,后端实际返回的还是密文,前端只在浏览器里实时掩码”。嗯,差点被判定为违规,从此我学乖了:掩码必须在显示层完成,不要在服务端或传输层暴露原始数据。
四者对比:一张表说清
| 特性 | 加密 | 哈希 | 截断 | 掩码 |
|---|---|---|---|---|
| 是否可逆 | 是 | 否(理论上) | 否 | 否(仅显示) |
| 是否需要密钥 | 是 | 否(但需盐值) | 否 | 否 |
| 撞库风险 | 无(密文不可识别) | 高(无盐时极高) | 低 | 低 |
| 是否属于持卡人数据 | 是 | 是 | 是(剩余部分) | 否(仅显示) |
| 典型场景 | 需要还原完整PAN | 去重、匹配、风控 | 统计分析 | 客服界面、打印小票 |
一张图理解四者的关系
我习惯把四种方案画成一个“保护程度-保留可用性”的二维图。你看下面这张SVG就明白了——越往上,数据越安全但越难用;越往右,业务越好用但越不安全。
从图上你也能看出来,没有“最佳”方案,只有“最适合当前场景”的方案。如果有机构告诉你“只用一种方案就能全面合规”,那建议你多问一句:你们做过风险分析了吗?
我的个人判断框架(给团队用过多次):
- 如果未来可能需要还原→加密(同时规划密钥管理)
- 如果只做等值比较且能容忍不可逆→加盐哈希
- 如果数据只用于粗粒度统计且能接受信息损失→截断
- 如果只在界面展示或打印→掩码(不要存掩码后的值)
选对方案,能省下至少80%的合规改造工作量。我也曾在这上面吃过亏,选错了方案导致后期返工,那感受真不好受。