diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 7eef5f7..ff6c3db 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -243,6 +243,9 @@ void CConfigManager::init() { m_config.addConfigValue("general:text_trim", Hyprlang::INT{1}); m_config.addConfigValue("general:hide_cursor", Hyprlang::INT{0}); m_config.addConfigValue("general:ignore_empty_input", Hyprlang::INT{0}); + // WARNING: auto_submit_after leaks the exact password length to observers. + // Do not enable it with a normal PAM/system password; it is intended only for PIN-like configurations. + m_config.addConfigValue("general:auto_submit_after", Hyprlang::INT{0}); m_config.addConfigValue("general:immediate_render", Hyprlang::INT{0}); m_config.addConfigValue("general:fractional_scaling", Hyprlang::INT{2}); m_config.addConfigValue("general:screencopy_mode", Hyprlang::INT{0}); diff --git a/src/core/LockSurface.cpp b/src/core/LockSurface.cpp index cbc990e..42a8745 100644 --- a/src/core/LockSurface.cpp +++ b/src/core/LockSurface.cpp @@ -158,6 +158,8 @@ void CSessionLockSurface::render() { } needsFrame = FEEDBACK.needsFrame || g_pAnimationManager->shouldTickForNext(); + + g_pHyprlock->onLockSurfaceRendered(); } void CSessionLockSurface::onCallback() { diff --git a/src/core/hyprlock.cpp b/src/core/hyprlock.cpp index a9567b6..5632b18 100644 --- a/src/core/hyprlock.cpp +++ b/src/core/hyprlock.cpp @@ -539,6 +539,8 @@ bool CHyprlock::isLockAquired() { } void CHyprlock::clearPasswordBuffer() { + cancelPendingAutoSubmit(); + if (m_sPasswordState.passBuffer.empty()) return; @@ -663,10 +665,13 @@ void CHyprlock::handleKeySym(xkb_keysym_t sym, bool composed) { if (SYM == XKB_KEY_Escape || (m_bCtrl && (SYM == XKB_KEY_u || SYM == XKB_KEY_BackSpace || SYM == XKB_KEY_a))) { Log::logger->log(Log::INFO, "Clearing password buffer"); + cancelPendingAutoSubmit(); m_sPasswordState.passBuffer = ""; } else if (SYM == XKB_KEY_Return || SYM == XKB_KEY_KP_Enter) { Log::logger->log(Log::INFO, "Authenticating"); + cancelPendingAutoSubmit(); + static const auto IGNOREEMPTY = g_pConfigManager->getValue("general:ignore_empty_input"); if (m_sPasswordState.passBuffer.empty() && *IGNOREEMPTY) { @@ -676,6 +681,8 @@ void CHyprlock::handleKeySym(xkb_keysym_t sym, bool composed) { g_pAuth->submitInput(m_sPasswordState.passBuffer); } else if (SYM == XKB_KEY_BackSpace || SYM == XKB_KEY_Delete) { + cancelPendingAutoSubmit(); + if (m_sPasswordState.passBuffer.length() > 0) { // handle utf-8 while ((m_sPasswordState.passBuffer.back() & 0xc0) == 0x80) @@ -691,11 +698,75 @@ void CHyprlock::handleKeySym(xkb_keysym_t sym, bool composed) { int len = (composed) ? xkb_compose_state_get_utf8(g_pSeatManager->m_pXKBComposeState, buf, sizeof(buf)) /* nullbyte */ + 1 : xkb_keysym_to_utf8(SYM, buf, sizeof(buf)) /* already includes a nullbyte */; - if (len > 1) + if (len > 1) { m_sPasswordState.passBuffer += std::string{buf, len - 1}; + + static const auto AUTOSUBMITAFTER = g_pConfigManager->getValue("general:auto_submit_after"); + if (*AUTOSUBMITAFTER > 0 && getPasswordBufferDisplayLen() == static_cast(*AUTOSUBMITAFTER)) + enqueuePendingAutoSubmit(getPasswordBufferDisplayLen()); + } } } +void CHyprlock::enqueuePendingAutoSubmit(size_t expectedDisplayLen) { + if (m_sAutoSubmitState.pending) + return; + + const auto READYOUTPUTS = std::ranges::count_if(m_vOutputs, [](const auto& output) { return output && output->m_sessionLockSurface && output->m_sessionLockSurface->readyForFrame; }); + + m_sAutoSubmitState.pending = true; + m_sAutoSubmitState.expectedBufferLen = m_sPasswordState.passBuffer.length(); + m_sAutoSubmitState.expectedDisplayLen = expectedDisplayLen; + // The first render after a new character only moves the dot animation target. + // Wait for follow-up renders so the final dot is actually visible before auth clears the buffer. + m_sAutoSubmitState.rendersUntilSubmit = std::max(1, READYOUTPUTS) * 3; + m_sAutoSubmitState.fallbackTimer = addTimer(std::chrono::milliseconds(500), [this](ASP self, void* data) { submitPendingAutoSubmit(); }, nullptr); +} + +void CHyprlock::cancelPendingAutoSubmit() { + if (m_sAutoSubmitState.fallbackTimer) { + m_sAutoSubmitState.fallbackTimer->cancel(); + m_sAutoSubmitState.fallbackTimer.reset(); + } + + m_sAutoSubmitState.pending = false; + m_sAutoSubmitState.expectedBufferLen = 0; + m_sAutoSubmitState.expectedDisplayLen = 0; + m_sAutoSubmitState.rendersUntilSubmit = 0; +} + +void CHyprlock::submitPendingAutoSubmit() { + if (!m_sAutoSubmitState.pending) + return; + + const auto EXPECTEDBUFFERLEN = m_sAutoSubmitState.expectedBufferLen; + const auto EXPECTEDDISPLAYLEN = m_sAutoSubmitState.expectedDisplayLen; + + cancelPendingAutoSubmit(); + + if (m_sPasswordState.passBuffer.length() != EXPECTEDBUFFERLEN || getPasswordBufferDisplayLen() != EXPECTEDDISPLAYLEN) + return; + + Log::logger->log(Log::INFO, "Auto-submitting password"); + g_pAuth->submitInput(m_sPasswordState.passBuffer); +} + +void CHyprlock::onLockSurfaceRendered() { + if (!m_sAutoSubmitState.pending) + return; + + if (m_sPasswordState.passBuffer.length() != m_sAutoSubmitState.expectedBufferLen || getPasswordBufferDisplayLen() != m_sAutoSubmitState.expectedDisplayLen) { + cancelPendingAutoSubmit(); + return; + } + + if (m_sAutoSubmitState.rendersUntilSubmit > 0) + --m_sAutoSubmitState.rendersUntilSubmit; + + if (m_sAutoSubmitState.rendersUntilSubmit == 0) + submitPendingAutoSubmit(); +} + void CHyprlock::onClick(uint32_t button, bool down, const Vector2D& pos) { if (!down) return; diff --git a/src/core/hyprlock.hpp b/src/core/hyprlock.hpp index 0c9dbd5..fed2d4d 100644 --- a/src/core/hyprlock.hpp +++ b/src/core/hyprlock.hpp @@ -57,6 +57,7 @@ class CHyprlock { void handleKeySym(xkb_keysym_t sym, bool compose); void onPasswordCheckTimer(); void clearPasswordBuffer(); + void onLockSurfaceRendered(); bool passwordCheckWaiting(); std::optional passwordLastFailReason(); @@ -124,6 +125,10 @@ class CHyprlock { bool m_fadeOutOrTerminate = false; bool m_bTerminate = false; + void enqueuePendingAutoSubmit(size_t expectedDisplayLen); + void cancelPendingAutoSubmit(); + void submitPendingAutoSubmit(); + struct { wl_display* display = nullptr; SP registry = nullptr; @@ -146,6 +151,14 @@ class CHyprlock { bool displayFailText = false; } m_sPasswordState; + struct { + ASP fallbackTimer = nullptr; + size_t expectedBufferLen = 0; + size_t expectedDisplayLen = 0; + size_t rendersUntilSubmit = 0; + bool pending = false; + } m_sAutoSubmitState; + struct { std::mutex timersMutex; std::mutex eventRequestMutex;