From b8089e31c2f35958021a32414411aecaddf3b24b Mon Sep 17 00:00:00 2001 From: Tom <83423411+tom27052006@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:45:12 +0200 Subject: [PATCH] [Web 2026.9.0] Support new account recovery password payload (#7747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support new account recovery password payload * Address account recovery review feedback * Check the account recovery password before sending the recovery email --------- Co-authored-by: Daniel GarcĂ­a --- src/api/core/organizations.rs | 41 ++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6464f774..e0124ef1 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -26,6 +26,8 @@ use crate::{ util::{NumberOrString, convert_json_key_lcase_first}, }; +use super::accounts::{AuthenticationData, UnlockData}; + pub fn routes() -> Vec { routes![ get_organization, @@ -2751,9 +2753,14 @@ struct OrganizationUserResetPasswordEnrollmentRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct OrganizationUserRecoverAccountRequest { + // Legacy payload new_master_password_hash: Option, key: Option, + // Current payload + authentication_data: Option, + unlock_data: Option, + #[serde(default)] reset_master_password: bool, #[serde(default)] @@ -2838,6 +2845,29 @@ async fn recover_account( false }; + // Check the new password before the email below, so that a rejected request doesn't tell the user + // their password was reset + let new_password = if req.reset_master_password { + let (new_master_password_hash, new_key) = if let (Some(authentication_data), Some(unlock_data)) = + (req.authentication_data, req.unlock_data) + { + authentication_data.check(&user, &unlock_data)?; + + if !authentication_data.kdf.matches_user(&user) { + err!("KDF settings do not match the user account") + } + + (authentication_data.master_password_authentication_hash, unlock_data.master_key_wrapped_user_key) + } else if let (Some(new_master_password_hash), Some(new_key)) = (req.new_master_password_hash, req.key) { + (new_master_password_hash, new_key) + } else { + err_code!("Unprocessable request", "Missing fields to reset password", Status::UnprocessableEntity.code); + }; + Some((new_master_password_hash, new_key)) + } else { + None + }; + // Sending email first ensure working email configuration and the resulting user notification. // Also this might add some protection against security flaws and misuse if let Err(e) = mail::send_admin_account_recovery( @@ -2853,14 +2883,8 @@ async fn recover_account( err!(format!("Error sending user reset password email: {e:#?}")); } - if req.reset_master_password { - if let Some(key) = req.key - && let Some(hash) = req.new_master_password_hash - { - user.set_password(hash.as_str(), Some(key), true, None, &conn).await?; - } else { - err_code!("Unprocessable request", "Missing fields to reset password", Status::UnprocessableEntity.code); - } + if let Some((new_master_password_hash, new_key)) = new_password { + user.set_password(&new_master_password_hash, Some(new_key), true, None, &conn).await?; } if req.reset_two_factor { @@ -2919,6 +2943,7 @@ async fn get_reset_password_details( "kdfIterations": user.client_kdf_iter, "kdfMemory": user.client_kdf_memory, "kdfParallelism": user.client_kdf_parallelism, + "masterPasswordSalt": user.master_password_salt(), "resetPasswordKey": member.reset_password_key, "encryptedPrivateKey": org.private_key, })))