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:
parent
b519226cd6
commit
a1fd81c1a9
2 changed files with 134 additions and 44 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue