「零知识」不是一句营销话术,而是一组可以被第三方验证的工程约束。这篇文章把源钥从你输入主密码、到密文落盘的整条链路逐段拆开,说明我们为什么看不到你的数据,以及这种「看不到」是如何在代码里被保证的。
「零知识」到底在说什么
零知识(Zero Knowledge)最早是密码学里的形式化证明:证明者让验证者相信「我知道某个秘密」,但验证者既无法猜出秘密,也无法从证明过程中得到任何关于秘密的信息。放到产品语境里,它落到一句可以被验证的话——源钥的服务器只保存密文,任何人(包括源钥员工)都无法把密文还原成你的明文条目。
这个承诺必须能被第三方独立核查才有意义,所以我们把客户端的加密实现完整开源:密钥派生、条目序列化、同步协议、自动填充路径全部可读可改。任何人都可以在自己的机器上审计代码,甚至完整复现一遍源钥的加密流程,看看实际跑出来的密文与我们描述的是否一致。
把话说得更直白一点:如果某天源钥团队想读取你的保险库,唯一的办法是拿到你的设备、你的主密码,或者在客户端里埋一个后门。前两者取决于你,后者在开源仓库里藏不住。
- 服务器只保存密文与运行服务所必需的少量元数据
- 加解密全部发生在你的设备上,密钥不进入网络
- 没有万能密钥、没有后门、也没有「客服代查」通道
从主密码到密钥:PBKDF2 与 Argon2id 的取舍
主密码本身从不上传。它在你的设备上经过一次「密钥派生」:配合每账户独立的随机盐值,通过 PBKDF2-HMAC-SHA256 拉伸成 256 位主密钥。这个过程被刻意设计得很慢——慢是安全特征,不是性能缺陷,因为攻击者拿到密文后面对的是同样的计算量。
当前 Web 端默认使用 PBKDF2-HMAC-SHA256、600,000 次迭代、16 字节随机盐。桌面与移动客户端额外提供 Argon2id 选项(推荐参数 m=64 MiB、t=3、p=4),它在内存受限的移动设备上比 PBKDF2 更能抵抗 GPU 与 ASIC 的并行破解。我们让参数可升级:客户端可以在后续版本里提高迭代次数,你解锁一次即完成重派生,密文不需要迁移。
盐值的作用是让相同的主密码在不同账户上得到完全不同的密钥。没有盐,攻击者可以对所有用户跑一遍字典;有了盐,每一次哈希都是一次独立的高成本计算。它不保密,但必须唯一且随机。
- 盐值:每个账户 16 字节随机数,加密前写入账户记录
- 迭代次数:随客户端版本提升,可平滑升级,不需要重设密码
- 主密钥只存在于内存,锁定、超时或退出即被清除
salt = CSPRNG(16 bytes)
master = PBKDF2-HMAC-SHA256(masterPassword, salt, 600000, 256 bits)AES-256-GCM:为什么是 GCM 而不是 CBC
条目数据使用 AES-256-GCM 加密。它同时提供机密性与完整性:GCM 在加密的同时用 GHASH 计算认证标签,任何一个比特被改动,解密都会直接失败并抛出错误,而不是悄悄产出一段看似正常的垃圾明文。这一步很重要——它让密文篡改成为可检测事件。
我们没有选择 AES-CBC。CBC 只保证「密文没被改」,不保证「明文没被换」,需要额外套一层 HMAC,还要小心比较顺序避免时序侧信道;GCM 把两件事合并成一条 AEAD 路径,实现更小,也更难用错。
每条保险库拥有独立的数据密钥(data key),不同保险库之间不共享密钥。条目级别的密钥由数据密钥派生,所以单独导出某个条目不会连带暴露整库;数据密钥可以轮换,而不需要用户更换主密码。
- 256 位密钥长度,128 位认证标签
- 解密失败即报错,绝不返回部分明文或降级结果
- 条目密钥与保险库密钥相互独立,单条泄露不影响全库
nonce = CSPRNG(12 bytes)
ciphertext, tag = AES-256-GCM(dataKey, plaintext, nonce, aad)IV 与 nonce:每次写入都必须换新的随机数
GCM 要求同一把密钥下 nonce 绝不可重复。一旦重复,安全性会崩塌:在特定条件下,攻击者可以恢复出密钥流,并伪造出通过认证的密文。源钥的做法是每次加密都生成新的 96 位随机 nonce,取自系统密码学安全随机数发生器,并把它与密文一起存储,解密时原样取回。
客户端不会复用 nonce,即便同一条目被反复编辑——每一次写入都是一次全新的加密。这带来一个实际后果:条目历史版本无法通过「解密旧版本再比对」来实现,我们改为在服务端保留旧的密文版本(仍然是密文),由客户端在需要时分别解开。
附件走的是另一条路径:文件被切成固定大小的块,每块独立加密并使用独立 nonce,避免单块数据超过 GCM 的安全上限,也让上传失败时可以从断点续传而不必重来。
- 96 位随机 nonce,来源为系统 CSPRNG,绝不计数复用
- 加密失败不重试写入,直接提示用户,避免出现状态不一致
- 附件分块独立加密,支持断点续传
主密钥、账户密钥与条目密钥的三层结构
实际运行的密钥是分层的:主密码派生出主密钥,主密钥解开账户密钥,账户密钥解开保险库数据密钥,数据密钥再派生出各条目的密钥。每一层都用 HKDF-SHA256 按「用途」派生子密钥,不同用途之间互不相关——换密码的密钥与同步的密钥不是同一把。
分层的意义在于把风险隔离:某一层被攻破,损失被限制在该层以内。数据密钥可以轮换而用户无感;条目密钥泄露只影响那一条目;不同功能的密钥空间互不重叠,攻击者无法用一个被攻破的上下文去推断另一个。
这也是为什么我们的「主密码强度」提示主要针对人类因素:真正决定安全边界的是盐值、迭代次数与内存硬度这些由我们控制的参数,而主密码只需要足够长、足够独特、不被复用。
- 主密码 → 主密钥 → 账户密钥 → 数据密钥 → 条目密钥
- 每一层按用途使用 HKDF 派生子密钥,互不相关
- 数据密钥可独立轮换,用户不需要重新设置主密码
服务器实际保存了什么
我们的数据库里只有这些东西:用于查找账户的邮箱规范化哈希、账户的 KDF 参数与盐值、加密后的保险库信封、加密后的条目、加密后的附件分片,以及同步所需的设备列表与时间戳。
我们不保存明文密码、不保存主密码、不保存恢复码,也不保存任何一把能直接解开条目的数据密钥。邮件地址本身默认加密存储,但为了在登录时快速定位账户,需要保留一个不可逆的查找哈希——这也是我们建议对不同身份使用不同别名邮箱的原因之一。
说实话,服务器仍然能看到一些元信息:你的账户是否存在、密文体积有多大、什么时候同步过、多久没上线。这些是提供同步服务绕不开的,也是我们判断是否需要引入盲索引等进一步方案时反复评估的边界。
- 看得到:账户是否存在、密文大小、同步时间戳
- 看不到:任何条目的明文、任何可解密的密钥
- 也看不到:你的邮箱明文(开启全盲注册后连查找哈希都不保留)
边界:我们能做和不能做的事
零知识保护的是「服务器读不到你的数据」,它不能替你解决本机感染、钓鱼与弱密码这三类问题。装在你电脑上的恶意扩展,仍有可能在你解锁之后读取内存中的明文——这是零知识模型的固有边界,任何声称能解决它的方案都需要你交出解密能力。
同样诚实地说明另一半:如果你忘记了主密码,服务器上没有你的密钥,我们无法代为恢复;如果你在注册时没有设置紧急访问、也没有保存恢复码,那么数据在实践中等同于丢失。这不是产品缺陷,而是零知识的必然代价——能找回,就意味着有人能替你找。
所以我们把「设置紧急访问」写进新手引导,而不是藏进设置深处。它是零知识体系里唯一合理的恢复路径。
把这篇文章分享给同事
如果你正在推动团队迁移到通行密钥,这篇可能正好用得上。
林知远
首席技术官 / 加密架构
密码学与客户端架构出身,负责密钥分层、同步协议与开源仓库。
关注方向:加密与客户端