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 18, 2024
1 parent 1169452 commit b3128e9
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 9 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.3)
hyprutils>=0.2.3
sdbus-c++)

file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp")
add_executable(hyprlock ${SRCFILES})
Expand Down
4 changes: 4 additions & 0 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
hyprutils,
pam,
pango,
sdbus-cpp,
systemdLibs,
wayland,
wayland-protocols,
wayland-scanner,
Expand Down Expand Up @@ -45,6 +47,8 @@ stdenv.mkDerivation {
hyprutils
pam
pango
sdbus-cpp
systemdLibs
wayland
wayland-protocols
];
Expand Down
3 changes: 3 additions & 0 deletions src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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{""});
Expand Down
221 changes: 221 additions & 0 deletions src/core/Fingerprint.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
#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 = "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 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, "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, "fprint: PrepareForSleep (start: {})", start);
if (start) {
m_sDBUSState.sleeping = true;
stopVerify();
m_sDBUSState.inhibitLock.reset();
} else {
m_sDBUSState.sleeping = false;
inhibitSleep();
startVerify();
}
});
m_sDBUSState.login->finishRegistration();
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, "/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) {}
});

m_sDBUSState.device->finishRegistration();
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 != MatchResult::Disconnected)
return;
switch (matchResult) {
case MatchResult::NoMatch:
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 MatchResult::UnknownError:
stopVerify();
m_sDBUSState.message = "Unknown fingerprint error, disabling fingerprint auth";
break;
case MatchResult::Match:
stopVerify();
m_bAuthenticated = true;
m_sDBUSState.message = "";
g_pHyprlock->unlock();
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.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;
}
70 changes: 70 additions & 0 deletions src/core/Fingerprint.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#pragma once

#include "hyprlock.hpp"

#include <memory>
#include <optional>
#include <string>
#include <condition_variable>
#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:
enum class MatchResult {
NoMatch,
Match,
Retry,
SwipeTooShort,
FingerNotCentered,
RemoveAndRetry,
Disconnected,
UnknownError,
};
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;

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 updateMessage = true);
bool stopVerify();
bool releaseDevice();
};

inline std::unique_ptr<CFingerprint> g_pFingerprint;
Loading

0 comments on commit b3128e9

Please sign in to comment.