// ABOUTME: PAM authentication via raw FFI wrapper around libpam.so. // ABOUTME: Provides authenticate(username, password) for the lockscreen with secure password wiping. use std::ffi::CString; use std::ptr; use zeroize::Zeroizing; // PAM return codes const PAM_SUCCESS: i32 = 0; const PAM_BUF_ERR: i32 = 5; const PAM_AUTH_ERR: i32 = 7; // PAM message styles const PAM_PROMPT_ECHO_OFF: libc::c_int = 1; const PAM_PROMPT_ECHO_ON: libc::c_int = 2; /// PAM message structure (pam_message). #[repr(C)] struct PamMessage { msg_style: libc::c_int, msg: *const libc::c_char, } /// PAM response structure (pam_response). #[repr(C)] struct PamResponse { resp: *mut libc::c_char, resp_retcode: libc::c_int, } /// PAM conversation callback type. 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; /// PAM conversation structure (pam_conv). #[repr(C)] struct PamConv { conv: PamConvFn, appdata_ptr: *mut libc::c_void, } // libpam function declarations unsafe extern "C" { fn pam_start( service_name: *const libc::c_char, user: *const libc::c_char, pam_conversation: *const PamConv, pamh: *mut *mut libc::c_void, ) -> libc::c_int; fn pam_authenticate(pamh: *mut libc::c_void, flags: libc::c_int) -> libc::c_int; fn pam_acct_mgmt(pamh: *mut libc::c_void, flags: libc::c_int) -> libc::c_int; fn pam_end(pamh: *mut libc::c_void, pam_status: libc::c_int) -> libc::c_int; } /// PAM conversation callback — provides the password to PAM. /// /// # Safety /// Called by libpam during authentication. Allocates response memory with calloc/strdup /// which PAM will free. The appdata_ptr must point to a valid CString (the password). unsafe extern "C" fn pam_conv_callback( num_msg: libc::c_int, msg: *mut *const PamMessage, resp: *mut *mut PamResponse, appdata_ptr: *mut libc::c_void, ) -> libc::c_int { unsafe { if num_msg <= 0 { return PAM_AUTH_ERR; } // Safety: appdata_ptr was set to a valid *const CString in authenticate() let password = appdata_ptr as *const CString; if password.is_null() { return PAM_AUTH_ERR; } // Safety: calloc returns zeroed memory for num_msg PamResponse structs. // PAM owns this memory and will free() it. let resp_array = libc::calloc( num_msg as libc::size_t, std::mem::size_of::() as libc::size_t, ) as *mut PamResponse; if resp_array.is_null() { return PAM_BUF_ERR; } for i in 0..num_msg as isize { let resp_ptr = resp_array.offset(i); // Safety: msg is an array of pointers provided by PAM let pam_msg = *msg.offset(i); let msg_style = (*pam_msg).msg_style; match msg_style { PAM_PROMPT_ECHO_OFF => { // Password prompt — provide the password via strdup let dup = libc::strdup((*password).as_ptr()); if dup.is_null() { // strdup failed (OOM) — free all previously allocated strings for j in 0..i { let prev = resp_array.offset(j); if !(*prev).resp.is_null() { libc::free((*prev).resp as *mut libc::c_void); } } libc::free(resp_array as *mut libc::c_void); return PAM_BUF_ERR; } (*resp_ptr).resp = dup; } PAM_PROMPT_ECHO_ON => { // Visible prompt — provide empty string, never the password let empty = libc::strdup(b"\0".as_ptr() as *const libc::c_char); if empty.is_null() { for j in 0..i { let prev = resp_array.offset(j); if !(*prev).resp.is_null() { libc::free((*prev).resp as *mut libc::c_void); } } libc::free(resp_array as *mut libc::c_void); return PAM_BUF_ERR; } (*resp_ptr).resp = empty; } _ => { // PAM_ERROR_MSG, PAM_TEXT_INFO, or unknown — no response expected (*resp_ptr).resp = ptr::null_mut(); } } (*resp_ptr).resp_retcode = 0; } // Safety: resp is a valid pointer provided by PAM *resp = resp_array; PAM_SUCCESS } } /// Authenticate a user via PAM. /// /// Returns true on success, false on authentication failure. /// The password is wiped from memory after use via zeroize. pub fn authenticate(username: &str, password: &str) -> bool { // CString::new takes ownership of the Vec — Zeroizing wipes on drop let password_cstr = match CString::new(password.as_bytes().to_vec()) { Ok(c) => Zeroizing::new(c), Err(_) => return false, // Password contains null byte }; let service = match CString::new("moonlock") { Ok(c) => c, Err(_) => return false, }; let username_cstr = match CString::new(username) { Ok(c) => c, Err(_) => return false, }; let conv = PamConv { conv: pam_conv_callback, appdata_ptr: std::ptr::from_ref::(&password_cstr) as *mut libc::c_void, }; let mut handle: *mut libc::c_void = ptr::null_mut(); // Safety: All pointers are valid CStrings that outlive the pam_start call. // handle is an output parameter that PAM will set. let ret = unsafe { pam_start( service.as_ptr(), username_cstr.as_ptr(), &conv, &mut handle, ) }; if ret != PAM_SUCCESS { return false; } if handle.is_null() { return false; } // Safety: handle is valid and non-null after successful pam_start let auth_ret = unsafe { pam_authenticate(handle, 0) }; let acct_ret = if auth_ret == PAM_SUCCESS { // Safety: handle is valid, check account restrictions unsafe { pam_acct_mgmt(handle, 0) } } else { auth_ret }; // Safety: handle is valid, pam_end cleans up the PAM session unsafe { pam_end(handle, acct_ret) }; acct_ret == PAM_SUCCESS } /// Check account restrictions via PAM without authentication. /// /// Used after fingerprint unlock to enforce account policies (lockout, expiry) /// that would otherwise be bypassed when not going through pam_authenticate. /// Returns true if the account is valid and allowed to log in. pub fn check_account(username: &str) -> bool { let service = match CString::new("moonlock") { Ok(c) => c, Err(_) => return false, }; let username_cstr = match CString::new(username) { Ok(c) => c, Err(_) => return false, }; // No password needed — we only check account status, not authenticate. // PAM conv callback is required by pam_start but won't be called for acct_mgmt. let empty_password = Zeroizing::new(CString::new("").unwrap()); let conv = PamConv { conv: pam_conv_callback, appdata_ptr: std::ptr::from_ref::(&empty_password) as *mut libc::c_void, }; let mut handle: *mut libc::c_void = ptr::null_mut(); let ret = unsafe { pam_start( service.as_ptr(), username_cstr.as_ptr(), &conv, &mut handle, ) }; if ret != PAM_SUCCESS || handle.is_null() { return false; } let acct_ret = unsafe { pam_acct_mgmt(handle, 0) }; unsafe { pam_end(handle, acct_ret) }; acct_ret == PAM_SUCCESS } #[cfg(test)] mod tests { use super::*; #[test] fn pam_service_name_is_valid_cstring() { let service = CString::new("moonlock").unwrap(); assert_eq!(service.as_bytes(), b"moonlock"); } #[test] fn password_with_null_byte_fails() { // authenticate should return false for passwords with embedded nulls let result = authenticate("testuser", "pass\0word"); assert!(!result); } #[test] fn zeroizing_wipes_password() { let password = Zeroizing::new(vec![0x41u8, 0x42, 0x43]); let ptr = password.as_ptr(); drop(password); // After drop, the memory should be zeroed (though we can't reliably // test this since the allocator may reuse the memory). This test // verifies the Zeroizing wrapper compiles and drops correctly. assert!(!ptr.is_null()); } #[test] fn empty_username_fails() { // Empty username should not crash, just fail auth let result = authenticate("", "password"); assert!(!result); } #[test] fn check_account_empty_username_fails() { let result = check_account(""); assert!(!result); } #[test] fn check_account_null_byte_username_fails() { let result = check_account("user\0name"); assert!(!result); } }