fix: decrypt against current Bitwarden cloud (headers, 2FA, camelCase, cipher keys)#46
Merged
Merged
Conversation
…, cipher keys)
Daily-driving Vault against Bitwarden's hosted service surfaced a chain of
incompatibilities with the modern API — each masked the next, so they only
became visible one at a time. None were reachable by the wiremock fixtures,
which encoded an older API shape.
vault-api (login / identity):
- Send the Bitwarden client-identification headers (`Bitwarden-Client-Name`,
`Bitwarden-Client-Version`, `Device-Type`) on every request. The hosted
server rejects requests without `Bitwarden-Client-Version` (HTTP 400
`version_header_missing`) on the post-auth path, so a correct password
failed *after* authenticating.
- Parse the 2FA challenge tolerantly: the hosted server sends provider ids as
JSON strings (`["0","7"]`), which a strict `Vec<u32>` rejected — failing the
whole TwoFactorErrorBody parse and surfacing a real 2FA challenge as
`invalid_grant` ("bad password").
vault-api + vault-core (sync / decryption):
- `/sync` and every nested cipher are camelCase on hosted Bitwarden and
Vaultwarden; the wire structs were PascalCase, so every field fell to its
`#[serde(default)]` and a full vault synced as zero items. Flip the
API-service structs to camelCase (identity-service structs stay PascalCase,
matching that endpoint).
- Support "cipher key encryption": when a cipher carries its own `key` (the
default for current vaults), its fields are encrypted under that per-item
key — wrapped under the user key — not the user key directly. Decrypting
with the user key failed MAC on every such item.
vault-agent (resilience):
- Skip ciphers that fail to decrypt (e.g. organization items, whose keys are
wrapped under an org key Vault doesn't hold) in `list`/`get`/resolve instead
of aborting the whole operation; tally a one-line count to the log. Also log
the raw server body on a login 400 so an opaque "bad password" can't mask an
actionable response again.
vault-cli:
- `vault sync` shows a TTY spinner while the agent pulls and decrypts `/sync`,
then "synced N items"; suppressed under `--json` / non-TTY.
Tests cover the camelCase sync shape, the string-id 2FA body, cipher-key
round-trips, and search/list tolerance. No new dependencies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Daily-driving Vault against Bitwarden's hosted service surfaced a chain of incompatibilities with the modern API — each masked the next, so they only became visible one at a time (none were reachable by the wiremock fixtures, which encoded an older API shape).
Fixes
Login (
vault-api)Bitwarden-Client-Name,Bitwarden-Client-Version,Device-Type) on every request — the hosted server rejects a missingBitwarden-Client-Versionwith400 version_header_missingon the post-auth path, so a correct password failed after authenticating.["0","7"]); a strictVec<u32>failed the whole parse and surfaced a real 2FA challenge asinvalid_grant("bad password").Sync & decryption (
vault-api+vault-core)/syncand every nested cipher are camelCase on hosted Bitwarden and Vaultwarden; the wire structs were PascalCase, so a full vault synced as zero items. API-service structs flipped to camelCase (identity-service structs stay PascalCase, matching that endpoint).key(wrapped under the user key), not the user key directly — decrypting with the user key failedMAC verification.Resilience (
vault-agent)list/get/resolve skip ciphers they can't decrypt (e.g. organization items, whose keys are wrapped under an org key Vault doesn't hold yet) instead of aborting the whole op; a one-line count is logged. The raw server body is logged on a login 400.UX (
vault-cli)vault syncshows a TTY spinner → "synced N items" (quiet under--json/ non-TTY).Verification
Full
just cigreen (fmt, clippy-D warnings, test). New tests cover the camelCase sync shape, the string-id 2FA body, cipher-key round-trips, and search/list tolerance. Verified live against Bitwarden cloud: login (password + TOTP), sync (2274 items), list, get, reveal/copy. No new dependencies.Known follow-up
Organization/Collection items (the bulk of a vault that uses Collections) are currently skipped — org-key (RSA) support is the next PR.
🤖 Generated with Claude Code