pub mod accounts; mod ciphers; mod emergency_access; mod events; mod folders; mod organizations; mod public; mod sends; pub mod two_factor; use std::collections::HashMap; use std::sync::{Mutex, OnceLock}; use crate::db::models::WebAuthnCredential; pub use accounts::purge_auth_requests; pub use ciphers::{purge_trashed_ciphers, CipherData, CipherSyncData, CipherSyncType}; pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job}; pub use events::{event_cleanup_job, log_event, log_user_event}; use reqwest::Method; pub use sends::purge_sends; pub fn routes() -> Vec { let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains]; let mut hibp_routes = routes![hibp_breach]; let mut meta_routes = routes![alive, now, version, config, get_api_webauthn, post_api_webauthn, post_api_webauthn_attestation_options]; let mut routes = Vec::new(); routes.append(&mut accounts::routes()); routes.append(&mut ciphers::routes()); routes.append(&mut emergency_access::routes()); routes.append(&mut events::routes()); routes.append(&mut folders::routes()); routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); routes.append(&mut sends::routes()); routes.append(&mut public::routes()); routes.append(&mut eq_domains_routes); routes.append(&mut hibp_routes); routes.append(&mut meta_routes); routes } pub fn events_routes() -> Vec { let mut routes = Vec::new(); routes.append(&mut events::main_routes()); routes } // // Move this somewhere else // use rocket::{serde::json::Json, serde::json::Value, Catcher, Route}; use rocket::http::Status; use webauthn_rs::proto::UserVerificationPolicy; use webauthn_rs::RegistrationState; use crate::{ api::{JsonResult, Notify, UpdateType}, auth::Headers, db::DbConn, error::Error, http_client::make_http_request, }; use crate::api::core::two_factor::webauthn::{RegisterPublicKeyCredentialCopy, WebauthnConfig}; use crate::api::{ApiResult, PasswordOrOtpData}; use crate::db::models::UserId; use crate::util::parse_experimental_client_feature_flags; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct GlobalDomain { r#type: i32, domains: Vec, excluded: bool, } const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json"); #[get("/settings/domains")] fn get_eq_domains(headers: Headers) -> Json { _get_eq_domains(headers, false) } fn _get_eq_domains(headers: Headers, no_excluded: bool) -> Json { let user = headers.user; use serde_json::from_str; let equivalent_domains: Vec> = from_str(&user.equivalent_domains).unwrap(); let excluded_globals: Vec = from_str(&user.excluded_globals).unwrap(); let mut globals: Vec = from_str(GLOBAL_DOMAINS).unwrap(); for global in &mut globals { global.excluded = excluded_globals.contains(&global.r#type); } if no_excluded { globals.retain(|g| !g.excluded); } Json(json!({ "equivalentDomains": equivalent_domains, "globalEquivalentDomains": globals, "object": "domains", })) } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct EquivDomainData { excluded_global_equivalent_domains: Option>, equivalent_domains: Option>>, } #[post("/settings/domains", data = "")] async fn post_eq_domains( data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { let data: EquivDomainData = data.into_inner(); let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default(); let equivalent_domains = data.equivalent_domains.unwrap_or_default(); let mut user = headers.user; use serde_json::to_string; user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string()); user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string()); user.save(&mut conn).await?; nt.send_user_update(UpdateType::SyncSettings, &user, &headers.device.push_uuid, &mut conn).await; Ok(Json(json!({}))) } #[put("/settings/domains", data = "")] async fn put_eq_domains(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { post_eq_domains(data, headers, conn, nt).await } #[get("/hibp/breach?")] async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult { let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect(); if let Some(api_key) = crate::CONFIG.hibp_api_key() { let url = format!( "https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false" ); let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?; // If we get a 404, return a 404, it means no breached accounts if res.status() == 404 { return Err(Error::empty().with_code(404)); } let value: Value = res.error_for_status()?.json().await?; Ok(Json(value)) } else { Ok(Json(json!([{ "name": "HaveIBeenPwned", "title": "Manual HIBP Check", "domain": "haveibeenpwned.com", "breachDate": "2019-08-18T00:00:00Z", "addedDate": "2019-08-18T00:00:00Z", "description": format!("Go to: https://haveibeenpwned.com/account/{username} for a manual check.

HaveIBeenPwned API key not set!
Go to https://haveibeenpwned.com/API/Key to purchase an API key from HaveIBeenPwned.

"), "logoPath": "vw_static/hibp.png", "pwnCount": 0, "dataClasses": [ "Error - No API key set!" ] }]))) } } // We use DbConn here to let the alive healthcheck also verify the database connection. #[get("/alive")] fn alive(_conn: DbConn) -> Json { now() } #[get("/now")] pub fn now() -> Json { Json(crate::util::format_date(&chrono::Utc::now().naive_utc())) } #[get("/version")] fn version() -> Json<&'static str> { Json(crate::VERSION.unwrap_or_default()) } static WEBAUTHN_STATES: OnceLock>> = OnceLock::new(); #[post("/webauthn/attestation-options", data = "")] async fn post_api_webauthn_attestation_options(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; // TODO what does delete_if_valid do? data.validate(&user, false, &mut conn).await?; // C# does this check as well // await ValidateIfUserCanUsePasskeyLogin(user.Id); // TODO add existing keys here when the table exists // let registrations = get_webauthn_registrations(&user.uuid, &mut conn) // .await? // .1 // .into_iter() // .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering // .collect(); let registrations = Vec::new(); let (challenge, state) = WebauthnConfig::load(true).generate_challenge_register_options( user.uuid.as_bytes().to_vec(), user.email, user.name, Some(registrations), Some(UserVerificationPolicy::Required), None, )?; WEBAUTHN_STATES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap().insert(user.uuid, state); let mut options = serde_json::to_value(challenge.public_key)?; options["status"] = "ok".into(); options["errorMessage"] = "".into(); // TODO does this need to be set? options["extensions"] = Value::Object(serde_json::Map::new()); // TODO make this nicer let mut webauthn_credential_create_options = Value::Object(serde_json::Map::new()); webauthn_credential_create_options["options"] = options; webauthn_credential_create_options["object"] = "webauthnCredentialCreateOptions".into(); // TODO this hopefully shouldn't be needed // webauthn_credential_create_options["token"] = "atoken".into(); Ok(Json(webauthn_credential_create_options)) } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] // TODO remove allow dead_code #[allow(dead_code)] struct WebAuthnLoginCredentialCreateRequest { device_response: RegisterPublicKeyCredentialCopy, name: String, // TODO this is hopefully not needed // token: String, supports_prf: bool, encrypted_user_key: String, encrypted_public_key: String, encrypted_private_key: String, } #[post("/webauthn", data = "")] async fn post_api_webauthn(data: Json, headers: Headers, mut conn: DbConn) -> ApiResult { // this check await ValidateIfUserCanUsePasskeyLogin(user.Id); again let data: WebAuthnLoginCredentialCreateRequest = data.into_inner(); // let data: WebAuthnLoginCredentialCreateRequest = serde_json::from_str(&data)?; let user = headers.user; // TODO Retrieve and delete the saved challenge state here // Verify the credentials with the saved state let (credential, _data) = { let mut states = WEBAUTHN_STATES.get().unwrap().lock().unwrap(); let state = states.remove(&user.uuid).unwrap(); WebauthnConfig::load(true).register_credential(&data.device_response.into(), &state, |_| Ok(false))? }; // TODO add existing keys here when the table exists // let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1; // // TODO: Check for repeated ID's // registrations.push(WebauthnRegistration { // id: data.id.into_i32()?, // name: data.name, // migrated: false, // // credential, // }); // let registrations = Vec::new(); // TODO Save the registration // TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) // .save(&mut conn) // .await?; WebAuthnCredential::new( user.uuid, data.name, serde_json::to_string(&credential)?, data.supports_prf, data.encrypted_user_key, data.encrypted_public_key, data.encrypted_private_key, ).save(&mut conn).await?; Ok(Status::Ok) } #[get("/webauthn")] async fn get_api_webauthn(headers: Headers, mut conn: DbConn) -> Json { let user = headers.user; let data = WebAuthnCredential::find_all_by_user(&user.uuid, &mut conn) .await .into_iter() .map(|wac| { // TODO generate prfStatus from GetPrfStatus() in C# json!({ "id": wac.uuid, "name": wac.name, "prfStatus": 0, "encryptedUserKey": wac.encrypted_user_key, "encryptedPublicKey": wac.encrypted_public_key, "object": "webauthnCredential", }) }).collect::(); Json(json!({ "object": "list", "data": data, "continuationToken": null })) } #[get("/config")] fn config() -> Json { let domain = crate::CONFIG.domain(); // Official available feature flags can be found here: // Server (v2025.5.0): https://github.com/bitwarden/server/blob/4a7db112a0952c6df8bacf36c317e9c4e58c3651/src/Core/Constants.cs#L102 // Client (v2025.5.0): https://github.com/bitwarden/clients/blob/9df8a3cc50ed45f52513e62c23fcc8a4b745f078/libs/common/src/enums/feature-flag.enum.ts#L10 // Android (v2025.4.0): https://github.com/bitwarden/android/blob/bee09de972c3870de0d54a0067996be473ec55c7/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt#L27 // iOS (v2025.4.0): https://github.com/bitwarden/ios/blob/956e05db67344c912e3a1b8cb2609165d67da1c9/BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift#L7 let mut feature_states = parse_experimental_client_feature_flags(&crate::CONFIG.experimental_client_feature_flags()); feature_states.insert("duo-redirect".to_string(), true); feature_states.insert("email-verification".to_string(), true); feature_states.insert("unauth-ui-refresh".to_string(), true); Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version // We should make sure that we keep this updated when we support the new server features // Version history: // - Individual cipher key encryption: 2024.2.0 "version": "2025.4.0", "gitHash": option_env!("GIT_REV"), "server": { "name": "Vaultwarden", "url": "https://github.com/dani-garcia/vaultwarden" }, "settings": { "disableUserRegistration": !crate::CONFIG.signups_allowed() && crate::CONFIG.signups_domains_whitelist().is_empty(), }, "environment": { "vault": domain, "api": format!("{domain}/api"), "identity": format!("{domain}/identity"), "notifications": format!("{domain}/notifications"), "sso": "", "cloudRegion": null, }, // Bitwarden uses this for the self-hosted servers to indicate the default push technology "push": { "pushTechnology": 0, "vapidPublicKey": null }, "featureStates": feature_states, "object": "config", })) } pub fn catchers() -> Vec { catchers![api_not_found] } #[catch(404)] fn api_not_found() -> Json { Json(json!({ "error": { "code": 404, "reason": "Not Found", "description": "The requested resource could not be found." } })) }