From 3f20cc2679c48373281db4f2c30bf3df97d547da Mon Sep 17 00:00:00 2001 From: Maximilian Seidler Date: Mon, 16 Dec 2024 15:59:39 +0100 Subject: [PATCH 1/2] auth: add sodium pwhash authentication --- CMakeLists.txt | 1 + nix/default.nix | 2 + src/auth/Auth.cpp | 5 ++ src/auth/Auth.hpp | 1 + src/auth/SodiumPWHash.cpp | 124 +++++++++++++++++++++++++++++++++++ src/auth/SodiumPWHash.hpp | 43 ++++++++++++ src/config/ConfigManager.cpp | 1 + 7 files changed, 177 insertions(+) create mode 100644 src/auth/SodiumPWHash.cpp create mode 100644 src/auth/SodiumPWHash.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0da7db93..408bee3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ pkg_check_modules( gbm hyprutils>=0.2.6 sdbus-c++>=2.0.0 + libsodium hyprgraphics) file(GLOB_RECURSE SRCFILES CONFIGURE_DEPENDS "src/*.cpp") diff --git a/nix/default.nix b/nix/default.nix index 9b31c58c..4f4626f1 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -8,6 +8,7 @@ libdrm, libGL, libjpeg, + libsodium, libwebp, libxkbcommon, mesa, @@ -38,6 +39,7 @@ stdenv.mkDerivation { buildInputs = [ cairo file + libsodium libdrm libGL libjpeg diff --git a/src/auth/Auth.cpp b/src/auth/Auth.cpp index d767084d..afc82c78 100644 --- a/src/auth/Auth.cpp +++ b/src/auth/Auth.cpp @@ -1,6 +1,7 @@ #include "Auth.hpp" #include "Pam.hpp" #include "Fingerprint.hpp" +#include "SodiumPWHash.hpp" #include "../config/ConfigManager.hpp" #include "../core/hyprlock.hpp" #include "src/helpers/Log.hpp" @@ -15,7 +16,11 @@ CAuth::CAuth() { static auto* const PENABLEFINGERPRINT = (Hyprlang::INT* const*)g_pConfigManager->getValuePtr("auth:fingerprint:enabled"); if (**PENABLEFINGERPRINT) m_vImpls.push_back(std::make_shared()); + static auto* const PENABLESODIUM = (Hyprlang::INT* const*)g_pConfigManager->getValuePtr("auth:sodium:enabled"); + if (**PENABLESODIUM) + m_vImpls.push_back(std::make_shared()); + RASSERT(!(**PENABLEPAM && **PENABLESODIUM), "Pam and sodium hash authentication are mutually exclusive! Please enable one or the other."); RASSERT(!m_vImpls.empty(), "At least one authentication method must be enabled!"); } diff --git a/src/auth/Auth.hpp b/src/auth/Auth.hpp index 49a618c0..cb0777f2 100644 --- a/src/auth/Auth.hpp +++ b/src/auth/Auth.hpp @@ -7,6 +7,7 @@ enum eAuthImplementations { AUTH_IMPL_PAM = 0, AUTH_IMPL_FINGERPRINT = 1, + AUTH_IMPL_SODIUM = 2, }; class IAuthImplementation { diff --git a/src/auth/SodiumPWHash.cpp b/src/auth/SodiumPWHash.cpp new file mode 100644 index 00000000..bca3e6ea --- /dev/null +++ b/src/auth/SodiumPWHash.cpp @@ -0,0 +1,124 @@ +#include "SodiumPWHash.hpp" + +#include "../helpers/Log.hpp" +#include "../core/hyprlock.hpp" + +#include +#include +#include + +static std::string getSecretsConfigPath() { + static const auto [PWHASHPATH, DOTDIR] = Hyprutils::Path::findConfig("hyprlock_pwhash"); + (void)DOTDIR; + + RASSERT(PWHASHPATH.has_value(), "[SodiumAuth] Failed to find hyprlock_pwhash.conf. Please use \"hyprlock-setpwhash\" to generate it!"); + // check permissions + using std::filesystem::perms; + const auto PERMS = std::filesystem::status(PWHASHPATH.value()).permissions(); + if ((PERMS & perms::group_read) != perms::none || (PERMS & perms::group_write) != perms::none || (PERMS & perms::others_read) != perms::none || + (PERMS & perms::others_write) != perms::none) { + RASSERT(false, "[SodiumAuth] hyprlock_pwhash.conf has insecure permissions"); + } + return PWHASHPATH.value(); +} + +void* const* CSodiumPWHash::getConfigValuePtr(const std::string& name) { + return m_config.getConfigValuePtr(name.c_str())->getDataStaticPtr(); +} + +CSodiumPWHash::CSodiumPWHash() : m_config(getSecretsConfigPath().c_str(), {}) { + m_config.addConfigValue("pw_hash", Hyprlang::STRING{""}); + m_config.commence(); + auto result = m_config.parse(); + + if (result.error) + Debug::log(ERR, "Config has errors:\n{}\nProceeding ignoring faulty entries", result.getError()); + + m_checkerThread = std::thread([this]() { checkerLoop(); }); +} + +CSodiumPWHash::~CSodiumPWHash() { + ; +} + +void CSodiumPWHash::init() { + RASSERT(sodium_init() >= 0, "Failed to initialize libsodium"); +} + +void CSodiumPWHash::handleInput(const std::string& input) { + std::lock_guard lk(m_sCheckerState.requestMutex); + + m_sCheckerState.input = input; + m_sCheckerState.requested = true; + + m_sCheckerState.requestCV.notify_all(); +} + +bool CSodiumPWHash::checkWaiting() { + return m_sCheckerState.requested; +} + +std::optional CSodiumPWHash::getLastFailText() { + return m_sCheckerState.failText.empty() ? std::nullopt : std::optional(m_sCheckerState.failText); +} + +std::optional CSodiumPWHash::getLastPrompt() { + return "Password: "; +} + +void CSodiumPWHash::terminate() { + m_sCheckerState.requestCV.notify_all(); + if (m_checkerThread.joinable()) + m_checkerThread.join(); +} + +void CSodiumPWHash::rehash(std::string& input) { + const auto CONFIGPATH = getSecretsConfigPath(); + + char hash[crypto_pwhash_STRBYTES]; + if (crypto_pwhash_str(hash, input.c_str(), input.size(), crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE) != 0) { + Debug::log(ERR, "[Sodium] Failed to hash password"); + return; + } + + std::ofstream out(CONFIGPATH); + out << "hyprlock {\n pw_hash = " << hash << "\n}\n"; + out.close(); + + // set perms to -rw------- + using std::filesystem::perms; + std::filesystem::permissions(CONFIGPATH, perms::owner_read | perms::owner_write); +} + +void CSodiumPWHash::checkerLoop() { + static auto* const PPWHASH = (Hyprlang::STRING*)getConfigValuePtr("pw_hash"); + const auto PWHASH = std::string(*PPWHASH); + const bool NEEDSREHASH = crypto_pwhash_str_needs_rehash(PWHASH.c_str(), crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE) != 0; + if (NEEDSREHASH) + Debug::log(WARN, "[Sodium] Password hash needs rehashing"); + + while (true) { + std::unique_lock lk(m_sCheckerState.requestMutex); + m_sCheckerState.requestCV.wait(lk, [this]() { return m_sCheckerState.requested || g_pHyprlock->m_bTerminate; }); + + if (g_pHyprlock->isUnlocked()) + return; + + if (PWHASH.empty() || PWHASH.size() > crypto_pwhash_STRBYTES) { + m_sCheckerState.failText = "Invalid password hash"; + Debug::log(ERR, "[SodiumAuth] Invalid password hash set in secrets.conf"); + g_pAuth->enqueueFail(); + } else if (crypto_pwhash_str_verify(PWHASH.c_str(), m_sCheckerState.input.c_str(), m_sCheckerState.input.length()) == 0) { + if (NEEDSREHASH) + rehash(m_sCheckerState.input); + g_pAuth->enqueueUnlock(); + } else { + g_pAuth->enqueueFail(); + m_sCheckerState.failText = "Failed to authenticate"; + Debug::log(LOG, "[SodiumAuth] Failed to authenticate"); + } + + m_sCheckerState.input.clear(); + m_sCheckerState.requested = false; + } +} diff --git a/src/auth/SodiumPWHash.hpp b/src/auth/SodiumPWHash.hpp new file mode 100644 index 00000000..bfbf8a51 --- /dev/null +++ b/src/auth/SodiumPWHash.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "Auth.hpp" + +#include +#include +#include +#include +#include + +class CSodiumPWHash : public IAuthImplementation { + public: + CSodiumPWHash(); + + virtual ~CSodiumPWHash(); + virtual eAuthImplementations getImplType() { + return AUTH_IMPL_SODIUM; + } + virtual void init(); + virtual void handleInput(const std::string& input); + virtual bool checkWaiting(); + virtual std::optional getLastFailText(); + virtual std::optional getLastPrompt(); + virtual void terminate(); + + private: + void* const* getConfigValuePtr(const std::string& name); + + struct { + std::condition_variable requestCV; + std::string input; + bool requested = false; + std::mutex requestMutex; + std::string failText; + } m_sCheckerState; + + std::thread m_checkerThread; + void checkerLoop(); + + void rehash(std::string& input); + + Hyprlang::CConfig m_config; +}; diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 0c8f9ab6..b3318f75 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -178,6 +178,7 @@ void CConfigManager::init() { m_config.addConfigValue("auth:fingerprint:enabled", Hyprlang::INT{0}); m_config.addConfigValue("auth:fingerprint:ready_message", Hyprlang::STRING{"(Scan fingerprint to unlock)"}); m_config.addConfigValue("auth:fingerprint:present_message", Hyprlang::STRING{"Scanning fingerprint"}); + m_config.addConfigValue("auth:sodium:enabled", Hyprlang::INT{1}); m_config.addSpecialCategory("background", Hyprlang::SSpecialCategoryOptions{.key = nullptr, .anonymousKeyBased = true}); m_config.addSpecialConfigValue("background", "monitor", Hyprlang::STRING{""}); From a8d6ec7b02f64191b2d0af67292d673507a12ee8 Mon Sep 17 00:00:00 2001 From: Maximilian Seidler Date: Mon, 16 Dec 2024 16:13:48 +0100 Subject: [PATCH 2/2] auth: add hyprlock-setpwhash target for setting the password hash --- CMakeLists.txt | 6 +- setpwhash/main.cpp | 167 ++++++++++++++++++++++++++++++++++++++ src/auth/SodiumPWHash.cpp | 27 +----- src/auth/SodiumPWHash.hpp | 2 - 4 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 setpwhash/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 408bee3b..aa923015 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -116,8 +116,12 @@ protocol("protocols/wlr-screencopy-unstable-v1.xml" protocol("unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml" "linux-dmabuf-unstable-v1" false) +# hyprlock-setpwhash +add_executable(hyprlock-setpwhash "setpwhash/main.cpp") +target_link_libraries(hyprlock-setpwhash PRIVATE sodium hyprutils) + # Installation -install(TARGETS hyprlock) +install(TARGETS hyprlock hyprlock-setpwhash) install(FILES ${CMAKE_SOURCE_DIR}/pam/hyprlock DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d) diff --git a/setpwhash/main.cpp b/setpwhash/main.cpp new file mode 100644 index 00000000..24570bb8 --- /dev/null +++ b/setpwhash/main.cpp @@ -0,0 +1,167 @@ +#include "../src/helpers/Log.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::filesystem::perms; + +void setStdinEcho(bool enable = true) { + struct termios tty; + tcgetattr(STDIN_FILENO, &tty); + if (!enable) + tty.c_lflag &= ~ECHO; + else + tty.c_lflag |= ECHO; + RASSERT(tcsetattr(STDIN_FILENO, TCSANOW, &tty) == 0, "Failed to set terminal attributes"); +} + +// returns the first none-whitespace char +int getChoice() { + std::string input; + std::getline(std::cin, input); + const auto p = input.find_first_not_of(" \n"); + return (p == std::string::npos) ? 0 : input[p]; +} + +constexpr auto CHOOSELIMITSPROMPT = R"#( +Choose how hard it will be to brute force your password. +This also defines how long it will take to check the password. +1 - interactive (least security, pretty fast checking) +2 - moderate (medium security, takes below a second on most machines) +3 - sensitive (decent security, takes around 2-4 seconds on most machines) + +Type 1, 2 or 3, or Enter for default (2): )#"; + +unsigned int getOpsLimit(int choice) { + switch (choice) { + case '1': return crypto_pwhash_OPSLIMIT_INTERACTIVE; + case '2': return crypto_pwhash_OPSLIMIT_MODERATE; + case '3': return crypto_pwhash_OPSLIMIT_SENSITIVE; + default: return crypto_pwhash_OPSLIMIT_MODERATE; + } + std::unreachable(); +} + +unsigned int getMemLimit(int choice) { + switch (choice) { + case '1': return crypto_pwhash_MEMLIMIT_INTERACTIVE; + case '2': return crypto_pwhash_MEMLIMIT_MODERATE; + case '3': return crypto_pwhash_MEMLIMIT_SENSITIVE; + default: return crypto_pwhash_MEMLIMIT_MODERATE; + } + std::unreachable(); +} + +void help() { + std::println("Usage: hyprlock-setpwhash\n" + "Interactive utility to set the password hash for hyprlock"); +} + +int main(int argc, char** argv, char** envp) { + std::vector args(argv, argv + argc); + + RASSERT(sodium_init() >= 0, "Failed to initialize libsodium"); + + for (std::size_t i = 1; i < args.size(); ++i) { + const std::string arg = argv[i]; + + if (arg == "--help" || arg == "-h") { + help(); + return 0; + } else { + std::cerr << "Unknown argument: " << arg << std::endl; + help(); + return 1; + } + } + + const auto [SECRETSCONF, DOTDIR] = Hyprutils::Path::findConfig("hyprlock_pwhash"); + if (SECRETSCONF.has_value()) { + // check permissions + std::println("{} already exists.", SECRETSCONF.value()); + std::print("Do you want to overwrite it? [y/N] "); + const auto CHOICE = getChoice(); + + if (CHOICE != 'y' && CHOICE != 'Y') { + std::println("Keeping existing secrets!"); + + const auto PERMS = std::filesystem::status(SECRETSCONF.value()).permissions(); + if ((PERMS & perms::group_read) != perms::none || (PERMS & perms::group_write) != perms::none || (PERMS & perms::others_read) != perms::none || + (PERMS & perms::others_write) != perms::none) { + std::println("Setting permissions of {} to -rw-------", SECRETSCONF.value()); + + // set perms to -rw------- + std::filesystem::permissions(SECRETSCONF.value(), perms::owner_read | perms::owner_write); + } + return 0; + } + } + + RASSERT(DOTDIR.has_value(), "Failed to find config directory!"); + const auto DEST = DOTDIR.value() + "/hypr/hyprlock_pwhash.conf"; + + std::println("Note: We are going to write a password hash to {}\n" + " If you choose a weak password and this hash gets leaked,\n" + " someone might be able to guess your password using a password list or brute force.\n" + " So best to keep it safe and (or) choose a good password.", + DEST); + + std::print(CHOOSELIMITSPROMPT); + const auto CHOICE = getChoice(); + + setStdinEcho(false); + std::string pw = ""; + while (true) { + std::print("New password: "); + std::getline(std::cin, pw); + std::print("\r"); + + if (pw.empty()) { + std::println("Empty password"); + continue; + } + + if (pw.size() < 4) { + std::println("Less than 4 characters? Nope."); + continue; + } + + std::string pw2 = ""; + std::print("Repeat password: "); + std::getline(std::cin, pw2); + std::print("\r"); + + if (pw != pw2) { + std::println("Ups, passwords do not match"); + continue; + } + + break; + } + setStdinEcho(true); + + char hash[crypto_pwhash_STRBYTES]; + if (crypto_pwhash_str(hash, pw.c_str(), pw.size(), getOpsLimit(CHOICE), getMemLimit(CHOICE)) != 0) { + std::println("[Sodium] Failed to hash password"); + return 1; + } + + { + std::ofstream out(DEST); + out << "pw_hash = " << hash << std::endl; + } + + // set perms to -rw------- + std::filesystem::permissions(DEST, perms::owner_read | perms::owner_write); + + std::println("Done!"); + return 0; +} diff --git a/src/auth/SodiumPWHash.cpp b/src/auth/SodiumPWHash.cpp index bca3e6ea..0a0e53cd 100644 --- a/src/auth/SodiumPWHash.cpp +++ b/src/auth/SodiumPWHash.cpp @@ -72,30 +72,9 @@ void CSodiumPWHash::terminate() { m_checkerThread.join(); } -void CSodiumPWHash::rehash(std::string& input) { - const auto CONFIGPATH = getSecretsConfigPath(); - - char hash[crypto_pwhash_STRBYTES]; - if (crypto_pwhash_str(hash, input.c_str(), input.size(), crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE) != 0) { - Debug::log(ERR, "[Sodium] Failed to hash password"); - return; - } - - std::ofstream out(CONFIGPATH); - out << "hyprlock {\n pw_hash = " << hash << "\n}\n"; - out.close(); - - // set perms to -rw------- - using std::filesystem::perms; - std::filesystem::permissions(CONFIGPATH, perms::owner_read | perms::owner_write); -} - void CSodiumPWHash::checkerLoop() { - static auto* const PPWHASH = (Hyprlang::STRING*)getConfigValuePtr("pw_hash"); - const auto PWHASH = std::string(*PPWHASH); - const bool NEEDSREHASH = crypto_pwhash_str_needs_rehash(PWHASH.c_str(), crypto_pwhash_OPSLIMIT_MODERATE, crypto_pwhash_MEMLIMIT_MODERATE) != 0; - if (NEEDSREHASH) - Debug::log(WARN, "[Sodium] Password hash needs rehashing"); + static auto* const PPWHASH = (Hyprlang::STRING*)getConfigValuePtr("pw_hash"); + const auto PWHASH = std::string(*PPWHASH); while (true) { std::unique_lock lk(m_sCheckerState.requestMutex); @@ -109,8 +88,6 @@ void CSodiumPWHash::checkerLoop() { Debug::log(ERR, "[SodiumAuth] Invalid password hash set in secrets.conf"); g_pAuth->enqueueFail(); } else if (crypto_pwhash_str_verify(PWHASH.c_str(), m_sCheckerState.input.c_str(), m_sCheckerState.input.length()) == 0) { - if (NEEDSREHASH) - rehash(m_sCheckerState.input); g_pAuth->enqueueUnlock(); } else { g_pAuth->enqueueFail(); diff --git a/src/auth/SodiumPWHash.hpp b/src/auth/SodiumPWHash.hpp index bfbf8a51..0bbd743b 100644 --- a/src/auth/SodiumPWHash.hpp +++ b/src/auth/SodiumPWHash.hpp @@ -37,7 +37,5 @@ class CSodiumPWHash : public IAuthImplementation { std::thread m_checkerThread; void checkerLoop(); - void rehash(std::string& input); - Hyprlang::CConfig m_config; };