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 14, 2024
1 parent 1169452 commit 3d5fd21
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 1 deletion.
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.3)
hyprutils>=0.2.3
sdbus-c++)

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

#include <filesystem>
#include <unistd.h>
#include <pwd.h>

#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 auto* const PFINGERPRINTREADY = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:fingerprint_ready_message"));
m_sFingerprintReady = *PFINGERPRINTREADY;
}

static void fingerprintUnlockCallback(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();

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

std::optional<std::string> 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->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;
}
std::unique_lock<std::mutex> lk(m_sDBUSState.messageMutex);
switch (matchResult) {
case MatchResult::NoMatch: //The verification did not match, Device.VerifyStop should now be called.
stopVerify();
if (m_sDBUSState.retries < 2) {
startVerify();
done = false;
m_sDBUSState.retries++;
m_sDBUSState.message = 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.messageMutex);
if (m_sDBUSState.message == RETRY_MESSAGE) {
m_sDBUSState.message = m_sFingerprintReady;
g_pHyprlock->enqueueForceUpdateTimers();
}
},
nullptr);
} else {
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;
}
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();

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

void terminate();
bool setupDBUS();

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

std::mutex messageMutex;

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;

std::string m_sFingerprintReady;
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();

bool createDeviceProxy();
bool claimDevice();
bool startVerify();
bool stopVerify();
bool releaseDevice();
};

inline std::unique_ptr<CFingerprint> g_pFingerprint;
5 changes: 5 additions & 0 deletions 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
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 "../../core/Auth.hpp"
#include "../../core/Fingerprint.hpp"
#include <chrono>
#include <unistd.h>
#include <pwd.h>
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 3d5fd21

Please sign in to comment.