从 vaultwarden 数据库直接解密凭据:三个 KDF 相关的坑

举报
yd_268799136 发表于 2026/11/01 15:16:52 2026/11/01
【摘要】 密码库是零知识加密,但库文件和主密码都在手里时可以在本地解开。记三个踩过的坑:邮箱当盐、prelogin 参数与库不一致、两步验证怎么过,附完整解密链路。

为什么要直接读密码库

自动化登录最大的摩擦点是凭据:账号密码存在密码库里,脚本却拿不到。Bitwarden 兼容的 vaultwarden 是零知识加密,服务端只存密文,所以要么走官方客户端(需要主密码交互),要么自己在本地把密文解开。

这次在本地解 vaultwarden 的库,把账号密码和 TOTP 种子直接取出来用,踩了三个坑,记一下。

坑一:邮箱写错也会显示「欢迎回来」

vaultwarden 的 KDF 用邮箱作为盐。邮箱错一个字符,算出来的 master key 完全不同,登录自然失败。

问题是登录页对不存在的邮箱也返回「欢迎回来 xxx@gmail.com」,提交密码后才报「无效的主密码」。我先按自己常用的邮箱试,一直报密码错误,实际库里存的是另一个地址。翻数据库才看到:

select email from users;

教训:别猜邮箱,先从库里取。

坑二:prelogin 返回的 KDF 参数和数据库不一致

Bitwarden 的登录流程第一步是 POST /identity/accounts/prelogin 拿 KDF 参数。我第一次拿到的响应是:

{"kdf":1,"kdfIterations":3,"kdfMemory":64,"kdfParallelism":4}

看起来是 Argon2id(kdf=1),于是按 Argon2 算,怎么都不对。后来直接查库:

select client_kdf_type, client_kdf_iter from users;
-- 0 | 600000

数据库里是 PBKDF2-SHA256,600000 次迭代(kdf=0)。换成 PBKDF2 后立刻变成「需要两步验证」,说明密码已经对了。

那次 prelogin 拿到 Argon2 参数的原因没深究,可能是缓存或参数异常。总之以数据库为准更可靠。

坑三:两步验证怎么过

过了密码这一关,响应变成:

{"TwoFactorProviders":["0"],"error":"invalid_grant","error_description":"Two factor required."}

0 是 TOTP。想用恢复码(provider 8)绕过去,试了大小写三种写法都是 400。

真正的解法是不走登录接口:直接本地解密。

完整解密链路

vaultwarden 的关键数据分三层:

  1. users.akey 是用主密码派生的密钥加密的用户密钥
  2. 用户密钥(64 字节,enc32+mac32)用来解密所有条目
  3. TOTP 种子存在 twofactor.data,vaultwarden 里是明文 base32

第一步,主密码派生:

master_key = PBKDF2-SHA256(password, email, 600000, 32)
stretched  = HKDF-Expand(master_key, "enc", 32) + HKDF-Expand(master_key, "mac", 32)

第二步,解 akey。akay 是 Bitwarden 的 EncString 格式 2.<iv>|<ct>|<mac>,用 stretched 的后 32 字节校验 HMAC、前 32 字节做 AES-256-CBC 解密,得到 64 字节用户密钥。

iv   = ct[:16]; body = ct[16:-32]; mac = ct[-32:]
expect = HMAC-SHA256(stretched[32:], iv + body)
assert mac == expect
userkey = AES256CBC_decrypt(stretched[:32], iv, body)

第三步,条目。vaultwarden 的 ciphers.data 是一个 JSON 字符串,里面的 username / password / totp 各是独立的 EncString,用用户密钥解。

d = json.loads(row.data)
username = decrypt(d["username"], userkey)
password = decrypt(d["password"], userkey)
totp     = decrypt(d["totp"], userkey)   # otpauth:// URI

第四步,用 TOTP 种子生成动态码,走正常的带 twoFactorProvider=0 的登录请求,拿到 access_token,之后就都是普通 API。

几个值得注意的细节

  • EncString 的 type 决定算法:2 是 AES-256-CBC + HMAC-SHA256(当前默认),0/1 是纯 AES-CBC 没有 MAC 校验。
  • TOTP 可能没有加密:twofactor.data 里是 32 位 base32 明文,直接可用。这条不保证所有部署都一样,先看有没有 . 分隔判断格式。
  • PBKDF2 的迭代次数差别很大:600000 次在普通机器上约 0.3 秒,用 Argon2id 也是同一量级。
  • 零知识加密的前提是主密码保密:能本地解库,说明谁拿到库文件和主密码,谁就拿到全部明文。库文件权限和备份加密比什么都重要。

实践建议

  • 先 select email, client_kdf_type, client_kdf_iter from users 确认参数,别信接口返回值。
  • 登录报「密码错误」时,先核对邮箱,再核对 KDF 类型,最后才怀疑密码。
  • 服务端日志能看到的只有密文和哈希,本地解密不需要联网,也不留请求痕迹。
  • 解出来的 TOTP 种子和密码要放在内存或 600 权限的临时文件里,用完即删。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。