diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 98b8e25343..24eb2d32b0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: run: | sudo apt update sudo apt install build-essential cmake g++ - sudo apt install qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev + sudo apt install qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.gitignore b/.gitignore index 9ab62e1908..9ffa696ff8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ desktop.ini # MSVC Files CMakeSettings.json CMakePresets.json +CMakeUserPresets.json .vs/ -out/ \ No newline at end of file +out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cae1eb7787..5f4e3077a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 2.7.6 (2023-08-15) + +### Changes +- Significant improvement to visual when drag/drop entries [#9698] +- Automatically prompt for Quick Unlock when showing unlock dialog [#9697] +- Improve colorful lock icon and fix file MIME icon on KDE [#9632] +- Ability to search by entry UUID [#9571] +- Add challenge-response support for NitroKey 3 [#9631] +- Auto-Type: Disable entry level Auto-Type when disabled at group/entry [#9672] +- Browser: Show warning when adding duplicate URL's to entry [#9588][#9635] +- Browser: Improve error message when proxy cannot be found [#9385] + +### Fixes +- Fix crash on exit on macOS [#9620] +- Fix crash on search if entry doesn't have a group [#9633] +- Fix several issues with Quick Unlock [#9697] +- Enable save button when not auto-saving non-data changes [#9634] +- Several UI/UX fixes [#9647] +- Move toolbar back to top of window when disabling movement [#9699] +- Browser: Fix closing password generator dialog with X button [#9636] +- Browser: Fix handling of expired credentials [#9595] +- Windows: Prevent white flicker when launching application [#9637] +- Linux: Fix warning message about allow screencapture [#9638] +- FdoSecrets: Fix access confirmation dialog showing even when disabled [#9690] + ## 2.7.5 (2023-05-14) ### Changes diff --git a/cmake/CLangFormat.cmake b/cmake/CLangFormat.cmake index b2df97d4d3..7984f25286 100644 --- a/cmake/CLangFormat.cmake +++ b/cmake/CLangFormat.cmake @@ -18,7 +18,7 @@ set(EXCLUDED_DIRS src/thirdparty src/zxcvbn # objective-c directories - src/touchid + src/quickunlock/touchid src/autotype/mac src/gui/osutils/macutils) diff --git a/docs/topics/DownloadInstall.adoc b/docs/topics/DownloadInstall.adoc index f5a967ec52..21172fae43 100644 --- a/docs/topics/DownloadInstall.adoc +++ b/docs/topics/DownloadInstall.adoc @@ -59,7 +59,7 @@ image::linux_store.png[] The Snap and Flatpak options are sandboxed applications (more secure). The Native option is installed with the operating system files. Read more about the limitations of these options here: https://keepassxc.org/docs/#faq-appsnap-yubikey[KeePassXC Snap FAQ] -NOTE: KeePassXC stores a configuration file in `~/.cache` to remember window position, recent files, and other local settings. If you mount this folder to a tmpdisk you will lose settings after reboot. +NOTE: KeePassXC stores a configuration file in `~/.local/state` to remember window position, recent files, and other local settings. If you mount this folder to a tmpdisk you will lose settings after reboot. === macOS To install the KeePassXC app on macOS, double click on the downloaded DMG file and use the click and drag option as shown: diff --git a/release-tool b/release-tool index 0f43ec6323..43f0580006 100755 --- a/release-tool +++ b/release-tool @@ -1445,7 +1445,7 @@ i18n() { elif [ "$cmd" == "tx-pull" ]; then logInfo "Pulling updated translations from Transifex..." - tx pull -af --minimum-perc=45 --parallel -r "$resource" $@ + tx pull -af --minimum-perc=60 -r "$resource" $@ fi } diff --git a/release-tool.ps1 b/release-tool.ps1 index 827fe0262b..368a18f6fc 100644 --- a/release-tool.ps1 +++ b/release-tool.ps1 @@ -287,7 +287,7 @@ if ($Merge) { "-extensions c,cpp,h,js,mm,qrc,ui -no-obsolete ./src -ts share/translations/keepassxc_en.ts" Write-Host "Pulling updated translations from Transifex..." - Invoke-Cmd "tx" "pull -af --minimum-perc=60 --parallel -r keepassxc.share-translations-keepassxc-en-ts--develop" + Invoke-Cmd "tx" "pull -af --minimum-perc=60 -r keepassxc.share-translations-keepassxc-en-ts--develop" # Only commit if there are changes $changes = & git status --porcelain @@ -429,8 +429,8 @@ if ($Merge) { # SIG # Begin signature block # MIIfXAYJKoZIhvcNAQcCoIIfTTCCH0kCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG -# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCyL72KYcPC3xiu -# XNxyDy0dxda/uihakmlcLUtXbyvnkaCCGSAwggU6MIIEIqADAgECAhBYotctjMD9 +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC3+AltPeIvGycP +# LJr+5kqYiFnGPXyfdXgAkgrw+aI/AqCCGSAwggU6MIIEIqADAgECAhBYotctjMD9 # icz/IeDU7cdKMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAkdCMRswGQYDVQQI # ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoT # D1NlY3RpZ28gTGltaXRlZDEkMCIGA1UEAxMbU2VjdGlnbyBSU0EgQ29kZSBTaWdu @@ -570,13 +570,13 @@ if ($Merge) { # Z28gUlNBIENvZGUgU2lnbmluZyBDQQIQWKLXLYzA/YnM/yHg1O3HSjANBglghkgB # ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ # AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G -# CSqGSIb3DQEJBDEiBCAqTz6Ip2LLYNFN5K1JJDf7b5f7S+xfp3YvZbz/pRmklTAN -# BgkqhkiG9w0BAQEFAASCAQAKN1cBYbsh7MV3GuUP/EmgN1E/SnQt8ZmqO7kinpF3 -# xIaoeAAUWBD8tBHJHGEMyykgkkDaDexoxIiOFfTCbVI1PasUxuvuA/I+8ReVSRjH -# E8+iT9Gh2IL1O2a0Wzjk7Xf0YeGK1rj635N9bhQeO7U8mBOZxe87kez5wj9e9jjM -# VK3dU69ymf6QHfilkQjPhIYBgdIwcwhXJZDZuh6TJFHzdsxigYoKY9LCEKDS1YkX -# ucGpn3xZ0DGg7RESFNF+Uhw/8NbaLH+pJomvgchJeCesmaQjUB7YC6dPofYFGS1Q -# CkdWSEZIUNYPr9uovh1lxsGrUSmu85boJigDrL6BkbQjoYIDSzCCA0cGCSqGSIb3 +# CSqGSIb3DQEJBDEiBCBp7GODztdZALJXKtlDO7iMqjlod5DXJQUaVMMwaRVDkzAN +# BgkqhkiG9w0BAQEFAASCAQB8i7HZnhNcD6S7hrG+nk6bDcg8LyL+C3QOnmxIQKA3 +# +TQB02qB83WI+ErrH7GQHgi+7kB4K8NYs1dK/yYIp6pwgXUnYqQlsQCYzMRk9Shn +# gvJWO04dV3V17NHfAXHT/+gHKTOOUJf58/Yabo87/vu4K5gE2g3TOrMHm0G9x1k8 +# PXgW6mzMD6xEz0tuvXdGZ8BSZ5VlDYV5ITchn8Eni29RTSIBIbZHbMWI5Gcsbzqd +# ZLKHmVOoT2Las0VWNzq96+1X1HjFGhPqAIm19ByZyGI3OO9fgP6lfGuHyE2eyYUp +# MKQ6qr8nfPzmp3bF0JLSGV3pEViDOqRgkkQmOXHfHlqsoYIDSzCCA0cGCSqGSIb3 # DQEJBjGCAzgwggM0AgEBMIGRMH0xCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVh # dGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3Rp # Z28gTGltaXRlZDElMCMGA1UEAxMcU2VjdGlnbyBSU0EgVGltZSBTdGFtcGluZyBD diff --git a/share/CMakeLists.txt b/share/CMakeLists.txt index 90f7e6e683..f120fc6e2c 100644 --- a/share/CMakeLists.txt +++ b/share/CMakeLists.txt @@ -58,7 +58,12 @@ if(UNIX AND NOT APPLE AND NOT HAIKU) EXCLUDE PATTERN "actions" EXCLUDE PATTERN "categories" EXCLUDE) endif(KEEPASSXC_DIST_FLATPAK) configure_file(linux/${APP_ID}.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.desktop @ONLY) + configure_file(linux/${APP_ID}.policy.in ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.policy @ONLY) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) + if("${CMAKE_SYSTEM}" MATCHES "Linux") + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.policy DESTINATION ${CMAKE_INSTALL_DATADIR}/polkit-1/actions) + endif() install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) endif(UNIX AND NOT APPLE AND NOT HAIKU) diff --git a/share/linux/org.keepassxc.KeePassXC.appdata.xml b/share/linux/org.keepassxc.KeePassXC.appdata.xml index aa540f4f65..5c5bd1742e 100644 --- a/share/linux/org.keepassxc.KeePassXC.appdata.xml +++ b/share/linux/org.keepassxc.KeePassXC.appdata.xml @@ -52,6 +52,31 @@ + + +
    +
  • Significant improvement to visual when drag/drop entries [#9698]
  • +
  • Automatically prompt for Quick Unlock when showing unlock dialog [#9697]
  • +
  • Improve colorful lock icon [#9632]
  • +
  • Ability to search by entry UUID [#9571]
  • +
  • Add challenge-response support for NitroKey 3 [#9631]
  • +
  • Auto-Type: Disable entry level Auto-Type when disabled at group/entry [#9672]
  • +
  • Browser: Show warning when adding duplicate URL's to entry [#9588][#9635]
  • +
  • Browser: Improve error message when proxy cannot be found [#9385]
  • +
  • Fix crash on exit on macOS [#9620]
  • +
  • Fix crash on search if entry doesn't have a group [#9633]
  • +
  • Fix several issues with Quick Unlock [#9697]
  • +
  • Enable save button when not auto-saving non-data changes [#9634]
  • +
  • Several UI/UX fixes [#9647]
  • +
  • Move toolbar back to top of window when disabling movement [#9699]
  • +
  • Browser: Fix closing password generator dialog with X button [#9636]
  • +
  • Browser: Fix handling of expired credentials [#9595]
  • +
  • Windows: Prevent white flicker when launching application [#9637]
  • +
  • Linux: Fix warning message about allow screencapture [#9638]
  • +
  • FdoSecrets: Fix access confirmation dialog showing even when disabled [#9690]
  • +
+
+
    diff --git a/share/linux/org.keepassxc.KeePassXC.policy.in b/share/linux/org.keepassxc.KeePassXC.policy.in new file mode 100644 index 0000000000..e5b837e0cc --- /dev/null +++ b/share/linux/org.keepassxc.KeePassXC.policy.in @@ -0,0 +1,18 @@ + + + + KeePassXC Developers + + @APP_ICON_NAME@ + + + Quick Unlock for a KeePassXC Database + Authentication is required to unlock a KeePassXC Database + + no + auth_self + + + diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index d8748810b8..30e0b98a6b 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -1518,10 +1518,6 @@ To prevent this error from appearing, you must go to "Database Settings / S Retry with empty password - - Failed to authenticate with Touch ID - - Failed to open key file: %1 @@ -1576,11 +1572,7 @@ If you do not have a key file, please leave the field empty. - Failed to authenticate with Windows Hello: %1 - - - - Windows Hello setup was canceled or failed. Quick unlock has not been enabled. + Failed to authenticate with Quick Unlock: %1 @@ -3840,6 +3832,10 @@ Error: %1 Has TOTP + + Background Color + + EntryPreviewWidget @@ -7979,6 +7975,62 @@ Kernel: %3 %4 allow screenshots and app recording (Windows/macOS) + + AES initialization failed + + + + AES encrypt failed + + + + Failed to store in Linux Keyring + + + + Could not locate key in keyring + + + + Could not read key in keyring + + + + AES decrypt failed + + + + No Polkit authentication agent was available + + + + Polkit authorization failed + + + + No Quick Unlock provider is available + + + + Polkit returned an error: %1 + + + + Failed to init KeePassXC crypto. + + + + Failed to encrypt key data. + + + + Failed to get Windows Hello credential. + + + + Failed to decrypt key data. + + QtIOCompressor @@ -8994,25 +9046,6 @@ Example: JBSWY3DPEHPK3PXP - - WindowsHello - - Failed to init KeePassXC crypto. - - - - Failed to encrypt key data. - - - - Failed to get Windows Hello credential. - - - - Failed to decrypt key data. - - - YubiKey diff --git a/share/windows/wix-template.xml b/share/windows/wix-template.xml index ae937ce709..add2af2974 100644 --- a/share/windows/wix-template.xml +++ b/share/windows/wix-template.xml @@ -92,6 +92,9 @@ + + + @@ -116,12 +119,17 @@ - - - + + + + + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b8099eed59..298355e8d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (C) 2018 KeePassXC Team +# Copyright (C) 2023 KeePassXC Team # Copyright (C) 2010 Felix Geyer # # This program is free software: you can redistribute it and/or modify @@ -60,6 +60,7 @@ set(keepassx_SOURCES core/TimeInfo.cpp core/Tools.cpp core/Translator.cpp + core/UrlTools.cpp cli/Utils.cpp cli/TextStream.cpp crypto/Crypto.cpp @@ -193,6 +194,7 @@ set(keepassx_SOURCES streams/qtiocompressor.cpp streams/StoreDataStream.cpp streams/SymmetricCipherStream.cpp + quickunlock/QuickUnlockInterface.cpp totp/totp.cpp) if(APPLE) set(keepassx_SOURCES @@ -208,6 +210,12 @@ if(UNIX AND NOT APPLE) ${keepassx_SOURCES} gui/osutils/nixutils/ScreenLockListenerDBus.cpp gui/osutils/nixutils/NixUtils.cpp) + if("${CMAKE_SYSTEM}" MATCHES "Linux") + set(keepassx_SOURCES + ${keepassx_SOURCES} + quickunlock/Polkit.cpp + quickunlock/PolkitDbusTypes.cpp) + endif() if(WITH_XC_X11) list(APPEND keepassx_SOURCES gui/osutils/nixutils/X11Funcs.cpp) @@ -216,6 +224,21 @@ if(UNIX AND NOT APPLE) gui/org.keepassxc.KeePassXC.MainWindow.xml gui/MainWindow.h MainWindow) + + set_source_files_properties( + quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml + PROPERTIES + INCLUDE "quickunlock/PolkitDbusTypes.h" + ) + qt5_add_dbus_interface(keepassx_SOURCES + quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml + polkit_dbus + ) + + find_library(KEYUTILS_LIBRARIES NAMES keyutils) + if(NOT KEYUTILS_LIBRARIES) + message(FATAL_ERROR "Could not find libkeyutils") + endif() endif() if(WIN32) set(keepassx_SOURCES @@ -223,7 +246,7 @@ if(WIN32) gui/osutils/winutils/ScreenLockListenerWin.cpp gui/osutils/winutils/WinUtils.cpp) if (MSVC) - list(APPEND keepassx_SOURCES winhello/WindowsHello.cpp) + list(APPEND keepassx_SOURCES quickunlock/WindowsHello.cpp) endif() endif() @@ -315,9 +338,9 @@ if(WITH_XC_NETWORKING) endif() if(APPLE) - list(APPEND keepassx_SOURCES touchid/TouchID.mm) + list(APPEND keepassx_SOURCES quickunlock/TouchID.mm) # TODO: Remove -Wno-error once deprecation warnings have been resolved. - set_source_files_properties(touchid/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast") + set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast") endif() configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h) @@ -343,6 +366,7 @@ target_link_libraries(keepassx_core ${ZXCVBN_LIBRARIES} ${ZLIB_LIBRARIES} ${ARGON2_LIBRARIES} + ${KEYUTILS_LIBRARIES} ${thirdparty_LIBRARIES} ) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index b412409a59..cff27209a7 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -25,6 +25,7 @@ #include "BrowserMessageBuilder.h" #include "BrowserSettings.h" #include "core/Tools.h" +#include "core/UrlTools.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "gui/osutils/OSUtils.h" @@ -115,7 +116,7 @@ bool BrowserService::openDatabase(bool triggerUnlock) return true; } - if (triggerUnlock) { + if (triggerUnlock && !m_bringToFrontRequested) { m_bringToFrontRequested = true; updateWindowState(); emit requestUnlock(); @@ -544,33 +545,6 @@ bool BrowserService::isPasswordGeneratorRequested() const return m_passwordGeneratorRequested; } -// Returns true if URLs are identical. Paths with "/" are removed during comparison. -// URLs without scheme reverts to https. -// Special handling is needed because QUrl::matches() with QUrl::StripTrailingSlash does not strip "/" paths. -bool BrowserService::isUrlIdentical(const QString& first, const QString& second) const -{ - auto trimUrl = [](QString url) { - url = url.trimmed(); - if (url.endsWith("/")) { - url.remove(url.length() - 1, 1); - } - - return url; - }; - - if (first.isEmpty() || second.isEmpty()) { - return false; - } - - const auto firstUrl = trimUrl(first); - const auto secondUrl = trimUrl(second); - if (firstUrl == secondUrl) { - return true; - } - - return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash); -} - QString BrowserService::storeKey(const QString& key) { auto db = getDatabase(); @@ -727,8 +701,7 @@ bool BrowserService::updateEntry(const EntryParameters& entryParameters, const Q tr("Do you want to update the information in %1 - %2?") .arg(QUrl(entryParameters.siteUrl).host(), username), MessageBox::Save | MessageBox::Cancel, - MessageBox::Cancel, - MessageBox::Raise); + MessageBox::Cancel); } if (browserSettings()->alwaysAllowUpdate() || dialogResult == MessageBox::Save) { @@ -1080,18 +1053,6 @@ int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl return *std::max_element(priorityList.begin(), priorityList.end()); } -bool BrowserService::schemeFound(const QString& url) -{ - QUrl address(url); - return !address.scheme().isEmpty(); -} - -bool BrowserService::isIpAddress(const QString& host) const -{ - QHostAddress address(host); - return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol; -} - bool BrowserService::removeFirstDomain(QString& hostname) { int pos = hostname.indexOf("."); @@ -1187,7 +1148,7 @@ bool BrowserService::handleURL(const QString& entryUrl, } // Match the base domain - if (getTopLevelDomainFromUrl(siteQUrl.host()) != getTopLevelDomainFromUrl(entryQUrl.host())) { + if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) { return false; } @@ -1197,34 +1158,6 @@ bool BrowserService::handleURL(const QString& entryUrl, } return false; -}; - -/** - * Gets the base domain of URL. - * - * Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk - */ -QString BrowserService::getTopLevelDomainFromUrl(const QString& url) const -{ - QUrl qurl = QUrl::fromUserInput(url); - QString host = qurl.host(); - - // If the hostname is an IP address, return it directly - if (isIpAddress(host)) { - return host; - } - - if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) { - return {}; - } - - // Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example - host.chop(qurl.topLevelDomain().length()); - // Split the URL and select the last part, e.g. https://another.example -> example - QString baseDomain = host.split('.').last(); - // Append the top level domain back to the URL, e.g. example -> example.co.uk - baseDomain.append(qurl.topLevelDomain()); - return baseDomain; } QSharedPointer BrowserService::getDatabase() diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 46bffef014..ca3579e024 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -82,7 +82,6 @@ class BrowserService : public QObject QString getCurrentTotp(const QString& uuid); void showPasswordGenerator(const KeyPairMessage& keyPairMessage); bool isPasswordGeneratorRequested() const; - bool isUrlIdentical(const QString& first, const QString& second) const; void addEntry(const EntryParameters& entryParameters, const QString& group, @@ -146,7 +145,6 @@ private slots: Group* getDefaultEntryGroup(const QSharedPointer& selectedDb = {}); int sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl); bool schemeFound(const QString& url); - bool isIpAddress(const QString& host) const; bool removeFirstDomain(QString& hostname); bool shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false); @@ -154,8 +152,6 @@ private slots: const QString& siteUrl, const QString& formUrl, const bool omitWwwSubdomain = false); - QString getTopLevelDomainFromUrl(const QString& url) const; - QString baseDomain(const QString& hostname) const; QSharedPointer getDatabase(); QSharedPointer selectedDatabase(); QString getDatabaseRootUuid(); diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 25048513eb..a5ab7e2b62 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -472,6 +472,28 @@ void Config::init(const QString& configFileName, const QString& localConfigFileN QDir().rmdir(QFileInfo(localConfigFileName).absolutePath()); } +#if defined(Q_OS_LINUX) + // Upgrade from previous KeePassXC version which stores its config + // in ~/.cache on Linux instead of ~/.local/state. + // Move file to correct location before continuing. + if (!QFile::exists(localConfigFileName)) { + QString oldLocalConfigPath = + QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/keepassxc"; + QString suffix; +#ifdef QT_DEBUG + suffix = "_debug"; +#endif + oldLocalConfigPath += QString("/keepassxc%1.ini").arg(suffix); + oldLocalConfigPath = QDir::toNativeSeparators(oldLocalConfigPath); + if (QFile::exists(oldLocalConfigPath)) { + QDir().mkpath(QFileInfo(localConfigFileName).absolutePath()); + QFile::copy(oldLocalConfigPath, localConfigFileName); + QFile::remove(oldLocalConfigPath); + QDir().rmdir(QFileInfo(oldLocalConfigPath).absolutePath()); + } + } +#endif + m_settings.reset(new QSettings(configFileName, QSettings::IniFormat)); if (!localConfigFileName.isEmpty() && configFileName != localConfigFileName) { m_localSettings.reset(new QSettings(localConfigFileName, QSettings::IniFormat)); @@ -512,7 +534,16 @@ QPair Config::defaultConfigFiles() #else // On case-sensitive Operating Systems, force use of lowercase app directories configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/keepassxc"; - localConfigPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/keepassxc"; + // Qt does not support XDG_STATE_HOME yet, change this once XDG_STATE_HOME is added + QString xdgStateHome = QFile::decodeName(qgetenv("XDG_STATE_HOME")); + if (!xdgStateHome.startsWith(u'/')) { + xdgStateHome.clear(); // spec says relative paths should be ignored + } + if (xdgStateHome.isEmpty()) { + xdgStateHome = QDir::homePath() + "/.local/state"; + } + + localConfigPath = xdgStateHome + "/keepassxc"; #endif QString suffix; diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 488cc2e4e7..aa36dad126 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -113,6 +113,8 @@ bool Database::open(QSharedPointer key, QString* error) * Unless `readOnly` is set to false, the database will be opened in * read-write mode and fall back to read-only if that is not possible. * + * If key is provided as null, only headers will be read. + * * @param filePath path to the file * @param key composite key for unlocking the database * @param error error message in case of failure @@ -996,3 +998,14 @@ void Database::stopModifiedTimer() { QMetaObject::invokeMethod(&m_modifiedTimer, "stop"); } + +QUuid Database::publicUuid() +{ + + if (!publicCustomData().contains("KPXC_PUBLIC_UUID")) { + publicCustomData().insert("KPXC_PUBLIC_UUID", QUuid::createUuid().toRfc4122()); + markAsModified(); + } + + return QUuid::fromRfc4122(publicCustomData()["KPXC_PUBLIC_UUID"].toByteArray()); +} diff --git a/src/core/Database.h b/src/core/Database.h index 6d8e0403bf..d4a0a7bd51 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -102,6 +102,7 @@ class Database : public ModifiableObject bool hasNonDataChanges() const; bool isSaving(); + QUuid publicUuid(); QUuid uuid() const; QString filePath() const; QString canonicalFilePath() const; diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 6577971169..824f9ff924 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -5,7 +5,7 @@ * Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, * author Giuseppe D'Angelo * Copyright (C) 2021 The Qt Company Ltd. - * Copyright (C) 2021 KeePassXC Team + * Copyright (C) 2023 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -271,35 +271,6 @@ namespace Tools } } - bool checkUrlValid(const QString& urlField) - { - if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive) - || urlField.startsWith("kdbx://", Qt::CaseInsensitive) - || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) { - return true; - } - - QUrl url; - if (urlField.contains("://")) { - url = urlField; - } else { - url = QUrl::fromUserInput(urlField); - } - - if (url.scheme() != "file" && url.host().isEmpty()) { - return false; - } - - // Check for illegal characters. Adds also the wildcard * to the list - QRegularExpression re("[<>\\^`{|}\\*]"); - auto match = re.match(urlField); - if (match.hasMatch()) { - return false; - } - - return true; - } - /**************************************************************************** * * Copyright (C) 2020 Giuseppe D'Angelo . diff --git a/src/core/Tools.h b/src/core/Tools.h index a8094d0a30..3df2ca0085 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -1,6 +1,6 @@ /* * Copyright (C) 2012 Felix Geyer - * Copyright (C) 2021 KeePassXC Team + * Copyright (C) 2023 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,7 +38,6 @@ namespace Tools bool isBase64(const QByteArray& ba); void sleep(int ms); void wait(int ms); - bool checkUrlValid(const QString& urlField); QString uuidToHex(const QUuid& uuid); QUuid hexToUuid(const QString& uuid); bool isValidUuid(const QString& uuidStr); diff --git a/src/core/UrlTools.cpp b/src/core/UrlTools.cpp new file mode 100644 index 0000000000..bd6db52718 --- /dev/null +++ b/src/core/UrlTools.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "UrlTools.h" +#ifdef WITH_XC_NETWORKING +#include +#include +#include +#endif +#include +#include + +Q_GLOBAL_STATIC(UrlTools, s_urlTools) + +UrlTools* UrlTools::instance() +{ + return s_urlTools; +} + +QUrl UrlTools::convertVariantToUrl(const QVariant& var) const +{ + QUrl url; + if (var.canConvert()) { + url = var.toUrl(); + } + return url; +} + +#ifdef WITH_XC_NETWORKING +QUrl UrlTools::getRedirectTarget(QNetworkReply* reply) const +{ + QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + QUrl url = convertVariantToUrl(var); + return url; +} + +/** + * Gets the base domain of URL or hostname. + * + * Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk + * Up-to-date list can be found: https://publicsuffix.org/list/public_suffix_list.dat + */ +QString UrlTools::getBaseDomainFromUrl(const QString& url) const +{ + auto qUrl = QUrl::fromUserInput(url); + + auto host = qUrl.host(); + if (isIpAddress(host)) { + return host; + } + + const auto tld = getTopLevelDomainFromUrl(qUrl.toString()); + if (tld.isEmpty() || tld.length() + 1 >= host.length()) { + return host; + } + + // Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example + host.chop(tld.length() + 1); + // Split the URL and select the last part, e.g. https://another.example -> example + QString baseDomain = host.split('.').last(); + // Append the top level domain back to the URL, e.g. example -> example.co.uk + baseDomain.append(QString(".%1").arg(tld)); + + return baseDomain; +} + +/** + * Gets the top level domain from URL. + * + * Returns the TLD e.g. https://another.example.co.uk -> co.uk + */ +QString UrlTools::getTopLevelDomainFromUrl(const QString& url) const +{ + auto host = QUrl::fromUserInput(url).host(); + if (isIpAddress(host)) { + return host; + } + + const auto numberOfDomainParts = host.split('.').length(); + static const auto dummy = QByteArrayLiteral(""); + + // Only loop the amount of different parts found + for (auto i = 0; i < numberOfDomainParts; ++i) { + // Cut the first part from host + host = host.mid(host.indexOf('.') + 1); + + QNetworkCookie cookie(dummy, dummy); + cookie.setDomain(host); + + // Check if dummy cookie's domain/TLD matches with public suffix list + if (!QNetworkCookieJar{}.setCookiesFromUrl(QList{cookie}, url)) { + return host; + } + } + + return host; +} + +bool UrlTools::isIpAddress(const QString& host) const +{ + QHostAddress address(host); + return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol; +} +#endif + +// Returns true if URLs are identical. Paths with "/" are removed during comparison. +// URLs without scheme reverts to https. +// Special handling is needed because QUrl::matches() with QUrl::StripTrailingSlash does not strip "/" paths. +bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const +{ + auto trimUrl = [](QString url) { + url = url.trimmed(); + if (url.endsWith("/")) { + url.remove(url.length() - 1, 1); + } + + return url; + }; + + if (first.isEmpty() || second.isEmpty()) { + return false; + } + + const auto firstUrl = trimUrl(first); + const auto secondUrl = trimUrl(second); + if (firstUrl == secondUrl) { + return true; + } + + return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash); +} + +bool UrlTools::isUrlValid(const QString& urlField) const +{ + if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive) + || urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) { + return true; + } + + QUrl url; + if (urlField.contains("://")) { + url = urlField; + } else { + url = QUrl::fromUserInput(urlField); + } + + if (url.scheme() != "file" && url.host().isEmpty()) { + return false; + } + + // Check for illegal characters. Adds also the wildcard * to the list + QRegularExpression re("[<>\\^`{|}\\*]"); + auto match = re.match(urlField); + if (match.hasMatch()) { + return false; + } + + return true; +} diff --git a/src/core/UrlTools.h b/src/core/UrlTools.h new file mode 100644 index 0000000000..c86152d038 --- /dev/null +++ b/src/core/UrlTools.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_URLTOOLS_H +#define KEEPASSXC_URLTOOLS_H + +#include "config-keepassx.h" +#include +#include +#include +#include + +class UrlTools : public QObject +{ + Q_OBJECT + +public: + explicit UrlTools() = default; + static UrlTools* instance(); + +#ifdef WITH_XC_NETWORKING + QUrl getRedirectTarget(QNetworkReply* reply) const; + QString getBaseDomainFromUrl(const QString& url) const; + QString getTopLevelDomainFromUrl(const QString& url) const; + bool isIpAddress(const QString& host) const; +#endif + bool isUrlIdentical(const QString& first, const QString& second) const; + bool isUrlValid(const QString& urlField) const; + +private: + QUrl convertVariantToUrl(const QVariant& var) const; + +private: + Q_DISABLE_COPY(UrlTools); +}; + +static inline UrlTools* urlTools() +{ + return UrlTools::instance(); +} + +#endif // KEEPASSXC_URLTOOLS_H diff --git a/src/format/KdbxReader.cpp b/src/format/KdbxReader.cpp index 5610897c84..b552bd1cbe 100644 --- a/src/format/KdbxReader.cpp +++ b/src/format/KdbxReader.cpp @@ -27,6 +27,8 @@ /** * Read KDBX magic header numbers from a device. * + * Passing a null key will only read in the unprotected headers. + * * @param device input device * @param sig1 KDBX signature 1 * @param sig2 KDBX signature 2 @@ -55,6 +57,8 @@ bool KdbxReader::readMagicNumbers(QIODevice* device, quint32& sig1, quint32& sig * Read KDBX stream from device. * The device will automatically be reset to 0 before reading. * + * Passing a null key will only read in the unprotected headers. + * * @param device input device * @param key database encryption composite key * @param db database to read into @@ -91,6 +95,11 @@ bool KdbxReader::readDatabase(QIODevice* device, QSharedPointerEnableCopyOnDoubleClickCheckBox->setChecked( config()->get(Config::Security_EnableCopyOnDoubleClick).toBool()); - bool quickUnlockAvailable = false; -#if defined(Q_OS_MACOS) - quickUnlockAvailable = TouchID::getInstance().isAvailable(); -#elif defined(Q_CC_MSVC) - quickUnlockAvailable = getWindowsHello()->isAvailable(); - connect(getWindowsHello(), &WindowsHello::availableChanged, m_secUi->quickUnlockCheckBox, &QCheckBox::setEnabled); -#endif - m_secUi->quickUnlockCheckBox->setEnabled(quickUnlockAvailable); + m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable()); m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); for (const ExtraPage& page : asConst(m_extraPages)) { diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 319fdf89ff..1cd176023f 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -26,19 +26,12 @@ #include "gui/MessageBox.h" #include "keys/ChallengeResponseKey.h" #include "keys/FileKey.h" - -#ifdef Q_OS_MACOS -#include "touchid/TouchID.h" -#endif -#ifdef Q_CC_MSVC -#include "winhello/WindowsHello.h" -#endif +#include "quickunlock/QuickUnlockInterface.h" #include #include #include #include - namespace { constexpr int clearFormsDelay = 30000; @@ -46,25 +39,17 @@ namespace bool isQuickUnlockAvailable() { if (config()->get(Config::Security_QuickUnlock).toBool()) { -#if defined(Q_CC_MSVC) - return getWindowsHello()->isAvailable(); -#elif defined(Q_OS_MACOS) - return TouchID::getInstance().isAvailable(); -#endif + return getQuickUnlock()->isAvailable(); } return false; } - bool canPerformQuickUnlock(const QString& filename) + bool canPerformQuickUnlock(const QUuid& dbUuid) { if (isQuickUnlockAvailable()) { -#if defined(Q_CC_MSVC) - return getWindowsHello()->hasKey(filename); -#elif defined(Q_OS_MACOS) - return TouchID::getInstance().containsKey(filename); -#endif + return getQuickUnlock()->hasKey(dbUuid); } - Q_UNUSED(filename); + Q_UNUSED(dbUuid); return false; } } // namespace @@ -149,7 +134,7 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event) DialogyWidget::showEvent(event); if (isOnQuickUnlockScreen()) { m_ui->quickUnlockButton->setFocus(); - if (!canPerformQuickUnlock(m_filename)) { + if (m_db.isNull() || !canPerformQuickUnlock(m_db->publicUuid())) { resetQuickUnlock(); } } else { @@ -178,6 +163,12 @@ void DatabaseOpenWidget::load(const QString& filename) clearForms(); m_filename = filename; + + // Read public headers + QString error; + m_db.reset(new Database()); + m_db->open(m_filename, nullptr, &error); + m_ui->fileNameLabel->setRawText(m_filename); if (config()->get(Config::RememberLastKeyFiles).toBool()) { @@ -187,7 +178,7 @@ void DatabaseOpenWidget::load(const QString& filename) } } - if (canPerformQuickUnlock(m_filename)) { + if (canPerformQuickUnlock(m_db->publicUuid())) { m_ui->centralStack->setCurrentIndex(1); m_ui->quickUnlockButton->setFocus(); } else { @@ -215,7 +206,10 @@ void DatabaseOpenWidget::clearForms() m_ui->keyFileLineEdit->setClearButtonEnabled(true); m_ui->challengeResponseCombo->clear(); m_ui->centralStack->setCurrentIndex(0); - m_db.reset(); + + QString error; + m_db.reset(new Database()); + m_db->open(m_filename, nullptr, &error); } QSharedPointer DatabaseOpenWidget::database() @@ -274,6 +268,8 @@ void DatabaseOpenWidget::openDatabase() msgBox->exec(); if (msgBox->clickedButton() != btn) { m_db.reset(new Database()); + m_db->open(m_filename, nullptr, &error); + m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error); setUserInteractionLock(false); return; @@ -283,17 +279,7 @@ void DatabaseOpenWidget::openDatabase() // Save Quick Unlock credentials if available if (!blockQuickUnlock && isQuickUnlockAvailable()) { auto keyData = databaseKey->serialize(); -#if defined(Q_CC_MSVC) - // Store the password using Windows Hello - if (!getWindowsHello()->storeKey(m_filename, keyData)) { - getMainWindow()->displayTabMessage( - tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled."), - MessageWidget::MessageType::Warning); - } -#elif defined(Q_OS_MACOS) - // Store the password using TouchID - TouchID::getInstance().storeKey(m_filename, keyData); -#endif + getQuickUnlock()->setKey(m_db->publicUuid(), keyData); m_ui->messageWidget->hideMessage(); } @@ -338,27 +324,15 @@ QSharedPointer DatabaseOpenWidget::buildDatabaseKey() { auto databaseKey = QSharedPointer::create(); - if (canPerformQuickUnlock(m_filename)) { + if (!m_db.isNull() && canPerformQuickUnlock(m_db->publicUuid())) { // try to retrieve the stored password using Windows Hello QByteArray keyData; -#ifdef Q_CC_MSVC - if (!getWindowsHello()->getKey(m_filename, keyData)) { - // Failed to retrieve Quick Unlock data - auto error = getWindowsHello()->errorString(); - if (!error.isEmpty()) { - m_ui->messageWidget->showMessage(tr("Failed to authenticate with Windows Hello: %1").arg(error), - MessageWidget::Error); - resetQuickUnlock(); - } - return {}; - } -#elif defined(Q_OS_MACOS) - if (!TouchID::getInstance().getKey(m_filename, keyData)) { - // Failed to retrieve Quick Unlock data - m_ui->messageWidget->showMessage(tr("Failed to authenticate with Touch ID"), MessageWidget::Error); + if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) { + m_ui->messageWidget->showMessage( + tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()), + MessageWidget::Error); return {}; } -#endif databaseKey->setRawKey(keyData); return databaseKey; } @@ -553,10 +527,11 @@ void DatabaseOpenWidget::triggerQuickUnlock() */ void DatabaseOpenWidget::resetQuickUnlock() { -#if defined(Q_CC_MSVC) - getWindowsHello()->reset(m_filename); -#elif defined(Q_OS_MACOS) - TouchID::getInstance().reset(m_filename); -#endif + if (!isQuickUnlockAvailable()) { + return; + } + if (!m_db.isNull()) { + getQuickUnlock()->reset(m_db->publicUuid()); + } load(m_filename); } diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index d4e50116f8..4e422ebb2f 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 KeePassXC Team + * Copyright (C) 2023 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,6 +41,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) , m_dbWidgetStateSync(new DatabaseWidgetStateSync(this)) , m_dbWidgetPendingLock(nullptr) , m_databaseOpenDialog(new DatabaseOpenDialog(this)) + , m_databaseOpenInProgress(false) { auto* tabBar = new QTabBar(this); tabBar->setAcceptDrops(true); @@ -857,6 +858,7 @@ void DatabaseTabWidget::emitDatabaseLockChanged() emit databaseLocked(dbWidget); } else { emit databaseUnlocked(dbWidget); + m_databaseOpenInProgress = false; } } @@ -889,6 +891,11 @@ void DatabaseTabWidget::performGlobalAutoType(const QString& search) void DatabaseTabWidget::performBrowserUnlock() { + if (m_databaseOpenInProgress) { + return; + } + + m_databaseOpenInProgress = true; auto dbWidget = currentDatabaseWidget(); if (dbWidget && dbWidget->isLocked()) { unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::Browser); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 3a6791a806..58f5408e3a 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 KeePassXC Team + * Copyright (C) 2023 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -117,6 +117,7 @@ private slots: QPointer m_dbWidgetPendingLock; QPointer m_databaseOpenDialog; QTimer m_lockDelayTimer; + bool m_databaseOpenInProgress; }; #endif // KEEPASSX_DATABASETABWIDGET_H diff --git a/src/gui/IconDownloader.cpp b/src/gui/IconDownloader.cpp index 7e3fff0aec..1adb269229 100644 --- a/src/gui/IconDownloader.cpp +++ b/src/gui/IconDownloader.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 KeePassXC Team + * Copyright (C) 2023 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ #include "IconDownloader.h" #include "core/Config.h" #include "core/NetworkManager.h" +#include "core/UrlTools.h" #include #include @@ -40,37 +41,6 @@ IconDownloader::~IconDownloader() abortDownload(); } -namespace -{ - // Try to get the 2nd level domain of the host part of a QUrl. For example, - // "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk" - // would become "example.co.uk". - QString getSecondLevelDomain(const QUrl& url) - { - QString fqdn = url.host(); - fqdn.truncate(fqdn.length() - url.topLevelDomain().length()); - QStringList parts = fqdn.split('.'); - QString newdom = parts.takeLast() + url.topLevelDomain(); - return newdom; - } - - QUrl convertVariantToUrl(const QVariant& var) - { - QUrl url; - if (var.canConvert()) { - url = var.toUrl(); - } - return url; - } - - QUrl getRedirectTarget(QNetworkReply* reply) - { - QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - QUrl url = convertVariantToUrl(var); - return url; - } -} // namespace - void IconDownloader::setUrl(const QString& entryUrl) { m_url = entryUrl; @@ -114,7 +84,7 @@ void IconDownloader::setUrl(const QString& entryUrl) // Determine the second-level domain, if available QString secondLevelDomain; if (!hostIsIp) { - secondLevelDomain = getSecondLevelDomain(url); + secondLevelDomain = urlTools()->getBaseDomainFromUrl(url.toString()); } // Start with the "fallback" url (if enabled) to try to get the best favicon @@ -202,7 +172,7 @@ void IconDownloader::fetchFinished() QString url = m_url; bool error = (m_reply->error() != QNetworkReply::NoError); - QUrl redirectTarget = getRedirectTarget(m_reply); + QUrl redirectTarget = urlTools()->getRedirectTarget(m_reply); m_reply->deleteLater(); m_reply = nullptr; diff --git a/src/gui/URLEdit.cpp b/src/gui/URLEdit.cpp index d249ddd850..f5fbbb24be 100644 --- a/src/gui/URLEdit.cpp +++ b/src/gui/URLEdit.cpp @@ -1,6 +1,6 @@ /* + * Copyright (C) 2023 KeePassXC Team * Copyright (C) 2014 Felix Geyer - * Copyright (C) 2020 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ #include "URLEdit.h" #include "core/Tools.h" +#include "core/UrlTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" @@ -44,7 +45,7 @@ void URLEdit::updateStylesheet() { const QString stylesheetTemplate("QLineEdit { background: %1; }"); - if (!Tools::checkUrlValid(text())) { + if (!urlTools()->isUrlValid(text())) { StateColorPalette statePalette; QColor color = statePalette.color(StateColorPalette::ColorRole::Error); setStyleSheet(stylesheetTemplate.arg(color.name())); diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp index 41b4eea6de..bac749979c 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp @@ -25,13 +25,7 @@ #include "keys/ChallengeResponseKey.h" #include "keys/FileKey.h" #include "keys/PasswordKey.h" - -#ifdef Q_OS_MACOS -#include "touchid/TouchID.h" -#endif -#ifdef Q_CC_MSVC -#include "winhello/WindowsHello.h" -#endif +#include "quickunlock/QuickUnlockInterface.h" #include #include @@ -198,11 +192,7 @@ bool DatabaseSettingsWidgetDatabaseKey::save() m_db->setKey(newKey, true, false, false); -#if defined(Q_OS_MACOS) - TouchID::getInstance().reset(m_db->filePath()); -#elif defined(Q_CC_MSVC) - getWindowsHello()->reset(m_db->filePath()); -#endif + getQuickUnlock()->reset(m_db->publicUuid()); emit editFinished(true); if (m_isDirty) { diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 877efe9d2b..234a08ba7e 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -116,7 +116,7 @@ int EntryModel::columnCount(const QModelIndex& parent) const return 0; } - return 15; + return 16; } QVariant EntryModel::data(const QModelIndex& index, int role) const @@ -230,6 +230,13 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const return result; } + case Color: + QColor backgroundColor; + backgroundColor.setNamedColor(entry->backgroundColor()); + if (backgroundColor.isValid()) { + result = "▍"; + return result; + } } } else if (role == Qt::UserRole) { // Qt::UserRole is used as sort role, see EntryView::EntryView() switch (index.column()) { @@ -314,6 +321,15 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } return font; } else if (role == Qt::ForegroundRole) { + + if (index.column() == Color) { + QColor backgroundColor; + backgroundColor.setNamedColor(entry->backgroundColor()); + if (backgroundColor.isValid()) { + return backgroundColor; + } + } + QColor foregroundColor; foregroundColor.setNamedColor(entry->foregroundColor()); if (entry->hasReferences()) { @@ -327,10 +343,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const return QVariant(foregroundColor); } } else if (role == Qt::BackgroundRole) { - QColor backgroundColor; - backgroundColor.setNamedColor(entry->backgroundColor()); - if (backgroundColor.isValid()) { - return QVariant(backgroundColor); + if (m_backgroundColorVisible) { + QColor backgroundColor; + backgroundColor.setNamedColor(entry->backgroundColor()); + if (backgroundColor.isValid()) { + return QVariant(backgroundColor); + } } } else if (role == Qt::ToolTipRole) { if (index.column() == PasswordStrength && !entry->password().isEmpty() && !entry->excludeFromReports()) { @@ -414,6 +432,8 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro return tr("Has attachments"); case Totp: return tr("Has TOTP"); + case Color: + return tr("Background Color"); } } @@ -596,3 +616,7 @@ void EntryModel::makeConnections(const Group* group) connect(group, SIGNAL(entryMovedDown()), SLOT(entryMovedDown())); connect(group, SIGNAL(entryDataChanged(Entry*)), SLOT(entryDataChanged(Entry*))); } +void EntryModel::setBackgroundColorVisible(bool visible) +{ + m_backgroundColorVisible = visible; +} diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h index 8e79be3841..4ad81f4c48 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -48,7 +48,8 @@ class EntryModel : public QAbstractTableModel Attachments = 11, Totp = 12, Size = 13, - PasswordStrength = 14 + PasswordStrength = 14, + Color = 15 }; explicit EntryModel(QObject* parent = nullptr); @@ -67,6 +68,7 @@ class EntryModel : public QAbstractTableModel void setGroup(Group* group); void setEntries(const QList& entries); + void setBackgroundColorVisible(bool visible); private slots: void entryAboutToAdd(Entry* entry); @@ -85,6 +87,7 @@ private slots: void severConnections(); void makeConnections(const Group* group); + bool m_backgroundColorVisible = true; Group* m_group; QList m_entries; QList m_orgEntries; diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp index 55d8dd51cd..9a4340f5cd 100644 --- a/src/gui/entry/EntryURLModel.cpp +++ b/src/gui/entry/EntryURLModel.cpp @@ -20,7 +20,7 @@ #include "browser/BrowserService.h" #include "core/EntryAttributes.h" -#include "core/Tools.h" +#include "core/UrlTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" @@ -67,14 +67,14 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const } const auto value = m_entryAttributes->value(key); - const auto urlValid = Tools::checkUrlValid(value); + const auto urlValid = urlTools()->isUrlValid(value); // Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison. auto customAttributeKeys = m_entryAttributes->customKeys().filter(BrowserService::ADDITIONAL_URL); customAttributeKeys.removeOne(key); - const auto duplicateUrl = m_entryAttributes->values(customAttributeKeys).contains(value) - || browserService()->isUrlIdentical(value, m_entryUrl); + const auto duplicateUrl = + m_entryAttributes->values(customAttributeKeys).contains(value) || urlTools()->isUrlIdentical(value, m_entryUrl); if (role == Qt::BackgroundRole && (!urlValid || duplicateUrl)) { StateColorPalette statePalette; return statePalette.color(StateColorPalette::ColorRole::Error); diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 868250a666..7c5768ef4b 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -219,11 +219,12 @@ void EntryView::displaySearch(const QList& entries) m_model->setEntries(entries); header()->showSection(EntryModel::ParentGroup); + setFirstEntryActive(); + // Reset sort column to 'Group', overrides DatabaseWidgetStateSync m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); - setFirstEntryActive(); m_inSearchMode = true; } @@ -335,6 +336,7 @@ bool EntryView::setViewState(const QByteArray& state) bool status = header()->restoreState(state); resetFixedColumns(); m_columnsNeedRelayout = state.isEmpty(); + onHeaderChanged(); return status; } @@ -375,6 +377,9 @@ void EntryView::toggleColumnVisibility(QAction* action) // least one visible column remains, as the table header will disappear // entirely when all columns are hidden int columnIndex = action->data().toInt(); + if (columnIndex == EntryModel::Color) { + m_model->setBackgroundColorVisible(!action->isChecked()); + } if (action->isChecked()) { header()->showSection(columnIndex); if (header()->sectionSize(columnIndex) == 0) { @@ -446,6 +451,8 @@ void EntryView::resetFixedColumns() header()->resizeSection(col, width); } } + header()->setMinimumSectionSize(1); + header()->resizeSection(EntryModel::Color, ICON_ONLY_SECTION_SIZE); } /** @@ -474,6 +481,8 @@ void EntryView::resetViewToDefaults() header()->hideSection(EntryModel::Attachments); header()->hideSection(EntryModel::Size); header()->hideSection(EntryModel::PasswordStrength); + header()->hideSection(EntryModel::Color); + onHeaderChanged(); // Reset column order to logical indices for (int i = 0; i < header()->count(); ++i) { @@ -501,6 +510,11 @@ void EntryView::resetViewToDefaults() } } +void EntryView::onHeaderChanged() +{ + m_model->setBackgroundColorVisible(isColumnHidden(EntryModel::Color)); +} + void EntryView::showEvent(QShowEvent* event) { QTreeView::showEvent(event); @@ -545,6 +559,8 @@ void EntryView::startDrag(Qt::DropActions supportedActions) listWidget.addItem(item); } + listWidget.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listWidget.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); listWidget.setStyleSheet("QListWidget { background-color: palette(highlight); border: 1px solid palette(dark); " "padding: 4px; color: palette(highlighted-text); }"); auto width = listWidget.sizeHintForColumn(0) + 2 * listWidget.frameWidth(); diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index 3a0cc1d60d..759097b346 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -76,6 +76,7 @@ private slots: private: void resetFixedColumns(); bool isColumnHidden(int logicalIndex); + void onHeaderChanged(); EntryModel* const m_model; SortFilterHideProxyModel* const m_sortModel; diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp index ebbea91d36..194b62058e 100644 --- a/src/gui/osutils/nixutils/NixUtils.cpp +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -323,3 +324,29 @@ void NixUtils::setColorScheme(QDBusVariant value) m_systemColorschemePrefExists = true; emit interfaceThemeChanged(); } + +quint64 NixUtils::getProcessStartTime() const +{ + QString processStatPath = QString("/proc/%1/stat").arg(QCoreApplication::applicationPid()); + QFile processStatFile(processStatPath); + + if (!processStatFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qDebug() << "nixutils: failed to open " << processStatPath; + return 0; + } + + QTextStream processStatStream(&processStatFile); + QString processStatInfo = processStatStream.readLine(); + processStatFile.close(); + + auto startIndex = processStatInfo.indexOf(')', -1); + if (startIndex != -1) { + auto tokens = processStatInfo.midRef(startIndex + 2).split(' '); + if (tokens.size() >= 20) { + return tokens[19].toULongLong(); + } + } + + qDebug() << "nixutils: failed to parse " << processStatPath; + return 0; +} diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h index e3a17b9509..04cd086287 100644 --- a/src/gui/osutils/nixutils/NixUtils.h +++ b/src/gui/osutils/nixutils/NixUtils.h @@ -49,6 +49,8 @@ class NixUtils : public OSUtilsBase, QAbstractNativeEventFilter return false; } + quint64 getProcessStartTime() const; + private slots: void handleColorSchemeRead(QDBusVariant value); void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value); diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp index 123e02c2cb..22a7425d57 100644 --- a/src/gui/reports/ReportsDialog.cpp +++ b/src/gui/reports/ReportsDialog.cpp @@ -30,9 +30,6 @@ #include "core/Global.h" #include "core/Group.h" -#ifdef Q_OS_MACOS -#include "touchid/TouchID.h" -#endif class ReportsDialog::ExtraPage { diff --git a/src/quickunlock/Polkit.cpp b/src/quickunlock/Polkit.cpp new file mode 100644 index 0000000000..38b9380d6d --- /dev/null +++ b/src/quickunlock/Polkit.cpp @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Polkit.h" + +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "crypto/SymmetricCipher.h" +#include "gui/osutils/nixutils/NixUtils.h" + +#include +#include +#include +#include +#include + +extern "C" { +#include +} + +const QString polkit_service = "org.freedesktop.PolicyKit1"; +const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority"; + +namespace +{ + QString getKeyName(const QUuid& dbUuid) + { + static const QString keyPrefix = "keepassxc_polkit_keys_"; + return keyPrefix + dbUuid.toString(); + } +} // namespace + +Polkit::Polkit() +{ + PolkitSubject::registerMetaType(); + PolkitAuthorizationResults::registerMetaType(); + + /* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overriden + through an environment variable to return an alternative bus path. This bus could have an application + pretending to be polkit running on it, which could approve every authentication request + + Most Linux distros place the system bus at this exact path, so it is hard-coded. + For any other distros, this path will need to be patched before compilation. + */ + QDBusConnection bus = + QDBusConnection::connectToBus("unix:path=/run/dbus/system_bus_socket", "keepassxc_polkit_dbus"); + + m_available = bus.isConnected(); + if (!m_available) { + qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)"; + return; + } + + m_available = bus.interface()->isServiceRegistered(polkit_service); + + if (!m_available) { + qDebug() << "polkit: Polkit is not registered on dbus"; + return; + } + + m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus)); +} + +Polkit::~Polkit() +{ +} + +void Polkit::reset(const QUuid& dbUuid) +{ + m_encryptedMasterKeys.remove(dbUuid); +} + +bool Polkit::isAvailable() const +{ + return m_available; +} + +QString Polkit::errorString() const +{ + return m_error; +} + +void Polkit::reset() +{ + m_encryptedMasterKeys.clear(); +} + +bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key) +{ + reset(dbUuid); + + // Generate a random iv/key pair to encrypt the master password with + QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); + QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); + QByteArray keychainKeyValue = randomKey + randomIV; + + SymmetricCipher aes256Encrypt; + if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { + m_error = QObject::tr("AES initialization failed"); + return false; + } + + // Encrypt the master password + QByteArray encryptedMasterKey = key; + if (!aes256Encrypt.finish(encryptedMasterKey)) { + m_error = QObject::tr("AES encrypt failed"); + qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString(); + return false; + } + + // Add the iv/key pair into the linux keyring + key_serial_t key_serial = add_key("user", + getKeyName(dbUuid).toStdString().c_str(), + keychainKeyValue.constData(), + keychainKeyValue.size(), + KEY_SPEC_PROCESS_KEYRING); + if (key_serial < 0) { + m_error = QObject::tr("Failed to store in Linux Keyring"); + qDebug() << "polkit keyring failed to store: " << errno; + return false; + } + + // Scrub the keys from ram + Botan::secure_scrub_memory(randomKey.data(), randomKey.size()); + Botan::secure_scrub_memory(randomIV.data(), randomIV.size()); + Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size()); + + // Store encrypted master password and return + m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); + return true; +} + +bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) +{ + if (!m_polkit || !hasKey(dbUuid)) { + return false; + } + + PolkitSubject subject; + subject.kind = "unix-process"; + subject.details.insert("pid", static_cast(QCoreApplication::applicationPid())); + subject.details.insert("start-time", nixUtils()->getProcessStartTime()); + + QMap details; + + auto result = m_polkit->CheckAuthorization( + subject, + "org.keepassxc.KeePassXC.unlockDatabase", + details, + 0x00000001, + // AllowUserInteraction - wait for user to authenticate + // https://www.freedesktop.org/software/polkit/docs/0.105/eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html#eggdbus-enum-CheckAuthorizationFlags + ""); + + // A general error occurred + if (result.isError()) { + auto msg = result.error().message(); + m_error = QObject::tr("Polkit returned an error: %1").arg(msg); + qDebug() << "polkit returned an error: " << msg; + return false; + } + + PolkitAuthorizationResults authResult = result.value(); + if (authResult.is_authorized) { + QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid); + key_serial_t keySerial = + find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING); + + if (keySerial == -1) { + m_error = QObject::tr("Could not locate key in keyring"); + qDebug() << "polkit keyring failed to find: " << errno; + return false; + } + + void* keychainBuffer; + long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer); + + if (keychainDataSize == -1) { + m_error = QObject::tr("Could not read key in keyring"); + qDebug() << "polkit keyring failed to read: " << errno; + return false; + } + + QByteArray keychainBytes(static_cast(keychainBuffer), keychainDataSize); + + Botan::secure_scrub_memory(keychainBuffer, keychainDataSize); + free(keychainBuffer); + + QByteArray keychainKey = keychainBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); + QByteArray keychainIv = keychainBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); + + SymmetricCipher aes256Decrypt; + if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) { + m_error = QObject::tr("AES initialization failed"); + qDebug() << "polkit aes init failed"; + return false; + } + + key = encryptedMasterKey; + if (!aes256Decrypt.finish(key)) { + key.clear(); + m_error = QObject::tr("AES decrypt failed"); + qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString(); + return false; + } + + // Scrub the keys from ram + Botan::secure_scrub_memory(keychainKey.data(), keychainKey.size()); + Botan::secure_scrub_memory(keychainIv.data(), keychainIv.size()); + Botan::secure_scrub_memory(keychainBytes.data(), keychainBytes.size()); + Botan::secure_scrub_memory(encryptedMasterKey.data(), encryptedMasterKey.size()); + + return true; + } + + // Failed to authenticate + if (authResult.is_challenge) { + m_error = QObject::tr("No Polkit authentication agent was available"); + } else { + m_error = QObject::tr("Polkit authorization failed"); + } + + return false; +} + +bool Polkit::hasKey(const QUuid& dbUuid) const +{ + if (!m_encryptedMasterKeys.contains(dbUuid)) { + return false; + } + + return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1; +} diff --git a/src/quickunlock/Polkit.h b/src/quickunlock/Polkit.h new file mode 100644 index 0000000000..7dfc2db7b1 --- /dev/null +++ b/src/quickunlock/Polkit.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_POLKIT_H +#define KEEPASSX_POLKIT_H + +#include "QuickUnlockInterface.h" +#include "polkit_dbus.h" +#include +#include + +class Polkit : public QuickUnlockInterface +{ +public: + Polkit(); + ~Polkit() override; + + bool isAvailable() const override; + QString errorString() const override; + + bool setKey(const QUuid& dbUuid, const QByteArray& key) override; + bool getKey(const QUuid& dbUuid, QByteArray& key) override; + bool hasKey(const QUuid& dbUuid) const override; + + void reset(const QUuid& dbUuid) override; + void reset() override; + +private: + bool m_available; + QString m_error; + QHash m_encryptedMasterKeys; + + QScopedPointer m_polkit; +}; + +#endif // KEEPASSX_POLKIT_H diff --git a/src/quickunlock/PolkitDbusTypes.cpp b/src/quickunlock/PolkitDbusTypes.cpp new file mode 100644 index 0000000000..a4305dc445 --- /dev/null +++ b/src/quickunlock/PolkitDbusTypes.cpp @@ -0,0 +1,45 @@ +#include "PolkitDbusTypes.h" + +void PolkitSubject::registerMetaType() +{ + qRegisterMetaType("PolkitSubject"); + qDBusRegisterMetaType(); +} + +QDBusArgument& operator<<(QDBusArgument& argument, const PolkitSubject& subject) +{ + argument.beginStructure(); + argument << subject.kind << subject.details; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitSubject& subject) +{ + argument.beginStructure(); + argument >> subject.kind >> subject.details; + argument.endStructure(); + return argument; +} + +void PolkitAuthorizationResults::registerMetaType() +{ + qRegisterMetaType("PolkitAuthorizationResults"); + qDBusRegisterMetaType(); +} + +QDBusArgument& operator<<(QDBusArgument& argument, const PolkitAuthorizationResults& res) +{ + argument.beginStructure(); + argument << res.is_authorized << res.is_challenge << res.details; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& res) +{ + argument.beginStructure(); + argument >> res.is_authorized >> res.is_challenge >> res.details; + argument.endStructure(); + return argument; +} diff --git a/src/quickunlock/PolkitDbusTypes.h b/src/quickunlock/PolkitDbusTypes.h new file mode 100644 index 0000000000..83eb238893 --- /dev/null +++ b/src/quickunlock/PolkitDbusTypes.h @@ -0,0 +1,36 @@ +#ifndef KEEPASSX_POLKITDBUSTYPES_H +#define KEEPASSX_POLKITDBUSTYPES_H + +#include + +class PolkitSubject +{ +public: + QString kind; + QVariantMap details; + + static void registerMetaType(); + + friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitSubject& subject); + + friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitSubject& subject); +}; + +class PolkitAuthorizationResults +{ +public: + bool is_authorized; + bool is_challenge; + QMap details; + + static void registerMetaType(); + + friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitAuthorizationResults& subject); + + friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject); +}; + +Q_DECLARE_METATYPE(PolkitSubject); +Q_DECLARE_METATYPE(PolkitAuthorizationResults); + +#endif // KEEPASSX_POLKITDBUSTYPES_H diff --git a/src/quickunlock/QuickUnlockInterface.cpp b/src/quickunlock/QuickUnlockInterface.cpp new file mode 100644 index 0000000000..0e24736e80 --- /dev/null +++ b/src/quickunlock/QuickUnlockInterface.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "QuickUnlockInterface.h" +#include + +#if defined(Q_OS_MACOS) +#include "TouchID.h" +#define QUICKUNLOCK_IMPLEMENTATION TouchID +#elif defined(Q_CC_MSVC) +#include "WindowsHello.h" +#define QUICKUNLOCK_IMPLEMENTATION WindowsHello +#elif defined(Q_OS_LINUX) +#include "Polkit.h" +#define QUICKUNLOCK_IMPLEMENTATION Polkit +#else +#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock +#endif + +QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr}; + +QuickUnlockInterface* getQuickUnlock() +{ + if (!quickUnlockInstance) { + quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION(); + } + return quickUnlockInstance; +} + +bool NoQuickUnlock::isAvailable() const +{ + return false; +} + +QString NoQuickUnlock::errorString() const +{ + return QObject::tr("No Quick Unlock provider is available"); +} + +void NoQuickUnlock::reset() +{ +} + +bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key) +{ + Q_UNUSED(dbUuid) + Q_UNUSED(key) + return false; +} + +bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key) +{ + Q_UNUSED(dbUuid) + Q_UNUSED(key) + return false; +} + +bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const +{ + Q_UNUSED(dbUuid) + return false; +} + +void NoQuickUnlock::reset(const QUuid& dbUuid) +{ + Q_UNUSED(dbUuid) +} diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h new file mode 100644 index 0000000000..54aeb8a627 --- /dev/null +++ b/src/quickunlock/QuickUnlockInterface.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H +#define KEEPASSXC_QUICKUNLOCKINTERFACE_H + +#include + +class QuickUnlockInterface +{ + Q_DISABLE_COPY(QuickUnlockInterface) + +public: + QuickUnlockInterface() = default; + virtual ~QuickUnlockInterface() = default; + + virtual bool isAvailable() const = 0; + virtual QString errorString() const = 0; + + virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0; + virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0; + virtual bool hasKey(const QUuid& dbUuid) const = 0; + + virtual void reset(const QUuid& dbUuid) = 0; + virtual void reset() = 0; +}; + +class NoQuickUnlock : public QuickUnlockInterface +{ +public: + bool isAvailable() const override; + QString errorString() const override; + + bool setKey(const QUuid& dbUuid, const QByteArray& key) override; + bool getKey(const QUuid& dbUuid, QByteArray& key) override; + bool hasKey(const QUuid& dbUuid) const override; + + void reset(const QUuid& dbUuid) override; + void reset() override; +}; + +QuickUnlockInterface* getQuickUnlock(); + +#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H diff --git a/src/quickunlock/TouchID.h b/src/quickunlock/TouchID.h new file mode 100644 index 0000000000..2cca7ea464 --- /dev/null +++ b/src/quickunlock/TouchID.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_TOUCHID_H +#define KEEPASSX_TOUCHID_H + +#include "QuickUnlockInterface.h" +#include + +class TouchID : public QuickUnlockInterface +{ +public: + bool isAvailable() const override; + QString errorString() const override; + + bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override; + bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override; + bool hasKey(const QUuid& dbUuid) const override; + + void reset(const QUuid& dbUuid = "") override; + void reset() override; + +private: + static bool isWatchAvailable(); + static bool isTouchIdAvailable(); + + static void deleteKeyEntry(const QString& accountName); + static QString databaseKeyName(const QUuid& dbUuid); + + QHash m_encryptedMasterKeys; +}; + +#endif // KEEPASSX_TOUCHID_H diff --git a/src/touchid/TouchID.mm b/src/quickunlock/TouchID.mm similarity index 85% rename from src/touchid/TouchID.mm rename to src/quickunlock/TouchID.mm index cc858a89a8..502a508c4b 100644 --- a/src/touchid/TouchID.mm +++ b/src/quickunlock/TouchID.mm @@ -1,4 +1,4 @@ -#include "touchid/TouchID.h" +#include "quickunlock/TouchID.h" #include "crypto/Random.h" #include "crypto/SymmetricCipher.h" @@ -13,6 +13,7 @@ #include #include +#include #define TOUCH_ID_ENABLE_DEBUG_LOGS() 0 #if TOUCH_ID_ENABLE_DEBUG_LOGS() @@ -54,16 +55,6 @@ inline CFMutableDictionaryRef makeDictionary() { return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); } -/** - * Singleton - */ -TouchID& TouchID::getInstance() -{ - static TouchID instance; // Guaranteed to be destroyed. - // Instantiated on first use. - return instance; -} - //! Try to delete an existing keychain entry void TouchID::deleteKeyEntry(const QString& accountName) { @@ -77,14 +68,24 @@ inline CFMutableDictionaryRef makeDictionary() { // get data from the KeyChain OSStatus status = SecItemDelete(query); - LogStatusError("TouchID::storeKey - Status deleting existing entry", status); + LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status); } -QString TouchID::databaseKeyName(const QString &databasePath) +QString TouchID::databaseKeyName(const QUuid& dbUuid) { static const QString keyPrefix = "KeepassXC_TouchID_Keys_"; - const QByteArray pathHash = CryptoHash::hash(databasePath.toUtf8(), CryptoHash::Sha256).toHex(); - return keyPrefix + pathHash; + return keyPrefix + dbUuid.toString(); +} + +QString TouchID::errorString() const +{ + // TODO + return ""; +} + +void TouchID::reset() +{ + m_encryptedMasterKeys.clear(); } /** @@ -92,15 +93,15 @@ inline CFMutableDictionaryRef makeDictionary() { * protects the database. The encrypted PasswordKey is kept in memory while the * AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch. */ -bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKey) +bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey) { - if (databasePath.isEmpty() || passwordKey.isEmpty()) { - debug("TouchID::storeKey - illegal arguments"); + if (passwordKey.isEmpty()) { + debug("TouchID::setKey - illegal arguments"); return false; } - if (m_encryptedMasterKeys.contains(databasePath)) { - debug("TouchID::storeKey - Already stored key for this database"); + if (m_encryptedMasterKeys.contains(dbUuid)) { + debug("TouchID::setKey - Already stored key for this database"); return true; } @@ -110,7 +111,7 @@ inline CFMutableDictionaryRef makeDictionary() { SymmetricCipher aes256Encrypt; if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { - debug("TouchID::storeKey - AES initialisation failed"); + debug("TouchID::setKey - AES initialisation failed"); return false; } @@ -121,7 +122,7 @@ inline CFMutableDictionaryRef makeDictionary() { return false; } - const QString keyName = databaseKeyName(databasePath); + const QString keyName = databaseKeyName(dbUuid); deleteKeyEntry(keyName); // Try to delete the existing key entry @@ -152,7 +153,7 @@ inline CFMutableDictionaryRef makeDictionary() { if (sacObject == NULL || error != NULL) { NSError* e = (__bridge NSError*) error; - debug("TouchID::storeKey - Error creating security flags: %s", e.localizedDescription.UTF8String); + debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String); return false; } @@ -174,7 +175,7 @@ inline CFMutableDictionaryRef makeDictionary() { // add to KeyChain OSStatus status = SecItemAdd(attributes, NULL); - LogStatusError("TouchID::storeKey - Status adding new entry", status); + LogStatusError("TouchID::setKey - Status adding new entry", status); CFRelease(sacObject); CFRelease(attributes); @@ -188,8 +189,8 @@ inline CFMutableDictionaryRef makeDictionary() { Botan::secure_scrub_memory(randomIV.data(), randomIV.size()); // memorize which database the stored key is for - m_encryptedMasterKeys.insert(databasePath, encryptedMasterKey); - debug("TouchID::storeKey - Success!"); + m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); + debug("TouchID::setKey - Success!"); return true; } @@ -197,15 +198,11 @@ inline CFMutableDictionaryRef makeDictionary() { * Checks if an encrypted PasswordKey is available for the given database, tries to * decrypt it using the KeyChain and if successful, returns it. */ -bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const +bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey) { passwordKey.clear(); - if (databasePath.isEmpty()) { - debug("TouchID::getKey - missing database path"); - return false; - } - if (!containsKey(databasePath)) { + if (!hasKey(dbUuid)) { debug("TouchID::getKey - No stored key found"); return false; } @@ -213,7 +210,7 @@ inline CFMutableDictionaryRef makeDictionary() { // query the KeyChain for the AES key CFMutableDictionaryRef query = makeDictionary(); - const QString keyName = databaseKeyName(databasePath); + const QString keyName = databaseKeyName(dbUuid); NSString* accountName = keyName.toNSString(); // The NSString is released by Qt NSString* touchPromptMessage = QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database") @@ -254,7 +251,7 @@ inline CFMutableDictionaryRef makeDictionary() { } // decrypt PasswordKey from memory using AES - passwordKey = m_encryptedMasterKeys[databasePath]; + passwordKey = m_encryptedMasterKeys[dbUuid]; if (!aes256Decrypt.finish(passwordKey)) { passwordKey.clear(); debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData()); @@ -268,9 +265,9 @@ inline CFMutableDictionaryRef makeDictionary() { return true; } -bool TouchID::containsKey(const QString& dbPath) const +bool TouchID::hasKey(const QUuid& dbUuid) const { - return m_encryptedMasterKeys.contains(dbPath); + return m_encryptedMasterKeys.contains(dbUuid); } // TODO: Both functions below should probably handle the returned errors to @@ -336,7 +333,7 @@ inline CFMutableDictionaryRef makeDictionary() { } //! @return true if either TouchID or Apple Watch is available at the moment. -bool TouchID::isAvailable() +bool TouchID::isAvailable() const { // note: we cannot cache the check results because the configuration // is dynamic in its nature. User can close the laptop lid or take off @@ -349,12 +346,7 @@ inline CFMutableDictionaryRef makeDictionary() { /** * Resets the inner state either for all or for the given database */ -void TouchID::reset(const QString& databasePath) +void TouchID::reset(const QUuid& dbUuid) { - if (databasePath.isEmpty()) { - m_encryptedMasterKeys.clear(); - return; - } - - m_encryptedMasterKeys.remove(databasePath); + m_encryptedMasterKeys.remove(dbUuid); } diff --git a/src/winhello/WindowsHello.cpp b/src/quickunlock/WindowsHello.cpp similarity index 82% rename from src/winhello/WindowsHello.cpp rename to src/quickunlock/WindowsHello.cpp index bc244cc26c..890e3499a5 100644 --- a/src/winhello/WindowsHello.cpp +++ b/src/quickunlock/WindowsHello.cpp @@ -99,28 +99,10 @@ namespace } } // namespace -WindowsHello* WindowsHello::m_instance{nullptr}; -WindowsHello* WindowsHello::instance() -{ - if (!m_instance) { - m_instance = new WindowsHello(); - } - return m_instance; -} - -WindowsHello::WindowsHello(QObject* parent) - : QObject(parent) -{ - concurrency::create_task([this] { - bool state = KeyCredentialManager::IsSupportedAsync().get(); - m_available = state; - emit availableChanged(m_available); - }); -} - bool WindowsHello::isAvailable() const { - return m_available; + auto task = concurrency::create_task([] { return KeyCredentialManager::IsSupportedAsync().get(); }); + return task.get(); } QString WindowsHello::errorString() const @@ -128,7 +110,7 @@ QString WindowsHello::errorString() const return m_error; } -bool WindowsHello::storeKey(const QString& dbPath, const QByteArray& data) +bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data) { queueSecurityPromptFocus(); @@ -144,26 +126,26 @@ bool WindowsHello::storeKey(const QString& dbPath, const QByteArray& data) // Encrypt the data using AES-256-CBC SymmetricCipher cipher; if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, challenge)) { - m_error = tr("Failed to init KeePassXC crypto."); + m_error = QObject::tr("Failed to init KeePassXC crypto."); return false; } QByteArray encrypted = data; if (!cipher.finish(encrypted)) { - m_error = tr("Failed to encrypt key data."); + m_error = QObject::tr("Failed to encrypt key data."); return false; } // Prepend the challenge/IV to the encrypted data encrypted.prepend(challenge); - m_encryptedKeys.insert(dbPath, encrypted); + m_encryptedKeys.insert(dbUuid, encrypted); return true; } -bool WindowsHello::getKey(const QString& dbPath, QByteArray& data) +bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data) { data.clear(); - if (!hasKey(dbPath)) { - m_error = tr("Failed to get Windows Hello credential."); + if (!hasKey(dbUuid)) { + m_error = QObject::tr("Failed to get Windows Hello credential."); return false; } @@ -171,7 +153,7 @@ bool WindowsHello::getKey(const QString& dbPath, QByteArray& data) // Read the previously used challenge and encrypted data auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); - const auto& keydata = m_encryptedKeys.value(dbPath); + const auto& keydata = m_encryptedKeys.value(dbUuid); auto challenge = keydata.left(ivSize); auto encrypted = keydata.mid(ivSize); QByteArray key; @@ -183,7 +165,7 @@ bool WindowsHello::getKey(const QString& dbPath, QByteArray& data) // Decrypt the data using the generated key and IV from above SymmetricCipher cipher; if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) { - m_error = tr("Failed to init KeePassXC crypto."); + m_error = QObject::tr("Failed to init KeePassXC crypto."); return false; } @@ -191,21 +173,21 @@ bool WindowsHello::getKey(const QString& dbPath, QByteArray& data) data = encrypted; if (!cipher.finish(data)) { data.clear(); - m_error = tr("Failed to decrypt key data."); + m_error = QObject::tr("Failed to decrypt key data."); return false; } return true; } -void WindowsHello::reset(const QString& dbPath) +void WindowsHello::reset(const QUuid& dbUuid) { - m_encryptedKeys.remove(dbPath); + m_encryptedKeys.remove(dbUuid); } -bool WindowsHello::hasKey(const QString& dbPath) const +bool WindowsHello::hasKey(const QUuid& dbUuid) const { - return m_encryptedKeys.contains(dbPath); + return m_encryptedKeys.contains(dbUuid); } void WindowsHello::reset() diff --git a/src/winhello/WindowsHello.h b/src/quickunlock/WindowsHello.h similarity index 57% rename from src/winhello/WindowsHello.h rename to src/quickunlock/WindowsHello.h index 5faf7eb256..ea59f91c30 100644 --- a/src/winhello/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -18,41 +18,28 @@ #ifndef KEEPASSXC_WINDOWSHELLO_H #define KEEPASSXC_WINDOWSHELLO_H +#include "QuickUnlockInterface.h"; + #include #include -class WindowsHello : public QObject +class WindowsHello : public QuickUnlockInterface { - Q_OBJECT - public: - static WindowsHello* instance(); - bool isAvailable() const; - QString errorString() const; - void reset(); - - bool storeKey(const QString& dbPath, const QByteArray& key); - bool getKey(const QString& dbPath, QByteArray& key); - bool hasKey(const QString& dbPath) const; - void reset(const QString& dbPath); + WindowsHello() = default; + bool isAvailable() const override; + QString errorString() const override; + void reset() override; -signals: - void availableChanged(bool state); + bool setKey(const QUuid& dbUuid, const QByteArray& key) override; + bool getKey(const QUuid& dbUuid, QByteArray& key) override; + bool hasKey(const QUuid& dbUuid) const override; + void reset(const QUuid& dbUuid) override; private: - bool m_available = false; QString m_error; - QHash m_encryptedKeys; - - static WindowsHello* m_instance; - WindowsHello(QObject* parent = nullptr); - ~WindowsHello() override = default; + QHash m_encryptedKeys; Q_DISABLE_COPY(WindowsHello); }; -inline WindowsHello* getWindowsHello() -{ - return WindowsHello::instance(); -} - #endif // KEEPASSXC_WINDOWSHELLO_H diff --git a/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml b/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml new file mode 100644 index 0000000000..d46d71d2a0 --- /dev/null +++ b/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml @@ -0,0 +1,16 @@ + + + + + + + + + > + + + + + + + diff --git a/src/touchid/TouchID.h b/src/touchid/TouchID.h deleted file mode 100644 index e32f1fa126..0000000000 --- a/src/touchid/TouchID.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef KEEPASSX_TOUCHID_H -#define KEEPASSX_TOUCHID_H - -#include - -class TouchID -{ -public: - static TouchID& getInstance(); - -private: - TouchID() - { - // Nothing to do here - } - -public: - TouchID(TouchID const&) = delete; - void operator=(TouchID const&) = delete; - - bool storeKey(const QString& databasePath, const QByteArray& passwordKey); - bool getKey(const QString& databasePath, QByteArray& passwordKey) const; - bool containsKey(const QString& databasePath) const; - void reset(const QString& databasePath = ""); - - bool isAvailable(); - -private: - static bool isWatchAvailable(); - static bool isTouchIdAvailable(); - - static void deleteKeyEntry(const QString& accountName); - static QString databaseKeyName(const QString& databasePath); - -private: - QHash m_encryptedMasterKeys; -}; - -#endif // KEEPASSX_TOUCHID_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index db82da1639..1abe869a41 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -150,6 +150,8 @@ if(WITH_XC_NETWORKING) LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testicondownloader SOURCES TestIconDownloader.cpp LIBS ${TEST_LIBRARIES}) + + add_unit_test(NAME testurltools SOURCES TestUrlTools.cpp LIBS ${TEST_LIBRARIES}) endif() if(WITH_XC_AUTOTYPE) diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index d7345537d6..aa084921ed 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -144,54 +144,6 @@ void TestBrowser::testBuildResponse() QCOMPARE(firstArr["test"].toBool(), true); } -/** - * Tests for BrowserService - */ -void TestBrowser::testTopLevelDomain() -{ - QString url1 = "https://another.example.co.uk"; - QString url2 = "https://www.example.com"; - QString url3 = "http://test.net"; - QString url4 = "http://so.many.subdomains.co.jp"; - QString url5 = "https://192.168.0.1"; - QString url6 = "https://192.168.0.1:8000"; - - QString res1 = m_browserService->getTopLevelDomainFromUrl(url1); - QString res2 = m_browserService->getTopLevelDomainFromUrl(url2); - QString res3 = m_browserService->getTopLevelDomainFromUrl(url3); - QString res4 = m_browserService->getTopLevelDomainFromUrl(url4); - QString res5 = m_browserService->getTopLevelDomainFromUrl(url5); - QString res6 = m_browserService->getTopLevelDomainFromUrl(url6); - - QCOMPARE(res1, QString("example.co.uk")); - QCOMPARE(res2, QString("example.com")); - QCOMPARE(res3, QString("test.net")); - QCOMPARE(res4, QString("subdomains.co.jp")); - QCOMPARE(res5, QString("192.168.0.1")); - QCOMPARE(res6, QString("192.168.0.1")); -} - -void TestBrowser::testIsIpAddress() -{ - auto host1 = "example.com"; // Not valid - auto host2 = "192.168.0.1"; - auto host3 = "278.21.2.0"; // Not valid - auto host4 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; - auto host5 = "2001:db8:0:1:1:1:1:1"; - auto host6 = "fe80::1ff:fe23:4567:890a"; - auto host7 = "2001:20::1"; - auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid - - QVERIFY(!m_browserService->isIpAddress(host1)); - QVERIFY(m_browserService->isIpAddress(host2)); - QVERIFY(!m_browserService->isIpAddress(host3)); - QVERIFY(m_browserService->isIpAddress(host4)); - QVERIFY(m_browserService->isIpAddress(host5)); - QVERIFY(m_browserService->isIpAddress(host6)); - QVERIFY(m_browserService->isIpAddress(host7)); - QVERIFY(!m_browserService->isIpAddress(host8)); -} - void TestBrowser::testSortPriority() { QFETCH(QString, entryUrl); @@ -583,26 +535,6 @@ QList TestBrowser::createEntries(QStringList& urls, Group* root) const return entries; } -void TestBrowser::testValidURLs() -{ - QHash urls; - urls["https://github.com/login"] = true; - urls["https:///github.com/"] = false; - urls["http://github.com/**//*"] = false; - urls["http://*.github.com/login"] = false; - urls["//github.com"] = true; - urls["github.com/{}<>"] = false; - urls["http:/example.com"] = false; - urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true; - urls["file:///Users/testUser/Code/test.html"] = true; - urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true; - - QHashIterator i(urls); - while (i.hasNext()) { - i.next(); - QCOMPARE(Tools::checkUrlValid(i.key()), i.value()); - } -} void TestBrowser::testBestMatchingCredentials() { @@ -741,19 +673,3 @@ void TestBrowser::testBestMatchingWithAdditionalURLs() QCOMPARE(sorted.length(), 1); QCOMPARE(sorted[0]->url(), urls[0]); } - -void TestBrowser::testIsUrlIdentical() -{ - QVERIFY(browserService()->isUrlIdentical("https://example.com", "https://example.com")); - QVERIFY(browserService()->isUrlIdentical("https://example.com", " https://example.com ")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com", "https://example2.com")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com/", "https://example.com/#login")); - QVERIFY(browserService()->isUrlIdentical("https://example.com", "https://example.com/")); - QVERIFY(browserService()->isUrlIdentical("https://example.com/", "https://example.com")); - QVERIFY(browserService()->isUrlIdentical("https://example.com/ ", " https://example.com")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com/", " example.com")); - QVERIFY(browserService()->isUrlIdentical("https://example.com/path/to/nowhere", - "https://example.com/path/to/nowhere/")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com/", "://example.com/")); - QVERIFY(browserService()->isUrlIdentical("ftp://127.0.0.1/", "ftp://127.0.0.1")); -} diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h index ed8146b574..48ac3b1cd5 100644 --- a/tests/TestBrowser.h +++ b/tests/TestBrowser.h @@ -37,8 +37,6 @@ private slots: void testGetBase64FromKey(); void testIncrementNonce(); void testBuildResponse(); - void testTopLevelDomain(); - void testIsIpAddress(); void testSortPriority(); void testSortPriority_data(); void testSearchEntries(); @@ -49,10 +47,8 @@ private slots: void testSearchEntriesWithAdditionalURLs(); void testInvalidEntries(); void testSubdomainsAndPaths(); - void testValidURLs(); void testBestMatchingCredentials(); void testBestMatchingWithAdditionalURLs(); - void testIsUrlIdentical(); private: QList createEntries(QStringList& urls, Group* root) const; diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index b7b3473d78..3506426184 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -313,15 +313,11 @@ void TestEntryModel::testProxyModel() modelSource->setGroup(db->rootGroup()); - /** - * @author Fonic - * Update comparison value of modelProxy->columnCount() to account for - * additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified', - * 'Accessed', 'Paperclip', 'Attachments', and TOTP - */ + // Test hiding and showing a column + auto columnCount = modelProxy->columnCount(); QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int))); modelProxy->hideColumn(0, true); - QCOMPARE(modelProxy->columnCount(), 14); + QCOMPARE(modelProxy->columnCount(), columnCount - 1); QVERIFY(!spyColumnRemove.isEmpty()); int oldSpyColumnRemoveSize = spyColumnRemove.size(); @@ -335,15 +331,9 @@ void TestEntryModel::testProxyModel() entryList << entry; modelSource->setEntries(entryList); - /** - * @author Fonic - * Update comparison value of modelProxy->columnCount() to account for - * additional columns 'Password', 'Notes', 'Expires', 'Created', 'Modified', - * 'Accessed', 'Paperclip', 'Attachments', and TOTP - */ QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int))); modelProxy->hideColumn(0, false); - QCOMPARE(modelProxy->columnCount(), 15); + QCOMPARE(modelProxy->columnCount(), columnCount); QVERIFY(!spyColumnInsert.isEmpty()); int oldSpyColumnInsertSize = spyColumnInsert.size(); diff --git a/tests/TestUrlTools.cpp b/tests/TestUrlTools.cpp new file mode 100644 index 0000000000..0e3ef844ee --- /dev/null +++ b/tests/TestUrlTools.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestUrlTools.h" +#include + +QTEST_GUILESS_MAIN(TestUrlTools) + +void TestUrlTools::initTestCase() +{ + m_urlTools = urlTools(); +} + +void TestUrlTools::init() +{ +} + +void TestUrlTools::testTopLevelDomain() +{ + // Create list of URLs and expected TLD responses + QList> tldUrls{ + {QString("https://another.example.co.uk"), QString("co.uk")}, + {QString("https://www.example.com"), QString("com")}, + {QString("https://github.com"), QString("com")}, + {QString("http://test.net"), QString("net")}, + {QString("http://so.many.subdomains.co.jp"), QString("co.jp")}, + {QString("https://192.168.0.1"), QString("192.168.0.1")}, + {QString("https://192.168.0.1:8000"), QString("192.168.0.1")}, + {QString("https://www.nic.ar"), QString("ar")}, + {QString("https://no.no.no"), QString("no")}, + {QString("https://www.blogspot.com.ar"), QString("blogspot.com.ar")}, // blogspot.com.ar is a TLD + {QString("https://jap.an.ide.kyoto.jp"), QString("ide.kyoto.jp")}, // ide.kyoto.jp is a TLD + {QString("ar"), QString("ar")}, + }; + + for (const auto& u : tldUrls) { + QCOMPARE(urlTools()->getTopLevelDomainFromUrl(u.first), u.second); + } + + // Create list of URLs and expected base URL responses + QList> baseUrls{ + {QString("https://another.example.co.uk"), QString("example.co.uk")}, + {QString("https://www.example.com"), QString("example.com")}, + {QString("http://test.net"), QString("test.net")}, + {QString("http://so.many.subdomains.co.jp"), QString("subdomains.co.jp")}, + {QString("https://192.168.0.1"), QString("192.168.0.1")}, + {QString("https://192.168.0.1:8000"), QString("192.168.0.1")}, + {QString("https://www.nic.ar"), QString("nic.ar")}, + {QString("https://www.blogspot.com.ar"), QString("www.blogspot.com.ar")}, // blogspot.com.ar is a TLD + {QString("https://www.arpa"), QString("www.arpa")}, + {QString("https://jap.an.ide.kyoto.jp"), QString("an.ide.kyoto.jp")}, // ide.kyoto.jp is a TLD + {QString("https://kobe.jp"), QString("kobe.jp")}, + }; + + for (const auto& u : baseUrls) { + QCOMPARE(urlTools()->getBaseDomainFromUrl(u.first), u.second); + } +} + +void TestUrlTools::testIsIpAddress() +{ + auto host1 = "example.com"; // Not valid + auto host2 = "192.168.0.1"; + auto host3 = "278.21.2.0"; // Not valid + auto host4 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + auto host5 = "2001:db8:0:1:1:1:1:1"; + auto host6 = "fe80::1ff:fe23:4567:890a"; + auto host7 = "2001:20::1"; + auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid + + QVERIFY(!urlTools()->isIpAddress(host1)); + QVERIFY(urlTools()->isIpAddress(host2)); + QVERIFY(!urlTools()->isIpAddress(host3)); + QVERIFY(urlTools()->isIpAddress(host4)); + QVERIFY(urlTools()->isIpAddress(host5)); + QVERIFY(urlTools()->isIpAddress(host6)); + QVERIFY(urlTools()->isIpAddress(host7)); + QVERIFY(!urlTools()->isIpAddress(host8)); +} + +void TestUrlTools::testIsUrlIdentical() +{ + QVERIFY(urlTools()->isUrlIdentical("https://example.com", "https://example.com")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com", " https://example.com ")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com", "https://example2.com")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com/", "https://example.com/#login")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com", "https://example.com/")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com/", "https://example.com")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com/ ", " https://example.com")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com/", " example.com")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com/path/to/nowhere", "https://example.com/path/to/nowhere/")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com/", "://example.com/")); + QVERIFY(urlTools()->isUrlIdentical("ftp://127.0.0.1/", "ftp://127.0.0.1")); +} + +void TestUrlTools::testIsUrlValid() +{ + QHash urls; + urls["https://github.com/login"] = true; + urls["https:///github.com/"] = false; + urls["http://github.com/**//*"] = false; + urls["http://*.github.com/login"] = false; + urls["//github.com"] = true; + urls["github.com/{}<>"] = false; + urls["http:/example.com"] = false; + urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true; + urls["file:///Users/testUser/Code/test.html"] = true; + urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true; + + QHashIterator i(urls); + while (i.hasNext()) { + i.next(); + QCOMPARE(urlTools()->isUrlValid(i.key()), i.value()); + } +} diff --git a/tests/TestUrlTools.h b/tests/TestUrlTools.h new file mode 100644 index 0000000000..d26e470406 --- /dev/null +++ b/tests/TestUrlTools.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_TESTURLTOOLS_H +#define KEEPASSXC_TESTURLTOOLS_H + +#include "core/UrlTools.h" +#include +#include + +class TestUrlTools : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void init(); + + void testTopLevelDomain(); + void testIsIpAddress(); + void testIsUrlIdentical(); + void testIsUrlValid(); + +private: + QPointer m_urlTools; +}; +#endif // KEEPASSXC_TESTURLTOOLS_H diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 170bf3a175..4840092ea6 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -1082,6 +1082,13 @@ void TestGui::testSearch() QCOMPARE(groupView->currentGroup(), m_db->rootGroup()); QVERIFY(!m_dbWidget->isSearchActive()); + // check if first entry is selected after search + QTest::keyClicks(searchTextEdit, "some"); + QTRY_VERIFY(m_dbWidget->isSearchActive()); + QTRY_COMPARE(entryView->selectedEntries().length(), 1); + QModelIndex index_current = entryView->indexFromEntry(entryView->currentEntry()); + QTRY_COMPARE(index_current.row(), 0); + // Try to edit the first entry from the search view // Refocus back to search edit QTest::mouseClick(searchTextEdit, Qt::LeftButton);