From 996579c7c596a04ebfe2b1b03106650af1f13589 Mon Sep 17 00:00:00 2001 From: Sho Date: Sun, 2 Aug 2026 16:17:37 +0900 Subject: [PATCH] fix(lockscreen): implement triple-engine authentication using persistent libpam C-callback, sudo, and su fallback --- .../python/lockscreen_auth_service.py | 169 +++++++++++------- 1 file changed, 104 insertions(+), 65 deletions(-) diff --git a/quickshell/services/python/lockscreen_auth_service.py b/quickshell/services/python/lockscreen_auth_service.py index ae729d0..7a657b9 100755 --- a/quickshell/services/python/lockscreen_auth_service.py +++ b/quickshell/services/python/lockscreen_auth_service.py @@ -6,70 +6,67 @@ import ctypes.util import getpass import subprocess -def verify_via_chkpwd(username, password): - """Uses setuid unix_chkpwd helper for unprivileged Linux desktop password verification.""" - chkpwd_paths = [ - '/sbin/unix_chkpwd', - '/usr/sbin/unix_chkpwd', - '/usr/libexec/unix_chkpwd', - '/usr/lib/security/unix_chkpwd', - '/usr/lib/chkpwd/unix_chkpwd' - ] - - chkpwd_bin = None - for p in chkpwd_paths: - if os.path.exists(p): - chkpwd_bin = p - break - - if not chkpwd_bin: - return False - - try: - proc = subprocess.Popen( - [chkpwd_bin, username, 'nullhelper'], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - pwd_bytes = password.encode('utf-8') + b'\x00' - proc.communicate(input=pwd_bytes, timeout=3) - return (proc.returncode == 0) - except Exception: - return False +# ------------------------------------------------------------------------------ +# 1. PAM C API Authentication Engine (with persistent callback & libc allocations) +# ------------------------------------------------------------------------------ +libpam = None +libc = None + +try: + libpam_path = ctypes.util.find_library('pam') or 'libpam.so.0' + libpam = ctypes.CDLL(libpam_path) + libc = ctypes.CDLL(None) + libc.calloc.restype = ctypes.c_void_p + libc.calloc.argtypes = [ctypes.c_size_t, ctypes.c_size_t] + libc.strdup.restype = ctypes.c_void_p + libc.strdup.argtypes = [ctypes.c_char_p] +except Exception: + libpam = None + +class PamMessage(ctypes.Structure): + _fields_ = [('msg_style', ctypes.c_int), ('msg', ctypes.c_char_p)] + +class PamResponse(ctypes.Structure): + _fields_ = [('resp', ctypes.c_void_p), ('resp_retcode', ctypes.c_int)] + +PamConvFunc = ctypes.CFUNCTYPE( + ctypes.c_int, + ctypes.c_int, + ctypes.POINTER(ctypes.POINTER(PamMessage)), + ctypes.POINTER(ctypes.POINTER(PamResponse)), + ctypes.c_void_p +) + +class PamConv(ctypes.Structure): + _fields_ = [('conv', PamConvFunc), ('appdata_ptr', ctypes.c_void_p)] + +def _py_pam_conv(n_messages, messages, p_response, appdata_ptr): + if not appdata_ptr or not libc: + return 19 # PAM_CONV_ERR + arr = libc.calloc(n_messages, ctypes.sizeof(PamResponse)) + responses = ctypes.cast(arr, ctypes.POINTER(PamResponse)) + + for i in range(n_messages): + msg = messages[i].contents + if msg.msg_style in (1, 2): # PAM_PROMPT_ECHO_OFF or PAM_PROMPT_ECHO_ON + p_bytes = ctypes.cast(appdata_ptr, ctypes.c_char_p).value + if p_bytes: + responses[i].resp = libc.strdup(p_bytes) + responses[i].resp_retcode = 0 + + p_response[0] = responses + return 0 + +# Store persistent C-callback function reference to prevent GC cleanup +GLOBAL_PAM_CONV_FUNC = PamConvFunc(_py_pam_conv) def verify_via_libpam(username, password): - """Uses libpam C API for PAM service authentication.""" - try: - libpam_path = ctypes.util.find_library('pam') or 'libpam.so.0' - libpam = ctypes.CDLL(libpam_path) - libc = ctypes.CDLL(None) - except Exception: + if not libpam or not libc: return False - class PamMessage(ctypes.Structure): - _fields_ = [('msg_style', ctypes.c_int), ('msg', ctypes.c_char_p)] - - class PamResponse(ctypes.Structure): - _fields_ = [('resp', ctypes.c_char_p), ('resp_retcode', ctypes.c_int)] - - CONV_FUNC = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.POINTER(PamMessage)), ctypes.POINTER(ctypes.POINTER(PamResponse)), ctypes.c_void_p) - - class PamConv(ctypes.Structure): - _fields_ = [('conv', CONV_FUNC), ('appdata_ptr', ctypes.c_void_p)] - pwd_bytes = password.encode('utf-8') - - def conv_cb(num_msg, msg, resp, appdata_ptr): - response_array = (PamResponse * num_msg)() - for i in range(num_msg): - response_array[i].resp = libc.strdup(pwd_bytes) - response_array[i].resp_retcode = 0 - resp[0] = ctypes.cast(response_array, ctypes.POINTER(PamResponse)) - return 0 - - cb = CONV_FUNC(conv_cb) - conv = PamConv(cb, None) + pwd_buf = ctypes.c_char_p(pwd_bytes) + conv = PamConv(GLOBAL_PAM_CONV_FUNC, ctypes.cast(pwd_buf, ctypes.c_void_p)) for service in ['system-auth', 'system-local-login', 'system-login', 'login', 'passwd', 'kde', 'sddm', 'gdm', 'su', 'other']: try: @@ -82,19 +79,61 @@ def verify_via_libpam(username, password): return True except Exception: pass - + return False +# ------------------------------------------------------------------------------ +# 2. Sudo Authentication Engine (Fallback for user password validation) +# ------------------------------------------------------------------------------ +def verify_via_sudo(username, password): + try: + # Reset sudo timestamp cache first so sudo always asks for password + subprocess.run(['sudo', '-k'], capture_output=True, timeout=2) + + # Validate password via sudo -S -v + proc = subprocess.Popen( + ['sudo', '-S', '-v'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + pwd_bytes = (password + '\n').encode('utf-8') + _, _ = proc.communicate(input=pwd_bytes, timeout=3) + return (proc.returncode == 0) + except Exception: + return False + +# ------------------------------------------------------------------------------ +# 3. Su Authentication Engine (Fallback for user password validation) +# ------------------------------------------------------------------------------ +def verify_via_su(username, password): + try: + proc = subprocess.Popen( + ['su', '-c', 'true', username], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + pwd_bytes = (password + '\n').encode('utf-8') + _, _ = proc.communicate(input=pwd_bytes, timeout=3) + return (proc.returncode == 0) + except Exception: + return False + def verify_password(username, password): if not password: return False - - # 1. Primary method for non-root desktop processes: unix_chkpwd - if verify_via_chkpwd(username, password): + + # 1. Primary method: C libpam API + if verify_via_libpam(username, password): return True - # 2. Fallback method: libpam API - if verify_via_libpam(username, password): + # 2. Fallback method: Sudo password validation + if verify_via_sudo(username, password): + return True + + # 3. Fallback method: Su password validation + if verify_via_su(username, password): return True return False