diff --git a/src/coremods/core_oper.rs b/src/coremods/core_oper.rs index 6cdf0f3..99e2560 100644 --- a/src/coremods/core_oper.rs +++ b/src/coremods/core_oper.rs @@ -131,15 +131,20 @@ impl Command for Oper { s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); return CmdResult::Fail; }; - // bcrypt is slow — verify it off the core thread (result arrives as OperAuth). - if hash.starts_with("$2") { - if !s.spawn_auth(uid, hash, pass, level) { + // a KDF password (bcrypt / pbkdf2) is slow — verify it off the core thread + // (result arrives as OperAuth), so it can't freeze the server or be a DoS. + if crate::modules::password_hash::is_slow(&hash) { + let ok = s.spawn_crypto(move || { + let ok = crate::modules::password_hash::verify(&hash, &pass); + crate::ircd::Event::OperAuth { uid, ok, level } + }); + if !ok { s.numeric(uid, ERR_PASSWDMISMATCH, ":Too many auth attempts, try again"); return CmdResult::Fail; } return CmdResult::Ok; // pending; oper-up happens when the verify returns } - // fast hashes (plaintext / sha* / pbkdf2) verify inline + // fast hashes (plaintext / sha*) verify inline if crate::modules::password_hash::verify(&hash, &pass) { s.oper_up(uid); crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection diff --git a/src/ircd.rs b/src/ircd.rs index acb5bac..598bfe0 100644 --- a/src/ircd.rs +++ b/src/ircd.rs @@ -53,13 +53,19 @@ pub enum Event { uid: Uid, ident: Option, }, - /// A background OPER password verify finished. bcrypt is deliberately slow, so it - /// runs on a worker thread (see `Server::spawn_auth`) instead of freezing the core. + /// A background OPER password verify finished. KDF hashes are deliberately slow, + /// so they run on a worker thread (see `Server::spawn_crypto`), not on the core. OperAuth { uid: Uid, ok: bool, level: u32, }, + /// A background MKPASSWD hash finished (KDFs run off the core thread). + MkpasswdResult { + uid: Uid, + algo: String, + hash: Option, + }, /// 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 @@ -219,6 +225,22 @@ impl Ircd { .numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect"); } } + Event::MkpasswdResult { uid, algo, hash } => { + let nick = self + .server + .users + .get(&uid) + .map(|u| u.nick.clone()) + .unwrap_or_default(); + let line = match hash { + Some(h) => format!( + ":{} NOTICE {nick} :{algo} hashed password: {h}", + self.server.name + ), + None => format!(":{} NOTICE {nick} :Could not hash with '{algo}'", self.server.name), + }; + self.server.send(uid, line); + } Event::HttpResult { uid, tag, diff --git a/src/modules/password_hash.rs b/src/modules/password_hash.rs index 9582f59..1b0b602 100644 --- a/src/modules/password_hash.rs +++ b/src/modules/password_hash.rs @@ -129,6 +129,18 @@ fn make(algo: &str, plaintext: &str) -> Option { Some(format!("{}:{}", algo.to_ascii_lowercase(), hex(&d))) } +/// Whether *verifying* this stored credential is a deliberately-slow KDF (bcrypt or +/// pbkdf2) that should run off the core thread rather than inline. +pub fn is_slow(stored: &str) -> bool { + stored.starts_with("$2") || stored.starts_with("pbkdf2:") +} + +/// Whether *producing* a hash with this algorithm name is a slow KDF (for MKPASSWD). +pub fn is_slow_algo(algo: &str) -> bool { + let a = algo.to_ascii_lowercase(); + a == "bcrypt" || a.starts_with("bcrypt:") || a == "pbkdf2" +} + pub fn commands() -> Vec> { vec![Box::new(MkPasswd)] } @@ -152,13 +164,28 @@ impl Command for MkPasswd { ); return CmdResult::Fail; } - let (algo, pass) = (¶ms[0], ¶ms[1]); + let (algo, pass) = (params[0].clone(), params[1].clone()); let nick = s .users .get(&uid) .map(|u| u.nick.clone()) .unwrap_or_default(); - match make(algo, pass) { + // a KDF (bcrypt / pbkdf2) is slow — hash it off the core thread (result comes + // back as MkpasswdResult) so an oper's MKPASSWD can't freeze the whole server. + if is_slow_algo(&algo) { + let started = s.spawn_crypto(move || { + let hash = make(&algo, &pass); + crate::ircd::Event::MkpasswdResult { uid, algo, hash } + }); + if !started { + s.send( + uid, + format!(":{} NOTICE {nick} :Busy hashing, try again", s.name), + ); + } + return CmdResult::Ok; + } + match make(&algo, &pass) { Some(hashed) => { s.send( uid, diff --git a/src/server.rs b/src/server.rs index eca7157..39854c6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -450,24 +450,36 @@ impl Server { }); } - /// Verify an OPER password on a worker thread, delivering the result back as - /// `Event::OperAuth`. bcrypt is deliberately expensive (tens to hundreds of ms), - /// so running it inline would freeze the single-threaded core — and an OPER flood - /// against a bcrypt block would be a trivial DoS. Bounded so the flood can't spawn - /// unlimited hash threads; returns `false` when at capacity. - pub fn spawn_auth(&self, uid: Uid, hash: String, pass: String, level: u32) -> bool { + /// Run an expensive credential operation (a KDF: bcrypt / pbkdf2) on a worker + /// thread and deliver its result back as the `Event` the closure builds. These + /// hashes are deliberately slow (tens to hundreds of ms), so running one inline + /// would freeze the single-threaded core — and a flood of them (OPER, TITLE, …) + /// against a KDF credential would be a trivial DoS. Bounded so the flood can't + /// spawn unlimited threads; returns `false` when at capacity (the caller then + /// rejects the attempt). A `Drop` guard keeps the counter correct even if the + /// closure panics. + pub fn spawn_crypto(&self, f: F) -> bool + where + F: FnOnce() -> crate::ircd::Event + Send + 'static, + { use std::sync::atomic::{AtomicUsize, Ordering}; static ACTIVE: AtomicUsize = AtomicUsize::new(0); const MAX_ACTIVE: usize = 16; + struct Guard; + impl Drop for Guard { + fn drop(&mut self) { + ACTIVE.fetch_sub(1, Ordering::Relaxed); + } + } if ACTIVE.fetch_add(1, Ordering::Relaxed) >= MAX_ACTIVE { ACTIVE.fetch_sub(1, Ordering::Relaxed); return false; } let tx = self.event_tx.clone(); std::thread::spawn(move || { - let ok = crate::modules::password_hash::verify(&hash, &pass); - ACTIVE.fetch_sub(1, Ordering::Relaxed); - let _ = tx.send(crate::ircd::Event::OperAuth { uid, ok, level }); + let _guard = Guard; // decrements even on panic + let ev = f(); + let _ = tx.send(ev); }); true }