"""Fingerprint authentication setup for Elan 04f3:0c4b reader. This module sets up fingerprint authentication using fprintd with the Lenovo TOD (Touch OEM Drivers) driver for the Elan fingerprint sensor. Requires: - fprintd and libfprint packages - Lenovo TOD driver (libfprint-2-tod1-elan.so) - User account to enroll fingerprints for """ import subprocess import tempfile import urllib.request import zipfile from pathlib import Path from .utils import info, success, warn, error, fatal, run, prompt # Lenovo TOD driver download URL (Ubuntu 22.04 package, works on Gentoo) LENOVO_DRIVER_URL = "https://download.lenovo.com/pccbbs/mobiles/r1elf10w.zip" TOD_DRIVER_NAME = "libfprint-2-tod1-elan.so" TOD_INSTALL_DIR = Path("/usr/lib64/libfprint-2/tod-1") # PAM configuration for fingerprint (password OR fingerprint) PAM_FINGERPRINT_LINES = """\ # Fingerprint authentication - press Enter on empty password to use fingerprint auth [success=1 new_authtok_reqd=1 default=ignore] pam_unix.so try_first_pass likeauth nullok auth sufficient pam_fprintd.so """ # PAM files to configure PAM_FILES = [ "/etc/pam.d/sddm", "/etc/pam.d/hyprlock", ] def check_elan_device() -> bool: """Check if Elan fingerprint reader is present.""" try: result = subprocess.run( ["cat", "/sys/bus/usb/devices/*/product"], capture_output=True, text=True, check=False, ) return "ELAN:Fingerprint" in result.stdout except (FileNotFoundError, OSError): return False def install_packages() -> None: """Install fprintd and libfprint.""" info("Installing fingerprint packages...") from .utils import emerge emerge("sys-auth/fprintd", "sys-auth/libfprint") success("Fingerprint packages installed.") def download_tod_driver() -> Path: """Download Lenovo TOD driver and extract the .so file.""" dest = TOD_INSTALL_DIR / TOD_DRIVER_NAME # Check if already installed (idempotency) if dest.exists(): info("TOD driver already installed") return dest info(f"Downloading Lenovo TOD driver from {LENOVO_DRIVER_URL}...") with tempfile.TemporaryDirectory() as tmpdir: tmppath = Path(tmpdir) zip_path = tmppath / "driver.zip" # Download urllib.request.urlretrieve(LENOVO_DRIVER_URL, zip_path) success("Downloaded driver package.") # Extract info("Extracting driver...") with zipfile.ZipFile(zip_path, "r") as zf: zf.extractall(tmppath) # Find the .so file (it's nested in the archive) so_files = list(tmppath.rglob(TOD_DRIVER_NAME)) if not so_files: fatal(f"Could not find {TOD_DRIVER_NAME} in downloaded package") # Copy to install location TOD_INSTALL_DIR.mkdir(parents=True, exist_ok=True) dest = TOD_INSTALL_DIR / TOD_DRIVER_NAME import shutil shutil.copy2(so_files[0], dest) dest.chmod(0o755) success(f"Installed TOD driver to {dest}") return dest def verify_device() -> bool: """Verify fprintd can see the device.""" info("Verifying fingerprint device...") result = subprocess.run( ["fprintd-list", "root"], capture_output=True, text=True, check=False, ) if result.returncode != 0 or "No devices available" in result.stderr: return False return True def enroll_fingerprints(username: str) -> None: """Enroll fingerprints for a user.""" info(f"Enrolling fingerprints for user '{username}'...") # Check if already enrolled (idempotency) result = subprocess.run( ["fprintd-list", username], capture_output=True, text=True, check=False, ) if result.returncode == 0 and "right-index-finger" in result.stdout: info(f"Fingerprints already enrolled for {username}") response = prompt("Re-enroll fingerprints? [y/N]: ").strip().lower() if response not in ("y", "yes"): return print() print("You will be prompted to swipe your finger multiple times.") print("Press Ctrl+C to skip enrollment (you can do this later).") print() fingers = ["right-index-finger", "left-index-finger"] for finger in fingers: response = prompt(f"Enroll {finger}? [Y/n]: ").strip().lower() if response in ("", "y", "yes"): try: run("fprintd-enroll", "-f", finger, username) success(f"Enrolled {finger}.") except KeyboardInterrupt: warn(f"Skipped {finger}.") except subprocess.CalledProcessError: warn(f"Failed to enroll {finger}.") else: info(f"Skipped {finger}.") print() info("Testing fingerprint verification...") run("fprintd-verify", username, check=False) def configure_pam() -> None: """Configure PAM files for fingerprint authentication.""" info("Configuring PAM for fingerprint authentication...") for pam_file in PAM_FILES: pam_path = Path(pam_file) if not pam_path.exists(): warn(f"PAM file not found: {pam_file} (skipping)") continue content = pam_path.read_text() # Check if already configured if "pam_fprintd.so" in content: info(f"PAM already configured: {pam_file}") continue # Find the first 'auth' line and insert before it lines = content.splitlines() new_lines = [] inserted = False for line in lines: if not inserted and line.strip().startswith("auth"): # Insert fingerprint config before first auth line new_lines.extend(PAM_FINGERPRINT_LINES.strip().splitlines()) inserted = True new_lines.append(line) if inserted: pam_path.write_text("\n".join(new_lines) + "\n") success(f"Configured {pam_file}") else: warn(f"No auth line found in {pam_file}") def configure_sudo() -> None: """Optionally configure sudo for fingerprint.""" response = prompt("Enable fingerprint for sudo? [y/N]: ").strip().lower() if response not in ("y", "yes"): info("Skipped sudo fingerprint configuration.") return pam_path = Path("/etc/pam.d/sudo") if not pam_path.exists(): warn("PAM file not found: /etc/pam.d/sudo") return content = pam_path.read_text() if "pam_fprintd.so" in content: info("sudo PAM already configured for fingerprint.") return lines = content.splitlines() new_lines = [] inserted = False for line in lines: if not inserted and line.strip().startswith("auth"): new_lines.extend(PAM_FINGERPRINT_LINES.strip().splitlines()) inserted = True new_lines.append(line) if inserted: pam_path.write_text("\n".join(new_lines) + "\n") success("Configured /etc/pam.d/sudo") def setup_fingerprint(source_dir: Path) -> None: """Main fingerprint setup routine.""" info("=== Fingerprint Authentication Setup ===") print() # Check for hardware if not check_elan_device(): fatal("Elan fingerprint reader not detected. Is the device connected?") info("Detected Elan fingerprint reader (04f3:0c4b)") print() # Install packages install_packages() print() # Install TOD driver download_tod_driver() print() # Verify device is recognized if not verify_device(): error("fprintd cannot see the device after driver installation.") error("You may need to reboot and run this command again.") return success("Fingerprint device recognized by fprintd.") print() # Get username for enrollment username = prompt("Username to enroll fingerprints for: ").strip() if not username: fatal("Username required for enrollment.") # Enroll fingerprints enroll_fingerprints(username) print() # Configure PAM configure_pam() print() # Optionally configure sudo configure_sudo() print() success("=== Fingerprint setup complete ===") print() print("Usage:") print(" - SDDM: Press Enter on empty password field to use fingerprint") print(" - hyprlock: Press Enter on empty password field to use fingerprint") print(" - Enroll more fingers: fprintd-enroll -f ") print(" - Test: fprintd-verify ")