use chrono::{NaiveDateTime, TimeDelta, Utc}; use derive_more::{AsRef, Deref, Display, From}; use diesel::prelude::*; use serde_json::Value; use crate::{ CONFIG, api::EmptyResult, auth::DEFAULT_ACCESS_VALIDITY, crypto, db::{ DbConn, models::DeviceId, schema::{invitations, sso_users, twofactor_incomplete, users}, }, error::MapResult, sso::OIDCIdentifier, util::{format_date, get_uuid, retry}, }; use macros::UuidFromParam; use super::{ Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, UserSignatureKeyPair, }; #[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)] #[diesel(table_name = users)] #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct User { pub uuid: UserId, pub enabled: bool, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub verified_at: Option, pub last_verifying_at: Option, pub login_verify_count: i32, pub email: String, pub email_new: Option, pub email_new_token: Option, pub name: String, pub password_hash: Vec, pub salt: Vec, pub password_iterations: i32, pub password_hint: Option, pub akey: String, pub private_key: Option, pub public_key: Option, #[diesel(column_name = "totp_secret")] // Note, this is only added to the UserDb structs, not to User _totp_secret: Option, pub totp_recover: Option, pub security_stamp: String, pub stamp_exception: Option, pub equivalent_domains: String, pub excluded_globals: String, pub client_kdf_type: i32, pub client_kdf_iter: i32, pub client_kdf_memory: Option, pub client_kdf_parallelism: Option, pub api_key: Option, pub avatar_color: Option, pub external_id: Option, // Todo: Needs to be removed in the future, this is not used anymore. pub key_id: Option, // The v2 state: all set, with a `user_signature_key_pairs` row, or none, see `User::is_v2` pub signed_public_key: Option, pub security_state: Option, pub security_version: Option, /// JSON `{"wrappedUserKey1", "wrappedUserKey2"}` for sessions that still hold the v1 user key. Opaque to us. pub v2_upgrade_token: Option, } #[derive(Identifiable, Queryable, Insertable)] #[diesel(table_name = invitations)] #[diesel(primary_key(email))] pub struct Invitation { pub email: String, } #[derive(Identifiable, Queryable, Insertable, Selectable)] #[diesel(table_name = sso_users)] #[diesel(primary_key(user_uuid))] pub struct SsoUser { pub user_uuid: UserId, pub identifier: OIDCIdentifier, } pub enum UserKdfType { Pbkdf2 = 0, Argon2id = 1, } enum UserStatus { Enabled = 0, Invited = 1, _Disabled = 2, } /// A previous security stamp that is still accepted, until `expire`. #[derive(Serialize, Deserialize)] pub struct UserStampException { /// The routes the stamp is still accepted on, or `None` for any route. pub routes: Option>, pub security_stamp: String, pub expire: i64, } impl UserStampException { pub fn is_expired(&self) -> bool { Utc::now().timestamp() > self.expire } /// Whether this exception lets a token carrying `security_stamp` through on `route`. pub fn allows(&self, security_stamp: &str, route: &str) -> bool { !self.is_expired() && self.security_stamp == security_stamp && self.routes.as_ref().is_none_or(|routes| routes.iter().any(|r| r == route)) } } /// Local methods impl User { pub const CLIENT_KDF_TYPE_DEFAULT: i32 = UserKdfType::Pbkdf2 as i32; pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000; pub fn new(email: &str, name: Option) -> Self { let now = Utc::now().naive_utc(); let email = email.to_lowercase(); Self { uuid: UserId(get_uuid()), enabled: true, created_at: now, updated_at: now, verified_at: None, last_verifying_at: None, login_verify_count: 0, name: name.unwrap_or(email.clone()), email, akey: String::new(), email_new: None, email_new_token: None, password_hash: Vec::new(), salt: crypto::get_random_bytes::<64>().to_vec(), password_iterations: CONFIG.password_iterations(), security_stamp: get_uuid(), stamp_exception: None, password_hint: None, private_key: None, public_key: None, _totp_secret: None, totp_recover: None, equivalent_domains: "[]".to_owned(), excluded_globals: "[]".to_owned(), client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT, client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT, client_kdf_memory: None, client_kdf_parallelism: None, api_key: None, avatar_color: None, external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. key_id: None, signed_public_key: None, security_state: None, security_version: None, v2_upgrade_token: None, } } pub fn is_v2(&self) -> bool { self.signed_public_key.is_some() && self.security_state.is_some() && self.security_version.is_some() } pub fn check_valid_password(&self, password: &str) -> bool { crypto::verify_password_hash( password.as_bytes(), &self.salt, &self.password_hash, self.password_iterations.cast_unsigned(), ) } /// Upstream's `GetMasterPasswordSalt`: there is no separate salt, so the normalized email pub fn master_password_salt(&self) -> String { self.email.trim().to_lowercase() } pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool { if let Some(ref totp_recover) = self.totp_recover { crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) } else { false } } pub fn check_valid_api_key(&self, key: &str) -> bool { matches!(self.api_key, Some(ref api_key) if crypto::ct_eq(api_key, key)) } /// Set the password hash generated /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different. /// /// # Arguments /// /// * `password` - A str which contains a hashed version of the users master password. /// * `new_key` - A String which contains the new aKey value of the users master password. /// * `allow_next_route` - A Option> with the function names of the next allowed (rocket) routes. /// These routes are able to use the previous stamp id for the next 2 minutes. /// After these 2 minutes this stamp will expire. /// pub async fn set_password( &mut self, password: &str, new_key: Option, reset_security_stamp: bool, allow_next_route: Option>, conn: &DbConn, ) -> EmptyResult { self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations.cast_unsigned()); if let Some(route) = allow_next_route { self.set_stamp_exception(route); } if let Some(new_key) = new_key { self.akey = new_key; } if reset_security_stamp { self.reset_security_stamp(conn).await?; } Ok(()) } pub async fn reset_security_stamp(&mut self, conn: &DbConn) -> EmptyResult { self.security_stamp = get_uuid(); // Keeps the unexpired route exceptions, set for the stamp being replaced, and drops a rotation's grace let mut exceptions = self.stamp_exceptions(); exceptions.retain(|e| e.routes.is_some() && !e.is_expired()); self.set_stamp_exceptions(&exceptions); Device::rotate_refresh_tokens_by_user(&self.uuid, conn).await?; Ok(()) } /// Resets the stamp but lets issued access tokens run out, as upstream only checks it on refresh. pub async fn reset_security_stamp_after_key_rotation(&mut self, conn: &DbConn) -> EmptyResult { let previous_stamp = self.security_stamp.clone(); let mut grace: Vec = self.stamp_exceptions().into_iter().filter(|e| e.routes.is_none() && !e.is_expired()).collect(); self.reset_security_stamp(conn).await?; grace.push(UserStampException { routes: None, security_stamp: previous_stamp, expire: (Utc::now() + *DEFAULT_ACCESS_VALIDITY).timestamp(), }); let mut exceptions = self.stamp_exceptions(); exceptions.extend(grace); self.set_stamp_exceptions(&exceptions); Ok(()) } /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. /// /// # Arguments /// * `route_exception` - A Vec with the function names of the next allowed (rocket) routes. /// These routes are able to use the previous stamp id for the next 2 minutes. /// After these 2 minutes this stamp will expire. /// pub fn set_stamp_exception(&mut self, route_exception: Vec) { self.set_stamp_exceptions(&[UserStampException { routes: Some(route_exception), security_stamp: self.security_stamp.clone(), expire: (Utc::now() + TimeDelta::try_minutes(2).unwrap()).timestamp(), }]); } /// The previous security stamps that are still accepted, expired ones included. pub fn stamp_exceptions(&self) -> Vec { let Some(stored) = self.stamp_exception.as_deref() else { return Vec::new(); }; // Before key rotations kept sessions alive, only a single route exception was stored. serde_json::from_str::>(stored) .or_else(|_| serde_json::from_str::(stored).map(|e| vec![e])) .unwrap_or_default() } fn set_stamp_exceptions(&mut self, exceptions: &[UserStampException]) { self.stamp_exception = if exceptions.is_empty() { None } else { Some(serde_json::to_string(&exceptions).unwrap_or_default()) }; } pub fn display_name(&self) -> &str { // default to email if name is empty if self.name.is_empty() { &self.email } else { &self.name } } } /// Database methods impl User { pub fn verified(&self) -> bool { !CONFIG.mail_enabled() || self.verified_at.is_some() } async fn v2_signature_key_pair(&self, conn: &DbConn) -> Option { if !self.is_v2() { return None; } UserSignatureKeyPair::find_by_user(&self.uuid, conn).await } /// The `accountKeys` object (upstream's `PrivateKeysResponseModel`), null without a key pair pub async fn account_keys_json(&self, conn: &DbConn) -> Value { if self.private_key.is_some() { let (signed_public_key, signature_key_pair, security_state) = match self.v2_signature_key_pair(conn).await { Some(key_pair) => ( json!(self.signed_public_key), key_pair.to_json(), json!({ "securityState": self.security_state, "securityVersion": self.security_version, }), ), None => (Value::Null, Value::Null, Value::Null), }; json!({ "publicKeyEncryptionKeyPair": { "wrappedPrivateKey": self.private_key, "publicKey": self.public_key, "signedPublicKey": signed_public_key, "object": "publicKeyEncryptionKeyPair", }, "securityState": security_state, "signatureKeyPair": signature_key_pair, "object": "privateKeys" }) } else { Value::Null } } pub async fn public_keys_json(&self, conn: &DbConn) -> Value { let (signed_public_key, verifying_key) = match self.v2_signature_key_pair(conn).await { Some(key_pair) => (json!(self.signed_public_key), json!(key_pair.verifying_key)), None => (Value::Null, Value::Null), }; json!({ "publicKey": self.public_key, "signedPublicKey": signed_public_key, "verifyingKey": verifying_key, "object": "publicKeys" }) } pub fn v2_upgrade_token_json(&self) -> Option { self.v2_upgrade_token.as_ref().and_then(|token| serde_json::from_str(token).ok()) } pub async fn to_json(&self, conn: &DbConn) -> Value { let mut orgs_json = Vec::new(); for c in Membership::find_confirmed_by_user(&self.uuid, conn).await { orgs_json.push(c.to_json(conn).await); } let mut orgs_new_json = Vec::new(); for c in Membership::find_accepted_and_confirmed_by_user(&self.uuid, conn).await { orgs_new_json.push(c.to_json(conn).await); } let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).await.is_empty(); // TODO: Might want to save the status field in the DB let status = if self.password_hash.is_empty() { UserStatus::Invited } else { UserStatus::Enabled }; let account_keys = self.account_keys_json(conn).await; json!({ "_status": status as i32, "accountKeys": account_keys, "id": self.uuid, "name": self.name, "email": self.email, "emailVerified": self.verified(), "premium": true, "premiumFromOrganization": false, "culture": "en-US", "twoFactorEnabled": twofactor_enabled, "key": self.akey, "privateKey": self.private_key, "securityStamp": self.security_stamp, "organizations": orgs_json, "organizationsNew": orgs_new_json, "providers": [], "providerOrganizations": [], "forcePasswordReset": false, "avatarColor": self.avatar_color, "usesKeyConnector": false, "creationDate": format_date(&self.created_at), "object": "profile", }) } pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { if !crate::util::is_valid_email(&self.email) { err!(format!("User email {} is not a valid email address", self.email.escape_debug())) } self.updated_at = Utc::now().naive_utc(); db_run! { conn: mysql { diesel::insert_into(users::table) .values(&*self) .on_conflict(diesel::dsl::DuplicatedKeys) .do_update() .set(&*self) .execute(conn) .map_res("Error saving user") } postgresql, sqlite { diesel::insert_into(users::table) // Insert or update .values(&*self) .on_conflict(users::uuid) .do_update() .set(&*self) .execute(conn) .map_res("Error saving user") } } } pub async fn delete(self, conn: &DbConn) -> EmptyResult { for member in Membership::find_confirmed_by_user(&self.uuid, conn).await { if member.atype == MembershipType::Owner && Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1 { err!("Can't delete last owner") } } super::Send::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?; EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?; Membership::delete_all_by_user(&self.uuid, conn).await?; Cipher::delete_all_by_user(&self.uuid, conn).await?; Favorite::delete_all_by_user(&self.uuid, conn).await?; Folder::delete_all_by_user(&self.uuid, conn).await?; Device::delete_all_by_user(&self.uuid, conn).await?; TwoFactor::delete_all_by_user(&self.uuid, conn).await?; TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?; UserSignatureKeyPair::delete_all_by_user(&self.uuid, conn).await?; Invitation::take(&self.email, conn).await; // Delete invitation if any conn.run(move |conn| { diesel::delete(users::table.filter(users::uuid.eq(self.uuid))).execute(conn).map_res("Error deleting user") }) .await } pub async fn update_uuid_revision(uuid: &UserId, conn: &DbConn) { if let Err(e) = Self::update_revision_impl(uuid, &Utc::now().naive_utc(), conn).await { warn!("Failed to update revision for {uuid}: {e:#?}"); } } pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult { let updated_at = Utc::now().naive_utc(); conn.run(move |conn| { retry(|| diesel::update(users::table).set(users::updated_at.eq(updated_at)).execute(conn), 10) .map_res("Error updating revision date for all users") }) .await } pub async fn update_revision(&mut self, conn: &DbConn) -> EmptyResult { self.updated_at = Utc::now().naive_utc(); Self::update_revision_impl(&self.uuid, &self.updated_at, conn).await } async fn update_revision_impl(uuid: &UserId, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { conn.run(move |conn| { retry( || { diesel::update(users::table.filter(users::uuid.eq(uuid))) .set(users::updated_at.eq(date)) .execute(conn) }, 10, ) .map_res("Error updating user revision") }) .await } pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option { let lower_mail = mail.to_lowercase(); conn.run(move |conn| users::table.filter(users::email.eq(lower_mail)).first::(conn).ok()).await } pub async fn find_by_uuid(uuid: &UserId, conn: &DbConn) -> Option { conn.run(move |conn| users::table.filter(users::uuid.eq(uuid)).first::(conn).ok()).await } pub async fn find_by_device_for_email2fa(device_uuid: &DeviceId, conn: &DbConn) -> Option { if let Some(user_uuid) = conn .run(move |conn| { twofactor_incomplete::table .filter(twofactor_incomplete::device_uuid.eq(device_uuid)) .order_by(twofactor_incomplete::login_time.desc()) .select(twofactor_incomplete::user_uuid) .first::(conn) .ok() }) .await { return Self::find_by_uuid(&user_uuid, conn).await; } None } pub async fn get_all(conn: &DbConn) -> Vec<(Self, Option)> { conn.run(move |conn| { users::table .left_join(sso_users::table) .select(<(Self, Option)>::as_select()) .load(conn) .expect("Error loading groups for user") .into_iter() .collect() }) .await } pub async fn last_active(&self, conn: &DbConn) -> Option { match Device::find_latest_active_by_user(&self.uuid, conn).await { Some(device) => Some(device.updated_at), None => None, } } } impl Invitation { pub fn new(email: &str) -> Self { let email = email.to_lowercase(); Self { email, } } pub async fn save(&self, conn: &DbConn) -> EmptyResult { if !crate::util::is_valid_email(&self.email) { err!(format!("Invitation email {} is not a valid email address", self.email.escape_debug())) } db_run! { conn: // Not checking for ForeignKey Constraints here // Table invitations does not have any ForeignKey Constraints. mysql { diesel::insert_into(invitations::table) .values(self) .on_conflict(diesel::dsl::DuplicatedKeys) .do_nothing() .execute(conn) .map_res("Error saving invitation") } postgresql, sqlite { diesel::insert_into(invitations::table) .values(self) .on_conflict(invitations::email) .do_nothing() .execute(conn) .map_res("Error saving invitation") } } } pub async fn delete(self, conn: &DbConn) -> EmptyResult { conn.run(move |conn| { diesel::delete(invitations::table.filter(invitations::email.eq(self.email))) .execute(conn) .map_res("Error deleting invitation") }) .await } pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option { let lower_mail = mail.to_lowercase(); conn.run(move |conn| invitations::table.filter(invitations::email.eq(lower_mail)).first::(conn).ok()) .await } pub async fn take(mail: &str, conn: &DbConn) -> bool { match Self::find_by_mail(mail, conn).await { Some(invitation) => invitation.delete(conn).await.is_ok(), None => false, } } } #[derive( Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize, AsRef, Deref, Display, From, UuidFromParam, )] #[deref(forward)] #[from(forward)] pub struct UserId(String); #[derive( Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize, AsRef, Deref, Display, From, UuidFromParam, )] #[deref(forward)] #[from(forward)] pub struct KeyId(String); impl SsoUser { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: mysql { diesel::insert_into(sso_users::table) .values(self) .execute(conn) .map_res("Error saving SSO user") } postgresql, sqlite { diesel::insert_into(sso_users::table) .values(self) .execute(conn) .map_res("Error saving SSO user") } } } pub async fn find_by_identifier(identifier: &str, conn: &DbConn) -> Option<(User, Self)> { conn.run(move |conn| { users::table .inner_join(sso_users::table) .select(<(User, Self)>::as_select()) .filter(sso_users::identifier.eq(identifier)) .first::<(User, Self)>(conn) .ok() }) .await } pub async fn find_by_mail(mail: &str, conn: &DbConn) -> Option<(User, Option)> { let lower_mail = mail.to_lowercase(); conn.run(move |conn| { users::table .left_join(sso_users::table) .select(<(User, Option)>::as_select()) .filter(users::email.eq(lower_mail)) .first::<(User, Option)>(conn) .ok() }) .await } pub async fn delete(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { conn.run(move |conn| { diesel::delete(sso_users::table.filter(sso_users::user_uuid.eq(user_uuid))) .execute(conn) .map_res("Error deleting sso user") }) .await } }