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.
This commit is contained in:
qaz741wsd856 2025-12-04 15:32:47 +00:00
parent b519226cd6
commit a1fd81c1a9
2 changed files with 134 additions and 44 deletions

View file

@ -600,6 +600,19 @@ pub async fn post_rotatekey(
} }
/// POST /accounts/kdf - Change KDF settings (PBKDF2 <-> Argon2id) /// 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] #[worker::send]
pub async fn post_kdf( pub async fn post_kdf(
claims: Claims, claims: Claims,
@ -628,62 +641,63 @@ pub async fn post_kdf(
return Err(AppError::Unauthorized("Invalid password".to_string())); return Err(AppError::Unauthorized("Invalid password".to_string()));
} }
// Validate that KDF settings match between authentication and unlock data // Additional validation for complex format
if payload.authentication_data.kdf != payload.unlock_data.kdf { if let (Some(ref auth_data), Some(ref unlock_data)) =
return Err(AppError::BadRequest( (&payload.authentication_data, &payload.unlock_data)
"KDF settings must be equal for authentication and unlock".to_string(), {
)); // 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 // Extract KDF parameters from either format
if user.email != payload.authentication_data.salt let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) = payload
|| user.email != payload.unlock_data.salt .get_kdf_params()
{ .ok_or_else(|| AppError::BadRequest("Missing KDF parameters".to_string()))?;
return Err(AppError::BadRequest(
"Invalid master password salt".to_string(),
));
}
// Validate new KDF parameters // Validate new KDF parameters
let kdf = &payload.unlock_data.kdf; ensure_supported_kdf(kdf_type, kdf_iterations, kdf_memory, kdf_parallelism)?;
ensure_supported_kdf(
kdf.kdf,
kdf.kdf_iterations,
kdf.kdf_memory,
kdf.kdf_parallelism,
)?;
// Generate new salt and hash the new password // Generate new salt and hash the new password
let new_salt = generate_salt()?; let new_salt = generate_salt()?;
let new_hashed_password = hash_password_for_storage( let new_hashed_password =
&payload.authentication_data.master_password_authentication_hash, hash_password_for_storage(payload.get_new_password_hash(), &new_salt).await?;
&new_salt,
)
.await?;
// Generate new security stamp // Generate new security stamp
let new_security_stamp = Uuid::new_v4().to_string(); let new_security_stamp = Uuid::new_v4().to_string();
let now = Utc::now().to_rfc3339(); let now = Utc::now().to_rfc3339();
// Determine kdf_memory and kdf_parallelism based on KDF type // Determine kdf_memory and kdf_parallelism based on KDF type
let (kdf_memory, kdf_parallelism) = if kdf.kdf == KDF_TYPE_ARGON2ID { let (final_kdf_memory, final_kdf_parallelism) = if kdf_type == KDF_TYPE_ARGON2ID {
(kdf.kdf_memory, kdf.kdf_parallelism) (kdf_memory, kdf_parallelism)
} else { } else {
// For PBKDF2, clear these fields // For PBKDF2, clear these fields
(None, None) (None, None)
}; };
// Get the new encrypted user key
let new_key = payload.get_new_key();
// Update user record with new KDF settings and password // Update user record with new KDF settings and password
query!( query!(
&db, &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", "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_hashed_password,
new_salt, new_salt,
payload.unlock_data.master_key_wrapped_user_key, new_key,
kdf.kdf, kdf_type,
kdf.kdf_iterations, kdf_iterations,
kdf_memory, final_kdf_memory,
kdf_parallelism, final_kdf_parallelism,
new_security_stamp, new_security_stamp,
now, now,
user_id user_id

View file

@ -154,24 +154,49 @@ pub struct ChangePasswordRequest {
} }
// For POST /accounts/kdf request - Change KDF settings // 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)] #[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct KdfData { pub struct KdfParams {
#[serde(alias = "kdfType")] #[serde(alias = "kdfType")]
pub kdf: i32, pub kdf_type: i32,
#[serde(alias = "iterations")] pub iterations: i32,
pub kdf_iterations: i32, pub memory: Option<i32>,
#[serde(alias = "memory")] pub parallelism: Option<i32>,
pub kdf_memory: Option<i32>,
#[serde(alias = "parallelism")]
pub kdf_parallelism: Option<i32>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AuthenticationData { pub struct AuthenticationData {
pub salt: String, pub salt: String,
pub kdf: KdfData, pub kdf: KdfParams,
pub master_password_authentication_hash: String, pub master_password_authentication_hash: String,
} }
@ -179,16 +204,67 @@ pub struct AuthenticationData {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UnlockData { pub struct UnlockData {
pub salt: String, pub salt: String,
pub kdf: KdfData, pub kdf: KdfParams,
pub master_key_wrapped_user_key: String, pub master_key_wrapped_user_key: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ChangeKdfRequest { pub struct ChangeKdfRequest {
// Common fields (both formats)
pub key: String,
pub master_password_hash: String, pub master_password_hash: String,
pub authentication_data: AuthenticationData, pub new_master_password_hash: String,
pub unlock_data: UnlockData,
// Simple format fields (optional)
pub kdf: Option<i32>,
pub kdf_iterations: Option<i32>,
pub kdf_memory: Option<i32>,
pub kdf_parallelism: Option<i32>,
// Complex format fields (optional)
pub authentication_data: Option<AuthenticationData>,
pub unlock_data: Option<UnlockData>,
}
impl ChangeKdfRequest {
/// Extract KDF parameters from either simple or complex format
pub fn get_kdf_params(&self) -> Option<(i32, i32, Option<i32>, Option<i32>)> {
// 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 // For POST /accounts/key-management/rotate-user-account-keys request