diff options
| author | Dennis Kobert <dennis@kobert.dev> | 2026-03-11 14:15:45 +0100 |
|---|---|---|
| committer | Dennis Kobert <dennis@kobert.dev> | 2026-03-11 14:16:07 +0100 |
| commit | a9a74679fbec07cf158f2295513e3d4c4adc2666 (patch) | |
| tree | 5dcac28283ef53c2869bdedb9badff0896900bb6 /src-repo/src | |
| parent | 116bd5a46adc7e9caa27f58b19cafe68d3e085f0 (diff) | |
The pam-bindings crate has a critical bug where pam_try! returns
PAM_SUCCESS on error in release builds (anowell/pam-rs#16), causing
authentication to always succeed. This replaces it with nonstick
and a custom pam_client module with raw PAM FFI for thread-safe
conversation forwarding.
Diffstat (limited to 'src-repo/src')
| -rw-r--r-- | src-repo/src/lib.rs | 121 | ||||
| -rw-r--r-- | src-repo/src/mode.rs | 7 | ||||
| -rw-r--r-- | src-repo/src/pam_any_conversation.rs | 48 | ||||
| -rw-r--r-- | src-repo/src/pam_client.rs | 166 | ||||
| -rw-r--r-- | src-repo/src/raw_conv.rs | 116 | ||||
| -rw-r--r-- | src-repo/src/un_hide_input.rs | 9 |
6 files changed, 467 insertions, 0 deletions
diff --git a/src-repo/src/lib.rs b/src-repo/src/lib.rs new file mode 100644 index 0000000..a32eb5e --- /dev/null +++ b/src-repo/src/lib.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; +use std::ffi::CStr; +use std::sync::mpsc::channel; +use std::sync::{Arc, Mutex}; +use std::thread; + +use nonstick::handle::PamHandleModule; +use nonstick::{pam_hooks, ErrorCode, Flags, PamModule, Result as PamResult}; + +use crate::pam_client::{Client, PamResult as PamClientResult}; +use serde::{Deserialize, Serialize}; + +use crate::mode::Mode; +use crate::pam_any_conversation::PamAnyConversation; +use crate::raw_conv::RawConv; +use crate::un_hide_input::un_hide_input; + +mod mode; +mod pam_any_conversation; +mod pam_client; +mod raw_conv; +mod un_hide_input; + +struct PamAny; +pam_hooks!(PamAny); + +#[derive(Serialize, Deserialize, Debug)] +struct Input { + mode: Mode, + modules: HashMap<String, String>, +} + +impl<T: PamHandleModule> PamModule<T> for PamAny { + fn authenticate(handle: &mut T, args: Vec<&CStr>, _flags: Flags) -> PamResult<()> { + let arg_string = args + .iter() + .map(|s| s.to_str().unwrap_or("")) + .collect::<Vec<_>>() + .join(" "); + + // Support reading config from a file path (for NixOS compatibility) + let config_string = if arg_string.starts_with('/') { + let path = arg_string.split_whitespace().next().unwrap_or(""); + std::fs::read_to_string(path).map_err(|_| ErrorCode::ServiceError)? + } else { + arg_string + }; + + let input: Input = + serde_json::from_str(&config_string).map_err(|_| ErrorCode::ServiceError)?; + + let user = handle.get_user(None)?.to_owned(); + + // Get the raw PAM conversation to share across threads. + let handle_ptr = handle as *mut T as *mut libc::c_void; + let conv = RawConv::from_pam_handle(handle_ptr) + .ok_or(ErrorCode::ConversationError)?; + let conv = Arc::new(Mutex::new(conv)); + + let (tx, rx) = channel::<PamClientResult<()>>(); + let _handles: Vec<_> = input + .modules + .iter() + .map(|(service, service_display_name)| { + let service = service.to_owned(); + let tx = tx.clone(); + let conv = conv.clone(); + let user = user.clone(); + let service_display_name = service_display_name.to_owned(); + thread::spawn(move || { + let client = Client::with_conversation( + &service, + PamAnyConversation { + service_display_name, + user, + conv, + }, + ); + let result = match client { + Ok(mut c) => c.authenticate(), + Err(e) => Err(e), + }; + let _ = tx.send(result); + }) + }) + .collect(); + + match input.mode { + Mode::One => { + let mut failed_modules = 0; + for result in rx { + if result.is_ok() { + let _ = un_hide_input(); + return Ok(()); + } else { + failed_modules += 1; + if failed_modules == input.modules.len() { + return Err(ErrorCode::AuthenticationError); + } + } + } + Err(ErrorCode::AuthenticationError) + } + Mode::All => { + let mut successful_modules = 0; + for result in rx { + if result.is_ok() { + successful_modules += 1; + if successful_modules == input.modules.len() { + let _ = un_hide_input(); + return Ok(()); + } + } else { + return Err(ErrorCode::AuthenticationError); + } + } + Err(ErrorCode::AuthenticationError) + } + } + } +} diff --git a/src-repo/src/mode.rs b/src-repo/src/mode.rs new file mode 100644 index 0000000..8dbfb44 --- /dev/null +++ b/src-repo/src/mode.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum Mode { + One, + All +} diff --git a/src-repo/src/pam_any_conversation.rs b/src-repo/src/pam_any_conversation.rs new file mode 100644 index 0000000..825cb6a --- /dev/null +++ b/src-repo/src/pam_any_conversation.rs @@ -0,0 +1,48 @@ +use std::ffi::{CStr, CString}; +use std::sync::{Arc, Mutex}; +use std::thread; + +use crate::pam_client::Conversation; + +use crate::raw_conv::RawConv; + +pub struct PamAnyConversation { + pub service_display_name: String, + pub user: String, + pub conv: Arc<Mutex<RawConv>>, +} + +impl Conversation for PamAnyConversation { + fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> { + CString::new(self.user.as_str()).map_err(|_| ()) + } + + fn prompt_blind(&mut self, msg: &CStr) -> Result<CString, ()> { + let msg = msg.to_str().map_err(|_| ())?; + let conv = self.conv.lock().map_err(|_| ())?; + let response = conv.send_prompt(&format!("[{}] {}", self.service_display_name, msg))?; + response.ok_or(()) + } + + fn info(&mut self, msg: &CStr) { + let msg = msg.to_str().unwrap_or(""); + let msg = format!("[{}] {}", self.service_display_name, msg); + let conv = self.conv.clone(); + thread::spawn(move || { + if let Ok(conv) = conv.lock() { + let _ = conv.send_info(&msg); + } + }); + } + + fn error(&mut self, msg: &CStr) { + let msg = msg.to_str().unwrap_or(""); + let msg = format!("[{}] {}", self.service_display_name, msg); + let conv = self.conv.clone(); + thread::spawn(move || { + if let Ok(conv) = conv.lock() { + let _ = conv.send_error(&msg); + } + }); + } +} diff --git a/src-repo/src/pam_client.rs b/src-repo/src/pam_client.rs new file mode 100644 index 0000000..e0fbcc6 --- /dev/null +++ b/src-repo/src/pam_client.rs @@ -0,0 +1,166 @@ +use std::ffi::{CStr, CString}; +use std::ptr; + +const PAM_PROMPT_ECHO_OFF: libc::c_int = 1; +const PAM_PROMPT_ECHO_ON: libc::c_int = 2; +const PAM_ERROR_MSG: libc::c_int = 3; +const PAM_TEXT_INFO: libc::c_int = 4; +const PAM_SUCCESS: libc::c_int = 0; + +#[repr(C)] +struct PamMessage { + msg_style: libc::c_int, + msg: *const libc::c_char, +} + +#[repr(C)] +struct PamResponse { + resp: *mut libc::c_char, + resp_retcode: libc::c_int, +} + +type PamConvFn = unsafe extern "C" fn( + num_msg: libc::c_int, + msg: *mut *const PamMessage, + resp: *mut *mut PamResponse, + appdata_ptr: *mut libc::c_void, +) -> libc::c_int; + +#[repr(C)] +struct PamConv { + conv: PamConvFn, + appdata_ptr: *mut libc::c_void, +} + +type PamHandle = libc::c_void; + +extern "C" { + fn pam_start( + service_name: *const libc::c_char, + user: *const libc::c_char, + pam_conversation: *const PamConv, + pamh: *mut *mut PamHandle, + ) -> libc::c_int; + fn pam_authenticate(pamh: *mut PamHandle, flags: libc::c_int) -> libc::c_int; + fn pam_end(pamh: *mut PamHandle, pam_status: libc::c_int) -> libc::c_int; +} + +pub trait Conversation { + fn prompt_echo(&mut self, msg: &CStr) -> Result<CString, ()>; + fn prompt_blind(&mut self, msg: &CStr) -> Result<CString, ()>; + fn info(&mut self, msg: &CStr); + fn error(&mut self, msg: &CStr); +} + +pub struct Client<C: Conversation> { + handle: *mut PamHandle, + // Box the conversation so the pointer in PamConv remains stable. + conversation: Box<C>, +} + +unsafe extern "C" fn converse<C: Conversation>( + num_msg: libc::c_int, + msg: *mut *const PamMessage, + resp: *mut *mut PamResponse, + appdata_ptr: *mut libc::c_void, +) -> libc::c_int { + let conv = &mut *(appdata_ptr as *mut C); + let count = num_msg as usize; + + let responses = libc::calloc(count, std::mem::size_of::<PamResponse>()) as *mut PamResponse; + if responses.is_null() { + return libc::ENOMEM; + } + + for i in 0..count { + let message = &*(*msg.add(i)); + let c_msg = CStr::from_ptr(message.msg); + match message.msg_style { + PAM_PROMPT_ECHO_ON => match conv.prompt_echo(c_msg) { + Ok(s) => (*responses.add(i)).resp = libc::strdup(s.as_ptr()), + Err(_) => { + free_responses(responses, count); + return PAM_SUCCESS + 1; + } + }, + PAM_PROMPT_ECHO_OFF => match conv.prompt_blind(c_msg) { + Ok(s) => (*responses.add(i)).resp = libc::strdup(s.as_ptr()), + Err(_) => { + free_responses(responses, count); + return PAM_SUCCESS + 1; + } + }, + PAM_TEXT_INFO => conv.info(c_msg), + PAM_ERROR_MSG => conv.error(c_msg), + _ => {} + } + } + + *resp = responses; + PAM_SUCCESS +} + +unsafe fn free_responses(resp: *mut PamResponse, count: usize) { + for i in 0..count { + let r = &mut *resp.add(i); + if !r.resp.is_null() { + libc::free(r.resp as *mut libc::c_void); + } + } + libc::free(resp as *mut libc::c_void); +} + +#[derive(Debug)] +pub struct PamError(pub libc::c_int); + +pub type PamResult<T> = Result<T, PamError>; + +impl<C: Conversation> Client<C> { + pub fn with_conversation(service: &str, conversation: C) -> PamResult<Self> { + let mut conversation = Box::new(conversation); + let c_service = CString::new(service).map_err(|_| PamError(1))?; + + let pam_conv = PamConv { + conv: converse::<C>, + appdata_ptr: &mut *conversation as *mut C as *mut libc::c_void, + }; + + let mut handle: *mut PamHandle = ptr::null_mut(); + let ret = unsafe { + pam_start( + c_service.as_ptr(), + ptr::null(), + &pam_conv, + &mut handle, + ) + }; + + if ret != PAM_SUCCESS { + return Err(PamError(ret)); + } + + Ok(Client { + handle, + conversation, + }) + } + + pub fn authenticate(&mut self) -> PamResult<()> { + let ret = unsafe { pam_authenticate(self.handle, 0) }; + if ret == PAM_SUCCESS { + Ok(()) + } else { + Err(PamError(ret)) + } + } +} + +impl<C: Conversation> Drop for Client<C> { + fn drop(&mut self) { + if !self.handle.is_null() { + unsafe { + pam_end(self.handle, PAM_SUCCESS); + } + } + } +} diff --git a/src-repo/src/raw_conv.rs b/src-repo/src/raw_conv.rs new file mode 100644 index 0000000..fe93b64 --- /dev/null +++ b/src-repo/src/raw_conv.rs @@ -0,0 +1,116 @@ +use std::ffi::{CStr, CString}; +use std::ptr; + +const PAM_CONV: libc::c_int = 5; +const PAM_PROMPT_ECHO_OFF: libc::c_int = 1; +const PAM_PROMPT_ECHO_ON: libc::c_int = 2; +const PAM_ERROR_MSG: libc::c_int = 3; +const PAM_TEXT_INFO: libc::c_int = 4; + +#[repr(C)] +struct PamMessage { + msg_style: libc::c_int, + msg: *const libc::c_char, +} + +#[repr(C)] +struct PamResponse { + resp: *mut libc::c_char, + resp_retcode: libc::c_int, +} + +type PamConvFn = unsafe extern "C" fn( + num_msg: libc::c_int, + msg: *mut *const PamMessage, + resp: *mut *mut PamResponse, + appdata_ptr: *mut libc::c_void, +) -> libc::c_int; + +#[repr(C)] +struct PamConv { + conv: PamConvFn, + appdata_ptr: *mut libc::c_void, +} + +extern "C" { + fn pam_get_item( + pamh: *const libc::c_void, + item_type: libc::c_int, + item: *mut *const libc::c_void, + ) -> libc::c_int; +} + +/// Thread-safe wrapper around a raw PAM conversation function pointer. +pub struct RawConv { + conv_fn: PamConvFn, + appdata_ptr: *mut libc::c_void, +} + +unsafe impl Send for RawConv {} + +impl RawConv { + /// Extract the raw PAM conversation from a PAM handle. + pub fn from_pam_handle(pamh: *mut libc::c_void) -> Option<Self> { + unsafe { + let mut conv_ptr: *const libc::c_void = ptr::null(); + let ret = pam_get_item(pamh, PAM_CONV, &mut conv_ptr); + if ret != 0 || conv_ptr.is_null() { + return None; + } + let pam_conv = &*(conv_ptr as *const PamConv); + Some(RawConv { + conv_fn: pam_conv.conv, + appdata_ptr: pam_conv.appdata_ptr, + }) + } + } + + /// Send a message through the PAM conversation. + /// Returns the response string for prompt types, or None for info/error. + pub fn send(&self, msg_style: libc::c_int, msg: &str) -> Result<Option<CString>, ()> { + let c_msg = CString::new(msg).map_err(|_| ())?; + let pam_msg = PamMessage { + msg_style, + msg: c_msg.as_ptr(), + }; + let mut msg_ptr: *const PamMessage = &pam_msg; + let mut resp_ptr: *mut PamResponse = ptr::null_mut(); + + let ret = unsafe { (self.conv_fn)(1, &mut msg_ptr, &mut resp_ptr, self.appdata_ptr) }; + + if ret != 0 { + return Err(()); + } + + if resp_ptr.is_null() { + return Ok(None); + } + + let response = unsafe { + let resp = &*resp_ptr; + let result = if resp.resp.is_null() { + None + } else { + let s = CStr::from_ptr(resp.resp).to_owned(); + libc::free(resp.resp as *mut libc::c_void); + Some(s) + }; + libc::free(resp_ptr as *mut libc::c_void); + result + }; + + Ok(response) + } + + pub fn send_prompt(&self, msg: &str) -> Result<Option<CString>, ()> { + self.send(PAM_PROMPT_ECHO_OFF, msg) + } + + pub fn send_info(&self, msg: &str) -> Result<Option<CString>, ()> { + self.send(PAM_TEXT_INFO, msg) + } + + pub fn send_error(&self, msg: &str) -> Result<Option<CString>, ()> { + self.send(PAM_ERROR_MSG, msg) + } +} diff --git a/src-repo/src/un_hide_input.rs b/src-repo/src/un_hide_input.rs new file mode 100644 index 0000000..6350739 --- /dev/null +++ b/src-repo/src/un_hide_input.rs @@ -0,0 +1,9 @@ +use std::io; +use termios::{ECHO, ICANON, TCSANOW, tcsetattr, Termios}; + +pub fn un_hide_input() -> io::Result<()> { + let mut termios = Termios::from_fd(libc::STDIN_FILENO)?; + termios.c_lflag |= ECHO | ICANON; + tcsetattr(libc::STDIN_FILENO, TCSANOW, &termios)?; + Ok(()) +}
\ No newline at end of file |
