278 lines
8.3 KiB
Python
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>")
|