From 16932cfec1a081b24923e0005235af34804e7a3b Mon Sep 17 00:00:00 2001 From: moggiesir <21014913+moggiesir@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:11:29 -0700 Subject: [PATCH] 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. --- CMakeLists.txt | 3 +- src/config/ConfigManager.cpp | 3 + src/core/Fingerprint.cpp | 240 +++++++++++++++++++++++++++++++ src/core/Fingerprint.hpp | 71 +++++++++ src/core/hyprlock.cpp | 5 + src/renderer/widgets/IWidget.cpp | 7 + 6 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/core/Fingerprint.cpp create mode 100644 src/core/Fingerprint.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bf1773ce..ab087e8e 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++) file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") add_executable(hyprlock ${SRCFILES}) diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index b570b06e..25120ed5 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{""}); + m_config.addConfigValue("general:fingerprint_present_message", Hyprlang::STRING{""}); 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 00000000..e766a6a2 --- /dev/null +++ b/src/core/Fingerprint.cpp @@ -0,0 +1,240 @@ +#include "Fingerprint.hpp" +#include "../helpers/Log.hpp" +#include "src/config/ConfigManager.hpp" + +#include +#include +#include + +#include +#include + +static const auto FPRINT = "net.reactivated.Fprint"; +static const auto DEVICE = "net.reactivated.Fprint.Device"; +static const auto MANAGER = "net.reactivated.Fprint.Manager"; +static const auto LOGIN_MANAGER = "org.freedesktop.login1.Manager"; +static const auto RETRY_MESSAGE = "Could not match fingerprint. Try again."; + +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 void fingerprintUnlockCallback(std::shared_ptr self, void* data) { + g_pHyprlock->unlock(); +} + +void CFingerprint::start() { + static auto* const PENABLEFINGERPRINT = (Hyprlang::INT* const*)g_pConfigManager->getValuePtr("general:enable_fingerprint"); + const bool ENABLEFINGERPRINT = **PENABLEFINGERPRINT; + if (!ENABLEFINGERPRINT) { + return; + } + std::thread([this]() { + // For grace or SIGUSR1 unlocks + if (g_pHyprlock->isUnlocked()) + return; + + setupDBUS(); + + if (m_bAuthenticated) + g_pHyprlock->addTimer(std::chrono::milliseconds(1), fingerprintUnlockCallback, nullptr); + }).detach(); +} + +std::optional CFingerprint::getLastMessage() { + return m_sDBUSState.message.empty() ? std::nullopt : std::optional(m_sDBUSState.message); +} + +void CFingerprint::terminate() { + if (m_sDBUSState.connection) + m_sDBUSState.connection->leaveEventLoop(); +} + +bool CFingerprint::setupDBUS() { + m_sDBUSState.connection = sdbus::createSystemBusConnection(); + + registerSleepHandler(); + + if (m_sDBUSState.sleeping) { + m_sDBUSState.connection->enterEventLoop(); + } else if (startVerify()) { + m_sDBUSState.message = m_sFingerprintReady; + g_pHyprlock->enqueueForceUpdateTimers(); + m_sDBUSState.connection->enterEventLoop(); + } + + if (m_sDBUSState.abort) + return true; + + releaseDevice(); + return true; +} + +void CFingerprint::registerSleepHandler() { + m_sDBUSState.login = sdbus::createProxy(*m_sDBUSState.connection.get(), "org.freedesktop.login1", "/org/freedesktop/login1"); + + m_sDBUSState.sleeping = m_sDBUSState.login->getProperty("PreparingForSleep").onInterface(LOGIN_MANAGER); + m_sDBUSState.login->uponSignal("PrepareForSleep").onInterface(LOGIN_MANAGER).call([this](bool start) { + Debug::log(LOG, "prepare for sleep"); + if (start) { + m_sDBUSState.sleeping = true; + stopVerify(); + m_sDBUSState.inhibitLock.reset(); + } else { + m_sDBUSState.sleeping = false; + inhibitSleep(); + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (startVerify()) { + m_sDBUSState.message = m_sFingerprintReady; + g_pHyprlock->enqueueForceUpdateTimers(); + } + } + }); + m_sDBUSState.login->finishRegistration(); + if (!m_sDBUSState.sleeping) + inhibitSleep(); +} + +void CFingerprint::inhibitSleep() { + m_sDBUSState.login->callMethod("Inhibit") + .onInterface(LOGIN_MANAGER) + .withArguments("sleep", "hyprlock", "Release fingerprint before sleep", "delay") + .storeResultsTo(m_sDBUSState.inhibitLock); +} + +bool CFingerprint::createDeviceProxy() { + auto proxy = sdbus::createProxy(*m_sDBUSState.connection.get(), FPRINT, "/net/reactivated/Fprint/Manager"); + + sdbus::ObjectPath path; + try { + proxy->callMethod("GetDefaultDevice").onInterface(MANAGER).storeResultsTo(path); + } catch (sdbus::Error& e) { + Debug::log(WARN, "Couldn't connect to Fprint service ({})", e.what()); + return false; + } + Debug::log(LOG, "Using dbus path {}", path.c_str()); + m_sDBUSState.device = sdbus::createProxy(*m_sDBUSState.connection.get(), FPRINT, path); + + m_sDBUSState.device->uponSignal("VerifyFingerSelected").onInterface(DEVICE).call([](const std::string& finger) { Debug::log(LOG, "Finger: {}", 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.failed) + 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) {} + }); + + m_sDBUSState.device->finishRegistration(); + return true; +} + +void CFingerprint::handleVerifyStatus(const std::string& result, bool done) { + Debug::log(LOG, "Status: {} {}", done, result); + auto matchResult = s_mapStringToTestType[result]; + if (m_sDBUSState.sleeping && matchResult != MatchResult::Disconnected) { + return; + } + switch (matchResult) { + case MatchResult::NoMatch: + stopVerify(); + if (m_sDBUSState.retries < 2) { + startVerify(); + done = false; + m_sDBUSState.retries++; + m_sDBUSState.message = RETRY_MESSAGE; + } else { + m_sDBUSState.failed = true; + m_sDBUSState.message = "Fingerprint auth disabled: too many failed attempts."; + } + break; + case MatchResult::UnknownError: + stopVerify(); + m_sDBUSState.message = "Unknown fingerprint error."; + break; + case MatchResult::Match: + stopVerify(); + m_bAuthenticated = true; + m_sDBUSState.message = ""; + Debug::log(LOG, "matched"); + break; + case MatchResult::Retry: m_sDBUSState.message = "Please retry fingerprint scan."; break; + case MatchResult::SwipeTooShort: m_sDBUSState.message = "Swipe too short - try again."; break; + case MatchResult::FingerNotCentered: m_sDBUSState.message = "Finger not centered - try again."; break; + case MatchResult::RemoveAndRetry: m_sDBUSState.message = "Remove your finger and try again."; break; + case MatchResult::Disconnected: m_sDBUSState.abort = true; break; + } + g_pHyprlock->enqueueForceUpdateTimers(); + if (done || m_sDBUSState.abort) { + Debug::log(LOG, "leaving dbus loop"); + 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, "could not claim device, {}", e.what()); + return false; + } + Debug::log(LOG, "Claimed device"); + return true; +} + +bool CFingerprint::startVerify() { + 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, "Could not start verifying, {}", e.what()); + return false; + } + Debug::log(LOG, "started verifying"); + 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, "could not stop verifying, {}", e.what()); + return false; + } + Debug::log(LOG, "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, "could not release device, {}", e.what()); + return false; + } + Debug::log(LOG, "Released device"); + return true; +} diff --git a/src/core/Fingerprint.hpp b/src/core/Fingerprint.hpp new file mode 100644 index 00000000..89f74dc1 --- /dev/null +++ b/src/core/Fingerprint.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "hyprlock.hpp" + +#include +#include +#include +#include +#include + +class CFingerprint { + public: + CFingerprint(); + + void start(); + + std::optional getLastMessage(); + + void terminate(); + bool setupDBUS(); + + private: + enum class MatchResult { + NoMatch, + Match, + Retry, + SwipeTooShort, + FingerNotCentered, + RemoveAndRetry, + Disconnected, + UnknownError, + }; + struct SDBUSState { + std::string message = ""; + + std::unique_ptr connection; + std::unique_ptr login; + std::unique_ptr device; + sdbus::UnixFd inhibitLock; + + bool abort = false; + bool failed = false; + int retries = 0; + bool sleeping = false; + } m_sDBUSState; + + std::string m_sFingerprintReady; + std::string m_sFingerprintPresent; + bool m_bAuthenticated = false; + + std::map s_mapStringToTestType = {{"verify-no-match", MatchResult::NoMatch}, + {"verify-match", MatchResult::Match}, + {"verify-retry-scan", MatchResult::Retry}, + {"verify-swipe-too-short", MatchResult::SwipeTooShort}, + {"verify-finger-not-centered", MatchResult::FingerNotCentered}, + {"verify-remove-and-retry", MatchResult::RemoveAndRetry}, + {"verify-disconnected", MatchResult::Disconnected}, + {"verify-unknown-error", MatchResult::UnknownError}}; + void handleVerifyStatus(const std::string& result, const bool done); + + void registerSleepHandler(); + void inhibitSleep(); + + bool createDeviceProxy(); + bool claimDevice(); + bool startVerify(); + bool stopVerify(); + bool releaseDevice(); +}; + +inline std::unique_ptr g_pFingerprint; diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index 8f31e4fc..62d6e96a 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 @@ -417,6 +418,9 @@ void CHyprlock::run() { g_pAuth = std::make_unique(); g_pAuth->start(); + g_pFingerprint = std::make_unique(); + g_pFingerprint->start(); + registerSignalAction(SIGUSR1, handleUnlockSignal, SA_RESTART); registerSignalAction(SIGUSR2, handleForceUpdateSignal); registerSignalAction(SIGRTMIN, handlePollTerminate); @@ -572,6 +576,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 ef89ad4c..21a49f28 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);