Files
svfs/docs/encryption-design.md
T
chakcy 1620d3eb5a docs: 补 README 能力章节、缺陷报告与两份设计说明
- README:新增「数据能力」四节(内容寻址去重 / 快照与时间旅行 / 完整性校验与签名 /
  增量打包)与「发布到对象存储」一节,附实测对比表;重写「要不要用 --compress」
  (结论从"别用"反转为"默认就该开")、「迁移旧格式」;修正项目结构与字段表
- BUG_REPORT.md:13 节,含 P0/P1 逐条分析、P2/P3 清单、五批后续追加的记录,
  以及过程中被推翻的两处结论(第九节的压缩建议、P3-1 的 uv sync 判断)
- docs/encryption-design.md:加密的设计说明——去重与随机 nonce 的冲突、
  标准库没有密码学原语、以及现有 freq_blob 可作为每块附加数据的槽位
- docs/evidence-*.txt:各阶段的测试运行证据(修复前 / 修复后 / 双环境)
2026-09-14 00:11:53 +08:00

89 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 加密设计说明(写代码之前必须先定的两件事)
这份说明是给"后面考虑加加密"准备的。它不实现任何东西,只把两个**必须先决定**的
约束摆在前面——因为它们都会反过来改数据结构,等到写完再发现就要返工。
---
## 一、去重与随机 nonce 是冲突的
这是最要紧的一条。当前设计里 `blobs.hash` 是 `sha256(原始内容)` 并且是主键,
"相同内容只存一份"完全建立在这上面。加密会直接冲击它:
| 方案 | nonce 怎么来 | 去重 | 代价 |
|---|---|---|---|
| **A. 确定性(收敛)加密** | nonce 由 `HMAC(key, 明文hash)` 派生 | **保留** | 密文相同 ⇒ 泄露"这两份内容一样"(其实去重本来就泄露);可被确认攻击:拿到明文猜测的人能判断库里有没有这份内容 |
| **B. 每块随机 nonce** | `os.urandom` | **包内保留,跨包丢失** | 同一份内容在不同包里密文不同 → 对象名(当前是明文 hash)不再能当 URL;`vfsfetch` 的"名字即 sha256"校验失效;同一个 CDN 路径可能返回不同字节 |
| **C. 按文件加密** | 每个文件一份 | **完全丢失** | 相同的两个文件各存一份密文,体积翻倍 |
| **D. 只加密容器/对象,不动 blob** | 不适用 | 保留 | 服务方(插件/CDN 之外的那一层)拿不到明文;本地 `.svfs` 仍是明文 |
### 我的建议
**先明确你要防谁**,答案不同、方案不同:
* **防"存储方/传输方看到内容"**(S3、CDN、对象存储被拖库)→ 选 **D**:发布前把对象加密
(或直接用带 SSE 的桶 + TLS),本地 `.svfs` 保持明文。代价最低,完全不动现有结构,
而且**不影响去重与对象名**。这是我最推荐的起点。
* **防"本地库被拿走也读不出"**(笔记本丢了、镜像层被人翻)→ 需要 A 或 B:
* 如果你同时要"跨包去重 + 对象名稳定"(也就是现在的发布能力),只能选 **A**,
并接受确认攻击这个已知代价。这在"内容多半是公开前端资源"的场景里通常可以接受。
* 如果你只要"包内去重",**B** 更安全,但发布能力要跟着改:
对象名不能再用明文 hash,`manifest.json` 里得同时记录明文 hash 与密文 hash,
`vfsfetch` 改为校验"解密后再算 hash"(那就意味着客户端必须持有密钥)。
一句话:**A 与现在的发布/去重能力兼容,B 与它不兼容。这个岔路口必须先选。**
---
## 二、标准库没有密码学原语,所以"高效"指向一个依赖
Python 标准库只有 `hashlib` / `hmac` / `secrets` / `os.urandom`——**没有任何分组密码**。
所以:
| 选项 | 性能(量级) | 评价 |
|---|---|---|
| `cryptography`(pyca)的 AES-256-GCM | **1~5 GB/s**(AES-NI 硬件加速) | 推荐。单一依赖、AEAD 一次过、有认证 |
| 同库的 ChaCha20-Poly1305 | 1~3 GB/s(纯软件即很快) | 没有 AES-NI 的机器上更稳 |
| 纯 Python 实现 AES | **~1 MB/s** | 不可用(慢三个数量级),且自己写密码学实现本身就是风险 |
| 用 HMAC-SHA256 / SHA-256 计数器模式自建流密码 + HMAC 认证 | 约 0.5~1.5 GB/s | 构造上是成立的(PRF 派生 keystream + encrypt-then-MAC),但**必须**自己保证"同一密钥下计数器永不重复",且要正确实现认证——省一个依赖,多一份责任 |
**"尽可能高效"的实际含义**:用 AEAD(AES-GCM 或 ChaCha20-Poly1305),
而不是"自己写一个更快的"。自己写在 Python 里赢不了硬件指令。
顺带三条实现纪律:
1. **先压缩再加密**。压缩靠的是内容里的冗余,密文是随机-looking 的,压不动。
顺序反了等于白压。
2. **认证必须做**(AEAD 或 encrypt-then-MAC)。只加密不认证,攻击者可以翻动密文比特,
密文解出来就是"看起来合法但已被改动"的数据——而我们整个完整性体系(sha256 + 摘要
+ HMAC 签名)就是为了不让这种事发生。
3. **密钥不要进库**。库里的 `signatures` 表存的是 HMAC 签名,那是防篡改用的,
不是密钥保管。密钥从环境变量/密钥管理服务来。
---
## 三、现有结构对加密友好的地方
好消息是**几乎不用改 schema**:
* `blobs.freq_blob`(BLOB)本质是一个**通用的"每块附加数据"槽**。它现在是哈夫曼频率表,
加密的 nonce + 认证标签可以放这里,不需要加列。
* `blobs.compress_algo`(TEXT)现在存 `'none' / 'zlib' / 'huffman'`。加一档 `'aes-gcm'`
就走通了——`compression.py` 是唯一的算法分发点(见该模块 docstring 的契约说明)。
* 如果"先压后加"需要同时表达两层,建议**加一列 `encrypt_algo`** 而不是把算法名拼成
`'zlib+aes-gcm'` 这种字符串:拼字符串会让 `decompress_with` 的分发逻辑变脆,
而且查询"这个包里加密了吗"要靠 LIKE。加列的成本是一次 v4 迁移(迁移机制已经在了,
`migration.py` + `vfsmigrate`)。
---
## 四、建议的推进顺序
1. **先只做 D**(发布层的信封加密):不动 blob、不动去重、不动对象名。
验证一下"发布到不可信存储"这个需求是不是就满足了——大概率是。
2. 如果确实需要"静态加密整个库",再做 A(确定性 nonce),并接受确认攻击;
这时要同步改:`manifest.json` 增加密文字段、`vfsfetch` 的校验时机、
以及"未授权方拿不到密钥就什么也读不出"的密钥分发方案。
3. 无论走哪条,先给 `compression.py` 加一层算法链的**契约测试**
(`tests/test_compression.py` 已有骨架),保证加算法不破坏现有往返。