All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 2s
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)
301 lines
9.2 KiB
Rust
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);
|
|
}
|
|
}
|