summaryrefslogtreecommitdiff
path: root/src-repo/src/pam_client.rs
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/pam_client.rs
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/pam_client.rs')
-rw-r--r--src-repo/src/pam_client.rs166
1 files changed, 166 insertions, 0 deletions
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);
+ }
+ }
+ }
+}