mirror of
https://github.com/hyprwm/hyprlock.git
synced 2024-12-22 05:19:48 +01:00
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:
parent
578246b996
commit
cdef6593b6
9 changed files with 297 additions and 9 deletions
|
@ -49,6 +49,7 @@ pkg_check_modules(
|
|||
egl
|
||||
opengl
|
||||
xkbcommon
|
||||
libgcrypt
|
||||
libjpeg
|
||||
libwebp
|
||||
libmagic
|
||||
|
|
17
README.md
17
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
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
file,
|
||||
libdrm,
|
||||
libGL,
|
||||
libgcrypt,
|
||||
libjpeg,
|
||||
libwebp,
|
||||
libxkbcommon,
|
||||
|
@ -39,6 +40,7 @@ stdenv.mkDerivation {
|
|||
file
|
||||
libdrm
|
||||
libGL
|
||||
libgcrypt
|
||||
libjpeg
|
||||
libwebp
|
||||
libxkbcommon
|
||||
|
|
|
@ -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)"});
|
||||
|
|
|
@ -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
25
src/core/IAuth.hpp
Normal 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
191
src/core/PwAuth.cpp
Normal 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
47
src/core/PwAuth.hpp
Normal 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();
|
||||
};
|
|
@ -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>();
|
||||
|
|
Loading…
Reference in a new issue