Expand description
树形模式的目录名加密(文档 05 §3.2、§3.3)。
§为什么目录名需要单独一套机制
文件名有地方藏——写进该文件自己的 TLV_FILENAME,磁盘上的名字随便叫
什么都行。目录名没有这个奢侈:它就是磁盘上的一个真实路径组件,
没有任何文件头可以承载它,只能把密文编码进名字本身。
这带来了三个约束,下面每一条都是踩过的坑或明确的设计决策。
§一、必须用 base32,不能用 base64
这不是风格偏好,两条都是硬伤:
| 问题 | base64 | base32 |
|---|---|---|
含 / | 是,是路径分隔符 | 否 |
| 大小写敏感 | 是 | 否(全大写) |
大小写那条尤其致命:Windows 与 APFS 默认大小写不敏感,base64 下
两个只差大小写的密文目录名(aB... 与 Ab...)在磁盘上会直接撞车,
而且是随机撞——同一个明文目录名每次加密的 nonce 不同,密文不同,
只在偶然差一个字母大小写时才炸。这种缺陷在测试里几乎不可能复现。
代价是长度膨胀 1.6×(base64 是 1.33×),见下条。
§二、长度限制会真的撞上
多数文件系统单个路径组件上限 255 字节。我们的密文结构是
nonce(12) || ciphertext || tag(16),再乘 base32 的 1.6×,
于是原始目录名的可用长度只剩约 130 字节——中文按 UTF-8 算一字三节,
也就是 40 多个汉字。这不是极端情况,「2024年第三季度市场调研原始
数据备份」这种目录名很常见。
超长的处理:截断 base32 输出 + 追加哈希后缀保证唯一性。此时完整
目录名无法从名字本身恢复,所以额外在该目录下放一个
DIRNAME_SIDECAR 文件存完整密文。这是不得已的妥协,但比「加密到
一半突然报名字太长」要好。
§三、密钥是 vault 级的,不是文件级的
目录名不属于任何单个文件,无法用某个文件的 FEK。所以用 KEK 直接派生:
dirname_key = HKDF(KEK, salt=vault_salt, info="omy/v1/dirname")这有一个必须写进文档、也必须在 UI 里告知的副作用:目录结构的 可见性绑定整个 vault,而不是单个文件的 slot。多密码场景下,能解开 vault 的任一密码都能看到完整目录树。如果有人期望「给同事一个 只能看部分内容的密码」,树形模式做不到——这是设计约束不是缺陷, 但不说清楚就会变成安全事故。
§为什么手写 base32 而不加依赖
与 crate::pack 里不引入 walkdir 同一个理由:core 的依赖目前只有
密码学与格式相关的几个 crate,保持这份克制让 core 能被任意项目复用,
审计面也更小。base32 编解码是三十行的位操作,不值得为它引入一个新的
供应链节点。
Structs§
- Dirname
Key - 目录名加密密钥。
- Encrypted
Dirname - 加密一个目录名的结果。
Constants§
- DIRNAME_
SIDECAR - 存放超长目录名完整密文的边车文件名。
- DIR_
SUFFIX - 密文目录名的后缀。
Functions§
- decrypt_
dirname - 解密目录名。
- decrypt_
dirname_ with - 用目录密钥解密目录名(两层结构)。
- encrypt_
dirname - 加密目录名。
- encrypt_
dirname_ with - 用目录密钥加密目录名(两层结构)。
- looks_
encrypted - 判断一个磁盘名是否可能是密文目录名。