moonlock/src/auth.rs
nevaforget 1d8921abee
All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
fix: audit fixes — blur offset, lock-before-IO, FP signal lifecycle, TOCTOU (v0.6.6)
Third triple audit (quality, performance, security). Key fixes:
- Blur padding offset: texture at (-pad,-pad) prevents edge darkening on all sides
- Wallpaper loads after lock.lock() — disk I/O no longer delays lock acquisition
- begin_verification disconnects old signal handler before registering new one
- resume_async resets failed_attempts to prevent premature exhaustion
- Unknown VerifyStatus with done=true triggers restart instead of hanging
- symlink_metadata() replaces separate is_file()+is_symlink() (TOCTOU)
- faillock_warning dead code removed, blur sigma clamped to [0,100]
- Redundant Zeroizing<Vec<u8>> removed, on_verify_status restricted to pub(crate)
- Warn logging for non-UTF-8 GECOS and avatar path errors
- Default impl for FingerprintListener, 3 new tests (47 total)
2026-03-30 13:09:02 +02:00

301 lines
9.2 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;
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::<PamResponse>() 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<CString> 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::<CString>(&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::<CString>(&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);
}
}