//! PAM stays on a worker thread. No password is placed in argv, logs or IPC. use std::ffi::{CStr, CString, c_char, c_int, c_void}; use zeroize::Zeroizing; #[repr(C)] struct Message { style: c_int, text: *const c_char, } #[repr(C)] struct Response { text: *mut c_char, code: c_int, } #[repr(C)] struct Conversation { callback: unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int, data: *mut c_void, } struct Credentials { user: CString, password: Zeroizing>, password_prompts: u8, } const SUCCESS: c_int = 0; const CONV_ERR: c_int = 19; // Linux-PAM owns successful response allocations and frees them using free(). unsafe extern "C" fn converse( count: c_int, messages: *mut *const Message, out: *mut *mut Response, data: *mut c_void, ) -> c_int { if count <= 0 || count > 32 || messages.is_null() || out.is_null() || data.is_null() { return CONV_ERR; } unsafe { *out = std::ptr::null_mut(); let credentials = &mut *(data as *mut Credentials); let responses = libc::calloc(count as usize, std::mem::size_of::()) as *mut Response; if responses.is_null() { return CONV_ERR; } for i in 0..count as usize { let msg = *messages.add(i); let bytes = if msg.is_null() { None } else { match (*msg).style { 1 => { credentials.password_prompts = credentials.password_prompts.saturating_add(1); (credentials.password_prompts == 1) .then_some(credentials.password.as_slice()) } // Never send the password as a second-factor response. 2 => Some(credentials.user.as_bytes_with_nul()), // PAM_PROMPT_ECHO_ON 3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO _ => None, } }; if let Some(bytes) = bytes { if !bytes.is_empty() { let allocation = libc::malloc(bytes.len()) as *mut c_char; if !allocation.is_null() { std::ptr::copy_nonoverlapping( bytes.as_ptr(), allocation.cast(), bytes.len(), ); (*responses.add(i)).text = allocation; continue; } } else { continue; } } for j in 0..i { let ptr = (*responses.add(j)).text; if !ptr.is_null() { libc::explicit_bzero(ptr.cast(), CStr::from_ptr(ptr).to_bytes().len()); libc::free(ptr.cast()); } } libc::free(responses.cast()); return CONV_ERR; } *out = responses; SUCCESS } } pub fn username() -> Result { // Never trust USER/LOGNAME for the account being unlocked. unsafe { let mut entry: libc::passwd = std::mem::zeroed(); let mut result = std::ptr::null_mut(); let mut buffer = vec![0u8; 16384]; let status = libc::getpwuid_r( libc::getuid(), &mut entry, buffer.as_mut_ptr().cast(), buffer.len(), &mut result, ); if status != 0 || result.is_null() || entry.pw_name.is_null() { return Err("Cannot determine the session user".into()); } Ok(CStr::from_ptr(entry.pw_name).to_string_lossy().into_owned()) } } pub fn available() -> bool { std::path::Path::new("/etc/pam.d/ferese-lock").is_file() && unsafe { libloading::Library::new("libpam.so.0") }.is_ok() } pub fn authenticate(user: String, password: Zeroizing) -> bool { authenticate_with(&user, &password, None) } fn authenticate_with(user: &str, password: &str, config: Option<&CStr>) -> bool { if password.is_empty() || password.contains('\0') { return false; } let Ok(user) = CString::new(user) else { return false; }; let mut secret = Zeroizing::new(password.as_bytes().to_vec()); secret.push(0); let mut credentials = Credentials { user, password: secret, password_prompts: 0, }; unsafe { let Ok(library) = libloading::Library::new("libpam.so.0") else { return false; }; type Start = unsafe extern "C" fn( *const c_char, *const c_char, *const Conversation, *mut *mut c_void, ) -> c_int; type StartConf = unsafe extern "C" fn( *const c_char, *const c_char, *const Conversation, *const c_char, *mut *mut c_void, ) -> c_int; type Check = unsafe extern "C" fn(*mut c_void, c_int) -> c_int; let (Ok(start), Ok(auth), Ok(account), Ok(end)) = ( library.get::(b"pam_start\0"), library.get::(b"pam_authenticate\0"), library.get::(b"pam_acct_mgmt\0"), library.get::(b"pam_end\0"), ) else { return false; }; let conversation = Conversation { callback: converse, data: (&mut credentials as *mut Credentials).cast(), }; let mut handle = std::ptr::null_mut(); let status = if let Some(config) = config { let Ok(start) = library.get::(b"pam_start_confdir\0") else { return false; }; start( c"ferese-lock".as_ptr(), credentials.user.as_ptr(), &conversation, config.as_ptr(), &mut handle, ) } else { start( c"ferese-lock".as_ptr(), credentials.user.as_ptr(), &conversation, &mut handle, ) }; if status != SUCCESS || handle.is_null() { return false; } let mut status = auth(handle, 1); // PAM_DISALLOW_NULL_AUTHTOK if status == SUCCESS { status = account(handle, 0); } let ended = end(handle, status); status == SUCCESS && ended == SUCCESS } } #[cfg(test)] mod tests { use super::*; #[test] fn pam_requires_authentication_and_account_permission() { let directory = tempfile::tempdir().unwrap(); let path = CString::new(directory.path().as_os_str().as_encoded_bytes()).unwrap(); for (auth, account, allowed) in [ ("permit", "permit", true), ("deny", "permit", false), ("permit", "deny", false), ] { std::fs::write( directory.path().join("ferese-lock"), format!("auth required pam_{auth}.so\naccount required pam_{account}.so\n"), ) .unwrap(); assert_eq!(authenticate_with("test", "test-only", Some(&path)), allowed); } assert!(!authenticate_with("test", "", Some(&path))); assert!(!authenticate_with("test", "bad\0value", Some(&path))); } #[test] fn conversation_rejects_multiple_secret_prompts_and_unknown_styles() { let mut credentials = Credentials { user: CString::new("tester").unwrap(), password: Zeroizing::new(b"test-only\0".to_vec()), password_prompts: 0, }; let first = Message { style: 1, text: c"Password".as_ptr(), }; let mut prompts = [&first as *const Message, &first as *const Message]; let mut responses = std::ptr::null_mut(); unsafe { assert_eq!( converse( 2, prompts.as_mut_ptr(), &mut responses, (&mut credentials as *mut Credentials).cast() ), CONV_ERR ); assert!(responses.is_null()); let unknown = Message { style: 99, text: std::ptr::null(), }; prompts[0] = &unknown; assert_eq!( converse( 1, prompts.as_mut_ptr(), &mut responses, (&mut credentials as *mut Credentials).cast() ), CONV_ERR ); assert!(responses.is_null()); } } #[test] fn current_user_comes_from_uid() { assert!(!username().unwrap().is_empty()); } }