2026-02-05 18:05:06 -05:00

278 lines
8.3 KiB
Python

"""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 <finger> <username>")
print(" - Test: fprintd-verify <username>")