Skip to content

Commit

Permalink
auth: Support parallel fingerprint auth
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
moggiesir committed Oct 12, 2024
1 parent 7362ce3 commit 91b25f7
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ pkg_check_modules(
pangocairo
libdrm
gbm
hyprutils>=0.2.0)
hyprutils>=0.2.0
sdbus-c++)

file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
add_executable(hyprlock ${SRCFILES})
Expand Down
1 change: 1 addition & 0 deletions src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ 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.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true});
m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""});
Expand Down
237 changes: 237 additions & 0 deletions src/core/Fingerprint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#include "Fingerprint.hpp"
#include "../helpers/Log.hpp"
#include "src/config/ConfigManager.hpp"

#include <filesystem>
#include <unistd.h>
#include <pwd.h>
#include <security/pam_appl.h>
#if __has_include(<security/pam_misc.h>)
#include <security/pam_misc.h>
#endif

#include <cstring>
#include <thread>

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 void fingerprintCheckTimerCallback(std::shared_ptr<CTimer> 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();

g_pHyprlock->addTimer(std::chrono::milliseconds(1), fingerprintCheckTimerCallback, nullptr);
}).detach();
}

bool CFingerprint::isAuthenticated() {
return m_bAuthenticated;
}

std::optional<std::string> CFingerprint::getLastFailText() {
return m_sDBUSState.failText.empty() ? std::nullopt : std::optional(m_sDBUSState.failText);
}

void CFingerprint::terminate() {
m_sDBUSState.connection->leaveEventLoop();
}

bool CFingerprint::setupDBUS() {
Debug::log(LOG, "{}", m_sDBUSState.sleeping);
m_sDBUSState.connection = sdbus::createSystemBusConnection();

registerSleepHandler();

bool preparingForSleep = m_sDBUSState.login->getProperty("PreparingForSleep").onInterface(LOGIN_MANAGER);
if (preparingForSleep) {
m_sDBUSState.sleeping = true;
Debug::log(LOG, "Preparing for sleep");
} else {
m_sDBUSState.sleeping = false;
}

if (preparingForSleep || !startVerify()) {
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.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));
startVerify();
}
});
m_sDBUSState.login->finishRegistration();
inhibitSleep();
}

void CFingerprint::inhibitSleep() {
m_sDBUSState.login->callMethod("Inhibit")
.onInterface(LOGIN_MANAGER)
.withArguments("sleep", "hyprlock", "Release fingerprint before sleep", "delay")
.storeResultsTo(m_sDBUSState.inhibitLock);
}

std::optional<sdbus::Error> 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 e;
}
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->finishRegistration();
return {};
}

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;
}
std::unique_lock<std::mutex> lk(m_sDBUSState.failTextMutex);
switch (matchResult) {
case MatchResult::NoMatch: //The verification did not match, Device.VerifyStop should now be called.
stopVerify();
if (m_sDBUSState.retries < 3) {
startVerify();
done = false;
m_sDBUSState.retries++;
m_sDBUSState.failText = RETRY_MESSAGE;
if (clearRetryTextTimer) clearRetryTextTimer->cancel();
clearRetryTextTimer = g_pHyprlock->addTimer(
std::chrono::seconds(2),
[this](std::shared_ptr<CTimer>, void* data) {
std::unique_lock<std::mutex> lk(m_sDBUSState.failTextMutex);
if (m_sDBUSState.failText == RETRY_MESSAGE) {
m_sDBUSState.failText = "";
g_pHyprlock->enqueueForceUpdateTimers();
}
},
nullptr);
} else {
m_sDBUSState.failText = "Fingerprint auth disabled: too many failed attempts.";
}
break;
case MatchResult::UnknownError:
stopVerify();
m_sDBUSState.failText = "Unknown fingerprint error.";
break;
case MatchResult::Match:
stopVerify();
m_bAuthenticated = true;
m_sDBUSState.failText = "";
Debug::log(LOG, "matched");
break;
case MatchResult::Retry: m_sDBUSState.failText = "Please retry fingerprint scan."; break;
case MatchResult::SwipeTooShort: m_sDBUSState.failText = "Swipe too short - try again."; break;
case MatchResult::FingerNotCentered: m_sDBUSState.failText = "Finger not centered - try again."; break;
case MatchResult::RemoveAndRetry: m_sDBUSState.failText = "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();
}
}

std::optional<sdbus::Error> 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 e;
}
Debug::log(LOG, "Claimed device");
return {};
}

std::optional<sdbus::Error> CFingerprint::startVerify() {
if (!m_sDBUSState.device) {
auto result = createDeviceProxy();
if (result.has_value())
return result;

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 e;
}
Debug::log(LOG, "started verifying");
return {};
}

std::optional<sdbus::Error> CFingerprint::stopVerify() {
if (!m_sDBUSState.device)
return {};
try {
m_sDBUSState.device->callMethod("VerifyStop").onInterface(DEVICE);
} catch (sdbus::Error& e) {
Debug::log(WARN, "could not stop verifying, {}", e.what());
return e;
}
Debug::log(LOG, "Stopped verification");
return {};
}

std::optional<sdbus::Error> CFingerprint::releaseDevice() {
if (!m_sDBUSState.device)
return {};
try {
m_sDBUSState.device->callMethod("Release").onInterface(DEVICE);
} catch (sdbus::Error& e) {
Debug::log(WARN, "could not release device, {}", e.what());
return e;
}
Debug::log(LOG, "Released device");
return {};
}
73 changes: 73 additions & 0 deletions src/core/Fingerprint.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#pragma once

#include "hyprlock.hpp"

#include <memory>
#include <optional>
#include <string>
#include <mutex>
#include <condition_variable>
#include <sdbus-c++/sdbus-c++.h>

class CFingerprint {
public:
CFingerprint();

void start();
bool isAuthenticated();

std::optional<std::string> getLastFailText();

void terminate();
bool setupDBUS();

private:
enum class MatchResult {
NoMatch,
Match,
Retry,
SwipeTooShort,
FingerNotCentered,
RemoveAndRetry,
Disconnected,
UnknownError,
};
struct SDBUSState {
std::string failText = "";

std::mutex failTextMutex;

std::unique_ptr<sdbus::IConnection> connection;
std::unique_ptr<sdbus::IProxy> login;
std::unique_ptr<sdbus::IProxy> device;
sdbus::UnixFd inhibitLock;

bool abort = false;
int retries = 0;
bool sleeping = false;
} m_sDBUSState;

bool m_bAuthenticated = false;
std::shared_ptr<CTimer> clearRetryTextTimer;

std::map<std::string, MatchResult> 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();

std::optional<sdbus::Error> createDeviceProxy();
std::optional<sdbus::Error> claimDevice();
std::optional<sdbus::Error> startVerify();
std::optional<sdbus::Error> stopVerify();
std::optional<sdbus::Error> releaseDevice();
};

inline std::unique_ptr<CFingerprint> g_pFingerprint;
7 changes: 6 additions & 1 deletion src/core/hyprlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -417,6 +418,9 @@ void CHyprlock::run() {
g_pAuth = std::make_unique<CAuth>();
g_pAuth->start();

g_pFingerprint = std::make_unique<CFingerprint>();
g_pFingerprint->start();

registerSignalAction(SIGUSR1, handleUnlockSignal, SA_RESTART);
registerSignalAction(SIGUSR2, handleForceUpdateSignal);
registerSignalAction(SIGRTMIN, handlePollTerminate);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -783,7 +788,7 @@ static const ext_session_lock_v1_listener sessionLockListener = {

void CHyprlock::onPasswordCheckTimer() {
// check result
if (g_pAuth->isAuthenticated()) {
if (g_pAuth->isAuthenticated() || g_pFingerprint->isAuthenticated()) {
unlock();
} else {
m_sPasswordState.passBuffer = "";
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/widgets/IWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "../../helpers/Log.hpp"
#include "../../core/hyprlock.hpp"
#include "src/core/Auth.hpp"
#include "src/core/Fingerprint.hpp"
#include <chrono>
#include <unistd.h>
#include <pwd.h>
Expand Down Expand Up @@ -160,6 +161,12 @@ IWidget::SFormatResult IWidget::formatString(std::string in) {
result.allowForceUpdate = true;
}

if (in.contains("$FPRINTMESSAGE")) {
const auto FPRINTMESSAGE = g_pFingerprint->getLastFailText();
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);
Expand Down

0 comments on commit 91b25f7

Please sign in to comment.