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)
///
/// 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

View file

@ -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<i32>,
#[serde(alias = "parallelism")]
pub kdf_parallelism: Option<i32>,
pub kdf_type: i32,
pub iterations: i32,
pub memory: Option<i32>,
pub parallelism: Option<i32>,
}
#[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<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