密码学基础回顾:哈希函数、对称加密、伪随机函数
做盲索引之前,先得把密码学三样老家伙摸透。哈希函数、对称加密、伪随机函数——这三兄弟在盲索引里各司其职,少一个都不行。我刚开始学的时候,觉得它们就是独立的工具包,后来在项目里踩了坑才明白:它们之间的组合方式,直接决定了索引的安全性和效率。
这一章我们不搞数学证明,只聊工程里最常用的性质、常见算法,以及我亲自踩过的坑。你记住一句话:
哈希函数:单向指纹锁
哈希函数,说白了就是把任意长度的数据,映射成一个固定长度的摘要。而且这个映射是单向的——你很难从摘要反推出原始数据。这个性质在盲索引里有两个关键用处:
- 位置绑定:用哈希值作为索引槽位的寻址依据。
- 完整性验证:检测数据是否被篡改。
常见的哈希算法,我建议你直接上 SHA-256。MD5 和 SHA-1 我个人已经弃用了,原因很简单:碰撞攻击不再是理论可能。2017 年 Google 就公开演示了 SHA-1 碰撞,而盲索引里一旦发生碰撞,两个不同数据会映射到同一个索引槽,那数据就乱套了。
看我写一段代码,感受一下:
import hashlib
def blind_hash(data: bytes, salt: bytes) -> str:
"""简单的带盐哈希,输出十六进制摘要"""
return hashlib.sha256(salt + data).hexdigest()
# 示例
salt = b"my_secret_salt_2024"
h = blind_hash(b"user_email@example.com", salt)
print(h) # 输出:a7b1c2d3...(64个十六进制字符)
这里要注意:哈希函数本身就是确定性的——相同输入永远得到相同输出。这个特性在盲索引里既是优点也是缺点。优点是索引查询可以精确匹配;缺点是攻击者可以通过观察索引槽位的访问模式来推断信息。嗯,后面讲到“盲”的原理时,我们会用对称加密来修补这个缺陷。
对称加密:双向保险柜
对称加密和哈希最大的区别在于:它可逆。你用密钥加密,同一个人用同一个密钥解密。盲索引里用对称加密不是为了隐藏数据内容,而是为了隐藏“数据与索引之间的映射关系”。
算法选择上,我强烈推荐 AES-256-GCM。为什么?ECB 模式有样式泄露风险,CBC 模式需要处理 IV 的随机性和完整性,而 GCM 模式同时提供加密和认证,一次搞定。我曾经在生产环境中因为忘了对 IV 做随机化,导致索引重建时所有密文都一样,于是泄露了数据的分布模式——那一次被安全审计报告狠批了一通。
下面是对称加密在盲索引中的典型用法(伪代码):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def encrypt_index_value(key: bytes, plaintext: bytes, nonce: bytes) -> bytes:
aesgcm = AESGCM(key)
return aesgcm.encrypt(nonce, plaintext, None)
def decrypt_index_value(key: bytes, ciphertext: bytes, nonce: bytes) -> bytes:
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, None)
注意这里的 key 从哪里来?不能硬编码,那就要靠下一个角色:伪随机函数。
伪随机函数(PRF):密钥工厂
伪随机函数(Pseudo-Random Function,PRF)是一种看上去完全随机的函数,但实际上是确定性的——给定同一个密钥和同一个输入,输出永远一样。这个性质在盲索引里至关重要:我们要为每个索引条目派生一个唯一的加密密钥,但又不能真的随机生成一堆密钥(存储不下)。
工程里最常见的 PRF 实现就是 HMAC。HMAC 基于哈希函数(通常是 SHA-256)构造,安全性有保证。举个例子,我们可以用主密钥(Master Key)和索引条目里的某个“标签”来派生子密钥:
import hmac
import hashlib
def derive_key(master_key: bytes, label: bytes, output_len: int = 32) -> bytes:
"""用 HMAC-SHA256 派生任意长度的子密钥"""
# 实际工程中可能还要考虑计数器、分离上下文等
return hmac.new(master_key, label, hashlib.sha256).digest()[:output_len]
你想想看,这样就实现了:
- 密钥压缩:只需要存一个主密钥,就能派生出上亿个子密钥。
- 不可预测性:即使攻击者知道某个子密钥,也无法反推出主密钥或其他子密钥。
- 独立安全性:每个索引条目用自己的子密钥加密,相互隔离。
当然,这里有个隐含前提:主密钥必须安全存储。我见过团队把主密钥硬编码在配置文件里,结果仓库泄露导致整个系统沦陷。嗯,密钥管理是另一门学问,我们后面章节会专门讲。
三者关系:一张图看懂
下面这张 SVG 把哈希、对称加密、PRF 在盲索引中的角色画清楚了:
从图上你能看到,三者不是并列关系,而是分层协作:哈希产生位置,对称加密混淆位置,PRF 提供密钥。缺少任何一层,盲索引就会变成“半盲”甚至“睁眼”状态。
一张表格对比清楚
| 属性 | 哈希函数 | 对称加密 | 伪随机函数 |
|---|---|---|---|
| 输入 | 任意数据 + 可选盐 | 明文 + 密钥 + 随机数 | 密钥 + 标签/上下文 |
| 输出 | 固定长度摘要 | 密文(长度略大于明文) | 固定长度伪随机串 |
| 可逆性 | 不可逆 | 可逆(有密钥) | 不可逆(由哈希保证) |
| 确定性 | 是 | 否(随机IV影响密文) | 是(相同输入相同输出) |
| 盲索引中的作用 | 定位索引槽 | 隐藏定位关系 | 派生加/解密密钥 |
Hash(key, data) → digest,对称加密用 Encrypt(key, nonce, plain) → cipher,PRF 用 KDF(master_key, tag) → sub_key。接口统一了,后续替换算法(比如从 SHA-256 升级到 SHA-3)就只需要换底层库,逻辑不变。