From 679f332b8f2fca35e5b11ca4ac320685b740f8e9 Mon Sep 17 00:00:00 2001 From: Vedran Hrabar Date: Sat, 18 Jul 2026 20:36:36 +0200 Subject: [PATCH 1/4] feat(detector): implement caching for detector backend creation #4 --- secureEye/src/auth/detector_factory.py | 28 ++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/secureEye/src/auth/detector_factory.py b/secureEye/src/auth/detector_factory.py index 7189f21..333f693 100644 --- a/secureEye/src/auth/detector_factory.py +++ b/secureEye/src/auth/detector_factory.py @@ -2,6 +2,7 @@ from __future__ import annotations +import threading from dataclasses import dataclass from typing import Any @@ -10,6 +11,17 @@ class DetectorFactoryError(RuntimeError): """Raised when a detector backend cannot be initialized.""" +# cache build detector +_detector_cache: dict[tuple, DetectorBundle] = {} +_detector_cache_lock = threading.Lock() + + +def _cache_key(config) -> tuple: + backend = config.get("core", "detector_backend", fallback="dlib").strip().lower() + use_cnn = config.getboolean("core", "use_cnn", fallback=False) + return (backend, use_cnn) + + @dataclass class DetectorBundle: backend: str @@ -23,6 +35,22 @@ def _is_missing_module(exc: ModuleNotFoundError, *candidates: str) -> bool: def create_detector(config) -> DetectorBundle: + """Return a detector backend for ``config``, building it once and caching it. + + The first call for a given backend configuration builds the backend; every + subsequent call reuses the cached bundle. + """ + key = _cache_key(config) + with _detector_cache_lock: + cached = _detector_cache.get(key) + if cached is not None: + return cached + bundle = _build_detector(config) + _detector_cache[key] = bundle + return bundle + + +def _build_detector(config) -> DetectorBundle: """Create a detector backend from config. Supported values for [core] detector_backend: From efae18e245eadc0aa396704e6fc9d65c77934bf2 Mon Sep 17 00:00:00 2001 From: Vedran Hrabar Date: Sat, 18 Jul 2026 20:38:33 +0200 Subject: [PATCH 2/4] feat(detector): wire detector caching into flow #4 --- secureEye/src/authd/main.py | 26 +++++++++++++++++++++++++- secureEye/src/pam/main.cc | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/secureEye/src/authd/main.py b/secureEye/src/authd/main.py index eb61d8c..e1a9782 100644 --- a/secureEye/src/authd/main.py +++ b/secureEye/src/authd/main.py @@ -1,14 +1,18 @@ +import configparser import json import os import signal import socket import struct import sys +import syslog import threading from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError from dataclasses import dataclass +import paths_factory from auth import ExitCode, AuthSession +from auth.detector_factory import create_detector SOCKET_PATH = os.environ.get("SECUREEYE_AUTHD_SOCKET", "/run/secureeye/authd.sock") PROTO_VERSION = 1 @@ -16,6 +20,9 @@ DEFAULT_DEADLINE_MS = 2500 INTERNAL_ERROR_CODE = 99 +# time between the daemon's own worker deadline and the client's transport deadline +RESPONSE_MARGIN_MS = 700 + @dataclass class AuthRequest: @@ -153,7 +160,7 @@ def _handle_client(conn: socket.socket, peer: str) -> None: payload = _read_frame(conn) req = _validate_payload(payload) - worker_timeout = max(0.1, (req.deadline_ms - 300) / 1000.0) + worker_timeout = max(0.1, (req.deadline_ms - RESPONSE_MARGIN_MS) / 1000.0) # Run auth in a worker and do not block daemon shutdown waiting for timed-out requests. session = AuthSession() @@ -203,6 +210,21 @@ def _handle_client(conn: socket.socket, peer: str) -> None: pass +def _warm_detector() -> None: + """ + Build (and cache) the configured detector before serving requests. + """ + try: + config = configparser.ConfigParser() + config.read(paths_factory.config_file_path()) + create_detector(config) + except Exception as exc: + syslog.syslog( + syslog.LOG_WARNING, + f"secureeye-authd: detector warmup skipped: {exc}", + ) + + def main() -> int: stop = threading.Event() @@ -210,6 +232,8 @@ def main() -> int: signal.signal(signal.SIGINT, lambda signum, _frame: stop.set()) signal.signal(signal.SIGTERM, lambda signum, _frame: stop.set()) + _warm_detector() + # setup socket srv = _prepare_socket(SOCKET_PATH) diff --git a/secureEye/src/pam/main.cc b/secureEye/src/pam/main.cc index 04eb857..9d45e27 100644 --- a/secureEye/src/pam/main.cc +++ b/secureEye/src/pam/main.cc @@ -602,7 +602,7 @@ auto identify(pam_handle_t *pamh, int flags, int argc, const char **argv, video_timeout_s > 0 ? static_cast(video_timeout_s) * 1000 : static_cast(AUTH_TIMEOUT_MS); const auto auth_timeout_ms = static_cast( - std::clamp(configured_timeout_ms + 1500, 1000, 30000)); + std::clamp(configured_timeout_ms + 1900, 1000, 30000)); // NOTE: We should replace mutex and condition_variable by atomic wait, but // it's too recent (C++20) From d9085c80f561bfc73cafdb291857964311e82bde Mon Sep 17 00:00:00 2001 From: Vedran Hrabar Date: Thu, 23 Jul 2026 15:19:01 +0200 Subject: [PATCH 3/4] feat(Package-Arch): add initial ArchOS packaging for SecureEye face authentication #25 Signed-off-by: Vedran Hrabar --- secureEye/archlinux/secureEye/.SRCINFO | 54 +++++++++ secureEye/archlinux/secureEye/PKGBUILD | 112 ++++++++++++++++++ .../secureEye/secureeye-authd.install | 24 ++++ 3 files changed, 190 insertions(+) create mode 100644 secureEye/archlinux/secureEye/.SRCINFO create mode 100644 secureEye/archlinux/secureEye/PKGBUILD create mode 100644 secureEye/archlinux/secureEye/secureeye-authd.install diff --git a/secureEye/archlinux/secureEye/.SRCINFO b/secureEye/archlinux/secureEye/.SRCINFO new file mode 100644 index 0000000..5e863d4 --- /dev/null +++ b/secureEye/archlinux/secureEye/.SRCINFO @@ -0,0 +1,54 @@ +pkgbase = secureeye + pkgdesc = Face authentication for Linux + pkgver = 0.1.3 + pkgrel = 1 + url = https://github.com/vhrabar/SecureEye + arch = x86_64 + arch = aarch64 + license = GPL-2.0-only + license = MIT + makedepends = meson>=0.64 + makedepends = ninja + makedepends = pkgconf + makedepends = pam + makedepends = libevdev + makedepends = libinih + makedepends = python + makedepends = systemd + source = secureeye-0.1.3.tar.gz::https://github.com/vhrabar/SecureEye/archive/v0.1.3.tar.gz + source = secureeye-authd.install + sha256sums = c848207d44068849a13150a8b47a3d45806a9a43b9c868d1f5915ed8c2cf2d18 + sha256sums = e68c31756e7196406ec24bd55cdab1af473a35c2abdd4c44a2ca9ace525beb73 + +pkgname = libpam-secureeye + pkgdesc = PAM module for SecureEye face authentication + depends = pam + depends = libevdev + depends = libinih + depends = gcc-libs + depends = glibc + optdepends = secureeye-authd: authentication daemon, without it the module always fails + +pkgname = secureeye-authd + pkgdesc = SecureEye authentication daemon, CLI and Python runtime components + install = secureeye-authd.install + depends = python + depends = python-numpy + depends = python-opencv + depends = python-matplotlib + depends = python-cffi + depends = python-absl + depends = python-flatbuffers + depends = portaudio + depends = v4l-utils + depends = systemd + optdepends = libpam-secureeye: PAM integration, required to actually log in with SecureEye + optdepends = python-dlib: dlib recognition backend + optdepends = python-mediapipe: mediapipe recognition backend + optdepends = ffmpeg: ffmpeg camera capture backend + optdepends = python-ffmpeg-python: ffmpeg camera capture backend + optdepends = python-keyboard: hotkey rubberstamp + backup = etc/secureEye/config.ini + depends_x86_64 = python-mediapipe + depends_x86_64 = python-sounddevice + depends_aarch64 = python-dlib diff --git a/secureEye/archlinux/secureEye/PKGBUILD b/secureEye/archlinux/secureEye/PKGBUILD new file mode 100644 index 0000000..355b56a --- /dev/null +++ b/secureEye/archlinux/secureEye/PKGBUILD @@ -0,0 +1,112 @@ +# Maintainer: Vedran Hrabar + +pkgbase=secureeye +pkgname=('libpam-secureeye' 'secureeye-authd') +pkgver=0.1.2 +pkgrel=1 +pkgdesc="Face authentication for Linux" +arch=('x86_64' 'aarch64') +url="https://github.com/vhrabar/SecureEye" +license=('GPL-2.0-only' 'MIT') +makedepends=( + 'meson>=0.64' + 'ninja' + 'pkgconf' + 'pam' + 'libevdev' + 'libinih' + 'python' + 'systemd' +) +source=("$pkgbase-$pkgver.tar.gz::$url/archive/v${pkgver}.tar.gz" + 'secureeye-authd.install') +sha256sums=('c848207d44068849a13150a8b47a3d45806a9a43b9c868d1f5915ed8c2cf2d18' + 'e68c31756e7196406ec24bd55cdab1af473a35c2abdd4c44a2ca9ace525beb73') + +_srcname="SecureEye-$pkgver" + +prepare() { + cd "$_srcname" + + # The deb/rpm packages run the daemon from a wheel venv built at install + sed -i 's|/usr/lib/secureeye-authd/venv/bin/python3|/usr/bin/python3|' \ + secureEye/src/systemd/secureeye-authd.service.in +} + +build() { + meson setup "$_srcname" build \ + --wrap-mode=nodownload \ + --buildtype=plain \ + --prefix=/usr \ + --libdir=lib \ + --sysconfdir=/etc \ + --localstatedir=/var \ + -Dinstall_pam_config=false \ + -Dconfig_dir=/etc/secureEye \ + -Duser_models_dir=/etc/secureEye/models + meson compile -C build +} + +package_libpam-secureeye() { + pkgdesc="PAM module for SecureEye face authentication" + depends=('pam' 'libevdev' 'libinih' 'gcc-libs' 'glibc') + optdepends=('secureeye-authd: authentication daemon, without it the module always fails') + + # Install tags keep each split package to its own files. + meson install -C build --destdir "$pkgdir" --tags pam_module + + install -Dm644 "$_srcname/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 "$_srcname/licenses/MIT.txt" "$pkgdir/usr/share/licenses/$pkgname/MIT.txt" + install -Dm644 "$_srcname/NOTICE" "$pkgdir/usr/share/licenses/$pkgname/NOTICE" +} + +package_secureeye-authd() { + pkgdesc="SecureEye authentication daemon, CLI and Python runtime components" + depends=( + 'python' + 'python-numpy' + 'python-opencv' + 'python-matplotlib' + 'python-cffi' + 'python-absl' + 'python-flatbuffers' + 'portaudio' + 'v4l-utils' + 'systemd' + ) + # amd64 -> mediapipe (default backend) + # aarch64 -> dlib + depends_x86_64=('python-mediapipe' 'python-sounddevice') + depends_aarch64=('python-dlib') + optdepends=( + 'libpam-secureeye: PAM integration, required to actually log in with SecureEye' + 'python-dlib: dlib recognition backend' + 'python-mediapipe: mediapipe recognition backend' + 'ffmpeg: ffmpeg camera capture backend' + 'python-ffmpeg-python: ffmpeg camera capture backend' + 'python-keyboard: hotkey rubberstamp' + ) + backup=('etc/secureEye/config.ini') + install=secureeye-authd.install + + meson install -C build --destdir "$pkgdir" \ + --tags bin,py_sources,config,systemd,bash_completion,meta,man + + # user_models_dir, created by the CLI on first enrolment. + install -dm755 "$pkgdir/etc/secureEye/models" + + # The systemd unit runs as the "secureeye" user; systemd's pacman hook + # creates it from this fragment. + install -Dm644 "$_srcname/secureEye/rpm/secureeye-authd.sysusers" \ + "$pkgdir/usr/lib/sysusers.d/secureeye-authd.conf" + + if [[ $CARCH != x86_64 ]]; then + sed -i 's/^detector_backend = mediapipe/detector_backend = dlib/' \ + "$pkgdir/etc/secureEye/config.ini" + fi + + install -Dm644 "$_srcname/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 "$_srcname/licenses/MIT.txt" "$pkgdir/usr/share/licenses/$pkgname/MIT.txt" + install -Dm644 "$_srcname/NOTICE" "$pkgdir/usr/share/licenses/$pkgname/NOTICE" + install -Dm644 "$_srcname/README.md" "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/secureEye/archlinux/secureEye/secureeye-authd.install b/secureEye/archlinux/secureEye/secureeye-authd.install new file mode 100644 index 0000000..8462e69 --- /dev/null +++ b/secureEye/archlinux/secureEye/secureeye-authd.install @@ -0,0 +1,24 @@ +post_install() { + cat <<'EOF' +==> SecureEye is installed but not yet active. To finish setup: + + 1. Start the daemon: + systemctl enable --now secureeye-authd.service + 2. Enrol a face model: + sudo secureEye add + 3. Enable the PAM module (Arch has no pam-auth-update), e.g. for sudo add + this line at the top of /etc/pam.d/sudo: + auth sufficient pam_secureEye.so + Keep a root shell open while testing so a misconfiguration cannot lock + you out. + + Configuration lives in /etc/secureEye/config.ini. +EOF +} + +post_upgrade() { + if [ "$(vercmp "$2" 0.1.2)" -lt 0 ]; then + post_install + fi + systemctl try-restart secureeye-authd.service >/dev/null 2>&1 || true +} From 267210e86ce4ae02a52805b779967cef176d006b Mon Sep 17 00:00:00 2001 From: Vedran Hrabar Date: Thu, 23 Jul 2026 15:19:34 +0200 Subject: [PATCH 4/4] docs(README): update installation instructions for Arch Linux and clarify PAM configuration #25 Signed-off-by: Vedran Hrabar --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d53819d..5c62097 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # SecureEye [![Tests](https://img.shields.io/github/actions/workflow/status/vhrabar/SecureEye/pytests.yml?style=for-the-badge&label=tests&logo=pytest&logoColor=white)](https://github.com/vhrabar/SecureEye/actions/workflows/pytests.yml) -[![Lint](https://img.shields.io/github/actions/workflow/status/vhrabar/SecureEye/lint.yml?style=for-the-badge&label=lint&logo=ruff&logoColor=white)](https://github.com/vhrabar/SecureEye/actions/workflows/lint.yml) -[![CodeQL](https://img.shields.io/github/actions/workflow/status/vhrabar/SecureEye/CodeQL.yml?style=for-the-badge&label=codeql&logo=github)](https://github.com/vhrabar/SecureEye/actions/workflows/CodeQL.yml) +[![Copr build status](https://copr.fedorainfracloud.org/coprs/vhrabar/SecureEye/package/secure-eye/status_image/last_build.png)](https://copr.fedorainfracloud.org/coprs/vhrabar/SecureEye/package/secure-eye/) [![Latest release](https://img.shields.io/github/v/release/vhrabar/SecureEye?include_prereleases&sort=semver&style=for-the-badge&logo=github)](https://github.com/vhrabar/SecureEye/releases) [![Status: Alpha](https://img.shields.io/badge/status-alpha-orange?style=for-the-badge&logo=semver&logoColor=white)](#) @@ -53,6 +52,8 @@ These mirror the package `Build-Depends` in `secureEye/debian/control`: #### Install Dependencies +On **Debian / Ubuntu**: + ```bash sudo apt-get update && sudo apt-get install -y \ meson ninja-build pkg-config build-essential \ @@ -60,6 +61,13 @@ sudo apt-get update && sudo apt-get install -y \ libpam0g-dev libinih-dev libevdev-dev ``` +On **Arch Linux** (`libinih` provides `INIReader`, `pam` provides the PAM headers): + +```bash +sudo pacman -S --needed base-devel meson ninja pkgconf \ + python python-pip pam libinih libevdev +``` + #### Build ```bash @@ -68,13 +76,14 @@ meson compile -C build ``` > [!WARNING] -> Do **not** run `meson install` on a machine where you also use the `.deb` -> packages. Meson's default prefix is `/usr/local`, and `/usr/local/lib/...` -> shadows the packaged `/usr/lib/...` systemd unit (and `/usr/local/bin` shadows -> `/usr/bin`), which breaks the daemon and CLI. A bare `meson install` also does -> **not** create the recognition virtualenv, that is built by the -> `secureeye-authd` package at install time, so the daemon will not start. -> For a working system install, build, and install the Debian packages below. +> Do **not** run `meson install` on a machine where you also use the packaged +> builds (`.deb`, `.rpm` or the AUR package). Meson's default prefix is +> `/usr/local`, and `/usr/local/lib/...` shadows the packaged `/usr/lib/...` +> systemd unit (and `/usr/local/bin` shadows `/usr/bin`), which breaks the +> daemon and CLI. On Debian and Fedora a bare `meson install` also does **not** +> create the recognition virtualenv, that is built by the `secureeye-authd` +> package at install time, so the daemon will not start. For a working system +> install, build and install your distribution's packages below. ### Debian / Ubuntu & derivatives @@ -141,6 +150,50 @@ sudo dnf install ./libpam-secureeye-*.rpm ./secureeye-authd-*.rpm ./secure-eye-* > enabled automatically. Enable it as shown in **Usage step 3b** below (the > Debian-only `pam-auth-update` / `common-auth` steps do not apply). +### Arch Linux & derivatives + +SecureEye is packaged for the AUR as `secureeye`, which builds two packages: + +- `libpam-secureeye`: the C/C++ PAM module (no Python) +- `secureeye-authd`: the authentication daemon and Python recognition runtime + +There is no transitional metapackage; install both. With an AUR helper: + +```bash +paru -S libpam-secureeye secureeye-authd # or: yay -S ... +``` + +Or manually with `makepkg` (the recognition dependencies `python-mediapipe` +and `python-sounddevice` also come from the AUR and must be built first): + +```bash +git clone https://aur.archlinux.org/secureeye.git +cd secureeye +makepkg -si +``` + +You can also build straight from a checkout of this repository: + +```bash +cd secureEye/archlinux/secureEye +makepkg -si +``` + +Unlike the `.deb`/`.rpm` packages, the Arch build does **not** bundle a recognition virtualenv: every dependency is a +real package and the daemon runs on the system interpreter, so there is nothing to rebuild after a Python upgrade. On +`aarch64` there is no MediaPipe, so the package depends on +`python-dlib` and ships `detector_backend = dlib` in the default config. + +Following Arch policy, the service is **not** started for you: + +```bash +sudo systemctl enable --now secureeye-authd.service +``` + +> [!NOTE] +> Arch has neither `pam-auth-update` nor `authselect`, so the PAM module is +> **not** enabled automatically. Enable it as shown in **Usage step 3c** below. + --- ## Usage @@ -192,6 +245,19 @@ auth sufficient pam_secureEye.so Do **not** edit `/etc/pam.d/system-auth` directly as authselect overwrites it. +**3c) Arch Linux.** There is no `pam-auth-update` and no `authselect`; edit the PAM stack yourself. For a single service +(recommended, e.g. `sudo` only), add this as the **first** `auth` line of `/etc/pam.d/sudo`: + +``` +auth sufficient pam_secureEye.so +``` + +To cover every service that includes it (login, `sudo`, display-manager greeters, `polkit`), add the same line at the +top of the `auth` section of +`/etc/pam.d/system-auth` instead. That file belongs to the `pam` package, so back it up and re-apply your change when +pacman leaves a `system-auth.pacnew` +after an upgrade. + **4. Try it.** Open a new terminal and run `sudo -i` — you should be able to authenticate by showing your face. If face auth fails or times out, SecureEye falls back to your password. Please check