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.
This commit is contained in:
Michael Raitza 2024-12-02 17:33:46 +01:00
parent 578246b996
commit cdef6593b6
9 changed files with 297 additions and 9 deletions

View file

@ -49,6 +49,7 @@ pkg_check_modules(
egl
opengl
xkbcommon
libgcrypt
libjpeg
libwebp
libmagic

View file

@ -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

View file

@ -7,6 +7,7 @@
file,
libdrm,
libGL,
libgcrypt,
libjpeg,
libwebp,
libxkbcommon,
@ -39,6 +40,7 @@ stdenv.mkDerivation {
file
libdrm
libGL
libgcrypt
libjpeg
libwebp
libxkbcommon

View file

@ -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)"});

View file

@ -1,12 +1,12 @@
#pragma once
#include <memory>
#include "IAuth.hpp"
#include <optional>
#include <string>
#include <mutex>
#include <condition_variable>
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<CAuth> g_pAuth;

25
src/core/IAuth.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <memory>
#include <optional>
#include <string>
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<std::string> getLastFailText() = 0;
virtual std::optional<std::string> 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<CIAuth> g_pAuth;

191
src/core/PwAuth.cpp Normal file
View file

@ -0,0 +1,191 @@
#include "PwAuth.hpp"
#include "hyprlock.hpp"
#include "../helpers/Log.hpp"
#include "../config/ConfigManager.hpp"
#include <cstddef>
#include <cstring>
#define GCRYPT_NO_DEPRECATED
#define GCRYPT_NO_MPI_MACROS
#define NEED_LIBGCRYPT_VERSION nullptr
#include <gcrypt.h>
using namespace std::chrono_literals;
static std::unique_ptr<unsigned char[]> hex2Bytes(const std::string& hex) noexcept {
auto bytes = std::make_unique<unsigned char[]>(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<unsigned char>(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<CTimer> 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<unsigned char[]>(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<CTimer> self, void* data) {
g_pHyprlock->clearPasswordBuffer();
}
void CPwAuth::waitForInput() {
g_pHyprlock->addTimer(1ms, clearInputTimerCallback, nullptr);
if (m_bLibFailed)
return;
std::unique_lock<std::mutex> 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<std::mutex> 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<std::string> CPwAuth::getLastPrompt() {
std::string pmpt = "Password: ";
return pmpt;
}
std::optional<std::string> 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;
}

47
src/core/PwAuth.hpp Normal file
View file

@ -0,0 +1,47 @@
#pragma once
#include "IAuth.hpp"
#include <condition_variable>
#include <mutex>
#include <optional>
#include <string>
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<std::string> getLastPrompt();
std::optional<std::string> getLastFailText();
bool checkWaiting();
void terminate();
private:
SState m_sState;
bool m_bBlockInput = true;
bool m_bAuthenticated = false;
bool m_bLibFailed = false;
std::unique_ptr<unsigned char[]> m_aHash;
std::string m_szSalt;
int m_iAlgo = -1;
unsigned int m_iDigestLen = 0;
void reset();
};

View file

@ -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<CAuth>();
auto H = std::string(*(Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:password_hash")));
if (H.empty())
g_pAuth = std::make_unique<CAuth>();
else
g_pAuth = std::make_unique<CPwAuth>();
g_pAuth->start();
g_pFingerprint = std::make_unique<CFingerprint>();