diff --git a/src/api/client/session/sso.rs b/src/api/client/session/sso.rs index b615aa7f..ab45f3d9 100644 --- a/src/api/client/session/sso.rs +++ b/src/api/client/session/sso.rs @@ -34,7 +34,7 @@ use tuwunel_service::{ oauth::{ CODE_VERIFIER_LENGTH, Provider, SESSION_ID_LENGTH, Session, UserInfo, unique_id_sub, }, - users::Register, + users::{PASSWORD_SENTINEL, Register}, }; use url::Url; @@ -480,7 +480,7 @@ async fn register_user( .users .full_register(Register { user_id: Some(user_id), - password: Some("*"), + password: Some(PASSWORD_SENTINEL), origin: Some("sso"), displayname: userinfo.name.as_deref(), grant_first_user_admin: true, diff --git a/src/service/migrations.rs b/src/service/migrations.rs index fd5c3e6a..6a7b712f 100644 --- a/src/service/migrations.rs +++ b/src/service/migrations.rs @@ -9,10 +9,11 @@ use ruma::{ push::Ruleset, }; use tuwunel_core::{ - Err, Result, debug, debug_info, debug_warn, error, info, + Err, Result, debug, debug_info, debug_warn, err, error, info, itertools::Itertools, matrix::PduCount, result::NotFound, + utils, utils::{ IterStream, ReadyExt, stream::{TryExpect, TryIgnore}, @@ -66,6 +67,7 @@ async fn fresh(services: &Services) -> Result { db["global"].insert(b"retroactively_fix_bad_data_from_roomuserid_joined", []); db["global"].insert(b"fix_referencedevents_missing_sep", []); db["global"].insert(b"fix_readreceiptid_readreceipt_duplicates", []); + db["global"].insert(b"fix_hashed_sentinel_passwords", []); // Create the admin room and server user on first run if services.config.create_admin_room { @@ -145,6 +147,14 @@ async fn migrate(services: &Services) -> Result { fix_readreceiptid_readreceipt_duplicates(services).await?; } + if db["global"] + .get(b"fix_hashed_sentinel_passwords") + .await + .is_not_found() + { + fix_hashed_sentinel_passwords(services).await?; + } + if services.globals.db.database_version().await < 17 { services.globals.db.bump_database_version(17); info!("Migration: Bumped database version to 17"); @@ -582,3 +592,52 @@ async fn fix_readreceiptid_readreceipt_duplicates(services: &Services) -> Result db["global"].insert(b"fix_readreceiptid_readreceipt_duplicates", []); db.engine.sort() } + +async fn fix_hashed_sentinel_passwords(services: &Services) -> Result { + use tuwunel_core::utils::hash::verify_password; + + const PASSWORD_SENTINEL: &str = "*"; + + let db = &services.db; + let cork = db.cork_and_sync(); + let userid_password = db["userid_password"].clone(); + let hashed_sentinel = utils::hash::password(PASSWORD_SENTINEL).map_err(|e| { + err!("Could not apply migration: failed to hash sentinel password: {e:?}") + })?; + + warn!( + "Fixing occurrences of password-hash {hashed_sentinel:?} generated from \ + {PASSWORD_SENTINEL:?}" + ); + + let (checked, good, bad) = userid_password + .stream() + .expect_ok() + .ready_fold( + (0, 0, 0), + |(mut checked, mut good, mut bad): (usize, usize, usize), + (key, val): (&str, &str)| { + let good_sentinel = val == PASSWORD_SENTINEL; + let bad_sentinel = !val.is_empty() + && !good_sentinel + && verify_password(PASSWORD_SENTINEL, val).is_ok(); + + checked = checked.saturating_add(usize::from(true)); + good = good.saturating_add(usize::from(good_sentinel)); + bad = bad.saturating_add(usize::from(bad_sentinel)); + + if bad_sentinel { + userid_password.insert(key, PASSWORD_SENTINEL); + } + + (checked, good, bad) + }, + ) + .await; + + drop(cork); + info!(?checked, ?good, ?bad, "Fixed any occurrences of hashed sentinel passwords"); + + db["global"].insert(b"fix_hashed_sentinel_passwords", []); + db.engine.sort() +} diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index a4ea1189..e1894564 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -24,6 +24,9 @@ use tuwunel_database::{Deserialized, Json, Map}; pub use self::{keys::parse_master_key, register::Register}; +pub const PASSWORD_SENTINEL: &str = "*"; +pub const PASSWORD_DISABLED: &str = ""; + pub struct Service { services: Arc, db: Data, @@ -222,10 +225,7 @@ impl Service { // exception is made for that origin in the condition below. Note that users // with no origin are also password-origin users. let allowed_origins = ["password", "sso"]; - - if let Some(password) = password - && password != "*" - { + if password.is_some() && password != Some(PASSWORD_SENTINEL) { let origin = self.origin(user_id).await; let origin = origin.as_deref().unwrap_or("password"); @@ -236,17 +236,20 @@ impl Service { } } - let is_sentinel = password.is_some_and(|p| p == "*"); - match password.map(utils::hash::password) { | None => { - self.db.userid_password.insert(user_id, b""); + self.db + .userid_password + .insert(user_id, PASSWORD_DISABLED); + }, + | Some(Ok(_)) if password == Some(PASSWORD_SENTINEL) => { + self.db + .userid_password + .insert(user_id, PASSWORD_SENTINEL); }, | Some(Ok(hash)) => { self.db.userid_password.insert(user_id, hash); - if !is_sentinel { - self.db.userid_origin.insert(user_id, "password"); - } + self.db.userid_origin.insert(user_id, "password"); }, | Some(Err(e)) => { return Err!(Request(InvalidParam(