diff --git a/quickshell/services/python/lockscreen_auth_service.py b/quickshell/services/python/lockscreen_auth_service.py index 3cd5dde..ae729d0 100755 --- a/quickshell/services/python/lockscreen_auth_service.py +++ b/quickshell/services/python/lockscreen_auth_service.py @@ -4,33 +4,60 @@ import os import ctypes import ctypes.util import getpass +import subprocess -libpam = None -try: - libpam_path = ctypes.util.find_library('pam') or 'libpam.so.0' - libpam = ctypes.CDLL(libpam_path) -except Exception: - libpam = None - -libc = ctypes.CDLL(None) - -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)] - -def verify_password(username, password): - if not password: +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 - if not libpam: + + 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 +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: + 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): @@ -44,22 +71,40 @@ def verify_password(username, password): cb = CONV_FUNC(conv_cb) conv = PamConv(cb, None) - for service in ['system-auth', 'login', 'passwd', 'kde', 'sddm', 'gdm', 'su', 'shadow']: - pamh = ctypes.c_void_p() - res = libpam.pam_start(service.encode('utf-8'), username.encode('utf-8'), ctypes.byref(conv), ctypes.byref(pamh)) - if res == 0: - auth_res = libpam.pam_authenticate(pamh, 0) - libpam.pam_end(pamh, auth_res) - if auth_res == 0: - return True + for service in ['system-auth', 'system-local-login', 'system-login', 'login', 'passwd', 'kde', 'sddm', 'gdm', 'su', 'other']: + try: + pamh = ctypes.c_void_p() + res = libpam.pam_start(service.encode('utf-8'), username.encode('utf-8'), ctypes.byref(conv), ctypes.byref(pamh)) + if res == 0: + auth_res = libpam.pam_authenticate(pamh, 0) + libpam.pam_end(pamh, auth_res) + if auth_res == 0: + return True + except Exception: + pass + + 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): + return True + + # 2. Fallback method: libpam API + if verify_via_libpam(username, password): + return True + return False def main(): - if len(sys.argv) < 2: - # Read from stdin - password = sys.stdin.read().strip() + password = "" + if len(sys.argv) >= 2: + password = sys.argv[1] else: - password = sys.argv[1].strip() + password = sys.stdin.read().strip() username = getpass.getuser() if len(sys.argv) >= 3: