From 2b3495be655196fab7f7ba9f0da59b8c29e9b77c Mon Sep 17 00:00:00 2001 From: reverse Date: Wed, 12 Aug 2026 13:07:28 +0000 Subject: [PATCH] =?UTF-8?q?customtitle:=20verify=20a=20KDF=20/TITLE=20pass?= =?UTF-8?q?word=20off=20the=20core=20thread=20(Event::TitleAuth)=20?= =?UTF-8?q?=E2=80=94=20/TITLE=20spam=20can't=20freeze=20the=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ircd.rs | 19 ++++++++ src/modules/customtitle.rs | 91 ++++++++++++++++++++++++-------------- 2 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/ircd.rs b/src/ircd.rs index 598bfe0..b9cafce 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -66,6 +66,13 @@ pub enum Event { algo: String, hash: Option, }, + /// A background `/TITLE` password verify finished (see `crate::modules::customtitle`). + TitleAuth { + uid: Uid, + ok: bool, + title: String, + vhost: String, + }, /// A module's async HTTP request finished. `tag` is `":"` /// so the core can route the reply back to the module that issued it (e.g. /// account registration, captcha verification). `status` is 0 on transport @@ -241,6 +248,18 @@ impl Ircd { }; self.server.send(uid, line); } + Event::TitleAuth { + uid, + ok, + title, + vhost, + } => { + if ok { + crate::modules::customtitle::grant(&mut self.server, uid, &title, &vhost); + } else { + crate::modules::customtitle::deny(&self.server, uid); + } + } Event::HttpResult { uid, tag, diff --git a/src/modules/customtitle.rs b/src/modules/customtitle.rs index 2dfdb16..b599df4 100644 --- a/src/modules/customtitle.rs +++ b/src/modules/customtitle.rs @@ -29,6 +29,37 @@ pub fn commands() -> Vec> { vec![Box::new(TitleCmd)] } +fn nick(s: &Server, uid: Uid) -> String { + s.users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default() +} + +/// Apply a verified title: store it, apply the vhost (unless `*`), and confirm. +pub fn grant(s: &mut Server, uid: Uid, title: &str, vhost: &str) { + if let Some(u) = s.users.get_mut(&uid) { + u.ext.set(Title(title.to_string())); + } + if vhost != "*" && !vhost.is_empty() { + s.change_host_ident(uid, None, Some(vhost)); + } + let nick = nick(s, uid); + s.send( + uid, + format!(":{} NOTICE {nick} :*** TITLE: you are now known as \"{title}\".", s.name), + ); +} + +/// Reject a TITLE attempt (bad name or password). +pub fn deny(s: &Server, uid: Uid) { + let nick = nick(s, uid); + s.send( + uid, + format!(":{} NOTICE {nick} :*** TITLE: invalid title name or password.", s.name), + ); +} + struct TitleCmd; impl Command for TitleCmd { fn name(&self) -> &'static str { @@ -39,47 +70,43 @@ impl Command for TitleCmd { } fn handle(&self, s: &mut Server, uid: Uid, params: &[String]) -> CmdResult { let (name, pass) = (params[0].clone(), params[1].clone()); - // find a matching block - let found = s.conf_all("customtitle").iter().find_map(|line| { + // find the block by name (first wins) + let block = s.conf_all("customtitle").iter().find_map(|line| { let mut it = line.split_whitespace(); let cname = it.next()?; let cpass = it.next()?; let cvhost = it.next()?; let title = it.collect::>().join(" "); - if cname == name && !title.is_empty() && password_hash::verify(cpass, &pass) { - Some((title, cvhost.to_string())) - } else { - None - } + (cname == name && !title.is_empty()) + .then(|| (cpass.to_string(), cvhost.to_string(), title)) }); - let nick = s - .users - .get(&uid) - .map(|u| u.nick.clone()) - .unwrap_or_default(); - let Some((title, vhost)) = found else { - s.send( - uid, - format!( - ":{} NOTICE {nick} :*** TITLE: invalid title name or password.", - s.name - ), - ); + let Some((cpass, cvhost, title)) = block else { + deny(s, uid); return CmdResult::Fail; }; - if let Some(u) = s.users.get_mut(&uid) { - u.ext.set(Title(title.clone())); + // a KDF title password is slow — verify it off the core thread (result comes + // back as TitleAuth) so /TITLE spam can't freeze the server. + if password_hash::is_slow(&cpass) { + let started = s.spawn_crypto(move || { + let ok = password_hash::verify(&cpass, &pass); + crate::ircd::Event::TitleAuth { + uid, + ok, + title, + vhost: cvhost, + } + }); + if !started { + deny(s, uid); + } + return CmdResult::Ok; } - if vhost != "*" && !vhost.is_empty() { - s.change_host_ident(uid, None, Some(&vhost)); + if password_hash::verify(&cpass, &pass) { + grant(s, uid, &title, &cvhost); + CmdResult::Ok + } else { + deny(s, uid); + CmdResult::Fail } - s.send( - uid, - format!( - ":{} NOTICE {nick} :*** TITLE: you are now known as \"{title}\".", - s.name - ), - ); - CmdResult::Ok } }