Rewrite moonlock from Python to Rust (v0.4.0)

Complete rewrite of the Wayland lockscreen from Python/PyGObject to
Rust/gtk4-rs for memory safety in security-critical PAM code and
consistency with the moonset/moongreet Rust ecosystem.

Modules: main, lockscreen, auth (PAM FFI), fingerprint (fprintd D-Bus),
config, i18n, users, power. 37 unit tests.

Security: PAM conversation callback with Zeroizing password, panic hook
that never unlocks, root check, ext-session-lock-v1 compositor policy,
absolute loginctl path, avatar symlink rejection.
This commit is contained in:
2026-03-27 23:09:54 +01:00
parent 7de3737a61
commit 817a9547ad
41 changed files with 3075 additions and 2264 deletions
+190
View File
@@ -0,0 +1,190 @@
// 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;
/// 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 {
// 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 {
// Safety: strdup allocates with malloc — PAM will free() the resp strings.
// We dereference password which is valid for the lifetime of authenticate().
let resp_ptr = resp_array.offset(i);
(*resp_ptr).resp = libc::strdup((*password).as_ptr());
(*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;
}
// Safety: handle is valid 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);
}
}