diff --git a/CMakeLists.txt b/CMakeLists.txt index 0fc8db81b..abac73c94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,9 +20,10 @@ project(KeePassXC) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING - "Choose the type of build, options are: None Debug Release RelWithDebInfo Debug DebugFull Profile MinSizeRel." + "Choose the type of build, options are: Debug Release RelWithDebInfo Profile" FORCE) endif() +string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) @@ -40,22 +41,21 @@ option(WITH_ASAN "Enable address sanitizer checks (Linux / macOS only)" OFF) option(WITH_COVERAGE "Use to build with coverage tests (GCC only)." OFF) option(WITH_APP_BUNDLE "Enable Application Bundle for macOS" ON) -set(WITH_XC_ALL OFF CACHE BOOLEAN "Build in all available plugins") +set(WITH_XC_ALL OFF CACHE BOOL "Build in all available plugins") option(WITH_XC_AUTOTYPE "Include Auto-Type." ON) -option(WITH_XC_NETWORKING "Include networking code (e.g. for downlading website icons)." OFF) +option(WITH_XC_NETWORKING "Include networking code (e.g. for downloading website icons)." OFF) option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF) option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) -option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF) -option(WITH_XC_KEESHARE_SECURE "Sharing integration with secured KeeShare containers" OFF) +option(WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)" OFF) option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON) if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() if(WITH_XC_ALL) - # Enable all options + # Enable all options (except update check) set(WITH_XC_AUTOTYPE ON) set(WITH_XC_NETWORKING ON) set(WITH_XC_BROWSER ON) @@ -67,18 +67,16 @@ if(WITH_XC_ALL) endif() endif() -if(WITH_XC_KEESHARE_SECURE) - set(WITH_XC_KEESHARE ON) -endif() - if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE) set(WITH_XC_CRYPTO_SSH ON) else() set(WITH_XC_CRYPTO_SSH OFF) endif() -if(WITH_XC_UPDATECHECK) - set(WITH_XC_NETWORKING ON) +# Prefer WITH_XC_NETWORKING setting over WITH_XC_UPDATECHECK +if(NOT WITH_XC_NETWORKING AND WITH_XC_UPDATECHECK) + message(STATUS "Disabling WITH_XC_UPDATECHECK because WITH_XC_NETWORKING is disabled") + set(WITH_XC_UPDATECHECK OFF) endif() set(KEEPASSXC_VERSION_MAJOR "2") @@ -162,11 +160,15 @@ if("${CMAKE_SIZEOF_VOID_P}" EQUAL "4") set(IS_32BIT TRUE) endif() -if("${CMAKE_C_COMPILER}" MATCHES "clang$" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") +if("${CMAKE_C_COMPILER}" MATCHES "clang$" + OR "${CMAKE_EXTRA_GENERATOR_C_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__" + OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_COMPILER_IS_CLANG 1) endif() -if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") +if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$" + OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__" + OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_COMPILER_IS_CLANGXX 1) endif() @@ -199,7 +201,7 @@ add_gcc_compiler_flags("-Wformat=2 -Wmissing-format-attribute") add_gcc_compiler_flags("-fvisibility=hidden") add_gcc_compiler_cxxflags("-fvisibility-inlines-hidden") -if(CMAKE_BUILD_TYPE STREQUAL "Debug") +if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug") add_gcc_compiler_flags("-Werror") endif() @@ -230,7 +232,6 @@ if(WITH_ASAN) endif() -string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) if(CMAKE_BUILD_TYPE_LOWER MATCHES "(release|relwithdebinfo|minsizerel)") add_gcc_compiler_flags("-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2") endif() @@ -264,6 +265,11 @@ endif() add_gcc_compiler_cflags("-std=c99") add_gcc_compiler_cxxflags("-std=c++11") +if((CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.9.99) OR + (CMAKE_COMPILER_IS_CLANGXX AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.6.99)) + add_gcc_compiler_cxxflags("-fsized-deallocation") +endif() + if(APPLE) add_gcc_compiler_cxxflags("-stdlib=libc++") endif() @@ -276,7 +282,7 @@ if(MINGW) set(CMAKE_RC_COMPILER_INIT windres) enable_language(RC) set(CMAKE_RC_COMPILE_OBJECT " -O coff -i -o ") - if(NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + if(NOT (CMAKE_BUILD_TYPE_LOWER STREQUAL "debug" OR CMAKE_BUILD_TYPE_LOWER STREQUAL "relwithdebinfo")) # Enable DEP and ASLR set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase") @@ -365,10 +371,17 @@ if(APPLE) set(CMAKE_MACOSX_RPATH TRUE) find_program(MACDEPLOYQT_EXE macdeployqt HINTS ${Qt5_PREFIX}/bin ENV PATH) if(NOT MACDEPLOYQT_EXE) - message(FATAL_ERROR "macdeployqt is required to build in macOS") + message(FATAL_ERROR "macdeployqt is required to build on macOS") else() message(STATUS "Using macdeployqt: ${MACDEPLOYQT_EXE}") endif() +elseif(MINGW) + find_program(WINDEPLOYQT_EXE windeployqt HINTS ${Qt5_PREFIX}/bin ENV PATH) + if(NOT WINDEPLOYQT_EXE) + message(FATAL_ERROR "windeployqt is required to build on Windows") + else() + message(STATUS "Using windeployqt: ${WINDEPLOYQT_EXE}") + endif() endif() # Debian sets the the build type to None for package builds. @@ -380,6 +393,7 @@ find_package(Gcrypt 1.7.0 REQUIRED) find_package(Argon2 REQUIRED) find_package(ZLIB REQUIRED) find_package(QREncode REQUIRED) +find_package(sodium 1.0.12 REQUIRED) set(CMAKE_REQUIRED_INCLUDES ${ZLIB_INCLUDE_DIR}) @@ -387,20 +401,7 @@ if(ZLIB_VERSION_STRING VERSION_LESS "1.2.0") message(FATAL_ERROR "zlib 1.2.0 or higher is required to use the gzip format") endif() -include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) - -# Optional -if(WITH_XC_KEESHARE) - set(WITH_XC_KEESHARE_INSECURE ON) - if(WITH_XC_KEESHARE_SECURE) - # ZLIB is needed and already required - find_package(QuaZip REQUIRED) - include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) - endif() -else() - set(WITH_XC_KEESHARE_INSECURE OFF) - set(WITH_XC_KEESHARE_SECURE OFF) -endif() +include_directories(SYSTEM ${ARGON2_INCLUDE_DIR} ${sodium_INCLUDE_DIR}) # Optional if(WITH_XC_YUBIKEY) diff --git a/INSTALL.md b/INSTALL.md index 9ecaf83f7..c0c3a7a08 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -25,8 +25,8 @@ The following libraries are required: * zlib * libmicrohttpd * libxi, libxtst, qtx11extras (optional for auto-type on X11) -* libsodium (>= 1.0.12, optional for KeePassXC-Browser support) -* argon2 +* libsodium (>= 1.0.12) +* libargon2 * qrencode * yubikey ykpers (optional to support YubiKey) @@ -99,18 +99,26 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct -DWITH_XC_AUTOTYPE=[ON|OFF] Enable/Disable Auto-Type (default: ON) -DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF) -DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF) - -DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (favicon download) (default: OFF) + -DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF) -DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF) - -DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group syncronization extension (default: OFF) -DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF) + -DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group synchronization extension (default: OFF) + -DWITH_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare signed containers, requires libquazip5 (default: OFF) -DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF) - -DWITH_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare secure containers, requires libquazip5 (default: OFF) + + -DWITH_XC_UPDATECHECK=[ON|OFF] Enable/Disable automatic updating checking (requires WITH_XC_NETWORKING) (default: ON) + -DWITH_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON) -DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF) -DWITH_DEV_BUILD=[ON|OFF] Enable/Disable deprecated method warnings (default: OFF) -DWITH_ASAN=[ON|OFF] Enable/Disable address sanitizer checks (Linux / macOS only) (default: OFF) -DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF) -DWITH_APP_BUNDLE=[ON|OFF] Enable Application Bundle for macOS (default: ON) + + -DKEEPASSXC_BUILD_TYPE=[Snapshot|PreRelease|Release] Set the build type to show/hide stability warnings (default: "Snapshot") + -DKEEPASSXC_DIST_TYPE=[Snap|AppImage|Other] Specify the distribution method (default: "Other") + -DOVERRIDE_VERSION=[X.X.X] Specify a version number when building. Used with snapshot builds (default: "") + -DGIT_HEAD_OVERRIDE=[XXXXXXX] Specify the 7 digit git commit ref for this build. Used with distribution builds (default: "") ``` * If you are on MacOS you must add this parameter to **Cmake**, with the Qt version you have installed
`-DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake/` diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake index d10791745..f5287b75b 100644 --- a/cmake/CodeCoverage.cmake +++ b/cmake/CodeCoverage.cmake @@ -112,7 +112,7 @@ mark_as_advanced( CMAKE_EXE_LINKER_FLAGS_COVERAGE CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) -if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") +if(NOT CMAKE_BUILD_TYPE_LOWER STREQUAL "debug") message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading") endif() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug" diff --git a/cmake/FindQuaZip.cmake b/cmake/FindQuaZip.cmake index 8d3091810..a387e2f81 100644 --- a/cmake/FindQuaZip.cmake +++ b/cmake/FindQuaZip.cmake @@ -1,41 +1,24 @@ -# QUAZIP_FOUND - QuaZip library was found -# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir -# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR) -# QUAZIP_LIBRARIES - List of QuaZip libraries -# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers +# QUAZIP_FOUND - QuaZip library was found +# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir +# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR) +# QUAZIP_LIBRARIES - List of QuaZip libraries +# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers -IF(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) - # in cache already - SET(QUAZIP_FOUND TRUE) -ELSE(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) - IF(Qt5Core_FOUND) - set(QUAZIP_LIB_VERSION_SUFFIX 5) - ENDIF() - IF(WIN32) - FIND_PATH(QUAZIP_LIBRARY_DIR - WIN32_DEBUG_POSTFIX d - NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll - HINTS "C:/Programme/" "C:/Program Files" - PATH_SUFFIXES QuaZip/lib +if(MINGW) + find_library(QUAZIP_LIBRARIES libquazip5) + find_path(QUAZIP_INCLUDE_DIR quazip.h PATH_SUFFIXES quazip5) + find_path(QUAZIP_ZLIB_INCLUDE_DIR zlib.h) +else() + find_library(QUAZIP_LIBRARIES + NAMES quazip5 quazip + PATHS /usr/lib /usr/lib64 /usr/local/lib ) - FIND_LIBRARY(QUAZIP_LIBRARIES NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll HINTS ${QUAZIP_LIBRARY_DIR}) - FIND_PATH(QUAZIP_INCLUDE_DIR NAMES quazip.h HINTS ${QUAZIP_LIBRARY_DIR}/../ PATH_SUFFIXES include/quazip5) - FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR NAMES zlib.h) - ELSE(WIN32) - FIND_PACKAGE(PkgConfig) - pkg_check_modules(PC_QUAZIP quazip) - FIND_LIBRARY(QUAZIP_LIBRARIES - WIN32_DEBUG_POSTFIX d - NAMES quazip${QUAZIP_LIB_VERSION_SUFFIX} - HINTS /usr/lib /usr/lib64 + find_path(QUAZIP_INCLUDE_DIR quazip.h + PATHS /usr/include /usr/local/include + PATH_SUFFIXES quazip5 quazip ) - FIND_PATH(QUAZIP_INCLUDE_DIR quazip.h - HINTS /usr/include /usr/local/include - PATH_SUFFIXES quazip${QUAZIP_LIB_VERSION_SUFFIX} - ) - FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR zlib.h HINTS /usr/include /usr/local/include) - ENDIF(WIN32) - INCLUDE(FindPackageHandleStandardArgs) - SET(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR}) - find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS) -ENDIF(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) + find_path(QUAZIP_ZLIB_INCLUDE_DIR zlib.h PATHS /usr/include /usr/local/include) +endif() +include(FindPackageHandleStandardArgs) +set(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR}) +find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS) diff --git a/snap/local/launchers/README.md b/snap/local/launchers/README.md new file mode 100644 index 000000000..334fbbdcc --- /dev/null +++ b/snap/local/launchers/README.md @@ -0,0 +1,11 @@ +# /snap/local/launchers +Here are the launchers, or wrapper programs to deal with some runtime-fixable problems for the snapped applications, like setting proper environmental variables in snap. + +In convention launchers are named _something_-launch, for dealing certain problem with _something_, and usually can be called in a stacked manner to consolidate their modifications. + +```yaml +apps: + _app_name_: + command: foo-launch bar-launch _app_command_ +``` + diff --git a/snap/local/launchers/gtk3-env-launch b/snap/local/launchers/gtk3-env-launch new file mode 100755 index 000000000..f017e8611 --- /dev/null +++ b/snap/local/launchers/gtk3-env-launch @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# This is the maintainence launcher for the snap, make necessary runtime environment changes to make the snap work here. You may also insert security confinement/deprecation/obsoletion notice of the snap here. + +set \ + -o errexit \ + -o errtrace \ + -o nounset \ + -o pipefail + +# gtk-common-themes support +export QT_QPA_PLATFORMTHEME=gtk3 + +# Finally run the next part of the command chain +exec "${@}" diff --git a/snapcraft.yaml b/snap/snapcraft.yaml similarity index 75% rename from snapcraft.yaml rename to snap/snapcraft.yaml index e249c6cb3..9ba5d8f53 100644 --- a/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -9,16 +9,28 @@ description: | confinement: strict base: core18 -plugs: - icon-themes: # fix mouse cursor theme +plugs: # plugs for theming, font settings, cursor and to use gtk3 file chooser + gtk-3-themes: + interface: content + target: $SNAP/data-dir/themes + default-provider: gtk-common-themes:gtk-3-themes + icon-themes: interface: content target: $SNAP/data-dir/icons - default-provider: gtk-common-themes + default-provider: gtk-common-themes:icon-themes + sound-themes: + interface: content + target: $SNAP/data-dir/sounds + default-provider: gtk-common-themes:sounds-themes apps: keepassxc: - command: desktop-launch keepassxc - plugs: [unity7, x11, opengl, gsettings, home, network, network-bind, removable-media, raw-usb, wayland, desktop-legacy] + adapter: full + command: usr/bin/keepassxc -style fusion + command-chain: + - bin/desktop-launch + - bin/gtk3-env-launch + plugs: [unity7, x11, opengl, gsettings, home, network, network-bind, removable-media, raw-usb, wayland, desktop-legacy, desktop] desktop: usr/share/applications/org.keepassxc.KeePassXC.desktop environment: DISABLE_WAYLAND: 1 @@ -73,7 +85,7 @@ parts: - libquazip5-1 - libusb-1.0-0 - qtwayland5 - - qt5-style-plugins # for mouse cursor theme fix + - qt5-gtk-platformtheme # for theming, font settings, cursor and to use gtk3 file chooser override-build: | snapcraftctl build sed -i 's|Icon=keepassxc|Icon=${SNAP}/usr/share/icons/hicolor/256x256/apps/keepassxc.png|g' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.keepassxc.KeePassXC.desktop @@ -82,7 +94,15 @@ parts: stage: - -opt after: [desktop-qt5] - + + launchers: # custom launcher to set QT_QPA_PLATFORMTHEME=gtk3 correctly + source: snap/local/launchers + plugin: dump + organize: + '*': bin/ + stage: + - -bin/README.* + desktop-qt5: source: https://github.com/ubuntu/snapcraft-desktop-helpers.git source-subdir: qt diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ef12fadb2..5ddcb76ea 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,9 +16,6 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) -configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h) -configure_file(git-info.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/git-info.h) - find_library(ZXCVBN_LIBRARIES zxcvbn) if(NOT ZXCVBN_LIBRARIES) add_library(zxcvbn STATIC zxcvbn/zxcvbn.c) @@ -27,6 +24,7 @@ if(NOT ZXCVBN_LIBRARIES) endif(NOT ZXCVBN_LIBRARIES) set(keepassx_SOURCES + core/Alloc.cpp core/AutoTypeAssociations.cpp core/AutoTypeMatch.cpp core/Compare.cpp @@ -167,7 +165,8 @@ if(APPLE) core/ScreenLockListenerMac.cpp core/MacPasteboard.cpp gui/macutils/MacUtils.cpp - gui/macutils/AppKitImpl.mm) + gui/macutils/AppKitImpl.mm + gui/macutils/AppKit.h) endif() if(UNIX AND NOT APPLE) set(keepassx_SOURCES @@ -192,8 +191,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") -add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare") -add_feature_info(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources") +add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") add_feature_info(UpdateCheck WITH_XC_UPDATECHECK "Automatic update checking") if(APPLE) @@ -255,8 +253,13 @@ endif() if(WITH_XC_TOUCHID) list(APPEND keepassx_SOURCES touchid/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 -Wno-error") endif() +configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h) +configure_file(git-info.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/git-info.h) + add_library(autotype STATIC ${autotype_SOURCES}) target_link_libraries(autotype Qt5::Core Qt5::Widgets) @@ -271,6 +274,7 @@ target_link_libraries(keepassx_core Qt5::Concurrent Qt5::Network Qt5::Widgets + ${sodium_LIBRARY_RELEASE} ${YUBIKEY_LIBRARIES} ${ZXCVBN_LIBRARIES} ${ARGON2_LIBRARIES} @@ -411,25 +415,19 @@ if(MINGW) install(CODE "set(gp_tool \"objdump\")" COMPONENT Runtime) - include(DeployQt4) - install_qt4_executable(${PROGNAME}.exe) + # Deploy all 3rd party library dependencies first + install(CODE "include(BundleUtilities) + fixup_bundle(\"\${CMAKE_INSTALL_PREFIX}/${PROGNAME}.exe\" \"\" \"\")" + COMPONENT Runtime) - # install Qt5 plugins - set(PLUGINS_DIR ${Qt5_PREFIX}/share/qt5/plugins) - install(FILES - ${PLUGINS_DIR}/platforms/qwindows$<$:d>.dll - ${PLUGINS_DIR}/platforms/qdirect2d$<$:d>.dll - DESTINATION "platforms") - install(FILES ${PLUGINS_DIR}/styles/qwindowsvistastyle$<$:d>.dll DESTINATION "styles") - install(FILES ${PLUGINS_DIR}/platforminputcontexts/qtvirtualkeyboardplugin$<$:d>.dll DESTINATION "platforminputcontexts") - install(FILES ${PLUGINS_DIR}/iconengines/qsvgicon$<$:d>.dll DESTINATION "iconengines") - install(FILES - ${PLUGINS_DIR}/imageformats/qgif$<$:d>.dll - ${PLUGINS_DIR}/imageformats/qicns$<$:d>.dll - ${PLUGINS_DIR}/imageformats/qico$<$:d>.dll - ${PLUGINS_DIR}/imageformats/qjpeg$<$:d>.dll - ${PLUGINS_DIR}/imageformats/qwebp$<$:d>.dll - DESTINATION "imageformats") + # Use windeployqt.exe to setup Qt dependencies + set(WINDEPLOYQT_MODE "--release") + if(CMAKE_BUILD_TYPE_LOWER STREQUAL "debug") + set(WINDEPLOYQT_MODE "--debug") + endif() + + install(CODE "execute_process(COMMAND ${WINDEPLOYQT_EXE} ${PROGNAME}.exe ${WINDEPLOYQT_MODE} WORKING_DIRECTORY \${CMAKE_INSTALL_PREFIX} OUTPUT_QUIET)" + COMPONENT Runtime) # install CA cert chains install(FILES ${Qt5_PREFIX}/ssl/certs/ca-bundle.crt DESTINATION "ssl/certs") diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 9c06c2487..112a7cda9 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -296,6 +296,7 @@ QString BrowserService::storeKey(const QString& key) do { QInputDialog keyDialog; + connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), &keyDialog, SLOT(reject())); keyDialog.setWindowTitle(tr("KeePassXC: New key association request")); keyDialog.setLabelText(tr("You have received an association request for the above key.\n\n" "If you would like to allow it access to your KeePassXC database,\n" @@ -310,7 +311,7 @@ QString BrowserService::storeKey(const QString& key) id = keyDialog.textValue(); - if (ok != QDialog::Accepted || id.isEmpty()) { + if (ok != QDialog::Accepted || id.isEmpty() || !isDatabaseOpened()) { hideWindow(); return {}; } @@ -406,6 +407,11 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id, return QJsonArray(); } + // Ensure that database is not locked when the popup was visible + if (!isDatabaseOpened()) { + return QJsonArray(); + } + // Sort results pwEntries = sortEntries(pwEntries, host, submitUrl); @@ -760,6 +766,7 @@ bool BrowserService::confirmEntries(QList& pwEntriesToConfirm, m_dialogActive = true; BrowserAccessControlDialog accessControlDialog; + connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), &accessControlDialog, SLOT(reject())); accessControlDialog.setUrl(url); accessControlDialog.setItems(pwEntriesToConfirm); diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt index 10189d931..7e813eb5b 100755 --- a/src/browser/CMakeLists.txt +++ b/src/browser/CMakeLists.txt @@ -16,7 +16,6 @@ if(WITH_XC_BROWSER) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) - find_package(sodium 1.0.12 REQUIRED) set(keepassxcbrowser_SOURCES BrowserAccessControlDialog.cpp @@ -33,5 +32,5 @@ if(WITH_XC_BROWSER) Variant.cpp) add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES}) - target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network sodium) + target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${sodium_LIBRARY_RELEASE}) endif() diff --git a/src/cli/Add.cpp b/src/cli/Add.cpp index 395b84919..975d549e5 100644 --- a/src/cli/Add.cpp +++ b/src/cli/Add.cpp @@ -84,7 +84,7 @@ int Add::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli add"); + errorTextStream << parser.helpText().replace("[options]", "add [options]"); return EXIT_FAILURE; } diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index c3f97a2cd..2f4a7275e 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -38,6 +38,7 @@ target_link_libraries(keepassxc-cli keepassx_core Qt5::Core ${GCRYPT_LIBRARIES} + ${sodium_LIBRARY_RELEASE} ${ARGON2_LIBRARIES} ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES} diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index 31b421de6..e1e74c682 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -63,7 +63,7 @@ int Clip::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2 && args.size() != 3) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli clip"); + errorTextStream << parser.helpText().replace("[options]", "clip [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Create.cpp b/src/cli/Create.cpp index b8c094f90..80dcb5691 100644 --- a/src/cli/Create.cpp +++ b/src/cli/Create.cpp @@ -70,7 +70,7 @@ int Create::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() < 1) { - out << parser.helpText().replace("keepassxc-cli", "keepassxc-cli create"); + out << parser.helpText().replace("[options]", "create [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Diceware.cpp b/src/cli/Diceware.cpp index f11347344..c663cfc39 100644 --- a/src/cli/Diceware.cpp +++ b/src/cli/Diceware.cpp @@ -58,7 +58,7 @@ int Diceware::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (!args.isEmpty()) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli diceware"); + errorTextStream << parser.helpText().replace("[options]", "diceware [options]"); return EXIT_FAILURE; } @@ -78,7 +78,7 @@ int Diceware::execute(const QStringList& arguments) } if (!dicewareGenerator.isValid()) { - outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli diceware"); + outputTextStream << parser.helpText().replace("[options]", "diceware [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Edit.cpp b/src/cli/Edit.cpp index 76e996c98..59cedd7c9 100644 --- a/src/cli/Edit.cpp +++ b/src/cli/Edit.cpp @@ -88,7 +88,7 @@ int Edit::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli edit"); + errorTextStream << parser.helpText().replace("[options]", "edit [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Estimate.cpp b/src/cli/Estimate.cpp index 7064963f4..c278b50f3 100644 --- a/src/cli/Estimate.cpp +++ b/src/cli/Estimate.cpp @@ -171,7 +171,7 @@ int Estimate::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() > 1) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli estimate"); + errorTextStream << parser.helpText().replace("[options]", "estimate [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index 45c961332..054a391bd 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -53,7 +53,7 @@ int Extract::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 1) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli extract"); + errorTextStream << parser.helpText().replace("[options]", "extract [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Generate.cpp b/src/cli/Generate.cpp index 5f0ad98ac..e8ca90275 100644 --- a/src/cli/Generate.cpp +++ b/src/cli/Generate.cpp @@ -84,7 +84,7 @@ int Generate::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (!args.isEmpty()) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli generate"); + errorTextStream << parser.helpText().replace("[options]", "generate [options]"); return EXIT_FAILURE; } @@ -128,7 +128,7 @@ int Generate::execute(const QStringList& arguments) passwordGenerator.setExcludedChars(parser.value(exclude)); if (!passwordGenerator.isValid()) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli generate"); + errorTextStream << parser.helpText().replace("[options]", "generate [options]"); return EXIT_FAILURE; } diff --git a/src/cli/List.cpp b/src/cli/List.cpp index ebf7bfda1..52797470c 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -59,7 +59,7 @@ int List::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 1 && args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli ls"); + errorTextStream << parser.helpText().replace("[options]", "ls [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Locate.cpp b/src/cli/Locate.cpp index 81bbdd55d..af5f24196 100644 --- a/src/cli/Locate.cpp +++ b/src/cli/Locate.cpp @@ -56,7 +56,7 @@ int Locate::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli locate"); + errorTextStream << parser.helpText().replace("[options]", "locate [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index a7357394f..2356f5d3a 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -69,7 +69,7 @@ int Merge::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli merge"); + errorTextStream << parser.helpText().replace("[options]", "merge [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Remove.cpp b/src/cli/Remove.cpp index 07da23b7b..bb2374e9a 100644 --- a/src/cli/Remove.cpp +++ b/src/cli/Remove.cpp @@ -58,7 +58,7 @@ int Remove::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli rm"); + errorTextStream << parser.helpText().replace("[options]", "rm [options]"); return EXIT_FAILURE; } diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index d16fbfe3c..3abccd79c 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -69,7 +69,7 @@ int Show::execute(const QStringList& arguments) const QStringList args = parser.positionalArguments(); if (args.size() != 2) { - errorTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli show"); + errorTextStream << parser.helpText().replace("[options]", "show [options]"); return EXIT_FAILURE; } diff --git a/src/cli/TextStream.cpp b/src/cli/TextStream.cpp index d75cb74a9..938fd6292 100644 --- a/src/cli/TextStream.cpp +++ b/src/cli/TextStream.cpp @@ -19,6 +19,9 @@ #include #include +#ifdef Q_OS_WIN +#include +#endif TextStream::TextStream() { @@ -59,12 +62,26 @@ void TextStream::detectCodec() { QString codecName = "UTF-8"; auto env = QProcessEnvironment::systemEnvironment(); + #ifdef Q_OS_WIN - if (!env.contains("SHELL")) { - // native shell (no Msys or cygwin) + WINBOOL success = false; +#ifdef CP_UTF8 + success = SetConsoleOutputCP(CP_UTF8); +#endif + if (!success && !env.contains("SHELL")) { + // Fall back to cp850 if this is Windows without CP_UTF8 and we + // are running in a native shell (i.e., no Msys or Cygwin). codecName = "Windows-850"; } +#else + if (env.contains("LANG") && !env.value("LANG").isEmpty() && env.value("LANG") != "C") { + // Only override codec if LANG is set, otherwise Qt will assume + // US-ASCII, which is almost always wrong and results in + // Unicode passwords being displayed as question marks. + codecName = QTextCodec::codecForLocale()->name(); + } #endif + codecName = env.value("ENCODING_OVERRIDE", codecName); auto* codec = QTextCodec::codecForName(codecName.toLatin1()); if (codec) { diff --git a/src/core/Alloc.cpp b/src/core/Alloc.cpp new file mode 100644 index 000000000..a33b56196 --- /dev/null +++ b/src/core/Alloc.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2019 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 +#include +#include +#ifdef Q_OS_MACOS +#include +#else +#include +#endif + +#if defined(NDEBUG) && !defined(__cpp_sized_deallocation) +#warning "KeePassXC is being compiled without sized deallocation support. Deletes may be slow." +#endif + +/** + * Custom sized delete operator which securely zeroes out allocated + * memory before freeing it (requires C++14 sized deallocation support). + */ +void operator delete(void* ptr, std::size_t size) noexcept +{ + if (!ptr) { + return; + } + + sodium_memzero(ptr, size); + std::free(ptr); +} + +void operator delete[](void* ptr, std::size_t size) noexcept +{ + ::operator delete(ptr, size); +} + +/** + * Custom delete operator which securely zeroes out + * allocated memory before freeing it. + */ +void operator delete(void* ptr) noexcept +{ + if (!ptr) { + return; + } + +#if defined(Q_OS_WIN) + ::operator delete(ptr, _msize(ptr)); +#elif defined(Q_OS_MACOS) + ::operator delete(ptr, malloc_size(ptr)); +#elif defined(Q_OS_UNIX) + ::operator delete(ptr, malloc_usable_size(ptr)); +#else + // whatever OS this is, give up and simply free stuff + std::free(ptr); +#endif +} + +void operator delete[](void* ptr) noexcept +{ + ::operator delete(ptr); +} + +/** + * Custom insecure delete operator that does not zero out memory before + * freeing a buffer. Can be used for better performance. + */ +void operator delete(void* ptr, bool) noexcept +{ + std::free(ptr); +} + +void operator delete[](void* ptr, bool) noexcept +{ + ::operator delete(ptr, false); +} diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index a06bf74c1..2d1a3e087 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -85,6 +85,12 @@ namespace Bootstrap bootstrap(); MessageBox::initializeButtonDefs(); +#ifdef KEEPASSXC_DIST_SNAP + // snap: force fallback theme to avoid using system theme (gtk integration) + // with missing actions just like on Windows and macOS + QIcon::setThemeSearchPaths(QStringList() << ":/icons"); +#endif + #ifdef Q_OS_MACOS // Don't show menu icons on OSX QApplication::setAttribute(Qt::AA_DontShowIconsInMenus); diff --git a/src/core/CustomData.cpp b/src/core/CustomData.cpp index 86adae158..f009176a0 100644 --- a/src/core/CustomData.cpp +++ b/src/core/CustomData.cpp @@ -16,9 +16,12 @@ */ #include "CustomData.h" +#include "Clock.h" #include "core/Global.h" +const QString CustomData::LastModified = "_LAST_MODIFIED"; + CustomData::CustomData(QObject* parent) : QObject(parent) { @@ -60,6 +63,7 @@ void CustomData::set(const QString& key, const QString& value) if (addAttribute || changeValue) { m_data.insert(key, value); + updateLastModified(); emit customDataModified(); } @@ -74,6 +78,7 @@ void CustomData::remove(const QString& key) m_data.remove(key); + updateLastModified(); emit removed(key); emit customDataModified(); } @@ -94,6 +99,7 @@ void CustomData::rename(const QString& oldKey, const QString& newKey) m_data.remove(oldKey); m_data.insert(newKey, data); + updateLastModified(); emit customDataModified(); emit renamed(oldKey, newKey); } @@ -108,9 +114,19 @@ void CustomData::copyDataFrom(const CustomData* other) m_data = other->m_data; + updateLastModified(); emit reset(); emit customDataModified(); } + +QDateTime CustomData::getLastModified() const +{ + if (m_data.contains(LastModified)) { + return Clock::parse(m_data.value(LastModified)); + } + return {}; +} + bool CustomData::operator==(const CustomData& other) const { return (m_data == other.m_data); @@ -152,3 +168,13 @@ int CustomData::dataSize() const } return size; } + +void CustomData::updateLastModified() +{ + if (m_data.size() == 1 && m_data.contains(LastModified)) { + m_data.remove(LastModified); + return; + } + + m_data.insert(LastModified, Clock::currentDateTimeUtc().toString()); +} diff --git a/src/core/CustomData.h b/src/core/CustomData.h index d085c9409..126d4d84e 100644 --- a/src/core/CustomData.h +++ b/src/core/CustomData.h @@ -42,9 +42,12 @@ public: int size() const; int dataSize() const; void copyDataFrom(const CustomData* other); + QDateTime getLastModified() const; bool operator==(const CustomData& other) const; bool operator!=(const CustomData& other) const; + static const QString LastModified; + signals: void customDataModified(); void aboutToBeAdded(const QString& key); @@ -55,6 +58,10 @@ signals: void renamed(const QString& oldKey, const QString& newKey); void aboutToBeReset(); void reset(); + void lastModified(); + +private slots: + void updateLastModified(); private: QHash m_data; diff --git a/src/core/Merger.cpp b/src/core/Merger.cpp index c73248388..dcbed250f 100644 --- a/src/core/Merger.cpp +++ b/src/core/Merger.cpp @@ -609,9 +609,6 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) // TODO HNH: missing handling of recycle bin, names, templates for groups and entries, // public data (entries of newer dict override keys of older dict - ignoring // their own age - it is enough if one entry of the whole dict is newer) => possible lost update - // TODO HNH: CustomData is merged with entries of the new customData overwrite entries - // of the older CustomData - the dict with the newest entry is considered - // newer regardless of the age of the other entries => possible lost update ChangeList changes; auto* sourceMetadata = context.m_sourceDb->metadata(); auto* targetMetadata = context.m_targetDb->metadata(); @@ -624,5 +621,32 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) changes << tr("Adding missing icon %1").arg(QString::fromLatin1(customIconId.toRfc4122().toHex())); } } + + // Merge Custom Data if source is newer + const auto targetCustomDataModificationTime = sourceMetadata->customData()->getLastModified(); + const auto sourceCustomDataModificationTime = targetMetadata->customData()->getLastModified(); + if (!targetMetadata->customData()->contains(CustomData::LastModified) || + (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid() && + targetCustomDataModificationTime > sourceCustomDataModificationTime)) { + const auto sourceCustomDataKeys = sourceMetadata->customData()->keys(); + const auto targetCustomDataKeys = targetMetadata->customData()->keys(); + + // Check missing keys from source. Remove those from target + for (const auto& key : targetCustomDataKeys) { + if (!sourceMetadata->customData()->contains(key)) { + auto value = targetMetadata->customData()->value(key); + targetMetadata->customData()->remove(key); + changes << tr("Removed custom data %1 [%2]").arg(key, value); + } + } + + // Transfer new/existing keys + for (const auto& key : sourceCustomDataKeys) { + auto value = sourceMetadata->customData()->value(key); + targetMetadata->customData()->set(key, value); + changes << tr("Adding custom data %1 [%2]").arg(key, value); + } + } + return changes; } diff --git a/src/core/Metadata.cpp b/src/core/Metadata.cpp index 6448c391a..ff1ee71e7 100644 --- a/src/core/Metadata.cpp +++ b/src/core/Metadata.cpp @@ -195,7 +195,7 @@ QPixmap Metadata::customIconScaledPixmap(const QUuid& uuid) const QPixmapCache::Key& cacheKey = m_customIconScaledCacheKeys[uuid]; if (!QPixmapCache::find(cacheKey, &pixmap)) { - QImage image = m_customIcons.value(uuid).scaled(16, 16, Qt::KeepAspectRatio, Qt::SmoothTransformation); + QImage image = m_customIcons.value(uuid).scaled(16, 16, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); pixmap = QPixmap::fromImage(image); cacheKey = QPixmapCache::insert(pixmap); } diff --git a/src/core/Translator.cpp b/src/core/Translator.cpp index 595dadfa1..95de3ce91 100644 --- a/src/core/Translator.cpp +++ b/src/core/Translator.cpp @@ -34,13 +34,14 @@ */ void Translator::installTranslators() { + QLocale locale; QString language = config()->get("GUI/Language").toString(); - if (language == "system" || language.isEmpty()) { - language = QLocale::system().name(); - } - if (language == "en") { + if (!language.isEmpty() && language != "system") { // use actual English translation instead of the English locale source language - language = "en_US"; + if (language == "en") { + language = "en_US"; + } + locale = QLocale(language); } const QStringList paths = { @@ -51,11 +52,12 @@ void Translator::installTranslators() bool translationsLoaded = false; for (const QString& path : paths) { - translationsLoaded |= installTranslator(language, path) || installTranslator("en_US", path); + translationsLoaded |= installTranslator(locale, path) || installTranslator(QLocale("en_US"), path); if (!installQtTranslator(language, path)) { - installQtTranslator("en", path); + installQtTranslator(QLocale("en"), path); } } + if (!translationsLoaded) { // couldn't load configured language or fallback qWarning("Couldn't load translations."); @@ -114,10 +116,10 @@ QList> Translator::availableLanguages() * @param path local search path * @return true on success */ -bool Translator::installTranslator(const QString& language, const QString& path) +bool Translator::installTranslator(const QLocale& locale, const QString& path) { QScopedPointer translator(new QTranslator(qApp)); - if (translator->load(QString("keepassx_%1").arg(language), path)) { + if (translator->load(locale, "keepassx_", "", path)) { return QCoreApplication::installTranslator(translator.take()); } return false; @@ -131,13 +133,12 @@ bool Translator::installTranslator(const QString& language, const QString& path) * @param path local search path * @return true on success */ -bool Translator::installQtTranslator(const QString& language, const QString& path) +bool Translator::installQtTranslator(const QLocale& locale, const QString& path) { QScopedPointer qtTranslator(new QTranslator(qApp)); - if (qtTranslator->load(QString("qtbase_%1").arg(language), path)) { + if (qtTranslator->load(locale, "qtbase_", "", path)) { return QCoreApplication::installTranslator(qtTranslator.take()); - } else if (qtTranslator->load(QString("qtbase_%1").arg(language), - QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + } else if (qtTranslator->load(locale, "qtbase_", "", QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { return QCoreApplication::installTranslator(qtTranslator.take()); } return false; diff --git a/src/core/Translator.h b/src/core/Translator.h index cf62f48e4..cfc49d710 100644 --- a/src/core/Translator.h +++ b/src/core/Translator.h @@ -20,6 +20,7 @@ #include #include +#include class Translator { @@ -28,8 +29,8 @@ public: static QList> availableLanguages(); private: - static bool installTranslator(const QString& language, const QString& path); - static bool installQtTranslator(const QString& language, const QString& path); + static bool installTranslator(const QLocale& locale, const QString& path); + static bool installQtTranslator(const QLocale& locale, const QString& path); }; #endif // KEEPASSX_TRANSLATOR_H diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 22a49dece..2461230c8 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -64,6 +64,7 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) , m_globalAutoTypeModifiers(Qt::NoModifier) { setHeadline(tr("Application Settings")); + showApplyButton(false); m_secUi->setupUi(m_secWidget); m_generalUi->setupUi(m_generalWidget); @@ -75,7 +76,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) } connect(this, SIGNAL(accepted()), SLOT(saveSettings())); - connect(this, SIGNAL(apply()), SLOT(saveSettings())); connect(this, SIGNAL(rejected()), SLOT(reject())); // clang-format off diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index 344c2b81c..bf5cce2d3 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -212,7 +212,7 @@ - Use DuckDuckGo as fallback for downloading website icons + Use DuckDuckGo service to download website icons diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 313bfabb1..fb234795c 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -63,6 +63,10 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase())); connect(autoType(), SIGNAL(autotypeRejected()), SLOT(relockPendingDatabase())); // clang-format on + +#ifdef Q_OS_MACOS + connect(macUtils(), SIGNAL(lockDatabases()), SLOT(lockDatabases())); +#endif } DatabaseTabWidget::~DatabaseTabWidget() diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 8cfc40815..e4f175bf2 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1158,9 +1158,10 @@ void DatabaseWidget::onDatabaseModified() { if (!m_blockAutoSave && config()->get("AutoSaveAfterEveryChange").toBool()) { save(); + } else { + // Only block once, then reset + m_blockAutoSave = false; } - - m_blockAutoSave = false; } QString DatabaseWidget::getCurrentSearch() @@ -1258,11 +1259,13 @@ bool DatabaseWidget::lock() } if (m_db->isModified()) { + bool saved = false; + // Attempt to save on exit, but don't block locking if it fails if (config()->get("AutoSaveOnExit").toBool()) { - if (!save()) { - return false; - } - } else { + saved = save(); + } + + if (!saved) { QString msg; if (!m_db->metadata()->name().toHtmlEscaped().isEmpty()) { msg = tr("\"%1\" was modified.\nSave changes?").arg(m_db->metadata()->name().toHtmlEscaped()); @@ -1521,11 +1524,14 @@ bool DatabaseWidget::save() return true; } + // Read-only and new databases ask for filename if (m_db->isReadOnly() || m_db->filePath().isEmpty()) { return saveAs(); } + // Prevent recursions and infinite save loops blockAutoReload(true); + m_blockAutoSave = true; ++m_saveAttempts; // TODO: Make this async, but lock out the database widget to prevent re-entrance @@ -1536,6 +1542,7 @@ bool DatabaseWidget::save() if (ok) { m_saveAttempts = 0; + m_blockAutoSave = false; return true; } diff --git a/src/gui/DialogyWidget.cpp b/src/gui/DialogyWidget.cpp index 858d2949b..597bcc59d 100644 --- a/src/gui/DialogyWidget.cpp +++ b/src/gui/DialogyWidget.cpp @@ -35,7 +35,8 @@ void DialogyWidget::keyPressEvent(QKeyEvent* e) } } else #endif - if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { + if (!e->modifiers() || e->modifiers() == Qt::ControlModifier + || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index 242ae4542..dcc5160a3 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -197,8 +197,6 @@ void EditWidgetIcons::downloadFavicon() QString fullyQualifiedDomain = m_url.host(); - m_urlsToTry.append(QUrl(m_url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico")); - // Determine if host portion of URL is an IP address by resolving it and // searching for a match with the returned address(es). bool hostIsIp = false; @@ -209,32 +207,35 @@ void EditWidgetIcons::downloadFavicon() } } + // Determine the second-level domain, if available + QString secondLevelDomain; if (!hostIsIp) { - QString secondLevelDomain = getSecondLevelDomain(m_url); - - // Attempt to simply load the favicon.ico file - if (fullyQualifiedDomain != secondLevelDomain) { - m_urlsToTry.append(QUrl(m_url.scheme() + "://" + secondLevelDomain + "/favicon.ico")); - } + secondLevelDomain = getSecondLevelDomain(m_url); } - // Try to use alternative fallback URL, if enabled + // Start with the "fallback" url (if enabled) to try to get the best favicon if (config()->get("security/IconDownloadFallback", false).toBool()) { QUrl fallbackUrl = QUrl("https://icons.duckduckgo.com"); fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(fullyQualifiedDomain) + ".ico"); - m_urlsToTry.append(fallbackUrl); - if (!hostIsIp) { - QString secondLevelDomain = getSecondLevelDomain(m_url); - - if (fullyQualifiedDomain != secondLevelDomain) { - fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(secondLevelDomain) + ".ico"); - m_urlsToTry.append(fallbackUrl); - } + // Also try a direct pull of the second-level domain (if possible) + if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { + fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(secondLevelDomain) + ".ico"); + m_urlsToTry.append(fallbackUrl); } } + // Add a direct pull of the website's own favicon.ico file + m_urlsToTry.append(QUrl(m_url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico")); + + // Also try a direct pull of the second-level domain (if possible) + if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { + m_urlsToTry.append(QUrl(m_url.scheme() + "://" + secondLevelDomain + "/favicon.ico")); + } + + // Use the first URL to start the download process + // If a favicon is not found, the next URL will be tried startFetchFavicon(m_urlsToTry.takeFirst()); #endif } @@ -277,7 +278,7 @@ void EditWidgetIcons::fetchFinished() if (!image.isNull()) { if (!addCustomIcon(image)) { emit messageEditEntry(tr("Custom icon already exists"), MessageWidget::Information); - } else if (!this->isVisible()) { + } else if (!isVisible()) { // Show confirmation message if triggered from Entry tab download button emit messageEditEntry(tr("Custom icon successfully downloaded"), MessageWidget::Positive); } @@ -289,7 +290,7 @@ void EditWidgetIcons::fetchFinished() if (!fallbackEnabled) { emit messageEditEntry( tr("Unable to fetch favicon.") + "\n" - + tr("Hint: You can enable DuckDuckGo as a fallback under Tools>Settings>Security"), + + tr("You can enable the DuckDuckGo website icon service under Tools -> Settings -> Security"), MessageWidget::Error); } else { emit messageEditEntry(tr("Unable to fetch favicon."), MessageWidget::Error); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 0d95f6021..e5f5ea613 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -41,6 +41,10 @@ #include "keys/FileKey.h" #include "keys/PasswordKey.h" +#ifdef Q_OS_MACOS +#include "macutils/MacUtils.h" +#endif + #ifdef WITH_XC_UPDATECHECK #include "gui/MessageBox.h" #include "gui/UpdateCheckDialog.h" @@ -135,6 +139,7 @@ MainWindow::MainWindow() , m_trayIcon(nullptr) , m_appExitCalled(false) , m_appExiting(false) + , m_lastFocusOutTime(0) { g_MainWindow = this; @@ -370,6 +375,9 @@ MainWindow::MainWindow() #ifdef Q_OS_MACOS setUnifiedTitleAndToolBarOnMac(true); + if (macUtils()->isDarkMode()) { + setStyleSheet("QToolButton {color:white;}"); + } #endif #ifdef WITH_XC_UPDATECHECK @@ -396,6 +404,12 @@ MainWindow::MainWindow() connect(m_screenLockListener, SIGNAL(screenLocked()), SLOT(handleScreenLock())); #endif + // Tray Icon setup + connect(Application::instance(), SIGNAL(focusWindowChanged(QWindow*)), SLOT(focusWindowChanged(QWindow*))); + m_trayIconTriggerReason = QSystemTrayIcon::Unknown; + m_trayIconTriggerTimer.setSingleShot(true); + connect(&m_trayIconTriggerTimer, SIGNAL(timeout()), SLOT(processTrayIconTrigger())); + updateTrayIcon(); if (config()->hasAccessError()) { @@ -912,7 +926,7 @@ bool MainWindow::saveLastDatabases() } QStringList openDatabases; - for (int i=0; i < m_ui->tabWidget->count(); ++i) { + for (int i = 0; i < m_ui->tabWidget->count(); ++i) { auto dbWidget = m_ui->tabWidget->databaseWidgetFromIndex(i); openDatabases.append(dbWidget->database()->filePath()); } @@ -1031,10 +1045,38 @@ void MainWindow::applySettingsChanges() updateTrayIcon(); } +void MainWindow::focusWindowChanged(QWindow* focusWindow) +{ + if (focusWindow != windowHandle()) { + m_lastFocusOutTime = Clock::currentSecondsSinceEpoch(); + } +} + void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::MiddleClick) { + if (!m_trayIconTriggerTimer.isActive()) { + m_trayIconTriggerTimer.start(150); + } + // Overcome Qt bug https://bugreports.qt.io/browse/QTBUG-69698 + // Store last issued tray icon activation reason to properly + // capture doubleclick events + m_trayIconTriggerReason = reason; +} + +void MainWindow::processTrayIconTrigger() +{ + if (m_trayIconTriggerReason == QSystemTrayIcon::DoubleClick) { + // Always toggle window on double click toggleWindow(); + } else if (m_trayIconTriggerReason == QSystemTrayIcon::Trigger + || m_trayIconTriggerReason == QSystemTrayIcon::MiddleClick) { + // On single/middle click focus the window if it is not hidden + // and did not have focus less than a second ago, otherwise toggle + if (isHidden() || (Clock::currentSecondsSinceEpoch() - m_lastFocusOutTime) <= 1) { + toggleWindow(); + } else { + bringToFront(); + } } } diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 5a72d6f02..f1e543468 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -85,6 +85,7 @@ private slots: void showAboutDialog(); void showUpdateCheckStartup(); void showUpdateCheckDialog(); + void focusWindowChanged(QWindow* focusWindow); void hasUpdateAvailable(bool hasUpdate, const QString& version, bool isManuallyRequested); void openDonateUrl(); void openBugReportUrl(); @@ -107,6 +108,7 @@ private slots: void showGroupContextMenu(const QPoint& globalPos); void applySettingsChanges(); void trayIconTriggered(QSystemTrayIcon::ActivationReason reason); + void processTrayIconTrigger(); void lockDatabasesAfterInactivity(); void forgetTouchIDAfterInactivity(); void handleScreenLock(); @@ -146,6 +148,9 @@ private: bool m_appExitCalled; bool m_appExiting; + uint m_lastFocusOutTime; + QTimer m_trayIconTriggerTimer; + QSystemTrayIcon::ActivationReason m_trayIconTriggerReason; }; /** diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 063f8da2c..8d0f6f556 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -152,11 +152,6 @@ void EditEntryWidget::setupMain() m_mainUi->expirePresets->setMenu(createPresetsMenu()); connect(m_mainUi->expirePresets->menu(), SIGNAL(triggered(QAction*)), this, SLOT(useExpiryPreset(QAction*))); - QAction* action = new QAction(this); - action->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(action, SIGNAL(triggered()), this, SLOT(commitEntry())); - this->addAction(action); - m_mainUi->passwordGenerator->hide(); m_mainUi->passwordGenerator->reset(); } @@ -285,7 +280,6 @@ void EditEntryWidget::setupEntryUpdate() connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFaviconButtonEnable(QString))); #endif connect(m_mainUi->expireCheck, SIGNAL(stateChanged(int)), this, SLOT(setModified())); - connect(m_mainUi->notesEnabled, SIGNAL(stateChanged(int)), this, SLOT(setModified())); connect(m_mainUi->expireDatePicker, SIGNAL(dateTimeChanged(QDateTime)), this, SLOT(setModified())); connect(m_mainUi->notesEdit, SIGNAL(textChanged()), this, SLOT(setModified())); @@ -1111,8 +1105,9 @@ void EditEntryWidget::updateCurrentAttribute() void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) { - // Block signals to prevent extra calls + // Block signals to prevent modified being set m_advancedUi->protectAttributeButton->blockSignals(true); + m_advancedUi->attributesEdit->blockSignals(true); if (index.isValid()) { QString key = m_attributesModel->keyByIndex(index); @@ -1143,6 +1138,7 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) } m_advancedUi->protectAttributeButton->blockSignals(false); + m_advancedUi->attributesEdit->blockSignals(false); } void EditEntryWidget::protectCurrentAttribute(bool state) diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index fe83a943e..051f23d4b 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -227,6 +227,9 @@ void EditGroupWidget::cancel() tr("Entry has unsaved changes"), MessageBox::Cancel | MessageBox::Save | MessageBox::Discard, MessageBox::Cancel); + if (result == MessageBox::Cancel) { + return; + } if (result == MessageBox::Save) { apply(); setModified(false); diff --git a/src/gui/macutils/AppKit.h b/src/gui/macutils/AppKit.h index cdb822ffc..da81f6913 100644 --- a/src/gui/macutils/AppKit.h +++ b/src/gui/macutils/AppKit.h @@ -19,14 +19,15 @@ #ifndef KEEPASSX_APPKIT_H #define KEEPASSX_APPKIT_H +#include #include -extern "C" { - -class AppKit +class AppKit : public QObject { + Q_OBJECT + public: - AppKit(); + AppKit(QObject* parent = nullptr); ~AppKit(); pid_t lastActiveProcessId(); @@ -37,10 +38,11 @@ public: bool isHidden(pid_t pid); bool isDarkMode(); +signals: + void lockDatabases(); + private: void *self; }; -} // extern "C" - #endif // KEEPASSX_APPKIT_H diff --git a/src/gui/macutils/AppKitImpl.h b/src/gui/macutils/AppKitImpl.h index 3bf2d20ef..ca2506794 100644 --- a/src/gui/macutils/AppKitImpl.h +++ b/src/gui/macutils/AppKitImpl.h @@ -22,6 +22,10 @@ #import @interface AppKitImpl : NSObject +{ + AppKit *m_appkit; +} +- (id) initWithObject:(AppKit *)appkit; @property (strong) NSRunningApplication *lastActiveApplication; @@ -31,5 +35,6 @@ - (bool) hideProcess:(pid_t) pid; - (bool) isHidden:(pid_t) pid; - (bool) isDarkMode; +- (void) userSwitchHandler:(NSNotification*) notification; @end diff --git a/src/gui/macutils/AppKitImpl.mm b/src/gui/macutils/AppKitImpl.mm index cd709df27..4165e0d5e 100644 --- a/src/gui/macutils/AppKitImpl.mm +++ b/src/gui/macutils/AppKitImpl.mm @@ -22,19 +22,22 @@ @implementation AppKitImpl -AppKit::AppKit() +- (id) initWithObject:(AppKit *)appkit { - self = [[AppKitImpl alloc] init]; - [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast(self) + self = [super init]; + if (self) { + m_appkit = appkit; + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast(self) selector:@selector(didDeactivateApplicationObserver:) name:NSWorkspaceDidDeactivateApplicationNotification object:nil]; -} - -AppKit::~AppKit() -{ - [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:static_cast(self)]; - [static_cast(self) dealloc]; + + [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:static_cast(self) + selector:@selector(userSwitchHandler:) + name:NSWorkspaceSessionDidResignActiveNotification + object:nil]; + } + return self; } // @@ -104,10 +107,34 @@ AppKit::~AppKit() && NSOrderedSame == [style caseInsensitiveCompare:@"dark"] ); } +// +// Notification for user switch +// +- (void) userSwitchHandler:(NSNotification*) notification +{ + if ([[notification name] isEqualToString:NSWorkspaceSessionDidResignActiveNotification] && m_appkit) + { + emit m_appkit->lockDatabases(); + } +} + +@end + // // ------------------------- C++ Trampolines ------------------------- // +AppKit::AppKit(QObject* parent) : QObject(parent) +{ + self = [[AppKitImpl alloc] initWithObject:this]; +} + +AppKit::~AppKit() +{ + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:static_cast(self)]; + [static_cast(self) dealloc]; +} + pid_t AppKit::lastActiveProcessId() { return [static_cast(self) lastActiveApplication].processIdentifier; @@ -142,5 +169,3 @@ bool AppKit::isDarkMode() { return [static_cast(self) isDarkMode]; } - -@end diff --git a/src/gui/macutils/MacUtils.cpp b/src/gui/macutils/MacUtils.cpp index c362fe1bd..654923c31 100644 --- a/src/gui/macutils/MacUtils.cpp +++ b/src/gui/macutils/MacUtils.cpp @@ -24,7 +24,7 @@ MacUtils* MacUtils::m_instance = nullptr; MacUtils::MacUtils(QObject* parent) : QObject(parent) , m_appkit(new AppKit()) { - + connect(m_appkit.data(), SIGNAL(lockDatabases()), SIGNAL(lockDatabases())); } MacUtils::~MacUtils() diff --git a/src/gui/macutils/MacUtils.h b/src/gui/macutils/MacUtils.h index 39a06bd84..49644795e 100644 --- a/src/gui/macutils/MacUtils.h +++ b/src/gui/macutils/MacUtils.h @@ -39,14 +39,16 @@ public: bool isHidden(); bool isDarkMode(); +signals: + void lockDatabases(); + private: explicit MacUtils(QObject* parent = nullptr); ~MacUtils(); private: - std::unique_ptr m_appkit; + QScopedPointer m_appkit; static MacUtils* m_instance; - void* self; Q_DISABLE_COPY(MacUtils) }; diff --git a/src/keeshare/CMakeLists.txt b/src/keeshare/CMakeLists.txt index 14aa17b99..d791d3be6 100644 --- a/src/keeshare/CMakeLists.txt +++ b/src/keeshare/CMakeLists.txt @@ -1,4 +1,6 @@ if(WITH_XC_KEESHARE) + set(WITH_XC_KEESHARE_INSECURE ON PARENT_SCOPE) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) set(keeshare_SOURCES @@ -15,9 +17,19 @@ if(WITH_XC_KEESHARE) ) add_library(keeshare STATIC ${keeshare_SOURCES}) - if(WITH_XC_KEESHARE_SECURE) - target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${QUAZIP_LIBRARIES} ${crypto_ssh_LIB}) + target_link_libraries(keeshare PUBLIC Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB}) + + # Try to find libquazip5, if found, enable secure sharing + find_package(QuaZip) + if(QUAZIP_FOUND) + set(WITH_XC_KEESHARE_SECURE ON PARENT_SCOPE) + target_include_directories(keeshare SYSTEM PRIVATE ${QUAZIP_INCLUDE_DIR}) + target_link_libraries(keeshare PRIVATE ${QUAZIP_LIBRARIES}) else() - target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB}) + set(WITH_XC_KEESHARE_SECURE OFF PARENT_SCOPE) + message(STATUS "KeeShare: Secure container support is DISABLED; quazip library not found") endif() -endif() +else(WITH_XC_KEESHARE) + set(WITH_XC_KEESHARE_INSECURE OFF PARENT_SCOPE) + set(WITH_XC_KEESHARE_SECURE OFF PARENT_SCOPE) +endif(WITH_XC_KEESHARE) diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 33f5ed1f6..644f1c157 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -46,8 +46,8 @@ #include #if defined(WITH_XC_KEESHARE_SECURE) -#include -#include +#include +#include #endif namespace diff --git a/src/keys/FileKey.cpp b/src/keys/FileKey.cpp index 9d1e8f50f..da25ef4ae 100644 --- a/src/keys/FileKey.cpp +++ b/src/keys/FileKey.cpp @@ -18,19 +18,35 @@ #include "FileKey.h" -#include - #include "core/Tools.h" #include "crypto/CryptoHash.h" #include "crypto/Random.h" +#include + +#include +#include +#include +#include + QUuid FileKey::UUID("a584cbc4-c9b4-437e-81bb-362ca9709273"); +constexpr int FileKey::SHA256_SIZE; + FileKey::FileKey() : Key(UUID) + , m_key(static_cast(gcry_malloc_secure(SHA256_SIZE))) { } +FileKey::~FileKey() +{ + if (m_key) { + gcry_free(m_key); + m_key = nullptr; + } +} + /** * Read key file from device while trying to detect its file format. * @@ -148,7 +164,10 @@ bool FileKey::load(const QString& fileName, QString* errorMsg) */ QByteArray FileKey::rawKey() const { - return m_key; + if (!m_key) { + return {}; + } + return QByteArray::fromRawData(m_key, SHA256_SIZE); } /** @@ -223,12 +242,15 @@ bool FileKey::loadXml(QIODevice* device) } } + bool ok = false; if (!xmlReader.error() && correctMeta && !data.isEmpty()) { - m_key = data; - return true; + std::memcpy(m_key, data.data(), std::min(SHA256_SIZE, data.size())); + ok = true; } - return false; + sodium_memzero(data.data(), static_cast(data.capacity())); + + return ok; } /** @@ -293,7 +315,8 @@ bool FileKey::loadBinary(QIODevice* device) if (!Tools::readAllFromDevice(device, data) || data.size() != 32) { return false; } else { - m_key = data; + std::memcpy(m_key, data.data(), std::min(SHA256_SIZE, data.size())); + sodium_memzero(data.data(), static_cast(data.capacity())); return true; } } @@ -321,12 +344,15 @@ bool FileKey::loadHex(QIODevice* device) } QByteArray key = QByteArray::fromHex(data); + sodium_memzero(data.data(), static_cast(data.capacity())); if (key.size() != 32) { return false; } - m_key = key; + std::memcpy(m_key, key.data(), std::min(SHA256_SIZE, key.size())); + sodium_memzero(key.data(), static_cast(key.capacity())); + return true; } @@ -348,7 +374,9 @@ bool FileKey::loadHashed(QIODevice* device) cryptoHash.addData(buffer); } while (!buffer.isEmpty()); - m_key = cryptoHash.result(); + auto result = cryptoHash.result(); + std::memcpy(m_key, result.data(), std::min(SHA256_SIZE, result.size())); + sodium_memzero(result.data(), static_cast(result.capacity())); return true; } diff --git a/src/keys/FileKey.h b/src/keys/FileKey.h index d7486467b..290a04af0 100644 --- a/src/keys/FileKey.h +++ b/src/keys/FileKey.h @@ -40,6 +40,7 @@ public: }; FileKey(); + ~FileKey() override; bool load(QIODevice* device); bool load(const QString& fileName, QString* errorMsg = nullptr); QByteArray rawKey() const override; @@ -48,6 +49,8 @@ public: static bool create(const QString& fileName, QString* errorMsg = nullptr, int size = 128); private: + static constexpr int SHA256_SIZE = 32; + bool loadXml(QIODevice* device); bool loadXmlMeta(QXmlStreamReader& xmlReader); QByteArray loadXmlKey(QXmlStreamReader& xmlReader); @@ -55,7 +58,7 @@ private: bool loadHex(QIODevice* device); bool loadHashed(QIODevice* device); - QByteArray m_key; + char* m_key = nullptr; Type m_type = None; }; diff --git a/src/keys/PasswordKey.cpp b/src/keys/PasswordKey.cpp index 35ecb9989..2d0416af8 100644 --- a/src/keys/PasswordKey.cpp +++ b/src/keys/PasswordKey.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 Felix Geyer + * Copyright (C) 2019 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 @@ -16,35 +16,51 @@ */ #include "PasswordKey.h" +#include "core/Tools.h" #include "crypto/CryptoHash.h" +#include +#include +#include QUuid PasswordKey::UUID("77e90411-303a-43f2-b773-853b05635ead"); +constexpr int PasswordKey::SHA256_SIZE; + PasswordKey::PasswordKey() : Key(UUID) + , m_key(static_cast(gcry_malloc_secure(SHA256_SIZE))) { } PasswordKey::PasswordKey(const QString& password) : Key(UUID) + , m_key(static_cast(gcry_malloc_secure(SHA256_SIZE))) { setPassword(password); } +PasswordKey::~PasswordKey() +{ + if (m_key) { + gcry_free(m_key); + m_key = nullptr; + } +} + QSharedPointer PasswordKey::fromRawKey(const QByteArray& rawKey) { auto result = QSharedPointer::create(); - result->m_key = rawKey; + std::memcpy(result->m_key, rawKey.data(), std::min(SHA256_SIZE, rawKey.size())); return result; } QByteArray PasswordKey::rawKey() const { - return m_key; + return QByteArray::fromRawData(m_key, SHA256_SIZE); } void PasswordKey::setPassword(const QString& password) { - m_key = CryptoHash::hash(password.toUtf8(), CryptoHash::Sha256); + std::memcpy(m_key, CryptoHash::hash(password.toUtf8(), CryptoHash::Sha256).data(), SHA256_SIZE); } diff --git a/src/keys/PasswordKey.h b/src/keys/PasswordKey.h index 68ab79895..4408cabcf 100644 --- a/src/keys/PasswordKey.h +++ b/src/keys/PasswordKey.h @@ -30,13 +30,16 @@ public: PasswordKey(); explicit PasswordKey(const QString& password); + ~PasswordKey() override; QByteArray rawKey() const override; void setPassword(const QString& password); static QSharedPointer fromRawKey(const QByteArray& rawKey); private: - QByteArray m_key; + static constexpr int SHA256_SIZE = 32; + + char* m_key = nullptr; }; #endif // KEEPASSX_PASSWORDKEY_H diff --git a/src/keys/YkChallengeResponseKey.cpp b/src/keys/YkChallengeResponseKey.cpp index f9cbe3174..759d8d1bc 100644 --- a/src/keys/YkChallengeResponseKey.cpp +++ b/src/keys/YkChallengeResponseKey.cpp @@ -1,6 +1,6 @@ /* + * Copyright (C) 2019 KeePassXC Team * Copyright (C) 2014 Kyle Manna - * Copyright (C) 2017 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 @@ -32,6 +32,10 @@ #include #include +#include +#include +#include + QUuid YkChallengeResponseKey::UUID("e092495c-e77d-498b-84a1-05ae0d955508"); YkChallengeResponseKey::YkChallengeResponseKey(int slot, bool blocking) @@ -45,9 +49,18 @@ YkChallengeResponseKey::YkChallengeResponseKey(int slot, bool blocking) } } +YkChallengeResponseKey::~YkChallengeResponseKey() +{ + if (m_key) { + gcry_free(m_key); + m_keySize = 0; + m_key = nullptr; + } +} + QByteArray YkChallengeResponseKey::rawKey() const { - return m_key; + return QByteArray::fromRawData(m_key, static_cast(m_keySize)); } /** @@ -67,14 +80,22 @@ bool YkChallengeResponseKey::challenge(const QByteArray& challenge, unsigned int emit userInteractionRequired(); } + QByteArray key; auto result = AsyncTask::runAndWaitForFuture( - [this, challenge]() { return YubiKey::instance()->challenge(m_slot, true, challenge, m_key); }); + [this, challenge, &key]() { return YubiKey::instance()->challenge(m_slot, true, challenge, key); }); if (m_blocking) { emit userConfirmed(); } if (result == YubiKey::SUCCESS) { + if (m_key) { + gcry_free(m_key); + } + m_keySize = static_cast(key.size()); + m_key = static_cast(gcry_malloc_secure(m_keySize)); + std::memcpy(m_key, key.data(), m_keySize); + sodium_memzero(key.data(), static_cast(key.capacity())); return true; } } while (retries > 0); diff --git a/src/keys/YkChallengeResponseKey.h b/src/keys/YkChallengeResponseKey.h index b8467e7a6..5f7c40e72 100644 --- a/src/keys/YkChallengeResponseKey.h +++ b/src/keys/YkChallengeResponseKey.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2019 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 @@ -32,6 +32,7 @@ public: static QUuid UUID; explicit YkChallengeResponseKey(int slot = -1, bool blocking = false); + ~YkChallengeResponseKey() override; QByteArray rawKey() const override; bool challenge(const QByteArray& challenge) override; @@ -52,7 +53,8 @@ signals: void userConfirmed(); private: - QByteArray m_key; + char* m_key = nullptr; + std::size_t m_keySize = 0; int m_slot; bool m_blocking; }; diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index ff645dadb..bdbfa3b74 100755 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -18,12 +18,13 @@ if(WITH_XC_BROWSER) include_directories(${BROWSER_SOURCE_DIR}) set(proxy_SOURCES + ../core/Alloc.cpp keepassxc-proxy.cpp ${BROWSER_SOURCE_DIR}/NativeMessagingBase.cpp NativeMessagingHost.cpp) add_library(proxy STATIC ${proxy_SOURCES}) - target_link_libraries(proxy Qt5::Core Qt5::Network) + target_link_libraries(proxy Qt5::Core Qt5::Network ${sodium_LIBRARY_RELEASE}) add_executable(keepassxc-proxy keepassxc-proxy.cpp) target_link_libraries(keepassxc-proxy proxy) diff --git a/src/touchid/TouchID.mm b/src/touchid/TouchID.mm index 9ef72189b..7df5ad556 100644 --- a/src/touchid/TouchID.mm +++ b/src/touchid/TouchID.mm @@ -15,6 +15,7 @@ inline void debug(const char* message, ...) { + Q_UNUSED(message); // qWarning(...); } @@ -258,6 +259,7 @@ bool TouchID::authenticate(const QString& message) const NSString* authMessage = msg.toNSString(); // autoreleased [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:authMessage reply:^(BOOL success, NSError* error) { + Q_UNUSED(error); result = success ? kTouchIDResultAllowed : kTouchIDResultFailed; CFRunLoopWakeUp(CFRunLoopGetCurrent()); }]; diff --git a/tests/TestKdbx4.cpp b/tests/TestKdbx4.cpp index fe4679da5..88352d825 100644 --- a/tests/TestKdbx4.cpp +++ b/tests/TestKdbx4.cpp @@ -186,8 +186,10 @@ void TestKdbx4::testFormat400Upgrade() QCOMPARE(reader.version(), expectedVersion); QCOMPARE(targetDb->cipher(), cipherUuid); - QCOMPARE(*targetDb->metadata()->customData(), *sourceDb->metadata()->customData()); - QCOMPARE(*targetDb->rootGroup()->customData(), *sourceDb->rootGroup()->customData()); + QCOMPARE(targetDb->metadata()->customData()->value("CustomPublicData"), + sourceDb->metadata()->customData()->value("CustomPublicData")); + QCOMPARE(targetDb->rootGroup()->customData()->value("CustomGroupData"), + sourceDb->rootGroup()->customData()->value("CustomGroupData")); } // clang-format off @@ -346,20 +348,22 @@ void TestKdbx4::testCustomData() const QString customDataKey2 = "CD2"; const QString customData1 = "abcäöü"; const QString customData2 = "Hello World"; - const int dataSize = customDataKey1.toUtf8().size() + customDataKey1.toUtf8().size() + customData1.toUtf8().size() - + customData2.toUtf8().size(); // test custom database data db.metadata()->customData()->set(customDataKey1, customData1); db.metadata()->customData()->set(customDataKey2, customData2); - QCOMPARE(db.metadata()->customData()->size(), 2); + auto lastModified = db.metadata()->customData()->value(CustomData::LastModified); + const int dataSize = customDataKey1.toUtf8().size() + customDataKey1.toUtf8().size() + customData1.toUtf8().size() + + customData2.toUtf8().size() + lastModified.toUtf8().size() + + CustomData::LastModified.toUtf8().size(); + QCOMPARE(db.metadata()->customData()->size(), 3); QCOMPARE(db.metadata()->customData()->dataSize(), dataSize); // test custom root group data Group* root = db.rootGroup(); root->customData()->set(customDataKey1, customData1); root->customData()->set(customDataKey2, customData2); - QCOMPARE(root->customData()->size(), 2); + QCOMPARE(root->customData()->size(), 3); QCOMPARE(root->customData()->dataSize(), dataSize); // test copied custom group data @@ -378,9 +382,9 @@ void TestKdbx4::testCustomData() // test custom data deletion entry->customData()->set("additional item", "foobar"); - QCOMPARE(entry->customData()->size(), 3); + QCOMPARE(entry->customData()->size(), 4); entry->customData()->remove("additional item"); - QCOMPARE(entry->customData()->size(), 2); + QCOMPARE(entry->customData()->size(), 3); QCOMPARE(entry->customData()->dataSize(), dataSize); // test custom data on cloned groups diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp index 03eae32ef..4d9aef211 100644 --- a/tests/TestMerge.cpp +++ b/tests/TestMerge.cpp @@ -1164,6 +1164,65 @@ void TestMerge::testMetadata() // will be used - exception is the target has no recycle bin activated } +void TestMerge::testCustomdata() +{ + QScopedPointer dbDestination(new Database()); + QScopedPointer dbSource(createTestDatabase()); + QScopedPointer dbDestination2(new Database()); + QScopedPointer dbSource2(createTestDatabase()); + + m_clock->advanceSecond(1); + + dbDestination->metadata()->customData()->set("toBeDeleted", "value"); + dbDestination->metadata()->customData()->set("key3", "oldValue"); + + dbSource2->metadata()->customData()->set("key1", "value1"); + dbSource2->metadata()->customData()->set("key2", "value2"); + dbSource2->metadata()->customData()->set("key3", "newValue"); + dbSource2->metadata()->customData()->set("Browser", "n'8=3W@L^6d->d.]St_>]"); + + m_clock->advanceSecond(1); + + dbSource->metadata()->customData()->set("key1", "value1"); + dbSource->metadata()->customData()->set("key2", "value2"); + dbSource->metadata()->customData()->set("key3", "newValue"); + dbSource->metadata()->customData()->set("Browser", "n'8=3W@L^6d->d.]St_>]"); + + dbDestination2->metadata()->customData()->set("notToBeDeleted", "value"); + dbDestination2->metadata()->customData()->set("key3", "oldValue"); + + // Sanity check. + QVERIFY(!dbSource->metadata()->customData()->isEmpty()); + QVERIFY(!dbSource2->metadata()->customData()->isEmpty()); + + m_clock->advanceSecond(1); + + Merger merger(dbSource.data(), dbDestination.data()); + merger.merge(); + + Merger merger2(dbSource2.data(), dbDestination2.data()); + merger2.merge(); + + // Source is newer, data should be merged + QVERIFY(!dbDestination->metadata()->customData()->isEmpty()); + QVERIFY(dbDestination->metadata()->customData()->contains("key1")); + QVERIFY(dbDestination->metadata()->customData()->contains("key2")); + QVERIFY(dbDestination->metadata()->customData()->contains("Browser")); + QVERIFY(!dbDestination->metadata()->customData()->contains("toBeDeleted")); + QCOMPARE(dbDestination->metadata()->customData()->value("key1"), QString("value1")); + QCOMPARE(dbDestination->metadata()->customData()->value("key2"), QString("value2")); + QCOMPARE(dbDestination->metadata()->customData()->value("Browser"), QString("n'8=3W@L^6d->d.]St_>]")); + QCOMPARE(dbDestination->metadata()->customData()->value("key3"), QString("newValue")); // Old value should be replaced + + // Target is newer, no data is merged + QVERIFY(!dbDestination2->metadata()->customData()->isEmpty()); + QVERIFY(!dbDestination2->metadata()->customData()->contains("key1")); + QVERIFY(!dbDestination2->metadata()->customData()->contains("key2")); + QVERIFY(!dbDestination2->metadata()->customData()->contains("Browser")); + QVERIFY(dbDestination2->metadata()->customData()->contains("notToBeDeleted")); + QCOMPARE(dbDestination2->metadata()->customData()->value("key3"), QString("oldValue")); // Old value should not be replaced +} + void TestMerge::testDeletedEntry() { QScopedPointer dbDestination(createTestDatabase()); diff --git a/tests/TestMerge.h b/tests/TestMerge.h index 357f85262..15f67ca79 100644 --- a/tests/TestMerge.h +++ b/tests/TestMerge.h @@ -59,6 +59,7 @@ private slots: void testMergeCustomIcons(); void testMergeDuplicateCustomIcons(); void testMetadata(); + void testCustomdata(); void testDeletedEntry(); void testDeletedGroup(); void testDeletedRevertedEntry(); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 63e36fbaa..0db2a5dfb 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -1286,11 +1286,11 @@ void TestGui::testTrayRestoreHide() QSKIP("QSystemTrayIcon::isSystemTrayAvailable() = false, skipping tray restore/hide test..."); } + m_mainWindow->hideWindow(); + QVERIFY(!m_mainWindow->isVisible()); + auto* trayIcon = m_mainWindow->findChild(); - QVERIFY(m_mainWindow->isVisible()); - - trayIcon->activated(QSystemTrayIcon::Trigger); - QTRY_VERIFY(!m_mainWindow->isVisible()); + QVERIFY(trayIcon); trayIcon->activated(QSystemTrayIcon::Trigger); QTRY_VERIFY(m_mainWindow->isVisible()); @@ -1298,8 +1298,17 @@ void TestGui::testTrayRestoreHide() trayIcon->activated(QSystemTrayIcon::Trigger); QTRY_VERIFY(!m_mainWindow->isVisible()); - trayIcon->activated(QSystemTrayIcon::Trigger); + trayIcon->activated(QSystemTrayIcon::MiddleClick); QTRY_VERIFY(m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::MiddleClick); + QTRY_VERIFY(!m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::DoubleClick); + QTRY_VERIFY(m_mainWindow->isVisible()); + + trayIcon->activated(QSystemTrayIcon::DoubleClick); + QTRY_VERIFY(!m_mainWindow->isVisible()); } int TestGui::addCannedEntries()