Files
moonlock/src/auth.rs
T
nevaforget 17f8930ff7 fix: security and correctness audit fixes (v0.4.1)
PAM conv callback: check msg_style (password only for ECHO_OFF),
handle strdup OOM with proper cleanup, null-check PAM handle.

Fingerprint: self-wire D-Bus g-signal in start() via Rc<RefCell<>>
and connect_local — VerifyStatus signals are now actually dispatched.
VerifyStop before VerifyStart in restart_verify.

Lockscreen: password entry stays active after faillock threshold
(PAM decides lockout, not UI), use Zeroizing<String> from GTK entry.

Release builds exit(1) without ext-session-lock-v1 support.

Config: fingerprint_enabled as Option<bool> so empty user config
does not override system config.

Dead code: remove unused i18n strings and fingerprint accessors,
parameterize faillock_warning max_attempts.
2026-03-28 00:06:27 +01:00

240 lines
7.5 KiB
Rust

// 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;
// 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 {
// Safety: appdata_ptr was set to a valid *const CString in authenticate()
let password = appdata_ptr as *const CString;
if password.is_null() {
return 7; // 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::<PamResponse>() as libc::size_t,
) as *mut PamResponse;
if resp_array.is_null() {
return 7; // PAM_AUTH_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 {
// Use Zeroizing to ensure password bytes are wiped on drop
let password_bytes = Zeroizing::new(password.as_bytes().to_vec());
let password_cstr = match CString::new(password_bytes.as_slice()) {
Ok(c) => 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: &password_cstr as *const CString 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
}
#[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);
}
}