Skip to main content

forge_connectors/
vault.rs

1//! The Vaultwarden connector: the Bitwarden client API this family talks to.
2//!
3//! The module owns the wire shapes ([`models`]), the encryption primitives
4//! ([`crypto`]) and the HTTP client ([`client`]). It holds no session state:
5//! the workspace's `VaultSession` owns the tokens and the unlocked account, and
6//! hands this module a token per call. An [`Unlocked`] value - the derived keys
7//! and the decrypt/encrypt operations over them - crosses into the workspace
8//! that holds it. Everything here is testable offline against a local server.
9
10pub mod client;
11pub mod crypto;
12pub mod models;
13
14pub use client::VaultClient;
15pub use crypto::Unlocked;
16pub use models::{
17    AttachmentMeta, Folder, Organization, Prelogin, Profile, SyncData, TokenResponse, TwoFactorCode,
18};
19
20use thiserror::Error;
21
22/// Everything the connector can fail with, named so the facade can branch on it
23/// without parsing strings. A 401 is the refresh trigger; a 404 names a missing
24/// item; a 429 carries the server's retry hint.
25#[derive(Debug, Error)]
26pub enum VaultError {
27    /// The failure class only. A signed download URL carries its token in the
28    /// query string, so no error may print the URL it was sent to.
29    #[error("request failed: {}", http_class(.0))]
30    Http(#[from] reqwest::Error),
31    #[error("no unlocked key for organization {0}")]
32    MissingKey(String),
33    #[error("the account has no private key, so its RSA-wrapped items cannot be opened")]
34    MissingPrivateKey,
35    #[error("the account's keys changed while unlocked; unlock again")]
36    KeyRotated,
37    #[error("the account's user key could not be verified; unlock again")]
38    KeyUnverifiable,
39    #[error("invalid request: {0}")]
40    InvalidInput(String),
41    #[error("refused attachment URL: {0}")]
42    InvalidUrl(String),
43    #[error("the attachment is {size} bytes, over the {cap} byte cap")]
44    AttachmentTooLarge { size: u64, cap: u64 },
45    #[error("vault is locked")]
46    Locked,
47    #[error("unauthorized")]
48    Unauthorized,
49    #[error("forbidden: {0}")]
50    Forbidden(String),
51    #[error("not found: {0}")]
52    NotFound(String),
53    #[error("rate limited; retry after {retry_after:?} seconds")]
54    RateLimited { retry_after: Option<u64> },
55    #[error("API error {status}: {body}")]
56    Api { status: u16, body: String },
57    #[error("crypto error: {0}")]
58    Crypto(#[from] anyhow::Error),
59    #[error("invalid response: {0}")]
60    InvalidResponse(String),
61}
62
63fn http_class(error: &reqwest::Error) -> &'static str {
64    if error.is_timeout() {
65        "timed out"
66    } else if error.is_connect() {
67        "could not connect"
68    } else if error.is_body() {
69        "the response body could not be read"
70    } else if error.is_decode() {
71        "the response could not be decoded"
72    } else if error.is_request() {
73        "the request could not be sent"
74    } else {
75        "the request failed"
76    }
77}