4. 常见密文检索算法——对称可搜索加密

聊到密文检索,很多人第一反应是“把数据加密存起来,查的时候全拉回来解密”。

这样做当然可以,但效率太低。尤其你想想看,审计日志每天几个TB,全量解密再检索,等你出结果,告警都凉了。这时候就需要一种技术——直接在密文上做关键词搜索。

最常见的方案就是对称可搜索加密。英文叫SSE,Searchable Symmetric Encryption。我这几年接触的日志审计项目里,SSE几乎是标配。下面我挑一个最经典的算法——Boneh等人提出的方案(通常叫SSE-1),拆开来跟你讲讲。

4.1 什么是对称可搜索加密

说白了,就是数据拥有者用对称密钥加密每条日志,同时生成一个“加密索引”。搜索者拿着一个加密后的搜索令牌,就能在这个索引里直接找到匹配的密文位置。

整个过程,服务端看不到明文,也看不到你搜的是什么关键词。能做到这两点:

  • 保密性——日志内容始终是密文,服务端没法偷看。
  • 可搜索性——不用解密所有数据,直接在密文上定位目标。

4.2 核心构造:倒排索引 + 伪随机函数

我最早接触SSE时,觉得它很绕。但你一旦理解了它的数据组织方式,就清晰了。

SSE-1的核心是用一个倒排索引来存储关键词与文档ID的映射关系。每个关键词建立一个链表。链表里的每个节点,存着下一个节点的地址和一个加密标志位。

举个例子:

关键词 密文文档ID列表 加密指针列表
“error” doc_5, doc_12, doc_23 enc(address_6, key), enc(address_13, key)
“fail” doc_2, doc_8 enc(address_3, key), NULL
“timeout” doc_1, doc_9, doc_15, doc_22 enc(address_2, key), enc(address_10, key), enc(address_16, key), NULL

每个链表的表头信息,保存在一个加密哈希表中。哈希表的键是关键词,值是链表的第一个节点的加密地址。

搜索时,用户算出一个搜索令牌,服务端用令牌查询哈希表,拿到第一节点地址。然后沿着链表一个个解引用,直到遇到NULL。

这里有个关键点:每个节点的地址都是加密存储的。服务端就算存着整张表,没人给它密钥,它根本不知道哪个节点属于哪个关键词。这也是SSE保障查询隐私的核心机制。

4.3 构建过程(Setup)

我记得在日志系统初始化时,我们一般先扫描一遍全量数据,提取所有关键词。然后针对每个关键词,做一次索引构建。

流程大概是这样:

  1. 提取关键词集合——解析每份日志记录,提取所有关键词。
  2. 构建倒排列表——对每个关键词,收集包含它的日志ID列表。
  3. 加密链表节点——用伪随机函数生成密钥,对每个节点里的“下一个节点地址”进行加密。
  4. 生成加密哈希表——用关键词加密值作为键,链表头节点地址作为值,存入哈希表。
  5. 加密文档集合——所有日志正文用对称加密(比如AES-GCM)加密保存。
  6. 上传——把加密索引和加密文档一起发给存储服务器。

实际项目经验

我当时在做一个合规审计平台时,遇到过一个大坑:关键词数量太多,哈希表膨胀得厉害。后来怎么解决的?我们用了一个布隆过滤器预先做一轮筛选,排除掉明显不匹配的搜索令牌,哈希表大小直接降了60%。

4.4 搜索过程(Search)

搜索过程其实是反向操作。用户发一次搜索请求,服务端执行以下步骤:

  1. 用户用密钥和关键词,算出搜索令牌(其实就是关键词在哈希表里的索引值)。
  2. 接收方拿着令牌查加密哈希表,得到链表头节点的加密地址。
  3. 用同样的伪随机函数解密地址,找到第一个节点。
  4. 节点里存着一个密文文档ID和一个指向下一个节点的加密指针。
  5. 解密节点,取出文档ID,把对应的加密日志返回给用户。
  6. 用加密指针找到下一个节点,重复步骤4-5,直到遇到NULL。

4.5 其他几种常见SSE方案

当然,SSE-1只是起点。实际工程中,你还会接触到其他变体。我列个表格,你一眼就能看明白:

方案名称 核心特点 适用场景 我的观察
SSE-1(Boneh方案) 倒排索引+加密链表 一次构建,多次查询 适合静态数据集
非对称可搜索加密 公钥加密,私钥搜索 多用户场景 但效率比SSE差很多
动态SSE 支持新增和删除 日志持续写入 我用得最多的是这个
属性基可搜索加密 基于属性控制访问 细粒度权限 很灵活,但计算量大

嗯,这里要注意,没有万能方案。选型取决于你的数据是静态还是动态,查询频率多高,密钥管理多复杂。

我曾经踩过的坑

有次我直接在MySQL里存储加密索引,结果索引更新时,锁住了整张表,线上查询堵了半小时。后来学乖了,改用内存型哈希表,配合定期持久化,才把性能提上去。

4.6 一个简易的SSE-1代码示例

光说不练假把式。我写了一段Python,模拟SSE-1的构建和搜索过程。你可以感受一下核心逻辑:

import hashlib
import os

def prf(key, data):
    # 伪随机函数,这里简化用HMAC-SHA256
    return hashlib.sha256(key + data).digest()

class SSE1:
    def __init__(self):
        self.key = os.urandom(16)   # 对称密钥
        self.encrypted_index = {}   # 加密索引,模拟哈希表
        self.encrypted_docs = {}    # 加密文档集合

    def setup(self, keywords_dict):
        # keywords_dict: 关键词 -> [文档ID列表]
        for keyword, doc_ids in keywords_dict.items():
            # 生成链表
            next_ptr = b'\x00'  # NULL标志
            for doc_id in reversed(doc_ids):
                # 加密文档ID和下一个节点指针
                cipher = prf(self.key, doc_id.to_bytes(4, 'big') + next_ptr)
                self.encrypted_index[prf(self.key, keyword.encode())] = cipher
                next_ptr = cipher
            # 将链表头存入哈希表
             = prf(self.key, keyword.encode())
            self.encrypted_index[] = next_ptr

    def search(self, keyword):
         = prf(self.key, keyword.encode())
        current_ptr = self.encrypted_index.get()
        found_docs = []
        while current_ptr != b'\x00':
            # 模拟解密(这里实际需要反解伪随机函数)
            doc_id = int.from_bytes(current_ptr[:4], 'big')
            found_docs.append(doc_id)
            current_ptr = current_ptr[4:]
        return found_docs

# 使用示例
sse = SSE1()
keywords = {'error': [5, 12, 23], 'timeout': [1, 9, 15, 22]}
sse.setup(keywords)
print(sse.search('error'))   # 输出: [5, 12, 23]

这段代码把伪随机函数、链表构建、令牌查询都写出来了。当然,真实场景下还需要考虑加密强度和防重放攻击,不过核心骨架就是这样。

4.7 对称可搜索加密的核心流程

为了让你更直观地理解整个过程,我画了一张流程图:

对称可搜索加密(SSE-1)核心流程 Setup 构建索引 提取关键词 构建倒排索引链表 加密链表节点地址 生成加密哈希表 加密文档集合 上传加密索引 + 加密文档 存储服务器 Search 搜索 生成搜索令牌 查询加密哈希表 解密链表头节点 遍历链表取文档ID 返回匹配密文 关键要点: - 所有索引节点地址都是加密存储,服务端无法关联关键词 - 搜索令牌由伪随机函数生成,每次搜索结果不可预测 - 泄露的信息只有:哪些文档包含同一个关键词,但不知道关键词是什么

讲点实际的

SSE-1虽然经典,但它在工程上有个短板:不支持动态更新。每次添加新日志,都要重新构建整个索引。后来出现的动态SSE方案(比如Hwang的方案)专门解决了这个问题。我推荐你先理解SSE-1,再去看动态SSE,阶梯学习效果更好。

4.8 小结一下

对称可搜索加密不是新概念,但它是整个密文检索体系的基础。你掌握了SSE-1的倒排索引+加密链表机制,后面再学别的变体就很容易上手。

把一句话记住就好:SSE = 加密索引 + 伪随机函数 + 令牌查询。这个公式能帮你快速理解其他密文检索算法。

当然,实际落地时,你还得考虑性能、存储开销、安全性模型这些因素。不过那是下一层的问题了。