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:
+190
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user