From 00cc9f79b1a341da187cca2f820cacaa46e59195 Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 3 Jun 2026 15:12:21 +0200 Subject: [PATCH] Registration request update --- src/api/core/accounts.rs | 113 ++++++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 954b35bd..bd3d8820 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,17 +116,70 @@ 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() + } + + fn unprocessable(&self) -> bool { + let mut unprocessable = false; + *self.compat.fold( + |_| &false, + |rdcu| { + unprocessable = rdcu.master_password_authentication.kdf != rdcu.master_password_unlock.kdf + || rdcu.master_password_authentication.salt != self.email + || rdcu.master_password_unlock.salt != self.email; + &unprocessable + }, + ) + } +} + #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SetPasswordData { +struct RegisterDataOld { #[serde(flatten)] kdf: KDFData, + #[serde(alias = "userSymmetricKey")] key: String, - keys: Option, + + #[serde(alias = "masterPasswordHash")] master_password_hash: String, - master_password_hint: Option, - org_identifier: Option, +} + +#[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)] @@ -139,6 +189,39 @@ struct KeysData { 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 { + #[serde(flatten)] + kdf: KDFData, + + key: String, + keys: Option, + master_password_hash: String, + master_password_hint: Option, + org_identifier: Option, +} + /// 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 +260,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 +344,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 +383,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