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();