从 vaultwarden 数据库直接解密凭据:三个 KDF 相关的坑
为什么要直接读密码库
自动化登录最大的摩擦点是凭据:账号密码存在密码库里,脚本却拿不到。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 的关键数据分三层:
users.akey是用主密码派生的密钥加密的用户密钥- 用户密钥(64 字节,enc32+mac32)用来解密所有条目
- 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 权限的临时文件里,用完即删。
- 点赞
- 收藏
- 关注作者
评论(0)