atagen

malice

public
Star0
Watch0
235ce394f90da8ff124691ee4ee2997c979e7d3c
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: crates/malice-server/src/state.rs

973f8fbdaf80atagen2026-09-28 09:10 UTC1
use std::{
973f8fbdaf80atagen2026-09-28 09:10 UTC2
    collections::HashMap,
973f8fbdaf80atagen2026-09-28 09:10 UTC3
    path::{Path, PathBuf},
698e2238368eatagen2026-10-25 10:49 UTC4
    sync::{Arc, PoisonError, Weak},
973f8fbdaf80atagen2026-09-28 09:10 UTC5
    time::Duration,
973f8fbdaf80atagen2026-09-28 09:10 UTC6
};
973f8fbdaf80atagen2026-09-28 09:10 UTC7
973f8fbdaf80atagen2026-09-28 09:10 UTC8
use futures_util::StreamExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC9
use macltok::{
35a820422568atagen2026-10-24 10:15 UTC10
    AccountError, ClientHandle, EncryptedSessionStore, EngineConfig, EngineError, ErrorKind,
35a820422568atagen2026-10-24 10:15 UTC11
    Mirror, MirrorHandle, PasswordLogin, Restore, Retain, Session, SessionStatus, SessionStore,
35a820422568atagen2026-10-24 10:15 UTC12
    SoftLogout, StateChange, StoreConfig, Termination,
973f8fbdaf80atagen2026-09-28 09:10 UTC13
};
973f8fbdaf80atagen2026-09-28 09:10 UTC14
use rand_core::{OsRng, RngCore};
973f8fbdaf80atagen2026-09-28 09:10 UTC15
use sha2::{Digest, Sha256};
698e2238368eatagen2026-10-25 10:49 UTC16
use tokio::{
698e2238368eatagen2026-10-25 10:49 UTC17
    sync::{Mutex, OwnedMutexGuard},
698e2238368eatagen2026-10-25 10:49 UTC18
    task::JoinHandle,
698e2238368eatagen2026-10-25 10:49 UTC19
    time::timeout,
698e2238368eatagen2026-10-25 10:49 UTC20
};
973f8fbdaf80atagen2026-09-28 09:10 UTC21
544a170ead2catagen2026-10-20 11:47 UTC22
use malice_core::{
544a170ead2catagen2026-10-20 11:47 UTC23
    error::{CoreError, Failure},
544a170ead2catagen2026-10-20 11:47 UTC24
    login::login_error,
544a170ead2catagen2026-10-20 11:47 UTC25
};
544a170ead2catagen2026-10-20 11:47 UTC26
aa3dce5114d0atagen2026-10-26 06:09 UTC27
use crate::{csrf::valid_sid, push::HostedPush};
973f8fbdaf80atagen2026-09-28 09:10 UTC28
973f8fbdaf80atagen2026-09-28 09:10 UTC29
const AUTH_TIMEOUT: Duration = Duration::from_secs(20);
973f8fbdaf80atagen2026-09-28 09:10 UTC30
973f8fbdaf80atagen2026-09-28 09:10 UTC31
pub struct UserSession {
973f8fbdaf80atagen2026-09-28 09:10 UTC32
    pub sid: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC33
    pub user_id: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC34
    pub handle: ClientHandle,
973f8fbdaf80atagen2026-09-28 09:10 UTC35
    pub mirror: MirrorHandle,
973f8fbdaf80atagen2026-09-28 09:10 UTC36
    sessions: Arc<dyn SessionStore>,
973f8fbdaf80atagen2026-09-28 09:10 UTC37
    engine: Mutex<Option<JoinHandle<Result<Termination, EngineError>>>>,
973f8fbdaf80atagen2026-09-28 09:10 UTC38
    mirror_task: Mutex<Option<JoinHandle<()>>>,
973f8fbdaf80atagen2026-09-28 09:10 UTC39
}
973f8fbdaf80atagen2026-09-28 09:10 UTC40
973f8fbdaf80atagen2026-09-28 09:10 UTC41
struct Inner {
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    sessions: tokio::sync::RwLock<HashMap<String, Arc<UserSession>>>,
698e2238368eatagen2026-10-25 10:49 UTC43
    /// Serialises restores, logins and removals of the same sid so two engines
698e2238368eatagen2026-10-25 10:49 UTC44
    /// never open the same SQLite store at once, without one slow homeserver
698e2238368eatagen2026-10-25 10:49 UTC45
    /// holding up every other user.
698e2238368eatagen2026-10-25 10:49 UTC46
    sid_locks: std::sync::Mutex<HashMap<String, Weak<Mutex<()>>>>,
973f8fbdaf80atagen2026-09-28 09:10 UTC47
    data_dir: PathBuf,
973f8fbdaf80atagen2026-09-28 09:10 UTC48
    master_key: [u8; 32],
973f8fbdaf80atagen2026-09-28 09:10 UTC49
    cookie_secure: bool,
973f8fbdaf80atagen2026-09-28 09:10 UTC50
    allow_http_homeserver: bool,
973f8fbdaf80atagen2026-09-28 09:10 UTC51
}
973f8fbdaf80atagen2026-09-28 09:10 UTC52
973f8fbdaf80atagen2026-09-28 09:10 UTC53
#[derive(Clone)]
973f8fbdaf80atagen2026-09-28 09:10 UTC54
pub struct AppState {
973f8fbdaf80atagen2026-09-28 09:10 UTC55
    inner: Arc<Inner>,
aa3dce5114d0atagen2026-10-26 06:09 UTC56
    push: Option<Arc<HostedPush>>,
973f8fbdaf80atagen2026-09-28 09:10 UTC57
}
973f8fbdaf80atagen2026-09-28 09:10 UTC58
973f8fbdaf80atagen2026-09-28 09:10 UTC59
#[derive(Debug)]
973f8fbdaf80atagen2026-09-28 09:10 UTC60
pub enum RestoreError {
973f8fbdaf80atagen2026-09-28 09:10 UTC61
    /// No cookie session and nothing on disk: plain guest.
973f8fbdaf80atagen2026-09-28 09:10 UTC62
    NotFound,
973f8fbdaf80atagen2026-09-28 09:10 UTC63
    /// Credential on disk is unusable: caller should clear the cookie.
973f8fbdaf80atagen2026-09-28 09:10 UTC64
    Stale(String),
973f8fbdaf80atagen2026-09-28 09:10 UTC65
    /// Credential looks fine but the engine would not start.
973f8fbdaf80atagen2026-09-28 09:10 UTC66
    Unavailable(String),
973f8fbdaf80atagen2026-09-28 09:10 UTC67
    /// The server logged this device out; it can log back in as the same user.
973f8fbdaf80atagen2026-09-28 09:10 UTC68
    SoftLoggedOut { user_id: String, homeserver: String },
973f8fbdaf80atagen2026-09-28 09:10 UTC69
}
973f8fbdaf80atagen2026-09-28 09:10 UTC70
973f8fbdaf80atagen2026-09-28 09:10 UTC71
impl AppState {
973f8fbdaf80atagen2026-09-28 09:10 UTC72
    pub fn new(
973f8fbdaf80atagen2026-09-28 09:10 UTC73
        data_dir: PathBuf,
973f8fbdaf80atagen2026-09-28 09:10 UTC74
        master_key: [u8; 32],
973f8fbdaf80atagen2026-09-28 09:10 UTC75
        cookie_secure: bool,
973f8fbdaf80atagen2026-09-28 09:10 UTC76
        allow_http_homeserver: bool,
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    ) -> Self {
973f8fbdaf80atagen2026-09-28 09:10 UTC78
        Self {
973f8fbdaf80atagen2026-09-28 09:10 UTC79
            inner: Arc::new(Inner {
973f8fbdaf80atagen2026-09-28 09:10 UTC80
                sessions: tokio::sync::RwLock::new(HashMap::new()),
698e2238368eatagen2026-10-25 10:49 UTC81
                sid_locks: std::sync::Mutex::new(HashMap::new()),
973f8fbdaf80atagen2026-09-28 09:10 UTC82
                data_dir,
973f8fbdaf80atagen2026-09-28 09:10 UTC83
                master_key,
973f8fbdaf80atagen2026-09-28 09:10 UTC84
                cookie_secure,
973f8fbdaf80atagen2026-09-28 09:10 UTC85
                allow_http_homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC86
            }),
aa3dce5114d0atagen2026-10-26 06:09 UTC87
            push: None,
973f8fbdaf80atagen2026-09-28 09:10 UTC88
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC89
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC90
aa3dce5114d0atagen2026-10-26 06:09 UTC91
    pub fn with_push(self, push: HostedPush) -> Self {
aa3dce5114d0atagen2026-10-26 06:09 UTC92
        Self {
aa3dce5114d0atagen2026-10-26 06:09 UTC93
            push: Some(Arc::new(push)),
aa3dce5114d0atagen2026-10-26 06:09 UTC94
            ..self
aa3dce5114d0atagen2026-10-26 06:09 UTC95
        }
aa3dce5114d0atagen2026-10-26 06:09 UTC96
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC97
aa3dce5114d0atagen2026-10-26 06:09 UTC98
    pub fn push(&self) -> Option<&HostedPush> {
aa3dce5114d0atagen2026-10-26 06:09 UTC99
        self.push.as_deref()
aa3dce5114d0atagen2026-10-26 06:09 UTC100
    }
aa3dce5114d0atagen2026-10-26 06:09 UTC101
973f8fbdaf80atagen2026-09-28 09:10 UTC102
    pub fn allow_http_homeserver(&self) -> bool {
973f8fbdaf80atagen2026-09-28 09:10 UTC103
        self.inner.allow_http_homeserver
973f8fbdaf80atagen2026-09-28 09:10 UTC104
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC105
973f8fbdaf80atagen2026-09-28 09:10 UTC106
    pub fn cookie_secure(&self) -> bool {
973f8fbdaf80atagen2026-09-28 09:10 UTC107
        self.inner.cookie_secure
973f8fbdaf80atagen2026-09-28 09:10 UTC108
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC109
698e2238368eatagen2026-10-25 10:49 UTC110
    async fn lock_sid(&self, sid: &str) -> OwnedMutexGuard<()> {
698e2238368eatagen2026-10-25 10:49 UTC111
        let lock = {
698e2238368eatagen2026-10-25 10:49 UTC112
            let mut locks = self
698e2238368eatagen2026-10-25 10:49 UTC113
                .inner
698e2238368eatagen2026-10-25 10:49 UTC114
                .sid_locks
698e2238368eatagen2026-10-25 10:49 UTC115
                .lock()
698e2238368eatagen2026-10-25 10:49 UTC116
                .unwrap_or_else(PoisonError::into_inner);
698e2238368eatagen2026-10-25 10:49 UTC117
            locks.retain(|_, lock| lock.strong_count() > 0);
698e2238368eatagen2026-10-25 10:49 UTC118
            if let Some(lock) = locks.get(sid).and_then(Weak::upgrade) {
698e2238368eatagen2026-10-25 10:49 UTC119
                lock
698e2238368eatagen2026-10-25 10:49 UTC120
            } else {
698e2238368eatagen2026-10-25 10:49 UTC121
                let lock = Arc::new(Mutex::new(()));
698e2238368eatagen2026-10-25 10:49 UTC122
                locks.insert(sid.to_owned(), Arc::downgrade(&lock));
698e2238368eatagen2026-10-25 10:49 UTC123
                lock
698e2238368eatagen2026-10-25 10:49 UTC124
            }
698e2238368eatagen2026-10-25 10:49 UTC125
        };
698e2238368eatagen2026-10-25 10:49 UTC126
        lock.lock_owned().await
698e2238368eatagen2026-10-25 10:49 UTC127
    }
698e2238368eatagen2026-10-25 10:49 UTC128
973f8fbdaf80atagen2026-09-28 09:10 UTC129
    fn sessions_dir(&self) -> PathBuf {
973f8fbdaf80atagen2026-09-28 09:10 UTC130
        self.inner.data_dir.join("sessions")
973f8fbdaf80atagen2026-09-28 09:10 UTC131
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC132
973f8fbdaf80atagen2026-09-28 09:10 UTC133
    fn session_file(&self, sid: &str) -> PathBuf {
973f8fbdaf80atagen2026-09-28 09:10 UTC134
        self.sessions_dir().join(format!("{sid}.bin"))
973f8fbdaf80atagen2026-09-28 09:10 UTC135
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC136
973f8fbdaf80atagen2026-09-28 09:10 UTC137
    fn store_dir(&self, sid: &str) -> PathBuf {
973f8fbdaf80atagen2026-09-28 09:10 UTC138
        self.inner.data_dir.join("stores").join(sid)
973f8fbdaf80atagen2026-09-28 09:10 UTC139
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC140
973f8fbdaf80atagen2026-09-28 09:10 UTC141
    fn store_passphrase(&self, sid: &str) -> String {
973f8fbdaf80atagen2026-09-28 09:10 UTC142
        let mut hasher = Sha256::new();
973f8fbdaf80atagen2026-09-28 09:10 UTC143
        hasher.update(self.inner.master_key);
973f8fbdaf80atagen2026-09-28 09:10 UTC144
        hasher.update(b"malice-store-v1:");
973f8fbdaf80atagen2026-09-28 09:10 UTC145
        hasher.update(sid.as_bytes());
973f8fbdaf80atagen2026-09-28 09:10 UTC146
        hex::encode(hasher.finalize())
973f8fbdaf80atagen2026-09-28 09:10 UTC147
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC148
973f8fbdaf80atagen2026-09-28 09:10 UTC149
    fn store_config(&self, sid: &str) -> StoreConfig {
973f8fbdaf80atagen2026-09-28 09:10 UTC150
        StoreConfig::sqlite(self.store_dir(sid), self.store_passphrase(sid))
973f8fbdaf80atagen2026-09-28 09:10 UTC151
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC152
973f8fbdaf80atagen2026-09-28 09:10 UTC153
    /// A fresh handle onto a sid's record and store. Cheap: nothing touches
973f8fbdaf80atagen2026-09-28 09:10 UTC154
    /// disk until the handle is actually used.
973f8fbdaf80atagen2026-09-28 09:10 UTC155
    fn session_store(&self, sid: &str) -> Arc<dyn SessionStore> {
973f8fbdaf80atagen2026-09-28 09:10 UTC156
        Arc::new(EncryptedSessionStore::file(
973f8fbdaf80atagen2026-09-28 09:10 UTC157
            self.session_file(sid),
973f8fbdaf80atagen2026-09-28 09:10 UTC158
            self.store_config(sid),
973f8fbdaf80atagen2026-09-28 09:10 UTC159
        ))
973f8fbdaf80atagen2026-09-28 09:10 UTC160
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC161
973f8fbdaf80atagen2026-09-28 09:10 UTC162
    async fn apply_retention(&self, sessions: &Arc<dyn SessionStore>, retain: Retain) {
973f8fbdaf80atagen2026-09-28 09:10 UTC163
        let result = match retain {
973f8fbdaf80atagen2026-09-28 09:10 UTC164
            Retain::RecordAndStore => Ok(()),
973f8fbdaf80atagen2026-09-28 09:10 UTC165
            Retain::Store => sessions.soft_log_out().await,
973f8fbdaf80atagen2026-09-28 09:10 UTC166
            Retain::Nothing => sessions.discard().await,
973f8fbdaf80atagen2026-09-28 09:10 UTC167
        };
973f8fbdaf80atagen2026-09-28 09:10 UTC168
        if let Err(error) = result {
973f8fbdaf80atagen2026-09-28 09:10 UTC169
            tracing::warn!(%error, "applying session retention failed");
973f8fbdaf80atagen2026-09-28 09:10 UTC170
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC171
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC172
973f8fbdaf80atagen2026-09-28 09:10 UTC173
    /// Resolve a cookie sid to a live session, restoring from disk on demand.
973f8fbdaf80atagen2026-09-28 09:10 UTC174
    /// The shared map is never held across network or engine work: the fast
973f8fbdaf80atagen2026-09-28 09:10 UTC175
    /// path clones an `Arc` under a read lock, and restores serialise on a
973f8fbdaf80atagen2026-09-28 09:10 UTC176
    /// short init lock with a double-checked insert.
973f8fbdaf80atagen2026-09-28 09:10 UTC177
    pub async fn get_or_restore(&self, sid: &str) -> Result<Arc<UserSession>, RestoreError> {
973f8fbdaf80atagen2026-09-28 09:10 UTC178
        if !valid_sid(sid) {
973f8fbdaf80atagen2026-09-28 09:10 UTC179
            return Err(RestoreError::NotFound);
973f8fbdaf80atagen2026-09-28 09:10 UTC180
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC181
        if let Some(user) = self.inner.sessions.read().await.get(sid).cloned() {
973f8fbdaf80atagen2026-09-28 09:10 UTC182
            return Ok(user);
973f8fbdaf80atagen2026-09-28 09:10 UTC183
        }
698e2238368eatagen2026-10-25 10:49 UTC184
        let _init = self.lock_sid(sid).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC185
        if let Some(user) = self.inner.sessions.read().await.get(sid).cloned() {
973f8fbdaf80atagen2026-09-28 09:10 UTC186
            return Ok(user);
973f8fbdaf80atagen2026-09-28 09:10 UTC187
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC188
        let sessions = self.session_store(sid);
973f8fbdaf80atagen2026-09-28 09:10 UTC189
        let restore = Session::restore(sessions.clone()).await.map_err(|error| {
973f8fbdaf80atagen2026-09-28 09:10 UTC190
            tracing::warn!("restoring persisted session failed: {error}");
973f8fbdaf80atagen2026-09-28 09:10 UTC191
            restore_error(error)
973f8fbdaf80atagen2026-09-28 09:10 UTC192
        })?;
973f8fbdaf80atagen2026-09-28 09:10 UTC193
        match restore {
ecb8598b38afatagen2026-10-22 09:19 UTC194
            None | Some(Restore::OAuthPending) => Err(RestoreError::NotFound),
973f8fbdaf80atagen2026-09-28 09:10 UTC195
            Some(Restore::SoftLoggedOut(soft)) => Err(RestoreError::SoftLoggedOut {
973f8fbdaf80atagen2026-09-28 09:10 UTC196
                user_id: soft.user_id.to_string(),
973f8fbdaf80atagen2026-09-28 09:10 UTC197
                homeserver: soft.homeserver,
973f8fbdaf80atagen2026-09-28 09:10 UTC198
            }),
973f8fbdaf80atagen2026-09-28 09:10 UTC199
            Some(Restore::Resumed(session)) => self
973f8fbdaf80atagen2026-09-28 09:10 UTC200
                .start_user_session(sid.to_owned(), session, sessions)
973f8fbdaf80atagen2026-09-28 09:10 UTC201
                .await
973f8fbdaf80atagen2026-09-28 09:10 UTC202
                .map_err(|error| {
973f8fbdaf80atagen2026-09-28 09:10 UTC203
                    tracing::warn!("starting restored session failed: {error}");
973f8fbdaf80atagen2026-09-28 09:10 UTC204
                    RestoreError::Unavailable(error)
973f8fbdaf80atagen2026-09-28 09:10 UTC205
                }),
973f8fbdaf80atagen2026-09-28 09:10 UTC206
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC207
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC208
973f8fbdaf80atagen2026-09-28 09:10 UTC209
    /// Restore just far enough to tell whether `sid`'s record is a soft
973f8fbdaf80atagen2026-09-28 09:10 UTC210
    /// logout, without disturbing anything else.
973f8fbdaf80atagen2026-09-28 09:10 UTC211
    async fn soft_logged_out(&self, sid: &str) -> Option<SoftLogout> {
973f8fbdaf80atagen2026-09-28 09:10 UTC212
        match Session::restore(self.session_store(sid)).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC213
            Ok(Some(Restore::SoftLoggedOut(soft))) => Some(soft),
973f8fbdaf80atagen2026-09-28 09:10 UTC214
            _ => None,
973f8fbdaf80atagen2026-09-28 09:10 UTC215
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC216
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC217
973f8fbdaf80atagen2026-09-28 09:10 UTC218
    /// Password login: creates a fresh opaque sid and starts the engine,
973f8fbdaf80atagen2026-09-28 09:10 UTC219
    /// which saves the record itself. The password never leaves this call.
973f8fbdaf80atagen2026-09-28 09:10 UTC220
    /// When the cookie sid names the same user's soft-logged-out device, logs
973f8fbdaf80atagen2026-09-28 09:10 UTC221
    /// back in as that device against the same sid instead, keeping its keys.
973f8fbdaf80atagen2026-09-28 09:10 UTC222
    pub async fn login(
973f8fbdaf80atagen2026-09-28 09:10 UTC223
        &self,
973f8fbdaf80atagen2026-09-28 09:10 UTC224
        cookie_sid: Option<&str>,
544a170ead2catagen2026-10-20 11:47 UTC225
        login: PasswordLogin,
544a170ead2catagen2026-10-20 11:47 UTC226
        username: &str,
973f8fbdaf80atagen2026-09-28 09:10 UTC227
        password: String,
544a170ead2catagen2026-10-20 11:47 UTC228
    ) -> Result<Arc<UserSession>, CoreError> {
973f8fbdaf80atagen2026-09-28 09:10 UTC229
        if let Some(sid) = cookie_sid.filter(|sid| valid_sid(sid))
973f8fbdaf80atagen2026-09-28 09:10 UTC230
            && let Some(soft) = self.soft_logged_out(sid).await
973f8fbdaf80atagen2026-09-28 09:10 UTC231
        {
544a170ead2catagen2026-10-20 11:47 UTC232
            if username_matches(username, &soft.user_id) {
973f8fbdaf80atagen2026-09-28 09:10 UTC233
                let sessions = self.session_store(sid);
544a170ead2catagen2026-10-20 11:47 UTC234
                let session = login_with(
544a170ead2catagen2026-10-20 11:47 UTC235
                    soft.password_login(password.clone()),
544a170ead2catagen2026-10-20 11:47 UTC236
                    &password,
544a170ead2catagen2026-10-20 11:47 UTC237
                    sessions.clone(),
544a170ead2catagen2026-10-20 11:47 UTC238
                )
544a170ead2catagen2026-10-20 11:47 UTC239
                .await?;
698e2238368eatagen2026-10-25 10:49 UTC240
                let _init = self.lock_sid(sid).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC241
                return self
973f8fbdaf80atagen2026-09-28 09:10 UTC242
                    .start_user_session(sid.to_owned(), session, sessions)
544a170ead2catagen2026-10-20 11:47 UTC243
                    .await
544a170ead2catagen2026-10-20 11:47 UTC244
                    .map_err(|error| CoreError::new(Failure::Upstream, error));
973f8fbdaf80atagen2026-09-28 09:10 UTC245
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC246
            self.apply_retention(&self.session_store(sid), Retain::Nothing)
973f8fbdaf80atagen2026-09-28 09:10 UTC247
                .await;
973f8fbdaf80atagen2026-09-28 09:10 UTC248
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC249
        let sid = new_sid();
973f8fbdaf80atagen2026-09-28 09:10 UTC250
        let sessions = self.session_store(&sid);
544a170ead2catagen2026-10-20 11:47 UTC251
        let session = login_with(login, &password, sessions.clone()).await?;
698e2238368eatagen2026-10-25 10:49 UTC252
        let _init = self.lock_sid(&sid).await;
544a170ead2catagen2026-10-20 11:47 UTC253
        self.start_user_session(sid, session, sessions)
544a170ead2catagen2026-10-20 11:47 UTC254
            .await
544a170ead2catagen2026-10-20 11:47 UTC255
            .map_err(|error| CoreError::new(Failure::Upstream, error))
973f8fbdaf80atagen2026-09-28 09:10 UTC256
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC257
973f8fbdaf80atagen2026-09-28 09:10 UTC258
    async fn start_user_session(
973f8fbdaf80atagen2026-09-28 09:10 UTC259
        &self,
973f8fbdaf80atagen2026-09-28 09:10 UTC260
        sid: String,
973f8fbdaf80atagen2026-09-28 09:10 UTC261
        session: Session,
973f8fbdaf80atagen2026-09-28 09:10 UTC262
        sessions: Arc<dyn SessionStore>,
973f8fbdaf80atagen2026-09-28 09:10 UTC263
    ) -> Result<Arc<UserSession>, String> {
973f8fbdaf80atagen2026-09-28 09:10 UTC264
        let (engine, handle, events) = session
973f8fbdaf80atagen2026-09-28 09:10 UTC265
            .build(EngineConfig::default())
973f8fbdaf80atagen2026-09-28 09:10 UTC266
            .await
973f8fbdaf80atagen2026-09-28 09:10 UTC267
            .map_err(|error| format!("starting sync engine: {error}"))?;
973f8fbdaf80atagen2026-09-28 09:10 UTC268
        let engine_task = tokio::spawn(engine.run());
973f8fbdaf80atagen2026-09-28 09:10 UTC269
        let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);
973f8fbdaf80atagen2026-09-28 09:10 UTC270
        let mirror_task = tokio::spawn(mirror.run());
973f8fbdaf80atagen2026-09-28 09:10 UTC271
        let Some(user_id) = wait_authenticated(&mirror_handle).await else {
973f8fbdaf80atagen2026-09-28 09:10 UTC272
            engine_task.abort();
973f8fbdaf80atagen2026-09-28 09:10 UTC273
            let _ = engine_task.await;
973f8fbdaf80atagen2026-09-28 09:10 UTC274
            mirror_task.abort();
973f8fbdaf80atagen2026-09-28 09:10 UTC275
            let _ = mirror_task.await;
973f8fbdaf80atagen2026-09-28 09:10 UTC276
            return Err("sync engine stopped before authenticating".to_owned());
973f8fbdaf80atagen2026-09-28 09:10 UTC277
        };
973f8fbdaf80atagen2026-09-28 09:10 UTC278
        let user = Arc::new(UserSession {
973f8fbdaf80atagen2026-09-28 09:10 UTC279
            sid: sid.clone(),
973f8fbdaf80atagen2026-09-28 09:10 UTC280
            user_id,
973f8fbdaf80atagen2026-09-28 09:10 UTC281
            handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC282
            mirror: mirror_handle,
973f8fbdaf80atagen2026-09-28 09:10 UTC283
            sessions,
973f8fbdaf80atagen2026-09-28 09:10 UTC284
            engine: Mutex::new(Some(engine_task)),
973f8fbdaf80atagen2026-09-28 09:10 UTC285
            mirror_task: Mutex::new(Some(mirror_task)),
973f8fbdaf80atagen2026-09-28 09:10 UTC286
        });
973f8fbdaf80atagen2026-09-28 09:10 UTC287
        {
973f8fbdaf80atagen2026-09-28 09:10 UTC288
            let mut sessions_map = self.inner.sessions.write().await;
973f8fbdaf80atagen2026-09-28 09:10 UTC289
            sessions_map.insert(sid, user.clone());
973f8fbdaf80atagen2026-09-28 09:10 UTC290
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC291
        let supervisor_state = self.clone();
973f8fbdaf80atagen2026-09-28 09:10 UTC292
        let supervisor_user = user.clone();
973f8fbdaf80atagen2026-09-28 09:10 UTC293
        tokio::spawn(async move {
973f8fbdaf80atagen2026-09-28 09:10 UTC294
            supervise(supervisor_state, supervisor_user).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC295
        });
973f8fbdaf80atagen2026-09-28 09:10 UTC296
        Ok(user)
973f8fbdaf80atagen2026-09-28 09:10 UTC297
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC298
973f8fbdaf80atagen2026-09-28 09:10 UTC299
    /// Logout: best-effort server-side logout, then drop all local state and
973f8fbdaf80atagen2026-09-28 09:10 UTC300
    /// delete persisted secrets. Never holds the map lock across the network
973f8fbdaf80atagen2026-09-28 09:10 UTC301
    /// call. Forces `Retain::Nothing` regardless of what the engine's own
973f8fbdaf80atagen2026-09-28 09:10 UTC302
    /// termination said, so an explicit logout always ends in a full discard
973f8fbdaf80atagen2026-09-28 09:10 UTC303
    /// even if it raced the supervisor over a server-initiated soft logout.
973f8fbdaf80atagen2026-09-28 09:10 UTC304
    pub async fn logout(&self, sid: &str) {
973f8fbdaf80atagen2026-09-28 09:10 UTC305
        let user = { self.inner.sessions.read().await.get(sid).cloned() };
973f8fbdaf80atagen2026-09-28 09:10 UTC306
        if let Some(user) = user {
aa3dce5114d0atagen2026-10-26 06:09 UTC307
            let pushers = self
aa3dce5114d0atagen2026-10-26 06:09 UTC308
                .push()
aa3dce5114d0atagen2026-10-26 06:09 UTC309
                .map(|push| push.pusher_keys(sid))
aa3dce5114d0atagen2026-10-26 06:09 UTC310
                .unwrap_or_default();
973f8fbdaf80atagen2026-09-28 09:10 UTC311
            // answers once the server has let go of the device, and the engine stops itself
aa3dce5114d0atagen2026-10-26 06:09 UTC312
            let _ = timeout(Duration::from_secs(5), user.handle.logout(pushers)).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC313
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC314
        self.remove_session(sid, Retain::Nothing).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC315
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC316
973f8fbdaf80atagen2026-09-28 09:10 UTC317
    /// Forget an in-memory session, abort its engine and mirror tasks, and
973f8fbdaf80atagen2026-09-28 09:10 UTC318
    /// apply the given retention to its persisted secrets. Called both for
973f8fbdaf80atagen2026-09-28 09:10 UTC319
    /// explicit logout (always `Retain::Nothing`) and from the supervisor
973f8fbdaf80atagen2026-09-28 09:10 UTC320
    /// once an engine's `Termination` says what its record and store are
973f8fbdaf80atagen2026-09-28 09:10 UTC321
    /// still worth. Retention always runs, even if the session was already
973f8fbdaf80atagen2026-09-28 09:10 UTC322
    /// gone from the map, so a racing logout's `Nothing` still lands.
973f8fbdaf80atagen2026-09-28 09:10 UTC323
    pub async fn remove_session(&self, sid: &str, retain: Retain) {
973f8fbdaf80atagen2026-09-28 09:10 UTC324
        // A restore must not reopen the store between removing the map entry
973f8fbdaf80atagen2026-09-28 09:10 UTC325
        // and deleting its persisted credentials.
698e2238368eatagen2026-10-25 10:49 UTC326
        let _init = self.lock_sid(sid).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC327
        let user = self.inner.sessions.write().await.remove(sid);
973f8fbdaf80atagen2026-09-28 09:10 UTC328
        let sessions = match &user {
973f8fbdaf80atagen2026-09-28 09:10 UTC329
            Some(user) => user.sessions.clone(),
973f8fbdaf80atagen2026-09-28 09:10 UTC330
            None => self.session_store(sid),
973f8fbdaf80atagen2026-09-28 09:10 UTC331
        };
973f8fbdaf80atagen2026-09-28 09:10 UTC332
        if let Some(user) = &user {
973f8fbdaf80atagen2026-09-28 09:10 UTC333
            if let Some(task) = user.engine.lock().await.take() {
973f8fbdaf80atagen2026-09-28 09:10 UTC334
                task.abort();
973f8fbdaf80atagen2026-09-28 09:10 UTC335
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC336
            if let Some(task) = user.mirror_task.lock().await.take() {
973f8fbdaf80atagen2026-09-28 09:10 UTC337
                task.abort();
973f8fbdaf80atagen2026-09-28 09:10 UTC338
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC339
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC340
        self.apply_retention(&sessions, retain).await;
aa3dce5114d0atagen2026-10-26 06:09 UTC341
        if retain == Retain::Nothing
aa3dce5114d0atagen2026-10-26 06:09 UTC342
            && let Some(push) = self.push()
aa3dce5114d0atagen2026-10-26 06:09 UTC343
        {
aa3dce5114d0atagen2026-10-26 06:09 UTC344
            push.forget_session(sid);
aa3dce5114d0atagen2026-10-26 06:09 UTC345
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC346
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC347
973f8fbdaf80atagen2026-09-28 09:10 UTC348
    /// Graceful shutdown: ask every engine to stop, keep persisted records so
973f8fbdaf80atagen2026-09-28 09:10 UTC349
    /// the next boot restores them.
973f8fbdaf80atagen2026-09-28 09:10 UTC350
    pub async fn shutdown_gracefully(&self) {
973f8fbdaf80atagen2026-09-28 09:10 UTC351
        let users: Vec<Arc<UserSession>> =
973f8fbdaf80atagen2026-09-28 09:10 UTC352
            self.inner.sessions.read().await.values().cloned().collect();
973f8fbdaf80atagen2026-09-28 09:10 UTC353
        for user in &users {
973f8fbdaf80atagen2026-09-28 09:10 UTC354
            // the supervisor keeps waiting, so a shutdown can answer once `Stopped` is out
973f8fbdaf80atagen2026-09-28 09:10 UTC355
            let handle = user.handle.clone();
973f8fbdaf80atagen2026-09-28 09:10 UTC356
            tokio::spawn(async move {
973f8fbdaf80atagen2026-09-28 09:10 UTC357
                let _ = timeout(Duration::from_secs(5), handle.shutdown()).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC358
            });
973f8fbdaf80atagen2026-09-28 09:10 UTC359
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC360
        // Give engines a moment to emit `Stopped`; the supervisor then drops
973f8fbdaf80atagen2026-09-28 09:10 UTC361
        // each in-memory session while leaving persisted records behind.
973f8fbdaf80atagen2026-09-28 09:10 UTC362
        timeout(Duration::from_secs(5), async {
973f8fbdaf80atagen2026-09-28 09:10 UTC363
            loop {
973f8fbdaf80atagen2026-09-28 09:10 UTC364
                if self.inner.sessions.read().await.is_empty() {
973f8fbdaf80atagen2026-09-28 09:10 UTC365
                    break;
973f8fbdaf80atagen2026-09-28 09:10 UTC366
                }
973f8fbdaf80atagen2026-09-28 09:10 UTC367
                tokio::time::sleep(Duration::from_millis(50)).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC368
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC369
        })
973f8fbdaf80atagen2026-09-28 09:10 UTC370
        .await
973f8fbdaf80atagen2026-09-28 09:10 UTC371
        .ok();
973f8fbdaf80atagen2026-09-28 09:10 UTC372
        for user in &users {
973f8fbdaf80atagen2026-09-28 09:10 UTC373
            if let Some(task) = user.engine.lock().await.take() {
973f8fbdaf80atagen2026-09-28 09:10 UTC374
                task.abort();
973f8fbdaf80atagen2026-09-28 09:10 UTC375
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC376
            if let Some(task) = user.mirror_task.lock().await.take() {
973f8fbdaf80atagen2026-09-28 09:10 UTC377
                task.abort();
973f8fbdaf80atagen2026-09-28 09:10 UTC378
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC379
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC380
        self.inner.sessions.write().await.clear();
973f8fbdaf80atagen2026-09-28 09:10 UTC381
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC382
}
973f8fbdaf80atagen2026-09-28 09:10 UTC383
973f8fbdaf80atagen2026-09-28 09:10 UTC384
fn restore_error(error: AccountError) -> RestoreError {
973f8fbdaf80atagen2026-09-28 09:10 UTC385
    match error {
973f8fbdaf80atagen2026-09-28 09:10 UTC386
        AccountError::StoreInUse => {
973f8fbdaf80atagen2026-09-28 09:10 UTC387
            RestoreError::Unavailable("another connection is using this session".to_owned())
973f8fbdaf80atagen2026-09-28 09:10 UTC388
        }
35a820422568atagen2026-10-24 10:15 UTC389
        AccountError::Operation {
35a820422568atagen2026-10-24 10:15 UTC390
            kind: ErrorKind::Network | ErrorKind::Server | ErrorKind::RateLimited,
35a820422568atagen2026-10-24 10:15 UTC391
            ..
35a820422568atagen2026-10-24 10:15 UTC392
        } => RestoreError::Unavailable(format!("restoring persisted session failed: {error}")),
35a820422568atagen2026-10-24 10:15 UTC393
        error @ (AccountError::Operation { .. }
35a820422568atagen2026-10-24 10:15 UTC394
        | AccountError::MissingSession
35a820422568atagen2026-10-24 10:15 UTC395
        | AccountError::UnsupportedAuthentication
35a820422568atagen2026-10-24 10:15 UTC396
        | AccountError::OAuthCallbackAlreadySubmitted
35a820422568atagen2026-10-24 10:15 UTC397
        | AccountError::NoPendingOAuth
35a820422568atagen2026-10-24 10:15 UTC398
        | AccountError::InvalidHomeserver(_)
35a820422568atagen2026-10-24 10:15 UTC399
        | AccountError::AlreadyLoggedIn(_)
35a820422568atagen2026-10-24 10:15 UTC400
        | AccountError::StoreDeletePending
35a820422568atagen2026-10-24 10:15 UTC401
        | AccountError::Unsaved { .. }
35a820422568atagen2026-10-24 10:15 UTC402
        | AccountError::Engine(_)) => {
35a820422568atagen2026-10-24 10:15 UTC403
            RestoreError::Stale(format!("restoring persisted session failed: {error}"))
35a820422568atagen2026-10-24 10:15 UTC404
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC405
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC406
}
973f8fbdaf80atagen2026-09-28 09:10 UTC407
973f8fbdaf80atagen2026-09-28 09:10 UTC408
async fn login_with(
973f8fbdaf80atagen2026-09-28 09:10 UTC409
    login: PasswordLogin,
544a170ead2catagen2026-10-20 11:47 UTC410
    password: &str,
973f8fbdaf80atagen2026-09-28 09:10 UTC411
    sessions: Arc<dyn SessionStore>,
544a170ead2catagen2026-10-20 11:47 UTC412
) -> Result<Session, CoreError> {
973f8fbdaf80atagen2026-09-28 09:10 UTC413
    match Session::login_password(login, sessions).await {
973f8fbdaf80atagen2026-09-28 09:10 UTC414
        Ok(session) => Ok(session),
973f8fbdaf80atagen2026-09-28 09:10 UTC415
        Err(AccountError::Unsaved { session, .. }) => session
973f8fbdaf80atagen2026-09-28 09:10 UTC416
            .save()
973f8fbdaf80atagen2026-09-28 09:10 UTC417
            .await
544a170ead2catagen2026-10-20 11:47 UTC418
            .map_err(|_| CoreError::new(Failure::Upstream, "could not save the session")),
544a170ead2catagen2026-10-20 11:47 UTC419
        Err(error) => Err(login_error(&error, password)),
973f8fbdaf80atagen2026-09-28 09:10 UTC420
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC421
}
973f8fbdaf80atagen2026-09-28 09:10 UTC422
973f8fbdaf80atagen2026-09-28 09:10 UTC423
fn username_matches(submitted: &str, user_id: &ruma::OwnedUserId) -> bool {
973f8fbdaf80atagen2026-09-28 09:10 UTC424
    let submitted = submitted.trim();
973f8fbdaf80atagen2026-09-28 09:10 UTC425
    submitted == user_id.as_str() || submitted == user_id.localpart()
973f8fbdaf80atagen2026-09-28 09:10 UTC426
}
973f8fbdaf80atagen2026-09-28 09:10 UTC427
973f8fbdaf80atagen2026-09-28 09:10 UTC428
/// Wait for the mirror to learn the user id. Fails once the subscription
973f8fbdaf80atagen2026-09-28 09:10 UTC429
/// ends or the session leaves `Active` before that happens.
973f8fbdaf80atagen2026-09-28 09:10 UTC430
async fn wait_authenticated(mirror: &MirrorHandle) -> Option<String> {
973f8fbdaf80atagen2026-09-28 09:10 UTC431
    timeout(AUTH_TIMEOUT, async {
973f8fbdaf80atagen2026-09-28 09:10 UTC432
        let mut updates = mirror.subscribe();
973f8fbdaf80atagen2026-09-28 09:10 UTC433
        loop {
973f8fbdaf80atagen2026-09-28 09:10 UTC434
            let update = updates.next().await?;
973f8fbdaf80atagen2026-09-28 09:10 UTC435
            match update.change {
973f8fbdaf80atagen2026-09-28 09:10 UTC436
                StateChange::State(state) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC437
                    if let Some(user_id) = &state.user_id {
973f8fbdaf80atagen2026-09-28 09:10 UTC438
                        return Some(user_id.to_string());
973f8fbdaf80atagen2026-09-28 09:10 UTC439
                    }
973f8fbdaf80atagen2026-09-28 09:10 UTC440
                    if state.session != SessionStatus::Active {
973f8fbdaf80atagen2026-09-28 09:10 UTC441
                        return None;
973f8fbdaf80atagen2026-09-28 09:10 UTC442
                    }
973f8fbdaf80atagen2026-09-28 09:10 UTC443
                }
973f8fbdaf80atagen2026-09-28 09:10 UTC444
                StateChange::Authenticated(user_id) => return Some(user_id.to_string()),
973f8fbdaf80atagen2026-09-28 09:10 UTC445
                StateChange::Session(status) if status != SessionStatus::Active => return None,
35a820422568atagen2026-10-24 10:15 UTC446
                StateChange::Session(_)
35a820422568atagen2026-10-24 10:15 UTC447
                | StateChange::SessionSaveFailed(_)
35a820422568atagen2026-10-24 10:15 UTC448
                | StateChange::SessionSaved
35a820422568atagen2026-10-24 10:15 UTC449
                | StateChange::Connection(_)
35a820422568atagen2026-10-24 10:15 UTC450
                | StateChange::Encryption(_)
35a820422568atagen2026-10-24 10:15 UTC451
                | StateChange::Verification(_)
35a820422568atagen2026-10-24 10:15 UTC452
                | StateChange::VerificationTarget(_)
f40cf95cc4featagen2026-10-25 03:17 UTC453
                | StateChange::Mentions(_)
35a820422568atagen2026-10-24 10:15 UTC454
                | StateChange::NotificationSettings
35a820422568atagen2026-10-24 10:15 UTC455
                | StateChange::Rooms(_)
35a820422568atagen2026-10-24 10:15 UTC456
                | StateChange::ImagePacks(_)
35a820422568atagen2026-10-24 10:15 UTC457
                | StateChange::Timeline { .. }
35a820422568atagen2026-10-24 10:15 UTC458
                | StateChange::History { .. }
f40cf95cc4featagen2026-10-25 03:17 UTC459
                | StateChange::HistoryRepair(_)
1e2ab8104071atagen2026-10-25 08:05 UTC460
                | StateChange::RecentHistory { .. }
249e439c27e5atagen2026-10-25 13:26 UTC461
                | StateChange::ThreadList { .. }
249e439c27e5atagen2026-10-25 13:26 UTC462
                | StateChange::EventContextClosed { .. }
249e439c27e5atagen2026-10-25 13:26 UTC463
                | StateChange::SearchClosed { .. }
35a820422568atagen2026-10-24 10:15 UTC464
                | StateChange::Typing(_)
35a820422568atagen2026-10-24 10:15 UTC465
                | StateChange::Upload { .. }
35a820422568atagen2026-10-24 10:15 UTC466
                | StateChange::UploadFinished { .. }
35a820422568atagen2026-10-24 10:15 UTC467
                | StateChange::Failed(_) => {}
973f8fbdaf80atagen2026-09-28 09:10 UTC468
            }
973f8fbdaf80atagen2026-09-28 09:10 UTC469
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC470
    })
973f8fbdaf80atagen2026-09-28 09:10 UTC471
    .await
973f8fbdaf80atagen2026-09-28 09:10 UTC472
    .ok()
973f8fbdaf80atagen2026-09-28 09:10 UTC473
    .flatten()
973f8fbdaf80atagen2026-09-28 09:10 UTC474
}
973f8fbdaf80atagen2026-09-28 09:10 UTC475
973f8fbdaf80atagen2026-09-28 09:10 UTC476
/// Awaits a session's engine, then its mirror, removes it from the map, and
973f8fbdaf80atagen2026-09-28 09:10 UTC477
/// applies the retention its `Termination` earns. Taking the engine
973f8fbdaf80atagen2026-09-28 09:10 UTC478
/// `JoinHandle` is the single point two racing teardowns (this task and an
973f8fbdaf80atagen2026-09-28 09:10 UTC479
/// explicit logout) arbitrate over: whichever finds it empty does nothing
973f8fbdaf80atagen2026-09-28 09:10 UTC480
/// further, so the engine and mirror are never aborted twice.
973f8fbdaf80atagen2026-09-28 09:10 UTC481
async fn supervise(state: AppState, user: Arc<UserSession>) {
973f8fbdaf80atagen2026-09-28 09:10 UTC482
    let Some(engine_task) = user.engine.lock().await.take() else {
973f8fbdaf80atagen2026-09-28 09:10 UTC483
        return;
973f8fbdaf80atagen2026-09-28 09:10 UTC484
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC485
    let retain = match engine_task.await {
973f8fbdaf80atagen2026-09-28 09:10 UTC486
        Ok(Ok(termination)) => termination.retain(),
973f8fbdaf80atagen2026-09-28 09:10 UTC487
        Ok(Err(error)) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC488
            tracing::warn!(%error, "sync engine ended with an error; keeping its session");
973f8fbdaf80atagen2026-09-28 09:10 UTC489
            Retain::RecordAndStore
973f8fbdaf80atagen2026-09-28 09:10 UTC490
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC491
        Err(error) => {
973f8fbdaf80atagen2026-09-28 09:10 UTC492
            tracing::warn!(%error, "sync engine task did not finish cleanly; keeping its session");
973f8fbdaf80atagen2026-09-28 09:10 UTC493
            Retain::RecordAndStore
973f8fbdaf80atagen2026-09-28 09:10 UTC494
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC495
    };
973f8fbdaf80atagen2026-09-28 09:10 UTC496
    if let Some(mirror_task) = user.mirror_task.lock().await.take() {
973f8fbdaf80atagen2026-09-28 09:10 UTC497
        let _ = mirror_task.await;
973f8fbdaf80atagen2026-09-28 09:10 UTC498
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC499
    state.remove_session(&user.sid, retain).await;
973f8fbdaf80atagen2026-09-28 09:10 UTC500
}
973f8fbdaf80atagen2026-09-28 09:10 UTC501
973f8fbdaf80atagen2026-09-28 09:10 UTC502
fn new_sid() -> String {
973f8fbdaf80atagen2026-09-28 09:10 UTC503
    let mut bytes = [0u8; 32];
973f8fbdaf80atagen2026-09-28 09:10 UTC504
    OsRng.fill_bytes(&mut bytes);
973f8fbdaf80atagen2026-09-28 09:10 UTC505
    hex::encode(bytes)
973f8fbdaf80atagen2026-09-28 09:10 UTC506
}
973f8fbdaf80atagen2026-09-28 09:10 UTC507
973f8fbdaf80atagen2026-09-28 09:10 UTC508
/// Load the 32-byte master key, creating it with owner-only permissions when
973f8fbdaf80atagen2026-09-28 09:10 UTC509
/// absent. Secrets must never be world-readable.
973f8fbdaf80atagen2026-09-28 09:10 UTC510
pub fn load_or_create_master_key(data_dir: &Path) -> Result<[u8; 32], String> {
973f8fbdaf80atagen2026-09-28 09:10 UTC511
    create_private_dir(data_dir).map_err(|error| format!("creating data dir: {error}"))?;
973f8fbdaf80atagen2026-09-28 09:10 UTC512
    restrict_dir(data_dir);
973f8fbdaf80atagen2026-09-28 09:10 UTC513
    let path = data_dir.join("master.key");
973f8fbdaf80atagen2026-09-28 09:10 UTC514
    if path.exists() {
973f8fbdaf80atagen2026-09-28 09:10 UTC515
        let bytes = std::fs::read(&path).map_err(|error| format!("reading master key: {error}"))?;
973f8fbdaf80atagen2026-09-28 09:10 UTC516
        restrict_file(&path);
973f8fbdaf80atagen2026-09-28 09:10 UTC517
        if bytes.len() != 32 {
973f8fbdaf80atagen2026-09-28 09:10 UTC518
            return Err("master key has an unexpected length".to_owned());
973f8fbdaf80atagen2026-09-28 09:10 UTC519
        }
973f8fbdaf80atagen2026-09-28 09:10 UTC520
        let mut key = [0u8; 32];
973f8fbdaf80atagen2026-09-28 09:10 UTC521
        key.copy_from_slice(&bytes);
973f8fbdaf80atagen2026-09-28 09:10 UTC522
        return Ok(key);
973f8fbdaf80atagen2026-09-28 09:10 UTC523
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC524
    let mut key = [0u8; 32];
973f8fbdaf80atagen2026-09-28 09:10 UTC525
    OsRng.fill_bytes(&mut key);
973f8fbdaf80atagen2026-09-28 09:10 UTC526
    let mut file =
973f8fbdaf80atagen2026-09-28 09:10 UTC527
        create_private_file(&path).map_err(|error| format!("creating master key: {error}"))?;
973f8fbdaf80atagen2026-09-28 09:10 UTC528
    if let Err(error) = std::io::Write::write_all(&mut file, &key).and_then(|_| file.sync_all()) {
973f8fbdaf80atagen2026-09-28 09:10 UTC529
        let _ = std::fs::remove_file(&path);
973f8fbdaf80atagen2026-09-28 09:10 UTC530
        return Err(format!("writing master key: {error}"));
973f8fbdaf80atagen2026-09-28 09:10 UTC531
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC532
    Ok(key)
973f8fbdaf80atagen2026-09-28 09:10 UTC533
}
973f8fbdaf80atagen2026-09-28 09:10 UTC534
973f8fbdaf80atagen2026-09-28 09:10 UTC535
fn create_private_dir(path: &Path) -> std::io::Result<()> {
973f8fbdaf80atagen2026-09-28 09:10 UTC536
    let mut builder = std::fs::DirBuilder::new();
973f8fbdaf80atagen2026-09-28 09:10 UTC537
    builder.recursive(true);
973f8fbdaf80atagen2026-09-28 09:10 UTC538
    #[cfg(unix)]
973f8fbdaf80atagen2026-09-28 09:10 UTC539
    {
973f8fbdaf80atagen2026-09-28 09:10 UTC540
        use std::os::unix::fs::DirBuilderExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC541
        builder.mode(0o700);
973f8fbdaf80atagen2026-09-28 09:10 UTC542
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC543
    builder.create(path)
973f8fbdaf80atagen2026-09-28 09:10 UTC544
}
973f8fbdaf80atagen2026-09-28 09:10 UTC545
973f8fbdaf80atagen2026-09-28 09:10 UTC546
fn create_private_file(path: &Path) -> std::io::Result<std::fs::File> {
973f8fbdaf80atagen2026-09-28 09:10 UTC547
    let mut options = std::fs::OpenOptions::new();
973f8fbdaf80atagen2026-09-28 09:10 UTC548
    options.write(true).create_new(true);
973f8fbdaf80atagen2026-09-28 09:10 UTC549
    #[cfg(unix)]
973f8fbdaf80atagen2026-09-28 09:10 UTC550
    {
973f8fbdaf80atagen2026-09-28 09:10 UTC551
        use std::os::unix::fs::OpenOptionsExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC552
        options.mode(0o600);
973f8fbdaf80atagen2026-09-28 09:10 UTC553
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC554
    options.open(path)
973f8fbdaf80atagen2026-09-28 09:10 UTC555
}
973f8fbdaf80atagen2026-09-28 09:10 UTC556
973f8fbdaf80atagen2026-09-28 09:10 UTC557
pub fn ensure_dirs(data_dir: &Path) {
973f8fbdaf80atagen2026-09-28 09:10 UTC558
    for dir in [
973f8fbdaf80atagen2026-09-28 09:10 UTC559
        data_dir.to_path_buf(),
973f8fbdaf80atagen2026-09-28 09:10 UTC560
        data_dir.join("sessions"),
973f8fbdaf80atagen2026-09-28 09:10 UTC561
        data_dir.join("stores"),
973f8fbdaf80atagen2026-09-28 09:10 UTC562
    ] {
973f8fbdaf80atagen2026-09-28 09:10 UTC563
        let _ = create_private_dir(&dir);
973f8fbdaf80atagen2026-09-28 09:10 UTC564
        restrict_dir(&dir);
973f8fbdaf80atagen2026-09-28 09:10 UTC565
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC566
}
973f8fbdaf80atagen2026-09-28 09:10 UTC567
973f8fbdaf80atagen2026-09-28 09:10 UTC568
#[cfg(unix)]
973f8fbdaf80atagen2026-09-28 09:10 UTC569
fn restrict_dir(path: &Path) {
973f8fbdaf80atagen2026-09-28 09:10 UTC570
    use std::os::unix::fs::PermissionsExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC571
    let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
973f8fbdaf80atagen2026-09-28 09:10 UTC572
}
973f8fbdaf80atagen2026-09-28 09:10 UTC573
973f8fbdaf80atagen2026-09-28 09:10 UTC574
#[cfg(not(unix))]
973f8fbdaf80atagen2026-09-28 09:10 UTC575
fn restrict_dir(_path: &Path) {}
973f8fbdaf80atagen2026-09-28 09:10 UTC576
973f8fbdaf80atagen2026-09-28 09:10 UTC577
#[cfg(unix)]
973f8fbdaf80atagen2026-09-28 09:10 UTC578
fn restrict_file(path: &Path) {
973f8fbdaf80atagen2026-09-28 09:10 UTC579
    use std::os::unix::fs::PermissionsExt;
973f8fbdaf80atagen2026-09-28 09:10 UTC580
    let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
973f8fbdaf80atagen2026-09-28 09:10 UTC581
}
973f8fbdaf80atagen2026-09-28 09:10 UTC582
973f8fbdaf80atagen2026-09-28 09:10 UTC583
#[cfg(not(unix))]
973f8fbdaf80atagen2026-09-28 09:10 UTC584
fn restrict_file(_path: &Path) {}
698e2238368eatagen2026-10-25 10:49 UTC585
698e2238368eatagen2026-10-25 10:49 UTC586
#[cfg(test)]
698e2238368eatagen2026-10-25 10:49 UTC587
mod tests {
698e2238368eatagen2026-10-25 10:49 UTC588
    use super::*;
698e2238368eatagen2026-10-25 10:49 UTC589
698e2238368eatagen2026-10-25 10:49 UTC590
    fn state() -> (AppState, tempfile::TempDir) {
698e2238368eatagen2026-10-25 10:49 UTC591
        let dir = tempfile::tempdir().expect("temp dir");
698e2238368eatagen2026-10-25 10:49 UTC592
        (
698e2238368eatagen2026-10-25 10:49 UTC593
            AppState::new(dir.path().to_owned(), [7; 32], false, false),
698e2238368eatagen2026-10-25 10:49 UTC594
            dir,
698e2238368eatagen2026-10-25 10:49 UTC595
        )
698e2238368eatagen2026-10-25 10:49 UTC596
    }
698e2238368eatagen2026-10-25 10:49 UTC597
698e2238368eatagen2026-10-25 10:49 UTC598
    #[tokio::test]
698e2238368eatagen2026-10-25 10:49 UTC599
    async fn sid_locks_only_serialise_the_same_sid() {
698e2238368eatagen2026-10-25 10:49 UTC600
        let (state, _dir) = state();
698e2238368eatagen2026-10-25 10:49 UTC601
        let held = state.lock_sid("alpha").await;
698e2238368eatagen2026-10-25 10:49 UTC602
        let other = timeout(Duration::from_millis(50), state.lock_sid("beta")).await;
698e2238368eatagen2026-10-25 10:49 UTC603
        assert!(other.is_ok(), "a different sid must not wait");
698e2238368eatagen2026-10-25 10:49 UTC604
        let same = timeout(Duration::from_millis(50), state.lock_sid("alpha")).await;
698e2238368eatagen2026-10-25 10:49 UTC605
        assert!(same.is_err(), "the same sid must wait for the holder");
698e2238368eatagen2026-10-25 10:49 UTC606
        drop(held);
698e2238368eatagen2026-10-25 10:49 UTC607
        let again = timeout(Duration::from_millis(50), state.lock_sid("alpha")).await;
698e2238368eatagen2026-10-25 10:49 UTC608
        assert!(again.is_ok(), "the sid is free once the holder drops");
698e2238368eatagen2026-10-25 10:49 UTC609
    }
698e2238368eatagen2026-10-25 10:49 UTC610
698e2238368eatagen2026-10-25 10:49 UTC611
    #[tokio::test]
698e2238368eatagen2026-10-25 10:49 UTC612
    async fn sid_locks_forget_released_sids() {
698e2238368eatagen2026-10-25 10:49 UTC613
        let (state, _dir) = state();
698e2238368eatagen2026-10-25 10:49 UTC614
        drop(state.lock_sid("alpha").await);
698e2238368eatagen2026-10-25 10:49 UTC615
        drop(state.lock_sid("beta").await);
698e2238368eatagen2026-10-25 10:49 UTC616
        let live = state.inner.sid_locks.lock().unwrap().len();
698e2238368eatagen2026-10-25 10:49 UTC617
        assert!(live <= 1, "released locks are pruned on the next lookup");
698e2238368eatagen2026-10-25 10:49 UTC618
    }
698e2238368eatagen2026-10-25 10:49 UTC619
}