Add the SASL PLAIN verification exchange
fedserv now handles the SASL exchange an ircd relays over ENCAP (modes H/S/C/D): on PLAIN it answers the empty challenge, decodes the client payload, and verifies the credentials against the account store, replying D S or D F. Wiring the account login on success and advertising the mechanism list to the ircd are the remaining integration steps.
This commit is contained in:
parent
18a28ed49b
commit
63028d99e5
5 changed files with 96 additions and 1 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -29,6 +29,12 @@ dependencies = [
|
|||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
|
|
@ -107,6 +113,7 @@ version = "0.0.1"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"base64",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
argon2 = { version = "0.5", features = ["std"] }
|
||||
base64 = "0.22"
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ pub mod db;
|
|||
pub mod service;
|
||||
pub mod state;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::proto::{NetAction, NetEvent};
|
||||
use db::{Db, RegError};
|
||||
use service::{Sender, Service, ServiceCtx};
|
||||
|
|
@ -11,11 +13,12 @@ pub struct Engine {
|
|||
services: Vec<Box<dyn Service>>,
|
||||
network: Network,
|
||||
db: Db,
|
||||
sasl_sessions: HashMap<String, String>, // client uid -> in-progress mechanism
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(services: Vec<Box<dyn Service>>, db: Db) -> Self {
|
||||
Self { services, network: Network::default(), db }
|
||||
Self { services, network: Network::default(), db, sasl_sessions: HashMap::new() }
|
||||
}
|
||||
|
||||
// Sent right after the SERVER line: burst, introduce every service, endburst.
|
||||
|
|
@ -49,6 +52,41 @@ impl Engine {
|
|||
NetEvent::AccountRequest { reqid, kind, account, p2, p3, .. } => {
|
||||
self.account_request(reqid, kind, account, p2, p3)
|
||||
}
|
||||
NetEvent::Sasl { client, mode, data, .. } => self.sasl(client, mode, data),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// SASL agent side of the exchange the ircd relays to us (modes H/S/C/D).
|
||||
// PLAIN only for now: verify the credentials against the account store. NOTE:
|
||||
// this proves the password; wiring the account login (metadata) + advertising
|
||||
// the mechanism list to the ircd are the remaining integration steps.
|
||||
fn sasl(&mut self, client: String, mode: String, data: Vec<String>) -> Vec<NetAction> {
|
||||
let agent = match self.services.first() {
|
||||
Some(s) => s.uid().to_string(),
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let mk = |mode: &str, d: Vec<String>| {
|
||||
vec![NetAction::Sasl { agent: agent.clone(), client: client.clone(), mode: mode.to_string(), data: d }]
|
||||
};
|
||||
match mode.as_str() {
|
||||
"H" => Vec::new(), // host info
|
||||
"S" => match data.first().map(String::as_str) {
|
||||
Some("PLAIN") => {
|
||||
self.sasl_sessions.insert(client.clone(), "PLAIN".to_string());
|
||||
mk("C", vec!["+".to_string()]) // empty challenge -> client sends the payload
|
||||
}
|
||||
_ => mk("D", vec!["F".to_string()]), // unsupported mechanism
|
||||
},
|
||||
"C" => {
|
||||
let is_plain = self.sasl_sessions.remove(&client).as_deref() == Some("PLAIN");
|
||||
let ok = is_plain && verify_plain(&data, &self.db);
|
||||
mk("D", vec![if ok { "S".to_string() } else { "F".to_string() }])
|
||||
}
|
||||
"D" => {
|
||||
self.sasl_sessions.remove(&client);
|
||||
Vec::new()
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -92,3 +130,18 @@ impl Engine {
|
|||
ctx.actions
|
||||
}
|
||||
}
|
||||
|
||||
// Decode a SASL PLAIN payload (authzid \0 authcid \0 passwd) and verify it.
|
||||
fn verify_plain(data: &[String], db: &Db) -> bool {
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
let Some(b64) = data.first() else { return false };
|
||||
let Ok(raw) = STANDARD.decode(b64) else { return false };
|
||||
let parts: Vec<&[u8]> = raw.split(|&b| b == 0).collect();
|
||||
if parts.len() != 3 {
|
||||
return false;
|
||||
}
|
||||
match (std::str::from_utf8(parts[1]), std::str::from_utf8(parts[2])) {
|
||||
(Ok(authcid), Ok(passwd)) => db.verify(authcid, passwd),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,27 @@ impl Protocol for InspIrcd {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
// ENCAP <target> <subcmd> … — we only care about relayed SASL:
|
||||
// ENCAP <target> SASL <client> <agent> <mode> [data…]
|
||||
"ENCAP" => {
|
||||
let _target = tokens.next();
|
||||
match tokens.next().map(|s| s.to_ascii_uppercase()).as_deref() {
|
||||
Some("SASL") => {
|
||||
let p: Vec<&str> = tokens.collect();
|
||||
if p.len() >= 3 {
|
||||
vec![NetEvent::Sasl {
|
||||
client: p[0].to_string(),
|
||||
agent: p[1].to_string(),
|
||||
mode: p[2].to_string(),
|
||||
data: p[3..].iter().map(|s| s.to_string()).collect(),
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
||||
}
|
||||
}
|
||||
_ => vec![NetEvent::Unknown { line: line.to_string() }],
|
||||
}
|
||||
}
|
||||
|
|
@ -129,6 +150,15 @@ impl Protocol for InspIrcd {
|
|||
reqid, kind, account, status, code, message
|
||||
))]
|
||||
}
|
||||
// ENCAP * SASL <agent> <client> <mode> [data…]
|
||||
NetAction::Sasl { agent, client, mode, data } => {
|
||||
let mut line = format!("ENCAP * SASL {} {} {}", agent, client, mode);
|
||||
for d in data {
|
||||
line.push(' ');
|
||||
line.push_str(d);
|
||||
}
|
||||
vec![self.from_us(line)]
|
||||
}
|
||||
NetAction::Raw(s) => vec![s.clone()],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ pub enum NetEvent {
|
|||
Quit { uid: String },
|
||||
// An ircd relaying an IRCv3 account-registration request to us as the authority.
|
||||
AccountRequest { reqid: String, origin: String, kind: String, account: String, p2: String, p3: String },
|
||||
// An ircd relaying a SASL exchange step to us (the SASL agent). mode = H/S/C/D.
|
||||
Sasl { client: String, agent: String, mode: String, data: Vec<String> },
|
||||
Unknown { line: String },
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +29,8 @@ pub enum NetAction {
|
|||
Privmsg { from: String, to: String, text: String },
|
||||
Notice { from: String, to: String, text: String },
|
||||
AccountResponse { reqid: String, kind: String, account: String, status: String, code: String, message: String },
|
||||
// A SASL exchange step back to the ircd, sourced from our SASL agent. mode = C/D.
|
||||
Sasl { agent: String, client: String, mode: String, data: Vec<String> },
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue