diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index ff316117d5..d17dcc2442 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -113,6 +113,7 @@ jobs: -fprofile-arcs -ftest-coverage -fprofile-abs-path + -fprofile-update=atomic - os: ubuntu-latest buildname: Qt 6 Clang @@ -201,13 +202,13 @@ jobs: ${{runner.workspace}}/build/copyq/${{ matrix.cmake_preset }} run: | lcov --capture --directory . \ + --base-directory '${{github.workspace}}' \ + --no-external \ --rc branch_coverage=1 \ - --rc geninfo_unexecuted_blocks=1 \ - --ignore-errors mismatch \ + --rc geninfo_no_exception_branch=1 \ --output-file coverage-full.info - # Remove system and 3rd party files + # Remove 3rd party files in source tree lcov --remove coverage-full.info \ - '/usr/*' \ '*/plugins/itemfakevim/fakevim/*' \ '*/src/gui/fix_icon_id.h' \ --output-file coverage.info diff --git a/src/gui/clipboardbrowserplaceholder.cpp b/src/gui/clipboardbrowserplaceholder.cpp index a0745aedc6..93c7f71ede 100644 --- a/src/gui/clipboardbrowserplaceholder.cpp +++ b/src/gui/clipboardbrowserplaceholder.cpp @@ -158,6 +158,9 @@ void ClipboardBrowserPlaceholder::setStoreItems(bool store) void ClipboardBrowserPlaceholder::setEncryptedExpireSeconds(int seconds) { + if (shouldPromptForLockedTabPassword()) { + m_passwordExpiredAt = std::chrono::steady_clock::now(); + } m_encryptedExpireSeconds = seconds; restartPasswordExpiry(); } @@ -338,22 +341,18 @@ void ClipboardBrowserPlaceholder::restartExpiring() m_timerExpire.stop(); } -int ClipboardBrowserPlaceholder::encryptedExpireSeconds() const -{ - return m_encryptedExpireSeconds > 0 - ? m_encryptedExpireSeconds : m_sharedData->encryptedExpireSeconds; -} - int ClipboardBrowserPlaceholder::encryptedExpireRemainingMs() const { if (!m_sharedData->passwordPrompt) return -1; - const int timeoutSeconds = encryptedExpireSeconds(); + const int timeoutSeconds = m_encryptedExpireSeconds; if (timeoutSeconds <= 0) return -1; - const qint64 elapsedMs = m_sharedData->passwordPrompt->elapsedMsSinceLastSuccessfulPasswordPrompt(); + const auto lastPrompt = m_sharedData->passwordPrompt->lastSuccessfulPasswordPromptTime(); + const auto now = std::chrono::steady_clock::now(); + const auto elapsedMs = std::chrono::duration_cast(now - lastPrompt).count(); const qint64 timeoutMs = static_cast(timeoutSeconds) * 1000; return qMax(0, timeoutMs - elapsedMs); } @@ -363,6 +362,9 @@ bool ClipboardBrowserPlaceholder::shouldPromptForLockedTabPassword() const if (!m_sharedData->tabsEncrypted || !m_sharedData->passwordPrompt || !m_sharedData->encryptionKey.isValid()) return false; + if (m_passwordExpiredAt > m_sharedData->passwordPrompt->lastSuccessfulPasswordPromptTime()) + return true; + const int remainingMs = encryptedExpireRemainingMs(); return remainingMs == 0; } diff --git a/src/gui/clipboardbrowserplaceholder.h b/src/gui/clipboardbrowserplaceholder.h index ae7e7f0442..36271ebf07 100644 --- a/src/gui/clipboardbrowserplaceholder.h +++ b/src/gui/clipboardbrowserplaceholder.h @@ -69,7 +69,6 @@ class ClipboardBrowserPlaceholder final : public QWidget bool canExpire() const; bool hasActiveFocus() const; - int encryptedExpireSeconds() const; int encryptedExpireRemainingMs() const; bool shouldPromptForLockedTabPassword() const; void restartPasswordExpiry(); @@ -93,4 +92,5 @@ class ClipboardBrowserPlaceholder final : public QWidget QTimer m_timerExpire; QTimer m_timerPasswordExpire; QByteArray m_data; + std::chrono::steady_clock::time_point m_passwordExpiredAt; }; diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index eb4c14136e..96bc3f24d5 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1703,7 +1703,9 @@ ClipboardBrowserPlaceholder *MainWindow::createTab(const QString &name, TabNameM const TabProperties tab = tabs.tabProperties(name); placeholder->setStoreItems(tab.storeItems); - placeholder->setEncryptedExpireSeconds(tab.encryptedExpireSeconds); + placeholder->setEncryptedExpireSeconds( + tab.encryptedExpireSeconds > 0 + ? tab.encryptedExpireSeconds : m_sharedData->encryptedExpireSeconds); int maxItemCount = tab.maxItemCount; if (maxItemCount <= 0) diff --git a/src/gui/passwordprompt.cpp b/src/gui/passwordprompt.cpp index 1ffad23852..1d23358439 100644 --- a/src/gui/passwordprompt.cpp +++ b/src/gui/passwordprompt.cpp @@ -18,7 +18,7 @@ PasswordPrompt::PasswordPrompt(QWidget *parent) : QObject(parent) , m_parent(parent) { - m_lastSuccessfulPromptElapsed.start(); + m_lastSuccessfulPromptTime = std::chrono::steady_clock::now(); } void PasswordPrompt::prompt(PasswordSource source, Callback callback) @@ -47,11 +47,6 @@ Encryption::EncryptionKey PasswordPrompt::prompt(PasswordSource source) return key; } -qint64 PasswordPrompt::elapsedMsSinceLastSuccessfulPasswordPrompt() const -{ - return m_lastSuccessfulPromptElapsed.elapsed(); -} - void PasswordPrompt::runPromptQueue() { if (m_promptInProgress || m_pendingPrompts.isEmpty()) @@ -77,8 +72,9 @@ void PasswordPrompt::runPromptQueue() << (key.isValid() ? "with success" : "without success"); self->m_lastPromptKey = key; - if (key.isValid() && passwordEnteredManually) - self->m_lastSuccessfulPromptElapsed.start(); + if (key.isValid() && passwordEnteredManually) { + self->m_lastSuccessfulPromptTime = std::chrono::steady_clock::now(); + } if (!self->m_pendingPrompts.isEmpty()) { PendingPrompt pendingPrompt = std::move(self->m_pendingPrompts.first()); diff --git a/src/gui/passwordprompt.h b/src/gui/passwordprompt.h index 3ee0d81b9d..0967ac403b 100644 --- a/src/gui/passwordprompt.h +++ b/src/gui/passwordprompt.h @@ -23,7 +23,8 @@ class PasswordPrompt final : public QObject void prompt(PasswordSource source, Callback callback); Encryption::EncryptionKey prompt(PasswordSource source); - qint64 elapsedMsSinceLastSuccessfulPasswordPrompt() const; + std::chrono::steady_clock::time_point lastSuccessfulPasswordPromptTime() const + { return m_lastSuccessfulPromptTime; } private: struct PendingPrompt { @@ -36,6 +37,6 @@ class PasswordPrompt final : public QObject QWidget *m_parent = nullptr; bool m_promptInProgress = false; Encryption::EncryptionKey m_lastPromptKey; - QElapsedTimer m_lastSuccessfulPromptElapsed; + std::chrono::steady_clock::time_point m_lastSuccessfulPromptTime; QList m_pendingPrompts; }; diff --git a/src/tests/tests.h b/src/tests/tests.h index 5dfa9b2cec..662e75ecf6 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -334,7 +334,8 @@ private slots: void handleUnexpectedTypes(); void expireTabs(); - void expireEncryptedTabsPasswordAcrossTabs(); + void expireEncryptionPassword(); + void expireEncryptionPasswordOnConfigChange(); void dragNDropItemOrder(); void dragNDropItemToTabTree(); diff --git a/src/tests/tests_encryption_expire.cpp b/src/tests/tests_encryption_expire.cpp new file mode 100644 index 0000000000..e7b8026b3f --- /dev/null +++ b/src/tests/tests_encryption_expire.cpp @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "test_utils.h" +#include "tests.h" + +void Tests::expireEncryptionPassword() +{ +#ifdef WITH_QCA_ENCRYPTION + const QString tab1 = testTab(1); + const QString tab2 = testTab(2); + const Args args1 = Args("tab") << tab1 << "separator" << " "; + const Args args2 = Args("tab") << tab2 << "separator" << " "; + + RUN("config" << "encrypt_tabs" << "true", "true\n"); + + RUN(args1 << "add" << "A1", ""); + RUN(args2 << "add" << "B1", ""); + + // Setting an expiration should not cause any expiration yet + RUN("config" << "expire_encrypted_tab_seconds" << "1", "1\n"); + KEYS(clipboardBrowserId); + QTest::qWait(1500); + KEYS(clipboardBrowserId); + + RUN("config" << "expire_encrypted_tab_seconds" << "2", "2\n"); + KEYS(clipboardBrowserId); + + TEST( m_test->stopServer() ); + m_test->setEnv("COPYQ_PASSWORD", ""); + TEST( m_test->startServer() ); + + // Start expiration timer from manual password entry. + RUN_MULTIPLE( + [&]{ RUN("show" << tab1, ""); }, + [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); } + ); + + RUN_MULTIPLE( + [&]{ KEYS(clipboardBrowserId); }, + [&]{ RUN("selectedTab", tab1 + "\n"); }, + [&]{ RUN(args1 << "read" << "0", "A1"); }, + [&]{ RUN(args2 << "read" << "0", "B1"); } + ); + + RUN("show" << tab2, ""); + RUN("show" << tab1, ""); + RUN("show" << tab2, ""); + + KEYS(clipboardBrowserId); + QTest::qWait(2500); + KEYS(clipboardBrowserId); + + RUN_MULTIPLE( + [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); }, + [&]{ RUN(args1 << "read" << "0", "A1"); } + ); + KEYS(clipboardBrowserId); + RUN("selectedTab", tab2 + "\n"); + + RUN("show" << tab1, ""); + RUN_MULTIPLE( + [&]{ RUN("selectedTab", tab1 + "\n"); }, + [&]{ KEYS(clipboardBrowserId); }, + [&]{ RUN(args1 << "read" << "0", "A1"); } + ); + + RUN("show" << tab2, ""); + RUN(args2 << "read" << "0", "B1"); + + // Expire again: active tab should stay unlocked. + KEYS(clipboardBrowserId); + QTest::qWait(2500); + KEYS(clipboardBrowserId); + + RUN("show" << tab2, ""); + RUN(args2 << "read" << "0", "B1"); + + // Switching to the other expired tab should prompt again. + RUN_MULTIPLE( + [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); }, + [&]{ RUN("show" << tab1, ""); } + ); + RUN(args1 << "read" << "0", "A1"); + KEYS(clipboardBrowserId); + + // Avoid asking password for a new tab (if prompted recently) + const QString tab3 = testTab(3); + RUN("show" << tab3, ""); + + KEYS(clipboardBrowserId); + QTest::qWait(2500); + KEYS(clipboardBrowserId); + + // Read multiple expired tabs items, wait for password prompt once + RUN_MULTIPLE( + [&]{ RUN(args1 << "read" << "0", "A1"); }, + [&]{ RUN(args2 << "read" << "0", "B1"); }, + [&]{ QTest::qWait(200); KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); } + ); + + KEYS(clipboardBrowserId); + QTest::qWait(2500); + KEYS(clipboardBrowserId); + + // Expired tabs should require password, even if the configuration changed + RUN("config" << "expire_encrypted_tab_seconds" << "0", "0\n"); + RUN_MULTIPLE( + [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); }, + [&]{ RUN("show" << tab1, ""); } + ); + KEYS(clipboardBrowserId); +#else + SKIP("Encryption support not built-in"); +#endif +} + +void Tests::expireEncryptionPasswordOnConfigChange() +{ + // Expired tabs should require password, + // even if the expiration was disabled afterwards +#ifdef WITH_QCA_ENCRYPTION + const QString tab1 = testTab(1); + const Args args1{"tab", tab1}; + RUN(args1 << "add" << "A1", ""); + + RUN("config" << "encrypt_tabs" << "true", "true\n"); + + // Setting an expiration should not cause any expiration yet + RUN("config" << "expire_encrypted_tab_seconds" << "1", "1\n"); + KEYS(clipboardBrowserId); + QTest::qWait(1500); + KEYS(clipboardBrowserId); + + RUN("config" << "expire_encrypted_tab_seconds" << "0", "0\n"); + RUN_MULTIPLE( + [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); }, + [&]{ RUN("show" << tab1, ""); } + ); + KEYS(clipboardBrowserId); + RUN(args1 << "read" << "0", "A1"); +#else + SKIP("Encryption support not built-in"); +#endif +} diff --git a/src/tests/tests_expire.cpp b/src/tests/tests_expire.cpp index 04b0baa040..fb66768dc8 100644 --- a/src/tests/tests_expire.cpp +++ b/src/tests/tests_expire.cpp @@ -59,102 +59,3 @@ void Tests::expireTabs() RUN("tab", "temp2\n" + QString(clipboardTabName) + "\n"); RUN("size()", "0\n"); } - -void Tests::expireEncryptedTabsPasswordAcrossTabs() -{ -#ifdef WITH_QCA_ENCRYPTION - const QString tab1 = testTab(1); - const QString tab2 = testTab(2); - const Args args1 = Args("tab") << tab1 << "separator" << " "; - const Args args2 = Args("tab") << tab2 << "separator" << " "; - - RUN("config" << "encrypt_tabs" << "true", "true\n"); - - RUN(args1 << "add" << "A1", ""); - RUN(args2 << "add" << "B1", ""); - - // Setting an expiration should not cause any expiration yet - RUN("config" << "expire_encrypted_tab_seconds" << "1", "1\n"); - KEYS(clipboardBrowserId); - QTest::qWait(1500); - KEYS(clipboardBrowserId); - - RUN("config" << "expire_encrypted_tab_seconds" << "2", "2\n"); - KEYS(clipboardBrowserId); - - TEST( m_test->stopServer() ); - m_test->setEnv("COPYQ_PASSWORD", ""); - TEST( m_test->startServer() ); - - // Start expiration timer from manual password entry. - RUN_MULTIPLE( - [&]{ RUN("show" << tab1, ""); }, - [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); } - ); - - RUN_MULTIPLE( - [&]{ KEYS(clipboardBrowserId); }, - [&]{ RUN("selectedTab", tab1 + "\n"); }, - [&]{ RUN(args1 << "read" << "0", "A1"); }, - [&]{ RUN(args2 << "read" << "0", "B1"); } - ); - - RUN("show" << tab2, ""); - RUN("show" << tab1, ""); - RUN("show" << tab2, ""); - - KEYS(clipboardBrowserId); - QTest::qWait(2500); - KEYS(clipboardBrowserId); - - RUN_MULTIPLE( - [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); }, - [&]{ RUN(args1 << "read" << "0", "A1"); } - ); - KEYS(clipboardBrowserId); - RUN("selectedTab", tab2 + "\n"); - - RUN("show" << tab1, ""); - RUN_MULTIPLE( - [&]{ RUN("selectedTab", tab1 + "\n"); }, - [&]{ KEYS(clipboardBrowserId); }, - [&]{ RUN(args1 << "read" << "0", "A1"); } - ); - - RUN("show" << tab2, ""); - RUN(args2 << "read" << "0", "B1"); - - // Expire again: active tab should stay unlocked. - KEYS(clipboardBrowserId); - QTest::qWait(2500); - KEYS(clipboardBrowserId); - - RUN("show" << tab2, ""); - RUN(args2 << "read" << "0", "B1"); - - // Switching to the other expired tab should prompt again. - RUN_MULTIPLE( - [&]{ KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); }, - [&]{ RUN("show" << tab1, ""); } - ); - RUN(args1 << "read" << "0", "A1"); - KEYS(clipboardBrowserId); - - // Avoid asking password for a new tab (if prompted recently) - const QString tab3 = testTab(3); - RUN("show" << tab3, ""); - - KEYS(clipboardBrowserId); - QTest::qWait(2500); - KEYS(clipboardBrowserId); - - // Read multiple expired tabs items, wait for password prompt once - RUN_MULTIPLE( - [&]{ RUN(args1 << "read" << "0", "A1"); }, - [&]{ RUN(args2 << "read" << "0", "B1"); }, - [&]{ QTest::qWait(200); KEYS(passwordEntryCurrentId << ":TEST123" << "ENTER"); } - ); -#else - SKIP("Encryption support not built-in"); -#endif -}