diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..2742f25 --- /dev/null +++ b/.clangd @@ -0,0 +1,4 @@ +CompileFlags: + CompilationDatabase: "." + Add: [-std=gnu++17] + Compiler: /usr/bin/c++ diff --git a/CMakeLists.txt b/CMakeLists.txt index 29a8352..6ed6a9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,7 +167,7 @@ else() googletest URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip URL_HASH SHA256=1f357c27ca988c3f7c6b4bf68a9395005ac6761f034046e9dde0896e3aba00e4 - ${GTEST_DOWNLOAD_ARGS} + DOWNLOAD_EXTRACT_TIMESTAMP TRUE ) FetchContent_MakeAvailable(googletest) endif() @@ -254,7 +254,7 @@ set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") set(CPACK_GENERATOR "DEB") # Runtime dependencies (not -dev packages) for Ubuntu 24.04 # OpenCV is statically linked, so we don't need libopencv-* packages -set(CPACK_DEBIAN_PACKAGE_DEPENDS "libpam0g, libpam-runtime, v4l-utils, systemd, libgfortran5, libatlas3-base, libatomic1") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libpam0g, libpam-runtime, v4l-utils, libgfortran5, libatlas3-base, libatomic1") set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "rocm-opencl-runtime | intel-opencl-icd | nvidia-opencl-icd-340 | nvidia-opencl-icd") set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_SOURCE_DIR}/debian/postinst") diff --git a/Makefile b/Makefile index 237877d..9568ce1 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ deb: # Docker Build Targets docker-amd64: @echo "Building Docker (amd64)..." - @docker build --platform linux/amd64 -t linuxcampam:amd64 . + @docker build --platform linux/amd64 -t linuxcampam:amd64 -f docker/Dockerfile.amd64 . @echo "Verifying..." @docker run --rm --platform linux/amd64 --entrypoint /usr/bin/linuxcampam linuxcampam:amd64 help diff --git a/README.md b/README.md index cbaff1f..0fe9a20 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,15 @@ > Face unlock for Linux, like Windows Helloâ„¢. Any webcam works; IR recommended for best results. -LinuxCamPAM provides seamless face unlock for Linux `sudo`, `login`, and login/lock screens (GDM, SDDM, LightDM) using OpenCV and AI models (YuNet/SFace). I built it to solve my own need for speed and reliability, supporting hardware acceleration (OpenCL, CUDA) and smart dual-camera configurations (IR + RGB). Virtually any USB webcam works out of the box. +LinuxCamPAM provides seamless face unlock for Linux `sudo`, `login`, and login/lock screens (GDM, SDDM, LightDM) using OpenCV and AI models (YuNet/SFace). I built it to solve my own need for speed and reliability, supporting hardware acceleration (OpenCL) and smart dual-camera configurations (IR + RGB). Virtually any USB webcam works out of the box. + +## Why This Exists Now + +There are existing tools like `howdy`, but they often suffer from being slow, lacking hardware acceleration, or failing completely with modern Python environments and D-Bus integration under Wayland. + +- **Why OpenCV C++**: It runs instantly. No lag when typing `sudo`. +- **Why ONNX + YuNet + SFace**: Replaces the sluggish `dlib` backend for state-of-the-art face detection and recognition. +- **Native Backends**: Native OpenVINO is not enabled in the default static build to maintain a small package size (~15MB vs ~300MB+). OpenCL provides near-native performance for this use case without the bloat. ## Motivation diff --git a/debian/changelog b/debian/changelog index 116150d..db397df 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,78 +1,78 @@ linuxcampam (0.9.7.2-1) unstable; urgency=medium -* Fix: Correctly parse [Auth] and [Capture] configuration sections -* Fix: Ensure documentation consistency regarding permissions and configuration -* Fix: Add preinst script to archive conflicting manual installations to improve update stability -* Hotfix release for v0.9.7 issues + * Fix: Correctly parse [Auth] and [Capture] configuration sections + * Fix: Ensure documentation consistency regarding permissions and configuration + * Fix: Add preinst script to archive conflicting manual installations to improve update stability + * Hotfix release for v0.9.7 issues -- Vladimir Orlinski Fri, 30 Jan 2026 00:20:00 +0100 linuxcampam (0.9.7-1) unstable; urgency=medium -* Bump version to 0.9.7 to match project version -* Refined build process and docker optimization -* Upgrade OpenCV to 4.12.0 (statically linked) -* Update Face Detection models (YuNet 2023mar) + * Bump version to 0.9.7 to match project version + * Refined build process and docker optimization + * Upgrade OpenCV to 4.12.0 (statically linked) + * Update Face Detection models (YuNet 2023mar) -- Vladimir Orlinski Thu, 29 Jan 2026 13:50:00 +0100 linuxcampam (0.9.6-1) unstable; urgency=medium -* Dynamic debug logging via CLI (`linuxcampam debug on/off`) -* Version command for CLI and daemon -* Fix missing systemd service file in debian package -* Fix missing PAM config file in debian package -* Remove bloated -dev dependencies from runtime + * Dynamic debug logging via CLI (`linuxcampam debug on/off`) + * Version command for CLI and daemon + * Fix missing systemd service file in debian package + * Fix missing PAM config file in debian package + * Remove bloated -dev dependencies from runtime -- Vladimir Orlinski Tue, 07 Jan 2026 13:30:00 +0100 linuxcampam (0.9.5-1) unstable; urgency=medium -* GPU sync option to prevent OpenCL hangs on some AMD GPUs -* Fix missing i386/riscv64 debs in releases -* Add .dockerignore for cleaner cross-arch builds -* Auto-clear OpenCL kernel cache on startup to prevent upgrade issues + * GPU sync option to prevent OpenCL hangs on some AMD GPUs + * Fix missing i386/riscv64 debs in releases + * Add .dockerignore for cleaner cross-arch builds + * Auto-clear OpenCL kernel cache on startup to prevent upgrade issues -- Vladimir Orlinski Mon, 06 Jan 2026 15:00:00 +0100 linuxcampam (0.9.4-1) unstable; urgency=medium -* Feat: Implement security rate-limiting and lockout mechanism to prevent brute-force attacks. -* Feat: Add `show-config` CLI command for runtime configuration inspection. -* Security: Add comprehensive regression tests for lockout logic. + * Feat: Implement security rate-limiting and lockout mechanism to prevent brute-force attacks. + * Feat: Add `show-config` CLI command for runtime configuration inspection. + * Security: Add comprehensive regression tests for lockout logic. -- Vladimir Orlinski Sun, 05 Jan 2026 10:56:00 +0100 linuxcampam (0.9.3.3-1) unstable; urgency=medium -* Fix: Include AI models in Docker-based cross-arch builds (i386, riscv64). -* Fix: Preserve versioned .deb filenames in release artifacts. -* Note: Version bumped to clean test the release workflow. + * Fix: Include AI models in Docker-based cross-arch builds (i386, riscv64). + * Fix: Preserve versioned .deb filenames in release artifacts. + * Note: Version bumped to clean test the release workflow. -- Vladimir Orlinski Fri, 03 Jan 2026 21:41:00 +0100 linuxcampam (0.9.3.1-1) unstable; urgency=medium -* Multi-arch support (AARCH64, i386, RISC-V). -* Update Dockerfiles for packaging. -* Secure release workflow. + * Multi-arch support (AARCH64, i386, RISC-V). + * Update Dockerfiles for packaging. + * Secure release workflow. -- Vladimir Orlinski Sat, 03 Jan 2026 02:20:00 +0100 linuxcampam (0.9.2-1) unstable; urgency=medium -* Portable camera detection via V4L2 enumeration (no hardcoded paths). -* Portable GPU detection via sysfs (lspci no longer required). -* Setup fails with clear error if no cameras detected. -* Better IR emitter script guidance for non-Rust systems. + * Portable camera detection via V4L2 enumeration (no hardcoded paths). + * Portable GPU detection via sysfs (lspci no longer required). + * Setup fails with clear error if no cameras detected. + * Better IR emitter script guidance for non-Rust systems. -- Vladimir Orlinski Sat, 28 Dec 2025 23:35:00 +0100 linuxcampam (1.0.0-1) unstable; urgency=medium -* Initial release. -* Bundled stable ONNX models (2022mar/2021dec). -* Feature: Automatic PAM configuration via pam-auth-update. -* Feature: Rusticl OpenCL support enabled by default. + * Initial release. + * Bundled stable ONNX models (2022mar/2021dec). + * Feature: Automatic PAM configuration via pam-auth-update. + * Feature: Rusticl OpenCL support enabled by default. -- Vladimir Orlinski Fri, 19 Dec 2025 20:10:00 +0100 diff --git a/debian/control b/debian/control index a5d1d2b..a1f3403 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,7 @@ Homepage: https://github.com/Vladush/LinuxCamPAM Package: linuxcampam Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, libpam0g, systemd, v4l-utils +Depends: ${shlibs:Depends}, ${misc:Depends}, libpam0g, libpam-runtime, v4l-utils Recommends: rocm-opencl-runtime | intel-opencl-icd | nvidia-opencl-icd-340 | nvidia-opencl-icd Description: Face Authentication PAM Module (LinuxCamPAM) LinuxCamPAM is a Pluggable Authentication Module (PAM) that provides face diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64 index 99914cb..52eaf4a 100644 --- a/docker/Dockerfile.aarch64 +++ b/docker/Dockerfile.aarch64 @@ -15,3 +15,4 @@ RUN dpkg-buildpackage -b -uc -us # Verify man pages are in the generated deb RUN dpkg -c ../linuxcampam_*.deb | grep share/man +RUN apt-get update && apt-get install -y ../linuxcampam_*.deb diff --git a/docker/Dockerfile.riscv64 b/docker/Dockerfile.riscv64 index 7c53411..c8fcc88 100644 --- a/docker/Dockerfile.riscv64 +++ b/docker/Dockerfile.riscv64 @@ -73,11 +73,10 @@ FROM debian:sid-slim ENV DEBIAN_FRONTEND=noninteractive # Install Runtime Dependencies -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ libpam0g \ libpam-runtime \ v4l-utils \ - systemd \ libgfortran5 \ libgomp1 \ libatomic1 \ diff --git a/man/linuxcampam.conf.5.md b/man/linuxcampam.conf.5.md index cb891c6..bc5df7a 100644 --- a/man/linuxcampam.conf.5.md +++ b/man/linuxcampam.conf.5.md @@ -57,7 +57,7 @@ The file is in standard INI format. Sections and keys are case-insensitive. **provider_priority** = *LIST* : Comma-separated list of hardware backends to try. - (Default: `OpenCL,OpenVINO,CUDA,CPU`). + (Default: `OpenCL,CPU`). **camera_path_ir** = *PATH* : Path to IR camera (e.g., `/dev/video2`). diff --git a/scripts/install_ir_emitter.sh b/scripts/install_ir_emitter.sh index 5d88c27..5399e20 100755 --- a/scripts/install_ir_emitter.sh +++ b/scripts/install_ir_emitter.sh @@ -135,7 +135,12 @@ if [ -f "Cargo.toml" ]; then echo " sudo $0 $LATEST_TAG" echo "" - echo "=== Installation Complete ===" + IRE_VERSION=$(linux-enable-ir-emitter -V 2>/dev/null || true) + if [ -n "$IRE_VERSION" ]; then + echo "=== Installation Complete ($IRE_VERSION) ===" + else + echo "=== Installation Complete ===" + fi exit 0 fi @@ -158,5 +163,9 @@ else ninja -C build ninja -C build install fi - -echo "=== Installation Complete ===" +IRE_VERSION=$(linux-enable-ir-emitter -V 2>/dev/null || true) +if [ -n "$IRE_VERSION" ]; then + echo "=== Installation Complete ($IRE_VERSION) ===" +else + echo "=== Installation Complete ===" +fi diff --git a/scripts/setup_config.sh b/scripts/setup_config.sh index 1ee48d1..528b478 100755 --- a/scripts/setup_config.sh +++ b/scripts/setup_config.sh @@ -71,7 +71,8 @@ if [ -n "$IR_CAM" ]; then chmod +x /tmp/install_ir_emitter.sh /tmp/install_ir_emitter.sh echo "" - echo "[Setup] linux-enable-ir-emitter installed successfully." + IRE_VERSION=$(linux-enable-ir-emitter -V 2>/dev/null || echo "linux-enable-ir-emitter") + echo "[Setup] $IRE_VERSION installed successfully." read -p "[Setup] Would you like to configure the IR emitter now? [Y/n] " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then @@ -127,20 +128,29 @@ elif [ -n "$RGB_CAM" ]; then else echo "" echo "==============================================" - echo "[Setup] FATAL: No cameras detected!" - echo "==============================================" - echo "" - echo "LinuxCamPAM requires at least one camera to function." - echo "" - echo "Troubleshooting:" - echo " 1. Check if cameras are connected: ls -la /dev/video*" - echo " 2. Check camera details: v4l2-ctl --list-devices" - echo " 3. Verify permissions: groups \$USER | grep video" - echo "" - echo "If you have a camera but it wasn't detected, please" - echo "manually configure it in: $CONFIG_FILE" - echo "" - exit 1 + if [ ! -t 0 ] || [ "$DEBIAN_FRONTEND" = "noninteractive" ]; then + echo "[Setup] WARNING: No cameras detected!" + echo "==============================================" + echo "" + echo "Running non-interactively. Ignoring missing camera to continue setup." + echo "Please manually configure cameras later in: $CONFIG_FILE" + echo "" + else + echo "[Setup] FATAL: No cameras detected!" + echo "==============================================" + echo "" + echo "LinuxCamPAM requires at least one camera to function." + echo "" + echo "Troubleshooting:" + echo " 1. Check if cameras are connected: ls -la /dev/video*" + echo " 2. Check camera details: v4l2-ctl --list-devices" + echo " 3. Verify permissions: groups \$USER | grep video" + echo "" + echo "If you have a camera but it wasn't detected, please" + echo "manually configure it in: $CONFIG_FILE" + echo "" + exit 1 + fi fi echo "[Setup] Checking System RAM and CPU..." diff --git a/src/service/auth_engine.cpp b/src/service/auth_engine.cpp index 3749eb5..80da82a 100644 --- a/src/service/auth_engine.cpp +++ b/src/service/auth_engine.cpp @@ -97,15 +97,26 @@ bool AuthEngine::init(const fs::path &config_path) { void AuthEngine::initializeActiveCameras() { if (active_cameras.empty()) { active_cameras.reserve(config.camera_defs.size()); - std::transform(config.camera_defs.begin(), config.camera_defs.end(), - std::back_inserter(active_cameras), [&](const auto &def) { - ActiveCamera ac; - ac.config = def; // Copy config - log_info("Initializing Camera: " + def.id + " (" + - def.type + ") at " + def.path); - ac.cam = camera_factory_(def); - return ac; - }); + std::transform( + config.camera_defs.begin(), config.camera_defs.end(), + std::back_inserter(active_cameras), [&](const auto &def) { + ActiveCamera ac; + ac.config = def; // Copy config + log_info("Initializing Camera: " + def.id + " (" + def.type + + ") at " + def.path); + if (def.type == "ir") { + std::string ir_ver = linuxcampam::getIREmitterVersion( + config.ir_emitter_path.string()); + if (!ir_ver.empty()) { + log_info("IR Emitter Engine: " + config.ir_emitter_path.string() + + " (" + ir_ver + ")"); + } else { + log_info("IR Emitter Engine: Not Installed"); + } + } + ac.cam = camera_factory_(def); + return ac; + }); } } @@ -118,22 +129,8 @@ bool AuthEngine::loadModels() { int backend_id = cv::dnn::DNN_BACKEND_OPENCV; int target_id = cv::dnn::DNN_TARGET_CPU; - for (const auto &prov : config.provider_priority) { - if (prov == "CUDA") { - /* - if (cv::cuda::getCudaEnabledDeviceCount() > 0) { - backend_id = cv::dnn::DNN_BACKEND_CUDA; - target_id = cv::dnn::DNN_TARGET_CUDA; - log_info("Selecting CUDA Backend."); - break; - } - */ - } else if (prov == "OpenVINO") { - backend_id = cv::dnn::DNN_BACKEND_INFERENCE_ENGINE; - target_id = cv::dnn::DNN_TARGET_CPU; - log_info("Selecting OpenVINO Backend."); - break; - } else if (prov == "OpenCL") { + for (std::string_view prov : config.provider_priority) { + if (prov == "OpenCL") { if (cv::ocl::haveOpenCL()) { cv::ocl::setUseOpenCL(true); backend_id = cv::dnn::DNN_BACKEND_OPENCV; @@ -142,13 +139,19 @@ bool AuthEngine::loadModels() { // Log the OpenCL device name for assurance cv::ocl::Device dev = cv::ocl::Device::getDefault(); log_info("Hardware Device: " + dev.name() + " " + dev.version()); + break; } else { - log_warn("OpenCL requested but not " - "detected. Falling back to CPU."); - backend_id = cv::dnn::DNN_BACKEND_OPENCV; - target_id = cv::dnn::DNN_TARGET_CPU; + log_warn("OpenCL requested but not detected. Trying next provider " + "fallback."); } + } else if (prov == "CPU") { + backend_id = cv::dnn::DNN_BACKEND_OPENCV; + target_id = cv::dnn::DNN_TARGET_CPU; + log_info("Selecting CPU Backend."); break; + } else { + log_warn("Unsupported or unrecognized backend requested: " + + std::string(prov)); } } @@ -1042,6 +1045,25 @@ void AuthEngine::recordAuthAttempt(const std::string &username, bool success) { } } +std::string AuthEngine::getActiveProvider() const { + for (std::string_view prov : config.provider_priority) { + if (prov == "OpenCL") { + if (cv::ocl::haveOpenCL()) { + return "OpenCL"; + } + } else if (prov == "CPU") { + return "CPU"; + } + } + return "CPU (Default)"; +} + std::string AuthEngine::getConfigString() const { - return config.toString(); + std::string config_output = config.toString(); + + std::string provider = getActiveProvider(); + config_output += + " Active Provider: " + (provider.empty() ? "None" : provider) + "\n\n"; + + return config_output; } diff --git a/src/service/auth_engine.hpp b/src/service/auth_engine.hpp index 53b1e28..344cb76 100644 --- a/src/service/auth_engine.hpp +++ b/src/service/auth_engine.hpp @@ -4,13 +4,16 @@ #include "icamera.hpp" #include +#include #include #include +#include #include #include #include #include #include +#include #include namespace fs = std::filesystem; @@ -68,6 +71,8 @@ class AuthEngine { // Config visibility [[nodiscard]] std::string getConfigString() const; + [[nodiscard]] const Configuration &getConfig() const { return config; } + [[nodiscard]] std::string getActiveProvider() const; // Allows to swap out the camera implementation (e.g., using a mock for // testing). diff --git a/src/service/config.cpp b/src/service/config.cpp index 80629dc..3509123 100644 --- a/src/service/config.cpp +++ b/src/service/config.cpp @@ -268,7 +268,7 @@ void Configuration::parse_ini_into_self( } if (provider_priority.empty()) { Logger::log(LogLevel::DEBUG, "Using defaults for provider_priority"); - provider_priority = {"OpenCL", "OpenVINO", "CUDA", "CPU"}; + provider_priority = {"OpenCL", "CPU"}; } for (const auto &p : provider_priority) { @@ -350,6 +350,16 @@ std::string Configuration::toString() const { if (!cam.enroll_hdr.empty()) { ss << " Enroll HDR: " << cam.enroll_hdr << "\n"; } + if (cam.type == "ir") { + std::string ir_ver = + linuxcampam::getIREmitterVersion(ir_emitter_path.string()); + if (!ir_ver.empty()) { + ss << " IR Emitter Path: " << ir_emitter_path.string() << "\n"; + ss << " IR Emitter Version: " << ir_ver << "\n"; + } else { + ss << " IR Emitter: Not Installed\n"; + } + } ss << "\n"; } diff --git a/src/service/main.cpp b/src/service/main.cpp index 23e78ad..3917b43 100644 --- a/src/service/main.cpp +++ b/src/service/main.cpp @@ -87,10 +87,14 @@ void handle_client(int client_fd, AuthEngine &engine) { break; } case Command::GET_VERSION: { + std::string ir_status = linuxcampam::getIREmitterVersion( + engine.getConfig().ir_emitter_path.string()); + std::string ir_append = + ir_status.empty() ? "" : " (IR Emitter: " + ir_status + ")"; #ifdef LINUXCAMPAM_VERSION - response = LINUXCAMPAM_VERSION; + response = std::string(LINUXCAMPAM_VERSION) + ir_append; #else - response = "Unknown"; + response = "Unknown" + ir_append; #endif break; } diff --git a/src/service/utils.cpp b/src/service/utils.cpp index 044afff..238ba61 100644 --- a/src/service/utils.cpp +++ b/src/service/utils.cpp @@ -1,7 +1,9 @@ #include "utils.hpp" -#include "constants.hpp" // For MAX_USERNAME_LENGTH +#include "constants.hpp" + #include +#include #include #include #include @@ -114,6 +116,28 @@ std::vector> enumerateCameras() { return enumerateCameras(backend); } +std::string getIREmitterVersion(std::string_view path) { + if (path.empty()) + return {}; + + std::string cmd = std::string(path) + " -V 2>/dev/null"; + // NOLINTNEXTLINE(cert-env33-c) + FILE *fp = popen(cmd.c_str(), "r"); + if (!fp) + return {}; + + std::string version; + constexpr size_t buf_size = 128; + std::array buf{}; + if (fgets(buf.data(), static_cast(buf.size()), fp)) { + version = buf.data(); + if (!version.empty() && version.back() == '\n') + version.pop_back(); + } + pclose(fp); + return version; +} + bool isValidUsername(std::string_view username) { if (username.empty() || username.length() > linuxcampam::MAX_USERNAME_LENGTH) return false; diff --git a/src/service/utils.hpp b/src/service/utils.hpp index 02d7c15..2a4758d 100644 --- a/src/service/utils.hpp +++ b/src/service/utils.hpp @@ -88,6 +88,9 @@ enumerateCameras(const ICameraBackend &backend); [[nodiscard]] std::vector> enumerateCameras(); +// Helpers +[[nodiscard]] std::string getIREmitterVersion(std::string_view path); + // Security Utils [[nodiscard]] bool isValidUsername(std::string_view username);