From f48540fcd4421f1227e6e793e54f3f04b9df61d1 Mon Sep 17 00:00:00 2001 From: moggiesir <21014913+moggiesir@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:08:24 -0700 Subject: [PATCH] auth: Support parallel fingerprint auth (#514) * auth: Support parallel fingerprint auth I chose to use Fprint's dbus interface directly rather than going through pam (which uses Fprint's dbus interface) due to poor handling of system sleep somewhere between fprintd and pam. When preparing for sleep, fprintd puts the device to sleep, which causes VerifyStatus to emit with verify-unknown-error, which normally should be responded to by calling both Device.StopVerify and Device.Release (and this is what pam does). Unfortunately, if you try to release the device when the system is preparing for sleep, you'll get an error that the device is busy and then you can't can't claim or release the device for 30 seconds. pam also has a max timeout for pam_fprintd.so of 99 seconds, and so if we used pam, we'd have to deal with the timeouts and keep restarting the auth conversation. gdm/gnome-session lock seems to get around these issues by having a shutter on top of the lock screen that you have to interact with first that gives gnome-session a trigger to start fingerprint auth. * nix/overlays: add sdbus overlay --------- Co-authored-by: Mihai Fufezan --- CMakeLists.txt | 3 +- nix/default.nix | 4 + nix/overlays.nix | 14 ++ src/config/ConfigManager.cpp | 3 + src/core/Fingerprint.cpp | 240 +++++++++++++++++++++++++++++++ src/core/Fingerprint.hpp | 51 +++++++ src/core/hyprlock.cpp | 34 +++-- src/renderer/widgets/IWidget.cpp | 7 + 8 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 src/core/Fingerprint.cpp create mode 100644 src/core/Fingerprint.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bf1773c..fae0801 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,7 +56,8 @@ pkg_check_modules( pangocairo libdrm gbm - hyprutils>=0.2.3) + hyprutils>=0.2.3 + sdbus-c++>=2.0.0) file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") add_executable(hyprlock ${SRCFILES}) diff --git a/nix/default.nix b/nix/default.nix index 1530619..adb23f2 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -15,6 +15,8 @@ hyprutils, pam, pango, + sdbus-cpp, + systemdLibs, wayland, wayland-protocols, wayland-scanner, @@ -45,6 +47,8 @@ stdenv.mkDerivation { hyprutils pam pango + sdbus-cpp + systemdLibs wayland wayland-protocols ]; diff --git a/nix/overlays.nix b/nix/overlays.nix index d4ec360..40c56f2 100644 --- a/nix/overlays.nix +++ b/nix/overlays.nix @@ -15,6 +15,7 @@ in { hyprlock = lib.composeManyExtensions [ inputs.hyprlang.overlays.default inputs.hyprutils.overlays.default + inputs.self.overlays.sdbuscpp (final: prev: { hyprlock = prev.callPackage ./default.nix { stdenv = prev.gcc13Stdenv; @@ -23,4 +24,17 @@ in { }; }) ]; + + sdbuscpp = final: prev: { + sdbus-cpp = prev.sdbus-cpp.overrideAttrs (self: super: { + version = "2.0.0"; + + src = final.fetchFromGitHub { + owner = "Kistler-group"; + repo = "sdbus-cpp"; + rev = "refs/tags/v${self.version}"; + hash = "sha256-W8V5FRhV3jtERMFrZ4gf30OpIQLYoj2yYGpnYOmH2+g="; + }; + }); + }; } diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index b570b06..0d4f5ba 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -50,6 +50,9 @@ void CConfigManager::init() { m_config.addConfigValue("general:immediate_render", Hyprlang::INT{0}); m_config.addConfigValue("general:pam_module", Hyprlang::STRING{"hyprlock"}); m_config.addConfigValue("general:fractional_scaling", Hyprlang::INT{2}); + m_config.addConfigValue("general:enable_fingerprint", Hyprlang::INT{0}); + m_config.addConfigValue("general:fingerprint_ready_message", Hyprlang::STRING{"(Scan fingerprint to unlock)"}); + m_config.addConfigValue("general:fingerprint_present_message", Hyprlang::STRING{"Scanning fingerprint"}); m_config.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""}); diff --git a/src/core/Fingerprint.cpp b/src/core/Fingerprint.cpp new file mode 100644 index 0000000..7e14c7a --- /dev/null +++ b/src/core/Fingerprint.cpp @@ -0,0 +1,240 @@ +#include "Fingerprint.hpp" +#include "../helpers/Log.hpp" +#include "../config/ConfigManager.hpp" + +#include +#include +#include + +#include +#include + +static const auto FPRINT = sdbus::ServiceName{"net.reactivated.Fprint"}; +static const auto DEVICE = sdbus::ServiceName{"net.reactivated.Fprint.Device"}; +static const auto MANAGER = sdbus::ServiceName{"net.reactivated.Fprint.Manager"}; +static const auto LOGIN_MANAGER = sdbus::ServiceName{"org.freedesktop.login1.Manager"}; +static const auto RETRY_MESSAGE = "Could not match fingerprint. Try again."; + +enum MatchResult { + MATCH_INVALID = 0, + MATCH_NO_MATCH, + MATCH_MATCHED, + MATCH_RETRY, + MATCH_SWIPE_TOO_SHORT, + MATCH_FINGER_NOT_CENTERED, + MATCH_REMOVE_AND_RETRY, + MATCH_DISCONNECTED, + MATCH_UNKNOWN_ERROR, +}; + +static std::map s_mapStringToTestType = {{"verify-no-match", MATCH_NO_MATCH}, + {"verify-match", MATCH_MATCHED}, + {"verify-retry-scan", MATCH_RETRY}, + {"verify-swipe-too-short", MATCH_SWIPE_TOO_SHORT}, + {"verify-finger-not-centered", MATCH_FINGER_NOT_CENTERED}, + {"verify-remove-and-retry", MATCH_REMOVE_AND_RETRY}, + {"verify-disconnected", MATCH_DISCONNECTED}, + {"verify-unknown-error", MATCH_UNKNOWN_ERROR}}; + +CFingerprint::CFingerprint() { + static auto* const PFINGERPRINTREADY = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:fingerprint_ready_message")); + m_sFingerprintReady = *PFINGERPRINTREADY; + static auto* const PFINGERPRINTPRESENT = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:fingerprint_present_message")); + m_sFingerprintPresent = *PFINGERPRINTPRESENT; + static auto* const PENABLEFINGERPRINT = (Hyprlang::INT* const*)g_pConfigManager->getValuePtr("general:enable_fingerprint"); + m_bEnabled = **PENABLEFINGERPRINT; +} + +std::shared_ptr CFingerprint::start() { + if (!m_bEnabled) + return {}; + m_sDBUSState.connection = sdbus::createSystemBusConnection(); + registerSleepHandler(); + + // When entering sleep, the wake signal will trigger startVerify(). + if (m_sDBUSState.sleeping) + return m_sDBUSState.connection; + + startVerify(); + + return m_sDBUSState.connection; +} + +bool CFingerprint::isAuthenticated() { + return m_bAuthenticated; +} + +std::optional CFingerprint::getLastMessage() { + return m_sDBUSState.message.empty() ? std::nullopt : std::optional(m_sDBUSState.message); +} + +void CFingerprint::terminate() { + if (!m_sDBUSState.abort) + releaseDevice(); +} + +void CFingerprint::registerSleepHandler() { + m_sDBUSState.login = sdbus::createProxy(*m_sDBUSState.connection, sdbus::ServiceName{"org.freedesktop.login1"}, sdbus::ObjectPath{"/org/freedesktop/login1"}); + m_sDBUSState.sleeping = m_sDBUSState.login->getProperty("PreparingForSleep").onInterface(LOGIN_MANAGER).get(); + m_sDBUSState.login->uponSignal("PrepareForSleep").onInterface(LOGIN_MANAGER).call([this](bool start) { + Debug::log(LOG, "fprint: PrepareForSleep (start: {})", start); + if (start) { + m_sDBUSState.sleeping = true; + stopVerify(); + m_sDBUSState.inhibitLock.reset(); + } else { + m_sDBUSState.sleeping = false; + inhibitSleep(); + startVerify(); + } + }); + if (!m_sDBUSState.sleeping) + inhibitSleep(); +} + +void CFingerprint::inhibitSleep() { + m_sDBUSState.login->callMethod("Inhibit") + .onInterface(LOGIN_MANAGER) + .withArguments("sleep", "hyprlock", "Fingerprint verifcation must be stopped before sleep", "delay") + .storeResultsTo(m_sDBUSState.inhibitLock); +} + +bool CFingerprint::createDeviceProxy() { + auto proxy = sdbus::createProxy(*m_sDBUSState.connection, FPRINT, sdbus::ObjectPath{"/net/reactivated/Fprint/Manager"}); + + sdbus::ObjectPath path; + try { + proxy->callMethod("GetDefaultDevice").onInterface(MANAGER).storeResultsTo(path); + } catch (sdbus::Error& e) { + Debug::log(WARN, "fprint: couldn't connect to Fprint service ({})", e.what()); + return false; + } + Debug::log(LOG, "fprint: using device path {}", path.c_str()); + m_sDBUSState.device = sdbus::createProxy(*m_sDBUSState.connection, FPRINT, path); + + m_sDBUSState.device->uponSignal("VerifyFingerSelected").onInterface(DEVICE).call([](const std::string& finger) { Debug::log(LOG, "fprint: finger selected: {}", finger); }); + m_sDBUSState.device->uponSignal("VerifyStatus").onInterface(DEVICE).call([this](const std::string& result, const bool done) { handleVerifyStatus(result, done); }); + + m_sDBUSState.device->uponSignal("PropertiesChanged") + .onInterface("org.freedesktop.DBus.Properties") + .call([this](const std::string& interface, const std::map& properties) { + if (interface != DEVICE || m_sDBUSState.done) + return; + + try { + const auto presentVariant = properties.at("finger-present"); + bool isPresent = presentVariant.get(); + if (!isPresent) + return; + m_sDBUSState.message = m_sFingerprintPresent; + g_pHyprlock->enqueueForceUpdateTimers(); + } catch (std::out_of_range& e) {} + }); + + return true; +} + +void CFingerprint::handleVerifyStatus(const std::string& result, bool done) { + Debug::log(LOG, "fprint: handling status {}", result); + auto matchResult = s_mapStringToTestType[result]; + if (m_sDBUSState.sleeping && matchResult != MATCH_DISCONNECTED) + return; + switch (matchResult) { + case MATCH_INVALID: Debug::log(WARN, "fprint: unknown status: {}", result); break; + case MATCH_NO_MATCH: + stopVerify(); + if (m_sDBUSState.retries >= 3) { + m_sDBUSState.message = "Fingerprint auth disabled: too many failed attempts"; + } else if (startVerify(/* showMessage= */ false)) { + done = false; + m_sDBUSState.retries++; + m_sDBUSState.message = RETRY_MESSAGE; + } else { + m_sDBUSState.message = "Fingerprint auth disabled: could not restart verification"; + } + break; + case MATCH_UNKNOWN_ERROR: + stopVerify(); + m_sDBUSState.message = "Unknown fingerprint error, disabling fingerprint auth"; + break; + case MATCH_MATCHED: + stopVerify(); + m_bAuthenticated = true; + m_sDBUSState.message = ""; + g_pHyprlock->unlock(); + break; + case MATCH_RETRY: m_sDBUSState.message = "Please retry fingerprint scan"; break; + case MATCH_SWIPE_TOO_SHORT: m_sDBUSState.message = "Swipe too short - try again"; break; + case MATCH_FINGER_NOT_CENTERED: m_sDBUSState.message = "Finger not centered - try again"; break; + case MATCH_REMOVE_AND_RETRY: m_sDBUSState.message = "Remove your finger and try again"; break; + case MATCH_DISCONNECTED: + m_sDBUSState.message = "Fingerprint device disconnected"; + m_sDBUSState.abort = true; + break; + } + g_pHyprlock->enqueueForceUpdateTimers(); + if (done || m_sDBUSState.abort) { + m_sDBUSState.done = true; + m_sDBUSState.connection->leaveEventLoop(); + } +} + +bool CFingerprint::claimDevice() { + try { + const auto currentUser = ""; // Empty string means use the caller's id. + m_sDBUSState.device->callMethod("Claim").onInterface(DEVICE).withArguments(currentUser); + } catch (sdbus::Error& e) { + Debug::log(WARN, "fprint: could not claim device, {}", e.what()); + return false; + } + Debug::log(LOG, "fprint: claimed device"); + return true; +} + +bool CFingerprint::startVerify(bool updateMessage) { + if (!m_sDBUSState.device) { + if (!createDeviceProxy()) + return false; + + claimDevice(); + } + try { + auto finger = "any"; // Any finger. + m_sDBUSState.device->callMethod("VerifyStart").onInterface(DEVICE).withArguments(finger); + } catch (sdbus::Error& e) { + Debug::log(WARN, "fprint: could not start verifying, {}", e.what()); + return false; + } + Debug::log(LOG, "fprint: started verifying"); + if (updateMessage) { + m_sDBUSState.message = m_sFingerprintReady; + g_pHyprlock->enqueueForceUpdateTimers(); + } + return true; +} + +bool CFingerprint::stopVerify() { + if (!m_sDBUSState.device) + return false; + try { + m_sDBUSState.device->callMethod("VerifyStop").onInterface(DEVICE); + } catch (sdbus::Error& e) { + Debug::log(WARN, "fprint: could not stop verifying, {}", e.what()); + return false; + } + Debug::log(LOG, "fprint: stopped verification"); + return true; +} + +bool CFingerprint::releaseDevice() { + if (!m_sDBUSState.device) + return false; + try { + m_sDBUSState.device->callMethod("Release").onInterface(DEVICE); + } catch (sdbus::Error& e) { + Debug::log(WARN, "fprint: could not release device, {}", e.what()); + return false; + } + Debug::log(LOG, "fprint: released device"); + return true; +} diff --git a/src/core/Fingerprint.hpp b/src/core/Fingerprint.hpp new file mode 100644 index 0000000..a463f94 --- /dev/null +++ b/src/core/Fingerprint.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "hyprlock.hpp" + +#include +#include +#include +#include + +class CFingerprint { + public: + CFingerprint(); + + std::shared_ptr start(); + bool isAuthenticated(); + std::optional getLastMessage(); + void terminate(); + + private: + struct SDBUSState { + std::string message = ""; + + std::shared_ptr connection; + std::unique_ptr login; + std::unique_ptr device; + sdbus::UnixFd inhibitLock; + + bool abort = false; + bool done = false; + int retries = 0; + bool sleeping = false; + } m_sDBUSState; + + std::string m_sFingerprintReady; + std::string m_sFingerprintPresent; + bool m_bAuthenticated = false; + bool m_bEnabled = false; + + void handleVerifyStatus(const std::string& result, const bool done); + + void registerSleepHandler(); + void inhibitSleep(); + + bool createDeviceProxy(); + bool claimDevice(); + bool startVerify(bool updateMessage = true); + bool stopVerify(); + bool releaseDevice(); +}; + +inline std::unique_ptr g_pFingerprint; diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index 8f31e4f..0431dfb 100644 --- a/src/core/hyprlock.cpp +++ b/src/core/hyprlock.cpp @@ -4,6 +4,7 @@ #include "../renderer/Renderer.hpp" #include "Auth.hpp" #include "Egl.hpp" +#include "Fingerprint.hpp" #include "linux-dmabuf-unstable-v1-protocol.h" #include #include @@ -17,6 +18,7 @@ #include #include #include +#include CHyprlock::CHyprlock(const std::string& wlDisplay, const bool immediate, const bool immediateRender, const bool noFadeIn) { m_sWaylandState.display = wl_display_connect(wlDisplay.empty() ? nullptr : wlDisplay.c_str()); @@ -417,6 +419,9 @@ void CHyprlock::run() { g_pAuth = std::make_unique(); g_pAuth->start(); + g_pFingerprint = std::make_unique(); + std::shared_ptr conn = g_pFingerprint->start(); + registerSignalAction(SIGUSR1, handleUnlockSignal, SA_RESTART); registerSignalAction(SIGUSR2, handleForceUpdateSignal); registerSignalAction(SIGRTMIN, handlePollTerminate); @@ -425,16 +430,22 @@ void CHyprlock::run() { createSessionLockSurfaces(); - pollfd pollfds[] = { - { - .fd = wl_display_get_fd(m_sWaylandState.display), - .events = POLLIN, - }, + pollfd pollfds[2]; + pollfds[0] = { + .fd = wl_display_get_fd(m_sWaylandState.display), + .events = POLLIN, }; + if (conn) { + pollfds[1] = { + .fd = conn->getEventLoopPollData().fd, + .events = POLLIN, + }; + } + size_t fdcount = conn ? 2 : 1; - std::thread pollThr([this, &pollfds]() { + std::thread pollThr([this, &pollfds, fdcount]() { while (!m_bTerminate) { - int ret = poll(pollfds, 1, 5000 /* 5 seconds, reasonable. Just in case we need to terminate and the signal fails */); + int ret = poll(pollfds, fdcount, 5000 /* 5 seconds, reasonable. Just in case we need to terminate and the signal fails */); if (ret < 0) { if (errno == EINTR) @@ -446,7 +457,7 @@ void CHyprlock::run() { exit(1); } - for (size_t i = 0; i < 1; ++i) { + for (size_t i = 0; i < fdcount; ++i) { if (pollfds[i].revents & POLLHUP) { Debug::log(CRIT, "[core] Disconnected from pollfd id {}", i); attemptRestoreOnDeath(); @@ -509,6 +520,12 @@ void CHyprlock::run() { m_sLoopState.event = false; + if (pollfds[1].revents & POLLIN /* dbus */) { + while (conn && conn->processPendingEvent()) { + ; + } + } + if (pollfds[0].revents & POLLIN /* wl */) { Debug::log(TRACE, "got wl event"); wl_display_flush(m_sWaylandState.display); @@ -572,6 +589,7 @@ void CHyprlock::run() { pthread_kill(pollThr.native_handle(), SIGRTMIN); g_pAuth->terminate(); + g_pFingerprint->terminate(); // wait for threads to exit cleanly to avoid a coredump pollThr.join(); diff --git a/src/renderer/widgets/IWidget.cpp b/src/renderer/widgets/IWidget.cpp index ef89ad4..21a49f2 100644 --- a/src/renderer/widgets/IWidget.cpp +++ b/src/renderer/widgets/IWidget.cpp @@ -2,6 +2,7 @@ #include "../../helpers/Log.hpp" #include "../../core/hyprlock.hpp" #include "../../core/Auth.hpp" +#include "../../core/Fingerprint.hpp" #include #include #include @@ -166,6 +167,12 @@ IWidget::SFormatResult IWidget::formatString(std::string in) { result.allowForceUpdate = true; } + if (in.contains("$FPRINTMESSAGE")) { + const auto FPRINTMESSAGE = g_pFingerprint->getLastMessage(); + replaceInString(in, "$FPRINTMESSAGE", FPRINTMESSAGE.has_value() ? FPRINTMESSAGE.value() : ""); + result.allowForceUpdate = true; + } + if (in.starts_with("cmd[") && in.contains("]")) { // this is a command CVarList vars(in.substr(4, in.find_first_of(']') - 4), 0, ',', true);