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 <mihai@fufexan.net>
This commit is contained in:
moggiesir 2024-10-21 17:08:24 -07:00 committed by GitHub
parent b808086286
commit f48540fcd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 347 additions and 9 deletions

View file

@ -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})

View file

@ -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
];

View file

@ -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=";
};
});
};
}

View file

@ -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{""});

240
src/core/Fingerprint.cpp Normal file
View file

@ -0,0 +1,240 @@
#include "Fingerprint.hpp"
#include "../helpers/Log.hpp"
#include "../config/ConfigManager.hpp"
#include <filesystem>
#include <unistd.h>
#include <pwd.h>
#include <cstring>
#include <thread>
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<std::string, MatchResult> 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<sdbus::IConnection> 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<std::string> 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<bool>();
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<std::string, sdbus::Variant>& properties) {
if (interface != DEVICE || m_sDBUSState.done)
return;
try {
const auto presentVariant = properties.at("finger-present");
bool isPresent = presentVariant.get<bool>();
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;
}

51
src/core/Fingerprint.hpp Normal file
View file

@ -0,0 +1,51 @@
#pragma once
#include "hyprlock.hpp"
#include <memory>
#include <optional>
#include <string>
#include <sdbus-c++/sdbus-c++.h>
class CFingerprint {
public:
CFingerprint();
std::shared_ptr<sdbus::IConnection> start();
bool isAuthenticated();
std::optional<std::string> getLastMessage();
void terminate();
private:
struct SDBUSState {
std::string message = "";
std::shared_ptr<sdbus::IConnection> connection;
std::unique_ptr<sdbus::IProxy> login;
std::unique_ptr<sdbus::IProxy> 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<CFingerprint> g_pFingerprint;

View file

@ -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 <sys/wait.h>
#include <sys/poll.h>
@ -17,6 +18,7 @@
#include <filesystem>
#include <fstream>
#include <algorithm>
#include <sdbus-c++/sdbus-c++.h>
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<CAuth>();
g_pAuth->start();
g_pFingerprint = std::make_unique<CFingerprint>();
std::shared_ptr<sdbus::IConnection> 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();

View file

@ -2,6 +2,7 @@
#include "../../helpers/Log.hpp"
#include "../../core/hyprlock.hpp"
#include "../../core/Auth.hpp"
#include "../../core/Fingerprint.hpp"
#include <chrono>
#include <unistd.h>
#include <pwd.h>
@ -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);