summaryrefslogtreecommitdiff
path: root/src-repo/src
diff options
context:
space:
mode:
authorDennis Kobert <dennis@kobert.dev>2026-03-11 14:15:45 +0100
committerDennis Kobert <dennis@kobert.dev>2026-03-11 14:16:07 +0100
commita9a74679fbec07cf158f2295513e3d4c4adc2666 (patch)
tree5dcac28283ef53c2869bdedb9badff0896900bb6 /src-repo/src
parent116bd5a46adc7e9caa27f58b19cafe68d3e085f0 (diff)
Port pam-any from pam-bindings to nonstick crateHEADmain
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.rs121
-rw-r--r--src-repo/src/mode.rs7
-rw-r--r--src-repo/src/pam_any_conversation.rs48
-rw-r--r--src-repo/src/pam_client.rs166
-rw-r--r--src-repo/src/raw_conv.rs116
-rw-r--r--src-repo/src/un_hide_input.rs9
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