第3章 SDK架构与核心组件介绍

做密文检索SDK开发这几年,我最大的感触是:架构设计没想清楚,后面调试简直是噩梦。你想想看,加密检索这件事本身就涉及多个环节——数据怎么加密、索引怎么建、查询怎么转、结果怎么解密。每个环节一旦耦合过深,出了问题你都搞不清是哪个模块在“捣乱”。

所以这一章,我想带你把这套SDK的骨架摸清楚。说白了,就是让你知道每个模块是干什么的,彼此怎么配合。我自己在落地第一个版本时,就因为没理清密钥管理和加密引擎的边界,导致线上一次密钥轮换把索引全搞废了……嗯,踩过坑才知道架构分层有多重要。

3.1 整体架构分层

这套SDK采用三层架构,从上到下分别是:

  • 客户端接入层:提供面向业务开发者的API,比如初始化、文档加密、查询生成、结果解密。你直接调用的接口都在这一层。
  • 核心引擎层:包括加密引擎、索引引擎、查询转换引擎、结果过滤器。真正的密文操作全在这里。
  • 基础支撑层:密钥管理、随机数生成、序列化与反序列化、网络通信封装。这些底层能力被上层引擎调用。

我个人习惯把每一层之间的数据流画成一张图。下面这张图就是我们SDK的核心骨架——你只要记住这张图,后面章节里所有接口的调用顺序就都串起来了。

SDK 架构与核心组件示意图 客户端接入层(API) EncryptDoc · GenerateQuery · DecryptResult · InitSDK 核心引擎层 加密引擎 AES-GCM 索引引擎 倒排 + 布隆 查询转换引擎 生成 结果过滤器 解密·去噪 基础支撑层 密钥管理 · 随机数 · 序列化 · 网络通信 调用链:InitSDK → 加密引擎加密文档 → 索引引擎构建密文索引 → 查询时查询转换引擎生成检索 → 结果过滤器处理返回

3.2 核心组件详解

3.2.1 加密引擎

加密引擎是整个SDK的“保险柜”。它的职责很简单:把明文数据变成别人看不懂的密文,同时保持检索能力。具体来说,它内部封装了两个关键算法:

  • AES-256-GCM:对文档内容进行对称加密。我一开始为了性能想用CTR模式,但后来发现完整性校验在检索场景里太容易被忽略,最后还是老老实实用GCM。
  • OPSE(保序加密):对数值型字段(比如价格、时间戳)做保序加密。这样服务端可以直接按密文排序,不需要解密。

重点:加密引擎不负责密钥的生成和存储,它只管“用”密钥。密钥由基础支撑层的密钥管理组件统一管理。这是分层设计的关键——我曾经见过有人直接把密钥硬编码到加密代码里,结果密钥轮换时要去改几十处调用。

加密引擎的典型接口签名如下:

// C# 风格伪代码
public class CryptoEngine
{
    // 加密文档,返回密文+加密盐
    public (byte[] ciphertext, byte[] salt) EncryptDocument(
        byte[] plaintext, 
        KeyContext keyCtx);
    
    // 解密文档
    public byte[] DecryptDocument(
        byte[] ciphertext, 
        byte[] salt, 
        KeyContext keyCtx);
}

3.2.2 索引引擎

索引引擎是密文检索的“大脑”。没有它,你只能把密文全部拉回来解密再查,那性能就彻底完蛋了。它主要做三件事:

  1. 分词与特征提取:把文档内容切分成关键词,每个关键词经过一个可逆映射(比如单词→密文)。
  2. 倒排索引构建:用密文作为键,存储文档ID列表。注意,这里的是密文形式的,服务端看不到明文关键词。
  3. 布隆过滤器辅助:对于模糊查询,用布隆过滤器快速判断文档是否可能包含某个模式,减少无效扫描。

我记得在做一个日志检索项目时,索引引擎的布隆过滤器参数没调好(假阳性率设得太低),结果布隆占了几个GB内存,导致边缘设备直接OOM。后来我给出的经验值是:对于100万级别文档,布隆容量设为1.5倍文档数,哈希函数用3个就够用了。

Tip:索引构建是CPU密集型操作,尤其是分词和生成。建议在初始化阶段一次性构建,运行时只做增量更新。我们SDK的 IndexBuilder 类提供了 BuildFromStream 和 AppendDocument 两个方法,分别对应全量构建和增量更新。

3.2.3 查询转换引擎

查询转换引擎把用户输入的明文查询(比如“价格大于100”)转换成密文检索请求。你想想看,服务端无法理解明文,它只能根据密文和加密后的数值范围来匹配。所以这个引擎负责:

  • 解析查询语法(支持AND、OR、范围等)
  • 对每个关键词/数值边界生成对应的密文
  • 组合成可执行的检索计划(比如:先查_A的倒排列表,再与_B的交集结果做合并)

查询转换引擎有一个很容易忽略的细节:数值范围查询的边界处理。假如你要查“价格≥100且≤200”,保序加密后的两个边界值必须严格遵循同一把密钥、同一套参数。我遇到过开发同学在两个地方分别调用加密接口,结果生成的密文边界不同,导致查询漏掉一半数据。修复方法很简单:用同一个 RangeQueryBuilder 实例一次性生成上下界。

3.2.4 结果过滤器

结果过滤器在检索流程的末端。服务端返回的密文结果集会包含一些“误匹配”(布隆过滤器的假阳性、保序加密的边界模糊等)。过滤器要做:

  • 二次解密验证:用加密时带的盐和密钥对命中文档的标题/摘要做快速解密,确认是否真的匹配。
  • 噪声清除:把不匹配的记录剔除,只保留真正的结果。
  • 排序与分页:根据解密后的字段排序(比如按时间倒序),再截取指定页。

警告:结果过滤器必须在客户端本地运行,绝不能把解密密钥传给服务端。否则整个密文检索就失去了意义。我们的SDK强制在 ResultFilter.FilterAsync() 方法中要求传入一个本地的 KeyCache 引用,就是防止你误传密钥。

3.3 组件交互流程

现在我们看看这些组件怎么配合。我把一次完整的索引+查询流程画成表格,方便你对照SDK的API调用顺序:

步骤组件执行动作关键数据
1客户端接入层调用 InitSDK(keyConfig)密钥上下文、算法参数
2基础支撑层从密钥管理获取主密钥,派生引擎密钥会话密钥(临时)
3加密引擎对文档内容EncryptDocument()密文、盐、密文列表
4索引引擎将密文写入倒排索引密文 → 文档ID映射
5查询转换引擎解析用户查询,生成检索计划查询 + 范围边界
6服务端根据检索计划执行密文检索候选文档ID列表
7结果过滤器解密候选文档,去噪、排序、分页最终明文结果

提醒:第3步和第4步在一次性建索引时是连续的,但在增量更新场景下,EncryptDocument 和 AppendDocument 可以分开调用。我自己做电商商品索引时,每天凌晨跑全量重建,实时商品则走增量 — 这时候必须保证增量使用的密钥和全量是同一套,否则索引就分裂了。

3.4 避坑指南

最后,我总结几个在架构设计阶段容易踩的坑,都是我曾经用线上事故换来的教训:

  • 密钥与引擎分离不彻底:加密引擎内部不要缓存主密钥,每次调用都应该从密钥管理组件拉取密钥上下文。否则轮换密钥时缓存导致旧密钥继续生效。
  • 索引引擎的线程安全:如果你的SDK支持多线程并发构建索引(比如并行处理多个文档),一定要给倒排索引加读写锁。我遇到过并发写入导致索引链表断裂,查询时挂死。
  • 查询转换的语法解析不要复用明文解析器:明文解析器只认纯文本,但密文检索需要处理和加密边界的组合。我曾经偷懒直接复用了一个开源语法树,结果范围查询的上下界加密不对,整个模块返工。
  • 结果过滤器的超时处理:如果服务端返回的候选集过大(比如布隆误判率飙高),滤波器逐个解密可能会超时。建议设置最大候选条数,超限时让用户重新限定查询条件,而不是无限等待。