mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-11-13 00:08:36 +02:00
* Implement V2 key rotation support * Clear user key ids left stale by key rotations * Refuse key ids that aren't 32 lowercase hex characters, like upstream * Save the signature key pair before the v2 account state * Gate v2 logins on upstream's v2 encryption check * Document the v2 encryption feature flags * Remove the v2 upgrade feature flags With force-upgrade-v2-encryption on, the clients loop on any v1 account that stays v1 on a vault route: the migration check runs fullSync(false), which writes lastSync, which triggers the scheduler again. No server response stops it, so don't serve the flag until the clients fix it. pm-30144-sdk-key-rotation and pm-31050-no-logout-key-upgrade-rotation only serve that upgrade, so they go too. * Move the v2 upgrade token migration to its own version Diesel identifies a migration by the date in its folder name alone, and silently drops one of two folders that share it. The Custom Roles migration uses the same date. * Refuse rotations from a replaced user key, like upstream Both rotation endpoints accept the optional oldUserKeyId. When it and the stored key id are both set and differ, the rotation is refused with a 409 asking the client to sync, before the account keys are checked or anything is written. Also correct the comment on saving the signature key pair first: by then the vault data is already re-encrypted, so a failure there doesn't keep the account usable. * Update the v2 client minimum in .env.template and the key pair comments V2 accounts need 2026.10.1, or 2026.10.2 for the desktop app, so the per-flag versions are only enough to create one. The comments on saving the signature key pair now cover the rotation of an account that is already v2.
145 lines
5.1 KiB
Rust
145 lines
5.1 KiB
Rust
mod admin;
|
|
pub mod core;
|
|
mod icons;
|
|
mod identity;
|
|
mod notifications;
|
|
mod push;
|
|
mod web;
|
|
|
|
use rocket::serde::json::Json;
|
|
use serde_json::Value;
|
|
|
|
pub use crate::api::{
|
|
admin::catchers as admin_catchers,
|
|
admin::routes as admin_routes,
|
|
core::catchers as core_catchers,
|
|
core::purge_auth_requests,
|
|
core::purge_sends,
|
|
core::purge_trashed_ciphers,
|
|
core::routes as core_routes,
|
|
core::two_factor::send_incomplete_2fa_notifications,
|
|
core::{emergency_notification_reminder_job, emergency_request_timeout_job},
|
|
core::{event_cleanup_job, events_routes as core_events_routes},
|
|
icons::routes as icons_routes,
|
|
identity::routes as identity_routes,
|
|
notifications::routes as notifications_routes,
|
|
notifications::{AnonymousNotify, LogOutReason, Notify, UpdateType, WS_ANONYMOUS_SUBSCRIPTIONS, WS_USERS},
|
|
push::{
|
|
push_cipher_update, push_folder_update, push_logout, push_logout_to_relay, push_send_update, push_user_update,
|
|
register_push_device, unregister_push_device,
|
|
},
|
|
web::catchers as web_catchers,
|
|
web::routes as web_routes,
|
|
web::{STATIC_FILES, invalidate_css_cache},
|
|
};
|
|
use crate::{
|
|
CONFIG,
|
|
db::{
|
|
DbConn,
|
|
models::{OrgPolicy, OrgPolicyType, User},
|
|
},
|
|
};
|
|
|
|
// Type aliases for API methods results
|
|
pub type ApiResult<T> = Result<T, crate::error::Error>;
|
|
pub type JsonResult = ApiResult<Json<Value>>;
|
|
pub type EmptyResult = ApiResult<()>;
|
|
|
|
// Common structs representing JSON data received
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct PasswordOrOtpData {
|
|
#[serde(alias = "MasterPasswordHash")]
|
|
master_password_hash: Option<String>,
|
|
otp: Option<String>,
|
|
}
|
|
|
|
impl PasswordOrOtpData {
|
|
/// Tokens used via this struct can be used multiple times during the process
|
|
/// First for the validation to continue, after that to enable or validate the following actions
|
|
/// This is different per caller, so it can be adjusted to delete the token or not
|
|
pub async fn validate(&self, user: &User, delete_if_valid: bool, conn: &DbConn) -> EmptyResult {
|
|
use crate::api::core::two_factor::protected_actions::validate_protected_action_otp;
|
|
|
|
match (self.master_password_hash.as_deref(), self.otp.as_deref()) {
|
|
(Some(pw_hash), None) => {
|
|
if !user.check_valid_password(pw_hash) {
|
|
err!("Invalid password");
|
|
}
|
|
}
|
|
(None, Some(otp)) => {
|
|
validate_protected_action_otp(otp, &user.uuid, delete_if_valid, conn).await?;
|
|
}
|
|
_ => err!("No validation provided"),
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[expect(clippy::struct_excessive_bools, reason = "Bitwarden clients expect the data in this specific format")]
|
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct MasterPasswordPolicy {
|
|
min_complexity: Option<u8>,
|
|
min_length: Option<u32>,
|
|
require_lower: bool,
|
|
require_upper: bool,
|
|
require_numbers: bool,
|
|
require_special: bool,
|
|
enforce_on_login: bool,
|
|
}
|
|
|
|
// Fetch all valid Master Password Policies and merge them into one with all trues and largest numbers as one policy
|
|
async fn master_password_policy(user: &User, conn: &DbConn) -> Value {
|
|
let master_password_policies: Vec<MasterPasswordPolicy> =
|
|
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(
|
|
&user.uuid,
|
|
OrgPolicyType::MasterPassword,
|
|
conn,
|
|
)
|
|
.await
|
|
.into_iter()
|
|
.filter_map(|p| serde_json::from_str(&p.data).ok())
|
|
.collect();
|
|
|
|
let mut mpp_json = if !master_password_policies.is_empty() {
|
|
json!(master_password_policies.into_iter().reduce(|acc, policy| {
|
|
MasterPasswordPolicy {
|
|
min_complexity: acc.min_complexity.max(policy.min_complexity),
|
|
min_length: acc.min_length.max(policy.min_length),
|
|
require_lower: acc.require_lower || policy.require_lower,
|
|
require_upper: acc.require_upper || policy.require_upper,
|
|
require_numbers: acc.require_numbers || policy.require_numbers,
|
|
require_special: acc.require_special || policy.require_special,
|
|
enforce_on_login: acc.enforce_on_login || policy.enforce_on_login,
|
|
}
|
|
}))
|
|
} else if CONFIG.sso_enabled() {
|
|
CONFIG.sso_master_password_policy_value().unwrap_or(json!({}))
|
|
} else {
|
|
json!({})
|
|
};
|
|
|
|
// NOTE: Upstream still uses PascalCase here for `Object`!
|
|
mpp_json["Object"] = json!("masterPasswordPolicy");
|
|
mpp_json
|
|
}
|
|
|
|
// Identity responses use PascalCase keys like upstream, some clients (Android) only read those
|
|
async fn identity_master_password_policy(user: &User, conn: &DbConn) -> Value {
|
|
match master_password_policy(user, conn).await {
|
|
Value::Object(policy) => Value::Object(
|
|
policy
|
|
.into_iter()
|
|
.map(|(mut key, value)| {
|
|
if let Some(first) = key.get_mut(..1) {
|
|
first.make_ascii_uppercase();
|
|
}
|
|
(key, value)
|
|
})
|
|
.collect(),
|
|
),
|
|
policy => policy,
|
|
}
|
|
}
|