Skip to content

Commit cdef659

Browse files
committed
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.
1 parent 578246b commit cdef659

File tree

9 files changed

+297
-9
lines changed

9 files changed

+297
-9
lines changed

CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pkg_check_modules(
4949
egl
5050
opengl
5151
xkbcommon
52+
libgcrypt
5253
libjpeg
5354
libwebp
5455
libmagic

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ Hyprland's simple, yet multi-threaded and GPU-accelerated screen locking utility
1414
## Docs / Configuration
1515
[See the wiki](https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock/)
1616

17+
### Password hash configuration
18+
If PAM authentication is unavailable to you, you can use password hash authentication via `libgcrypt`.
19+
Activated it by setting `general:password_hash` to the desired value as a string of hexadecimal numbers.
20+
You can select the hash function with `general:password_hash` with the default being `SHA256`.
21+
Other known hash functions are `SHA3-256`, `SHA512_256` or `SHAKE128`.
22+
You can also salt the by setting `hash_salt`.
23+
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.
24+
25+
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:
26+
``` sh
27+
# Produces 10 bytes salt
28+
SALT=$(openssl rand -hex 10)
29+
printf "hash_salt = %s\n" "$SALT"
30+
# Enter your password (no echo) and press ENTER.
31+
{ read -s v; echo "$v${SALT}" } | openssl sha3-256 -hex
32+
```
33+
1734
## Arch install
1835
```sh
1936
pacman -S hyprlock # binary x86 tagged release

nix/default.nix

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
file,
88
libdrm,
99
libGL,
10+
libgcrypt,
1011
libjpeg,
1112
libwebp,
1213
libxkbcommon,
@@ -39,6 +40,7 @@ stdenv.mkDerivation {
3940
file
4041
libdrm
4142
libGL
43+
libgcrypt
4244
libjpeg
4345
libwebp
4446
libxkbcommon

src/config/ConfigManager.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ void CConfigManager::init() {
172172
m_config.addConfigValue("general:ignore_empty_input", Hyprlang::INT{0});
173173
m_config.addConfigValue("general:immediate_render", Hyprlang::INT{0});
174174
m_config.addConfigValue("general:pam_module", Hyprlang::STRING{"hyprlock"});
175+
m_config.addConfigValue("general:hash_algorithm", Hyprlang::STRING{"SHA256"});
176+
m_config.addConfigValue("general:hash_salt", Hyprlang::STRING{""});
177+
m_config.addConfigValue("general:password_hash", Hyprlang::STRING{""});
175178
m_config.addConfigValue("general:fractional_scaling", Hyprlang::INT{2});
176179
m_config.addConfigValue("general:enable_fingerprint", Hyprlang::INT{0});
177180
m_config.addConfigValue("general:fingerprint_ready_message", Hyprlang::STRING{"(Scan fingerprint to unlock)"});

src/core/Auth.hpp

+4-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#pragma once
22

3-
#include <memory>
3+
#include "IAuth.hpp"
44
#include <optional>
55
#include <string>
66
#include <mutex>
77
#include <condition_variable>
88

9-
class CAuth {
9+
class CAuth : public CIAuth {
1010
public:
1111
struct SPamConversationState {
1212
std::string input = "";
@@ -21,7 +21,8 @@ class CAuth {
2121
bool failTextFromPam = false;
2222
};
2323

24-
CAuth();
24+
explicit CAuth();
25+
CAuth(const CAuth&) = delete;
2526

2627
void start();
2728
bool auth();
@@ -37,9 +38,6 @@ class CAuth {
3738

3839
void terminate();
3940

40-
// Should only be set via the main thread
41-
bool m_bDisplayFailText = false;
42-
4341
private:
4442
SPamConversationState m_sConversationState;
4543

@@ -50,5 +48,3 @@ class CAuth {
5048

5149
void resetConversation();
5250
};
53-
54-
inline std::unique_ptr<CAuth> g_pAuth;

src/core/IAuth.hpp

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#pragma once
2+
3+
#include <memory>
4+
#include <optional>
5+
#include <string>
6+
7+
class CIAuth {
8+
public:
9+
virtual void start() = 0;
10+
virtual bool auth() = 0;
11+
virtual bool isAuthenticated() = 0;
12+
virtual void waitForInput() = 0;
13+
virtual void submitInput(std::string input) = 0;
14+
virtual std::optional<std::string> getLastFailText() = 0;
15+
virtual std::optional<std::string> getLastPrompt() = 0;
16+
virtual bool checkWaiting() = 0;
17+
virtual void terminate() = 0;
18+
19+
CIAuth() = default;
20+
21+
// Should only be set via the main thread
22+
bool m_bDisplayFailText = false;
23+
};
24+
25+
inline std::unique_ptr<CIAuth> g_pAuth;

src/core/PwAuth.cpp

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#include "PwAuth.hpp"
2+
#include "hyprlock.hpp"
3+
#include "../helpers/Log.hpp"
4+
#include "../config/ConfigManager.hpp"
5+
#include <cstddef>
6+
#include <cstring>
7+
#define GCRYPT_NO_DEPRECATED
8+
#define GCRYPT_NO_MPI_MACROS
9+
#define NEED_LIBGCRYPT_VERSION nullptr
10+
#include <gcrypt.h>
11+
12+
using namespace std::chrono_literals;
13+
14+
static std::unique_ptr<unsigned char[]> hex2Bytes(const std::string& hex) noexcept {
15+
auto bytes = std::make_unique<unsigned char[]>(hex.length() / 2);
16+
for (std::size_t i = 0; i < hex.length() / 2; ++i) {
17+
try {
18+
auto v = std::stoi(hex.substr(2 * i, 2), nullptr, 16);
19+
if (v >= 0)
20+
bytes[i] = static_cast<unsigned char>(v);
21+
else
22+
throw std::invalid_argument("invalid hex value");
23+
} catch (std::invalid_argument const& e) {
24+
Debug::log(ERR, "auth: invalid password_hash");
25+
bytes = nullptr;
26+
} catch (std::out_of_range const& e) {
27+
// Should never happen, as 2-byte substrings should never go o-o-r.
28+
Debug::log(CRIT, "auth: implementation error in hex2Bytes conversion");
29+
bytes = nullptr;
30+
}
31+
}
32+
return bytes;
33+
}
34+
35+
static std::string bytes2Hex(const unsigned char* bytes, std::size_t len) {
36+
std::stringstream ss;
37+
ss << std::setw(2) << std::setfill('0') << std::hex;
38+
for (std::size_t i = 0; i < len; ++i)
39+
ss << (int)bytes[i];
40+
return ss.str();
41+
}
42+
43+
CPwAuth::CPwAuth() {
44+
45+
if (gcry_check_version(NEED_LIBGCRYPT_VERSION))
46+
gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
47+
else
48+
Debug::log(CRIT, "libgcrypt too old");
49+
50+
if (gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
51+
52+
// Handle the hash algorithm
53+
static auto const ALGO = *(Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:hash_algorithm"));
54+
m_iAlgo = gcry_md_map_name(ALGO);
55+
m_iDigestLen = gcry_md_get_algo_dlen(m_iAlgo);
56+
if (m_iAlgo) {
57+
static auto const err = gcry_err_code(gcry_md_test_algo(m_iAlgo));
58+
if (err == GPG_ERR_NO_ERROR) {
59+
60+
// Handle the salt
61+
static auto* const SALT = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:hash_salt"));
62+
m_szSalt = std::string(*SALT);
63+
64+
// Handle the expected hash
65+
static auto* const HASH = (Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:password_hash"));
66+
static auto const hash = std::string(*HASH);
67+
if (hash.empty() || (hash.size() % 2) || (hash.length() != 2uL * m_iDigestLen)) {
68+
Debug::log(ERR, "auth: password_hash has incorrect length for algorithm {} (got: {}, expected: {})", ALGO, hash.size(), 2uL * m_iDigestLen);
69+
m_bLibFailed = true;
70+
} else {
71+
m_aHash = hex2Bytes(hash);
72+
if (!m_aHash || hash.empty())
73+
m_bLibFailed = true;
74+
}
75+
} else {
76+
// Might be due to FIPS mode
77+
Debug::log(CRIT, "auth: hash algorithm unavailable: {}", ALGO);
78+
m_bLibFailed = true;
79+
}
80+
} else {
81+
Debug::log(ERR, "auth: unknown hash algorithm: {}", ALGO);
82+
m_bLibFailed = true;
83+
}
84+
} else {
85+
Debug::log(CRIT, "libgcrypt could not be initialized");
86+
m_bLibFailed = true;
87+
}
88+
}
89+
90+
static void passwordCheckTimerCallback(std::shared_ptr<CTimer> self, void* data) {
91+
g_pHyprlock->onPasswordCheckTimer();
92+
}
93+
94+
void CPwAuth::start() {
95+
std::thread([this]() {
96+
reset();
97+
98+
waitForInput();
99+
100+
// For grace or SIGUSR1 unlocks
101+
if (g_pHyprlock->isUnlocked())
102+
return;
103+
104+
const auto AUTHENTICATED = auth();
105+
m_bAuthenticated = AUTHENTICATED;
106+
107+
if (g_pHyprlock->isUnlocked())
108+
return;
109+
110+
g_pHyprlock->addTimer(1ms, passwordCheckTimerCallback, nullptr);
111+
}).detach();
112+
}
113+
114+
bool CPwAuth::auth() {
115+
if (m_bLibFailed)
116+
return true;
117+
118+
bool verdict;
119+
auto digest = std::make_unique<unsigned char[]>(m_iDigestLen);
120+
auto istr = m_sState.input;
121+
istr.append(m_szSalt);
122+
123+
gcry_md_hash_buffer(m_iAlgo, digest.get(), istr.c_str(), istr.size());
124+
Debug::log(TRACE, "auth: resulting hash {}", bytes2Hex(digest.get(), m_iDigestLen));
125+
Debug::log(TRACE, "auth: expected hash {}", bytes2Hex(m_aHash.get(), m_iDigestLen));
126+
verdict = !std::memcmp(m_aHash.get(), digest.get(), m_iDigestLen);
127+
128+
if (verdict)
129+
Debug::log(LOG, "auth: authenticated");
130+
else
131+
Debug::log(ERR, "auth: unsuccessful");
132+
133+
m_sState.authenticating = false;
134+
/// DEBUG Code; replace constant with verdict
135+
return verdict;
136+
}
137+
138+
bool CPwAuth::isAuthenticated() {
139+
return m_bAuthenticated;
140+
}
141+
142+
// clearing the input must be done from the main thread
143+
static void clearInputTimerCallback(std::shared_ptr<CTimer> self, void* data) {
144+
g_pHyprlock->clearPasswordBuffer();
145+
}
146+
147+
void CPwAuth::waitForInput() {
148+
g_pHyprlock->addTimer(1ms, clearInputTimerCallback, nullptr);
149+
if (m_bLibFailed)
150+
return;
151+
152+
std::unique_lock<std::mutex> lk(m_sState.inputMutex);
153+
m_bBlockInput = false;
154+
m_sState.inputRequested = true;
155+
m_sState.inputSubmittedCondition.wait(lk, [this] { return !m_sState.inputRequested || g_pHyprlock->m_bTerminate; });
156+
m_bBlockInput = true;
157+
}
158+
159+
void CPwAuth::submitInput(std::string input) {
160+
std::unique_lock<std::mutex> lk(m_sState.inputMutex);
161+
if (!m_sState.inputRequested)
162+
Debug::log(ERR, "SubmitInput called, but the auth thread is not waiting for input!");
163+
m_sState.input = input;
164+
m_sState.inputRequested = false;
165+
m_sState.authenticating = true;
166+
m_sState.inputSubmittedCondition.notify_all();
167+
}
168+
169+
std::optional<std::string> CPwAuth::getLastPrompt() {
170+
std::string pmpt = "Password: ";
171+
return pmpt;
172+
}
173+
174+
std::optional<std::string> CPwAuth::getLastFailText() {
175+
std::string ret = "Password incorrect";
176+
return ret;
177+
}
178+
179+
bool CPwAuth::checkWaiting() {
180+
return m_bBlockInput;
181+
}
182+
183+
void CPwAuth::terminate() {
184+
m_sState.inputSubmittedCondition.notify_all();
185+
}
186+
187+
void CPwAuth::reset() {
188+
m_sState.input = "";
189+
m_sState.inputRequested = false;
190+
m_sState.authenticating = false;
191+
}

src/core/PwAuth.hpp

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
#include "IAuth.hpp"
4+
#include <condition_variable>
5+
#include <mutex>
6+
#include <optional>
7+
#include <string>
8+
9+
class CPwAuth : public CIAuth {
10+
public:
11+
struct SState {
12+
std::string input = "";
13+
14+
std::mutex inputMutex;
15+
std::condition_variable inputSubmittedCondition;
16+
17+
bool inputRequested = false;
18+
bool authenticating = false;
19+
};
20+
21+
explicit CPwAuth();
22+
CPwAuth(const CPwAuth&) = delete;
23+
24+
void start();
25+
bool auth();
26+
bool isAuthenticated();
27+
void waitForInput();
28+
void submitInput(std::string input);
29+
30+
std::optional<std::string> getLastPrompt();
31+
std::optional<std::string> getLastFailText();
32+
33+
bool checkWaiting();
34+
void terminate();
35+
36+
private:
37+
SState m_sState;
38+
bool m_bBlockInput = true;
39+
bool m_bAuthenticated = false;
40+
bool m_bLibFailed = false;
41+
std::unique_ptr<unsigned char[]> m_aHash;
42+
std::string m_szSalt;
43+
int m_iAlgo = -1;
44+
unsigned int m_iDigestLen = 0;
45+
46+
void reset();
47+
};

src/core/hyprlock.cpp

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
#include "../helpers/Log.hpp"
33
#include "../config/ConfigManager.hpp"
44
#include "../renderer/Renderer.hpp"
5+
#include "IAuth.hpp"
56
#include "Auth.hpp"
7+
#include "PwAuth.hpp"
68
#include "Egl.hpp"
79
#include "Fingerprint.hpp"
810
#include "linux-dmabuf-unstable-v1-protocol.h"
@@ -416,7 +418,11 @@ void CHyprlock::run() {
416418
exit(1);
417419
}
418420

419-
g_pAuth = std::make_unique<CAuth>();
421+
auto H = std::string(*(Hyprlang::STRING*)(g_pConfigManager->getValuePtr("general:password_hash")));
422+
if (H.empty())
423+
g_pAuth = std::make_unique<CAuth>();
424+
else
425+
g_pAuth = std::make_unique<CPwAuth>();
420426
g_pAuth->start();
421427

422428
g_pFingerprint = std::make_unique<CFingerprint>();

0 commit comments

Comments
 (0)