From cdef6593b6636f0daf78246511487e08273a8bba Mon Sep 17 00:00:00 2001 From: Michael Raitza Date: Mon, 2 Dec 2024 17:33:46 +0100 Subject: [PATCH] Add password hash authentication As an alternative to PAM authentication, password hash authentication only relies on the availability of libgcrypt (and some program like OpenSSL or sha256sum to create the hash). Supports salted hashes and all hash algorithms that are available in the actual libgcrypt installation. --- CMakeLists.txt | 1 + README.md | 17 ++++ nix/default.nix | 2 + src/config/ConfigManager.cpp | 3 + src/core/Auth.hpp | 12 +-- src/core/IAuth.hpp | 25 +++++ src/core/PwAuth.cpp | 191 +++++++++++++++++++++++++++++++++++ src/core/PwAuth.hpp | 47 +++++++++ src/core/hyprlock.cpp | 8 +- 9 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 src/core/IAuth.hpp create mode 100644 src/core/PwAuth.cpp create mode 100644 src/core/PwAuth.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fae0801..e31f0a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ pkg_check_modules( egl opengl xkbcommon + libgcrypt libjpeg libwebp libmagic diff --git a/README.md b/README.md index d6f26b9..c7f79c6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,23 @@ Hyprland's simple, yet multi-threaded and GPU-accelerated screen locking utility ## Docs / Configuration [See the wiki](https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock/) +### Password hash configuration +If PAM authentication is unavailable to you, you can use password hash authentication via `libgcrypt`. +Activated it by setting `general:password_hash` to the desired value as a string of hexadecimal numbers. +You can select the hash function with `general:password_hash` with the default being `SHA256`. +Other known hash functions are `SHA3-256`, `SHA512_256` or `SHAKE128`. +You can also salt the by setting `hash_salt`. +Set an individual salt (and matching hash) on different systems or across different users to possibly mask that you/users are using the same password. + +You can set up a new password hash by first selecting the hash function (e.g. `SHA3-256`) and then using OpenSSL to create the salt and hash: +``` sh +# Produces 10 bytes salt +SALT=$(openssl rand -hex 10) +printf "hash_salt = %s\n" "$SALT" +# Enter your password (no echo) and press ENTER. +{ read -s v; echo "$v${SALT}" } | openssl sha3-256 -hex +``` + ## Arch install ```sh pacman -S hyprlock # binary x86 tagged release diff --git a/nix/default.nix b/nix/default.nix index adb23f2..f69a1b4 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -7,6 +7,7 @@ file, libdrm, libGL, + libgcrypt, libjpeg, libwebp, libxkbcommon, @@ -39,6 +40,7 @@ stdenv.mkDerivation { file libdrm libGL + libgcrypt libjpeg libwebp libxkbcommon diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 2ce8a10..68c0a60 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -172,6 +172,9 @@ void CConfigManager::init() { m_config.addConfigValue("general:ignore_empty_input", Hyprlang::INT{0}); m_config.addConfigValue("general:immediate_render", Hyprlang::INT{0}); m_config.addConfigValue("general:pam_module", Hyprlang::STRING{"hyprlock"}); + m_config.addConfigValue("general:hash_algorithm", Hyprlang::STRING{"SHA256"}); + m_config.addConfigValue("general:hash_salt", Hyprlang::STRING{""}); + m_config.addConfigValue("general:password_hash", Hyprlang::STRING{""}); 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)"}); diff --git a/src/core/Auth.hpp b/src/core/Auth.hpp index a6f8289..4da1a45 100644 --- a/src/core/Auth.hpp +++ b/src/core/Auth.hpp @@ -1,12 +1,12 @@ #pragma once -#include +#include "IAuth.hpp" #include #include #include #include -class CAuth { +class CAuth : public CIAuth { public: struct SPamConversationState { std::string input = ""; @@ -21,7 +21,8 @@ class CAuth { bool failTextFromPam = false; }; - CAuth(); + explicit CAuth(); + CAuth(const CAuth&) = delete; void start(); bool auth(); @@ -37,9 +38,6 @@ class CAuth { void terminate(); - // Should only be set via the main thread - bool m_bDisplayFailText = false; - private: SPamConversationState m_sConversationState; @@ -50,5 +48,3 @@ class CAuth { void resetConversation(); }; - -inline std::unique_ptr g_pAuth; diff --git a/src/core/IAuth.hpp b/src/core/IAuth.hpp new file mode 100644 index 0000000..f249bc2 --- /dev/null +++ b/src/core/IAuth.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +class CIAuth { + public: + virtual void start() = 0; + virtual bool auth() = 0; + virtual bool isAuthenticated() = 0; + virtual void waitForInput() = 0; + virtual void submitInput(std::string input) = 0; + virtual std::optional getLastFailText() = 0; + virtual std::optional getLastPrompt() = 0; + virtual bool checkWaiting() = 0; + virtual void terminate() = 0; + + CIAuth() = default; + + // Should only be set via the main thread + bool m_bDisplayFailText = false; +}; + +inline std::unique_ptr g_pAuth; diff --git a/src/core/PwAuth.cpp b/src/core/PwAuth.cpp new file mode 100644 index 0000000..d6733c6 --- /dev/null +++ b/src/core/PwAuth.cpp @@ -0,0 +1,191 @@ +#include "PwAuth.hpp" +#include "hyprlock.hpp" +#include "../helpers/Log.hpp" +#include "../config/ConfigManager.hpp" +#include +#include +#define GCRYPT_NO_DEPRECATED +#define GCRYPT_NO_MPI_MACROS +#define NEED_LIBGCRYPT_VERSION nullptr +#include + +using namespace std::chrono_literals; + +static std::unique_ptr hex2Bytes(const std::string& hex) noexcept { + auto bytes = std::make_unique(hex.length() / 2); + for (std::size_t i = 0; i < hex.length() / 2; ++i) { + try { + auto v = std::stoi(hex.substr(2 * i, 2), nullptr, 16); + if (v >= 0) + bytes[i] = static_cast(v); + else + throw std::invalid_argument("invalid hex value"); + } catch (std::invalid_argument const& e) { + Debug::log(ERR, "auth: invalid password_hash"); + bytes = nullptr; + } catch (std::out_of_range const& e) { + // Should never happen, as 2-byte substrings should never go o-o-r. + Debug::log(CRIT, "auth: implementation error in hex2Bytes conversion"); + bytes = nullptr; + } + } + return bytes; +} + +static std::string bytes2Hex(const unsigned char* bytes, std::size_t len) { + std::stringstream ss; + ss << std::setw(2) << std::setfill('0') << std::hex; + for (std::size_t i = 0; i < len; ++i) + ss << (int)bytes[i]; + return ss.str(); +} + +CPwAuth::CPwAuth() { + + if (gcry_check_version(NEED_LIBGCRYPT_VERSION)) + gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0); + else + Debug::log(CRIT, "libgcrypt too old"); + + if (gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { + + // Handle the hash algorithm + static auto const ALGO = *(Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:hash_algorithm")); + m_iAlgo = gcry_md_map_name(ALGO); + m_iDigestLen = gcry_md_get_algo_dlen(m_iAlgo); + if (m_iAlgo) { + static auto const err = gcry_err_code(gcry_md_test_algo(m_iAlgo)); + if (err == GPG_ERR_NO_ERROR) { + + // Handle the salt + static auto* const SALT = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:hash_salt")); + m_szSalt = std::string(*SALT); + + // Handle the expected hash + static auto* const HASH = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:password_hash")); + static auto const hash = std::string(*HASH); + if (hash.empty() || (hash.size() % 2) || (hash.length() != 2uL * m_iDigestLen)) { + Debug::log(ERR, "auth: password_hash has incorrect length for algorithm {} (got: {}, expected: {})", ALGO, hash.size(), 2uL * m_iDigestLen); + m_bLibFailed = true; + } else { + m_aHash = hex2Bytes(hash); + if (!m_aHash || hash.empty()) + m_bLibFailed = true; + } + } else { + // Might be due to FIPS mode + Debug::log(CRIT, "auth: hash algorithm unavailable: {}", ALGO); + m_bLibFailed = true; + } + } else { + Debug::log(ERR, "auth: unknown hash algorithm: {}", ALGO); + m_bLibFailed = true; + } + } else { + Debug::log(CRIT, "libgcrypt could not be initialized"); + m_bLibFailed = true; + } +} + +static void passwordCheckTimerCallback(std::shared_ptr self, void* data) { + g_pHyprlock->onPasswordCheckTimer(); +} + +void CPwAuth::start() { + std::thread([this]() { + reset(); + + waitForInput(); + + // For grace or SIGUSR1 unlocks + if (g_pHyprlock->isUnlocked()) + return; + + const auto AUTHENTICATED = auth(); + m_bAuthenticated = AUTHENTICATED; + + if (g_pHyprlock->isUnlocked()) + return; + + g_pHyprlock->addTimer(1ms, passwordCheckTimerCallback, nullptr); + }).detach(); +} + +bool CPwAuth::auth() { + if (m_bLibFailed) + return true; + + bool verdict; + auto digest = std::make_unique(m_iDigestLen); + auto istr = m_sState.input; + istr.append(m_szSalt); + + gcry_md_hash_buffer(m_iAlgo, digest.get(), istr.c_str(), istr.size()); + Debug::log(TRACE, "auth: resulting hash {}", bytes2Hex(digest.get(), m_iDigestLen)); + Debug::log(TRACE, "auth: expected hash {}", bytes2Hex(m_aHash.get(), m_iDigestLen)); + verdict = !std::memcmp(m_aHash.get(), digest.get(), m_iDigestLen); + + if (verdict) + Debug::log(LOG, "auth: authenticated"); + else + Debug::log(ERR, "auth: unsuccessful"); + + m_sState.authenticating = false; + /// DEBUG Code; replace constant with verdict + return verdict; +} + +bool CPwAuth::isAuthenticated() { + return m_bAuthenticated; +} + +// clearing the input must be done from the main thread +static void clearInputTimerCallback(std::shared_ptr self, void* data) { + g_pHyprlock->clearPasswordBuffer(); +} + +void CPwAuth::waitForInput() { + g_pHyprlock->addTimer(1ms, clearInputTimerCallback, nullptr); + if (m_bLibFailed) + return; + + std::unique_lock lk(m_sState.inputMutex); + m_bBlockInput = false; + m_sState.inputRequested = true; + m_sState.inputSubmittedCondition.wait(lk, [this] { return !m_sState.inputRequested || g_pHyprlock->m_bTerminate; }); + m_bBlockInput = true; +} + +void CPwAuth::submitInput(std::string input) { + std::unique_lock lk(m_sState.inputMutex); + if (!m_sState.inputRequested) + Debug::log(ERR, "SubmitInput called, but the auth thread is not waiting for input!"); + m_sState.input = input; + m_sState.inputRequested = false; + m_sState.authenticating = true; + m_sState.inputSubmittedCondition.notify_all(); +} + +std::optional CPwAuth::getLastPrompt() { + std::string pmpt = "Password: "; + return pmpt; +} + +std::optional CPwAuth::getLastFailText() { + std::string ret = "Password incorrect"; + return ret; +} + +bool CPwAuth::checkWaiting() { + return m_bBlockInput; +} + +void CPwAuth::terminate() { + m_sState.inputSubmittedCondition.notify_all(); +} + +void CPwAuth::reset() { + m_sState.input = ""; + m_sState.inputRequested = false; + m_sState.authenticating = false; +} diff --git a/src/core/PwAuth.hpp b/src/core/PwAuth.hpp new file mode 100644 index 0000000..3c9ecdc --- /dev/null +++ b/src/core/PwAuth.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "IAuth.hpp" +#include +#include +#include +#include + +class CPwAuth : public CIAuth { + public: + struct SState { + std::string input = ""; + + std::mutex inputMutex; + std::condition_variable inputSubmittedCondition; + + bool inputRequested = false; + bool authenticating = false; + }; + + explicit CPwAuth(); + CPwAuth(const CPwAuth&) = delete; + + void start(); + bool auth(); + bool isAuthenticated(); + void waitForInput(); + void submitInput(std::string input); + + std::optional getLastPrompt(); + std::optional getLastFailText(); + + bool checkWaiting(); + void terminate(); + + private: + SState m_sState; + bool m_bBlockInput = true; + bool m_bAuthenticated = false; + bool m_bLibFailed = false; + std::unique_ptr m_aHash; + std::string m_szSalt; + int m_iAlgo = -1; + unsigned int m_iDigestLen = 0; + + void reset(); +}; diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index 4503e7b..ad89818 100644 --- a/src/core/hyprlock.cpp +++ b/src/core/hyprlock.cpp @@ -2,7 +2,9 @@ #include "../helpers/Log.hpp" #include "../config/ConfigManager.hpp" #include "../renderer/Renderer.hpp" +#include "IAuth.hpp" #include "Auth.hpp" +#include "PwAuth.hpp" #include "Egl.hpp" #include "Fingerprint.hpp" #include "linux-dmabuf-unstable-v1-protocol.h" @@ -416,7 +418,11 @@ void CHyprlock::run() { exit(1); } - g_pAuth = std::make_unique(); + auto H = std::string(*(Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:password_hash"))); + if (H.empty()) + g_pAuth = std::make_unique(); + else + g_pAuth = std::make_unique(); g_pAuth->start(); g_pFingerprint = std::make_unique();