From a1fd81c1a9c4dbd7707a8f79df5cf7fec7b1eee6 Mon Sep 17 00:00:00 2001 From: qaz741wsd856 Date: Thu, 4 Dec 2025 15:32:47 +0000 Subject: [PATCH] feat: implement backward compatibility for KDF settings in accounts handler - Added support for both simple and complex KDF request formats in the post_kdf handler. - Enhanced KDF validation to ensure compatibility with legacy formats. - Updated data structures to reflect changes in KDF parameters and request handling. - Introduced methods to extract KDF parameters and new password hash from either format. --- src/handlers/accounts.rs | 78 +++++++++++++++++------------- src/models/user.rs | 100 ++++++++++++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 44 deletions(-) diff --git a/src/handlers/accounts.rs b/src/handlers/accounts.rs index f3429cc..758542e 100644 --- a/src/handlers/accounts.rs +++ b/src/handlers/accounts.rs @@ -600,6 +600,19 @@ pub async fn post_rotatekey( } /// POST /accounts/kdf - Change KDF settings (PBKDF2 <-> Argon2id) +/// +/// API Format History: +/// - Bitwarden switched to complex format in v2025.10.0 +/// - Vaultwarden followed in PR #6458, WITHOUT backward compatibility +/// - We implement backward compatibility to support both formats +/// +/// Supports two request formats: +/// +/// 1. Simple/Legacy format (Bitwarden < v2025.10.0, e.g. web vault 2025.07): +/// { "kdf": 0, "kdfIterations": 650000, "key": "...", "masterPasswordHash": "...", "newMasterPasswordHash": "..." } +/// +/// 2. Complex format (Bitwarden >= v2025.10.0, e.g. official client 2025.11.x): +/// { "authenticationData": {...}, "unlockData": {...}, "key": "...", "masterPasswordHash": "...", "newMasterPasswordHash": "..." } #[worker::send] pub async fn post_kdf( claims: Claims, @@ -628,62 +641,63 @@ pub async fn post_kdf( return Err(AppError::Unauthorized("Invalid password".to_string())); } - // Validate that KDF settings match between authentication and unlock data - if payload.authentication_data.kdf != payload.unlock_data.kdf { - return Err(AppError::BadRequest( - "KDF settings must be equal for authentication and unlock".to_string(), - )); + // Additional validation for complex format + if let (Some(ref auth_data), Some(ref unlock_data)) = + (&payload.authentication_data, &payload.unlock_data) + { + // KDF settings must match between authentication and unlock + if auth_data.kdf != unlock_data.kdf { + return Err(AppError::BadRequest( + "KDF settings must be equal for authentication and unlock".to_string(), + )); + } + // Salt (email) must match + if user.email != auth_data.salt || user.email != unlock_data.salt { + return Err(AppError::BadRequest( + "Invalid master password salt".to_string(), + )); + } } - // Validate that salt (email) matches - if user.email != payload.authentication_data.salt - || user.email != payload.unlock_data.salt - { - return Err(AppError::BadRequest( - "Invalid master password salt".to_string(), - )); - } + // Extract KDF parameters from either format + let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) = payload + .get_kdf_params() + .ok_or_else(|| AppError::BadRequest("Missing KDF parameters".to_string()))?; // Validate new KDF parameters - let kdf = &payload.unlock_data.kdf; - ensure_supported_kdf( - kdf.kdf, - kdf.kdf_iterations, - kdf.kdf_memory, - kdf.kdf_parallelism, - )?; + ensure_supported_kdf(kdf_type, kdf_iterations, kdf_memory, kdf_parallelism)?; // Generate new salt and hash the new password let new_salt = generate_salt()?; - let new_hashed_password = hash_password_for_storage( - &payload.authentication_data.master_password_authentication_hash, - &new_salt, - ) - .await?; + let new_hashed_password = + hash_password_for_storage(payload.get_new_password_hash(), &new_salt).await?; // Generate new security stamp let new_security_stamp = Uuid::new_v4().to_string(); let now = Utc::now().to_rfc3339(); // Determine kdf_memory and kdf_parallelism based on KDF type - let (kdf_memory, kdf_parallelism) = if kdf.kdf == KDF_TYPE_ARGON2ID { - (kdf.kdf_memory, kdf.kdf_parallelism) + let (final_kdf_memory, final_kdf_parallelism) = if kdf_type == KDF_TYPE_ARGON2ID { + (kdf_memory, kdf_parallelism) } else { // For PBKDF2, clear these fields (None, None) }; + // Get the new encrypted user key + let new_key = payload.get_new_key(); + // Update user record with new KDF settings and password query!( &db, "UPDATE users SET master_password_hash = ?1, password_salt = ?2, key = ?3, kdf_type = ?4, kdf_iterations = ?5, kdf_memory = ?6, kdf_parallelism = ?7, security_stamp = ?8, updated_at = ?9 WHERE id = ?10", new_hashed_password, new_salt, - payload.unlock_data.master_key_wrapped_user_key, - kdf.kdf, - kdf.kdf_iterations, - kdf_memory, - kdf_parallelism, + new_key, + kdf_type, + kdf_iterations, + final_kdf_memory, + final_kdf_parallelism, new_security_stamp, now, user_id diff --git a/src/models/user.rs b/src/models/user.rs index 39c3cbc..e082366 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -154,24 +154,49 @@ pub struct ChangePasswordRequest { } // For POST /accounts/kdf request - Change KDF settings +// +// API Format History: +// - Bitwarden switched to complex format in v2025.10.0 +// - Vaultwarden followed in PR #6458, WITHOUT backward compatibility +// - We implement backward compatibility to support both formats +// +// Supports two formats: +// +// 1. Simple/Legacy format (Bitwarden < v2025.10.0, e.g. web vault 2025.07): +// { +// "kdf": 0, +// "kdfIterations": 650000, +// "kdfMemory": null, +// "kdfParallelism": null, +// "key": "...", +// "masterPasswordHash": "...", +// "newMasterPasswordHash": "..." +// } +// +// 2. Complex format (Bitwarden >= v2025.10.0, e.g. official client 2025.11.x): +// { +// "authenticationData": { "kdf": {...}, "masterPasswordAuthenticationHash": "...", "salt": "..." }, +// "unlockData": { "kdf": {...}, "masterKeyWrappedUserKey": "...", "salt": "..." }, +// "key": "...", +// "masterPasswordHash": "...", +// "newMasterPasswordHash": "..." +// } + #[derive(Debug, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub struct KdfData { +pub struct KdfParams { #[serde(alias = "kdfType")] - pub kdf: i32, - #[serde(alias = "iterations")] - pub kdf_iterations: i32, - #[serde(alias = "memory")] - pub kdf_memory: Option, - #[serde(alias = "parallelism")] - pub kdf_parallelism: Option, + pub kdf_type: i32, + pub iterations: i32, + pub memory: Option, + pub parallelism: Option, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthenticationData { pub salt: String, - pub kdf: KdfData, + pub kdf: KdfParams, pub master_password_authentication_hash: String, } @@ -179,16 +204,67 @@ pub struct AuthenticationData { #[serde(rename_all = "camelCase")] pub struct UnlockData { pub salt: String, - pub kdf: KdfData, + pub kdf: KdfParams, pub master_key_wrapped_user_key: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChangeKdfRequest { + // Common fields (both formats) + pub key: String, pub master_password_hash: String, - pub authentication_data: AuthenticationData, - pub unlock_data: UnlockData, + pub new_master_password_hash: String, + + // Simple format fields (optional) + pub kdf: Option, + pub kdf_iterations: Option, + pub kdf_memory: Option, + pub kdf_parallelism: Option, + + // Complex format fields (optional) + pub authentication_data: Option, + pub unlock_data: Option, +} + +impl ChangeKdfRequest { + /// Extract KDF parameters from either simple or complex format + pub fn get_kdf_params(&self) -> Option<(i32, i32, Option, Option)> { + // Try complex format first (unlock_data takes precedence) + if let Some(ref unlock_data) = self.unlock_data { + return Some(( + unlock_data.kdf.kdf_type, + unlock_data.kdf.iterations, + unlock_data.kdf.memory, + unlock_data.kdf.parallelism, + )); + } + + // Fall back to simple format + if let (Some(kdf), Some(iterations)) = (self.kdf, self.kdf_iterations) { + return Some((kdf, iterations, self.kdf_memory, self.kdf_parallelism)); + } + + None + } + + /// Get the new password hash to store (from authentication_data if available) + pub fn get_new_password_hash(&self) -> &str { + if let Some(ref auth_data) = self.authentication_data { + &auth_data.master_password_authentication_hash + } else { + &self.new_master_password_hash + } + } + + /// Get the new encrypted user key (from unlock_data if available, else from key) + pub fn get_new_key(&self) -> &str { + if let Some(ref unlock_data) = self.unlock_data { + &unlock_data.master_key_wrapped_user_key + } else { + &self.key + } + } } // For POST /accounts/key-management/rotate-user-account-keys request