fix(lockscreen): implement triple-engine authentication using persistent libpam C-callback, sudo, and su fallback

This commit is contained in:
Sho 2026-08-02 16:17:37 +09:00
parent 00baedf8c9
commit 996579c7c5

View File

@ -6,70 +6,67 @@ import ctypes.util
import getpass import getpass
import subprocess import subprocess
def verify_via_chkpwd(username, password): # ------------------------------------------------------------------------------
"""Uses setuid unix_chkpwd helper for unprivileged Linux desktop password verification.""" # 1. PAM C API Authentication Engine (with persistent callback & libc allocations)
chkpwd_paths = [ # ------------------------------------------------------------------------------
'/sbin/unix_chkpwd', libpam = None
'/usr/sbin/unix_chkpwd', libc = None
'/usr/libexec/unix_chkpwd',
'/usr/lib/security/unix_chkpwd',
'/usr/lib/chkpwd/unix_chkpwd'
]
chkpwd_bin = None try:
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
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_path = ctypes.util.find_library('pam') or 'libpam.so.0'
libpam = ctypes.CDLL(libpam_path) libpam = ctypes.CDLL(libpam_path)
libc = ctypes.CDLL(None) libc = ctypes.CDLL(None)
except Exception: libc.calloc.restype = ctypes.c_void_p
return False 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): class PamMessage(ctypes.Structure):
_fields_ = [('msg_style', ctypes.c_int), ('msg', ctypes.c_char_p)] _fields_ = [('msg_style', ctypes.c_int), ('msg', ctypes.c_char_p)]
class PamResponse(ctypes.Structure): class PamResponse(ctypes.Structure):
_fields_ = [('resp', ctypes.c_char_p), ('resp_retcode', ctypes.c_int)] _fields_ = [('resp', ctypes.c_void_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) 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): class PamConv(ctypes.Structure):
_fields_ = [('conv', CONV_FUNC), ('appdata_ptr', ctypes.c_void_p)] _fields_ = [('conv', PamConvFunc), ('appdata_ptr', ctypes.c_void_p)]
pwd_bytes = password.encode('utf-8') 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))
def conv_cb(num_msg, msg, resp, appdata_ptr): for i in range(n_messages):
response_array = (PamResponse * num_msg)() msg = messages[i].contents
for i in range(num_msg): if msg.msg_style in (1, 2): # PAM_PROMPT_ECHO_OFF or PAM_PROMPT_ECHO_ON
response_array[i].resp = libc.strdup(pwd_bytes) p_bytes = ctypes.cast(appdata_ptr, ctypes.c_char_p).value
response_array[i].resp_retcode = 0 if p_bytes:
resp[0] = ctypes.cast(response_array, ctypes.POINTER(PamResponse)) responses[i].resp = libc.strdup(p_bytes)
responses[i].resp_retcode = 0
p_response[0] = responses
return 0 return 0
cb = CONV_FUNC(conv_cb) # Store persistent C-callback function reference to prevent GC cleanup
conv = PamConv(cb, None) GLOBAL_PAM_CONV_FUNC = PamConvFunc(_py_pam_conv)
def verify_via_libpam(username, password):
if not libpam or not libc:
return False
pwd_bytes = password.encode('utf-8')
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']: for service in ['system-auth', 'system-local-login', 'system-login', 'login', 'passwd', 'kde', 'sddm', 'gdm', 'su', 'other']:
try: try:
@ -85,16 +82,58 @@ def verify_via_libpam(username, password):
return False 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): def verify_password(username, password):
if not password: if not password:
return False return False
# 1. Primary method for non-root desktop processes: unix_chkpwd # 1. Primary method: C libpam API
if verify_via_chkpwd(username, password): if verify_via_libpam(username, password):
return True return True
# 2. Fallback method: libpam API # 2. Fallback method: Sudo password validation
if verify_via_libpam(username, password): if verify_via_sudo(username, password):
return True
# 3. Fallback method: Su password validation
if verify_via_su(username, password):
return True return True
return False return False