From 06518c57361f366d7d4b00fc9cb441dae57174c5 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Tue, 12 Dec 2017 10:15:23 +0200 Subject: [PATCH] keepassxc-browser --- CMakeLists.txt | 4 + Dockerfile | 6 +- INSTALL.md | 12 +- README.md | 45 +- cmake/Findsodium.cmake | 267 ++++++++ snapcraft.yaml | 3 +- src/CMakeLists.txt | 31 + src/browser/BrowserAccessControlDialog.cpp | 59 ++ src/browser/BrowserAccessControlDialog.h | 48 ++ src/browser/BrowserAccessControlDialog.ui | 69 ++ src/browser/BrowserAction.cpp | 546 +++++++++++++++ src/browser/BrowserAction.h | 97 +++ src/browser/BrowserClients.cpp | 77 +++ src/browser/BrowserClients.h | 56 ++ src/browser/BrowserEntryConfig.cpp | 109 +++ src/browser/BrowserEntryConfig.h | 60 ++ src/browser/BrowserOptionDialog.cpp | 104 +++ src/browser/BrowserOptionDialog.h | 50 ++ src/browser/BrowserOptionDialog.ui | 327 +++++++++ src/browser/BrowserService.cpp | 734 +++++++++++++++++++++ src/browser/BrowserService.h | 82 +++ src/browser/BrowserSettings.cpp | 377 +++++++++++ src/browser/BrowserSettings.h | 105 +++ src/browser/CMakeLists.txt | 37 ++ src/browser/HostInstaller.cpp | 230 +++++++ src/browser/HostInstaller.h | 63 ++ src/browser/NativeMessagingBase.cpp | 141 ++++ src/browser/NativeMessagingBase.h | 61 ++ src/browser/NativeMessagingHost.cpp | 200 ++++++ src/browser/NativeMessagingHost.h | 67 ++ src/browser/Variant.cpp | 37 ++ src/browser/Variant.h | 25 + src/config-keepassx.h.cmake | 1 + src/core/PassphraseGenerator.cpp | 13 +- src/core/PassphraseGenerator.h | 3 +- src/gui/AboutDialog.cpp | 3 + src/gui/MainWindow.cpp | 57 ++ src/gui/PasswordGeneratorWidget.cpp | 5 +- src/main.cpp | 15 +- src/proxy/CMakeLists.txt | 34 + src/proxy/NativeMessagingHost.cpp | 95 +++ src/proxy/NativeMessagingHost.h | 43 ++ src/proxy/keepassxc-proxy.cpp | 66 ++ 43 files changed, 4422 insertions(+), 42 deletions(-) create mode 100644 cmake/Findsodium.cmake create mode 100755 src/browser/BrowserAccessControlDialog.cpp create mode 100755 src/browser/BrowserAccessControlDialog.h create mode 100755 src/browser/BrowserAccessControlDialog.ui create mode 100755 src/browser/BrowserAction.cpp create mode 100755 src/browser/BrowserAction.h create mode 100755 src/browser/BrowserClients.cpp create mode 100755 src/browser/BrowserClients.h create mode 100644 src/browser/BrowserEntryConfig.cpp create mode 100644 src/browser/BrowserEntryConfig.h create mode 100755 src/browser/BrowserOptionDialog.cpp create mode 100755 src/browser/BrowserOptionDialog.h create mode 100755 src/browser/BrowserOptionDialog.ui create mode 100644 src/browser/BrowserService.cpp create mode 100644 src/browser/BrowserService.h create mode 100755 src/browser/BrowserSettings.cpp create mode 100755 src/browser/BrowserSettings.h create mode 100755 src/browser/CMakeLists.txt create mode 100644 src/browser/HostInstaller.cpp create mode 100644 src/browser/HostInstaller.h create mode 100644 src/browser/NativeMessagingBase.cpp create mode 100644 src/browser/NativeMessagingBase.h create mode 100755 src/browser/NativeMessagingHost.cpp create mode 100755 src/browser/NativeMessagingHost.h create mode 100644 src/browser/Variant.cpp create mode 100644 src/browser/Variant.h create mode 100755 src/proxy/CMakeLists.txt create mode 100755 src/proxy/NativeMessagingHost.cpp create mode 100755 src/proxy/NativeMessagingHost.h create mode 100644 src/proxy/keepassxc-proxy.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 77f4d8624..1d5c198ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ option(WITH_XC_AUTOTYPE "Include Auto-Type." ON) option(WITH_XC_HTTP "Include KeePassHTTP and Custom Icon Downloads." OFF) option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) +option(WITH_XC_BROWSER "Include browser extension support." OFF) # Process ui files automatically from source files set(CMAKE_AUTOUIC ON) @@ -200,11 +201,13 @@ endif() if(MINGW) set(CLI_INSTALL_DIR ".") + set(PROXY_INSTALL_DIR ".") set(BIN_INSTALL_DIR ".") set(PLUGIN_INSTALL_DIR ".") set(DATA_INSTALL_DIR "share") elseif(APPLE AND WITH_APP_BUNDLE) set(CLI_INSTALL_DIR "/usr/local/bin") + set(PROXY_INSTALL_DIR "/usr/local/bin") set(BIN_INSTALL_DIR ".") set(PLUGIN_INSTALL_DIR "${PROGNAME}.app/Contents/PlugIns") set(DATA_INSTALL_DIR "${PROGNAME}.app/Contents/Resources") @@ -212,6 +215,7 @@ else() include(GNUInstallDirs) set(CLI_INSTALL_DIR "${CMAKE_INSTALL_BINDIR}") + set(PROXY_INSTALL_DIR "${CMAKE_INSTALL_BINDIR}") set(BIN_INSTALL_DIR "${CMAKE_INSTALL_BINDIR}") set(PLUGIN_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/keepassxc") set(DATA_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/keepassxc") diff --git a/Dockerfile b/Dockerfile index 66ecb4c04..c8c637c07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,8 @@ RUN set -x \ RUN set -x \ && add-apt-repository ppa:beineri/opt-qt${QT5_PPA_VERSION}-trusty \ - && add-apt-repository ppa:phoerious/keepassxc + && add-apt-repository ppa:phoerious/keepassxc \ + && LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php RUN set -x \ && apt-get update -y \ @@ -46,7 +47,8 @@ RUN set -x \ libxtst-dev \ mesa-common-dev \ libyubikey-dev \ - libykpers-1-dev + libykpers-1-dev \ + libsodium-dev ENV CMAKE_PREFIX_PATH=/opt/qt${QT5_VERSION}/lib/cmake ENV LD_LIBRARY_PATH=/opt/qt${QT5_VERSION}/lib diff --git a/INSTALL.md b/INSTALL.md index 2d3f7cb2b..0bfa86b2c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ You can visit the online version of this document at the following link: https://github.com/keepassxreboot/keepassx/wiki/Install-Instruction-from-Source -The [KeePassXC QuickStart](./docs/QUICKSTART.md) gets you started using KeePassXC on your +The [KeePassXC QuickStart](./docs/QUICKSTART.md) gets you started using KeePassXC on your Windows, Mac, or Linux computer using the pre-built binaries. Build Dependencies @@ -25,6 +25,7 @@ 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) Prepare the Building Environment @@ -40,7 +41,7 @@ Build Steps To compile from source, open a **Terminal (on Linux/MacOS)** or a **MSYS2-MinGW shell (on Windows)**
**Note:** on Windows make sure you are using a **MINGW shell** by checking the label before the current path -First, download the KeePassXC [source tarball](https://keepassxc.org/download#source) +First, download the KeePassXC [source tarball](https://keepassxc.org/download#source) or check out the latest version from our [Git repository](https://github.com/keepassxreboot/keepassxc). To clone the project from Git, `cd` to a suitable location and run @@ -66,10 +67,10 @@ cd build cmake -DWITH_TESTS=OFF ...and other options - see below... make ``` -These steps place the compiled KeePassXC binary inside the `./build/src/` directory. +These steps place the compiled KeePassXC binary inside the `./build/src/` directory. (Note the cmake notes/options below.) -**Cmake Notes:** +**Cmake Notes:** * Common cmake parameters @@ -86,7 +87,8 @@ 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_HTTP=[ON|OFF] Enable/Disable KeePassHTTP and custom icon downloads (default: OFF) -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_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) diff --git a/README.md b/README.md index 685ebee0b..5c2373259 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -# KeePassXC +# KeePassXC [![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(id:KeepassXC_TeamCityCi\)/statusIcon?guest=1)](https://ci.keepassxc.org/viewType.html?buildTypeId=KeepassXC_TeamCityCi&guest=1) [![Coverage Status](https://coveralls.io/repos/github/keepassxreboot/keepassxc/badge.svg)](https://coveralls.io/github/keepassxreboot/keepassxc) ## About KeePassXC [KeePassXC](https://keepassxc.org) is a cross-platform community fork of -[KeePassX](https://www.keepassx.org/). -Our goal is to extend and improve it with new features and bugfixes -to provide a feature-rich, fully cross-platform and modern +[KeePassX](https://www.keepassx.org/). +Our goal is to extend and improve it with new features and bugfixes +to provide a feature-rich, fully cross-platform and modern open-source password manager. ## Installation -The [KeePassXC QuickStart](./docs/QUICKSTART.md) gets you started using -KeePassXC on your Windows, Mac, or Linux computer using pre-compiled binaries -from the [downloads page](https://keepassxc.org/download). - -Additionally, individual Linux distributions may ship their own versions, +The [KeePassXC QuickStart](./docs/QUICKSTART.md) gets you started using +KeePassXC on your Windows, Mac, or Linux computer using pre-compiled binaries +from the [downloads page](https://keepassxc.org/download). + +Additionally, individual Linux distributions may ship their own versions, so please check out your distribution's package list to see if KeePassXC is available. ## Additional features compared to KeePassX @@ -29,24 +29,25 @@ so please check out your distribution's package list to see if KeePassXC is avai - Using website favicons as entry icons - Merging of databases - Automatic reload when the database changed on disk -- Browser integration with KeePassHTTP-Connector for -[Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepasshttp-connector/) and -[Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb), and -[passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. [[See note about KeePassHTTP]](#note-about-keepasshttp) +- Browser integration with KeePassHTTP-Connector for +[Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepasshttp-connector/) and +[Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb), and +[passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. [[See note about KeePassHTTP]](#Note_about_KeePassHTTP) +- Browser integration with keepassxc-browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/iopaggbpplllidnfmcghoonnokmjoicf) - Many bug fixes For a full list of features and changes, read the [CHANGELOG](CHANGELOG) document. ## Building KeePassXC -Detailed instructions are available in the [Build and Install](./INSTALL.md) +Detailed instructions are available in the [Build and Install](./INSTALL.md) page or on the [Wiki page](https://github.com/keepassxreboot/keepassxc/wiki/Building-KeePassXC). ## Contributing -We are always looking for suggestions how to improve our application. -If you find any bugs or have an idea for a new feature, please let us know by -opening a report in our [issue tracker](https://github.com/keepassxreboot/keepassxc/issues) +We are always looking for suggestions how to improve our application. +If you find any bugs or have an idea for a new feature, please let us know by +opening a report in our [issue tracker](https://github.com/keepassxreboot/keepassxc/issues) on GitHub or join us on IRC on freenode channels #keepassxc or #keepassxc-dev. You can of course also directly contribute your own code. We are happy to accept your pull requests. @@ -57,11 +58,11 @@ Please read the [CONTRIBUTING document](.github/CONTRIBUTING.md) for further inf The KeePassHTTP protocol is not a highly secure protocol. It has a certain flaw which could allow an attacker to decrypt your passwords should they manage to impersonate the web browser extension from a remote address. - -(See [here](https://github.com/pfn/keepasshttp/issues/258) and [here](https://github.com/keepassxreboot/keepassxc/issues/147)). +(See [here](https://github.com/pfn/keepasshttp/issues/258) and [here](https://github.com/keepassxreboot/keepassxc/issues/147)). -To minimize the risk, KeePassXC strictly limits communication between itself -and the browser plugin to your local computer (localhost). -This makes your passwords quite safe, +To minimize the risk, KeePassXC strictly limits communication between itself +and the browser plugin to your local computer (localhost). +This makes your passwords quite safe, but as with all open source software, use it at your own risk! diff --git a/cmake/Findsodium.cmake b/cmake/Findsodium.cmake new file mode 100644 index 000000000..ad7031e5a --- /dev/null +++ b/cmake/Findsodium.cmake @@ -0,0 +1,267 @@ +# Written in 2016 by Henrik Steffen Gaßmann +# +# To the extent possible under law, the author(s) have dedicated all +# copyright and related and neighboring rights to this software to the +# public domain worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication +# along with this software. If not, see +# +# http://creativecommons.org/publicdomain/zero/1.0/ +# +######################################################################## +# Tries to find the local libsodium installation. +# +# On Windows the sodium_DIR environment variable is used as a default +# hint which can be overridden by setting the corresponding cmake variable. +# +# Once done the following variables will be defined: +# +# sodium_FOUND +# sodium_INCLUDE_DIR +# sodium_LIBRARY_DEBUG +# sodium_LIBRARY_RELEASE +# +# +# Furthermore an imported "sodium" target is created. +# + +if (CMAKE_C_COMPILER_ID STREQUAL "GNU" + OR CMAKE_C_COMPILER_ID STREQUAL "Clang") + set(_GCC_COMPATIBLE 1) +endif() + +# static library option +option(sodium_USE_STATIC_LIBS "enable to statically link against sodium") +if(NOT (sodium_USE_STATIC_LIBS EQUAL sodium_USE_STATIC_LIBS_LAST)) + unset(sodium_LIBRARY CACHE) + unset(sodium_LIBRARY_DEBUG CACHE) + unset(sodium_LIBRARY_RELEASE CACHE) + unset(sodium_DLL_DEBUG CACHE) + unset(sodium_DLL_RELEASE CACHE) + set(sodium_USE_STATIC_LIBS_LAST ${sodium_USE_STATIC_LIBS} CACHE INTERNAL "internal change tracking variable") +endif() + + +######################################################################## +# UNIX +if (UNIX) + # import pkg-config + find_package(PkgConfig QUIET) + if (PKG_CONFIG_FOUND) + pkg_check_modules(sodium_PKG QUIET libsodium) + endif() + + if(sodium_USE_STATIC_LIBS) + set(XPREFIX sodium_PKG_STATIC) + else() + set(XPREFIX sodium_PKG) + endif() + + find_path(sodium_INCLUDE_DIR sodium.h + HINTS ${${XPREFIX}_INCLUDE_DIRS} + ) + find_library(sodium_LIBRARY_DEBUG NAMES ${${XPREFIX}_LIBRARIES} sodium + HINTS ${${XPREFIX}_LIBRARY_DIRS} + ) + find_library(sodium_LIBRARY_RELEASE NAMES ${${XPREFIX}_LIBRARIES} sodium + HINTS ${${XPREFIX}_LIBRARY_DIRS} + ) + + +######################################################################## +# Windows +elseif (WIN32) + set(sodium_DIR "$ENV{sodium_DIR}" CACHE FILEPATH "sodium install directory") + mark_as_advanced(sodium_DIR) + + find_path(sodium_INCLUDE_DIR sodium.h + HINTS ${sodium_DIR} + PATH_SUFFIXES include + ) + + if (MSVC) + # detect target architecture + file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/arch.c" [=[ + #if defined _M_IX86 + #error ARCH_VALUE x86_32 + #elif defined _M_X64 + #error ARCH_VALUE x86_64 + #endif + #error ARCH_VALUE unknown + ]=]) + try_compile(_UNUSED_VAR "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/arch.c" + OUTPUT_VARIABLE _COMPILATION_LOG + ) + string(REGEX REPLACE ".*ARCH_VALUE ([a-zA-Z0-9_]+).*" "\\1" _TARGET_ARCH "${_COMPILATION_LOG}") + + # construct library path + if (_TARGET_ARCH STREQUAL "x86_32") + string(APPEND _PLATFORM_PATH "Win32") + elseif(_TARGET_ARCH STREQUAL "x86_64") + string(APPEND _PLATFORM_PATH "x64") + else() + message(FATAL_ERROR "the ${_TARGET_ARCH} architecture is not supported by Findsodium.cmake.") + endif() + string(APPEND _PLATFORM_PATH "/$$CONFIG$$") + + if (MSVC_VERSION LESS 1900) + math(EXPR _VS_VERSION "${MSVC_VERSION} / 10 - 60") + else() + math(EXPR _VS_VERSION "${MSVC_VERSION} / 10 - 50") + endif() + string(APPEND _PLATFORM_PATH "/v${_VS_VERSION}") + + if (sodium_USE_STATIC_LIBS) + string(APPEND _PLATFORM_PATH "/static") + else() + string(APPEND _PLATFORM_PATH "/dynamic") + endif() + + string(REPLACE "$$CONFIG$$" "Debug" _DEBUG_PATH_SUFFIX "${_PLATFORM_PATH}") + string(REPLACE "$$CONFIG$$" "Release" _RELEASE_PATH_SUFFIX "${_PLATFORM_PATH}") + + find_library(sodium_LIBRARY_DEBUG libsodium.lib + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_DEBUG_PATH_SUFFIX} + ) + find_library(sodium_LIBRARY_RELEASE libsodium.lib + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_RELEASE_PATH_SUFFIX} + ) + if (NOT sodium_USE_STATIC_LIBS) + set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") + find_library(sodium_DLL_DEBUG libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_DEBUG_PATH_SUFFIX} + ) + find_library(sodium_DLL_RELEASE libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES ${_RELEASE_PATH_SUFFIX} + ) + endif() + + elseif(_GCC_COMPATIBLE) + if (sodium_USE_STATIC_LIBS) + find_library(sodium_LIBRARY_DEBUG libsodium.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + find_library(sodium_LIBRARY_RELEASE libsodium.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + else() + find_library(sodium_LIBRARY_DEBUG libsodium.dll.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + find_library(sodium_LIBRARY_RELEASE libsodium.dll.a + HINTS ${sodium_DIR} + PATH_SUFFIXES lib + ) + + file(GLOB _DLL + LIST_DIRECTORIES false + RELATIVE "${sodium_DIR}/bin" + "${sodium_DIR}/bin/libsodium*.dll" + ) + find_library(sodium_DLL_DEBUG ${_DLL} libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES bin + ) + find_library(sodium_DLL_RELEASE ${_DLL} libsodium + HINTS ${sodium_DIR} + PATH_SUFFIXES bin + ) + endif() + else() + message(FATAL_ERROR "this platform is not supported by FindSodium.cmake") + endif() + + +######################################################################## +# unsupported +else() + message(FATAL_ERROR "this platform is not supported by FindSodium.cmake") +endif() + + +######################################################################## +# common stuff + +# extract sodium version +if (sodium_INCLUDE_DIR) + set(_VERSION_HEADER "${_INCLUDE_DIR}/sodium/version.h") + if (EXISTS _VERSION_HEADER) + file(READ "${_VERSION_HEADER}" _VERSION_HEADER_CONTENT) + string(REGEX REPLACE ".*#[ \t]*define[ \t]*SODIUM_VERSION_STRING[ \t]*\"([^\n]*)\".*" "\\1" + sodium_VERSION "${_VERSION_HEADER_CONTENT}") + set(sodium_VERSION "${sodium_VERSION}" PARENT_SCOPE) + endif() +endif() + +# communicate results +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(sodium + REQUIRED_VARS + sodium_LIBRARY_RELEASE + sodium_LIBRARY_DEBUG + sodium_INCLUDE_DIR + VERSION_VAR + sodium_VERSION +) + +# mark file paths as advanced +mark_as_advanced(sodium_INCLUDE_DIR) +mark_as_advanced(sodium_LIBRARY_DEBUG) +mark_as_advanced(sodium_LIBRARY_RELEASE) +if (WIN32) + mark_as_advanced(sodium_DLL_DEBUG) + mark_as_advanced(sodium_DLL_RELEASE) +endif() + +# create imported target +if(sodium_USE_STATIC_LIBS) + set(_LIB_TYPE STATIC) +else() + set(_LIB_TYPE SHARED) +endif() +add_library(sodium ${_LIB_TYPE} IMPORTED) + +set_target_properties(sodium PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${sodium_INCLUDE_DIR}" + IMPORTED_LINK_INTERFACE_LANGUAGES "C" +) + +if (sodium_USE_STATIC_LIBS) + set_target_properties(sodium PROPERTIES + INTERFACE_COMPILE_DEFINITIONS "SODIUM_STATIC" + IMPORTED_LOCATION "${sodium_LIBRARY_RELEASE}" + IMPORTED_LOCATION_DEBUG "${sodium_LIBRARY_DEBUG}" + ) +else() + if (UNIX) + set_target_properties(sodium PROPERTIES + IMPORTED_LOCATION "${sodium_LIBRARY_RELEASE}" + IMPORTED_LOCATION_DEBUG "${sodium_LIBRARY_DEBUG}" + ) + elseif (WIN32) + set_target_properties(sodium PROPERTIES + IMPORTED_IMPLIB "${sodium_LIBRARY_RELEASE}" + IMPORTED_IMPLIB_DEBUG "${sodium_LIBRARY_DEBUG}" + ) + if (NOT (sodium_DLL_DEBUG MATCHES ".*-NOTFOUND")) + set_target_properties(sodium PROPERTIES + IMPORTED_LOCATION_DEBUG "${sodium_DLL_DEBUG}" + ) + endif() + if (NOT (sodium_DLL_RELEASE MATCHES ".*-NOTFOUND")) + set_target_properties(sodium PROPERTIES + IMPORTED_LOCATION_RELWITHDEBINFO "${sodium_DLL_RELEASE}" + IMPORTED_LOCATION_MINSIZEREL "${sodium_DLL_RELEASE}" + IMPORTED_LOCATION_RELEASE "${sodium_DLL_RELEASE}" + ) + endif() + endif() +endif() diff --git a/snapcraft.yaml b/snapcraft.yaml index 97a4bc255..d614c354f 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -29,6 +29,7 @@ parts: - -DWITH_XC_AUTOTYPE=ON - -DWITH_XC_HTTP=ON - -DWITH_XC_YUBIKEY=ON + - -DWITH_XC_BROWSER=ON build-packages: - g++ - libgcrypt20-dev @@ -41,6 +42,7 @@ parts: - libxtst-dev - libyubikey-dev - libykpers-1-dev + - libsodium-dev stage-packages: - dbus - qttranslations5-l10n # common translations @@ -65,4 +67,3 @@ parts: - libqt5svg5 # for loading icon themes which are svg - locales-all - xdg-user-dirs - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 00d628aca..29a2c452c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -182,12 +182,20 @@ add_feature_info(AutoType WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(KeePassHTTP WITH_XC_HTTP "Browser integration compatible with ChromeIPass and PassIFox") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") +add_feature_info(keepassxc-browser WITH_XC_BROWSER "KeePassXC browser support with keepassxc-browser") add_subdirectory(http) if(WITH_XC_HTTP) set(keepasshttp_LIB keepasshttp qhttp Qt5::Network) endif() +set(BROWSER_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/browser) +add_subdirectory(browser) +add_subdirectory(proxy) +if(WITH_XC_BROWSER) + set(keepassxcbrowser_LIB keepassxcbrowser) +endif() + add_subdirectory(autotype) add_subdirectory(cli) @@ -231,6 +239,7 @@ add_library(keepassx_core STATIC ${keepassx_SOURCES}) set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUILDING_CORE) target_link_libraries(keepassx_core ${keepasshttp_LIB} + ${keepassxcbrowser_LIB} ${autotype_LIB} ${sshagent_LIB} ${YUBIKEY_LIBRARIES} @@ -297,6 +306,28 @@ if(APPLE AND WITH_APP_BUNDLE) COMMAND ${MACDEPLOYQT_EXE} ${PROGNAME}.app WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src COMMENT "Deploying app bundle") + + if(WITH_XC_BROWSER) + set(PROXY_BINARY_DIR "${CMAKE_BINARY_DIR}/src/proxy/keepassxc-proxy") + set(PROXY_APP_DIR "${PROGNAME}.app/Contents/MacOS/keepassxc-proxy") + add_custom_command(TARGET ${PROGNAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${PROXY_BINARY_DIR} ${PROXY_APP_DIR} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src + COMMENT "Copying keepassxc-proxy inside the application") + + add_custom_command(TARGET ${PROGNAME} + POST_BUILD + COMMAND ${CMAKE_INSTALL_NAME_TOOL} -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore "@executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore" ${PROXY_APP_DIR} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src + COMMENT "Changing linking of keepassxc-proxy QtCore") + + add_custom_command(TARGET ${PROGNAME} + POST_BUILD + COMMAND ${CMAKE_INSTALL_NAME_TOOL} -change /usr/local/opt/qt/lib/QtNetwork.framework/Versions/5/QtNetwork "@executable_path/../Frameworks/QtNetwork.framework/Versions/5/QtNetwork" ${PROXY_APP_DIR} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src + COMMENT "Changing linking of keepassxc-proxy QtNetwork") + endif() endif() install(TARGETS ${PROGNAME} diff --git a/src/browser/BrowserAccessControlDialog.cpp b/src/browser/BrowserAccessControlDialog.cpp new file mode 100755 index 000000000..7090a4d16 --- /dev/null +++ b/src/browser/BrowserAccessControlDialog.cpp @@ -0,0 +1,59 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "BrowserAccessControlDialog.h" +#include "ui_BrowserAccessControlDialog.h" +#include "core/Entry.h" + +BrowserAccessControlDialog::BrowserAccessControlDialog(QWidget* parent) : + QDialog(parent), + ui(new Ui::BrowserAccessControlDialog()) +{ + this->setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + + ui->setupUi(this); + connect(ui->allowButton, SIGNAL(clicked()), this, SLOT(accept())); + connect(ui->denyButton, SIGNAL(clicked()), this, SLOT(reject())); +} + +BrowserAccessControlDialog::~BrowserAccessControlDialog() +{ +} + +void BrowserAccessControlDialog::setUrl(const QString& url) +{ + ui->label->setText(QString(tr("%1 has requested access to passwords for the following item(s).\n" + "Please select whether you want to allow access.")).arg(QUrl(url).host())); +} + +void BrowserAccessControlDialog::setItems(const QList& items) +{ + for (Entry* entry : items) { + ui->itemsList->addItem(entry->title() + " - " + entry->username()); + } +} + +bool BrowserAccessControlDialog::remember() const +{ + return ui->rememberDecisionCheckBox->isChecked(); +} + +void BrowserAccessControlDialog::setRemember(bool r) +{ + ui->rememberDecisionCheckBox->setChecked(r); +} diff --git a/src/browser/BrowserAccessControlDialog.h b/src/browser/BrowserAccessControlDialog.h new file mode 100755 index 000000000..d2a66ce0d --- /dev/null +++ b/src/browser/BrowserAccessControlDialog.h @@ -0,0 +1,48 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 BROWSERACCESSCONTROLDIALOG_H +#define BROWSERACCESSCONTROLDIALOG_H + +#include +#include + +class Entry; + +namespace Ui { +class BrowserAccessControlDialog; +} + +class BrowserAccessControlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit BrowserAccessControlDialog(QWidget* parent = nullptr); + ~BrowserAccessControlDialog(); + + void setUrl(const QString& url); + void setItems(const QList& items); + bool remember() const; + void setRemember(bool r); + +private: + QScopedPointer ui; +}; + +#endif // BROWSERACCESSCONTROLDIALOG_H diff --git a/src/browser/BrowserAccessControlDialog.ui b/src/browser/BrowserAccessControlDialog.ui new file mode 100755 index 000000000..d4f104544 --- /dev/null +++ b/src/browser/BrowserAccessControlDialog.ui @@ -0,0 +1,69 @@ + + + BrowserAccessControlDialog + + + + 0 + 0 + 400 + 221 + + + + keepassxc-browser Confirm Access + + + + + + + + + + + + + + + + Remember this decision + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Allow + + + + + + + Deny + + + + + + + + + + diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp new file mode 100755 index 000000000..ebd9e2d9a --- /dev/null +++ b/src/browser/BrowserAction.cpp @@ -0,0 +1,546 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "BrowserAction.h" +#include "BrowserSettings.h" +#include "sodium.h" +#include "sodium/crypto_box.h" +#include "sodium/randombytes.h" +#include "config-keepassx.h" + +BrowserAction::BrowserAction(BrowserService& browserService) : + m_mutex(QMutex::Recursive), + m_browserService(browserService), + m_associated(false) +{ + +} + +QJsonObject BrowserAction::readResponse(const QJsonObject& json) +{ + if (json.isEmpty()) { + return QJsonObject(); + } + + const QString action = json.value("action").toString(); + if (action.isEmpty()) { + return QJsonObject(); + } + + QMutexLocker locker(&m_mutex); + if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !m_browserService.isDatabaseOpened()) { + if (m_clientPublicKey.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); + } else if (!m_browserService.openDatabase()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); + } + } + + return handleAction(json); +} + + +// Private functions +/////////////////////// + +QJsonObject BrowserAction::handleAction(const QJsonObject& json) +{ + QString action = json.value("action").toString(); + if (action.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); + } + + if (action.compare("change-public-keys", Qt::CaseSensitive) == 0) { + return handleChangePublicKeys(json, action); + } else if (action.compare("get-databasehash", Qt::CaseSensitive) == 0) { + return handleGetDatabaseHash(json, action); + } else if (action.compare("associate", Qt::CaseSensitive) == 0) { + return handleAssociate(json, action); + } else if (action.compare("test-associate", Qt::CaseSensitive) == 0) { + return handleTestAssociate(json, action); + } else if (action.compare("get-logins", Qt::CaseSensitive) == 0) { + return handleGetLogins(json, action); + } else if (action.compare("generate-password", Qt::CaseSensitive) == 0) { + return handleGeneratePassword(json, action); + } else if (action.compare("set-login", Qt::CaseSensitive) == 0) { + return handleSetLogin(json, action); + } else if (action.compare("lock-database", Qt::CaseSensitive) == 0) { + return handleLockDatabase(json, action); + } + + // Action was not recognized + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); +} + +QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QString& action) +{ + QMutexLocker locker(&m_mutex); + const QString nonce = json.value("nonce").toString(); + const QString clientPublicKey = json.value("publicKey").toString(); + + if (clientPublicKey.isEmpty() || nonce.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); + } + + m_associated = false; + unsigned char pk[crypto_box_PUBLICKEYBYTES]; + unsigned char sk[crypto_box_SECRETKEYBYTES]; + crypto_box_keypair(pk, sk); + + const QString publicKey = getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES); + const QString secretKey = getBase64FromKey(sk, crypto_box_SECRETKEYBYTES); + m_clientPublicKey = clientPublicKey; + m_publicKey = publicKey; + m_secretKey = secretKey; + + QJsonObject response = buildMessage(incrementNonce(nonce)); + response["action"] = action; + response["publicKey"] = publicKey; + + return response; +} + +QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + if (hash.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); + } + + QString command = decrypted.value("action").toString(); + if (!command.isEmpty() && command.compare("get-databasehash", Qt::CaseSensitive) == 0) { + QJsonObject message; + message["hash"] = hash; + message["version"] = KEEPASSX_VERSION; + return buildResponse(action, message, incrementNonce(nonce)); + } + + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); +} + +QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString key = decrypted.value("key").toString(); + if (key.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + QMutexLocker locker(&m_mutex); + if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) { + const QString id = m_browserService.storeKey(key); + if (id.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); + } + + m_associated = true; + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + message["id"] = id; + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); +} + +QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString responseKey = decrypted.value("key").toString(); + const QString id = decrypted.value("id").toString(); + if (responseKey.isEmpty() || id.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); + } + + QMutexLocker locker(&m_mutex); + const QString key = m_browserService.getKey(id); + if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + m_associated = true; + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + message["id"] = id; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + + QMutexLocker locker(&m_mutex); + if (!m_associated) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString url = decrypted.value("url").toString(); + if (url.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED); + } + + const QString id = decrypted.value("id").toString(); + const QString submit = decrypted.value("submitUrl").toString(); + const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, ""); + + if (users.isEmpty()) { + return QJsonObject(); // No logins found. Not an error, return an empty JSON object. + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["count"] = users.count(); + message["entries"] = users; + message["hash"] = hash; + message["id"] = id; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const QString& action) +{ + const QString nonce = json.value("nonce").toString(); + const QString password = BrowserSettings::generatePassword(); + const QString bits = QString::number(BrowserSettings::getbits()); // For some reason this always returns 1140 bits? + + if (nonce.isEmpty() || password.isEmpty()) { + return QJsonObject(); + } + + QJsonArray arr; + QJsonObject passwd; + passwd["login"] = QString::number(password.length() * 8); //bits; + passwd["password"] = password; + arr.append(passwd); + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["entries"] = arr; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + + QMutexLocker locker(&m_mutex); + if (!m_associated) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString url = decrypted.value("url").toString(); + if (url.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED); + } + + const QString id = decrypted.value("id").toString(); + const QString login = decrypted.value("login").toString(); + const QString password = decrypted.value("password").toString(); + const QString submitUrl = decrypted.value("submitUrl").toString(); + const QString uuid = decrypted.value("uuid").toString(); + const QString realm; + + if (uuid.isEmpty()) { + m_browserService.addEntry(id, login, password, url, submitUrl, realm); + } else { + m_browserService.updateEntry(id, uuid, login, password, url); + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["count"] = QJsonValue::Null; + message["entries"] = QJsonValue::Null; + message["error"] = ""; + message["hash"] = hash; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + if (hash.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); + } + + QString command = decrypted.value("action").toString(); + if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) { + QMutexLocker locker(&m_mutex); + m_browserService.lockDatabase(); + + const QString newNonce = incrementNonce(nonce); + QJsonObject message = buildMessage(newNonce); + + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); +} + +QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const +{ + QJsonObject response; + response["action"] = action; + response["errorCode"] = QString::number(errorCode); + response["error"] = getErrorMessage(errorCode); + return response; +} + +QJsonObject BrowserAction::buildMessage(const QString& nonce) const +{ + QJsonObject message; + message["version"] = KEEPASSX_VERSION; + message["success"] = "true"; + message["nonce"] = nonce; + return message; +} + +QJsonObject BrowserAction::buildResponse(const QString& action, const QJsonObject& message, const QString& nonce) +{ + QJsonObject response; + response["action"] = action; + response["message"] = encryptMessage(message, nonce); + response["nonce"] = nonce; + return response; +} + +QString BrowserAction::getErrorMessage(const int errorCode) const +{ + switch (errorCode) { + case ERROR_KEEPASS_DATABASE_NOT_OPENED: return "Database not opened"; + case ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED: return "Database hash not available"; + case ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED: return "Client public key not received"; + case ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE: return "Cannot decrypt message"; + case ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED: return "Timeout or cannot connect to KeePassXC"; + case ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED: return "Action cancelled or denied"; + case ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE: return "Cannot encrypt message or public key not found. Is Native Messaging enabled in KeePassXC?"; + case ERROR_KEEPASS_ASSOCIATION_FAILED: return "KeePassXC association failed, try again"; + case ERROR_KEEPASS_KEY_CHANGE_FAILED: return "Key change was not successful"; + case ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED: return "Encryption key is not recognized"; + case ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND: return "No saved databases found"; + case ERROR_KEEPASS_INCORRECT_ACTION: return "Incorrect action"; + case ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED: return "Empty message received"; + case ERROR_KEEPASS_NO_URL_PROVIDED: return "No URL provided"; + default: return "Unknown error"; + } +} + +QString BrowserAction::getDatabaseHash() +{ + QMutexLocker locker(&m_mutex); + QByteArray hash = QCryptographicHash::hash( + (m_browserService.getDatabaseRootUuid() + m_browserService.getDatabaseRecycleBinUuid()).toUtf8(), + QCryptographicHash::Sha256).toHex(); + return QString(hash); +} + +QString BrowserAction::encryptMessage(const QJsonObject& message, const QString& nonce) +{ + if (message.isEmpty() || nonce.isEmpty()) { + return QString(); + } + + const QString reply(QJsonDocument(message).toJson()); + if (!reply.isEmpty()) { + return encrypt(reply, nonce); + } + + return QString(); +} + +QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& nonce, const QString& action) +{ + if (message.isEmpty() || nonce.isEmpty()) { + return QJsonObject(); + } + + QByteArray ba = decrypt(message, nonce); + if (!ba.isEmpty()) { + return getJsonObject(ba); + } + + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); +} + +QString BrowserAction::encrypt(const QString plaintext, const QString nonce) +{ + QMutexLocker locker(&m_mutex); + const QByteArray ma = plaintext.toUtf8(); + const QByteArray na = base64Decode(nonce); + const QByteArray ca = base64Decode(m_clientPublicKey); + const QByteArray sa = base64Decode(m_secretKey); + + std::vector m(ma.cbegin(), ma.cend()); + std::vector n(na.cbegin(), na.cend()); + std::vector ck(ca.cbegin(), ca.cend()); + std::vector sk(sa.cbegin(), sa.cend()); + + std::vector e; + e.resize(max_length); + + if (m.empty() || n.empty() || ck.empty() || sk.empty()) { + return QString(); + } + + if (crypto_box_easy(e.data(), m.data(), m.size(), n.data(), ck.data(), sk.data()) == 0) { + QByteArray res = getQByteArray(e.data(), (crypto_box_MACBYTES + ma.length())); + return res.toBase64(); + } + + return QString(); +} + +QByteArray BrowserAction::decrypt(const QString encrypted, const QString nonce) +{ + QMutexLocker locker(&m_mutex); + const QByteArray ma = base64Decode(encrypted); + const QByteArray na = base64Decode(nonce); + const QByteArray ca = base64Decode(m_clientPublicKey); + const QByteArray sa = base64Decode(m_secretKey); + + std::vector m(ma.cbegin(), ma.cend()); + std::vector n(na.cbegin(), na.cend()); + std::vector ck(ca.cbegin(), ca.cend()); + std::vector sk(sa.cbegin(), sa.cend()); + + std::vector d; + d.resize(max_length); + + if (m.empty() || n.empty() || ck.empty() || sk.empty()) { + return QByteArray(); + } + + if (crypto_box_open_easy(d.data(), m.data(), ma.length(), n.data(), ck.data(), sk.data()) == 0) { + return getQByteArray(d.data(), std::char_traits::length(reinterpret_cast(d.data()))); + } + + return QByteArray(); +} + +QString BrowserAction::getBase64FromKey(const uchar* array, const uint len) +{ + return getQByteArray(array, len).toBase64(); +} + +QByteArray BrowserAction::getQByteArray(const uchar* array, const uint len) const +{ + QByteArray qba; + qba.reserve(len); + for (uint i = 0; i < len; ++i) { + qba.append(static_cast(array[i])); + } + return qba; +} + +QJsonObject BrowserAction::getJsonObject(const uchar* pArray, const uint len) const +{ + QByteArray arr = getQByteArray(pArray, len); + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(arr, &err)); + return doc.object(); +} + +QJsonObject BrowserAction::getJsonObject(const QByteArray ba) const +{ + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(ba, &err)); + return doc.object(); +} + +QByteArray BrowserAction::base64Decode(const QString str) +{ + return QByteArray::fromBase64(str.toUtf8()); +} + +QString BrowserAction::incrementNonce(const QString& nonce) +{ + const QByteArray nonceArray = base64Decode(nonce); + std::vector n(nonceArray.cbegin(), nonceArray.cend()); + + sodium_increment(n.data(), n.size()); + return getQByteArray(n.data(), n.size()).toBase64(); +} + +void BrowserAction::removeSharedEncryptionKeys() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeSharedEncryptionKeys(); +} + +void BrowserAction::removeStoredPermissions() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeStoredPermissions(); +} diff --git a/src/browser/BrowserAction.h b/src/browser/BrowserAction.h new file mode 100755 index 000000000..83d89fb7f --- /dev/null +++ b/src/browser/BrowserAction.h @@ -0,0 +1,97 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 BROWSERACTION_H +#define BROWSERACTION_H + +#include +#include +#include +#include +#include "BrowserService.h" + +class BrowserAction : public QObject +{ + Q_OBJECT + + enum { + ERROR_KEEPASS_DATABASE_NOT_OPENED = 1, + ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED = 2, + ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED = 3, + ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE = 4, + ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED = 5, + ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED = 6, + ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE = 7, + ERROR_KEEPASS_ASSOCIATION_FAILED = 8, + ERROR_KEEPASS_KEY_CHANGE_FAILED = 9, + ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED = 10, + ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND = 11, + ERROR_KEEPASS_INCORRECT_ACTION = 12, + ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED = 13, + ERROR_KEEPASS_NO_URL_PROVIDED = 14 + }; + +public: + BrowserAction(BrowserService& browserService); + ~BrowserAction() = default; + + QJsonObject readResponse(const QJsonObject& json); + +public slots: + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +private: + QJsonObject handleAction(const QJsonObject& json); + QJsonObject handleChangePublicKeys(const QJsonObject& json, const QString& action); + QJsonObject handleGetDatabaseHash(const QJsonObject& json, const QString& action); + QJsonObject handleAssociate(const QJsonObject& json, const QString& action); + QJsonObject handleTestAssociate(const QJsonObject& json, const QString& action); + QJsonObject handleGetLogins(const QJsonObject& json, const QString& action); + QJsonObject handleGeneratePassword(const QJsonObject& json, const QString& action); + QJsonObject handleSetLogin(const QJsonObject& json, const QString& action); + QJsonObject handleLockDatabase(const QJsonObject& json, const QString& action); + + QJsonObject buildMessage(const QString& nonce) const; + QJsonObject buildResponse(const QString& action, const QJsonObject& message, const QString& nonce); + QJsonObject getErrorReply(const QString& action, const int errorCode) const; + QString getErrorMessage(const int errorCode) const; + QString getDatabaseHash(); + + QString encryptMessage(const QJsonObject& message, const QString& nonce); + QJsonObject decryptMessage(const QString& message, const QString& nonce, const QString& action = QString()); + QString encrypt(const QString plaintext, const QString nonce); + QByteArray decrypt(const QString encrypted, const QString nonce); + + QString getBase64FromKey(const uchar* array, const uint len); + QByteArray getQByteArray(const uchar* array, const uint len) const; + QJsonObject getJsonObject(const uchar* pArray, const uint len) const; + QJsonObject getJsonObject(const QByteArray ba) const; + QByteArray base64Decode(const QString str); + QString incrementNonce(const QString& nonce); + +private: + QMutex m_mutex; + BrowserService& m_browserService; + QString m_clientPublicKey; + QString m_publicKey; + QString m_secretKey; + bool m_associated; +}; + +#endif // BROWSERACTION_H diff --git a/src/browser/BrowserClients.cpp b/src/browser/BrowserClients.cpp new file mode 100755 index 000000000..9c47fdd46 --- /dev/null +++ b/src/browser/BrowserClients.cpp @@ -0,0 +1,77 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "BrowserClients.h" + +BrowserClients::BrowserClients(BrowserService& browserService) : + m_mutex(QMutex::Recursive), + m_browserService(browserService) +{ + m_clients.reserve(1000); +} + +QJsonObject BrowserClients::readResponse(const QByteArray& arr) +{ + QJsonObject json; + const QJsonObject message = byteArrayToJson(arr); + const QString clientID = getClientID(message); + + if (!clientID.isEmpty()) { + const ClientPtr client = getClient(clientID); + if (client->browserAction) { + json = client->browserAction->readResponse(message); + } + } + + return json; +} + +QJsonObject BrowserClients::byteArrayToJson(const QByteArray& arr) const +{ + QJsonObject json; + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(arr, &err)); + if (doc.isObject()) { + json = doc.object(); + } + + return json; +} + +QString BrowserClients::getClientID(const QJsonObject& json) const +{ + return json["clientID"].toString(); +} + +BrowserClients::ClientPtr BrowserClients::getClient(const QString& clientID) +{ + QMutexLocker locker(&m_mutex); + for (const auto &i : m_clients) { + if (i->clientID.compare(clientID, Qt::CaseSensitive) == 0) { + return i; + } + } + + // clientID not found, create a new client + QSharedPointer ba = QSharedPointer::create(m_browserService); + ClientPtr client = ClientPtr::create(clientID, ba); + m_clients.push_back(client); + return m_clients.back(); +} diff --git a/src/browser/BrowserClients.h b/src/browser/BrowserClients.h new file mode 100755 index 000000000..f9052e7de --- /dev/null +++ b/src/browser/BrowserClients.h @@ -0,0 +1,56 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 BROWSERCLIENTS_H +#define BROWSERCLIENTS_H + +#include +#include +#include +#include +#include +#include "BrowserAction.h" + +class BrowserClients +{ + struct Client { + Client(const QString& id, QSharedPointer ba) : clientID(id), browserAction(ba) {} + QString clientID; + QSharedPointer browserAction; + }; + + typedef QSharedPointer ClientPtr; + +public: + BrowserClients(BrowserService& browserService); + ~BrowserClients() = default; + + QJsonObject readResponse(const QByteArray& arr); + +private: + QJsonObject byteArrayToJson(const QByteArray& arr) const; + QString getClientID(const QJsonObject& json) const; + ClientPtr getClient(const QString& clientID); + +private: + QMutex m_mutex; + QVector m_clients; + BrowserService& m_browserService; +}; + +#endif // BROWSERCLIENTS_H diff --git a/src/browser/BrowserEntryConfig.cpp b/src/browser/BrowserEntryConfig.cpp new file mode 100644 index 000000000..a390e5a75 --- /dev/null +++ b/src/browser/BrowserEntryConfig.cpp @@ -0,0 +1,109 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "BrowserEntryConfig.h" +#include +#include "core/Entry.h" +#include "core/EntryAttributes.h" + +static const char KEEPASSBROWSER_NAME[] = "keepassxc-browser Settings"; //TODO: duplicated string (also in Service.cpp) + + +BrowserEntryConfig::BrowserEntryConfig(QObject* parent) : + QObject(parent) +{ +} + +QStringList BrowserEntryConfig::allowedHosts() const +{ + return m_allowedHosts.toList(); +} + +void BrowserEntryConfig::setAllowedHosts(const QStringList& allowedHosts) +{ + m_allowedHosts = allowedHosts.toSet(); +} + +QStringList BrowserEntryConfig::deniedHosts() const +{ + return m_deniedHosts.toList(); +} + +void BrowserEntryConfig::setDeniedHosts(const QStringList& deniedHosts) +{ + m_deniedHosts = deniedHosts.toSet(); +} + +bool BrowserEntryConfig::isAllowed(const QString& host) const +{ + return m_allowedHosts.contains(host); +} + +void BrowserEntryConfig::allow(const QString& host) +{ + m_allowedHosts.insert(host); + m_deniedHosts.remove(host); +} + +bool BrowserEntryConfig::isDenied(const QString& host) const +{ + return m_deniedHosts.contains(host); +} + +void BrowserEntryConfig::deny(const QString& host) +{ + m_deniedHosts.insert(host); + m_allowedHosts.remove(host); +} + +QString BrowserEntryConfig::realm() const +{ + return m_realm; +} + +void BrowserEntryConfig::setRealm(const QString& realm) +{ + m_realm = realm; +} + +bool BrowserEntryConfig::load(const Entry* entry) +{ + QString s = entry->attributes()->value(KEEPASSBROWSER_NAME); + if (s.isEmpty()) { + return false; + } + + QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8()); + if (doc.isNull()) { + return false; + } + + QVariantMap map = doc.object().toVariantMap(); + for (QVariantMap::const_iterator iter = map.cbegin(); iter != map.cend(); ++iter) { + setProperty(iter.key().toLatin1(), iter.value()); + } + return true; +} + +void BrowserEntryConfig::save(Entry* entry) +{ + QVariantMap v = qo2qv(this); + QJsonObject o = QJsonObject::fromVariantMap(v); + QByteArray json = QJsonDocument(o).toJson(QJsonDocument::Compact); + entry->attributes()->set(KEEPASSBROWSER_NAME, json); +} diff --git a/src/browser/BrowserEntryConfig.h b/src/browser/BrowserEntryConfig.h new file mode 100644 index 000000000..0ffaf985b --- /dev/null +++ b/src/browser/BrowserEntryConfig.h @@ -0,0 +1,60 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 BROWSERENTRYCONFIG_H +#define BROWSERENTRYCONFIG_H + +#include +#include +#include +#include +#include "Variant.h" + +class Entry; + +class BrowserEntryConfig : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList Allow READ allowedHosts WRITE setAllowedHosts) + Q_PROPERTY(QStringList Deny READ deniedHosts WRITE setDeniedHosts ) + Q_PROPERTY(QString Realm READ realm WRITE setRealm ) + +public: + BrowserEntryConfig(QObject* object = 0); + + bool load(const Entry* entry); + void save(Entry* entry); + bool isAllowed(const QString& host) const; + void allow(const QString& host); + bool isDenied(const QString& host) const; + void deny(const QString& host); + QString realm() const; + void setRealm(const QString& realm); + +private: + QStringList allowedHosts() const; + void setAllowedHosts(const QStringList& allowedHosts); + QStringList deniedHosts() const; + void setDeniedHosts(const QStringList& deniedHosts); + + QSet m_allowedHosts; + QSet m_deniedHosts; + QString m_realm; +}; + +#endif // BROWSERENTRYCONFIG_H diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp new file mode 100755 index 000000000..9a876ac45 --- /dev/null +++ b/src/browser/BrowserOptionDialog.cpp @@ -0,0 +1,104 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "BrowserOptionDialog.h" +#include "ui_BrowserOptionDialog.h" +#include "BrowserSettings.h" +#include "core/FilePath.h" + +#include + +BrowserOptionDialog::BrowserOptionDialog(QWidget* parent) : + QWidget(parent), + m_ui(new Ui::BrowserOptionDialog()) +{ + m_ui->setupUi(this); + connect(m_ui->removeSharedEncryptionKeys, SIGNAL(clicked()), this, SIGNAL(removeSharedEncryptionKeys())); + connect(m_ui->removeStoredPermissions, SIGNAL(clicked()), this, SIGNAL(removeStoredPermissions())); + + m_ui->warningWidget->showMessage(tr("The following options can be dangerous!\nChange them only if you know what you are doing."), MessageWidget::Warning); + m_ui->warningWidget->setIcon(FilePath::instance()->icon("status", "dialog-warning")); + m_ui->warningWidget->setCloseButtonVisible(false); + + m_ui->tabWidget->setEnabled(m_ui->enableBrowserSupport->isChecked()); + connect(m_ui->enableBrowserSupport, SIGNAL(toggled(bool)), m_ui->tabWidget, SLOT(setEnabled(bool))); + + m_ui->customProxyLocation->setEnabled(m_ui->useCustomProxy->isChecked()); + connect(m_ui->useCustomProxy, SIGNAL(toggled(bool)), m_ui->customProxyLocation, SLOT(setEnabled(bool))); +} + +BrowserOptionDialog::~BrowserOptionDialog() +{ +} + +void BrowserOptionDialog::loadSettings() +{ + BrowserSettings settings; + m_ui->enableBrowserSupport->setChecked(settings.isEnabled()); + + m_ui->showNotification->setChecked(settings.showNotification()); + m_ui->bestMatchOnly->setChecked(settings.bestMatchOnly()); + m_ui->unlockDatabase->setChecked(settings.unlockDatabase()); + m_ui->matchUrlScheme->setChecked(settings.matchUrlScheme()); + + if (settings.sortByUsername()) { + m_ui->sortByUsername->setChecked(true); + } else { + m_ui->sortByTitle->setChecked(true); + } + + m_ui->alwaysAllowAccess->setChecked(settings.alwaysAllowAccess()); + m_ui->alwaysAllowUpdate->setChecked(settings.alwaysAllowUpdate()); + m_ui->searchInAllDatabases->setChecked(settings.searchInAllDatabases()); + m_ui->supportKphFields->setChecked(settings.supportKphFields()); + m_ui->supportBrowserProxy->setChecked(settings.supportBrowserProxy()); + m_ui->useCustomProxy->setChecked(settings.useCustomProxy()); + m_ui->customProxyLocation->setText(settings.customProxyLocation()); + m_ui->updateBinaryPath->setChecked(settings.updateBinaryPath()); + m_ui->chromeSupport->setChecked(settings.chromeSupport()); + m_ui->chromiumSupport->setChecked(settings.chromiumSupport()); + m_ui->firefoxSupport->setChecked(settings.firefoxSupport()); + m_ui->vivaldiSupport->setChecked(settings.vivaldiSupport()); +} + +void BrowserOptionDialog::saveSettings() +{ + BrowserSettings settings; + settings.setEnabled(m_ui->enableBrowserSupport->isChecked()); + settings.setShowNotification(m_ui->showNotification->isChecked()); + settings.setBestMatchOnly(m_ui->bestMatchOnly->isChecked()); + settings.setUnlockDatabase(m_ui->unlockDatabase->isChecked()); + settings.setMatchUrlScheme(m_ui->matchUrlScheme->isChecked()); + settings.setSortByUsername(m_ui->sortByUsername->isChecked()); + + settings.setSupportBrowserProxy(m_ui->supportBrowserProxy->isChecked()); + settings.setUseCustomProxy(m_ui->useCustomProxy->isChecked()); + settings.setCustomProxyLocation(m_ui->customProxyLocation->text()); + + settings.setUpdateBinaryPath(m_ui->updateBinaryPath->isChecked()); + settings.setAlwaysAllowAccess(m_ui->alwaysAllowAccess->isChecked()); + settings.setAlwaysAllowUpdate(m_ui->alwaysAllowUpdate->isChecked()); + settings.setSearchInAllDatabases(m_ui->searchInAllDatabases->isChecked()); + settings.setSupportKphFields(m_ui->supportKphFields->isChecked()); + + settings.setChromeSupport(m_ui->chromeSupport->isChecked()); + settings.setChromiumSupport(m_ui->chromiumSupport->isChecked()); + settings.setFirefoxSupport(m_ui->firefoxSupport->isChecked()); + settings.setVivaldiSupport(m_ui->vivaldiSupport->isChecked()); +} diff --git a/src/browser/BrowserOptionDialog.h b/src/browser/BrowserOptionDialog.h new file mode 100755 index 000000000..798d215d6 --- /dev/null +++ b/src/browser/BrowserOptionDialog.h @@ -0,0 +1,50 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 BROWSEROPTIONDIALOG_H +#define BROWSEROPTIONDIALOG_H + +#include +#include + +namespace Ui { +class BrowserOptionDialog; +} + +class BrowserOptionDialog : public QWidget +{ + Q_OBJECT + +public: + explicit BrowserOptionDialog(QWidget* parent = nullptr); + ~BrowserOptionDialog(); + +public slots: + void loadSettings(); + void saveSettings(); + +signals: + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +private: + QScopedPointer m_ui; +}; + +#endif // BROWSEROPTIONDIALOG_H diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserOptionDialog.ui new file mode 100755 index 000000000..81eaa229d --- /dev/null +++ b/src/browser/BrowserOptionDialog.ui @@ -0,0 +1,327 @@ + + + BrowserOptionDialog + + + + 0 + 0 + 577 + 404 + + + + Dialog + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + This is required for accessing your databases with keepassxc-browser + + + Enable KeepassXC browser extension + + + + + + + 0 + + + + General + + + + + + Sh&ow a notification when credentials are requested + + + true + + + + + + + Only returns the best matches for a specific URL instead of all entries for the whole domain. + + + &Return only best-matching entries + + + + + + + Re&quest to unlock the database if it is locked + + + true + + + + + + + Only entries with the same scheme (http://, https://, ...) are returned. + + + &Match URL schemes + + + + + + + Sort matching entries by &username + + + + + + + Sort &matching entries by title + + + + + + + R&emove all shared encryption keys from active database + + + + + + + Re&move all stored permissions from entries in active database + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Supported browsers + + + + + + Native messaging requires certain .json files to be installed. Already installed browsers are automatically detected. + + + true + + + + + + + Enable KeePassXC native messaging extension for these browsers: + + + + + + + Chrome + + + false + + + + + + + Chromium + + + false + + + + + + + Firefox + + + false + + + + + + + Vivaldi + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Advanced + + + + + + + 0 + 0 + + + + + + + + Always allow &access to entries + + + + + + + Always allow &updating entries + + + + + + + Only the selected database has to be connected with a client. + + + Searc&h in all opened databases for matching entries + + + + + + + Automatically creating or updating string fields is not supported. + + + &Return advanced string fields which start with "KPH: " + + + + + + + Updates KeePassXC or keepassxc-proxy binary path automatically to native messaging scripts on startup. + + + &Update KeePassXC binary path automatically to native messaging scripts on startup + + + + + + + Support a proxy application between KeePassXC and browser extension. + + + &Enable support for proxy application between KeePassXC and browser extension + + + + + + + Use a custom proxy location if you installed a proxy manually. + + + &Use a custom proxy location + + + + + + + 999 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + MessageWidget + QWidget +
gui/MessageWidget.h
+ 1 +
+
+ + +
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp new file mode 100644 index 000000000..97b8fea42 --- /dev/null +++ b/src/browser/BrowserService.cpp @@ -0,0 +1,734 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 +#include +#include "BrowserService.h" +#include "BrowserSettings.h" +#include "BrowserEntryConfig.h" +#include "BrowserAccessControlDialog.h" +#include "core/Database.h" +#include "core/Group.h" +#include "core/EntrySearcher.h" +#include "core/Metadata.h" +#include "core/Uuid.h" +#include "core/PasswordGenerator.h" + + +// de887cc3-0363-43b8-974b-5911b8816224 +static const unsigned char KEEPASSXCBROWSER_UUID_DATA[] = { + 0xde, 0x88, 0x7c, 0xc3, 0x03, 0x63, 0x43, 0xb8, + 0x97, 0x4b, 0x59, 0x11, 0xb8, 0x81, 0x62, 0x24 +}; +static const Uuid KEEPASSXCBROWSER_UUID = Uuid(QByteArray::fromRawData(reinterpret_cast(KEEPASSXCBROWSER_UUID_DATA), sizeof(KEEPASSXCBROWSER_UUID_DATA))); +static const char KEEPASSXCBROWSER_NAME[] = "keepassxc-browser Settings"; +static const char ASSOCIATE_KEY_PREFIX[] = "Public Key: "; +static const char KEEPASSXCBROWSER_GROUP_NAME[] = "keepassxc-browser Passwords"; +static int KEEPASSXCBROWSER_DEFAULT_ICON = 1; + +BrowserService::BrowserService(DatabaseTabWidget* parent) : + m_dbTabWidget(parent), + m_dialogActive(false) +{ + connect(m_dbTabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), this, SLOT(databaseLocked(DatabaseWidget*))); + connect(m_dbTabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), this, SLOT(databaseUnlocked(DatabaseWidget*))); + connect(m_dbTabWidget, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), this, SLOT(activateDatabaseChanged(DatabaseWidget*))); +} + +bool BrowserService::isDatabaseOpened() const +{ + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return false; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + return true; + } + + return false; +} + +bool BrowserService::openDatabase() +{ + if (!BrowserSettings::unlockDatabase()) { + return false; + } + + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return false; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + return true; + } + + m_dbTabWidget->activateWindow(); + return false; +} + +void BrowserService::lockDatabase() +{ + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "lockDatabase", Qt::BlockingQueuedConnection); + } + + DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return; + } + + if (dbWidget->currentMode() == DatabaseWidget::ViewMode || dbWidget->currentMode() == DatabaseWidget::EditMode) { + dbWidget->lock(); + } +} + +QString BrowserService::getDatabaseRootUuid() +{ + Database* db = getDatabase(); + if (!db) { + return QString(); + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return QString(); + } + + return rootGroup->uuid().toHex(); +} + +QString BrowserService::getDatabaseRecycleBinUuid() +{ + Database* db = getDatabase(); + if (!db) { + return QString(); + } + + Group* recycleBin = db->metadata()->recycleBin(); + if (!recycleBin) { + return QString(); + } + return recycleBin->uuid().toHex(); +} + +Entry* BrowserService::getConfigEntry(bool create) +{ + Entry* entry = nullptr; + Database* db = getDatabase(); + if (!db) { + return nullptr; + } + + entry = db->resolveEntry(KEEPASSXCBROWSER_UUID); + if (!entry && create) { + entry = new Entry(); + entry->setTitle(QLatin1String(KEEPASSXCBROWSER_NAME)); + entry->setUuid(KEEPASSXCBROWSER_UUID); + entry->setAutoTypeEnabled(false); + entry->setGroup(db->rootGroup()); + return entry; + } + + if (entry && entry->group() == db->metadata()->recycleBin()) { + if (!create) { + return nullptr; + } else { + entry->setGroup(db->rootGroup()); + return entry; + } + } + + return entry; +} + +QString BrowserService::storeKey(const QString& key) +{ + QString id; + + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "storeKey", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QString, id), + Q_ARG(const QString&, key)); + return id; + } + + Entry* config = getConfigEntry(true); + if (!config) { + return QString(); + } + + bool contains = false; + QMessageBox::StandardButton dialogResult = QMessageBox::No; + + do { + bool ok = false; + id = QInputDialog::getText(0, tr("KeePassXC: New key association request"), + tr("You have received an association " + "request for the above key.\n" + "If you would like to allow it access " + "to your KeePassXC database,\n" + "give it a unique name to identify and accept it."), + QLineEdit::Normal, QString(), &ok); + if (!ok || id.isEmpty()) { + return QString(); + } + + contains = config->attributes()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); + dialogResult = QMessageBox::warning(0, tr("KeePassXC: Overwrite existing key?"), + tr("A shared encryption key with the name \"%1\" already exists.\nDo you want to overwrite it?").arg(id), + QMessageBox::Yes | QMessageBox::No); + } while (contains && dialogResult == QMessageBox::No); + + config->attributes()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key, true); + return id; +} + +QString BrowserService::getKey(const QString& id) +{ + Entry* config = getConfigEntry(); + if (!config) { + return QString(); + } + + return config->attributes()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); +} + +// No need to use KeepassHttpProtocol. Just return a JSON array. +QJsonArray BrowserService::findMatchingEntries(const QString& id, const QString& url, const QString& submitUrl, const QString& realm) +{ + QJsonArray result; + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "findMatchingEntries", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QJsonArray, result), + Q_ARG(const QString&, id), + Q_ARG(const QString&, url), + Q_ARG(const QString&, submitUrl), + Q_ARG(const QString&, realm)); + return result; + } + + const bool alwaysAllowAccess = BrowserSettings::alwaysAllowAccess(); + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + + // Check entries for authorization + QList pwEntriesToConfirm; + QList pwEntries; + for (Entry* entry : searchEntries(url)) { + switch (checkAccess(entry, host, submitHost, realm)) { + case Denied: + continue; + + case Unknown: + if (alwaysAllowAccess) { + pwEntries.append(entry); + } else { + pwEntriesToConfirm.append(entry); + } + break; + + case Allowed: + pwEntries.append(entry); + break; + } + } + + // Confirm entries + if (confirmEntries(pwEntriesToConfirm, url, host, submitHost, realm)) { + pwEntries.append(pwEntriesToConfirm); + } + + if (pwEntries.isEmpty()) { + return QJsonArray(); + } + + // Sort results + pwEntries = sortEntries(pwEntries, host, submitUrl); + + // Fill the list + for (Entry* entry : pwEntries) { + result << prepareEntry(entry); + } + + return result; +} + +void BrowserService::addEntry(const QString&, const QString& login, const QString& password, const QString& url, const QString& submitUrl, const QString& realm) +{ + Group* group = findCreateAddEntryGroup(); + if (!group) { + return; + } + + Entry* entry = new Entry(); + entry->setUuid(Uuid::random()); + entry->setTitle(QUrl(url).host()); + entry->setUrl(url); + entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); + entry->setUsername(login); + entry->setPassword(password); + entry->setGroup(group); + + const QString host = QUrl(url).host(); + const QString submitHost = QUrl(submitUrl).host(); + BrowserEntryConfig config; + config.allow(host); + + if (!submitHost.isEmpty()) { + config.allow(submitHost); + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); +} + +void BrowserService::updateEntry(const QString& id, const QString& uuid, const QString& login, const QString& password, const QString& url) +{ + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "updateEntry", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, id), + Q_ARG(const QString&, uuid), + Q_ARG(const QString&, login), + Q_ARG(const QString&, password), + Q_ARG(const QString&, url)); + } + + Database* db = getDatabase(); + if (!db) { + return; + } + + Entry* entry = db->resolveEntry(Uuid::fromHex(uuid)); + if (!entry) { + return; + } + + QString username = entry->username(); + if (username.isEmpty()) { + return; + } + + if (username.compare(login, Qt::CaseSensitive) != 0 || entry->password().compare(password, Qt::CaseSensitive) != 0) { + QMessageBox::StandardButton dialogResult = QMessageBox::No; + if (!BrowserSettings::alwaysAllowUpdate()) { + dialogResult = QMessageBox::warning(0, tr("KeePassXC: Update Entry"), + tr("Do you want to update the information in %1 - %2?") + .arg(QUrl(url).host()).arg(username), + QMessageBox::Yes|QMessageBox::No); + } + + if (BrowserSettings::alwaysAllowUpdate() || dialogResult == QMessageBox::Yes) { + entry->beginUpdate(); + entry->setUsername(login); + entry->setPassword(password); + entry->endUpdate(); + } + } +} + +QList BrowserService::searchEntries(Database* db, const QString& hostname) +{ + QList entries; + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return entries; + } + + for (Entry* entry : EntrySearcher().search(hostname, rootGroup, Qt::CaseInsensitive)) { + QString title = entry->title(); + QString url = entry->url(); + + // Filter to match hostname in Title and Url fields + if ((!title.isEmpty() && hostname.contains(title)) + || (!url.isEmpty() && hostname.contains(url)) + || (matchUrlScheme(title) && hostname.endsWith(QUrl(title).host())) + || (matchUrlScheme(url) && hostname.endsWith(QUrl(url).host())) ) { + entries.append(entry); + } + } + + return entries; +} + +QList BrowserService::searchEntries(const QString& text) +{ + // Get the list of databases to search + QList databases; + if (BrowserSettings::searchInAllDatabases()) { + const int count = m_dbTabWidget->count(); + for (int i = 0; i < count; ++i) { + if (DatabaseWidget* dbWidget = qobject_cast(m_dbTabWidget->widget(i))) { + if (Database* db = dbWidget->database()) { + databases << db; + } + } + } + } else if (Database* db = getDatabase()) { + databases << db; + } + + // Search entries matching the hostname + QString hostname = QUrl(text).host(); + QList entries; + do { + for (Database* db : databases) { + entries << searchEntries(db, hostname); + } + } while (entries.isEmpty() && removeFirstDomain(hostname)); + + return entries; +} + +void BrowserService::removeSharedEncryptionKeys() +{ + if (!isDatabaseOpened()) { + QMessageBox::critical(0, tr("KeePassXC: Database locked!"), + tr("The active database is locked!\n" + "Please unlock the selected database or choose another one which is unlocked."), + QMessageBox::Ok); + return; + } + + Entry* entry = getConfigEntry(); + if (!entry) { + QMessageBox::information(0, tr("KeePassXC: Settings not available!"), + tr("The active database does not contain a settings entry."), + QMessageBox::Ok); + return; + } + + QStringList keysToRemove; + for (const QString& key : entry->attributes()->keys()) { + if (key.startsWith(ASSOCIATE_KEY_PREFIX)) { + keysToRemove << key; + } + } + + if (keysToRemove.isEmpty()) { + QMessageBox::information(0, tr("KeePassXC: No keys found"), + tr("No shared encryption keys found in KeePassXC Settings."), + QMessageBox::Ok); + return; + } + + entry->beginUpdate(); + for (const QString& key : keysToRemove) { + entry->attributes()->remove(key); + } + entry->endUpdate(); + + const int count = keysToRemove.count(); + QMessageBox::information(0, tr("KeePassXC: Removed keys from database"), + tr("Successfully removed %n encryption key(s) from KeePassXC settings.", "", count), + QMessageBox::Ok); + +} + +void BrowserService::removeStoredPermissions() +{ + if (!isDatabaseOpened()) { + QMessageBox::critical(0, tr("KeePassXC: Database locked!"), + tr("The active database is locked!\n" + "Please unlock the selected database or choose another one which is unlocked."), + QMessageBox::Ok); + return; + } + + Database* db = m_dbTabWidget->currentDatabaseWidget()->database(); + if (!db) { + return; + } + + QList entries = db->rootGroup()->entriesRecursive(); + + QProgressDialog progress(tr("Removing stored permissions…"), tr("Abort"), 0, entries.count()); + progress.setWindowModality(Qt::WindowModal); + + uint counter = 0; + for (Entry* entry : entries) { + if (progress.wasCanceled()) { + return; + } + + if (entry->attributes()->contains(KEEPASSXCBROWSER_NAME)) { + entry->beginUpdate(); + entry->attributes()->remove(KEEPASSXCBROWSER_NAME); + entry->endUpdate(); + ++counter; + } + progress.setValue(progress.value() + 1); + } + progress.reset(); + + if (counter > 0) { + QMessageBox::information(0, tr("KeePassXC: Removed permissions"), + tr("Successfully removed permissions from %n entry(s).", "", counter), + QMessageBox::Ok); + } else { + QMessageBox::information(0, tr("KeePassXC: No entry with permissions found!"), + tr("The active database does not contain an entry with permissions."), + QMessageBox::Ok); + } +} + +QList BrowserService::sortEntries(QList& pwEntries, const QString& host, const QString& entryUrl) +{ + QUrl url(entryUrl); + if (url.scheme().isEmpty()) { + url.setScheme("http"); + } + + const QString submitUrl = url.toString(QUrl::StripTrailingSlash); + const QString baseSubmitUrl = url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + + QMultiMap priorities; + for (const Entry* entry : pwEntries) { + priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry); + } + + QString field = BrowserSettings::sortByTitle() ? "Title" : "UserName"; + std::sort(pwEntries.begin(), pwEntries.end(), [&priorities, &field](const Entry* left, const Entry* right) { + int res = priorities.key(left) - priorities.key(right); + if (res == 0) { + return QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field)) < 0; + } + return res < 0; + }); + + return pwEntries; +} + +bool BrowserService::confirmEntries(QList& pwEntriesToConfirm, const QString& url, const QString& host, const QString& submitHost, const QString& realm) +{ + if (pwEntriesToConfirm.isEmpty() || m_dialogActive) { + return false; + } + + m_dialogActive = true; + BrowserAccessControlDialog accessControlDialog; + accessControlDialog.setUrl(url); + accessControlDialog.setItems(pwEntriesToConfirm); + + int res = accessControlDialog.exec(); + if (accessControlDialog.remember()) { + for (Entry* entry : pwEntriesToConfirm) { + BrowserEntryConfig config; + config.load(entry); + if (res == QDialog::Accepted) { + config.allow(host); + if (!submitHost.isEmpty() && host != submitHost) + config.allow(submitHost); + } else if (res == QDialog::Rejected) { + config.deny(host); + if (!submitHost.isEmpty() && host != submitHost) { + config.deny(submitHost); + } + } + if (!realm.isEmpty()) { + config.setRealm(realm); + } + config.save(entry); + } + } + + m_dialogActive = false; + if (res == QDialog::Accepted) { + return true; + } + + return false; +} + +QJsonObject BrowserService::prepareEntry(const Entry* entry) +{ + QJsonObject res; + res["login"] = entry->resolveMultiplePlaceholders(entry->username()); + res["password"] = entry->resolveMultiplePlaceholders(entry->password()); + res["name"] = entry->resolveMultiplePlaceholders(entry->title()); + res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuid().toHex()); + + if (BrowserSettings::supportKphFields()) { + const EntryAttributes* attr = entry->attributes(); + QJsonArray stringFields; + for (const QString& key : attr->keys()) { + if (key.startsWith(QLatin1String("KPH: "))) { + QJsonObject sField; + sField[key] = entry->resolveMultiplePlaceholders(attr->value(key)); + stringFields << sField; + } + } + res["stringFields"] = stringFields; + } + return res; +} + +BrowserService::Access BrowserService::checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm) +{ + BrowserEntryConfig config; + if (!config.load(entry)) { + return Unknown; + } + if ((config.isAllowed(host)) && (submitHost.isEmpty() || config.isAllowed(submitHost))) { + return Allowed; + } + if ((config.isDenied(host)) || (!submitHost.isEmpty() && config.isDenied(submitHost))) { + return Denied; + } + if (!realm.isEmpty() && config.realm() != realm) { + return Denied; + } + return Unknown; +} + +Group* BrowserService::findCreateAddEntryGroup() +{ + Database* db = getDatabase(); + if (!db) { + return nullptr; + } + + Group* rootGroup = db->rootGroup(); + if (!rootGroup) { + return nullptr; + } + + const QString groupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME); //TODO: setting to decide where new keys are created + + for (const Group* g : rootGroup->groupsRecursive(true)) { + if (g->name() == groupName) { + return db->resolveGroup(g->uuid()); + } + } + + Group* group = new Group(); + group->setUuid(Uuid::random()); + group->setName(groupName); + group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); + group->setParent(rootGroup); + return group; +} + +int BrowserService::sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const +{ + QUrl url(entry->url()); + if (url.scheme().isEmpty()) { + url.setScheme("http"); + } + const QString entryURL = url.toString(QUrl::StripTrailingSlash); + const QString baseEntryURL = url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (submitUrl == entryURL) { + return 100; + } + if (submitUrl.startsWith(entryURL) && entryURL != host && baseSubmitUrl != entryURL) { + return 90; + } + if (submitUrl.startsWith(baseEntryURL) && entryURL != host && baseSubmitUrl != baseEntryURL) { + return 80; + } + if (entryURL == host) { + return 70; + } + if (entryURL == baseSubmitUrl) { + return 60; + } + if (entryURL.startsWith(submitUrl)) { + return 50; + } + if (entryURL.startsWith(baseSubmitUrl) && baseSubmitUrl != host) { + return 40; + } + if (submitUrl.startsWith(entryURL)) { + return 30; + } + if (submitUrl.startsWith(baseEntryURL)) { + return 20; + } + if (entryURL.startsWith(host)) { + return 10; + } + if (host.startsWith(entryURL)) { + return 5; + } + return 0; +} + +bool BrowserService::matchUrlScheme(const QString& url) +{ + QUrl address(url); + return !address.scheme().isEmpty(); +} + +bool BrowserService::removeFirstDomain(QString& hostname) +{ + int pos = hostname.indexOf("."); + if (pos < 0) { + return false; + } + + // Don't remove the second-level domain if it's the only one + if (hostname.count(".") > 1) { + hostname = hostname.mid(pos + 1); + return !hostname.isEmpty(); + } + + // Nothing removed + return false; +} + +Database* BrowserService::getDatabase() +{ + if (DatabaseWidget* dbWidget = m_dbTabWidget->currentDatabaseWidget()) { + if (Database* db = dbWidget->database()) { + return db; + } + } + return nullptr; +} + +void BrowserService::databaseLocked(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + emit databaseLocked(); + } +} + +void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + emit databaseUnlocked(); + } +} + +void BrowserService::activateDatabaseChanged(DatabaseWidget* dbWidget) +{ + if (dbWidget) { + auto currentMode = dbWidget->currentMode(); + if (currentMode == DatabaseWidget::ViewMode || currentMode == DatabaseWidget::EditMode) { + emit databaseUnlocked(); + } else { + emit databaseLocked(); + } + } +} diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h new file mode 100644 index 000000000..859daee2a --- /dev/null +++ b/src/browser/BrowserService.h @@ -0,0 +1,82 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen +* 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 +* 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 BROWSERSERVICE_H +#define BROWSERSERVICE_H + +#include +#include +#include "gui/DatabaseTabWidget.h" +#include "core/Entry.h" + +enum { max_length = 16*1024 }; + +class BrowserService : public QObject +{ + Q_OBJECT + +public: + explicit BrowserService(DatabaseTabWidget* parent); + + bool isDatabaseOpened() const; + bool openDatabase(); + QString getDatabaseRootUuid(); + QString getDatabaseRecycleBinUuid(); + Entry* getConfigEntry(bool create = false); + QString getKey(const QString& id); + void addEntry(const QString& id, const QString& login, const QString& password, const QString& url, const QString& submitUrl, const QString& realm); + QList searchEntries(Database* db, const QString& hostname); + QList searchEntries(const QString& text); + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +public slots: + QJsonArray findMatchingEntries(const QString& id, const QString& url, const QString& submitUrl, const QString& realm); + QString storeKey(const QString& key); + void updateEntry(const QString& id, const QString& uuid, const QString& login, const QString& password, const QString& url); + void databaseLocked(DatabaseWidget* dbWidget); + void databaseUnlocked(DatabaseWidget* dbWidget); + void activateDatabaseChanged(DatabaseWidget* dbWidget); + void lockDatabase(); + +signals: + void databaseLocked(); + void databaseUnlocked(); + void databaseChanged(); + +private: + enum Access { Denied, Unknown, Allowed}; + +private: + QList sortEntries(QList& pwEntries, const QString& host, const QString& submitUrl); + bool confirmEntries(QList& pwEntriesToConfirm, const QString& url, const QString& host, const QString& submitHost, const QString& realm); + QJsonObject prepareEntry(const Entry* entry); + Access checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm); + Group* findCreateAddEntryGroup(); + int sortPriority(const Entry* entry, const QString &host, const QString& submitUrl, const QString& baseSubmitUrl) const; + bool matchUrlScheme(const QString& url); + bool removeFirstDomain(QString& hostname); + Database* getDatabase(); + +private: + DatabaseTabWidget* const m_dbTabWidget; + bool m_dialogActive; +}; + +#endif // BROWSERSERVICE_H diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp new file mode 100755 index 000000000..d15f57d46 --- /dev/null +++ b/src/browser/BrowserSettings.cpp @@ -0,0 +1,377 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "BrowserSettings.h" +#include "core/Config.h" + +PasswordGenerator BrowserSettings::m_passwordGenerator; +PassphraseGenerator BrowserSettings::m_passPhraseGenerator; +HostInstaller BrowserSettings::m_hostInstaller; + +bool BrowserSettings::isEnabled() +{ + return config()->get("Browser/Enabled", false).toBool(); +} + +void BrowserSettings::setEnabled(bool enabled) +{ + config()->set("Browser/Enabled", enabled); +} + +bool BrowserSettings::showNotification() +{ + return config()->get("Browser/ShowNotification", true).toBool(); +} + +void BrowserSettings::setShowNotification(bool showNotification) +{ + config()->set("Browser/ShowNotification", showNotification); +} + +bool BrowserSettings::bestMatchOnly() +{ + return config()->get("Browser/BestMatchOnly", false).toBool(); +} + +void BrowserSettings::setBestMatchOnly(bool bestMatchOnly) +{ + config()->set("Browser/BestMatchOnly", bestMatchOnly); +} + +bool BrowserSettings::unlockDatabase() +{ + return config()->get("Browser/UnlockDatabase", true).toBool(); +} + +void BrowserSettings::setUnlockDatabase(bool unlockDatabase) +{ + config()->set("Browser/UnlockDatabase", unlockDatabase); +} + +bool BrowserSettings::matchUrlScheme() +{ + return config()->get("Browser/MatchUrlScheme", true).toBool(); +} + +void BrowserSettings::setMatchUrlScheme(bool matchUrlScheme) +{ + config()->set("Browser/MatchUrlScheme", matchUrlScheme); +} + +bool BrowserSettings::sortByUsername() +{ + return config()->get("Browser/SortByUsername", false).toBool(); +} + +void BrowserSettings::setSortByUsername(bool sortByUsername) +{ + config()->set("Browser/SortByUsername", sortByUsername); +} + +bool BrowserSettings::sortByTitle() +{ + return !sortByUsername(); +} + +void BrowserSettings::setSortByTitle(bool sortByUsertitle) +{ + setSortByUsername(!sortByUsertitle); +} + +bool BrowserSettings::alwaysAllowAccess() +{ + return config()->get("Browser/AlwaysAllowAccess", false).toBool(); +} + +void BrowserSettings::setAlwaysAllowAccess(bool alwaysAllowAccess) +{ + config()->set("Browser/AlwaysAllowAccess", alwaysAllowAccess); +} + +bool BrowserSettings::alwaysAllowUpdate() +{ + return config()->get("Browser/AlwaysAllowUpdate", false).toBool(); +} + +void BrowserSettings::setAlwaysAllowUpdate(bool alwaysAllowUpdate) +{ + config()->set("Browser/AlwaysAllowUpdate", alwaysAllowUpdate); +} + +bool BrowserSettings::searchInAllDatabases() +{ + return config()->get("Browser/SearchInAllDatabases", false).toBool(); +} + +void BrowserSettings::setSearchInAllDatabases(bool searchInAllDatabases) +{ + config()->set("Browser/SearchInAllDatabases", searchInAllDatabases); +} + +bool BrowserSettings::supportKphFields() +{ + return config()->get("Browser/SupportKphFields", true).toBool(); +} + +void BrowserSettings::setSupportKphFields(bool supportKphFields) +{ + config()->set("Browser/SupportKphFields", supportKphFields); +} + +bool BrowserSettings::supportBrowserProxy() +{ + return config()->get("Browser/SupportBrowserProxy", true).toBool(); +} + +void BrowserSettings::setSupportBrowserProxy(bool enabled) +{ + config()->set("Browser/SupportBrowserProxy", enabled); +} + +bool BrowserSettings::useCustomProxy() +{ + return config()->get("Browser/UseCustomProxy", false).toBool(); +} + +void BrowserSettings::setUseCustomProxy(bool enabled) +{ + config()->set("Browser/UseCustomProxy", enabled); +} + +QString BrowserSettings::customProxyLocation() +{ + return config()->get("Browser/CustomProxyLocation", " ").toString(); +} + +void BrowserSettings::setCustomProxyLocation(QString location) +{ + config()->set("Browser/CustomProxyLocation", location); +} + +bool BrowserSettings::updateBinaryPath() +{ + return config()->get("Browser/UpdateBinaryPath", false).toBool(); +} + +void BrowserSettings::setUpdateBinaryPath(bool enabled) +{ + config()->set("Browser/UpdateBinaryPath", enabled); +} + +bool BrowserSettings::chromeSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROME); +} + +void BrowserSettings::setChromeSupport(bool enabled) { + m_hostInstaller.installBrowser(HostInstaller::SupportedBrowsers::CHROME, enabled, supportBrowserProxy(), customProxyLocation()); +} + +bool BrowserSettings::chromiumSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::CHROMIUM); +} + +void BrowserSettings::setChromiumSupport(bool enabled) { + m_hostInstaller.installBrowser(HostInstaller::SupportedBrowsers::CHROMIUM, enabled, supportBrowserProxy(), customProxyLocation()); +} + +bool BrowserSettings::firefoxSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::FIREFOX); +} + +void BrowserSettings::setFirefoxSupport(bool enabled) { + m_hostInstaller.installBrowser(HostInstaller::SupportedBrowsers::FIREFOX, enabled, supportBrowserProxy(), customProxyLocation()); +} + +bool BrowserSettings::vivaldiSupport() { + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::VIVALDI); +} + +void BrowserSettings::setVivaldiSupport(bool enabled) { + m_hostInstaller.installBrowser(HostInstaller::SupportedBrowsers::VIVALDI, enabled, supportBrowserProxy(), customProxyLocation()); +} + +bool BrowserSettings::passwordUseNumbers() +{ + return config()->get("generator/Numbers", true).toBool(); +} + +void BrowserSettings::setPasswordUseNumbers(bool useNumbers) +{ + config()->set("generator/Numbers", useNumbers); +} + +bool BrowserSettings::passwordUseLowercase() +{ + return config()->get("generator/LowerCase", true).toBool(); +} + +void BrowserSettings::setPasswordUseLowercase(bool useLowercase) +{ + config()->set("generator/LowerCase", useLowercase); +} + +bool BrowserSettings::passwordUseUppercase() +{ + return config()->get("generator/UpperCase", true).toBool(); +} + +void BrowserSettings::setPasswordUseUppercase(bool useUppercase) +{ + config()->set("generator/UpperCase", useUppercase); +} + +bool BrowserSettings::passwordUseSpecial() +{ + return config()->get("generator/SpecialChars", false).toBool(); +} + +void BrowserSettings::setPasswordUseSpecial(bool useSpecial) +{ + config()->set("generator/SpecialChars", useSpecial); +} + +bool BrowserSettings::passwordUseEASCII() +{ + return config()->get("generator/EASCII", false).toBool(); +} + +void BrowserSettings::setPasswordUseEASCII(bool useEASCII) +{ + config()->set("generator/EASCII", useEASCII); +} + +int BrowserSettings::passPhraseWordCount() +{ + return config()->get("generator/WordCount", 6).toInt(); +} + +void BrowserSettings::setPassPhraseWordCount(int wordCount) +{ + config()->set("generator/WordCount", wordCount); +} + +QString BrowserSettings::passPhraseWordSeparator() +{ + return config()->get("generator/WordSeparator", " ").toString(); +} + +void BrowserSettings::setPassPhraseWordSeparator(QString separator) +{ + config()->set("generator/WordSeparator", separator); +} + +int BrowserSettings::generatorType() +{ + return config()->get("generator/Type", 0).toInt(); +} + +void BrowserSettings::setGeneratorType(int type) +{ + config()->set("generator/Type", type); +} + +bool BrowserSettings::passwordEveryGroup() +{ + return config()->get("generator/EnsureEvery", true).toBool(); +} + +void BrowserSettings::setPasswordEveryGroup(bool everyGroup) +{ + config()->get("generator/EnsureEvery", everyGroup); +} + +bool BrowserSettings::passwordExcludeAlike() +{ + return config()->get("generator/ExcludeAlike", true).toBool(); +} + +void BrowserSettings::setPasswordExcludeAlike(bool excludeAlike) +{ + config()->set("generator/ExcludeAlike", excludeAlike); +} + +int BrowserSettings::passwordLength() +{ + return config()->get("generator/Length", 20).toInt(); +} + +void BrowserSettings::setPasswordLength(int length) +{ + config()->set("generator/Length", length); + m_passwordGenerator.setLength(length); +} + +PasswordGenerator::CharClasses BrowserSettings::passwordCharClasses() +{ + PasswordGenerator::CharClasses classes; + if (passwordUseLowercase()) { + classes |= PasswordGenerator::LowerLetters; + } + if (passwordUseUppercase()) { + classes |= PasswordGenerator::UpperLetters; + } + if (passwordUseNumbers()) { + classes |= PasswordGenerator::Numbers; + } + if (passwordUseSpecial()) { + classes |= PasswordGenerator::SpecialCharacters; + } + if (passwordUseEASCII()) { + classes |= PasswordGenerator::EASCII; + } + return classes; +} + +PasswordGenerator::GeneratorFlags BrowserSettings::passwordGeneratorFlags() +{ + PasswordGenerator::GeneratorFlags flags; + if (passwordExcludeAlike()) { + flags |= PasswordGenerator::ExcludeLookAlike; + } + if (passwordEveryGroup()) { + flags |= PasswordGenerator::CharFromEveryGroup; + } + return flags; +} + +QString BrowserSettings::generatePassword() +{ + if (generatorType() == 0) { + m_passwordGenerator.setLength(passwordLength()); + m_passwordGenerator.setCharClasses(passwordCharClasses()); + m_passwordGenerator.setFlags(passwordGeneratorFlags()); + return m_passwordGenerator.generatePassword(); + } else { + m_passPhraseGenerator.setDefaultWordList(); + m_passPhraseGenerator.setWordCount(passPhraseWordCount()); + m_passPhraseGenerator.setWordSeparator(passPhraseWordSeparator()); + return m_passPhraseGenerator.generatePassphrase(); + } +} + +int BrowserSettings::getbits() +{ + return m_passwordGenerator.getbits(); +} + +void BrowserSettings::updateBinaryPaths(QString customProxyLocation) +{ + bool isProxy = supportBrowserProxy(); + m_hostInstaller.updateBinaryPaths(isProxy, customProxyLocation); +} diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h new file mode 100755 index 000000000..8d08eef71 --- /dev/null +++ b/src/browser/BrowserSettings.h @@ -0,0 +1,105 @@ +/* +* Copyright (C) 2013 Francois Ferrand +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 BROWSERSETTINGS_H +#define BROWSERSETTINGS_H + +#include "core/PasswordGenerator.h" +#include "core/PassphraseGenerator.h" +#include "HostInstaller.h" + +class BrowserSettings +{ +public: + static bool isEnabled(); + static void setEnabled(bool enabled); + + static bool showNotification(); //TODO!! + static void setShowNotification(bool showNotification); + static bool bestMatchOnly(); //TODO!! + static void setBestMatchOnly(bool bestMatchOnly); + static bool unlockDatabase(); //TODO!! + static void setUnlockDatabase(bool unlockDatabase); + static bool matchUrlScheme(); + static void setMatchUrlScheme(bool matchUrlScheme); + static bool sortByUsername(); + static void setSortByUsername(bool sortByUsername = true); + static bool sortByTitle(); + static void setSortByTitle(bool sortByUsertitle = true); + static bool alwaysAllowAccess(); + static void setAlwaysAllowAccess(bool alwaysAllowAccess); + static bool alwaysAllowUpdate(); + static void setAlwaysAllowUpdate(bool alwaysAllowUpdate); + static bool searchInAllDatabases();//TODO!! + static void setSearchInAllDatabases(bool searchInAllDatabases); + static bool supportKphFields(); + static void setSupportKphFields(bool supportKphFields); + + static bool supportBrowserProxy(); + static void setSupportBrowserProxy(bool enabled); + static bool useCustomProxy(); + static void setUseCustomProxy(bool enabled); + static QString customProxyLocation(); + static void setCustomProxyLocation(QString location); + static bool updateBinaryPath(); + static void setUpdateBinaryPath(bool enabled); + static bool chromeSupport(); + static void setChromeSupport(bool enabled); + static bool chromiumSupport(); + static void setChromiumSupport(bool enabled); + static bool firefoxSupport(); + static void setFirefoxSupport(bool enabled); + static bool vivaldiSupport(); + static void setVivaldiSupport(bool enabled); + + static bool passwordUseNumbers(); + static void setPasswordUseNumbers(bool useNumbers); + static bool passwordUseLowercase(); + static void setPasswordUseLowercase(bool useLowercase); + static bool passwordUseUppercase(); + static void setPasswordUseUppercase(bool useUppercase); + static bool passwordUseSpecial(); + static void setPasswordUseSpecial(bool useSpecial); + static bool passwordUseEASCII(); + static void setPasswordUseEASCII(bool useEASCII); + static int passPhraseWordCount(); + static void setPassPhraseWordCount(int wordCount); + static QString passPhraseWordSeparator(); + static void setPassPhraseWordSeparator(QString separator); + static int generatorType(); + static void setGeneratorType(int type); + static bool passwordEveryGroup(); + static void setPasswordEveryGroup(bool everyGroup); + static bool passwordExcludeAlike(); + static void setPasswordExcludeAlike(bool excludeAlike); + static int passwordLength(); + static void setPasswordLength(int length); + static PasswordGenerator::CharClasses passwordCharClasses(); + static PasswordGenerator::GeneratorFlags passwordGeneratorFlags(); + static QString generatePassword(); + static int getbits(); + static void updateBinaryPaths(QString customProxyLocation = QString()); + +private: + static PasswordGenerator m_passwordGenerator; + static PassphraseGenerator m_passPhraseGenerator; + static HostInstaller m_hostInstaller; +}; + +#endif // BROWSERSETTINGS_H diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt new file mode 100755 index 000000000..61215c181 --- /dev/null +++ b/src/browser/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (C) 2017 Sami Vänttinen +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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 . + +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 + BrowserAction.cpp + BrowserClients.cpp + BrowserEntryConfig.cpp + BrowserOptionDialog.cpp + BrowserService.cpp + BrowserSettings.cpp + HostInstaller.cpp + NativeMessagingBase.cpp + NativeMessagingHost.cpp + Variant.cpp + ) + + add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES}) + target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network sodium) +endif() diff --git a/src/browser/HostInstaller.cpp b/src/browser/HostInstaller.cpp new file mode 100644 index 000000000..bbb5fb6f5 --- /dev/null +++ b/src/browser/HostInstaller.cpp @@ -0,0 +1,230 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "HostInstaller.h" +#include +#include +#include +#include +#include +#include +#include + +const QString HostInstaller::HOST_NAME = "org.keepassxc.keepassxc_browser"; +const QStringList HostInstaller::ALLOWED_ORIGINS = QStringList() + << "chrome-extension://iopaggbpplllidnfmcghoonnokmjoicf/" + << "chrome-extension://fhakpkpdnjecjfceboihdjpfmgajebii/" + << "chrome-extension://jaikbblhommnkeialomogohhdlndpfbi/"; + +const QStringList HostInstaller::ALLOWED_EXTENSIONS = QStringList() + << "keepassxc-browser@keepassxc.org"; + +#if defined(Q_OS_OSX) + const QString HostInstaller::TARGET_DIR_CHROME = "/Library/Application Support/Google/Chrome/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_CHROMIUM = "/Library/Application Support/Chromium/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_FIREFOX = "/Library/Application Support/Mozilla/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_VIVALDI = "/Library/Application Support/Vivaldi/NativeMessagingHosts"; +#elif defined(Q_OS_LINUX) + const QString HostInstaller::TARGET_DIR_CHROME = "/.config/google-chrome/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_CHROMIUM = "/.config/chromium/NativeMessagingHosts"; + const QString HostInstaller::TARGET_DIR_FIREFOX = "/.mozilla/native-messaging-hosts"; + const QString HostInstaller::TARGET_DIR_VIVALDI = "/.config/vivaldi/NativeMessagingHosts"; +#elif defined(Q_OS_WIN) + const QString HostInstaller::TARGET_DIR_CHROME = "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; + const QString HostInstaller::TARGET_DIR_CHROMIUM = "HKEY_CURRENT_USER\\Software\\Chromium\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; + const QString HostInstaller::TARGET_DIR_FIREFOX = "HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; + const QString HostInstaller::TARGET_DIR_VIVALDI = "HKEY_CURRENT_USER\\Software\\Vivaldi\\NativeMessagingHosts\\" + HostInstaller::HOST_NAME; +#endif + +HostInstaller::HostInstaller() +{ + +} + +bool HostInstaller::checkIfInstalled(SupportedBrowsers browser) +{ + QString fileName = getPath(browser); +#ifdef Q_OS_WIN + QSettings settings(getTargetPath(browser), QSettings::NativeFormat); + return registryEntryFound(settings); +#else + return QFile::exists(fileName); +#endif +} + +void HostInstaller::installBrowser(SupportedBrowsers browser, const bool& enabled, const bool& proxy, const QString& location) +{ + if (enabled) { + #ifdef Q_OS_WIN + // Create a registry key + QSettings settings(getTargetPath(browser), QSettings::NativeFormat); + if (!registryEntryFound(settings)) { + settings.setValue("Default", getPath(browser)); + } + #endif + // Always create the script file + QJsonObject script = constructFile(browser, proxy, location); + if (!saveFile(browser, script)) { + QMessageBox::critical(0, tr("KeePassXC: Cannot save file!"), + tr("Cannot save the native messaging script file."), QMessageBox::Ok); + } + } else { + // Remove the script file + QString fileName = getPath(browser); + QFile::remove(fileName); + #ifdef Q_OS_WIN + // Remove the registry entry + QSettings settings(getTargetPath(browser), QSettings::NativeFormat); + if (registryEntryFound(settings)) { + settings.remove("Default"); + } + #endif + } +} + +void HostInstaller::updateBinaryPaths(const bool& proxy, const QString& location) +{ + for (int i = 0; i < 4; ++i) { + if (checkIfInstalled(static_cast(i))) { + installBrowser(static_cast(i), true, proxy, location); + } + } +} + +QString HostInstaller::getTargetPath(SupportedBrowsers browser) const +{ + switch (browser) { + case SupportedBrowsers::CHROME: return HostInstaller::TARGET_DIR_CHROME; + case SupportedBrowsers::CHROMIUM: return HostInstaller::TARGET_DIR_CHROMIUM; + case SupportedBrowsers::FIREFOX: return HostInstaller::TARGET_DIR_FIREFOX; + case SupportedBrowsers::VIVALDI: return HostInstaller::TARGET_DIR_VIVALDI; + default: return QString(); + } +} + +QString HostInstaller::getBrowserName(SupportedBrowsers browser) const +{ + switch (browser) { + case SupportedBrowsers::CHROME: return "chrome"; + case SupportedBrowsers::CHROMIUM: return "chromium"; + case SupportedBrowsers::FIREFOX: return "firefox"; + case SupportedBrowsers::VIVALDI: return "vivaldi"; + default: return QString(); + } +} + +QString HostInstaller::getPath(SupportedBrowsers browser) const +{ +#ifdef Q_OS_WIN + // If portable settings file exists save the JSON scripts to application folder + QString userPath; + QString portablePath = QCoreApplication::applicationDirPath() + "/keepassxc.ini"; + if (QFile::exists(portablePath)) { + userPath = QCoreApplication::applicationDirPath(); + } else { + userPath = QDir::fromNativeSeparators(QStandardPaths::writableLocation(QStandardPaths::DataLocation)); + } + + QString winPath = QString("%1/%2_%3.json").arg(userPath, HostInstaller::HOST_NAME, getBrowserName(browser)); + winPath.replace("/","\\"); + return winPath; +#else + QString path = getTargetPath(browser); + return QString("%1%2/%3.json").arg(QDir::homePath(), path, HostInstaller::HOST_NAME); +#endif +} + +QString HostInstaller::getInstallDir(SupportedBrowsers browser) const +{ + QString path = getTargetPath(browser); +#ifdef Q_OS_WIN + return QCoreApplication::applicationDirPath(); +#else + return QString("%1%2").arg(QDir::homePath(), path); +#endif +} + +QJsonObject HostInstaller::constructFile(SupportedBrowsers browser, const bool& proxy, const QString& location) +{ + QString path; + if (proxy) { + if (!location.isEmpty()) { + path = location; + } else { + path = QFileInfo(QCoreApplication::applicationFilePath()).absolutePath(); + path.append("/keepassxc-proxy"); +#ifdef Q_OS_WIN + path.append(".exe"); +#endif + } + } else { + path = QFileInfo(QCoreApplication::applicationFilePath()).absoluteFilePath(); + } +#ifdef Q_OS_WIN + path.replace("/","\\"); +#endif + + QJsonObject script; + script["name"] = HostInstaller::HOST_NAME; + script["description"] = "KeePassXC integration with native messaging support"; + script["path"] = path; + script["type"] = "stdio"; + + QJsonArray arr; + if (browser == SupportedBrowsers::FIREFOX) { + for (const QString extension : HostInstaller::ALLOWED_EXTENSIONS) { + arr.append(extension); + } + script["allowed_extensions"] = arr; + } else { + for (const QString origin : HostInstaller::ALLOWED_ORIGINS) { + arr.append(origin); + } + script["allowed_origins"] = arr; + } + + return script; +} + +bool HostInstaller::registryEntryFound(const QSettings& settings) +{ + return !settings.value("Default").isNull(); +} + +bool HostInstaller::saveFile(SupportedBrowsers browser, const QJsonObject& script) +{ + QString path = getPath(browser); + QString installDir = getInstallDir(browser); + QDir dir(installDir); + if (!dir.exists()) { + QDir().mkpath(installDir); + } + + QFile scriptFile(path); + if (!scriptFile.open(QIODevice::WriteOnly)) { + return false; + } + + QJsonDocument doc(script); + qint64 bytesWritten = scriptFile.write(doc.toJson()); + if (bytesWritten < 0) { + return false; + } + + return true; +} diff --git a/src/browser/HostInstaller.h b/src/browser/HostInstaller.h new file mode 100644 index 000000000..c3fc85620 --- /dev/null +++ b/src/browser/HostInstaller.h @@ -0,0 +1,63 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 HOSTINSTALLER_H +#define HOSTINSTALLER_H + +#include +#include +#include + +class HostInstaller : public QObject +{ + Q_OBJECT + +public: + enum SupportedBrowsers : int { + CHROME = 0, + CHROMIUM = 1, + FIREFOX = 2, + VIVALDI = 3 + }; + +public: + HostInstaller(); + bool checkIfInstalled(SupportedBrowsers browser); + void installBrowser(SupportedBrowsers browser, const bool& enabled, const bool& proxy = false, const QString& location = ""); + void updateBinaryPaths(const bool& proxy, const QString& location = ""); + +private: + QString getTargetPath(SupportedBrowsers browser) const; + QString getBrowserName(SupportedBrowsers browser) const; + QString getPath(SupportedBrowsers browser) const; + QString getInstallDir(SupportedBrowsers browser) const; + QJsonObject constructFile(SupportedBrowsers browser, const bool& proxy, const QString& location); + bool registryEntryFound(const QSettings& settings); + bool saveFile(SupportedBrowsers browser, const QJsonObject& script); + +private: + static const QString HOST_NAME; + static const QStringList ALLOWED_EXTENSIONS; + static const QStringList ALLOWED_ORIGINS; + static const QString TARGET_DIR_CHROME; + static const QString TARGET_DIR_CHROMIUM; + static const QString TARGET_DIR_FIREFOX; + static const QString TARGET_DIR_VIVALDI; +}; + +#endif // HOSTINSTALLER_H diff --git a/src/browser/NativeMessagingBase.cpp b/src/browser/NativeMessagingBase.cpp new file mode 100644 index 000000000..fb4959688 --- /dev/null +++ b/src/browser/NativeMessagingBase.cpp @@ -0,0 +1,141 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "NativeMessagingBase.h" +#include + +#if defined(Q_OS_UNIX) && !defined(Q_OS_LINUX) +#include +#include +#include +#include +#endif + +#ifdef Q_OS_LINUX +#include +#include +#endif + +#ifdef Q_OS_WIN +#include +#include +#endif + +NativeMessagingBase::NativeMessagingBase() +{ +#ifdef Q_OS_WIN + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#else + m_notifier.reset(new QSocketNotifier(fileno(stdin), QSocketNotifier::Read, this)); + connect(m_notifier.data(), SIGNAL(activated(int)), this, SLOT(newNativeMessage())); +#endif +} + +void NativeMessagingBase::newNativeMessage() +{ +#if defined(Q_OS_UNIX) && !defined(Q_OS_LINUX) + struct kevent ev[1]; + struct timespec ts = { 5, 0 }; + + int fd = kqueue(); + if (fd == -1) { + m_notifier->setEnabled(false); + return; + } + + EV_SET(ev, fileno(stdin), EVFILT_READ, EV_ADD, 0, 0, nullptr); + if (kevent(fd, ev, 1, nullptr, 0, &ts) == -1) { + m_notifier->setEnabled(false); + return; + } + + int ret = kevent(fd, NULL, 0, ev, 1, &ts); + if (ret < 1) { + m_notifier->setEnabled(false); + ::close(fd); + return; + } +#elif defined(Q_OS_LINUX) + int fd = epoll_create(5); + struct epoll_event event; + event.events = EPOLLIN; + event.data.fd = 0; + if (epoll_ctl(fd, EPOLL_CTL_ADD, 0, &event) != 0) { + m_notifier->setEnabled(false); + return; + } + + if (epoll_wait(fd, &event, 1, 5000) < 1) { + m_notifier->setEnabled(false); + ::close(fd); + return; + } +#endif + readLength(); +#ifndef Q_OS_WIN + ::close(fd); +#endif +} + +void NativeMessagingBase::readNativeMessages() +{ +#ifdef Q_OS_WIN + quint32 length = 0; + while (m_running.load() && !std::cin.eof()) { + length = 0; + std::cin.read(reinterpret_cast(&length), 4); + readStdIn(length); + QThread::msleep(1); + } +#endif +} + +QString NativeMessagingBase::jsonToString(const QJsonObject& json) const +{ + return QString(QJsonDocument(json).toJson(QJsonDocument::Compact)); +} + +void NativeMessagingBase::sendReply(const QJsonObject& json) +{ + if (!json.isEmpty()) { + sendReply(jsonToString(json)); + } +} + +void NativeMessagingBase::sendReply(const QString& reply) +{ + if (!reply.isEmpty()) { + uint len = reply.length(); + std::cout << char(((len>>0) & 0xFF)) << char(((len>>8) & 0xFF)) << char(((len>>16) & 0xFF)) << char(((len>>24) & 0xFF)); + std::cout << reply.toStdString() << std::flush; + } +} + +QString NativeMessagingBase::getLocalServerPath() const +{ +#if defined(Q_OS_WIN) + return QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/kpxc_server"; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_MAC) + // Use XDG_RUNTIME_DIR instead of /tmp/ if it's available + QString path = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + "/kpxc_server"; + return path.isEmpty() ? "/tmp/kpxc_server" : path; +#else // Q_OS_MAC and others + return "/tmp/kpxc_server"; +#endif +} diff --git a/src/browser/NativeMessagingBase.h b/src/browser/NativeMessagingBase.h new file mode 100644 index 000000000..4253b7585 --- /dev/null +++ b/src/browser/NativeMessagingBase.h @@ -0,0 +1,61 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 NATIVEMESSAGINGBASE_H +#define NATIVEMESSAGINGBASE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class NativeMessagingBase : public QObject +{ + Q_OBJECT + +public: + explicit NativeMessagingBase(); + ~NativeMessagingBase() = default; + +protected slots: + void newNativeMessage(); + +protected: + virtual void readLength() = 0; + virtual void readStdIn(const quint32 length) = 0; + void readNativeMessages(); + QString jsonToString(const QJsonObject& json) const; + void sendReply(const QJsonObject& json); + void sendReply(const QString& reply); + QString getLocalServerPath() const; + +protected: + QAtomicInteger m_running; + QSharedPointer m_notifier; + QFuture m_future; +}; + +#endif // NATIVEMESSAGINGBASE_H diff --git a/src/browser/NativeMessagingHost.cpp b/src/browser/NativeMessagingHost.cpp new file mode 100755 index 000000000..1dcd24cb1 --- /dev/null +++ b/src/browser/NativeMessagingHost.cpp @@ -0,0 +1,200 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 +#include "sodium.h" +#include "NativeMessagingHost.h" +#include "BrowserSettings.h" + +NativeMessagingHost::NativeMessagingHost(DatabaseTabWidget* parent) : + NativeMessagingBase(), + m_mutex(QMutex::Recursive), + m_browserClients(m_browserService), + m_browserService(parent) +{ + m_localServer.reset(new QLocalServer(this)); + m_localServer->setSocketOptions(QLocalServer::UserAccessOption); + m_running.store(false); + + if (BrowserSettings::isEnabled() && !m_running) { + run(); + } + + connect(&m_browserService, SIGNAL(databaseLocked()), this, SLOT(databaseLocked())); + connect(&m_browserService, SIGNAL(databaseUnlocked()), this, SLOT(databaseUnlocked())); +} + +NativeMessagingHost::~NativeMessagingHost() +{ + stop(); +} + +int NativeMessagingHost::init() +{ + QMutexLocker locker(&m_mutex); + return sodium_init(); +} + +void NativeMessagingHost::run() +{ + QMutexLocker locker(&m_mutex); + if (!m_running.load() && init() == -1) { + return; + } + + // Update KeePassXC/keepassxc-proxy binary paths to Native Messaging scripts + if (BrowserSettings::updateBinaryPath()) { + BrowserSettings::updateBinaryPaths(BrowserSettings::useCustomProxy() ? BrowserSettings::customProxyLocation() : ""); + } + + m_running.store(true); +#ifdef Q_OS_WIN + m_future = QtConcurrent::run(this, static_cast(&NativeMessagingHost::readNativeMessages)); +#endif + + if (BrowserSettings::supportBrowserProxy()) { + QString serverPath = getLocalServerPath(); + QFile::remove(serverPath); + m_localServer->listen(serverPath); + connect(m_localServer.data(), SIGNAL(newConnection()), this, SLOT(newLocalConnection())); + } else { + m_localServer->close(); + } +} + +void NativeMessagingHost::stop() +{ + databaseLocked(); + QMutexLocker locker(&m_mutex); + m_socketList.clear(); + m_running.testAndSetOrdered(true, false); + m_future.waitForFinished(); + m_localServer->close(); +} + +void NativeMessagingHost::readLength() +{ + quint32 length = 0; + std::cin.read(reinterpret_cast(&length), 4); + if (!std::cin.eof() && length > 0) + { + readStdIn(length); + } +} + +void NativeMessagingHost::readStdIn(const quint32 length) +{ + if (length > 0) { + QByteArray arr; + arr.reserve(length); + + for (quint32 i = 0; i < length; ++i) { + arr.append(getchar()); + } + + if (arr.length() > 0) { + QMutexLocker locker(&m_mutex); + sendReply(m_browserClients.readResponse(arr)); + } + } +} + +void NativeMessagingHost::newLocalConnection() +{ + QLocalSocket* socket = m_localServer->nextPendingConnection(); + connect(socket, SIGNAL(readyRead()), this, SLOT(newLocalMessage())); + connect(socket, SIGNAL(disconnected()), this, SLOT(disconnectSocket())); +} + +void NativeMessagingHost::newLocalMessage() +{ + QLocalSocket* socket = qobject_cast(QObject::sender()); + + if (!socket || socket->bytesAvailable() <= 0) { + return; + } + + QByteArray arr = socket->readAll(); + if (arr.isEmpty()) { + return; + } + + QMutexLocker locker(&m_mutex); + if (!m_socketList.contains(socket)) { + m_socketList.push_back(socket); + } + + QString reply = jsonToString(m_browserClients.readResponse(arr)); + if (socket && socket->isValid() && socket->state() == QLocalSocket::ConnectedState) { + QByteArray arr = reply.toUtf8(); + socket->write(arr.constData(), arr.length()); + socket->flush(); + } +} + +void NativeMessagingHost::sendReplyToAllClients(const QJsonObject& json) +{ + QString reply = jsonToString(json); + QMutexLocker locker(&m_mutex); + for (const auto socket : m_socketList) { + if (socket && socket->isValid() && socket->state() == QLocalSocket::ConnectedState) { + QByteArray arr = reply.toUtf8(); + socket->write(arr.constData(), arr.length()); + socket->flush(); + } + } +} + +void NativeMessagingHost::disconnectSocket() +{ + QLocalSocket* socket(qobject_cast(QObject::sender())); + QMutexLocker locker(&m_mutex); + for (auto s : m_socketList) { + if (s == socket) { + m_socketList.removeOne(s); + } + } +} + +void NativeMessagingHost::removeSharedEncryptionKeys() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeSharedEncryptionKeys(); +} + +void NativeMessagingHost::removeStoredPermissions() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeStoredPermissions(); +} + +void NativeMessagingHost::databaseLocked() +{ + QJsonObject response; + response["action"] = "database-locked"; + sendReplyToAllClients(response); +} + +void NativeMessagingHost::databaseUnlocked() +{ + QJsonObject response; + response["action"] = "database-unlocked"; + sendReplyToAllClients(response); +} diff --git a/src/browser/NativeMessagingHost.h b/src/browser/NativeMessagingHost.h new file mode 100755 index 000000000..80825237b --- /dev/null +++ b/src/browser/NativeMessagingHost.h @@ -0,0 +1,67 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 NATIVEMESSAGINGHOST_H +#define NATIVEMESSAGINGHOST_H + +#include "NativeMessagingBase.h" +#include "BrowserClients.h" +#include "BrowserService.h" +#include "gui/DatabaseTabWidget.h" + +class NativeMessagingHost : public NativeMessagingBase +{ + Q_OBJECT + + typedef QList SocketList; + +public: + explicit NativeMessagingHost(DatabaseTabWidget* parent = 0); + ~NativeMessagingHost(); + int init(); + void run(); + void stop(); + +public slots: + void removeSharedEncryptionKeys(); + void removeStoredPermissions(); + +signals: + void quit(); + +private: + void readLength(); + void readStdIn(const quint32 length); + void sendReplyToAllClients(const QJsonObject& json); + +private slots: + void databaseLocked(); + void databaseUnlocked(); + void newLocalConnection(); + void newLocalMessage(); + void disconnectSocket(); + +private: + QMutex m_mutex; + BrowserClients m_browserClients; + BrowserService m_browserService; + QSharedPointer m_localServer; + SocketList m_socketList; +}; + +#endif // NATIVEMESSAGINGHOST_H diff --git a/src/browser/Variant.cpp b/src/browser/Variant.cpp new file mode 100644 index 000000000..2d42ac4ec --- /dev/null +++ b/src/browser/Variant.cpp @@ -0,0 +1,37 @@ +/* +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "Variant.h" + +QVariantMap qo2qv(const QObject* object, const QStringList& ignoredProperties) +{ + QVariantMap result; + const QMetaObject* metaobject = object->metaObject(); + int count = metaobject->propertyCount(); + for (int i = 0; i < count; ++i) { + QMetaProperty metaproperty = metaobject->property(i); + const char* name = metaproperty.name(); + + if (ignoredProperties.contains(QLatin1String(name)) || (!metaproperty.isReadable())) { + continue; + } + + QVariant value = object->property(name); + result[QLatin1String(name)] = value; + } + return result; +} diff --git a/src/browser/Variant.h b/src/browser/Variant.h new file mode 100644 index 000000000..e467b4211 --- /dev/null +++ b/src/browser/Variant.h @@ -0,0 +1,25 @@ +/* +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 VARIANT_H +#define VARIANT_H + +#include + +QVariantMap qo2qv(const QObject* object, const QStringList& ignoredProperties = QStringList(QString(QLatin1String("objectName")))); + +#endif // VARIANT_H diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index b06e702a9..5a00a5dba 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -16,6 +16,7 @@ #cmakedefine WITH_XC_AUTOTYPE #cmakedefine WITH_XC_YUBIKEY #cmakedefine WITH_XC_SSHAGENT +#cmakedefine WITH_XC_BROWSER #cmakedefine KEEPASSXC_DIST #cmakedefine KEEPASSXC_DIST_TYPE "@KEEPASSXC_DIST_TYPE@" diff --git a/src/core/PassphraseGenerator.cpp b/src/core/PassphraseGenerator.cpp index 1af614795..115c70d6d 100644 --- a/src/core/PassphraseGenerator.cpp +++ b/src/core/PassphraseGenerator.cpp @@ -28,8 +28,7 @@ PassphraseGenerator::PassphraseGenerator() : m_wordCount(0) , m_separator(' ') { - const QString path = filePath()->dataPath("wordlists/eff_large.wordlist"); - setWordList(path); + } double PassphraseGenerator::calculateEntropy(QString passphrase) @@ -46,12 +45,12 @@ double PassphraseGenerator::calculateEntropy(QString passphrase) void PassphraseGenerator::setWordCount(int wordCount) { if (wordCount > 0) { - m_wordCount = wordCount; + m_wordCount = wordCount; } else { // safe default if something goes wrong m_wordCount = 7; } - + } void PassphraseGenerator::setWordList(QString path) @@ -75,6 +74,12 @@ void PassphraseGenerator::setWordList(QString path) } } +void PassphraseGenerator::setDefaultWordList() +{ + const QString path = filePath()->dataPath("wordlists/eff_large.wordlist"); + setWordList(path); +} + void PassphraseGenerator::setWordSeparator(QString separator) { m_separator = separator; } diff --git a/src/core/PassphraseGenerator.h b/src/core/PassphraseGenerator.h index 3be2d5836..20845ff21 100644 --- a/src/core/PassphraseGenerator.h +++ b/src/core/PassphraseGenerator.h @@ -30,6 +30,7 @@ public: double calculateEntropy(QString passphrase); void setWordCount(int wordCount); void setWordList(QString path); + void setDefaultWordList(); void setWordSeparator(QString separator); bool isValid() const; @@ -43,4 +44,4 @@ private: Q_DISABLE_COPY(PassphraseGenerator) }; -#endif // KEEPASSX_PASSPHRASEGENERATOR_H \ No newline at end of file +#endif // KEEPASSX_PASSPHRASEGENERATOR_H diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index e89a7fdcb..44ab895e8 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -90,6 +90,9 @@ AboutDialog::AboutDialog(QWidget* parent) #ifdef WITH_XC_SSHAGENT extensions += "\n- SSH Agent"; #endif +#ifdef WITH_XC_BROWSER + extensions += "\n- Native messaging browser extension"; +#endif if (extensions.isEmpty()) extensions = " None"; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 72be3a4aa..4804b47b2 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -54,6 +54,12 @@ #include "sshagent/SSHAgent.h" #endif +#ifdef WITH_XC_BROWSER +#include "browser/NativeMessagingHost.h" +#include "browser/BrowserSettings.h" +#include "browser/BrowserOptionDialog.h" +#endif + #include "gui/SettingsWidget.h" #include "gui/PasswordGeneratorWidget.h" @@ -104,6 +110,54 @@ private: }; #endif +#ifdef WITH_XC_BROWSER +class BrowserPlugin: public ISettingsPage +{ + public: + BrowserPlugin(DatabaseTabWidget* tabWidget) { + m_nativeMessagingHost = QSharedPointer(new NativeMessagingHost(tabWidget)); + } + + ~BrowserPlugin() { + + } + + QString name() override + { + return QObject::tr("Browser extension with native messaging"); + } + + QIcon icon() override + { + return FilePath::instance()->icon("apps", "internet-web-browser"); + } + + QWidget* createWidget() override { + BrowserOptionDialog* dlg = new BrowserOptionDialog(); + QObject::connect(dlg, SIGNAL(removeSharedEncryptionKeys()), m_nativeMessagingHost.data(), SLOT(removeSharedEncryptionKeys())); + QObject::connect(dlg, SIGNAL(removeStoredPermissions()), m_nativeMessagingHost.data(), SLOT(removeStoredPermissions())); + return dlg; + } + + void loadSettings(QWidget* widget) override + { + qobject_cast(widget)->loadSettings(); + } + + void saveSettings(QWidget* widget) override + { + qobject_cast(widget)->saveSettings(); + if (BrowserSettings::isEnabled()) { + m_nativeMessagingHost->run(); + } else { + m_nativeMessagingHost->stop(); + } + } + private: + QSharedPointer m_nativeMessagingHost; +}; +#endif + const QString MainWindow::BaseWindowTitle = "KeePassXC"; MainWindow::MainWindow() @@ -134,6 +188,9 @@ MainWindow::MainWindow() SSHAgent::init(this); m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif + #ifdef WITH_XC_BROWSER + m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); + #endif setWindowIcon(filePath()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index 70375a048..1c72bd353 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -80,7 +80,8 @@ PasswordGeneratorWidget::PasswordGeneratorWidget(QWidget* parent) m_ui->comboBoxWordList->setVisible(false); m_ui->labelWordList->setVisible(false); } - + + m_dicewareGenerator->setDefaultWordList(); loadSettings(); reset(); } @@ -164,7 +165,7 @@ void PasswordGeneratorWidget::keyPressEvent(QKeyEvent* e) } void PasswordGeneratorWidget::regeneratePassword() -{ +{ if (m_ui->tabWidget->currentIndex() == Password) { if (m_passwordGenerator->isValid()) { QString password = m_passwordGenerator->generatePassword(); diff --git a/src/main.cpp b/src/main.cpp index 82331f072..deabeabb7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -69,12 +69,18 @@ int main(int argc, char** argv) "keyfile"); QCommandLineOption pwstdinOption("pw-stdin", QCoreApplication::translate("main", "read password of the database from stdin")); + // This is needed under Windows where clients send --parent-window parameter with Native Messaging connect method + QCommandLineOption parentWindowOption(QStringList() << "pw" + << "parent-window", + QCoreApplication::translate("main", "Parent window handle"), + "handle"); parser.addHelpOption(); parser.addVersionOption(); parser.addOption(configOption); parser.addOption(keyfileOption); parser.addOption(pwstdinOption); + parser.addOption(parentWindowOption); parser.process(app); const QStringList fileNames = parser.positionalArguments(); @@ -86,7 +92,7 @@ int main(int argc, char** argv) qWarning() << QCoreApplication::translate("Main", "Another instance of KeePassXC is already running.").toUtf8().constData(); return 0; } - + QApplication::setQuitOnLastWindowClosed(false); if (!Crypto::init()) { @@ -116,7 +122,7 @@ int main(int argc, char** argv) QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(openFile(QString)), &mainWindow, SLOT(openDatabase(QString))); QObject::connect(&app, SIGNAL(quitSignalReceived()), &mainWindow, SLOT(appExit()), Qt::DirectConnection); - + // start minimized if configured bool minimizeOnStartup = config()->get("GUI/MinimizeOnStartup").toBool(); bool minimizeToTray = config()->get("GUI/MinimizeToTray").toBool(); @@ -126,7 +132,7 @@ int main(int argc, char** argv) if (!(minimizeOnStartup && minimizeToTray)) { mainWindow.show(); } - + if (config()->get("OpenPreviousDatabasesOnStartup").toBool()) { const QStringList fileNames = config()->get("LastOpenedDatabases").toStringList(); for (const QString& filename: fileNames) { @@ -138,13 +144,12 @@ int main(int argc, char** argv) const bool pwstdin = parser.isSet(pwstdinOption); for (const QString& filename: fileNames) { - if (!filename.isEmpty() && QFile::exists(filename)) { + if (!filename.isEmpty() && QFile::exists(filename) && !filename.endsWith(".json", Qt::CaseInsensitive)) { QString password; if (pwstdin) { static QTextStream in(stdin, QIODevice::ReadOnly); password = in.readLine(); } - mainWindow.openDatabase(filename, password, parser.value(keyfileOption)); } } diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt new file mode 100755 index 000000000..4fe32428d --- /dev/null +++ b/src/proxy/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (C) 2017 Sami Vänttinen +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# 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 . + +if(WITH_XC_BROWSER) + include_directories(${BROWSER_SOURCE_DIR}) + + set(proxy_SOURCES + keepassxc-proxy.cpp + ${BROWSER_SOURCE_DIR}/NativeMessagingBase.cpp + NativeMessagingHost.cpp) + + add_library(proxy STATIC ${proxy_SOURCES}) + target_link_libraries(proxy Qt5::Core Qt5::Network) + add_executable(keepassxc-proxy keepassxc-proxy.cpp) + target_link_libraries(keepassxc-proxy proxy) + + install(TARGETS keepassxc-proxy + BUNDLE DESTINATION . COMPONENT Runtime + RUNTIME DESTINATION ${PROXY_INSTALL_DIR} COMPONENT Runtime) + +endif() diff --git a/src/proxy/NativeMessagingHost.cpp b/src/proxy/NativeMessagingHost.cpp new file mode 100755 index 000000000..2add63814 --- /dev/null +++ b/src/proxy/NativeMessagingHost.cpp @@ -0,0 +1,95 @@ +/* +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "NativeMessagingHost.h" + +NativeMessagingHost::NativeMessagingHost() : NativeMessagingBase() +{ + m_localSocket = new QLocalSocket(); + m_localSocket->connectToServer(getLocalServerPath()); +#ifdef Q_OS_WIN + m_running.store(true); + m_future = QtConcurrent::run(this, static_cast(&NativeMessagingHost::readNativeMessages)); +#endif + connect(m_localSocket, SIGNAL(readyRead()), this, SLOT(newLocalMessage())); + connect(m_localSocket, SIGNAL(disconnected()), this, SLOT(deleteSocket())); + connect(m_localSocket, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(socketStateChanged(QLocalSocket::LocalSocketState))); +} + +NativeMessagingHost::~NativeMessagingHost() +{ +#ifdef Q_OS_WIN + m_future.waitForFinished(); +#endif +} + +void NativeMessagingHost::readLength() +{ + quint32 length = 0; + std::cin.read(reinterpret_cast(&length), 4); + if (!std::cin.eof() && length > 0) { + readStdIn(length); + } else { + QCoreApplication::quit(); + } +} + +void NativeMessagingHost::readStdIn(const quint32 length) +{ + if (length > 0) { + QByteArray arr; + arr.reserve(length); + + for (quint32 i = 0; i < length; ++i) { + arr.append(getchar()); + } + + if (arr.length() > 0 && m_localSocket && m_localSocket->state() == QLocalSocket::ConnectedState) { + m_localSocket->write(arr.constData(), arr.length()); + m_localSocket->flush(); + } + } +} + +void NativeMessagingHost::newLocalMessage() +{ + if (!m_localSocket || m_localSocket->bytesAvailable() <= 0) { + return; + } + + QByteArray arr = m_localSocket->readAll(); + if (!arr.isEmpty()) { + sendReply(arr); + } +} + +void NativeMessagingHost::deleteSocket() +{ + if (m_notifier) { + m_notifier->setEnabled(false); + } + m_localSocket->deleteLater(); + QCoreApplication::quit(); +} + +void NativeMessagingHost::socketStateChanged(QLocalSocket::LocalSocketState socketState) +{ + if (socketState == QLocalSocket::UnconnectedState || socketState == QLocalSocket::ClosingState) { + m_running.testAndSetOrdered(true, false); + } +} diff --git a/src/proxy/NativeMessagingHost.h b/src/proxy/NativeMessagingHost.h new file mode 100755 index 000000000..41f3ed753 --- /dev/null +++ b/src/proxy/NativeMessagingHost.h @@ -0,0 +1,43 @@ +/* +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 NATIVEMESSAGINGHOST_H +#define NATIVEMESSAGINGHOST_H + +#include "NativeMessagingBase.h" + +class NativeMessagingHost : public NativeMessagingBase +{ + Q_OBJECT +public: + NativeMessagingHost(); + ~NativeMessagingHost(); + +public slots: + void newLocalMessage(); + void deleteSocket(); + void socketStateChanged(QLocalSocket::LocalSocketState socketState); + +private: + void readLength(); + void readStdIn(const quint32 length); + +private: + QLocalSocket* m_localSocket; +}; + +#endif // NATIVEMESSAGINGHOST_H diff --git a/src/proxy/keepassxc-proxy.cpp b/src/proxy/keepassxc-proxy.cpp new file mode 100644 index 000000000..9509c1eab --- /dev/null +++ b/src/proxy/keepassxc-proxy.cpp @@ -0,0 +1,66 @@ +/* +* Copyright (C) 2017 Sami Vänttinen +* 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 +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* 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 "NativeMessagingHost.h" + +#ifndef Q_OS_WIN +#include +#include +#include + +// (C) Gist: https://gist.github.com/azadkuh/a2ac6869661ebd3f8588 +void ignoreUnixSignals(std::initializer_list ignoreSignals) { + for (int sig : ignoreSignals) { + signal(sig, SIG_IGN); + } +} + +void catchUnixSignals(std::initializer_list quitSignals) { + auto handler = [](int sig) -> void { + std::cerr << sig; + QCoreApplication::quit(); + }; + + sigset_t blocking_mask; + sigemptyset(&blocking_mask); + for (auto sig : quitSignals) { + sigaddset(&blocking_mask, sig); + } + + struct sigaction sa; + sa.sa_handler = handler; + sa.sa_mask = blocking_mask; + sa.sa_flags = 0; + + for (auto sig : quitSignals) { + sigaction(sig, &sa, nullptr); + } +} +#endif + +int main(int argc, char *argv[]) +{ + QCoreApplication a(argc, argv); +#if defined(Q_OS_UNIX) || defined(Q_OS_LINUX) + catchUnixSignals({SIGQUIT, SIGINT, SIGTERM, SIGHUP}); +#endif + NativeMessagingHost host; + return a.exec(); +}