Skip to content

Commit

Permalink
Add password hash authentication
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spacefrogg committed Dec 10, 2024
1 parent 578246b commit cdef659
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 9 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pkg_check_modules(
egl
opengl
xkbcommon
libgcrypt
libjpeg
libwebp
libmagic
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
file,
libdrm,
libGL,
libgcrypt,
libjpeg,
libwebp,
libxkbcommon,
Expand Down Expand Up @@ -39,6 +40,7 @@ stdenv.mkDerivation {
file
libdrm
libGL
libgcrypt
libjpeg
libwebp
libxkbcommon
Expand Down
3 changes: 3 additions & 0 deletions src/config/ConfigManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)"});
Expand Down
12 changes: 4 additions & 8 deletions src/core/Auth.hpp
Original file line number Diff line number Diff line change
@@ -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 = "";
Expand All @@ -21,7 +21,8 @@ class CAuth {
bool failTextFromPam = false;
};

CAuth();
explicit CAuth();
CAuth(const CAuth&) = delete;

void start();
bool auth();
Expand All @@ -37,9 +38,6 @@ class CAuth {

void terminate();

// Should only be set via the main thread
bool m_bDisplayFailText = false;

private:
SPamConversationState m_sConversationState;

Expand All @@ -50,5 +48,3 @@ class CAuth {

void resetConversation();
};

inline std::unique_ptr<CAuth> g_pAuth;
25 changes: 25 additions & 0 deletions src/core/IAuth.hpp
Original file line number Diff line number Diff line change
@@ -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 changes: 191 additions & 0 deletions src/core/PwAuth.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 47 additions & 0 deletions src/core/PwAuth.hpp
Original file line number Diff line number Diff line change
@@ -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();
};
8 changes: 7 additions & 1 deletion src/core/hyprlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>();
Expand Down

0 comments on commit cdef659

Please sign in to comment.