core: generic spawn_crypto helper; offload all slow KDF hashing (OPER pbkdf2 too, and MKPASSWD) off the core thread
This commit is contained in:
parent
e246699fef
commit
7cb58586b4
4 changed files with 83 additions and 17 deletions
|
|
@ -131,15 +131,20 @@ impl Command for Oper {
|
||||||
s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");
|
s.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
};
|
};
|
||||||
// bcrypt is slow — verify it off the core thread (result arrives as OperAuth).
|
// a KDF password (bcrypt / pbkdf2) is slow — verify it off the core thread
|
||||||
if hash.starts_with("$2") {
|
// (result arrives as OperAuth), so it can't freeze the server or be a DoS.
|
||||||
if !s.spawn_auth(uid, hash, pass, level) {
|
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");
|
s.numeric(uid, ERR_PASSWDMISMATCH, ":Too many auth attempts, try again");
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
return CmdResult::Ok; // pending; oper-up happens when the verify returns
|
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) {
|
if crate::modules::password_hash::verify(&hash, &pass) {
|
||||||
s.oper_up(uid);
|
s.oper_up(uid);
|
||||||
crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection
|
crate::modules::operlevels::set(s, uid, level); // operlevels: KILL protection
|
||||||
|
|
|
||||||
26
src/ircd.rs
26
src/ircd.rs
|
|
@ -53,13 +53,19 @@ pub enum Event {
|
||||||
uid: Uid,
|
uid: Uid,
|
||||||
ident: Option<String>,
|
ident: Option<String>,
|
||||||
},
|
},
|
||||||
/// A background OPER password verify finished. bcrypt is deliberately slow, so it
|
/// A background OPER password verify finished. KDF hashes are deliberately slow,
|
||||||
/// runs on a worker thread (see `Server::spawn_auth`) instead of freezing the core.
|
/// so they run on a worker thread (see `Server::spawn_crypto`), not on the core.
|
||||||
OperAuth {
|
OperAuth {
|
||||||
uid: Uid,
|
uid: Uid,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
level: u32,
|
level: u32,
|
||||||
},
|
},
|
||||||
|
/// A background MKPASSWD hash finished (KDFs run off the core thread).
|
||||||
|
MkpasswdResult {
|
||||||
|
uid: Uid,
|
||||||
|
algo: String,
|
||||||
|
hash: Option<String>,
|
||||||
|
},
|
||||||
/// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
|
/// A module's async HTTP request finished. `tag` is `"<module>:<detail>"`
|
||||||
/// so the core can route the reply back to the module that issued it (e.g.
|
/// 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
|
/// account registration, captcha verification). `status` is 0 on transport
|
||||||
|
|
@ -219,6 +225,22 @@ impl Ircd {
|
||||||
.numeric(uid, ERR_PASSWDMISMATCH, ":Password incorrect");
|
.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 {
|
Event::HttpResult {
|
||||||
uid,
|
uid,
|
||||||
tag,
|
tag,
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,18 @@ fn make(algo: &str, plaintext: &str) -> Option<String> {
|
||||||
Some(format!("{}:{}", algo.to_ascii_lowercase(), hex(&d)))
|
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<Box<dyn Command>> {
|
pub fn commands() -> Vec<Box<dyn Command>> {
|
||||||
vec![Box::new(MkPasswd)]
|
vec![Box::new(MkPasswd)]
|
||||||
}
|
}
|
||||||
|
|
@ -152,13 +164,28 @@ impl Command for MkPasswd {
|
||||||
);
|
);
|
||||||
return CmdResult::Fail;
|
return CmdResult::Fail;
|
||||||
}
|
}
|
||||||
let (algo, pass) = (¶ms[0], ¶ms[1]);
|
let (algo, pass) = (params[0].clone(), params[1].clone());
|
||||||
let nick = s
|
let nick = s
|
||||||
.users
|
.users
|
||||||
.get(&uid)
|
.get(&uid)
|
||||||
.map(|u| u.nick.clone())
|
.map(|u| u.nick.clone())
|
||||||
.unwrap_or_default();
|
.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) => {
|
Some(hashed) => {
|
||||||
s.send(
|
s.send(
|
||||||
uid,
|
uid,
|
||||||
|
|
|
||||||
|
|
@ -450,24 +450,36 @@ impl Server {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify an OPER password on a worker thread, delivering the result back as
|
/// Run an expensive credential operation (a KDF: bcrypt / pbkdf2) on a worker
|
||||||
/// `Event::OperAuth`. bcrypt is deliberately expensive (tens to hundreds of ms),
|
/// thread and deliver its result back as the `Event` the closure builds. These
|
||||||
/// so running it inline would freeze the single-threaded core — and an OPER flood
|
/// hashes are deliberately slow (tens to hundreds of ms), so running one inline
|
||||||
/// against a bcrypt block would be a trivial DoS. Bounded so the flood can't spawn
|
/// would freeze the single-threaded core — and a flood of them (OPER, TITLE, …)
|
||||||
/// unlimited hash threads; returns `false` when at capacity.
|
/// against a KDF credential would be a trivial DoS. Bounded so the flood can't
|
||||||
pub fn spawn_auth(&self, uid: Uid, hash: String, pass: String, level: u32) -> bool {
|
/// 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<F>(&self, f: F) -> bool
|
||||||
|
where
|
||||||
|
F: FnOnce() -> crate::ircd::Event + Send + 'static,
|
||||||
|
{
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
static ACTIVE: AtomicUsize = AtomicUsize::new(0);
|
static ACTIVE: AtomicUsize = AtomicUsize::new(0);
|
||||||
const MAX_ACTIVE: usize = 16;
|
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 {
|
if ACTIVE.fetch_add(1, Ordering::Relaxed) >= MAX_ACTIVE {
|
||||||
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let tx = self.event_tx.clone();
|
let tx = self.event_tx.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let ok = crate::modules::password_hash::verify(&hash, &pass);
|
let _guard = Guard; // decrements even on panic
|
||||||
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
let ev = f();
|
||||||
let _ = tx.send(crate::ircd::Event::OperAuth { uid, ok, level });
|
let _ = tx.send(ev);
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue