From a058a35ccddf48e77665bcf9b3f5fc2711f63f87 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 7 Jul 2026 15:59:17 +0200 Subject: [PATCH] [v2026.5.0] Registration request update (#7295) * Registration request update * Review fix --------- Co-authored-by: Timshel --- src/api/core/accounts.rs | 120 ++++++++++++++++++++++++++++++++++----- 1 file changed, 105 insertions(+), 15 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 954b35bd..623edf24 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -97,14 +97,11 @@ pub struct RegisterData { email: String, #[serde(flatten)] - kdf: KDFData, + compat: RegisterDataCompat, - #[serde(alias = "userSymmetricKey")] - key: String, #[serde(alias = "userAsymmetricKeys")] keys: Option, - master_password_hash: String, master_password_hint: Option, name: Option, @@ -119,6 +116,102 @@ pub struct RegisterData { org_invite_token: Option, } +impl RegisterData { + fn hash(&self) -> String { + self.compat.fold(|rdc| &rdc.master_password_hash, |rdcu| &rdcu.master_password_authentication.hash).to_owned() + } + + fn kdf(&self) -> &KDFData { + self.compat.fold(|rdc| &rdc.kdf, |rdcu| &rdcu.master_password_authentication.kdf) + } + + fn key(&self) -> String { + self.compat.fold(|rdc| &rdc.key, |rdcu| &rdcu.master_password_unlock.key).to_owned() + } + + // When comparing with salt, email need to be normalized: + // - https://github.com/bitwarden/clients/blob/web-v2026.5.0/libs/common/src/key-management/master-password/services/master-password.service.ts#L171 + fn unprocessable(&self) -> bool { + let mut unprocessable = false; + *self.compat.fold( + |_| &false, + |rdcu| { + let email = self.email.trim().to_lowercase(); + unprocessable = rdcu.master_password_authentication.kdf != rdcu.master_password_unlock.kdf + || rdcu.master_password_authentication.salt != email + || rdcu.master_password_unlock.salt != email; + &unprocessable + }, + ) + } +} + +#[derive(Debug, Deserialize)] +struct RegisterDataOld { + #[serde(flatten)] + kdf: KDFData, + + #[serde(alias = "userSymmetricKey")] + key: String, + + #[serde(alias = "masterPasswordHash")] + master_password_hash: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RegisterDataCur { + master_password_authentication: MasterPasswordAuthentication, + master_password_unlock: MasterPasswordUnlock, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum RegisterDataCompat { + RegisterDataOld(RegisterDataOld), + RegisterDataCur(RegisterDataCur), +} + +impl RegisterDataCompat { + fn fold<'a, T>( + &'a self, + fct: impl FnOnce(&'a RegisterDataOld) -> &'a T, + fcu: impl FnOnce(&'a RegisterDataCur) -> &'a T, + ) -> &'a T { + match self { + RegisterDataCompat::RegisterDataOld(rdc) => fct(rdc), + RegisterDataCompat::RegisterDataCur(rdcu) => fcu(rdcu), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct KeysData { + encrypted_private_key: String, + public_key: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MasterPasswordAuthentication { + kdf: KDFData, + salt: String, + + #[serde(alias = "masterPasswordAuthenticationHash")] + hash: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MasterPasswordUnlock { + kdf: KDFData, + salt: String, + + #[serde(alias = "masterKeyWrappedUserKey")] + key: String, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SetPasswordData { @@ -132,13 +225,6 @@ pub struct SetPasswordData { org_identifier: Option, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KeysData { - encrypted_private_key: String, - public_key: String, -} - /// Trims whitespace from password hints, and converts blank password hints to `None`. fn clean_password_hint(password_hint: Option<&String>) -> Option { match password_hint { @@ -177,6 +263,10 @@ pub async fn register(data: Json, email_verification: bool, conn: let mut pending_emergency_access = None; + if data.unprocessable() { + err_code!("Unexpected RegisterData format", Status::UnprocessableEntity.code); + } + // First, validate the provided verification tokens if email_verification { match ( @@ -257,8 +347,8 @@ pub async fn register(data: Json, email_verification: bool, conn: err!("Registration not allowed or user already exists") } - if let Some(token) = data.org_invite_token { - let claims = decode_invite(&token)?; + if let Some(token) = data.org_invite_token.as_ref() { + let claims = decode_invite(token)?; if claims.email == email { // Verify the email address when signing up via a valid invite token email_verified = true; @@ -296,9 +386,9 @@ pub async fn register(data: Json, email_verification: bool, conn: // Make sure we don't leave a lingering invitation. Invitation::take(&email, &conn).await; - set_kdf_data(&mut user, &data.kdf)?; + set_kdf_data(&mut user, data.kdf())?; - user.set_password(&data.master_password_hash, Some(data.key), true, None, &conn).await?; + user.set_password(&data.hash(), Some(data.key()), true, None, &conn).await?; user.password_hint = password_hint; // Add extra fields if present