systemslibrarian/crypto-lab-search-vault
GitHub: systemslibrarian/crypto-lab-search-vault
一个在浏览器中演示可搜索对称加密(SSE)原理及其访问模式泄漏风险的教学项目,包含真实密码学原语实现的加密索引构建、搜索查询和泄漏滥用攻击复原。
Stars: 0 | Forks: 0
# 搜索 Vault
**可搜索对称加密 · 加密关键字搜索**
一种可搜索加密技术,服务器可以在不知道查询内容或数据的情况下,在加密数据库中找到匹配的记录——但它观察到的访问模式本身就是一个攻击面。
**在线演示:** https://systemslibrarian.github.io/crypto-lab-search-vault/
## 它是什么
一个加密的倒排索引,在你的浏览器中构建和搜索,外加已公开的、能将其泄漏信息还原为你原始查询的攻击方法。
该方案是一个单关键字 SSE 构造,采用 Song–Wagner–Perrig (2000) / Curtmola–Garay–Kamara–Ostrovsky (SSE-1, 2006) 风格,全程使用真实的密码学原语:
| 数值 | 派生方式 | 原语 |
| --- | --- | --- |
| 搜索 token `t_w` | `HMAC(K_prf, "token" ‖ w)` | HMAC-SHA-256 (RFC 2104 / FIPS 198-1) |
| 倒排列表 key `k_w` | `HMAC(K_enc, "posting" ‖ w)` | HMAC-SHA-256 |
| 索引地址 `addr_i` | `HMAC(t_w, "address" ‖ i)` | HMAC-SHA-256 |
| 倒排列表值 | `AES-256-GCM(k_w, docId)`, AAD = `addr_i` | AES-256-GCM (NIST SP 800-38D) |
| 文档正文 | `AES-256-GCM(k_d, title ‖ body)` | AES-256-GCM |
所有这些都通过 WebCrypto 运行。密钥通过 `crypto.getRandomValues` 在每次会话中生成,保存在内存中,且从不持久化或传输。
**安全模型。** 服务器是*诚实但好奇的*:它遵循协议并记录它看到的一切。它无法逆推出 token(它不持有任何密钥),也无法读取倒排列表或文档(AES-GCM)。按照设计,它确实能看到的是**泄漏轮廓**:
- **搜索模式** — token 是确定性的,因此重复查询是可关联的;
- **数量** — 一个 token 匹配了多少文档;
- **访问模式** — 确切是哪些文档匹配了,因为它必须把这些文档交出来。
第三点正是本演示的主题。泄漏滥用攻击(Islam–Kuzu–Kantarcioglu, NDSS 2012; Cash–Grubbs–Perry–Ristenpart, CCS 2015)在已知类似语料库背景统计信息的情况下,仅凭模式即可重构查询。这两种攻击都在此实现,并针对你使用页面时实际累积的日志运行。
**不是生产级加密 —— 这是一个教学演示。** 语料库特意设置得很小(24 个文档,14 个关键字),以便攻击在你观看时完成,索引是静态的,且“客户端”和“服务器”是同一页面中的两个对象,而不是两台主机。
## 展示区
1. **构建加密索引** — 逐步展示一个关键字如何变成一个加密的倒排索引,每次进行一次真实的 PRF 调用:关键字 → token → 倒排列表 key → 地址 → 加密封装值 → 一行数据落入服务器的存储中。显示的十六进制是保险库自身的真实字节,而非模拟重演。在它旁边是服务器的完整存储:55 行数据,没有任何信息说明哪些行属于同一个关键字。
2. **搜索它 —— 并观察服务器学到了什么** — 运行一次真实的查询并同时查看两端。客户端列显示关键字和解密后的结果;服务器列显示它接收到的 32 字节,它执行的地址遍历(`addr_0`、`addr_1`、…… 直到未命中),以及它返回的文档标识符。一个持续运行的日志记录了每一次查询。搜索未被索引的关键字会显示未命中的路径。
3. **单凭模式揭示了什么** — 仅根据日志构建的三个视图:查询频率直方图(搜索模式)、结果集重叠图(访问模式)和文档共现矩阵。以及出现在某个结果集中的数据库比例。
4. **破解它:将访问模式还原为查询** — 计数攻击会锁定任何在关键字全域中结果大小唯一的 token;IKK 攻击通过模拟退火将观察到的 token 共现矩阵与已知语料库的矩阵进行匹配。一个滑块会降低对手的背景知识水平,让你观察恢复率的下降过程。每个 token 的表格报告了对手的推断、其置信度以及该 token 实际代表的含义。
5. **轮到你了:你是服务器** — 你将获得 token、它们的结果大小、它们的重叠情况,以及攻击所获得的相同公开统计数据。手动为 token 分配关键字,为自己打分,然后让机器尝试解开同一个谜题。
6. **SSE、ORAM、FHE** — 它们各自隐藏了什么以及各自的代价。SSE 延迟列是在你的浏览器中针对此保险库测量的;ORAM 和 FHE 列被标记为引用的、数量级估计的数值,因为本页面两者都不运行。
此外还有一个**范围**面板,说明了哪些是真实的,哪些是缩减规模的,这不能证明什么,以及什么是故意超出范围的。
## 何时使用它
**在以下情况使用可搜索对称加密:**
- 你必须搜索一个大型的加密存储,并且每次查询的成本必须随*匹配项*的数量成比例增长,而不是随数据库的大小增长;
- 你关注的威胁是读取静态数据的被动主机,或存储系统的违规泄露;
- 你能容忍服务器知道哪些记录匹配了查询,并且你已经考虑过这对你的数据意味着什么。
**在以下情况请勿使用:**
- **查询本身是你必须防范存储提供商以保护的核心机密。** 这正是本演示旨在说明的情况:在拥有关于你语料库的背景知识的情况下,访问模式是可以被逆推的。如果你的查询是敏感的(记者的线人、患者的诊断代码),仅靠 SSE 是错误的工具——你需要 ORAM、PIR,或者干脆不外包索引。
- 你需要联合查询或排名查询,并假设泄漏情况没有改变。事实并非如此;越丰富的查询泄漏得越多。
- 你的索引在不断变化,而你尚未选择具有前向和后向隐私保护机制的方案。
- 你想尝试使用保序加密或确定性加密来让 SQL 正常工作。那比这种方式泄漏得多得多。
## 在线演示
https://systemslibrarian.github.io/crypto-lab-search-vault/
你可以:逐步查看 14 个关键字中任何一个的索引构建过程;搜索真实和不存在的关键字,看着服务器的日志被填满;观察一轮真实倾斜的查询;阅读三个泄漏视图;在任意级别的对手背景知识误差下运行这两种攻击;尝试自己将 token 去匿名化,并与机器的得分进行比较;并在你自己的浏览器中对 50 次真实搜索进行计时。
## 可能出现的问题
- **访问模式泄漏不是旁枝末节。** 拥有精确的背景统计数据时,展示区 4 中的攻击能在几十毫秒内恢复全部 14 个查询。即使对手的统计数据退化到远远超过现实误差的水平,大多数 token 依然会被攻破。
- **仅凭数量泄漏就足以造成问题。** 任何文档计数在词汇表中唯一的关键字,都会在不进行任何共现分析的情况下,仅凭结果大小被识别出来。填充结果集是标准的应对措施,但这需要消耗存储空间和带宽。
- **确定性 token 随着时间推移将查询关联起来。** 服务器无法读取 token,但它能识别出每一次重复,因此它可以建立每个用户的查询频率画像,并将其与公开的词频数据进行比对。
- **背景知识假设比听起来要弱。** 对手只需要一个*类似*语料库的统计数据,而不是你的——泄露的档案、公开的文件以及同类组织都能提供这些信息。
- **静态索引隐藏着持续存在的危险。** 本演示只构建一次索引。除非方案具有明确的前向隐私保护,否则以后添加文档时会泄漏新文档是否匹配了之前的查询。
- **篡改会导致闭环失败,这是刻意为之。** 每个倒排列表都以其自身的地址作为关联数据进行封装,因此服务器一旦重新定位、伪造或翻转任何一行数据,都会导致整个结果被拒绝,而不是静默地给出错误答案。有六个测试覆盖了这条路径。
## 实际应用
当全同态加密太慢时(这几乎是常态),可搜索加密便是得以部署的技术。它的各种变体出现在加密数据库产品、加密搜索设备,以及仍然提供搜索框的客户端加密笔记和邮件服务中。学术脉络贯穿 Song–Wagner–Perrig (2000) → Curtmola 等人的 SSE-CKA 安全定义 (2006) → Cash–Jarecki–Jutla–Krawczyk–Roşu–Steiner (2013) 的 OXT 联合查询方案 → 重塑了该领域预期的泄漏滥用攻击 (Islam–Kuzu–Kantarcioglu 2012, Cash–Grubbs–Perry–Ristenpart 2015) → 在其影响下设计的前向和后向隐私动态方案。
持久的教训正是本演示所围绕构建的核心:对于 SSE 来说,**泄漏轮廓即是安全模型。** 一个方案在抽象意义上不是“安全”或“不安全”的——它只有在*相对于既定的泄漏函数*时才谈得上安全,而这种泄漏是否可以接受,是一个关于你的数据和你的对手的问题,而不是关于密码学本身的问题。
## 如何在本地运行
```
npm install
npm run dev # http://localhost:5173
npm test # unit tests, including the spec KATs
npm run build # typecheck + production build
npm run test:a11y # axe-core WCAG 2.1 AA gate against the production build
```
`npm run test:a11y` 会自动在 **4237** 端口启动 `vite preview`,因此扫描的内容就是最终发布的内容。
## 相关演示
- [crypto-lab-oram-vault](https://systemslibrarian.github.io/crypto-lab-oram-vault/) — Path ORAM:隐藏本演示所泄漏的访问模式。
- [crypto-lab-psi-gate](https://systemslibrarian.github.io/crypto-lab-psi-gate/) — 私有集合求交:在不泄露非匹配元素的情况下对两个私有集合进行计算。
- [crypto-lab-fhe-arena](https://systemslibrarian.github.io/crypto-lab-fhe-arena/) — 全同态加密:同一权衡中代价高昂的一端。
## 构建与验证
**77 个单元测试**,全部通过,由 `npm test` 运行并作为部署的门禁。
**11 个规范已知答案向量:**
| 文件 | 向量 |
| --- | --- |
| `src/core/prf.test.ts` | 6 × HMAC-SHA-256,来自 RFC 4231 §4(测试用例 1, 2, 3, 4, 6, 7) |
| `src/core/prf.test.ts` | 3 × SHA-256,来自 FIPS 180-4(空消息、`"abc"`、448 位双块消息) |
| `src/core/aead.test.ts` | 2 × AES-256-GCM,来自 GCM 规范的向量集(测试用例 13 和 14),双向均已验证 |
除了 KAT 之外,测试套件还涵盖了:索引构建(每个关键字-文档对一个倒排列表,不同的伪随机地址,没有两个完全相同的密文);语料库中每个关键字的搜索正确性;未索引关键字和随机 token 的未命中路径;六个闭环失败案例(翻转的密文、被篡改的 IV、错误的密钥、截断的 blob、重新定位的倒排列表、伪造的倒排列表);服务器账本记录和未记录的内容;泄漏计算——包括一个类似证明的检查,验证观察到的共现矩阵与明文矩阵*完全一致*,以及一个颜色细化检查,验证语料库中没有两个关键字在信息论上是可互换的;以及攻击——在精确背景知识下的完全恢复、在噪声下的优雅降级、在固定种子下的确定性,以及对格式错误输入的拒绝。
**无障碍门禁:** `@axe-core/playwright` 在**两种主题**下通过四次扫描检查生产构建是否存在 WCAG 2.1 A/AA 违规——每个展示区都被驱动到其交互后的状态,以及作为每个条件渲染另一个分支的空/未命中状态。要求违规数为零;回归问题将阻止部署。
## 性能
搜索速度很快,因为 SSE 让服务器能够进行普通的索引查找:工作量与匹配文档的数量成正比,而不是与数据库的大小成正比。展示区 6 对其进行了实时测量——50 次完整搜索,每次涵盖 token 派生、地址探测、AES-GCM 倒排列表解密和文档获取——并报告你的浏览器的单次搜索数据。在现代笔记本电脑上,它可以在低个位数的毫秒级内完成。
攻击的成本同样低廉:计数攻击是一次表查找,而 IKK 退火(8 次重启 × 12,000 次迭代,外加贪婪法润色)能在几十毫秒内解析出 14 个 token。攻击成本低廉正是问题的关键所在。
*这是 [Crypto Lab](https://crypto-lab.systemslibrarian.dev/) 套件中 170 多个浏览器演示之一。*
*“所以,你们或吃或喝,无论做什么,都要为上帝的荣耀而行。” —— 哥林多前书 10:31*
标签:WebCrypto API, 前端加密, 可搜索加密, 密码学, 手动系统调用, 数据隐私, 特征检测, 自动化攻击