diff --git a/CMakeLists.txt b/CMakeLists.txt index 50ba622d..71675b66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ pkg_check_modules( gbm hyprutils>=0.8.0 sdbus-c++>=2.0.0 + libsodium hyprgraphics>=0.1.6) find_library(PAM_FOUND NAMES pam libpam) if(PAM_FOUND) @@ -153,8 +154,12 @@ protocolnew("stable/viewporter" "viewporter" false) protocolnew("staging/cursor-shape" "cursor-shape-v1" false) protocolnew("stable/tablet" "tablet-v2" false) +# hyprlock-pwhash +add_executable(hyprlock-pwhash "setpwhash/main.cpp") +target_link_libraries(hyprlock-pwhash PRIVATE sodium hyprutils) + # Installation -install(TARGETS hyprlock) +install(TARGETS hyprlock hyprlock-pwhash) install(FILES ${CMAKE_SOURCE_DIR}/pam/hyprlock DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d) diff --git a/nix/default.nix b/nix/default.nix index d3c51050..b0bec9c9 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -6,6 +6,7 @@ cairo, libdrm, libGL, + libsodium, libxkbcommon, libgbm, hyprgraphics, @@ -39,6 +40,7 @@ stdenv.mkDerivation { cairo libdrm libGL + libsodium libxkbcommon libgbm hyprgraphics diff --git a/setpwhash/main.cpp b/setpwhash/main.cpp new file mode 100644 index 00000000..62ec43ee --- /dev/null +++ b/setpwhash/main.cpp @@ -0,0 +1,192 @@ +#include "../src/helpers/Log.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::filesystem::perms; + +static 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 +static 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): )#"; + +static 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(); +} + +static 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(); +} + +static void help() { + std::println("Usage: hyprlock-setpwhash [options]\n\n" + "Options:\n" + " -c FILE, --config FILE - Specify config file to use\n" + " -h, --help - Show this help message\n\n" + "Interactive utility to set the password hash for hyprlock"); +} + +static std::optional parseArg(const std::vector& args, const std::string& flag, std::size_t& i) { + if (i + 1 < args.size()) { + return args[++i]; + } else { + std::println(stderr, "Error: Missing value for {} option.", flag); + return std::nullopt; + } +} + +int main(int argc, char** argv, char** envp) { + std::string configPath; + 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 if ((arg == "--config" || arg == "-c") && i + 1 < (std::size_t)argc) { + if (auto value = parseArg(args, arg, i); value) + configPath = *value; + else + return 1; + + } else { + std::cerr << "Unknown argument: " << arg << std::endl; + help(); + return 1; + } + } + + std::string DEST; + const auto [SECRETSCONF, DOTDIR] = Hyprutils::Path::findConfig("hyprlock_sodium"); + + if (!configPath.empty()) + DEST = configPath; + + else if (SECRETSCONF.has_value()) + DEST = SECRETSCONF.value(); + + else { + RASSERT(DOTDIR.has_value(), "Failed to find config directory!"); + DEST = DOTDIR.value() + "/hypr/hyprlock_sodium.conf"; + } + + if (std::filesystem::exists(DEST)) { + // check permissions + std::println("{} already exists.", DEST); + 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(DEST).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-------", DEST); + + // set perms to -rw------- + std::filesystem::permissions(DEST, perms::owner_read | perms::owner_write); + } + return 0; + } + } + + 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); + + if (pw.empty()) { + std::println("\rEmpty password"); + continue; + } + + if (pw.size() < 4) { + std::println("\rPassword too short"); + continue; + } + + std::string pw2 = ""; + std::print("\rRepeat password: "); + std::getline(std::cin, pw2); + + if (pw != pw2) { + std::println("\rPasswords 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); + // set perms to -rw------- + std::filesystem::permissions(DEST, perms::owner_read | perms::owner_write); + + out << "hash = " << hash << std::endl; + } + + std::println("\nDone!"); + return 0; +} diff --git a/src/auth/Auth.cpp b/src/auth/Auth.cpp index 15824acb..31bef1a8 100644 --- a/src/auth/Auth.cpp +++ b/src/auth/Auth.cpp @@ -1,5 +1,6 @@ #include "Auth.hpp" #include "Pam.hpp" +#include "SodiumPWHash.hpp" #include "Fingerprint.hpp" #include "../config/ConfigManager.hpp" #include "../core/hyprlock.hpp" @@ -10,8 +11,16 @@ CAuth::CAuth() { static const auto ENABLEPAM = g_pConfigManager->getValue("auth:pam:enabled"); + static const auto ENABLESODIUM = g_pConfigManager->getValue("auth:sodium:enabled"); + + RASSERT(!(*ENABLEPAM && *ENABLESODIUM), "Only one of PAM or Sodium authentication methods can be enabled!") + if (*ENABLEPAM) m_vImpls.emplace_back(makeShared()); + + if (*ENABLESODIUM) + m_vImpls.emplace_back(makeShared()); + static const auto ENABLEFINGERPRINT = g_pConfigManager->getValue("auth:fingerprint:enabled"); if (*ENABLEFINGERPRINT) m_vImpls.emplace_back(makeShared()); diff --git a/src/auth/Auth.hpp b/src/auth/Auth.hpp index 40c902b0..078074dc 100644 --- a/src/auth/Auth.hpp +++ b/src/auth/Auth.hpp @@ -7,8 +7,9 @@ #include "../core/Timer.hpp" enum eAuthImplementations { - AUTH_IMPL_PAM = 0, - AUTH_IMPL_FINGERPRINT = 1, + AUTH_IMPL_PAM = 0, + AUTH_IMPL_FINGERPRINT = 1, + AUTH_IMPL_SODIUMPWHASH = 2, }; class IAuthImplementation { diff --git a/src/auth/SodiumPWHash.cpp b/src/auth/SodiumPWHash.cpp new file mode 100644 index 00000000..9659fe4e --- /dev/null +++ b/src/auth/SodiumPWHash.cpp @@ -0,0 +1,148 @@ +#include "SodiumPWHash.hpp" +#include "Auth.hpp" +#include "../config/ConfigManager.hpp" +#include "../core/hyprlock.hpp" +#include "../helpers/Log.hpp" + +#include +#include +#include +#include +#include +#include + +static std::string getSecretsConfigPath() { + std::filesystem::path secrets_file; + static const auto PPWHASHSTEM = g_pConfigManager->getValue("auth:sodium:secret_file"); + const std::string PWHASHSTEM = *PPWHASHSTEM; + std::filesystem::path dir = g_pConfigManager->configCurrentPath; + dir = dir.parent_path(); + + RASSERT(!PWHASHSTEM.empty(), "[SodiumAuth] auth:sodium:secret_file must be set to a non-empty value"); + + if (PWHASHSTEM.contains('/')) { + if (PWHASHSTEM.starts_with('/')) + // An absolute path + secrets_file = PWHASHSTEM; + else + // A relative path to main config file + secrets_file = dir / PWHASHSTEM; + } else { + // A stem + secrets_file = dir / PWHASHSTEM; + secrets_file += ".conf"; + } + + RASSERT(std::filesystem::exists(secrets_file), "[SodiumAuth] Failed to find {}. Use \"hyprlock-pwhash\" to generate it!", secrets_file.c_str()); + + // check permissions + using std::filesystem::perms; + const auto PERMS = std::filesystem::status(secrets_file).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] {} has insecure permissions", secrets_file.c_str()); + } + return secrets_file; +} + +CSodiumPWHash::CSodiumPWHash() : m_config(getSecretsConfigPath().c_str(), {}) { + m_config.addConfigValue("hash", Hyprlang::STRING{""}); + m_config.commence(); + auto result = m_config.parse(); + + if (result.error) + Debug::log(ERR, "[SodiumAuth] Error in configuration:\n{}\nProceeding", result.getError()); +} + +CSodiumPWHash::~CSodiumPWHash() { + ; +} + +void* const* CSodiumPWHash::getConfigValuePtr(const std::string& name) { + return m_config.getConfigValuePtr(name.c_str())->getDataStaticPtr(); +} + +void CSodiumPWHash::init() { + RASSERT(sodium_init() >= 0, "[SodiumAuth] Failed to initialise libsodium"); + m_thread = std::thread([this]() { + while (true) { + m_sCheckerState.prompt = "Password: "; + waitForInput(); + + // For grace or SIGUSR1 unlocks + if (g_pHyprlock->isUnlocked()) + return; + + const auto AUTHENTICATED = auth(); + + // For SIGUSR1 unlocks + if (g_pHyprlock->isUnlocked()) + return; + + if (!AUTHENTICATED) + g_pAuth->enqueueFail(m_sCheckerState.failText, AUTH_IMPL_SODIUMPWHASH); + else { + g_pAuth->enqueueUnlock(); + break; + } + } + }); +} + +void CSodiumPWHash::waitForInput() { + std::unique_lock lk(m_sCheckerState.inputMutex); + m_bBlockInput = false; + m_sCheckerState.state = SODIUMHASH_INPUT; + m_sCheckerState.inputSubmittedCondition.wait(lk, [this]() { return (m_sCheckerState.state != SODIUMHASH_INPUT) || g_pHyprlock->m_bTerminate; }); + m_bBlockInput = true; +} + +bool CSodiumPWHash::auth() { + static auto const PPWHASH = (Hyprlang::STRING*)getConfigValuePtr("hash"); + const std::string PWHASH = *PPWHASH; + bool rv; + + if (PWHASH.empty() || PWHASH.size() > crypto_pwhash_STRBYTES) { + m_sCheckerState.failText = "Invalid hash. Check config"; + Debug::log(ERR, "[SodiumAuth] Invalid password hash set in configuration"); + rv = false; + } else if (crypto_pwhash_str_verify(PWHASH.c_str(), m_sCheckerState.input.c_str(), m_sCheckerState.input.length()) == 0) { + rv = true; + } else { + m_sCheckerState.failText = "Failed to authenticate"; + Debug::log(LOG, "[SodiumAuth] Failed to authenticate"); + rv = false; + } + m_sCheckerState.input.clear(); + m_sCheckerState.state = SODIUMHASH_IDLE; + return rv; +} + +void CSodiumPWHash::handleInput(const std::string& input) { + std::unique_lock lk(m_sCheckerState.inputMutex); + + if (m_sCheckerState.state != SODIUMHASH_INPUT) + Debug::log(ERR, "SubmitInput called, but auth thread is not waiting for input!"); + + m_sCheckerState.input = input; + m_sCheckerState.state = SODIUMHASH_AUTH; + m_sCheckerState.inputSubmittedCondition.notify_all(); +} + +bool CSodiumPWHash::checkWaiting() { + return m_bBlockInput || (m_sCheckerState.state == SODIUMHASH_AUTH); +} + +std::optional CSodiumPWHash::getLastFailText() { + return m_sCheckerState.failText.empty() ? std::nullopt : std::optional(m_sCheckerState.failText); +} + +std::optional CSodiumPWHash::getLastPrompt() { + return m_sCheckerState.prompt.empty() ? std::nullopt : std::optional(m_sCheckerState.prompt); +} + +void CSodiumPWHash::terminate() { + m_sCheckerState.inputSubmittedCondition.notify_all(); + if (m_thread.joinable()) + m_thread.join(); +} diff --git a/src/auth/SodiumPWHash.hpp b/src/auth/SodiumPWHash.hpp new file mode 100644 index 00000000..95ac4911 --- /dev/null +++ b/src/auth/SodiumPWHash.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "Auth.hpp" + +#include +#include +#include +#include +#include + +class CSodiumPWHash : public IAuthImplementation { + public: + enum EState { + SODIUMHASH_IDLE = 0, + SODIUMHASH_INPUT = 1, + SODIUMHASH_AUTH = 2, + }; + struct SCheckerState { + std::string input = ""; + std::string prompt = ""; + std::string failText = ""; + + std::mutex inputMutex; + std::condition_variable inputSubmittedCondition; + + EState state = SODIUMHASH_IDLE; + }; + + CSodiumPWHash(); + + virtual ~CSodiumPWHash(); + virtual eAuthImplementations getImplType() { + return AUTH_IMPL_SODIUMPWHASH; + } + 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: + bool m_bBlockInput; + Hyprlang::CConfig m_config; + std::thread m_thread; + SCheckerState m_sCheckerState; + + bool auth(); + void* const* getConfigValuePtr(const std::string& name); + void waitForInput(); +}; diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 00365d7e..9035007e 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -226,6 +226,8 @@ void CConfigManager::init() { 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:fingerprint:retry_delay", Hyprlang::INT{250}); + m_config.addConfigValue("auth:sodium:enabled", Hyprlang::INT{0}); + m_config.addConfigValue("auth:sodium:secret_file", Hyprlang::STRING{"hyprlock_sodium"}); m_config.addConfigValue("animations:enabled", Hyprlang::INT{1});