89 lines
5.7 KiB
Markdown
89 lines
5.7 KiB
Markdown
# 加密设计说明(写代码之前必须先定的两件事)
|
||||
|
|
|
|||
|
|
这份说明是给"后面考虑加加密"准备的。它不实现任何东西,只把两个**必须先决定**的
|
|||
|
|
约束摆在前面——因为它们都会反过来改数据结构,等到写完再发现就要返工。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、去重与随机 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` 已有骨架),保证加算法不破坏现有往返。
|