From dfbe8e8c4aa207b9e297c1d31e33709c3bfc245c Mon Sep 17 00:00:00 2001 From: reverse Date: Sun, 30 Aug 2026 04:06:06 +0000 Subject: [PATCH] sasl: offer and relay SCRAM-SHA-256 (advertised on plaintext too) --- README.md | 3 ++- docs/ircv3.md | 2 +- src/coremods/core_user.rs | 17 +++++++++++++---- src/users.rs | 7 +++++-- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a3be075..1f07b63 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ operational limit exposed as a config key. - **IRCv3** — message-tags (+msgid), server-time, labeled-response, batch, echo-message, account-tag, **CHATHISTORY** + **event-playback**, **multiline**, **message-redaction**, **read-marker**, **relaymsg**, **web-push** (VAPID / RFC - 8291), SASL, standard-replies, and `WATCH`/`MONITOR`/`SILENCE`/caller-id. + 8291), SASL (PLAIN/EXTERNAL/SCRAM-SHA-256), standard-replies, and + `WATCH`/`MONITOR`/`SILENCE`/caller-id. - **Operators** — `OPER`/`KILL`/`WALLOPS`/`GLOBOPS`, the `SA*`/`CHG*`/`SET*` override toolbox, x-lines (`K`/`G`/`Z`/`E`/`SHUN`/`QLINE`/`CBAN`/`RLINE`) persisted to disk, a **type/class privilege model** (per-type commands, named diff --git a/docs/ircv3.md b/docs/ircv3.md index 73081e0..fa108f1 100644 --- a/docs/ircv3.md +++ b/docs/ircv3.md @@ -37,7 +37,7 @@ informed as they change. This page groups what's supported. | Capability | What it adds | |------------|--------------| -| `sasl` | `AUTHENTICATE` with **PLAIN** or **EXTERNAL** (client-cert / CertFP), relayed to the services server. See [linking](linking.md). | +| `sasl` | `AUTHENTICATE` with **PLAIN**, **SCRAM-SHA-256** (challenge-response — no password on the wire, offered on plaintext too), or **EXTERNAL** (client-cert / CertFP), relayed to the services server. See [linking](linking.md). | | `draft/account-registration` | Create and confirm an account in-band with `REGISTER` / `VERIFY`. | ## History & messaging diff --git a/src/coremods/core_user.rs b/src/coremods/core_user.rs index 9d7471e..78cefb7 100644 --- a/src/coremods/core_user.rs +++ b/src/coremods/core_user.rs @@ -333,7 +333,9 @@ impl Command for Authenticate { if arg == "*" { s.numeric(uid, ERR_SASLABORTED, ":SASL authentication aborted"); CmdResult::Ok - } else if arg.eq_ignore_ascii_case("PLAIN") { + } else if arg.eq_ignore_ascii_case("PLAIN") + || arg.eq_ignore_ascii_case("SCRAM-SHA-256") + { if !have_services { s.numeric( uid, @@ -342,12 +344,19 @@ impl Command for Authenticate { ); return CmdResult::Fail; } + // SCRAM is challenge-response, so the password never crosses the wire — + // it's fine to offer over plaintext too. The rounds relay mech-agnostically. + let mech = if arg.eq_ignore_ascii_case("PLAIN") { + "PLAIN" + } else { + "SCRAM-SHA-256" + }; if let Some(u) = s.users.get_mut(&uid) { - u.sasl_mech = Some("PLAIN".to_string()); + u.sasl_mech = Some(mech.to_string()); } // start the exchange at services; its `C` challenge is relayed // back to the client as the `AUTHENTICATE +` prompt - s.sasl_relay(uid, "S PLAIN"); + s.sasl_relay(uid, &format!("S {mech}")); CmdResult::Ok } else if arg.eq_ignore_ascii_case("EXTERNAL") { // CertFP: only works on TLS with a client cert; the fingerprint @@ -373,7 +382,7 @@ impl Command for Authenticate { } } } else { - s.numeric(uid, RPL_SASLMECHS, "PLAIN :are available SASL mechanisms"); + s.numeric(uid, RPL_SASLMECHS, "PLAIN,SCRAM-SHA-256 :are available SASL mechanisms"); s.numeric(uid, ERR_SASLFAIL, ":Unsupported SASL mechanism"); CmdResult::Fail } diff --git a/src/users.rs b/src/users.rs index 18e18f9..0b1f462 100644 --- a/src/users.rs +++ b/src/users.rs @@ -194,9 +194,9 @@ impl Caps { .map(|c| { if *c == "sasl" && cap302 { if secure { - "sasl=PLAIN,EXTERNAL".to_string() + "sasl=PLAIN,EXTERNAL,SCRAM-SHA-256".to_string() } else { - "sasl=PLAIN".to_string() + "sasl=PLAIN,SCRAM-SHA-256".to_string() } } else if *c == "draft/multiline" && cap302 { format!("draft/multiline=max-bytes={mline_bytes},max-lines={mline_lines}") @@ -594,6 +594,9 @@ mod tests { assert!(Caps::ls_line(true, false, "", 4096, 24).contains("sasl=PLAIN")); // 302 shows mechs assert!(!Caps::ls_line(true, false, "", 4096, 24).contains("EXTERNAL")); // plaintext: no EXTERNAL assert!(Caps::ls_line(true, true, "", 4096, 24).contains("sasl=PLAIN,EXTERNAL")); // TLS offers it + // SCRAM-SHA-256 is offered on both transports (challenge-response, no wire password) + assert!(Caps::ls_line(true, false, "", 4096, 24).contains("SCRAM-SHA-256")); + assert!(Caps::ls_line(true, true, "", 4096, 24).contains("SCRAM-SHA-256")); assert!( Caps::ls_line(false, false, "", 4096, 24).contains("sasl") && !Caps::ls_line(false, false, "", 4096, 24).contains("sasl=")