第3章:非对称可搜索加密(PEKS)——公钥环境下的加密搜索

聊完对称可搜索加密(SSE),你肯定会问:万一通信双方事先没共享密钥呢?或者接收方想把搜索能力开放给多个发送方,但不想让他们知道自己的密钥?这时候就得靠非对称可搜索加密(PEKS)了。说白了,它是把公钥加密的思想揉进了可搜索加密里。

我在2018年接过一个医疗数据平台的项目,三甲医院想让他们旗下的药厂可以直接向自己的加密数据库存处方,但药厂不能拿到检索权限——典型的“发者无心搜,收者有权查”场景。那会儿我就搬出了 Boneh 等人2004年那篇经典。嗯,这一章咱们就把它吃透。

为什么需要PEKS?

对称方案有个硬伤:发送者必须持有接收者的对称密钥,否则没法生成关键词密文。这让密钥分发成了老大难。你想想看,一个云盘服务有几千个用户,每个文件上传者都要和文件主人先交换密钥,这谁受得了?

PEKS 解决了这个问题:发送者只需要接收者的公钥,就可以加密关键词,生成一个叫 PEKS 密文的东西。接收者用自己的私钥生成搜索陷门(trapdoor),交给服务器去找匹配的密文。服务器能判断是否匹配,但不知道关键词原文。

Boneh 等人的方案核心

他们的方案基于双线性配对(Bilinear Pairing)。先别怕,听我用大白话解释:假设你有两个生成元 g 和 h,配对函数 e 能把 g^a 和 h^b 映射成 e(g,h)^{ab}。这种“乘法迁移”性质正好可以用来做关键词匹配。

算法输入输出
KeyGen(λ)安全参数(pk = g^α, sk = α)
PEKS(pk, w)公钥、关键词密文 C = (g^r, H₂(e(H₁(w), pk^r)))
Trapdoor(sk, w)私钥、关键词陷门 T_w = H₁(w)^α
Test(pk, C, T_w)公钥、密文、陷门比较 H₂(e(T_w, g^r)) 与密文第二部分

注意上面 H₁ 和 H₂ 是哈希函数。测试过程里,服务器实际上在检查 e(T_w, g^r) 等于 e(H₁(w), pk^r),因为 e(H₁(w)^α, g^r) = e(H₁(w), g^rα) = e(H₁(w), (g^α)^r)。这步配对运算对服务器透明——它知道两值相等,但不知道 w 是什么。

一张图看懂流程

下面这张 SVG 图画出了 PEKS 的完整通信流程。先花十秒记住角色和动作,再往下看细节。

PEKS 核心流程 发送方 云服务器 接收方 ① 分发公钥 pk ② 上传 PEKS(pk,w) ③ 发送 Trapdoor(sk,w) ④ Test( pk, C, T_w ) ⑤ 返回匹配密文 ID

设计要点:Boneh 的方案巧妙利用配对运算将“加密关键词”和“私钥陷门”统一到同一个配对值上,服务器只需要比较两个哈希值就能判定,整个过程不用知道实际关键词。

代码级理解(伪代码)

我习惯用 Python + pypbc 来做原型验证。下面是一段展示核心逻辑的伪码,剥离了底层曲线细节,只保留算法骨架。

from pypbc import *
import hashlib
from Cryptodome.Random import random

# 初始化双线性群
params = Parameters(n=1024, qbits=512, rbits=256)
pairing = Pairing(params)
g = Element.random(pairing, G1)

# 密钥生成
sk = Element.random(pairing, Zr)      # α
pk = Element(pairing, G1, value=g ** sk)   # g^α

# 哈希函数模拟(实际需要 MapToPoint)
def H1(w): return Element.from_hash(pairing, G1, w)
def H2(gt): return hashlib.sha256(str(gt).encode()).digest()

# PEKS 加密
def peks(pk, w):
    r = Element.random(pairing, Zr)
    u = g ** r
    e_h1_pk_r = pairing.apply(H1(w), pk) ** r   # e(H1(w), pk^r)
    v = H2(e_h1_pk_r)
    return (u, v)

# 陷门生成
def trapdoor(sk, w):
    return H1(w) ** sk   # T_w = H1(w)^α

# 测试
def test(pk, C, T_w):
    u, v = C
    e_T_u = pairing.apply(T_w, u)     # e(T_w, g^r)
    return v == H2(e_T_u)

⚠ 避坑指南:我曾经在一个原型项目里直接把 H1 写成 SHA256 再转群元素,结果发现配对运算一直失败。正确的做法是用 MapToPoint 或完整哈希到曲线(Hashing to G1)。

安全特性与局限

PEKS 提供了两个关键安全属性:

  • 密文不可区分性(IND-CPA):即使攻击者能看到 Q 个不同关键词的密文,也无法区分新密文对应的是哪个关键词。
  • 陷门不可伪造性:没有私钥,攻击者无法为任意关键词生成合法的陷门。

但它有个致命的弱点——关键词猜测攻击(KGA)。如果关键词空间小(比如“紧急”、“常规”、“报销”这类几十个词),攻击者可以枚举所有可能的关键词,用发送方的公钥加密后跟截获的密文比对,猜出关键词。说白了,PEKS 本身不保护“搜索模式”隐私。

特性SSE(对称)PEKS(非对称)
密钥分发需要预先共享公钥公开
计算开销对称加密 + 哈希配对运算(慢一个量级)
抗KGA弱(若用确定性加密)天生脆弱(公钥公开)
适用场景B2B, 企业内部多方上传,未共享密钥

我的建议:在实际集成时,如果你想部署 PEKS,一定要搭配 匿名密钥交换协议(如 PKI + 短期密钥),或者把关键词空间做得非常大(比如哈希成随机串),否则 KGA 会让你的安全形同虚设。

实践中的取舍

Boneh 的方案之后有很多改进,比如基于身份加密的 IBEKS、支持多关键词的 PEKS。但万变不离其宗,核心就是配对运算。我在项目中遇到的最大痛点是性能:一次配对在手机端要 20ms,服务器端还行,但如果是百万级密文,遍历测试就是灾难。解决方案?做 转发安全(forward index),只扫与用户相关的密文分区。

嗯,PEKS 的知识点就这些。记住公钥、陷门、测试三件套,面试时能画出那张流程图,再指出 KGA 的坑,基本就稳了。