From 277f2200b763381624d1008e29a199c380f5e764 Mon Sep 17 00:00:00 2001 From: javalsai Date: Tue, 24 Feb 2026 18:05:44 +0100 Subject: [PATCH] intial version --- .gitignore | 1 + Cargo.lock | 405 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 +++ LICENSE | 11 ++ README.md | 17 +++ src/args.rs | 68 +++++++++ src/lib.rs | 126 ++++++++++++++++ src/main.rs | 76 ++++++++++ 8 files changed, 720 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/args.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..69e09c6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,405 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pam" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba" +dependencies = [ + "libc", + "memchr", + "pam-macros", + "pam-sys", + "users", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b" +dependencies = [ + "bindgen", + "libc", +] + +[[package]] +name = "pamsock" +version = "0.1.0" +dependencies = [ + "anstyle", + "clap", + "pam", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..31fe3ff --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "pamsock" +version = "0.1.0" +edition = "2024" + +[dependencies] +anstyle = "1.0.13" +clap = { version = "4.5.60", features = ["derive"] } +pam = "0.8.0" + +[lints.clippy] +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +unwrap_used = "deny" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a3094a --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..402689c --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# Pamsock + +Exposes PAM authentication via a unix socket. + +It's meanth to be secure enough to be mindlessly executed as root in its daemon form, because PAM requies root to interact with it. Code is simple enough for that reason, an audit should be really fast and dependency use is minimal for convenient stuff. + +# Building + +Requires `PAM_SERVICE` env variable at compile time (I recommend `login` for average distributions) to specify the name of the PAM service to login as. + +# Docs + +The protocol is dead simple and is documented with `cargo doc` under `lib::prot`. Also provides some utils to interact as a client with with library part of this project. + +# License + +Code licensed under the WTFPL. diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..b8fda22 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,68 @@ +use std::ffi::OsString; + +use clap::{Parser, Subcommand}; + +/// PAM though a Unix Socket +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +#[command(styles = get_clap_styles())] +pub struct Args { + #[arg(short, default_value = "pam")] + pub sock_abstract_name: OsString, + + #[command(subcommand)] + pub cmd: Option, +} + +#[derive(Subcommand, Debug)] +pub enum Subcommands { + /// Attempts to login using the sock, note that the use of this is unsecure as any process can + /// find the password from the process list. + Login { + /// User to attempt login as + #[arg()] + user: String, + + /// Password to attempt login with + #[arg()] + password: String, + }, +} + +fn get_clap_styles() -> clap::builder::Styles { + clap::builder::Styles::styled() + .usage( + anstyle::Style::new() + .bold() + .underline() + .fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(208)))), + ) + .header( + anstyle::Style::new() + .bold() + .underline() + .fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(208)))), + ) + .literal( + anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))), + ) + .invalid( + anstyle::Style::new() + .bold() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))), + ) + .error( + anstyle::Style::new() + .bold() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))), + ) + .valid( + anstyle::Style::new() + .bold() + .underline() + .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))), + ) + .placeholder( + anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))), + ) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7eebbb7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,126 @@ +pub mod prot { + //! # Protocol + //! + //! The authentication protocol is dead simple for now, it is a client-server model in which + //! both parts send/receive data in the same format. + //! + //! ## Communication + //! + //! It simply works by expecting a set of data, and if, anything is unexpected, the communication aborts. + //! + //! ### Data De/Serialization + //! + //! Integer primitives are sent as bytes in Big Endian order. + //! + //! Strings are sent in 2 steps, first it sends the byte lenth. The byte length is flexible and + //! depends on the protocol, is sent first, then, that numbers of bytes is expected, and to be + //! in utf-8 format. + //! + //! The byte lenth type associated still be annotated by `String` where N is the data type + //! that holds the length. + //! + //! ### Protocol + //! + //! The communication lacks versioning in the current state, as soon as the connection opens, + //! the client sends the user as a `String` and the password as a `String`. Numbers are + //! this small because higher users or passwords are rare or even impossible and this prevent + //! memory allocation abuse. + //! + //! Then the server responds with a single [`u8`], the given value is then mapped to the enum + //! [`ServerResponse`]. + + /// Represents the status after a login attempt + #[repr(u8)] + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub enum ServerResponse { + /// Something went wrong in the server, e.g. the server failed pam client initialization + ServerError = 0, + + /// The user is locked due to too many failed locin attempts. One must not rely on these, + /// it's common for lockers to not report the lock status and just report as a [`Failed`] + /// state. + /// + /// [`Failed`]: #variant.Failed + Locked = 1, + + /// The authentication likely went through but failed + Failed = 2, + + /// The authentication succeeded + Succeeded = 3, + } + + impl ServerResponse { + #[must_use] + pub const fn as_u8(self) -> u8 { + self as u8 + } + + #[must_use] + pub const fn from_u8(b: u8) -> Option { + match b { + 0 => Some(Self::ServerError), + 1 => Some(Self::Locked), + 2 => Some(Self::Failed), + 3 => Some(Self::Succeeded), + _ => None, + } + } + } + + #[must_use] + pub fn attempt_login( + mut stream: std::os::unix::net::UnixStream, + user: &str, + passwd: &str, + ) -> Option { + ucom::write_str(&mut stream, user)?; + ucom::write_str(&mut stream, passwd)?; + + ServerResponse::from_u8(ucom::read_u8(&mut stream)?) + } + + pub mod ucom { + //! # µcom + //! + //! Small module to serialize basic primitives in a very basic form. + + use std::io::{Read, Write}; + + pub fn read_duple(from: &mut impl Read) -> Option<(String, String)> { + let user = read_str(from)?; + let pass = read_str(from)?; + Some((user, pass)) + } + + pub fn read_str(from: &mut impl Read) -> Option { + let size = read_u8(from)? as usize; + let mut buf = vec![0; size]; + from.read_exact(&mut buf).ok()?; + String::from_utf8(buf).ok() + } + + pub fn read_u8(from: &mut impl Read) -> Option { + let mut size_buf = [0; 1]; + from.read_exact(&mut size_buf).ok()?; + Some(u8::from_be_bytes(size_buf)) + } + + pub fn write_duple(into: &mut impl Write, user: &str, pass: &str) -> Option<()> { + write_str(into, user)?; + write_str(into, pass)?; + Some(()) + } + + pub fn write_str(into: &mut impl Write, str: &str) -> Option<()> { + let buf = str.as_bytes(); + write_u8(into, buf.len().try_into().ok()?)?; + into.write_all(buf).ok() + } + + pub fn write_u8(into: &mut impl Write, u8: u8) -> Option<()> { + let size_buf = u8.to_be_bytes(); + into.write_all(&size_buf).ok() + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..23c17c5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,76 @@ +use std::{ + os::{ + linux::net::SocketAddrExt, + unix::{ + ffi::OsStrExt, + net::{SocketAddr, UnixListener, UnixStream}, + }, + }, + thread, +}; + +use clap::Parser; + +use pamsock::prot::{self, ServerResponse}; + +pub mod args; + +const SERVICE_NAME: &str = env!("PAM_SERVICE"); + +fn conn(mut stream: UnixStream) -> Option<()> { + fn authenticate(user: String, passwd: String) -> ServerResponse { + let Ok(mut auth) = pam::Client::with_password(SERVICE_NAME) else { + return ServerResponse::ServerError; + }; + + auth.conversation_mut().set_credentials(user, passwd); + match auth.authenticate() { + Ok(()) => ServerResponse::Succeeded, + // TODO: check that [`ServerResponse::Locked`] works at lasst on my machine and is no + // other err core (it doesnt) + Err(pam::PamError(pam::PamReturnCode::MaxTries)) => ServerResponse::Locked, + Err(_) => ServerResponse::Failed, + } + } + + let (user, passwd) = prot::ucom::read_duple(&mut stream)?; + let res = authenticate(user, passwd); + prot::ucom::write_u8(&mut stream, res.as_u8()); + Some(()) +} + +#[link(name = "c")] +unsafe extern "C" { + #[must_use] + safe fn geteuid() -> u32; +} + +fn main() -> std::io::Result<()> { + let args = args::Args::parse(); + + match args.cmd { + Some(args::Subcommands::Login { user, password }) => { + let addr = SocketAddr::from_abstract_name(args.sock_abstract_name.as_bytes())?; + let sock = UnixStream::connect_addr(&addr)?; + + println!("{:?}", prot::attempt_login(sock, &user, &password)); + } + None => { + if geteuid() != 0 { + eprintln!("\x1b[1;31mmust run as root\x1b[0m"); + return Ok(()); + } + + let addr = SocketAddr::from_abstract_name(args.sock_abstract_name.as_bytes())?; + let listener = UnixListener::bind_addr(&addr)?; + + for stream in listener.incoming() { + if let Ok(stream) = stream { + thread::spawn(|| conn(stream)); + } + } + } + } + + Ok(()) +}