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)
|
/// 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue