diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index df542958..4f937513 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -103,18 +103,8 @@ The Branch Strategy is based on [git-flow-lite](http://nvie.com/posts/a-successf * Use the imperative mood ("Move cursor to…" not "Moves cursor to…") * Limit the first line to 72 characters or less * Reference issues and pull requests liberally +* If your pull request fixes an existing issue, add "…, resolves #ISSUENUMBER" to your main commit * When only changing documentation, include `[ci skip]` in the commit description -* Consider starting the commit message with an applicable emoji: - * :memo: `:memo:` when writing docs - * :penguin: `:penguin:` when fixing something on Linux - * :apple: `:apple:` when fixing something on macOS - * :checkered_flag: `:checkered_flag:` when fixing something on Windows - * :bug: `:bug:` when fixing a bug - * :fire: `:fire:` when removing code or files - * :green_heart: `:green_heart:` when fixing the CI build - * :white_check_mark: `:white_check_mark:` when adding tests - * :lock: `:lock:` when dealing with security - ### Coding styleguide diff --git a/.gitignore b/.gitignore index 2d9bb4af..61dfb90d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ CMakeLists.txt.* build*/ +cmake-build-*/ release*/ .idea/ *.iml @@ -7,3 +8,5 @@ release*/ \.vscode/ *.swp + +.DS_Store diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 0472cccd..f8d7e105 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -34,6 +34,7 @@ fi APP="$1" LOWERAPP="$(echo "$APP" | tr '[:upper:]' '[:lower:]')" VERSION="$2" +export ARCH=x86_64 mkdir -p $APP.AppDir wget -q https://github.com/AppImage/AppImages/raw/master/functions.sh -O ./functions.sh @@ -44,6 +45,8 @@ if [ -d ./usr/lib/x86_64-linux-gnu ]; then LIB_DIR=./usr/lib/x86_64-linux-gnu elif [ -d ./usr/lib/i386-linux-gnu ]; then LIB_DIR=./usr/lib/i386-linux-gnu +elif [ -d ./usr/lib64 ]; then + LIB_DIR=./usr/lib64 fi cd $APP.AppDir @@ -53,7 +56,7 @@ rm -R ./usr/local rmdir ./opt 2> /dev/null # bundle Qt platform plugins and themes -QXCB_PLUGIN="$(find /usr/lib -name 'libqxcb.so' 2> /dev/null)" +QXCB_PLUGIN="$(find /usr/lib* -name 'libqxcb.so' 2> /dev/null)" if [ "$QXCB_PLUGIN" == "" ]; then QXCB_PLUGIN="$(find /opt/qt*/plugins -name 'libqxcb.so' 2> /dev/null)" fi @@ -65,33 +68,34 @@ get_apprun copy_deps delete_blacklisted -# remove dbus and systemd libs as they are not blacklisted -find . -name libdbus-1.so.3 -exec rm {} \; -find . -name libsystemd.so.0 -exec rm {} \; - get_desktop get_icon cat << EOF > ./usr/bin/keepassxc_env #!/usr/bin/env bash -#export QT_QPA_PLATFORMTHEME=gtk2 export LD_LIBRARY_PATH="..$(dirname ${QT_PLUGIN_PATH})/lib:\${LD_LIBRARY_PATH}" -export QT_PLUGIN_PATH="..${QT_PLUGIN_PATH}" +export QT_PLUGIN_PATH="..${QT_PLUGIN_PATH}:\${KPXC_QT_PLUGIN_PATH}" # unset XDG_DATA_DIRS to make tray icon work in Ubuntu Unity -# see https://github.com/probonopd/AppImageKit/issues/351 +# see https://github.com/AppImage/AppImageKit/issues/351 unset XDG_DATA_DIRS -exec keepassxc "\$@" +if [ "\${1}" == "cli" ]; then + shift + exec keepassxc-cli "\$@" +else + exec keepassxc "\$@" +fi EOF chmod +x ./usr/bin/keepassxc_env -sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' keepassxc.desktop -get_desktopintegration $LOWERAPP - -GLIBC_NEEDED=$(glibc_needed) +sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' org.${LOWERAPP}.${APP}.desktop +get_desktopintegration "org.${LOWERAPP}.${APP}" cd .. -generate_type2_appimage +GLIBC_NEEDED=$(glibc_needed) +NO_GLIBC_VERSION=true -mv ../out/*.AppImage .. -rmdir ../out > /dev/null 2>&1 +generate_type2_appimage -u "gh-releases-zsync|keepassxreboot|keepassxc|latest|KeePassXC-*-${ARCH}.AppImage.zsync" + +mv ../out/*.AppImage* ../ +rm -rf ../out diff --git a/CHANGELOG b/CHANGELOG index cc976c13..3719f8e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,20 @@ +2.2.2 (2017-10-22) +========================= + +- Fixed entries with empty URLs being reported to KeePassHTTP clients [#1031] +- Fixed YubiKey detection and enabled CLI tool for AppImage binary [#1100] +- Added AppStream description [#1082] +- Improved TOTP compatibility and added new Base32 implementation [#1069] +- Fixed error handling when processing invalid cipher stream [#1099] +- Fixed double warning display when opening a database [#1037] +- Fixed unlocking databases with --pw-stdin [#1087] +- Added ability to override QT_PLUGIN_PATH environment variable for AppImages [#1079] +- Fixed transform seed not being regenerated when saving the database [#1068] +- Fixed only one YubiKey slot being polled [#1048] +- Corrected an issue with entry icons while merging [#1008] +- Corrected desktop and tray icons in Snap package [#1030] +- Fixed screen lock and Google fallback settings [#1029] + 2.2.1 (2017-10-01) ========================= diff --git a/CMakeLists.txt b/CMakeLists.txt index 106cc1c1..ec7682c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,24 +36,34 @@ include(CheckCXXSourceCompiles) option(WITH_TESTS "Enable building of unit tests" ON) option(WITH_GUI_TESTS "Enable building of GUI tests" OFF) option(WITH_DEV_BUILD "Use only for development. Disables/warns about deprecated methods." OFF) -option(WITH_ASAN "Enable address sanitizer checks (Linux only)" OFF) +option(WITH_ASAN "Enable address sanitizer checks (Linux / macOS only)" OFF) option(WITH_COVERAGE "Use to build with coverage tests (GCC only)." OFF) -option(WITH_APP_BUNDLE "Enable Application Bundle for OS X" ON) +option(WITH_APP_BUNDLE "Enable Application Bundle for macOS" ON) 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) # Process ui files automatically from source files set(CMAKE_AUTOUIC ON) set(KEEPASSXC_VERSION_MAJOR "2") set(KEEPASSXC_VERSION_MINOR "2") -set(KEEPASSXC_VERSION_PATCH "1") +set(KEEPASSXC_VERSION_PATCH "2") set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION_MAJOR}.${KEEPASSXC_VERSION_MINOR}.${KEEPASSXC_VERSION_PATCH}") -# Special flag for snap builds -set(KEEPASSXC_SNAP_BUILD OFF CACHE BOOL "Set whether this is a build for snap or not") +# Distribution info +set(KEEPASSXC_DIST True) +set(KEEPASSXC_DIST_TYPE "Other" CACHE STRING "KeePassXC Distribution type") +set_property(CACHE KEEPASSXC_DIST_TYPE PROPERTY STRINGS Snap AppImage Other) +if(KEEPASSXC_DIST_TYPE STREQUAL "Snap") + set(KEEPASSXC_DIST_SNAP True) +elseif(KEEPASSXC_DIST_TYPE STREQUAL "AppImage") + set(KEEPASSXC_DIST_APPIMAGE True) +elseif(KEEPASSXC_DIST_TYPE STREQUAL "Other") + unset(KEEPASSXC_DIST) +endif() if("${CMAKE_C_COMPILER}" MATCHES "clang$" OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_COMPILER_IS_CLANG 1) @@ -87,7 +97,7 @@ if(WITH_APP_BUNDLE) endif() add_gcc_compiler_flags("-fno-common") -add_gcc_compiler_flags("-Wall -Wextra -Wundef -Wpointer-arith -Wno-long-long") +add_gcc_compiler_flags("-Wall -Werror -Wextra -Wundef -Wpointer-arith -Wno-long-long") add_gcc_compiler_flags("-Wformat=2 -Wmissing-format-attribute") add_gcc_compiler_flags("-fvisibility=hidden") add_gcc_compiler_cxxflags("-fvisibility-inlines-hidden") @@ -101,16 +111,20 @@ endif() add_gcc_compiler_cxxflags("-fno-exceptions -fno-rtti") add_gcc_compiler_cxxflags("-Wnon-virtual-dtor -Wold-style-cast -Woverloaded-virtual") add_gcc_compiler_cflags("-Wchar-subscripts -Wwrite-strings") + if(WITH_ASAN) - if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") - message(FATAL_ERROR "WITH_ASAN is only supported on Linux at the moment.") + if(NOT (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR APPLE)) + message(FATAL_ERROR "WITH_ASAN is only supported on Linux / macOS at the moment.") endif() add_gcc_compiler_flags("-fsanitize=address -DWITH_ASAN") - if(NOT (CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9)) - add_gcc_compiler_flags("-fsanitize=leak -DWITH_LSAN") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(NOT (CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9)) + add_gcc_compiler_flags("-fsanitize=leak -DWITH_LSAN") + endif() endif() + endif() string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) @@ -207,17 +221,36 @@ if(WITH_TESTS) enable_testing() endif(WITH_TESTS) -find_package(Qt5Core 5.2 REQUIRED) -find_package(Qt5Network 5.2 REQUIRED) -find_package(Qt5Concurrent 5.2 REQUIRED) -find_package(Qt5Widgets 5.2 REQUIRED) -find_package(Qt5Test 5.2 REQUIRED) -find_package(Qt5LinguistTools 5.2 REQUIRED) -find_package(Qt5Network 5.2 REQUIRED) -if (UNIX AND NOT APPLE) - find_package(Qt5DBus 5.2 REQUIRED) +if(UNIX AND NOT APPLE) + find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools DBus REQUIRED) +elseif(APPLE) + find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools REQUIRED + HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH + ) + find_package(Qt5 COMPONENTS MacExtras + HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH + ) +else() + find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools REQUIRED) endif() + +if(Qt5Core_VERSION VERSION_LESS "5.2.0") + message(FATAL_ERROR "Qt version 5.2.0 or higher is required") +endif() + set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +if(APPLE) + set(CMAKE_MACOSX_RPATH TRUE) + find_program(MACDEPLOYQT_EXE macdeployqt HINTS /usr/local/opt/qt5/bin ENV PATH) + if(NOT MACDEPLOYQT_EXE) + message(FATAL_ERROR "macdeployqt is required to build in macOS") + else() + message(STATUS "Using macdeployqt: ${MACDEPLOYQT_EXE}") + endif() +endif() # Debian sets the the build type to None for package builds. # Make sure we don't enable asserts there. @@ -231,17 +264,8 @@ find_package(ZLIB REQUIRED) set(CMAKE_REQUIRED_INCLUDES ${ZLIB_INCLUDE_DIR}) -check_cxx_source_compiles(" - #include - - #if !defined(ZLIB_VERNUM) || (ZLIB_VERNUM < 0x1200) - #error zlib 1.2.x or higher is required to use the gzip format - #endif - - int main() { return 0; }" ZLIB_SUPPORTS_GZIP) - -if(NOT ZLIB_SUPPORTS_GZIP) - message(FATAL_ERROR "zlib 1.2.x or higher is required to use the gzip format") +if(ZLIB_VERSION_STRING VERSION_LESS "1.2.0") + message(FATAL_ERROR "zlib 1.2.0 or higher is required to use the gzip format") endif() # Optional diff --git a/COPYING b/COPYING index 403e4564..7aa9c033 100644 --- a/COPYING +++ b/COPYING @@ -55,10 +55,6 @@ Files: cmake/GenerateProductVersion.cmake Copyright: 2015 halex2005 License: MIT -Files: cmake/CodeCoverage.cmake -Copyright: 2012 - 2015, Lars Bilke -License: BSD-3-clause - Files: share/icons/application/*/apps/keepassxc.png share/icons/application/scalable/apps/keepassxc.svgz share/icons/application/*/apps/keepassxc-dark.png @@ -156,6 +152,7 @@ License: LGPL-2.1 Comment: based on Nuvola icon theme Files: share/icons/application/*/actions/application-exit.png + share/icons/application/*/actions/chronometer.png share/icons/application/*/actions/configure.png share/icons/application/*/actions/dialog-close.png share/icons/application/*/actions/dialog-ok.png @@ -178,6 +175,7 @@ Files: share/icons/application/*/actions/application-exit.png share/icons/application/*/actions/view-history.png share/icons/application/*/apps/internet-web-browser.png share/icons/application/*/apps/preferences-desktop-icons.png + share/icons/application/*/apps/utilities-terminal.png share/icons/application/*/categories/preferences-other.png share/icons/application/*/status/dialog-error.png share/icons/application/*/status/dialog-information.png diff --git a/Dockerfile b/Dockerfile index 7da65821..a5966be3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,47 +16,48 @@ FROM ubuntu:14.04 -RUN set -x \ - && apt-get update \ - && apt-get install --yes software-properties-common +ENV QT5_VERSION=59 +ENV QT5_PPA_VERSION=${QT5_VERSION}2 RUN set -x \ - && add-apt-repository ppa:george-edison55/cmake-3.x - -ENV QT_VERSION=qt59 + && apt-get update -y \ + && apt-get -y install software-properties-common RUN set -x \ - && add-apt-repository --yes ppa:beineri/opt-${QT_VERSION}-trusty - + && add-apt-repository ppa:beineri/opt-qt${QT5_PPA_VERSION}-trusty \ + && add-apt-repository ppa:phoerious/keepassxc RUN set -x \ - && apt-get update \ - && apt-get install --yes \ + && apt-get update -y \ + && apt-get upgrade -y + +# build and runtime dependencies +RUN set -x \ + && apt-get install -y \ + cmake3 \ g++ \ - cmake \ libgcrypt20-dev \ - ${QT_VERSION}base \ - ${QT_VERSION}tools \ - ${QT_VERSION}x11extras \ + qt${QT5_VERSION}base \ + qt${QT5_VERSION}tools \ + qt${QT5_VERSION}x11extras \ + zlib1g-dev \ libxi-dev \ libxtst-dev \ - zlib1g-dev \ + mesa-common-dev \ libyubikey-dev \ - libykpers-1-dev \ - xvfb \ - wget \ - file \ - fuse \ - python + libykpers-1-dev +ENV CMAKE_PREFIX_PATH=/opt/qt${QT5_VERSION}/lib/cmake +ENV LD_LIBRARY_PATH=/opt/qt${QT5_VERSION}/lib RUN set -x \ - && apt-get install --yes mesa-common-dev - + && echo /opt/qt${QT_VERSION}/lib > /etc/ld.so.conf.d/qt${QT5_VERSION}.conf + +# AppImage dependencies +RUN set -x \ + && apt-get install -y \ + libfuse2 \ + wget + VOLUME /keepassxc/src VOLUME /keepassxc/out WORKDIR /keepassxc - -ENV CMAKE_PREFIX_PATH=/opt/${QT_VERSION}/lib/cmake -ENV LD_LIBRARY_PATH=/opt/${QT_VERSION}/lib -RUN set -x \ - && echo /opt/${QT_VERSION}/lib > /etc/ld.so.conf.d/${QT_VERSION}.conf diff --git a/INSTALL.md b/INSTALL.md index 45efa7a2..2d3f7cb2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,11 +1,13 @@ -Install KeePassXC +Build and Install KeePassXC ================= -This document will guide you across the steps to install KeePassXC. -You can visit the online version of this document at the following link +This document will guide you through the steps to build and install KeePassXC from source. +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 +Windows, Mac, or Linux computer using the pre-built binaries. Build Dependencies ================== @@ -28,10 +30,9 @@ The following libraries are required: Prepare the Building Environment ================================ -* Building Environment on Linux ==> https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-Linux -* Building Environment on Windows ==> https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-Windows -* Building Environment on MacOS ==> https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-OS-X - +* [Building Environment on Linux](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-Linux) +* [Building Environment on Windows](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-Windows) +* [Building Environment on MacOS](https://github.com/keepassxreboot/keepassxc/wiki/Set-up-Build-Environment-on-OS-X) Build Steps =========== @@ -39,32 +40,68 @@ 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 -Navigate to the path you have downloaded KeePassXC and type these commands: +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 + +```bash +git clone https://github.com/keepassxreboot/keepassxc.git +``` + +This will clone the entire contents of the repository and check out the current `develop` branch. + +To update the project from within the project's folder, you can run the following command: + +```bash +git pull +``` + +Navigate to the directory where you have downloaded KeePassXC and type these commands: ``` +cd directory-where-sources-live mkdir build cd build -cmake -DWITH_TESTS=OFF +cmake -DWITH_TESTS=OFF ...and other options - see below... make ``` +These steps place the compiled KeePassXC binary inside the `./build/src/` directory. +(Note the cmake notes/options below.) -**Note:** If you are on MacOS you must add this parameter to **Cmake**, with the Qt version you have installed
`-DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake/` +**Cmake Notes:** -You will have the compiled KeePassXC binary inside the `./build/src/` directory. +* Common cmake parameters -Common cmake parameters -``` --DCMAKE_INSTALL_PREFIX=/usr/local --DCMAKE_VERBOSE_MAKEFILE=ON --DCMAKE_BUILD_TYPE= --DWITH_GUI_TESTS=ON -``` + ``` + -DCMAKE_INSTALL_PREFIX=/usr/local + -DCMAKE_VERBOSE_MAKEFILE=ON + -DCMAKE_BUILD_TYPE= + -DWITH_GUI_TESTS=ON + ``` +* cmake accepts the following options: + + ``` + -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_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON) + -DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF) + -DWITH_DEV_BUILD=[ON|OFF] Enable/Disable deprecated method warnings (default: OFF) + -DWITH_ASAN=[ON|OFF] Enable/Disable address sanitizer checks (Linux / macOS only) (default: OFF) + -DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF) + ``` + +* If you are on MacOS you must add this parameter to **Cmake**, with the Qt version you have installed
`-DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.6.2/lib/cmake/` + +:exclamation: When building with ASan support on macOS, you need to use `export ASAN_OPTIONS=detect_leaks=0` before running the tests (no LSan support in macOS). Installation ============ -To install this binary execute the following: +After you have successfully built KeePassXC, install the binary by executing the following: ```bash sudo make install diff --git a/README.md b/README.md index 81a99e25..e4787cac 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ -# KeePassXC [![Travis Build Status](https://travis-ci.org/keepassxreboot/keepassxc.svg?branch=develop)](https://travis-ci.org/keepassxreboot/keepassxc) [![Coverage Status](https://coveralls.io/repos/github/keepassxreboot/keepassxc/badge.svg)](https://coveralls.io/github/keepassxreboot/keepassxc) +# KeePassXC +[![Travis Build Status](https://travis-ci.org/keepassxreboot/keepassxc.svg?branch=develop)](https://travis-ci.org/keepassxreboot/keepassxc) [![Coverage Status](https://coveralls.io/repos/github/keepassxreboot/keepassxc/badge.svg)](https://coveralls.io/github/keepassxreboot/keepassxc) -KeePass Cross-platform Community Edition - -## About -[KeePassXC](https://keepassxc.org) is a community fork of [KeePassX](https://www.keepassx.org/) with the goal to extend and improve it with new features and bugfixes to provide a feature-rich, fully cross-platform and modern open-source password manager. +## 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 +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, +so please check out your distribution's package list to see if KeePassXC is available. ## Additional features compared to KeePassX - Auto-Type on all three major platforms (Linux, Windows, macOS) @@ -18,66 +28,40 @@ KeePass Cross-platform Community Edition - Password strength meter - Using website favicons as entry icons - Merging of databases -- Automatic reload when the database was changed externally -- KeePassHTTP support for use with [PassIFox](https://addons.mozilla.org/en-us/firefox/addon/passifox/) in Mozilla Firefox, [chromeIPass](https://chrome.google.com/webstore/detail/chromeipass/ompiailgknfdndiefoaoiligalphfdae) in Google Chrome or Chromium and [passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. +- 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) - Many bug fixes For a full list of features and changes, read the [CHANGELOG](CHANGELOG) document. -### Note about KeePassHTTP -KeePassHTTP is not a highly secure protocol and has certain flaw which allow an attacker to decrypt your passwords when they manage to intercept communication between a KeePassHTTP server and PassIFox/chromeIPass over a network connection (see [here](https://github.com/pfn/keepasshttp/issues/258) and [here](https://github.com/keepassxreboot/keepassxc/issues/147)). KeePassXC therefore strictly limits communication between itself and the browser plugin to your local computer. As long as your computer is not compromised, your passwords are fairly safe that way, but use it at your own risk! +## Building KeePassXC -### Installation -Pre-compiled binaries can be found on 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. +Detailed instructions are available in the [Build and Install](./BUILD-INSTALL.md) +page or on the [Wiki page](https://github.com/keepassxreboot/keepassxc/wiki/Building-KeePassXC). -### Building KeePassXC +## Contributing -*More detailed instructions are available in the INSTALL file or on the [Wiki page](https://github.com/keepassxreboot/keepassxc/wiki/Building-KeePassXC).* - -First, you must 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 - -```bash -git clone https://github.com/keepassxreboot/keepassxc.git -``` - -This will clone the entire contents of the repository and check out the current `develop` branch. - -To update the project from within the project's folder, you can run the following command: - -```bash -git pull -``` - -Once you have downloaded the source code, you can `cd` into the source code directory, build and install KeePassXC: - -```bash -mkdir build -cd build -cmake -DWITH_TESTS=OFF .. -make -j8 -sudo make install -``` - -cmake accepts the following options: - -``` - -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_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON) - -DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF) - -DWITH_DEV_BUILD=[ON|OFF] Enable/Disable deprecated method warnings (default: OFF) - -DWITH_ASAN=[ON|OFF] Enable/Disable address sanitizer checks (Linux only) (default: OFF) - -DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF) -``` - -### 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) on GitHub or join us on IRC on freenode channels #keepassxc or #keepassxc-dev. +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. Please read the [CONTRIBUTING document](.github/CONTRIBUTING.md) for further information. + +### Note about KeePassHTTP +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)). + +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/docs/KeePassXC-Accept-Button.png b/docs/KeePassXC-Accept-Button.png new file mode 100644 index 00000000..de4b3926 Binary files /dev/null and b/docs/KeePassXC-Accept-Button.png differ diff --git a/docs/KeePassXC-Confirm.png b/docs/KeePassXC-Confirm.png new file mode 100644 index 00000000..989294a4 Binary files /dev/null and b/docs/KeePassXC-Confirm.png differ diff --git a/docs/KeePassXC-Connect.png b/docs/KeePassXC-Connect.png new file mode 100644 index 00000000..55b0f3d4 Binary files /dev/null and b/docs/KeePassXC-Connect.png differ diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 00000000..b4b2d38c --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,47 @@ +# Quick Start for KeePassXC + +This procedure gets KeePassXC running on your computer with browser integration, +using the pre-built binaries available for [download](https://keepassxc.org/download) +from [KeePassXC site](https://keepassxc.org). + +**TL;DR** KeePassXC saves your passwords securely. +When you double-click a URL in KeePassXC, it launches your default browser to that URL. +With browser integration configured, KeePassXC automatically enters +username/password credentials into web page fields. + +## Installing and Starting KeePassXC + +* [Download the native installer](https://keepassxc.org/download) and install +KeePassXC for your Windows, macOS, or Linux computer in the usual way for your platform. +* Open the KeePassXC application. +* Create a new database and give it a master key that's used to unlock the database file. +This database holds entries (usernames, passwords, account numbers, notes) +for all your websites, programs, etc. +* Create a few entries - enter the username, password, URL, and optionally notes about the entry. +* KeePassXC securely stores those entries in the database. + + +## Setting up Browser Integration with KeePassXC + +* *Within KeePassXC*, go to **Tools->Settings** (on macOS, go to **KeePassXC->Preferences**.) +* In **Browser Integration**, check **Enable KeePassHTTP server** +Leave the other options at their defaults. +* *In your default web browser,* install the KeePassHTTP-Connector extension/add-on. Instructions for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepasshttp-connector/?src=api) or [Chrome](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb?utm_source=chrome-app-launcher-info-dialog) +* Click the KeePassXC icon in the upper-right corner. You'll see the dialog below. +* Click the blue Connect button to make the browser extension connect to the KeePassXC application. +KeePassXC Connect dialog + +* *Switch back to KeePassXC.* You'll see a dialog (below) indicating that a request to connect has arrived. +* Give the connection a name (perhaps *Keepass-Browsername*, any unique name will suffice) and click OK to accept it. +* This one-time operation connects KeePassXC and your browser. +KeePassXC accept connection dialog + +## Using Browser Integration + +* *Within KeePassXC,* double-click the URL of an entry, +or select it and type Ctrl+U (Cmd+U on macOS). +* Your browser opens to that URL. +* If there are username/password fields on that page, you will see the dialog below. +Click *Allow* to confirm that KeePassXC may access the credentials to auto-fill the fields. +* Check *Remember this decision* to allow this each time you visit the page. +KeePassCX Confirm Access dialog diff --git a/release-tool b/release-tool index 680cdca2..42cd3a1d 100755 --- a/release-tool +++ b/release-tool @@ -119,9 +119,11 @@ EOF Sign previously compiled release packages Options: - -f, --files Files to sign (required) - -g, --gpg-key GPG key used to sign the files (default: '${GPG_KEY}') - -h, --help Show this help + -f, --files Files to sign (required) + -g, --gpg-key GPG key used to sign the files (default: '${GPG_KEY}') + --signtool Specify the signtool executable (default: 'signtool') + --signtool-key Provide a key to be used with signtool (for Windows EXE) + -h, --help Show this help EOF fi } @@ -257,19 +259,18 @@ checkChangeLog() { grep -qPzo "${RELEASE_NAME} \(\d{4}-\d{2}-\d{2}\)\n=+\n" CHANGELOG if [ $? -ne 0 ]; then - exitError "CHANGELOG does not contain any information about the '${RELEASE_NAME}' release!" + exitError "'CHANGELOG' has not been updated to the '${RELEASE_NAME}' release!" fi } -checkTransifexCommandExists() { - command -v tx > /dev/null - if [ 0 -ne $? ]; then - exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'" +checkAppStreamInfo() { + if [ ! -f share/linux/org.keepassxc.KeePassXC.appdata.xml ]; then + exitError "No AppStream info file found!" fi - command -v lupdate-qt5 > /dev/null - if [ 0 -ne $? ]; then - exitError "Qt Linguist tool (lupdate-qt5) is not installed! Please install using 'apt install qttools5-dev-tools'" + grep -qPzo "" share/linux/org.keepassxc.KeePassXC.appdata.xml + if [ $? -ne 0 ]; then + exitError "'share/linux/org.keepassxc.KeePassXC.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!" fi } @@ -281,7 +282,24 @@ checkSnapcraft() { grep -qPzo "version: ${RELEASE_NAME}" snapcraft.yaml if [ $? -ne 0 ]; then - exitError "snapcraft.yaml has not been updated to the '${RELEASE_NAME}' release!" + exitError "'snapcraft.yaml' has not been updated to the '${RELEASE_NAME}' release!" + fi +} + +checkTransifexCommandExists() { + command -v tx > /dev/null + if [ 0 -ne $? ]; then + exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'" + fi +} + +checkQt5LUpdateExists() { + command -v lupdate > /dev/null + if [ 0 -eq $? ] && ! $(lupdate -version | grep -q "lupdate version 5\."); then + command -v lupdate-qt5 > /dev/null + if [ 0 -ne $? ]; then + exitError "Qt Linguist tool (lupdate-qt5) is not installed! Please install using 'apt install qttools5-dev-tools'" + fi fi } @@ -296,6 +314,7 @@ performChecks() { logInfo "Validating toolset and repository..." checkTransifexCommandExists + checkQt5LUpdateExists checkGitRepository checkReleaseDoesNotExist checkWorkingTreeClean @@ -309,6 +328,7 @@ performChecks() { checkVersionInCMake checkChangeLog + checkAppStreamInfo checkSnapcraft logInfo "\e[1m\e[32mAll checks passed!\e[0m" @@ -528,10 +548,10 @@ build() { checkWorkingTreeClean OUTPUT_DIR="$(realpath "$OUTPUT_DIR")" - + logInfo "Checking out release tag '${TAG_NAME}'..." git checkout "$TAG_NAME" - + logInfo "Creating output directory..." mkdir -p "$OUTPUT_DIR" @@ -598,7 +618,8 @@ build() { # Building on Linux without Docker container logInfo "Configuring build..." cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off $CMAKE_OPTIONS \ - -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR" + -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" \ + -DKEEPASSXC_DIST_TYPE=AppImage "$SRC_DIR" logInfo "Compiling sources..." make $MAKE_OPTIONS @@ -615,7 +636,7 @@ build() { logInfo "Launching Docker container to compile sources..." docker run --name "$DOCKER_CONTAINER_NAME" --rm \ - --cap-add SYS_ADMIN --device /dev/fuse \ + --cap-add SYS_ADMIN --security-opt apparmor:unconfined --device /dev/fuse \ -e "CC=${CC}" -e "CXX=${CXX}" \ -v "$(realpath "$SRC_DIR"):/keepassxc/src:ro" \ -v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \ @@ -644,6 +665,8 @@ build() { # ----------------------------------------------------------------------- sign() { SIGN_FILES=() + SIGNTOOL="signtool" + SIGNTOOL_KEY="" while [ $# -ge 1 ]; do local arg="$1" @@ -657,6 +680,14 @@ sign() { -g|--gpg-key) GPG_KEY="$2" shift ;; + + --signtool) + SIGNTOOL="$2" + shift ;; + + --signtool-key) + SIGNTOOL_KEY="$2" + shift ;; -h|--help) printUsage "sign" @@ -675,13 +706,30 @@ sign() { printUsage "sign" exit 1 fi + + if [[ -n "$SIGNTOOL_KEY" && ! -f "$SIGNTOOL_KEY" ]]; then + exitError "Signtool Key was not found!" + elif [[ -f "$SIGNTOOL_KEY" && ! -x $(command -v "${SIGNTOOL}") ]]; then + exitError "signtool program not found on PATH!" + fi for f in "${SIGN_FILES[@]}"; do if [ ! -f "$f" ]; then exitError "File '${f}' does not exist!" fi + + if [[ -n "$SIGNTOOL_KEY" && ${f: -4} == '.exe' ]]; then + logInfo "Signing file '${f}' using signtool...\n" + read -s -p "Signtool Key Password: " password + echo + "${SIGNTOOL}" sign -f "${SIGNTOOL_KEY}" -p ${password} -v -t http://timestamp.comodoca.com/authenticode ${f} + + if [ 0 -ne $? ]; then + exitError "Signing failed!" + fi + fi - logInfo "Signing file '${f}'..." + logInfo "Signing file '${f}' using release key..." gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f" if [ 0 -ne $? ]; then diff --git a/share/CMakeLists.txt b/share/CMakeLists.txt index a609add7..81bb2693 100644 --- a/share/CMakeLists.txt +++ b/share/CMakeLists.txt @@ -30,7 +30,8 @@ if(UNIX AND NOT APPLE) install(DIRECTORY icons/application/ DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor FILES_MATCHING PATTERN "application-x-keepassxc.png" PATTERN "application-x-keepassxc.svgz" PATTERN "status" EXCLUDE PATTERN "actions" EXCLUDE PATTERN "categories" EXCLUDE) - install(FILES linux/keepassxc.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) + install(FILES linux/org.keepassxc.KeePassXC.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) + install(FILES linux/org.keepassxc.KeePassXC.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(FILES linux/keepassxc.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages) endif(UNIX AND NOT APPLE) diff --git a/share/icons/application/22x22/actions/chronometer.png b/share/icons/application/22x22/actions/chronometer.png new file mode 100644 index 00000000..71d6eabe Binary files /dev/null and b/share/icons/application/22x22/actions/chronometer.png differ diff --git a/share/icons/application/32x32/apps/utilities-terminal.png b/share/icons/application/32x32/apps/utilities-terminal.png new file mode 100644 index 00000000..3e4d324c Binary files /dev/null and b/share/icons/application/32x32/apps/utilities-terminal.png differ diff --git a/share/icons/svg/utilities-terminal.svgz b/share/icons/svg/utilities-terminal.svgz new file mode 100644 index 00000000..e913402f Binary files /dev/null and b/share/icons/svg/utilities-terminal.svgz differ diff --git a/share/keepassxc.ini b/share/keepassxc.ini index f7ff52cb..c6f0654f 100644 --- a/share/keepassxc.ini +++ b/share/keepassxc.ini @@ -1,5 +1,4 @@ [General] -ShowToolbar=true RememberLastDatabases=true RememberLastKeyFiles=true OpenPreviousDatabasesOnStartup=true diff --git a/share/linux/keepassxc.xml b/share/linux/keepassxc.xml index 757047d2..b26b4db2 100644 --- a/share/linux/keepassxc.xml +++ b/share/linux/keepassxc.xml @@ -1,7 +1,7 @@ - KeePass 2 database + KeePass 2 Database diff --git a/share/linux/org.keepassxc.KeePassXC.appdata.xml b/share/linux/org.keepassxc.KeePassXC.appdata.xml new file mode 100644 index 00000000..b2a4de6e --- /dev/null +++ b/share/linux/org.keepassxc.KeePassXC.appdata.xml @@ -0,0 +1,209 @@ + + + + org.keepassxc.KeePassXC.desktop + KeePassXC + CC-BY-3.0 + GPL-3.0+ + + application/x-keepass2 + + Community-driven port of the Windows application “KeePass Password Safe” + KeePassXC Team + +

+ KeePassXC is an application for people with extremely high demands on secure + personal data management. It has a light interface, is cross-platform and + published under the terms of the GNU General Public License. +

+
+ + org.keepassxc.KeePassXC.desktop + + https://keepassxc.org + https://github.com/keepassxreboot/keepassxc/issues + https://keepassxc.org/docs#faq + https://keepassxc.org/docs + https://www.transifex.com/keepassxc/keepassxc + + + + https://keepassxc.org/images/screenshots/linux/screen_001.png + Create, Import or Open Databases + + + https://keepassxc.org/images/screenshots/linux/screen_002.png + Organize with Groups and Entries + + + https://keepassxc.org/images/screenshots/linux/screen_003.png + Database Entry + + + https://keepassxc.org/images/screenshots/linux/screen_004.png + Icon Selection for Entry + + + https://keepassxc.org/images/screenshots/linux/screen_006.png + Password Generator + + + + + + +

Changes included in this release:

+
    +
  • Fixed entries with empty URLs being reported to KeePassHTTP clients [#1031]
  • +
  • Fixed YubiKey detection and enabled CLI tool for AppImage binary [#1100]
  • +
  • Added AppStream description [#1082]
  • +
  • Improved TOTP compatibility and added new Base32 implementation [#1069]
  • +
  • Fixed error handling when processing invalid cipher stream [#1099]
  • +
  • Fixed double warning display when opening a database [#1037]
  • +
  • Fixed unlocking databases with --pw-stdin [#1087]
  • +
  • Added ability to override QT_PLUGIN_PATH environment variable for AppImages [#1079]
  • +
  • Fixed transform seed not being regenerated when saving the database [#1068]
  • +
  • Fixed only one YubiKey slot being polled [#1048]
  • +
  • Corrected an issue with entry icons while merging [#1008]
  • +
  • Corrected desktop and tray icons in Snap package [#1030]
  • +
  • Fixed screen lock and Google fallback settings [#1029]
  • +
+
+
+ +

Changes included in this release:

+
    +
  • Corrected multiple snap issues [#934, #1011]
  • +
  • Corrected multiple custom icon issues [#708, #719, #994]
  • +
  • Corrected multiple Yubikey issues [#880]
  • +
  • Fixed single instance preventing load on occasion [#997]
  • +
  • Keep entry history when merging databases [#970]
  • +
  • Prevent data loss if passwords were mismatched [#1007]
  • +
  • Fixed crash after merge [#941]
  • +
  • Added configurable auto-type default delay [#703]
  • +
  • Unlock database dialog window comes to front [#663]
  • +
  • Translation and compiling fixes
  • +
+
+
+ + +

Changes included in this release:

+
    +
  • Added YubiKey 2FA integration for unlocking databases [#127]
  • +
  • Added TOTP support [#519]
  • +
  • Added CSV import tool [#146, #490]
  • +
  • Added KeePassXC CLI tool [#254]
  • +
  • Added diceware password generator [#373]
  • +
  • Added support for entry references [#370, #378]
  • +
  • Added support for Twofish encryption [#167]
  • +
  • Enabled DEP and ASLR for in-memory protection [#371]
  • +
  • Enabled single instance mode [#510]
  • +
  • Enabled portable mode [#645]
  • +
  • Enabled database lock on screensaver and session lock [#545]
  • +
  • Redesigned welcome screen with common features and recent databases [#292]
  • +
  • Multiple updates to search behavior [#168, #213, #374, #471, #603, #654]
  • +
  • Added auto-type fields {CLEARFIELD}, {SPACE}, {{}, {}} [#267, #427, #480]
  • +
  • Fixed auto-type errors on Linux [#550]
  • +
  • Prompt user prior to executing a cmd:// URL [#235]
  • +
  • Entry attributes can be protected (hidden) [#220]
  • +
  • Added extended ascii to password generator [#538]
  • +
  • Added new database icon to toolbar [#289]
  • +
  • Added context menu entry to empty recycle bin in databases [#520]
  • +
  • Added "apply" button to entry and group edit windows [#624]
  • +
  • Added macOS tray icon and enabled minimize on close [#583]
  • +
  • Fixed issues with unclean shutdowns [#170, #580]
  • +
  • Changed keyboard shortcut to create new database to CTRL+SHIFT+N [#515]
  • +
  • Compare window title to entry URLs [#556]
  • +
  • Implemented inline error messages [#162]
  • +
  • Ignore group expansion and other minor changes when making database "dirty" [#464]
  • +
  • Updated license and copyright information on souce files [#632]
  • +
  • Added contributors list to about dialog [#629]
  • +
+
+
+ + +

Changes included in this release:

+
    +
  • Bumped KeePassHTTP version to 1.8.4.2
  • +
  • KeePassHTTP confirmation window comes to foreground [#466]
  • +
+
+
+ + +

Changes included in this release:

+
    +
  • Fix possible overflow in zxcvbn library [#363]
  • +
  • Revert HiDPI setting to avoid problems on laptop screens [#332]
  • +
  • Set file meta properties in Windows executable [#330]
  • +
  • Suppress error message when auto-reloading a locked database [#345]
  • +
  • Improve usability of question dialog when database is already locked by a different instance [#346]
  • +
  • Fix compiler warnings in QHttp library [#351]
  • +
  • Use unified toolbar on Mac OS X [#361]
  • +
  • Fix an issue on X11 where the main window would be raised instead of closed on Alt+F4 [#362]
  • +
+
+
+ + +

Changes included in this release:

+
    +
  • Ask for save location when creating a new database [#302]
  • +
  • Remove Libmicrohttpd dependency to clean up the code and ensure better OS X compatibility [#317, #265]
  • +
  • Prevent Qt from degrading Wifi network performance on certain platforms [#318]
  • +
  • Visually refine user interface on OS X and other platforms [#299]
  • +
  • Remove unusable tray icon setting on OS X [#293]
  • +
  • Fix compositing glitches on Ubuntu and prevent flashing when minimizing to the tray at startup [#307]
  • +
  • Fix AppImage tray icon on Ubuntu [#277, #273]
  • +
  • Fix global menu disappearing after restoring KeePassXC from the tray on Ubuntu [#276]
  • +
  • Fix result order in entry search [#320]
  • +
  • Enable HiDPI scaling on supported platforms [#315]
  • +
  • Remove empty directories from installation target [#282]
  • +
+
+
+ + +

Changes included in this release:

+
    +
  • Enabled HTTP plugin build; plugin is disabled by default and limited to localhost [#147]
  • +
  • Escape HTML in dialog boxes [#247]
  • +
  • Corrected crashes in favicon download and password generator [#233, #226]
  • +
  • Increase font size of password meter [#228]
  • +
  • Fixed compatibility with Qt 5.8 [#211]
  • +
  • Use consistent button heights in password generator [#229]
  • +
+
+
+ + +

Changes included in this release:

+
    +
  • Show unlock dialog when using autotype on a closed database [#10, #89]
  • +
  • Show different tray icon when database is locked [#37, #46]
  • +
  • Support autotype on Windows and OS X [#42, #60, #63]
  • +
  • Add delay feature to autotype [#76, #77]
  • +
  • Add password strength meter [#84, #92]
  • +
  • Add option for automatically locking the database when minimizing the window [#57]
  • +
  • Add feature to download favicons and use them as entry icons [#30]
  • +
  • Automatically reload and merge database when the file changed on disk [#22, #33, #93]
  • +
  • Add tool for merging two databases [#22, #47, #143]
  • +
  • Add --pw-stdin commandline option to unlock the database by providing a password on STDIN [#54]
  • +
  • Add utility script for reading the database password from KWallet [#55]
  • +
  • Fix some KeePassHTTP settings not being remembered [#34, #65]
  • +
  • Make search box persistent [#15, #67, #157]
  • +
  • Enhance search feature by scoping the search to selected group [#16, #118]
  • +
  • Improve interaction between search field and entry list [#131, #141]
  • +
  • Add stand-alone password-generator [#18, #92]
  • +
  • Don't require password repetition when password is visible [#27, #92]
  • +
  • Add support for entry attributes in autotype sequences [#107]
  • +
  • Always focus password field when opening the database unlock widget [#116, #117]
  • +
  • Fix compilation errors on various platforms [#53, #126, #130]
  • +
  • Restructure and improve kdbx-extract utility [#160]
  • +
+
+
+
+
diff --git a/share/linux/keepassxc.desktop b/share/linux/org.keepassxc.KeePassXC.desktop similarity index 62% rename from share/linux/keepassxc.desktop rename to share/linux/org.keepassxc.KeePassXC.desktop index 1e72c4f8..d3b007bd 100644 --- a/share/linux/keepassxc.desktop +++ b/share/linux/org.keepassxc.KeePassXC.desktop @@ -1,13 +1,16 @@ [Desktop Entry] Name=KeePassXC -GenericName=Community Password Manager +GenericName=Password Manager GenericName[de]=Passwortverwaltung GenericName[es]=Gestor de contraseñas GenericName[fr]=Gestionnaire de mot de passe GenericName[ru]=менеджер паролей +Comment=Community-driven port of the Windows application “KeePass Password Safe” Exec=keepassxc %f +TryExec=keepassxc Icon=keepassxc Terminal=false Type=Application -Categories=Qt;Utility; +Version=1.0 +Categories=Utility;Security;Qt; MimeType=application/x-keepass2; diff --git a/share/translations/keepassx_cs.ts b/share/translations/keepassx_cs.ts index 6904b188..737c2b03 100644 --- a/share/translations/keepassx_cs.ts +++ b/share/translations/keepassx_cs.ts @@ -67,6 +67,10 @@ Jádro systému: %3 %4 Include the following information whenever you report a bug: K hlášení chyby vždy připojte následující údaje: + + Distribution: %1 + Distribuce: %1 + AccessControlDialog @@ -1095,7 +1099,7 @@ Chcete ji přesto otevřít? Custom icon already exists - + Tato vlastní ikona už existuje @@ -1318,6 +1322,17 @@ Můžete ho importovat pomocí Databáze → Importovat databázi ve formátu Ke Jedná se o jednosměrný převod. Databázi, vzniklou z importu, nepůjde otevřít ve staré verzi KeePassX 0.4. + + KeePass2Writer + + Unable to issue challenge-response. + Nedaří se vyvolat výzva-odpověď. + + + Unable to calculate master key + Nedaří se spočítat hlavní klíč + + Main @@ -1338,7 +1353,7 @@ Jedná se o jednosměrný převod. Databázi, vzniklou z importu, nepůjde otev Existing single-instance lock file is invalid. Launching new instance. - + Existující uzamykací soubor, zajišťující spuštění pouze jedné instance, není platný. Spouští se nová instance. diff --git a/share/translations/keepassx_da.ts b/share/translations/keepassx_da.ts index 55e91dd0..563f6adb 100644 --- a/share/translations/keepassx_da.ts +++ b/share/translations/keepassx_da.ts @@ -67,6 +67,10 @@ Kerne: %3 %4 Include the following information whenever you report a bug: Inkludér følgende information når du indrapporterer en fejl: + + Distribution: %1 + + AccessControlDialog @@ -1313,6 +1317,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Kan ikke beregne hovednøgle + + Main diff --git a/share/translations/keepassx_de.ts b/share/translations/keepassx_de.ts index 8c55f99a..2fd20f04 100644 --- a/share/translations/keepassx_de.ts +++ b/share/translations/keepassx_de.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Geben Sie folgende Informationen an, wenn Sie einen Bug melden: + + Distribution: %1 + Distribution: %1 + AccessControlDialog @@ -1092,7 +1096,7 @@ Möchten Sie diese dennoch öffnen? Custom icon already exists - + Es gibt bereits ein eigenes Symbol @@ -1315,6 +1319,17 @@ Zum Importieren gehen Sie auf Datenbank > 'KeePass 1-Datenbank importier Dieser Vorgang ist nur in eine Richtung möglich. Die importierte Datenbank kann später nicht mehr mit der alten KeePassX-Version 0.4 geöffnet werden. + + KeePass2Writer + + Unable to issue challenge-response. + Fehler beim Ausführen des Challenge-Response-Verfahrens + + + Unable to calculate master key + Berechnung des Hauptschlüssels gescheitert + + Main @@ -1335,7 +1350,7 @@ Dieser Vorgang ist nur in eine Richtung möglich. Die importierte Datenbank kann Existing single-instance lock file is invalid. Launching new instance. - + Vorhandene einmal-Sperrdatei ist ungültig. Starte neuen Vorgang. diff --git a/share/translations/keepassx_el.ts b/share/translations/keepassx_el.ts index a641c0d5..5224c024 100644 --- a/share/translations/keepassx_el.ts +++ b/share/translations/keepassx_el.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Συμπεριλάβετε τις ακόλουθες πληροφορίες όποτε αναφέρετε κάποιο σφάλμα: + + Distribution: %1 + + AccessControlDialog @@ -1311,6 +1315,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Σε θέση να υπολογίσει το κύριο κλειδί + + Main diff --git a/share/translations/keepassx_en.ts b/share/translations/keepassx_en.ts index c971512a..7cc9fc3c 100644 --- a/share/translations/keepassx_en.ts +++ b/share/translations/keepassx_en.ts @@ -66,6 +66,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: + + Distribution: %1 + + AccessControlDialog @@ -1309,6 +1313,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + + + Main diff --git a/share/translations/keepassx_es.ts b/share/translations/keepassx_es.ts index 0fa7b14c..f8176b80 100644 --- a/share/translations/keepassx_es.ts +++ b/share/translations/keepassx_es.ts @@ -67,6 +67,10 @@ Núcleo: %3 %4 Include the following information whenever you report a bug: Incluya la información siguiente cuando informe sobre un error: + + Distribution: %1 + + AccessControlDialog @@ -1317,6 +1321,17 @@ Puede importarla haciendo clic en Base de datos > 'Importar base de dato Esta migración es en único sentido. No podrá abrir la base de datos importada con la vieja versión 0.4 de KeePassX. + + KeePass2Writer + + Unable to issue challenge-response. + No se pudo hacer el desafío/respuesta: + + + Unable to calculate master key + No se puede calcular la clave maestra + + Main diff --git a/share/translations/keepassx_eu.ts b/share/translations/keepassx_eu.ts index 738dd29c..ffbb4a9a 100644 --- a/share/translations/keepassx_eu.ts +++ b/share/translations/keepassx_eu.ts @@ -65,6 +65,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: + + Distribution: %1 + + AccessControlDialog @@ -1300,6 +1304,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Ezin izan da gako nagusia kalkulatu + + Main diff --git a/share/translations/keepassx_fi.ts b/share/translations/keepassx_fi.ts index 5f186256..c7d61473 100644 --- a/share/translations/keepassx_fi.ts +++ b/share/translations/keepassx_fi.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Sisällytä seuraavat tiedot aina kun ilmoitat ongelmasta: + + Distribution: %1 + + AccessControlDialog @@ -1314,6 +1318,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Pääavaimen laskeminen ei onnistu + + Main diff --git a/share/translations/keepassx_fr.ts b/share/translations/keepassx_fr.ts index aa04ae5c..90580e14 100644 --- a/share/translations/keepassx_fr.ts +++ b/share/translations/keepassx_fr.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Inclure l'information suivante lorsque vous signaler un bug: + + Distribution: %1 + + AccessControlDialog @@ -1318,6 +1322,17 @@ Vous pouvez l'importer en cliquant sur Base de données>'Importer u Il s'agit d'une migration à sens unique. Vous ne pourrez pas ouvrir la base de données importée avec l'ancienne version de KeePassX 0.4. + + KeePass2Writer + + Unable to issue challenge-response. + Impossible de lancer une challenge-réponse. + + + Unable to calculate master key + Impossible de calculer la clé maître + + Main diff --git a/share/translations/keepassx_hu.ts b/share/translations/keepassx_hu.ts index d4c2880f..dc987705 100644 --- a/share/translations/keepassx_hu.ts +++ b/share/translations/keepassx_hu.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Minden hibajelentésnél legyenek mellékelve ezek az információk: + + Distribution: %1 + Disztribúció: %1 + AccessControlDialog @@ -1094,7 +1098,7 @@ Mindenképp megnyitja? Custom icon already exists - + Az egyéni ikon már létezik @@ -1317,6 +1321,17 @@ Be lehet importálni az Adatbázis > „KeePass 1 adatbázis importálása… Ez egyirányú migráció. Nem lehet majd megnyitni az importált adatbázist a régi KeePassX 0.4 verzióval. + + KeePass2Writer + + Unable to issue challenge-response. + Nem lehet kiutalni a kihívás-választ. + + + Unable to calculate master key + Nem lehet kiszámítani a mesterkulcsot + + Main @@ -1337,7 +1352,7 @@ Ez egyirányú migráció. Nem lehet majd megnyitni az importált adatbázist a Existing single-instance lock file is invalid. Launching new instance. - + A meglévő egypéldányos zárolási fájl érvénytelen. Új példány indítása. diff --git a/share/translations/keepassx_id.ts b/share/translations/keepassx_id.ts index 8f7cd023..f7db658d 100644 --- a/share/translations/keepassx_id.ts +++ b/share/translations/keepassx_id.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Sertakan informasi berikut setiap Anda melaporkan bug: + + Distribution: %1 + + AccessControlDialog @@ -1317,6 +1321,17 @@ Anda bisa mengimpornya dengan mengklik Basis Data > 'Impor basis data Ke Ini adalah migrasi satu arah. Anda tidak akan bisa membuka basis data yang diimpor dengan versi lama KeePassX 0.4. + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Tidak bisa mengkalkulasi kunci utama + + Main diff --git a/share/translations/keepassx_it.ts b/share/translations/keepassx_it.ts index f60f0b26..5125a8b6 100644 --- a/share/translations/keepassx_it.ts +++ b/share/translations/keepassx_it.ts @@ -3,7 +3,7 @@ AboutDialog About KeePassXC - A proposito di KeePassXC + Info su KeePassXC About @@ -15,11 +15,11 @@ Debug Info - Informazioni di debug + Informazioni debug Copy to clipboard - Copia negli appunti + Copia negli Appunti Version %1 @@ -49,7 +49,7 @@ Kernel: %3 %4 Report bugs at: <a href="https://github.com/keepassxreboot/keepassxc/issues" style="text-decoration: underline;">https://github.com</a> - Segnala eventuali problemi su: <a href="https://github.com/keepassxreboot/keepassxc/issues" style="text-decoration: underline;">https://github.com</a> + Segnala eventuali problemi in: <a href="https://github.com/keepassxreboot/keepassxc/issues" style="text-decoration: underline;">https://github.com</a> KeePassXC is distributed under the terms of the GNU General Public License (GPL) version 2 or (at your option) version 3. @@ -57,7 +57,7 @@ Kernel: %3 %4 Project Maintainers: - Manutentori del progetto: + Manutenzione progetto: <a href="https://github.com/keepassxreboot/keepassxc/graphs/contributors">See Contributions on GitHub</a> @@ -65,7 +65,11 @@ Kernel: %3 %4 Include the following information whenever you report a bug: - Includi le seguenti informazioni quando segnali un problema: + Quando segnali un problema includi le seguenti informazioni: + + + Distribution: %1 + Distribuzione: %1 @@ -85,23 +89,23 @@ Kernel: %3 %4 %1 has requested access to passwords for the following item(s). Please select whether you want to allow access. - %1 ha richiesto accesso alle passwords per il seguente elemento(i). -Perfavore seleziona se vuoi consentire l'accesso. + %1 ha richiesto accesso alle passwords per il seguente elemento/i. +Seleziona se vuoi consentire l'accesso. KeePassXC HTTP Confirm Access - KeePassXC HTTP Conferma Accesso + KeePassXC HTTP conferma accesso AutoType Couldn't find an entry that matches the window title: - Impossibile trovare una voce che corrisponda al titolo della finestra + Impossibile trovare un elemento che corrisponda al titolo della finestra: Auto-Type - KeePassXC - Auto-Completamento - KeePassXC + Auto completamento - KeePassXC @@ -123,11 +127,11 @@ Perfavore seleziona se vuoi consentire l'accesso. AutoTypeSelectDialog Select entry to Auto-Type: - Seleziona una voce per Auto-Type: + Seleziona un elemento per l'auto completamento: Auto-Type - KeePassXC - Auto-Completamento - KeePassXC + KeePassXC - Auto completamento @@ -170,7 +174,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Select a key file - Seleziona il file chiave + Seleziona un file chiave Do you really want to use an empty string as password? @@ -178,7 +182,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Different passwords supplied. - Sono state fornite password differenti. + Sono state inserite password differenti. Failed to set %1 as the Key file: @@ -196,7 +200,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Refresh - Ricarica + Aggiorna Empty password @@ -204,18 +208,18 @@ Perfavore seleziona se vuoi consentire l'accesso. Changing master key failed: no YubiKey inserted. - Cambio password principale fallito: nessuna YubiKey inserita. + Modifica password principale fallitoa. Nessuna YubiKey inserita. CloneDialog Clone Options - Opzioni di clonazione + Opzioni clonazione Replace username and password with references - Sostituisci nome user e password con riferimenti + Sostituisci nome utente e password con riferimenti Copy history @@ -238,7 +242,7 @@ Perfavore seleziona se vuoi consentire l'accesso. size, rows, columns - dimensione, righe. colonne + dimensione, righe, colonne Encoding @@ -262,11 +266,11 @@ Perfavore seleziona se vuoi consentire l'accesso. First record has field names - Il primo record ha i nomi dei campi + Il primo record contiene i nomi dei campi Number of headers line to discard - Numero di righe di intestazione da scartare + Numero righe di intestazione da scartare Consider '\' an escape character @@ -278,7 +282,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Column layout - Disposizione di colonna + Disposizione colonna Not present in CSV file @@ -286,7 +290,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Empty fieldname - Nome di campo vuoto + Nome campo vuoto column @@ -302,7 +306,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Error(s) detected in CSV file ! - Errore(i) rilevati nel file CSV! + Errore/i rilevati nel file CSV! more messages skipped] @@ -315,7 +319,7 @@ Perfavore seleziona se vuoi consentire l'accesso. CSV import: writer has errors: - Importazione CSV: lo scrittore ha errori: + Importazione CSV: rilevati errori: @@ -323,7 +327,7 @@ Perfavore seleziona se vuoi consentire l'accesso. CsvImportWizard Import CSV file - Importare un file CSV + Importa file CSV Error @@ -389,7 +393,7 @@ Perfavore seleziona se vuoi consentire l'accesso. Refresh - Ricarica + Aggiorna Challenge Response: @@ -416,36 +420,36 @@ Perfavore seleziona se vuoi consentire l'accesso. Unable to open the database. - Impossibile aprire il database + Impossibile aprire il database. Success - Successo + Completato The database has been successfully repaired You can now save it. - Il database è stato riparato con successo + Il database è stato correttamente riparato. Adesso puoi salvarlo. Unable to repair the database. - Impossibile riparare il database + Impossibile riparare il database. DatabaseSettingsWidget Database name: - Nome del database: + Nome database: Database description: - Descrizione del database: + Descrizione database: Transform rounds: - Round di trasformazione: + Arrotondamenti trasformazione: Default username: @@ -453,23 +457,23 @@ Adesso puoi salvarlo. MiB - MiB + MB Benchmark - Prestazione + Prestazioni Max. history items: - Max. oggetti nella cronologia: + Oggetti max. nella cronologia: Max. history size: - Max. grandezza della cronologia: + Grandezza max. cronologia: Use recycle bin - Usa il cestino + Usa il Cestino AES: 256 Bit (default) @@ -488,7 +492,7 @@ Adesso puoi salvarlo. DatabaseTabWidget Root - Root + Radice KeePass 2 Database @@ -520,21 +524,21 @@ Adesso puoi salvarlo. Close? - Chiudere? + Vuoi chiudere? Save changes? - Salvare modifiche? + Vuoi salvare modifiche? "%1" was modified. Save changes? "%1" è stata modificato. -Salvare le modifiche? +Vuoi salvare le modifiche? Writing the database failed. - Scrittura del database non riuscita. + Scrittura del database fallita. Save database as @@ -556,7 +560,7 @@ Salvare le modifiche? Can't lock the database as you are currently editing it. Please press cancel to finish your changes or discard them. Non è possibile bloccare il database dato che lo stai modificando. -Premere Annulla per completare le modifiche o scartarle. +Seleziona 'Annulla' per completare le modifiche o scartarle. This database has never been saved. @@ -576,19 +580,19 @@ Altrimenti le modifiche verranno perse. "%1" is in edit mode. Discard changes and close anyway? "%1" è in modalità modifica. -Annullare le modifiche e chiudere comunque? +Vuoi annullare le modifiche e chiudere comunque? Export database to CSV file - Esporta il database come file CSV + Esporta database come file CSV CSV file - file CSV + File CSV Writing the CSV file failed. - Scrittura del file CSV non riuscita. + Scrittura file CSV fallita. Unable to open the database. @@ -610,18 +614,18 @@ Vuoi salvare comunque? Database already opened - Database già caricato + Database già aperto The database you are trying to open is locked by another instance of KeePassXC. Do you want to open it anyway? - Il database che stai provando ad aprire è bloccato da una altra instanza di KeePassXC. + Il database che stai cercando di aprire è bloccato da un'altra instanza di KeePassXC. Vuoi aprilo comunque? Open read-only - Aperto in sola lettura + Apri in sola lettura File opened in read only mode. @@ -629,42 +633,42 @@ Vuoi aprilo comunque? Open CSV file - Apri un file CSV + Apri file CSV DatabaseWidget Change master key - Cambia chiave principale + Modifica chiave principale Delete entry? - Eliminare voce? + Vuoi eliminare l'elemento? Do you really want to delete the entry "%1" for good? - Vuoi veramente eliminare la voce "%1"? + Vuoi veramente eliminare l'elemento "%1"? Delete entries? - Eliminare voci? + Vuoi eliminare gli elementi? Do you really want to delete %1 entries for good? - Vuoi veramente eliminare %1 voci? + Vuoi veramente eliminare %1 elementi? Move entries to recycle bin? - Muovere le voci nel cestino? + Vuoi spostare gli elementi nel Cestino? Do you really want to move %n entry(s) to the recycle bin? - Vuoi veramente spostare %n voce(i) nel cestino?Vuoi veramente spostare %n voce(i) nel cestino? + Vuoi veramente spostare %n elemento nel Cestino?Vuoi veramente spostare %n elementi nel Cestino? Delete group? - Eliminare gruppo? + Vuoi eliminare il gruppo? Do you really want to delete the group "%1" for good? @@ -676,23 +680,23 @@ Vuoi aprilo comunque? Move entry to recycle bin? - Muovere la voce nel cestino? + Vuoi spostare l'elemento nel Cestino? Do you really want to move entry "%1" to the recycle bin? - Vuoi veramente spostare la voce "%1" nel cestino? + Vuoi veramente spostare l'elemento "%1" nel Cestino? Searching... - Ricerca in corso... + Ricerca... No current database. - Nessun database corrente. + Nessun database attuale. No source database, nothing to do. - Nessun database sorgente, niente da fare. + Nessun database sorgente. Nessuna operazione da fare. Search Results (%1) @@ -704,11 +708,11 @@ Vuoi aprilo comunque? Execute command? - Esegui comando? + Vuoi eseguire il comando? Do you really want to execute the following command?<br><br>%1<br> - Sei sicuro di voler eseguire il comando seguente?<br><br>%1<br> + Sei sicuro di voler eseguire il seguente comando?<br><br>%1<br> Remember my choice @@ -716,19 +720,19 @@ Vuoi aprilo comunque? Autoreload Request - Auto ricarica le richieste + Ricaricamento automatico richieste The database file has changed. Do you want to load the changes? - Il file del database è cambiato. Vuoi caricare le modifiche? + Il file del database è stato modificato. Vuoi caricare le modifiche? Merge Request - Richiesta di fusione + Richiesta di unione The database file has changed and you have unsaved changes.Do you want to merge your changes? - Il file del database è cambiato e ci sono delle modifiche non salvate. Vuoi unire i tuoi cambiamenti. + Il file del database è stato modificato e ci sono delle modifiche non salvate. Vuoi unire i tuoi cambiamenti? Could not open the new database file while attempting to autoreload this database. @@ -736,18 +740,18 @@ Vuoi aprilo comunque? Empty recycle bin? - Svuotare il cestino? + Vuoi svuotare il Cestino? Are you sure you want to permanently delete everything from your recycle bin? - Sei sicuro di voler eliminare tutto definitivamente dal tuo Cestino? + Sei sicuro di voler eliminare tutto definitivamente dal Cestino? EditEntryWidget Entry - Voce + Elemento Advanced @@ -759,7 +763,7 @@ Vuoi aprilo comunque? Auto-Type - Auto-Type + Completamento automatico Properties @@ -771,19 +775,19 @@ Vuoi aprilo comunque? Entry history - Cronologia voce + Cronologia elemento Add entry - Aggiungere voce + Aggiungi elemento Edit entry - Modificare voce + Modifica elemento Different passwords supplied. - Sono state immesse password differenti. + Sono state inserite password differenti. New attribute @@ -799,7 +803,7 @@ Vuoi aprilo comunque? Save attachment - Salvare l'allegato + Salva allegato Unable to save the attachment: @@ -825,7 +829,7 @@ Vuoi aprilo comunque? Confirm Remove - Conferma l'eliminazione + Conferma eliminazione Are you sure you want to remove this attribute? @@ -833,7 +837,7 @@ Vuoi aprilo comunque? [PROTECTED] Press reveal to view or edit - [PROTETTO] Premere rivelare per visualizzare o modificare + [PROTETTO] Seleziona 'Rileva' per visualizzare o modificare Are you sure you want to remove this attachment? @@ -844,7 +848,7 @@ Vuoi aprilo comunque? EditEntryWidgetAdvanced Additional attributes - Attributi addizionali + Attributi aggiuntivi Add @@ -868,22 +872,22 @@ Vuoi aprilo comunque? Edit Name - Modifica il nome + Modifica nome Protect - Proteggere + Proteggi Reveal - Rivelare + Rivela EditEntryWidgetAutoType Enable Auto-Type for this entry - Abilita Auto-Type per questa voce + Abilita completamento automatico per questo elemento + @@ -899,11 +903,11 @@ Vuoi aprilo comunque? Inherit default Auto-Type sequence from the &group - Eredita la sequenza per l'Auto-Completamento dal &gruppo + Eredita la sequenza per il completamento automatico dal &gruppo &Use custom Auto-Type sequence: - &Usa sequenza di Auto-Completamento personalizzata: + &Usa sequenza di compeltamento automatico personalizzata: Use default se&quence @@ -915,14 +919,14 @@ Vuoi aprilo comunque? Window Associations - Associazioni di finestra + Associazioni finestra EditEntryWidgetHistory Show - Mostra + Visualizza Restore @@ -961,7 +965,7 @@ Vuoi aprilo comunque? Expires - Scade: + Scade Presets @@ -1027,15 +1031,15 @@ Vuoi aprilo comunque? Auto-Type - Auto-Type + Completamento automatico &Use default Auto-Type sequence of parent group - &Usa la sequenza di auto-digitazione predefinita del gruppo genitore + &Usa la sequenza di completamento automatico predefinita del gruppo genitore Set default Auto-Type se&quence - Imposta la se&quenza predefinita di auto-digitazione + Imposta se&quenza predefinita di completamento automatico @@ -1066,7 +1070,7 @@ Vuoi aprilo comunque? Unable to fetch favicon. - Impossibile scaricare la favicon. + Impossibile scaricare favicon. Can't read icon @@ -1082,19 +1086,19 @@ Vuoi aprilo comunque? Confirm Delete - Conferma la cancellazione + Conferma eliminazione This icon is used by %1 entries, and will be replaced by the default icon. Are you sure you want to delete it? - Questa icona viene utilizzata da %1 voci, e sarà sostituita dall'icona predefinita. Sei sicuro di volerla eliminare? + Questa icona viene usata da %1 elementi, e sarà sostituita dall'icona predefinita. Sei sicuro di volerla eliminare? Hint: You can enable Google as a fallback under Tools>Settings>Security - Suggerimento: è possibile abilitare Google come ripiego in Strumenti>Impostazioni>Sicurezza + Suggerimento: è possibile abilitare Google come alternativa in 'Strumenti'>'Impostazioni'>'Sicurezza' Custom icon already exists - + L'icona personalizzata esiste già @@ -1120,7 +1124,7 @@ Vuoi aprilo comunque? Entry - Clone - - Clona + - Clona @@ -1188,11 +1192,11 @@ Vuoi aprilo comunque? Character Types - Tipi di carattere + Tipi carattere Upper Case Letters - Lettere Maiuscole + Lettere maiuscole A-Z @@ -1200,7 +1204,7 @@ Vuoi aprilo comunque? Lower Case Letters - Lettere Minuscole + Lettere minuscole a-z @@ -1273,7 +1277,7 @@ Vuoi aprilo comunque? Root - Root + Radice Unable to calculate master key @@ -1313,8 +1317,19 @@ You can import it by clicking on Database > 'Import KeePass 1 database...'. This is a one-way migration. You won't be able to open the imported database with the old KeePassX 0.4 version. Il file selezionato è un vecchio database di KeePass 1 (.kdb). -Puoi importarlo facendo clic su Database > 'Importa database KeePass 1...'. -Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il database importato con la vecchia versione di KeePassX 0.4. +È possibile importarlo facendo clic su 'Database' > 'Importa database KeePass 1...'. +Si tratta di una migrazione unidirezionale. Non sarà possibile aprire il database importato con la vecchia versione di KeePassX 0.4. + + + + KeePass2Writer + + Unable to issue challenge-response. + Non in grado dare la risposta di verifica. + + + Unable to calculate master key + Impossibile calcolare la chiave principale @@ -1333,11 +1348,11 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Another instance of KeePassXC is already running. - Un'altra istanza di KeePassXC è già in esecuzione. + È già in esecuzione un'altra istanza di KeePassXC. Existing single-instance lock file is invalid. Launching new instance. - + Il file di blocco singola istanza non è valido. Esegui una nuova istanza. @@ -1352,11 +1367,11 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Copy username to clipboard - Copia nome utente negli appunti + Copia nome utente negli Appunti Copy password to clipboard - Copia password negli appunti + Copia password negli Appunti Settings @@ -1364,7 +1379,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Show toolbar - Mostra barra degli strumenti + Visualizza barra strumenti read-only @@ -1372,7 +1387,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Toggle window - Cambia finestra + Abilita/disabilita finestra KeePass 2 Database @@ -1384,11 +1399,11 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Save repaired database - Salva il database riparato + Salva database riparato Writing the database failed. - Scrittura del database non riuscita. + Scrittura database fallita. &Recent databases @@ -1400,7 +1415,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Copy att&ribute to clipboard - Copia gli att&ributi nella clipboard + Copia gli att&ributi negli Appunti &Groups @@ -1440,7 +1455,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data &View/Edit entry - &Vedi/Modifica l'elemento + &Visualizza/modifica elemento &Delete entry @@ -1460,7 +1475,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data &Database settings - Impostazioni &Database + Impostazioni &database &Clone entry @@ -1496,7 +1511,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data &Perform Auto-Type - &Esegui Auto-Completamento + &Esegui completamento automatico &Open URL @@ -1520,11 +1535,11 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Password Generator - Generatore Password + Genera password Clear history - Cancella cronologia + Azzera cronologia &Database @@ -1540,7 +1555,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Empty recycle bin - Svuota il cestino + Svuota il Cestino Access error for config file %1 @@ -1552,7 +1567,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Please touch the button on your YubiKey! - Prego tocca il pulsante sulla tua YubiKey! + Seleziona il pulsante nelal YubiKey! &Help @@ -1564,27 +1579,27 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Sa&ve database as... - Sal&va il database come... + Sal&va database come... Change &master key... - Ca&mbia la chiave principale... + &Modifica chiave principale... &Export to CSV file... - &Esporta su un file CSV... + &Esporta in file CSV... Import KeePass 1 database... - Importa un database di KeePass 1... + Importa database di KeePass 1... Import CSV file... - Importa un file CSV... + Importa file CSV... Re&pair database... - Ri&para il database... + Ri&para database... Set up TOTP... @@ -1595,7 +1610,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data OptionDialog Dialog - Dialogo + Finestra General @@ -1603,15 +1618,15 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Sh&ow a notification when credentials are requested - M&ostra una notifica quando sono richeste le credenziali + Visualizza una n&otifica quando sono richeste le credenziali Sort matching entries by &username - Ordina le voci trovate per &nome utente + Ordina elementi trovati per &nome utente Re&move all stored permissions from entries in active database - R&imuovi tutti i permessi presenti dalle voci nel database attivo + R&imuovi tutti i permessi presenti negli elementi nel database attivo Advanced @@ -1619,15 +1634,15 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Always allow &access to entries - Permetti sempre di &accedere alle voci + Permetti sempre di &accedere agli elementi Always allow &updating entries - Permetti sempre di &aggiornare le voci + Permetti sempre di &aggiornare gli elementi Searc&h in all opened databases for matching entries - Cerc&a in tutti i database aperti per la ricerca delle voci + Cerc&a in tutti i database aperti gli elementi corrispondenti HTTP Port: @@ -1643,7 +1658,7 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Sort &matching entries by title - Ordina le voci per &titolo + Ordina gli elementi per &titolo KeePassXC will listen to this port on 127.0.0.1 @@ -1656,8 +1671,8 @@ Si tratta di una migrazione unidirezionale. Non sarai in grado di aprire il data Cannot bind to privileged ports below 1024! Using default port 19455. - Non è possibile collegarsi a porte sotto la 1024! -Utilizza la porta predefinita 19455. + Non è possibile usare porte sotto la 1024! +Usa la porta predefinita 19455. R&emove all shared encryption keys from active database @@ -1673,7 +1688,7 @@ Utilizza la porta predefinita 19455. This is required for accessing your databases from ChromeIPass or PassIFox - Questo è necessario per accedere ai tuoi database da ChromeIPass o PassIFox + Questo è necessario per accedere ai database da ChromeIPass o PassIFox Enable KeePassHTTP server @@ -1681,23 +1696,23 @@ Utilizza la porta predefinita 19455. Only returns the best matches for a specific URL instead of all entries for the whole domain. - Restituisce solo le corrispondenze migliori per un URL specifico invece di tutte le voci per l'intero dominio. + Restituisci solo le corrispondenze migliori per un'URL specifica invece di tutte gli elementi per l'intero dominio. &Return only best matching entries - &Restituisci solo le migliori voci corrispondenti + &Restituisci solo gli elementi corrispondenti migliori Only entries with the same scheme (http://, https://, ftp://, ...) are returned. - Solo le voci con lo stesso schema (http://, https://, ftp: //, ...) vengono restituite. + Solo gli elementi con lo stesso schema (http://, https://, ftp: //, ...) vengono restituite. &Match URL schemes - Co&mbina gli schemi URL + Sche&ma corrispndenza URL Password Generator - Generatore Password + Genera password Only the selected database has to be connected with a client. @@ -1718,7 +1733,7 @@ Modificale solo se sai quello che stai facendo. Character Types - Tipi di carattere + Tipi carattere Upper Case Letters @@ -1794,7 +1809,7 @@ Modificale solo se sai quello che stai facendo. Good - Sufficente + Buona Excellent @@ -1810,19 +1825,19 @@ Modificale solo se sai quello che stai facendo. Passphrase - Frase d'accesso + Frase accesso Wordlist: - Elenco di termini: + Elenco termini: Word Count: - Conteggio delle parole: + Conteggio parole: Word Separator: - Separatore delle parole: + Separatore parole: Copy @@ -1887,7 +1902,7 @@ Modificale solo se sai quello che stai facendo. YubiKey[%1] Challenge Response - Slot %2 - %3 - YubiKey [%1] Risposta di verifica - Slot %2 - %3 + YubiKey [%1] risposta di verifica - slot %2 - %3 Press @@ -1895,7 +1910,7 @@ Modificale solo se sai quello che stai facendo. Passive - Passivo + Passiva @@ -1936,7 +1951,7 @@ Modificale solo se sai quello che stai facendo. SearchWidget Case Sensitive - Case sensitive + Sensibile maiuscole/minuscole Search @@ -1944,7 +1959,7 @@ Modificale solo se sai quello che stai facendo. Clear - Pulisci + Azzera Search... @@ -1960,7 +1975,7 @@ Modificale solo se sai quello che stai facendo. A shared encryption-key with the name "%1" already exists. Do you want to overwrite it? - Una chiave di criptazione condivisa con il nome "%1" già esiste. + Una chiave di criptazione condivisa con il nome "%1" esiste già. Vuoi sovrascriverla? @@ -1971,11 +1986,11 @@ Vuoi sovrascriverla? The active database is locked! Please unlock the selected database or choose another one which is unlocked. Il database attivo è bloccato! -Sblocca il database selezionato o scegli un altro che è sbloccato +Sblocca il database selezionato o scegli un altro database sbloccato. Successfully removed %1 encryption-%2 from KeePassX/Http Settings. - Rimosso con successo %1 encryption-%2 dalle impostazioni KeePassX/Http. + Rimosso correttamente %1 encryption-%2 dalle impostazioni KeePassX/Http. No shared encryption-keys found in KeePassHttp Settings. @@ -1983,11 +1998,11 @@ Sblocca il database selezionato o scegli un altro che è sbloccato The active database does not contain an entry of KeePassHttp Settings. - Il database attivo non contiene nessuna voce delle impostazioni di KeePassHttp. + Il database attivo non contiene nessun elemento delle impostazioni di KeePassHttp. Removing stored permissions... - Rimuovi i permessi salvati... + Rimozione dei permessi salvati... Abort @@ -1995,11 +2010,11 @@ Sblocca il database selezionato o scegli un altro che è sbloccato Successfully removed permissions from %1 %2. - Permessi rimossi con successo da %1 %2. + Permessi rimossi correttamente da %1 %2. The active database does not contain an entry with permissions. - Il database attivo non contiene una voce con permessi. + Il database attivo non contiene un elemento con permessi. KeePassXC: New key association request @@ -2010,40 +2025,40 @@ Sblocca il database selezionato o scegli un altro che è sbloccato If you would like to allow it access to your KeePassXC database give it a unique name to identify and accept it. Hai ricevuto una richiesta di associazione per la chiave sovrastante. -Se vuoi permetterle di accedere al tuo database KeePassXC +Se vuoi permetterle di accedere al database KeePassXC imposta un nome unico per identificarla ed accettarla. KeePassXC: Overwrite existing key? - KeePassXC: Sovrascrivere chiave esistente? + KeePassXC- Vuoi sovrascrivere la chiave esistente? KeePassXC: Update Entry - KeePassXC: Aggiorna voce + KeePassXC- Aggiorna elemento KeePassXC: Database locked! - KeePassXC: Database bloccato! + KeePassXC- Database bloccato! KeePassXC: Removed keys from database - KeePassXC: Rimuovi chiavi dal database + KeePassXC - Chiavi rimosse dal database KeePassXC: No keys found - KeePassXC: Nessuna chiave trovata + KeePassXC - Nessuna chiave trovata KeePassXC: Settings not available! - KeePassXC: Impostazioni non disponibili! + KeePassXC - Impostazioni non disponibili! KeePassXC: Removed permissions - KeePassXC: Rimossi permessi + KeePassXC - Permessi rimossi KeePassXC: No entry with permissions found! - KeePassXC: Nessuna voce con permessi trovata! + KeePassXC - Nessun elemento con permessi trovata! @@ -2081,15 +2096,15 @@ imposta un nome unico per identificarla ed accettarla. Minimize when copying to clipboard - Minimizza quando si copia negli appunti + Minimizza quando si copia negli Appunti Use group icon on entry creation - Usa l'icona del gruppo alla creazione di una voce + Usa icona del gruppo alla creazione di un elemento Global Auto-Type shortcut - Scorciatoia Auto-Type globale + Scorciatoia completamento automatico globale Language @@ -2097,7 +2112,7 @@ imposta un nome unico per identificarla ed accettarla. Show a system tray icon - Mostra un'icona nell'area di notifica del sistema + Visualizza un'icona nell'area di notifica del sistema Hide window to system tray when minimized @@ -2105,7 +2120,7 @@ imposta un nome unico per identificarla ed accettarla. Load previous databases on startup - Carica i database precedenti all'avvio + All'avvio carica i database precedenti Automatically reload the database when modified externally @@ -2129,23 +2144,23 @@ imposta un nome unico per identificarla ed accettarla. Don't mark database as modified for non-data changes (e.g., expanding groups) - Non contrassegnare il database come modificato per modifiche non riguardanti i dati (ad es., espansione di gruppi) + Non contrassegnare il database come modificato per modifiche non riguardanti i dati (ad es. espansione dei gruppi) Auto-Type - Auto-Type + Completamento automatico Use entry title and URL to match windows for global Auto-Type - Usa il titolo della voce e l'URL per abbinare le finestre per l'auto-digitazione globale + Usa il titolo dell'elemento e l'URL per abbinare le finestre per il completamento automatico globale Always ask before performing Auto-Type - Chiedi sempre prima di effettuare l'auto-digitazione + Chiedi sempre prima di effettuare il completamento automatico Auto-Type delay - Ritardo dell'auto-digitazione + Ritardo completamento automatico ms @@ -2160,7 +2175,7 @@ imposta un nome unico per identificarla ed accettarla. SettingsWidgetSecurity Clear clipboard after - Pulisci appunti dopo + Svuota Appunti dopo sec @@ -2172,7 +2187,7 @@ imposta un nome unico per identificarla ed accettarla. Show passwords in cleartext by default - Mostra la password in chiaro in maniera predefinita + Visualizza la password in chiaro in maniera predefinita Lock databases after minimizing the window @@ -2192,7 +2207,7 @@ imposta un nome unico per identificarla ed accettarla. Lock databases when session is locked or lid is closed - Bloccare i database quando la sessione è bloccata o il coperchio è chiuso + Blocca i database quando la sessione è bloccata o il coperchio è chiuso Privacy @@ -2200,7 +2215,7 @@ imposta un nome unico per identificarla ed accettarla. Use Google as fallback for downloading website icons - Utilizza Google come ripiego per scaricare le icone del sito web + Usa Google come alternativa per scaricare le icone dal sito web @@ -2215,11 +2230,11 @@ imposta un nome unico per identificarla ed accettarla. Use custom settings - Utilizza le impostazioni personalizzate + Usa le impostazioni personalizzate Note: Change these settings only if you know what you are doing. - Nota: modificare queste impostazioni solo se sai quello che stai facendo. + Nota: modifica queste impostazioni solo se sai quello che stai facendo. Time step: @@ -2235,7 +2250,7 @@ imposta un nome unico per identificarla ed accettarla. Code size: - Dimensioni del codice: + Dimensioni codice: sec @@ -2258,7 +2273,7 @@ imposta un nome unico per identificarla ed accettarla. Expires in - Scade in + Scade tra seconds @@ -2315,7 +2330,7 @@ imposta un nome unico per identificarla ed accettarla. KeePassXC - cross-platform password manager - KeePassXC - gestore password multipiattaforma + KeePassXC - Gestore password multipiattaforma read password of the database from stdin @@ -2335,11 +2350,11 @@ imposta un nome unico per identificarla ed accettarla. Use a GUI prompt unlocking the database. - Utilizza un sollecito grafico per lo sblocco del database. + Usa una richiesta grafica per lo sblocco del database. Name of the entry to clip. - Nome della voce da troncare. + Nome dell'elemento da tagliare. Extract and print the content of a database. @@ -2355,7 +2370,7 @@ imposta un nome unico per identificarla ed accettarla. List database entries. - Elenco delle voci del database. + Elenco degli elementi del database. Path of the group to list. Default is / @@ -2363,7 +2378,7 @@ imposta un nome unico per identificarla ed accettarla. Print the UUIDs of the entries and groups. - Stampa gli UUID delle voci e dei gruppi. + Stampa gli UUID degli elementi e dei gruppi. Merge two databases. @@ -2371,15 +2386,15 @@ imposta un nome unico per identificarla ed accettarla. Path of the database to merge into. - Percorso del database di destinazione da unire + Percorso del database destinazione da unire. Path of the database to merge from. - Percorso del database di partenza da unire. + Percorso del database sorgente da unire. Use the same password for both database files. - Utilizza la stessa password per entrambi i file di database. + Usa la stessa password per entrambi i file dei database. Show a password. @@ -2387,7 +2402,7 @@ imposta un nome unico per identificarla ed accettarla. Name of the entry to show. - Nome della voce da mostrare. + Nome dell'elemento da visualizzare. \ No newline at end of file diff --git a/share/translations/keepassx_ja.ts b/share/translations/keepassx_ja.ts index 4ac8b7ee..28b7c533 100644 --- a/share/translations/keepassx_ja.ts +++ b/share/translations/keepassx_ja.ts @@ -67,6 +67,10 @@ CPU アーキテクチャ: %2 Include the following information whenever you report a bug: バグを報告する際に下記の情報を含めてください: + + Distribution: %1 + + AccessControlDialog @@ -1094,7 +1098,7 @@ Do you want to open it anyway? Custom icon already exists - + カスタムアイコンは既に存在します @@ -1317,6 +1321,17 @@ This is a one-way migration. You won't be able to open the imported databas これは一方向の移行操作であり、インポートされたデータベースは古い KeePassX 0.4 のバージョンでは開くことはできません。 + + KeePass2Writer + + Unable to issue challenge-response. + チャレンジレスポンスを発行することができません。 + + + Unable to calculate master key + マスターキーを計算できません + + Main @@ -1337,7 +1352,7 @@ This is a one-way migration. You won't be able to open the imported databas Existing single-instance lock file is invalid. Launching new instance. - + 既存のシングルインスタンスロックファイルは無効です。新しいインスタンスを起動します。 diff --git a/share/translations/keepassx_kk.ts b/share/translations/keepassx_kk.ts index 07305271..4531122b 100644 --- a/share/translations/keepassx_kk.ts +++ b/share/translations/keepassx_kk.ts @@ -64,6 +64,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: + + Distribution: %1 + + AccessControlDialog @@ -1307,6 +1311,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Басты парольді есептеу мүмкін емес + + Main diff --git a/share/translations/keepassx_ko.ts b/share/translations/keepassx_ko.ts index ff6c208b..0b528474 100644 --- a/share/translations/keepassx_ko.ts +++ b/share/translations/keepassx_ko.ts @@ -67,6 +67,10 @@ CPU 아키텍처: %2 Include the following information whenever you report a bug: 버그를 보고할 때 다음 정보를 포함하십시오: + + Distribution: %1 + + AccessControlDialog @@ -1315,6 +1319,17 @@ This is a one-way migration. You won't be able to open the imported databas 이 작업은 한 방향으로만 이뤄집니다. 가져온 데이터베이스는 KeePassX 0.4 버전에서 열 수 없습니다. + + KeePass2Writer + + Unable to issue challenge-response. + 질의 응답을 실행할 수 없습니다. + + + Unable to calculate master key + 마스터 키를 계산할 수 없습니다 + + Main diff --git a/share/translations/keepassx_lt.ts b/share/translations/keepassx_lt.ts index be7a82e0..a9a48383 100644 --- a/share/translations/keepassx_lt.ts +++ b/share/translations/keepassx_lt.ts @@ -67,6 +67,10 @@ Branduolys: %3 %4 Include the following information whenever you report a bug: Pranešdami apie klaidą, visuomet pateikite ir šią informaciją: + + Distribution: %1 + Platinimas: %1 + AccessControlDialog @@ -1094,7 +1098,7 @@ Ar vis tiek norite ją atverti? Custom icon already exists - + Tinkinta piktograma jau yra @@ -1317,6 +1321,17 @@ Jūs galite ją importuoti, nuspausdami Duomenų bazė > "Importuoti Kee Tai yra vienakryptis perkėlimas. Jūs negalėsite atverti importuotos duomenų bazės, naudodami senąją KeePassX 0.4 versija. + + KeePass2Writer + + Unable to issue challenge-response. + Nepavyko išduoti iššūkio atsakymo. + + + Unable to calculate master key + Nepavyko apskaičiuoti pagrindinio rakto + + Main @@ -1337,7 +1352,7 @@ Tai yra vienakryptis perkėlimas. Jūs negalėsite atverti importuotos duomenų Existing single-instance lock file is invalid. Launching new instance. - + Esamas vieno egzemplioriaus užrakto failas yra neteisingas. Paleidžiamas naujas egzempliorius. diff --git a/share/translations/keepassx_nl_NL.ts b/share/translations/keepassx_nl_NL.ts index c8fd30dd..e18f1a90 100644 --- a/share/translations/keepassx_nl_NL.ts +++ b/share/translations/keepassx_nl_NL.ts @@ -64,6 +64,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: + + Distribution: %1 + + AccessControlDialog @@ -1310,6 +1314,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Niet mogelijk om hoofdsleutel te berekenen + + Main diff --git a/share/translations/keepassx_pl.ts b/share/translations/keepassx_pl.ts index 1ed4ed05..66574202 100644 --- a/share/translations/keepassx_pl.ts +++ b/share/translations/keepassx_pl.ts @@ -67,6 +67,10 @@ Jądro: %3 %4 Include the following information whenever you report a bug: Uwzględnij następujące informacje, gdy zgłaszasz błąd: + + Distribution: %1 + Dystrybucja: %1 + AccessControlDialog @@ -1095,7 +1099,7 @@ Czy chcesz ją otworzyć mimo to? Custom icon already exists - + Ikona niestandardowa już istnieje @@ -1318,6 +1322,17 @@ Możesz zaimportować ją przez wybranie Baza danych > 'Importuj bazę d Jest to migracja w jedną stronę. Nie będzie można otworzyć importowanej bazy danych za pomocą starej wersji KeePassX 0.4. + + KeePass2Writer + + Unable to issue challenge-response. + Nie można wywołać wyzwania-odpowiedzi. + + + Unable to calculate master key + Nie mogę wyliczyć głównego klucza + + Main @@ -1338,7 +1353,7 @@ Jest to migracja w jedną stronę. Nie będzie można otworzyć importowanej baz Existing single-instance lock file is invalid. Launching new instance. - + Istniejący plik blokady pojedynczego wystąpienia jest nieprawidłowy. Uruchamianie nowego wystąpienia. diff --git a/share/translations/keepassx_pt_BR.ts b/share/translations/keepassx_pt_BR.ts index b4f79260..1ea54d85 100644 --- a/share/translations/keepassx_pt_BR.ts +++ b/share/translations/keepassx_pt_BR.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Inclua as informações abaixo quando reportar um erro: + + Distribution: %1 + + AccessControlDialog @@ -1314,6 +1318,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Não foi possível calcular a chave mestre + + Main diff --git a/share/translations/keepassx_pt_PT.ts b/share/translations/keepassx_pt_PT.ts index cf9f124f..0a671ed7 100644 --- a/share/translations/keepassx_pt_PT.ts +++ b/share/translations/keepassx_pt_PT.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Inclua as seguintes informações sempre que reportar um erro: + + Distribution: %1 + Distribuição: %1 + AccessControlDialog @@ -1095,7 +1099,7 @@ Ainda assim deseja abrir a base de dados? Custom icon already exists - + Já existe um ícone personalizado @@ -1318,6 +1322,17 @@ Pode importá-lo clicando em Base de dados - > 'Importar base de dados d Esta é uma migração unidirecional. Não será possível abrir a base de dados importada com a versão 0.4 do KeePassX. + + KeePass2Writer + + Unable to issue challenge-response. + Incapaz de emitir a pergunta de segurança. + + + Unable to calculate master key + Impossível de calcular a chave-mestre + + Main @@ -1338,7 +1353,7 @@ Esta é uma migração unidirecional. Não será possível abrir a base de dados Existing single-instance lock file is invalid. Launching new instance. - + O ficheiro de bloqueio da instância única é inválido. A iniciar nova instância. diff --git a/share/translations/keepassx_ru.ts b/share/translations/keepassx_ru.ts index c206b769..938d5eef 100644 --- a/share/translations/keepassx_ru.ts +++ b/share/translations/keepassx_ru.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Включите следующую информацию, когда сообщаете об ошибке: + + Distribution: %1 + + AccessControlDialog @@ -1318,6 +1322,17 @@ This is a one-way migration. You won't be able to open the imported databas Это одностороннее перемещение. Вы не сможете открыть импортированную базу данных на старой версии KeePassX 0,4. + + KeePass2Writer + + Unable to issue challenge-response. + Не удалось выполнить запрос ответа. + + + Unable to calculate master key + Невозможно вычислить мастер-пароль + + Main diff --git a/share/translations/keepassx_sl_SI.ts b/share/translations/keepassx_sl_SI.ts index f43dd17a..cec9bd6c 100644 --- a/share/translations/keepassx_sl_SI.ts +++ b/share/translations/keepassx_sl_SI.ts @@ -64,6 +64,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: + + Distribution: %1 + + AccessControlDialog @@ -1305,6 +1309,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Izračun glavnega ključa ni uspel + + Main diff --git a/share/translations/keepassx_sv.ts b/share/translations/keepassx_sv.ts index bc846e46..a9e8a85d 100644 --- a/share/translations/keepassx_sv.ts +++ b/share/translations/keepassx_sv.ts @@ -24,7 +24,8 @@ Version %1 - + Version %1 + Revision: %1 @@ -64,6 +65,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: + + Distribution: %1 + + AccessControlDialog @@ -196,7 +201,7 @@ Please select whether you want to allow access. Empty password - + Tomt lösenord Changing master key failed: no YubiKey inserted. @@ -254,7 +259,7 @@ Please select whether you want to allow access. Comments start with - + Kommentarer inleds med First record has field names @@ -282,7 +287,7 @@ Please select whether you want to allow access. Empty fieldname - + Tomt fältnamn column @@ -337,7 +342,7 @@ Please select whether you want to allow access. rows, - + rader, columns @@ -622,7 +627,7 @@ Do you want to open it anyway? Open CSV file - + Öppna CSV fil @@ -677,7 +682,7 @@ Do you want to open it anyway? Searching... - + Söker... No current database. @@ -689,11 +694,11 @@ Do you want to open it anyway? Search Results (%1) - + Sökresultat (%1) No Results - + Inget resultat Execute command? @@ -818,7 +823,7 @@ Do you want to open it anyway? Confirm Remove - + Bekräfta borttagning Are you sure you want to remove this attribute? @@ -865,7 +870,7 @@ Do you want to open it anyway? Protect - + Skydda Reveal @@ -1307,6 +1312,17 @@ This is a one-way migration. You won't be able to open the imported databas + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Kunde inte räkna nu master-nyckeln + + Main diff --git a/share/translations/keepassx_tr.ts b/share/translations/keepassx_tr.ts index 18639fc4..4514a420 100644 --- a/share/translations/keepassx_tr.ts +++ b/share/translations/keepassx_tr.ts @@ -67,6 +67,10 @@ MİB mimarisi: %2 Include the following information whenever you report a bug: Bir hata bildirirken şu bilgileri ekleyin: + + Distribution: %1 + + AccessControlDialog @@ -1313,6 +1317,17 @@ Veri tabanı > 'KeePass1 veri tabanı içe aktar...'a tıklayarak i Bu tek yönlü bir yer değiştirmedir. İçe aktarılan veri tabanını eski KeePassX 0.4 sürümüyle açamayacaksınız. + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + Ana anahtar hesaplanamıyor + + Main diff --git a/share/translations/keepassx_uk.ts b/share/translations/keepassx_uk.ts index e1f98f86..c8abb87f 100644 --- a/share/translations/keepassx_uk.ts +++ b/share/translations/keepassx_uk.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: Коли Ви повідомляєте про ваду, завжди долучайте таку інформацію: + + Distribution: %1 + + AccessControlDialog @@ -1318,6 +1322,17 @@ This is a one-way migration. You won't be able to open the imported databas Перетворення можливе лише в одному напрямку. Ви не зможете відкрити імпортоване сховище старою версією KeePassX 0.4. + + KeePass2Writer + + Unable to issue challenge-response. + Неможливо видати виклик-відповідь. + + + Unable to calculate master key + Неможливо вирахувати головний ключ + + Main diff --git a/share/translations/keepassx_zh_CN.ts b/share/translations/keepassx_zh_CN.ts index db791625..0d5e2806 100644 --- a/share/translations/keepassx_zh_CN.ts +++ b/share/translations/keepassx_zh_CN.ts @@ -67,6 +67,10 @@ CPU 架构:%2 Include the following information whenever you report a bug: 报告任何 bug 时,请包含以下信息: + + Distribution: %1 + + AccessControlDialog @@ -1316,6 +1320,17 @@ This is a one-way migration. You won't be able to open the imported databas 这是不可逆的迁移,导入后的数据库将无法由旧版本的 KeePassX 0.4 打开。 + + KeePass2Writer + + Unable to issue challenge-response. + 无法发出挑战应答 + + + Unable to calculate master key + 无法计算主密码 + + Main diff --git a/share/translations/keepassx_zh_TW.ts b/share/translations/keepassx_zh_TW.ts index af9c74b5..0dbabb3a 100644 --- a/share/translations/keepassx_zh_TW.ts +++ b/share/translations/keepassx_zh_TW.ts @@ -67,6 +67,10 @@ Kernel: %3 %4 Include the following information whenever you report a bug: 回報 Bug 時會包含以下資訊: + + Distribution: %1 + + AccessControlDialog @@ -1317,6 +1321,17 @@ This is a one-way migration. You won't be able to open the imported databas 這是單向遷移。你無法用舊的 KeePassX 0.4 的版本開啟已匯入的資料庫。 + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + 無法計算主金鑰 + + Main diff --git a/snapcraft.yaml b/snapcraft.yaml index c73b5328..7903eb03 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,18 +1,18 @@ name: keepassxc -version: 2.2.1 +version: 2.2.2 grade: stable -summary: community driven port of the windows application “Keepass Password Safe” +summary: Community-driven port of the Windows application “KeePass Password Safe” description: | KeePassXC is an application for people with extremely high demands on secure - personal data management. It has a light interface, is cross platform and - is published under the terms of the GNU General Public License. + personal data management. It has a light interface, is cross-platform and + published under the terms of the GNU General Public License. confinement: strict apps: keepassxc: command: desktop-launch keepassxc plugs: [unity7, x11, opengl, gsettings, home, network, network-bind, removable-media, raw-usb] - desktop: usr/share/applications/keepassxc.desktop + desktop: usr/share/applications/org.keepassxc.KeePassXC.desktop cli: command: keepassxc-cli plugs: [gsettings, home, removable-media, raw-usb] @@ -24,7 +24,7 @@ parts: configflags: - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX=/usr - - -DKEEPASSXC_SNAP_BUILD=ON + - -DKEEPASSXC_DIST_TYPE=Snap - -DWITH_TESTS=OFF - -DWITH_XC_AUTOTYPE=ON - -DWITH_XC_HTTP=ON @@ -42,7 +42,7 @@ parts: - libyubikey-dev - libykpers-1-dev install: | - sed -i 's|Icon=keepassxc|Icon=${SNAP}/usr/share/icons/hicolor/256x256/apps/keepassxc.png|g' $SNAPCRAFT_PART_INSTALL/usr/share/applications/keepassxc.desktop + sed -i 's|Icon=keepassxc|Icon=${SNAP}/usr/share/icons/hicolor/256x256/apps/keepassxc.png|g' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.keepassxc.KeePassXC.desktop after: [desktop-qt5] # Redefine desktop-qt5 stage packages to work with Ubuntu 17.04 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bd8a1fd3..24742ede 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -68,7 +68,6 @@ set(keepassx_SOURCES core/Uuid.cpp core/Base32.h core/Base32.cpp - core/Optional.h cli/Utils.cpp cli/Utils.h crypto/Crypto.cpp @@ -99,6 +98,7 @@ set(keepassx_SOURCES gui/DatabaseTabWidget.cpp gui/DatabaseWidget.cpp gui/DatabaseWidgetStateSync.cpp + gui/DetailsWidget.cpp gui/DialogyWidget.cpp gui/DragTabBar.cpp gui/EditWidget.cpp @@ -155,6 +155,8 @@ if(APPLE) set(keepassx_SOURCES ${keepassx_SOURCES} core/ScreenLockListenerMac.h core/ScreenLockListenerMac.cpp + core/MacPasteboard.h + core/MacPasteboard.cpp ) endif() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") @@ -177,6 +179,7 @@ set(keepassx_SOURCES_MAINEXE 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_subdirectory(http) if(WITH_XC_HTTP) @@ -186,6 +189,11 @@ endif() add_subdirectory(autotype) add_subdirectory(cli) +add_subdirectory(sshagent) +if(WITH_XC_SSHAGENT) + set(sshagent_LIB sshagent) +endif() + set(autotype_SOURCES core/Tools.cpp autotype/AutoType.cpp @@ -222,6 +230,7 @@ set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUIL target_link_libraries(keepassx_core ${keepasshttp_LIB} ${autotype_LIB} + ${sshagent_LIB} ${YUBIKEY_LIBRARIES} ${ZXCVBN_LIBRARIES} Qt5::Core @@ -234,6 +243,9 @@ target_link_libraries(keepassx_core if(APPLE) target_link_libraries(keepassx_core "-framework Foundation") + if(Qt5MacExtras_FOUND) + target_link_libraries(keepassx_core Qt5::MacExtras) + endif() endif() if (UNIX AND NOT APPLE) target_link_libraries(keepassx_core Qt5::DBus) @@ -264,13 +276,7 @@ if(APPLE AND WITH_APP_BUNDLE) set_target_properties(${PROGNAME} PROPERTIES MACOSX_BUNDLE ON MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist) -endif() -install(TARGETS ${PROGNAME} - BUNDLE DESTINATION . COMPONENT Runtime - RUNTIME DESTINATION ${BIN_INSTALL_DIR} COMPONENT Runtime) - -if(APPLE AND WITH_APP_BUNDLE) if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib") install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib" DESTINATION "${DATA_INSTALL_DIR}") @@ -284,16 +290,17 @@ if(APPLE AND WITH_APP_BUNDLE) set(CPACK_PACKAGE_FILE_NAME "${PROGNAME}-${KEEPASSXC_VERSION}") include(CPack) - if(NOT DEFINED QT_BINARY_DIR) - set(QT_BINARY_DIR "/usr/local/opt/qt5/bin" CACHE PATH "QT binary folder") - endif() add_custom_command(TARGET ${PROGNAME} POST_BUILD - COMMAND ${QT_BINARY_DIR}/macdeployqt ${PROGNAME}.app + COMMAND ${MACDEPLOYQT_EXE} ${PROGNAME}.app WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src COMMENT "Deploying app bundle") endif() +install(TARGETS ${PROGNAME} + BUNDLE DESTINATION . COMPONENT Runtime + RUNTIME DESTINATION ${BIN_INSTALL_DIR} COMPONENT Runtime) + if(MINGW) if(${CMAKE_SIZEOF_VOID_P} EQUAL "8") set(OUTPUT_FILE_POSTFIX "Win64") diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index f3496849..0d01a831 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -256,6 +256,13 @@ void AutoType::resetInAutoType() m_inAutoType = false; } +void AutoType::raiseWindow() +{ +#if defined(Q_OS_MAC) + m_plugin->raiseOwnWindow(); +#endif +} + void AutoType::unloadPlugin() { if (m_executor) { @@ -563,7 +570,8 @@ QString AutoType::autoTypeSequence(const Entry* entry, const QString& windowTitl bool match = false; const QList assocList = entry->autoTypeAssociations()->getAll(); for (const AutoTypeAssociations::Association& assoc : assocList) { - if (windowMatches(windowTitle, assoc.window)) { + const QString window = entry->resolveMultiplePlaceholders(assoc.window); + if (windowMatches(windowTitle, window)) { if (!assoc.sequence.isEmpty()) { sequence = assoc.sequence; } diff --git a/src/autotype/AutoType.h b/src/autotype/AutoType.h index 6f4a815f..e881975a 100644 --- a/src/autotype/AutoType.h +++ b/src/autotype/AutoType.h @@ -51,6 +51,7 @@ public: public slots: void performGlobalAutoType(const QList& dbList); + void raiseWindow(); signals: void globalShortcutTriggered(); diff --git a/src/autotype/mac/CMakeLists.txt b/src/autotype/mac/CMakeLists.txt index ac93de0e..08c53278 100644 --- a/src/autotype/mac/CMakeLists.txt +++ b/src/autotype/mac/CMakeLists.txt @@ -9,14 +9,12 @@ set(autotype_mac_mm_SOURCES add_library(keepassx-autotype-cocoa MODULE ${autotype_mac_SOURCES} ${autotype_mac_mm_SOURCES}) set_target_properties(keepassx-autotype-cocoa PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon") target_link_libraries(keepassx-autotype-cocoa ${PROGNAME} Qt5::Core Qt5::Widgets) -if(NOT DEFINED QT_BINARY_DIR) - set(QT_BINARY_DIR "/usr/local/opt/qt5/bin" CACHE PATH "QT binary folder") -endif() + if(WITH_APP_BUNDLE) add_custom_command(TARGET keepassx-autotype-cocoa POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/libkeepassx-autotype-cocoa.so ${PLUGIN_INSTALL_DIR} - COMMAND ${QT_BINARY_DIR}/macdeployqt ${PROGNAME}.app -executable=${PLUGIN_INSTALL_DIR}/libkeepassx-autotype-cocoa.so -no-plugins + COMMAND ${MACDEPLOYQT_EXE} ${PROGNAME}.app -executable=${PLUGIN_INSTALL_DIR}/libkeepassx-autotype-cocoa.so -no-plugins WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/src COMMENT "Deploying autotype plugin") else() diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index f89d447c..b06e702a 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -15,8 +15,12 @@ #cmakedefine WITH_XC_HTTP #cmakedefine WITH_XC_AUTOTYPE #cmakedefine WITH_XC_YUBIKEY +#cmakedefine WITH_XC_SSHAGENT -#cmakedefine KEEPASSXC_SNAP_BUILD +#cmakedefine KEEPASSXC_DIST +#cmakedefine KEEPASSXC_DIST_TYPE "@KEEPASSXC_DIST_TYPE@" +#cmakedefine KEEPASSXC_DIST_SNAP +#cmakedefine KEEPASSXC_DIST_APPIMAGE #cmakedefine HAVE_PR_SET_DUMPABLE 1 #cmakedefine HAVE_RLIMIT_CORE 1 diff --git a/src/core/Base32.cpp b/src/core/Base32.cpp index a58f2e42..78448ffb 100644 --- a/src/core/Base32.cpp +++ b/src/core/Base32.cpp @@ -15,7 +15,11 @@ * along with this program. If not, see . */ -// Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648 +/* Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648 + * Use the functions Base32::addPadding/1, Base32::removePadding/1 or + * Base32::sanitizeInput/1 to fix input or output for a particular + * applications (e.g. to use with Google Authenticator). + */ #include "Base32.h" @@ -36,18 +40,21 @@ constexpr quint8 ASCII_a = static_cast('a'); constexpr quint8 ASCII_z = static_cast('z'); constexpr quint8 ASCII_EQ = static_cast('='); -Optional Base32::decode(const QByteArray& encodedData) +QVariant Base32::decode(const QByteArray& encodedData) { - if (encodedData.size() <= 0) - return Optional(""); + if (encodedData.size() <= 0) { + return QVariant::fromValue(QByteArray("")); + } - if (encodedData.size() % 8 != 0) - return Optional(); + if (encodedData.size() % 8 != 0) { + return QVariant(); + } int nPads = 0; for (int i = -1; i > -7; --i) { - if ('=' == encodedData[encodedData.size()+i]) + if ('=' == encodedData[encodedData.size() + i]) { ++nPads; + } } int specialOffset; @@ -75,10 +82,9 @@ Optional Base32::decode(const QByteArray& encodedData) specialOffset = 0; } - Q_ASSERT(encodedData.size() > 0); - const int nQuantums = encodedData.size() / 8; - const int nBytes = (nQuantums - 1) * 5 + nSpecialBytes; + const int nQuanta = encodedData.size() / 8; + const int nBytes = nSpecialBytes > 0 ? (nQuanta - 1) * 5 + nSpecialBytes : nQuanta * 5; QByteArray data(nBytes, Qt::Uninitialized); @@ -89,19 +95,20 @@ Optional Base32::decode(const QByteArray& encodedData) quint64 quantum = 0; int nQuantumBytes = 5; - for (int n = 0; n < 8; n++) { - quint8 ch = static_cast(encodedData[i++]); + for (int n = 0; n < 8; ++n) { + auto ch = static_cast(encodedData[i++]); if ((ASCII_A <= ch && ch <= ASCII_Z) || (ASCII_a <= ch && ch <= ASCII_z)) { ch -= ASCII_A; - if (ch >= ALPH_POS_2) + if (ch >= ALPH_POS_2) { ch -= ASCII_a - ASCII_A; + } } else { - if (ch >= ASCII_2 && ch <= ASCII_7) { + if (ASCII_2 <= ch && ch <= ASCII_7) { ch -= ASCII_2; ch += ALPH_POS_2; } else { if (ASCII_EQ == ch) { - if(i == encodedData.size()) { + if (i == encodedData.size()) { // finished with special quantum quantum >>= specialOffset; nQuantumBytes = nSpecialBytes; @@ -109,7 +116,7 @@ Optional Base32::decode(const QByteArray& encodedData) continue; } else { // illegal character - return Optional(); + return QVariant(); } } } @@ -120,26 +127,30 @@ Optional Base32::decode(const QByteArray& encodedData) const int offset = (nQuantumBytes - 1) * 8; quint64 mask = quint64(0xFF) << offset; - for (int n = offset; n >= 0; n -= 8) { - char c = static_cast((quantum & mask) >> n); - data[o++] = c; + for (int n = offset; n >= 0 && o < nBytes; n -= 8) { + data[o++] = static_cast((quantum & mask) >> n); mask >>= 8; } } - return Optional(data); + Q_ASSERT(encodedData.size() == i); + Q_ASSERT(nBytes == o); + + return QVariant::fromValue(data); } QByteArray Base32::encode(const QByteArray& data) { - if (data.size() < 1) + if (data.size() < 1) { return QByteArray(); + } const int nBits = data.size() * 8; const int rBits = nBits % 40; // in {0, 8, 16, 24, 32} - const int nQuantums = nBits / 40 + (rBits > 0 ? 1 : 0); - QByteArray encodedData(nQuantums * 8, Qt::Uninitialized); - + const int nQuanta = nBits / 40 + (rBits > 0 ? 1 : 0); + const int nBytes = nQuanta * 8; + QByteArray encodedData(nBytes, Qt::Uninitialized); + int i = 0; int o = 0; int n; @@ -157,6 +168,7 @@ QByteArray Base32::encode(const QByteArray& data) int index; for (n = 35; n >= 0; n -= 5) { index = (quantum & mask) >> n; + Q_ASSERT(0 <= index && index <= 31); encodedData[o++] = alphabet[index]; mask >>= 5; } @@ -164,13 +176,14 @@ QByteArray Base32::encode(const QByteArray& data) // < 40-bits of input at final input group if (i < data.size()) { - Q_ASSERT(rBits > 0); + Q_ASSERT(8 <= rBits && rBits <= 32); quantum = 0; - for (n = rBits - 8; n >= 0; n -= 8) + for (n = rBits - 8; n >= 0; n -= 8) { quantum |= static_cast(data[i++]) << n; + } switch (rBits) { - case 8: // expand to 10 bits + case 8: // expand to 10 bits quantum <<= 2; mask = MASK_10BIT; n = 5; @@ -186,7 +199,7 @@ QByteArray Base32::encode(const QByteArray& data) n = 20; break; default: // expand to 35 bits - Q_ASSERT(rBits == 32); + Q_ASSERT(32 == rBits); quantum <<= 3; mask = MASK_35BIT; n = 30; @@ -194,17 +207,89 @@ QByteArray Base32::encode(const QByteArray& data) while (n >= 0) { int index = (quantum & mask) >> n; + Q_ASSERT(0 <= index && index <= 31); encodedData[o++] = alphabet[index]; mask >>= 5; n -= 5; } // add pad characters - while (o < encodedData.size()) + while (o < encodedData.size()) { encodedData[o++] = '='; + } } - Q_ASSERT(encodedData.size() == o); + Q_ASSERT(data.size() == i); + Q_ASSERT(nBytes == o); return encodedData; } +QByteArray Base32::addPadding(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0 || encodedData.size() % 8 == 0) { + return encodedData; + } + + const int rBytes = encodedData.size() % 8; + // rBytes must be a member of {2, 4, 5, 7} + if (1 == rBytes || 3 == rBytes || 6 == rBytes) { + return encodedData; + } + + QByteArray newEncodedData(encodedData); + for (int nPads = 8 - rBytes; nPads > 0; --nPads) { + newEncodedData.append('='); + } + + return newEncodedData; +} + +QByteArray Base32::removePadding(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0 || encodedData.size() % 8 != 0) { + return encodedData; // return same bad input + } + + int nPads = 0; + for (int i = -1; i > -7; --i) { + if ('=' == encodedData[encodedData.size() + i]) { + ++nPads; + } + } + + QByteArray newEncodedData(encodedData); + newEncodedData.remove(encodedData.size() - nPads, nPads); + newEncodedData.resize(encodedData.size() - nPads); + + return newEncodedData; +} + +QByteArray Base32::sanitizeInput(const QByteArray& encodedData) +{ + if (encodedData.size() <= 0) { + return encodedData; + } + + QByteArray newEncodedData(encodedData.size(), Qt::Uninitialized); + int i = 0; + for (auto ch : encodedData) { + switch (ch) { + case '0': + newEncodedData[i++] = 'O'; + break; + case '1': + newEncodedData[i++] = 'L'; + break; + case '8': + newEncodedData[i++] = 'B'; + break; + default: + if (('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('2' <= ch && ch <= '7')) { + newEncodedData[i++] = ch; + } + } + } + newEncodedData.resize(i); + + return addPadding(newEncodedData); +} diff --git a/src/core/Base32.h b/src/core/Base32.h index 701eb7ab..204368c5 100644 --- a/src/core/Base32.h +++ b/src/core/Base32.h @@ -15,22 +15,28 @@ * along with this program. If not, see . */ -// Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648 +/* Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648 + * Use the functions Base32::addPadding/1, Base32::removePadding/1 or + * Base32::sanitizeInput/1 to fix input or output for a particular + * applications (e.g. to use with Google Authenticator). + */ #ifndef BASE32_H #define BASE32_H -#include "Optional.h" -#include #include +#include +#include class Base32 { public: - Base32() =default; - Q_REQUIRED_RESULT static Optional decode(const QByteArray&); + Base32() = default; + Q_REQUIRED_RESULT static QVariant decode(const QByteArray&); Q_REQUIRED_RESULT static QByteArray encode(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray addPadding(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray removePadding(const QByteArray&); + Q_REQUIRED_RESULT static QByteArray sanitizeInput(const QByteArray&); }; - -#endif //BASE32_H +#endif // BASE32_H diff --git a/src/core/Config.cpp b/src/core/Config.cpp index a432b3c9..2047919b 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -114,7 +114,6 @@ void Config::init(const QString& fileName) m_defaults.insert("AutoSaveAfterEveryChange", false); m_defaults.insert("AutoReloadOnChange", true); m_defaults.insert("AutoSaveOnExit", false); - m_defaults.insert("ShowToolbar", true); m_defaults.insert("SearchLimitGroup", false); m_defaults.insert("MinimizeOnCopy", false); m_defaults.insert("UseGroupIconOnEntryCreation", false); @@ -131,6 +130,7 @@ void Config::init(const QString& fileName) m_defaults.insert("security/lockdatabasescreenlock", true); m_defaults.insert("security/passwordsrepeat", false); m_defaults.insert("security/passwordscleartext", false); + m_defaults.insert("security/hidepassworddetails", true); m_defaults.insert("security/autotypeask", true); m_defaults.insert("security/IconDownloadFallbackToGoogle", false); m_defaults.insert("GUI/Language", "system"); diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 924eec06..3913f809 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -260,6 +260,25 @@ bool Database::hasKey() const return m_data.hasKey; } +bool Database::transformKeyWithSeed(const QByteArray& transformSeed) +{ + Q_ASSERT(hasKey()); + + bool ok; + QString errorString; + + QByteArray transformedMasterKey = + m_data.key.transform(transformSeed, transformRounds(), &ok, &errorString); + if (!ok) { + return false; + } + + m_data.transformSeed = transformSeed; + m_data.transformedMasterKey = transformedMasterKey; + + return true; +} + bool Database::verifyKey(const CompositeKey& key) const { Q_ASSERT(hasKey()); diff --git a/src/core/Database.h b/src/core/Database.h index 2ab556a0..26b15025 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -106,6 +106,7 @@ public: */ bool setKey(const CompositeKey& key); bool hasKey() const; + bool transformKeyWithSeed(const QByteArray& transformSeed); bool verifyKey(const CompositeKey& key) const; void recycleEntry(Entry* entry); void recycleGroup(Group* group); diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 464628dc..53a1ad22 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -25,7 +25,11 @@ #include "core/Metadata.h" #include "totp/totp.h" +#include + const int Entry::DefaultIconNumber = 0; +const int Entry::ResolveMaximumDepth = 10; + Entry::Entry() : m_attributes(new EntryAttributes(this)) @@ -38,8 +42,8 @@ Entry::Entry() m_data.iconNumber = DefaultIconNumber; m_data.autoTypeEnabled = true; m_data.autoTypeObfuscation = 0; - m_data.totpStep = QTotp::defaultStep; - m_data.totpDigits = QTotp::defaultDigits; + m_data.totpStep = Totp::defaultStep; + m_data.totpDigits = Totp::defaultDigits; connect(m_attributes, SIGNAL(modified()), this, SIGNAL(modified())); connect(m_attributes, SIGNAL(defaultKeyModified()), SLOT(emitDataChanged())); @@ -241,7 +245,15 @@ QString Entry::url() const QString Entry::webUrl() const { - return resolveUrl(m_attributes->value(EntryAttributes::URLKey)); + QString url = resolveMultiplePlaceholders(m_attributes->value(EntryAttributes::URLKey)); + return resolveUrl(url); +} + +QString Entry::displayUrl() const +{ + QString url = maskPasswordPlaceholders(m_attributes->value(EntryAttributes::URLKey)); + url = resolveMultiplePlaceholders(url); + return resolveUrl(url); } QString Entry::username() const @@ -305,7 +317,7 @@ QString Entry::totp() const if (hasTotp()) { QString seed = totpSeed(); quint64 time = QDateTime::currentDateTime().toTime_t(); - QString output = QTotp::generateTotp(seed.toLatin1(), time, m_data.totpDigits, m_data.totpStep); + QString output = Totp::generateTotp(seed.toLatin1(), time, m_data.totpDigits, m_data.totpStep); return QString(output); } else { @@ -316,18 +328,30 @@ QString Entry::totp() const void Entry::setTotp(const QString& seed, quint8& step, quint8& digits) { if (step == 0) { - step = QTotp::defaultStep; + step = Totp::defaultStep; } if (digits == 0) { - digits = QTotp::defaultDigits; + digits = Totp::defaultDigits; } + QString data; + + const Totp::Encoder & enc = Totp::encoders.value(digits, Totp::defaultEncoder); if (m_attributes->hasKey("otp")) { - m_attributes->set("otp", QString("key=%1&step=%2&size=%3").arg(seed).arg(step).arg(digits), true); + data = QString("key=%1&step=%2&size=%3").arg(seed).arg(step).arg(enc.digits == 0 ? digits : enc.digits); + if (!enc.name.isEmpty()) { + data.append("&enocder=").append(enc.name); + } + m_attributes->set("otp", data, true); } else { m_attributes->set("TOTP Seed", seed, true); - m_attributes->set("TOTP Settings", QString("%1;%2").arg(step).arg(digits)); + if (!enc.shortName.isEmpty()) { + data = QString("%1;%2").arg(step).arg(enc.shortName); + } else { + data = QString("%1;%2").arg(step).arg(digits); + } + m_attributes->set("TOTP Settings", data); } } @@ -341,19 +365,24 @@ QString Entry::totpSeed() const secret = m_attributes->value("TOTP Seed"); } - m_data.totpDigits = QTotp::defaultDigits; - m_data.totpStep = QTotp::defaultStep; + m_data.totpDigits = Totp::defaultDigits; + m_data.totpStep = Totp::defaultStep; if (m_attributes->hasKey("TOTP Settings")) { - QRegExp rx("(\\d+);(\\d)", Qt::CaseInsensitive, QRegExp::RegExp); - int pos = rx.indexIn(m_attributes->value("TOTP Settings")); - if (pos > -1) { - m_data.totpStep = rx.cap(1).toUInt(); - m_data.totpDigits = rx.cap(2).toUInt(); + // this regex must be kept in sync with the set of allowed short names Totp::shortNameToEncoder + QRegularExpression rx(QString("(\\d+);((?:\\d+)|S)")); + QRegularExpressionMatch m = rx.match(m_attributes->value("TOTP Settings")); + if (m.hasMatch()) { + m_data.totpStep = m.captured(1).toUInt(); + if (Totp::shortNameToEncoder.contains(m.captured(2))) { + m_data.totpDigits = Totp::shortNameToEncoder[m.captured(2)]; + } else { + m_data.totpDigits = m.captured(2).toUInt(); + } } } - return QTotp::parseOtpString(secret, m_data.totpDigits, m_data.totpStep); + return Totp::parseOtpString(secret, m_data.totpDigits, m_data.totpStep); } quint8 Entry::totpStep() const @@ -670,6 +699,108 @@ void Entry::updateModifiedSinceBegin() m_modifiedSinceBegin = true; } +QString Entry::resolveMultiplePlaceholdersRecursive(const QString &str, int maxDepth) const +{ + if (maxDepth <= 0) { + qWarning("Maximum depth of replacement has been reached. Entry uuid: %s", qPrintable(uuid().toHex())); + return str; + } + + QString result = str; + QRegExp placeholderRegEx("(\\{[^\\}]+\\})", Qt::CaseInsensitive, QRegExp::RegExp2); + placeholderRegEx.setMinimal(true); + int pos = 0; + while ((pos = placeholderRegEx.indexIn(str, pos)) != -1) { + const QString found = placeholderRegEx.cap(1); + result.replace(found, resolvePlaceholderRecursive(found, maxDepth - 1)); + pos += placeholderRegEx.matchedLength(); + } + + if (result != str) { + result = resolveMultiplePlaceholdersRecursive(result, maxDepth - 1); + } + + return result; +} + +QString Entry::resolvePlaceholderRecursive(const QString &placeholder, int maxDepth) const +{ + const PlaceholderType typeOfPlaceholder = placeholderType(placeholder); + switch (typeOfPlaceholder) { + case PlaceholderType::NotPlaceholder: + return placeholder; + case PlaceholderType::Unknown: + qWarning("Can't resolve placeholder %s for entry with uuid %s", qPrintable(placeholder), + qPrintable(uuid().toHex())); + return placeholder; + case PlaceholderType::Title: + return title(); + case PlaceholderType::UserName: + return username(); + case PlaceholderType::Password: + return password(); + case PlaceholderType::Notes: + return notes(); + case PlaceholderType::Totp: + return totp(); + case PlaceholderType::Url: + return url(); + case PlaceholderType::UrlWithoutScheme: + case PlaceholderType::UrlScheme: + case PlaceholderType::UrlHost: + case PlaceholderType::UrlPort: + case PlaceholderType::UrlPath: + case PlaceholderType::UrlQuery: + case PlaceholderType::UrlFragment: + case PlaceholderType::UrlUserInfo: + case PlaceholderType::UrlUserName: + case PlaceholderType::UrlPassword: { + const QString strUrl = resolveMultiplePlaceholdersRecursive(url(), maxDepth - 1); + return resolveUrlPlaceholder(strUrl, typeOfPlaceholder); + } + case PlaceholderType::CustomAttribute: { + const QString key = placeholder.mid(3, placeholder.length() - 4); // {S:attr} => mid(3, len - 4) + return attributes()->hasKey(key) ? attributes()->value(key) : QString(); + } + case PlaceholderType::Reference: { + // resolving references in format: {REF:@I:} + // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing, + // but supporting lookups of standard fields and references by UUID only + + QString result; + QRegExp* referenceRegExp = m_attributes->referenceRegExp(); + if (referenceRegExp->indexIn(placeholder) != -1) { + constexpr int wantedFieldIndex = 1; + constexpr int referencedUuidIndex = 2; + const Uuid referencedUuid(QByteArray::fromHex(referenceRegExp->cap(referencedUuidIndex).toLatin1())); + const Entry* refEntry = m_group->database()->resolveEntry(referencedUuid); + if (refEntry) { + const QString wantedField = referenceRegExp->cap(wantedFieldIndex).toLower(); + if (wantedField == "t") { + result = refEntry->title(); + } else if (wantedField == "u") { + result = refEntry->username(); + } else if (wantedField == "p") { + result = refEntry->password(); + } else if (wantedField == "a") { + result = refEntry->url(); + } else if (wantedField == "n") { + result = refEntry->notes(); + } + + // Referencing fields of other entries only works with standard fields, not with custom user strings. + // If you want to reference a custom user string, you need to place a redirection in a standard field + // of the entry with the custom string, using {S:}, and reference the standard field. + result = refEntry->resolveMultiplePlaceholdersRecursive(result, maxDepth - 1); + } + } + return result; + } + } + + return placeholder; +} + Group* Entry::group() { return m_group; @@ -736,67 +867,82 @@ QString Entry::maskPasswordPlaceholders(const QString &str) const QString Entry::resolveMultiplePlaceholders(const QString& str) const { - QString result = str; - QRegExp tmplRegEx("(\\{.*\\})", Qt::CaseInsensitive, QRegExp::RegExp2); - tmplRegEx.setMinimal(true); - QStringList tmplList; - int pos = 0; - - while ((pos = tmplRegEx.indexIn(str, pos)) != -1) { - QString found = tmplRegEx.cap(1); - result.replace(found,resolvePlaceholder(found)); - pos += tmplRegEx.matchedLength(); - } - - return result; + return resolveMultiplePlaceholdersRecursive(str, ResolveMaximumDepth); } -QString Entry::resolvePlaceholder(const QString& str) const +QString Entry::resolvePlaceholder(const QString& placeholder) const { - QString result = str; + return resolvePlaceholderRecursive(placeholder, ResolveMaximumDepth); +} - const QList keyList = attributes()->keys(); - for (const QString& key : keyList) { - Qt::CaseSensitivity cs = Qt::CaseInsensitive; - QString k = key; +QString Entry::resolveUrlPlaceholder(const QString &str, Entry::PlaceholderType placeholderType) const +{ + if (str.isEmpty()) + return QString(); - if (!EntryAttributes::isDefaultAttribute(key)) { - cs = Qt::CaseSensitive; - k.prepend("{S:"); - } else { - k.prepend("{"); - } - - - k.append("}"); - if (result.compare(k,cs)==0) { - result.replace(result,attributes()->value(key)); - break; - } + const QUrl qurl(str); + switch (placeholderType) { + case PlaceholderType::UrlWithoutScheme: + return qurl.toString(QUrl::RemoveScheme | QUrl::FullyDecoded); + case PlaceholderType::UrlScheme: + return qurl.scheme(); + case PlaceholderType::UrlHost: + return qurl.host(); + case PlaceholderType::UrlPort: + return QString::number(qurl.port()); + case PlaceholderType::UrlPath: + return qurl.path(); + case PlaceholderType::UrlQuery: + return qurl.query(); + case PlaceholderType::UrlFragment: + return qurl.fragment(); + case PlaceholderType::UrlUserInfo: + return qurl.userInfo(); + case PlaceholderType::UrlUserName: + return qurl.userName(); + case PlaceholderType::UrlPassword: + return qurl.password(); + default: { + Q_ASSERT_X(false, "Entry::resolveUrlPlaceholder", "Bad url placeholder type"); + break; + } } - // resolving references in format: {REF:@I:} - // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing, - // but supporting lookups of standard fields and references by UUID only + return QString(); +} - QRegExp* tmpRegExp = m_attributes->referenceRegExp(); - if (tmpRegExp->indexIn(result) != -1) { - // cap(0) contains the whole reference - // cap(1) contains which field is wanted - // cap(2) contains the uuid of the referenced entry - Entry* tmpRefEntry = m_group->database()->resolveEntry(Uuid(QByteArray::fromHex(tmpRegExp->cap(2).toLatin1()))); - if (tmpRefEntry) { - // entry found, get the relevant field - QString tmpRefField = tmpRegExp->cap(1).toLower(); - if (tmpRefField == "t") result.replace(tmpRegExp->cap(0), tmpRefEntry->title(), Qt::CaseInsensitive); - else if (tmpRefField == "u") result.replace(tmpRegExp->cap(0), tmpRefEntry->username(), Qt::CaseInsensitive); - else if (tmpRefField == "p") result.replace(tmpRegExp->cap(0), tmpRefEntry->password(), Qt::CaseInsensitive); - else if (tmpRefField == "a") result.replace(tmpRegExp->cap(0), tmpRefEntry->url(), Qt::CaseInsensitive); - else if (tmpRefField == "n") result.replace(tmpRegExp->cap(0), tmpRefEntry->notes(), Qt::CaseInsensitive); - } +Entry::PlaceholderType Entry::placeholderType(const QString &placeholder) const +{ + if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) { + return PlaceholderType::NotPlaceholder; + } else if (placeholder.startsWith(QLatin1Literal("{S:"))) { + return PlaceholderType::CustomAttribute; + } else if (placeholder.startsWith(QLatin1Literal("{REF:"))) { + return PlaceholderType::Reference; } - return result; + static const QMap placeholders { + { QStringLiteral("{TITLE}"), PlaceholderType::Title }, + { QStringLiteral("{USERNAME}"), PlaceholderType::UserName }, + { QStringLiteral("{PASSWORD}"), PlaceholderType::Password }, + { QStringLiteral("{NOTES}"), PlaceholderType::Notes }, + { QStringLiteral("{TOTP}"), PlaceholderType::Totp }, + { QStringLiteral("{URL}"), PlaceholderType::Url }, + { QStringLiteral("{URL:RMVSCM}"), PlaceholderType::UrlWithoutScheme }, + { QStringLiteral("{URL:WITHOUTSCHEME}"), PlaceholderType::UrlWithoutScheme }, + { QStringLiteral("{URL:SCM}"), PlaceholderType::UrlScheme }, + { QStringLiteral("{URL:SCHEME}"), PlaceholderType::UrlScheme }, + { QStringLiteral("{URL:HOST}"), PlaceholderType::UrlHost }, + { QStringLiteral("{URL:PORT}"), PlaceholderType::UrlPort }, + { QStringLiteral("{URL:PATH}"), PlaceholderType::UrlPath }, + { QStringLiteral("{URL:QUERY}"), PlaceholderType::UrlQuery }, + { QStringLiteral("{URL:FRAGMENT}"), PlaceholderType::UrlFragment }, + { QStringLiteral("{URL:USERINFO}"), PlaceholderType::UrlUserInfo }, + { QStringLiteral("{URL:USERNAME}"), PlaceholderType::UrlUserName }, + { QStringLiteral("{URL:PASSWORD}"), PlaceholderType::UrlPassword } + }; + + return placeholders.value(placeholder.toUpper(), PlaceholderType::Unknown); } QString Entry::resolveUrl(const QString& url) const diff --git a/src/core/Entry.h b/src/core/Entry.h index a37637c2..d4d2b903 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -79,6 +79,7 @@ public: QString title() const; QString url() const; QString webUrl() const; + QString displayUrl() const; QString username() const; QString password() const; QString notes() const; @@ -96,6 +97,7 @@ public: const EntryAttachments* attachments() const; static const int DefaultIconNumber; + static const int ResolveMaximumDepth; void setUuid(const Uuid& uuid); void setIcon(int iconNumber); @@ -134,6 +136,29 @@ public: }; Q_DECLARE_FLAGS(CloneFlags, CloneFlag) + enum class PlaceholderType { + NotPlaceholder, + Unknown, + Title, + UserName, + Password, + Notes, + Totp, + Url, + UrlWithoutScheme, + UrlScheme, + UrlHost, + UrlPort, + UrlPath, + UrlQuery, + UrlFragment, + UrlUserInfo, + UrlUserName, + UrlPassword, + Reference, + CustomAttribute + }; + /** * Creates a duplicate of this entry except that the returned entry isn't * part of any group. @@ -145,6 +170,8 @@ public: QString maskPasswordPlaceholders(const QString& str) const; QString resolveMultiplePlaceholders(const QString& str) const; QString resolvePlaceholder(const QString& str) const; + QString resolveUrlPlaceholder(const QString &str, PlaceholderType placeholderType) const; + PlaceholderType placeholderType(const QString& placeholder) const; QString resolveUrl(const QString& url) const; /** @@ -174,6 +201,9 @@ private slots: void updateModifiedSinceBegin(); private: + QString resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const; + QString resolvePlaceholderRecursive(const QString& placeholder, int maxDepth) const; + const Database* database() const; template bool set(T& property, const T& value); diff --git a/src/core/EntryAttachments.cpp b/src/core/EntryAttachments.cpp index d700ed44..8ff66277 100644 --- a/src/core/EntryAttachments.cpp +++ b/src/core/EntryAttachments.cpp @@ -17,6 +17,8 @@ #include "EntryAttachments.h" +#include + EntryAttachments::EntryAttachments(QObject* parent) : QObject(parent) { @@ -71,7 +73,8 @@ void EntryAttachments::set(const QString& key, const QByteArray& value) void EntryAttachments::remove(const QString& key) { if (!m_attachments.contains(key)) { - Q_ASSERT(false); + Q_ASSERT_X(false, "EntryAttachments::remove", + qPrintable(QString("Can't find attachment for key %1").arg(key))); return; } @@ -83,6 +86,31 @@ void EntryAttachments::remove(const QString& key) emit modified(); } +void EntryAttachments::remove(const QStringList &keys) +{ + if (keys.isEmpty()) { + return; + } + + bool isModified = false; + for (const QString &key: keys) { + if (!m_attachments.contains(key)) { + Q_ASSERT_X(false, "EntryAttachments::remove", + qPrintable(QString("Can't find attachment for key %1").arg(key))); + continue; + } + + isModified = true; + emit aboutToBeRemoved(key); + m_attachments.remove(key); + emit removed(key); + } + + if (isModified) { + emit modified(); + } +} + void EntryAttachments::clear() { if (m_attachments.isEmpty()) { diff --git a/src/core/EntryAttachments.h b/src/core/EntryAttachments.h index 04c22cb3..8fa7c717 100644 --- a/src/core/EntryAttachments.h +++ b/src/core/EntryAttachments.h @@ -21,6 +21,8 @@ #include #include +class QStringList; + class EntryAttachments : public QObject { Q_OBJECT @@ -33,6 +35,7 @@ public: QByteArray value(const QString& key) const; void set(const QString& key, const QByteArray& value); void remove(const QString& key); + void remove(const QStringList& keys); void clear(); void copyDataFrom(const EntryAttachments* other); bool operator==(const EntryAttachments& other) const; diff --git a/src/core/FilePath.cpp b/src/core/FilePath.cpp index 90311b40..b5c9d102 100644 --- a/src/core/FilePath.cpp +++ b/src/core/FilePath.cpp @@ -91,7 +91,7 @@ QString FilePath::pluginPath(const QString& name) QIcon FilePath::applicationIcon() { -#ifdef KEEPASSXC_SNAP_BUILD +#ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc", false); #else return icon("apps", "keepassxc"); @@ -100,7 +100,7 @@ QIcon FilePath::applicationIcon() QIcon FilePath::trayIconLocked() { -#ifdef KEEPASSXC_SNAP_BUILD +#ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc-locked", false); #else return icon("apps", "keepassxc-locked"); @@ -109,7 +109,7 @@ QIcon FilePath::trayIconLocked() QIcon FilePath::trayIconUnlocked() { -#ifdef KEEPASSXC_SNAP_BUILD +#ifdef KEEPASSXC_DIST_SNAP return icon("apps", "keepassxc-unlocked", false); #else return icon("apps", "keepassxc-unlocked"); diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 644d2ff6..c7c12bfe 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -26,6 +26,11 @@ const int Group::DefaultIconNumber = 48; const int Group::RecycleBinIconNumber = 43; +Group::CloneFlags Group::DefaultCloneFlags = static_cast( + Group::CloneNewUuid | Group::CloneResetTimeInfo | Group::CloneIncludeEntries); +Entry::CloneFlags Group::DefaultEntryCloneFlags = static_cast( + Entry::CloneNewUuid | Entry::CloneResetTimeInfo); + Group::Group() : m_updateTimeinfo(true) { @@ -77,8 +82,7 @@ template inline bool Group::set(P& property, const V& value) updateTimeinfo(); emit modified(); return true; - } - else { + } else { return false; } } @@ -435,6 +439,23 @@ void Group::setParent(Database* db) QObject::setParent(db); } +QStringList Group::hierarchy() +{ + QStringList hierarchy; + Group* group = this; + Group* parent = m_parent; + hierarchy.prepend(group->name()); + + while (parent) { + group = group->parentGroup(); + parent = group->parentGroup(); + + hierarchy.prepend(group->name()); + } + + return hierarchy; +} + Database* Group::database() { return m_db; @@ -665,38 +686,59 @@ void Group::merge(const Group* other) Entry* existingEntry = rootGroup->findEntryByUuid(entry->uuid()); - // This entry does not exist at all. Create it. if (!existingEntry) { + // This entry does not exist at all. Create it. qDebug("New entry %s detected. Creating it.", qPrintable(entry->title())); entry->clone(Entry::CloneIncludeHistory)->setGroup(this); - // Entry is already present in the database. Update it. } else { + // Entry is already present in the database. Update it. bool locationChanged = existingEntry->timeInfo().locationChanged() < entry->timeInfo().locationChanged(); if (locationChanged && existingEntry->group() != this) { existingEntry->setGroup(this); qDebug("Location changed for entry %s. Updating it", qPrintable(existingEntry->title())); } - resolveConflict(existingEntry, entry); + resolveEntryConflict(existingEntry, entry); } } // merge groups recursively const QList dbChildren = other->children(); for (Group* group : dbChildren) { - // groups are searched by name instead of uuid - if (findChildByName(group->name())) { - findChildByName(group->name())->merge(group); - } else { + + Group* existingGroup = rootGroup->findChildByUuid(group->uuid()); + + if (!existingGroup) { qDebug("New group %s detected. Creating it.", qPrintable(group->name())); - Group* newGroup = group->clone(Entry::CloneNoFlags, true); + Group* newGroup = group->clone(Entry::CloneNoFlags, Group::CloneNoFlags); newGroup->setParent(this); newGroup->merge(group); + } else { + bool locationChanged = existingGroup->timeInfo().locationChanged() < group->timeInfo().locationChanged(); + if (locationChanged && existingGroup->parent() != this) { + existingGroup->setParent(this); + qDebug("Location changed for group %s. Updating it", qPrintable(existingGroup->name())); + } + resolveGroupConflict(existingGroup, group); + existingGroup->merge(group); } + } emit modified(); } +Group* Group::findChildByUuid(const Uuid& uuid) +{ + Q_ASSERT(!uuid.isNull()); + for (Group* group : groupsRecursive(true)) { + if (group->uuid() == uuid) { + return group; + } + } + + return nullptr; +} + Group* Group::findChildByName(const QString& name) { for (Group* group : asConst(m_children)) { @@ -708,16 +750,21 @@ Group* Group::findChildByName(const QString& name) return nullptr; } -Group* Group::clone(Entry::CloneFlags entryFlags, bool shallow) const +Group* Group::clone(Entry::CloneFlags entryFlags, Group::CloneFlags groupFlags) const { Group* clonedGroup = new Group(); clonedGroup->setUpdateTimeinfo(false); - clonedGroup->setUuid(Uuid::random()); + if (groupFlags & Group::CloneNewUuid) { + clonedGroup->setUuid(Uuid::random()); + } else { + clonedGroup->setUuid(this->uuid()); + } + clonedGroup->m_data = m_data; - if (!shallow) { + if (groupFlags & Group::CloneIncludeEntries) { const QList entryList = entries(); for (Entry* entry : entryList) { Entry* clonedEntry = entry->clone(entryFlags); @@ -726,18 +773,20 @@ Group* Group::clone(Entry::CloneFlags entryFlags, bool shallow) const const QList childrenGroups = children(); for (Group* groupChild : childrenGroups) { - Group* clonedGroupChild = groupChild->clone(entryFlags); + Group* clonedGroupChild = groupChild->clone(entryFlags, groupFlags); clonedGroupChild->setParent(clonedGroup); } } clonedGroup->setUpdateTimeinfo(true); + if (groupFlags & Group::CloneResetTimeInfo) { - QDateTime now = QDateTime::currentDateTimeUtc(); - clonedGroup->m_data.timeInfo.setCreationTime(now); - clonedGroup->m_data.timeInfo.setLastModificationTime(now); - clonedGroup->m_data.timeInfo.setLastAccessTime(now); - clonedGroup->m_data.timeInfo.setLocationChanged(now); + QDateTime now = QDateTime::currentDateTimeUtc(); + clonedGroup->m_data.timeInfo.setCreationTime(now); + clonedGroup->m_data.timeInfo.setLastModificationTime(now); + clonedGroup->m_data.timeInfo.setLastAccessTime(now); + clonedGroup->m_data.timeInfo.setLocationChanged(now); + } return clonedGroup; } @@ -891,7 +940,7 @@ bool Group::resolveAutoTypeEnabled() const } } -void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry) +void Group::resolveEntryConflict(Entry* existingEntry, Entry* otherEntry) { const QDateTime timeExisting = existingEntry->timeInfo().lastModificationTime(); const QDateTime timeOther = otherEntry->timeInfo().lastModificationTime(); @@ -929,6 +978,26 @@ void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry) } } +void Group::resolveGroupConflict(Group* existingGroup, Group* otherGroup) +{ + const QDateTime timeExisting = existingGroup->timeInfo().lastModificationTime(); + const QDateTime timeOther = otherGroup->timeInfo().lastModificationTime(); + + // only if the other group is newer, update the existing one. + if (timeExisting < timeOther) { + qDebug("Updating group %s.", qPrintable(existingGroup->name())); + existingGroup->setName(otherGroup->name()); + existingGroup->setNotes(otherGroup->notes()); + if (otherGroup->iconNumber() == 0) { + existingGroup->setIcon(otherGroup->iconUuid()); + } else { + existingGroup->setIcon(otherGroup->iconNumber()); + } + existingGroup->setExpiryTime(otherGroup->timeInfo().expiryTime()); + } + +} + QStringList Group::locate(QString locateTerm, QString currentPath) { Q_ASSERT(!locateTerm.isNull()); diff --git a/src/core/Group.h b/src/core/Group.h index 27400703..57d503a8 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -37,6 +37,14 @@ public: enum TriState { Inherit, Enable, Disable }; enum MergeMode { ModeInherit, KeepBoth, KeepNewer, KeepExisting }; + enum CloneFlag { + CloneNoFlags = 0, + CloneNewUuid = 1, // generate a random uuid for the clone + CloneResetTimeInfo = 2, // set all TimeInfo attributes to the current time + CloneIncludeEntries = 4, // clone the group entries + }; + Q_DECLARE_FLAGS(CloneFlags, CloneFlag) + struct GroupData { QString name; @@ -78,8 +86,11 @@ public: static const int DefaultIconNumber; static const int RecycleBinIconNumber; + static CloneFlags DefaultCloneFlags; + static Entry::CloneFlags DefaultEntryCloneFlags; Group* findChildByName(const QString& name); + Group* findChildByUuid(const Uuid& uuid); Entry* findEntry(QString entryId); Entry* findEntryByUuid(const Uuid& uuid); Entry* findEntryByPath(QString entryPath, QString basePath = QString("")); @@ -106,6 +117,7 @@ public: Group* parentGroup(); const Group* parentGroup() const; void setParent(Group* parent, int index = -1); + QStringList hierarchy(); Database* database(); const Database* database() const; @@ -118,14 +130,13 @@ public: QList groupsRecursive(bool includeSelf); QSet customIconsRecursive() const; /** - * Creates a duplicate of this group including all child entries and groups (if not shallow). - * The exceptions are that the returned group doesn't have a parent group - * and all TimeInfo attributes are set to the current time. + * Creates a duplicate of this group. * Note that you need to copy the custom icons manually when inserting the * new group into another database. */ - Group* clone(Entry::CloneFlags entryFlags = Entry::CloneNewUuid | Entry::CloneResetTimeInfo, - bool shallow = false) const; + Group* clone(Entry::CloneFlags entryFlags = DefaultEntryCloneFlags, + CloneFlags groupFlags = DefaultCloneFlags) const; + void copyDataFrom(const Group* other); void merge(const Group* other); QString print(bool recursive = false, int depth = 0); @@ -159,7 +170,8 @@ private: void removeEntry(Entry* entry); void setParent(Database* db); void markOlderEntry(Entry* entry); - void resolveConflict(Entry* existingEntry, Entry* otherEntry); + void resolveEntryConflict(Entry* existingEntry, Entry* otherEntry); + void resolveGroupConflict(Group* existingGroup, Group* otherGroup); void recSetDatabase(Database* db); void cleanupParent(); @@ -182,4 +194,6 @@ private: friend void Entry::setGroup(Group* group); }; +Q_DECLARE_OPERATORS_FOR_FLAGS(Group::CloneFlags) + #endif // KEEPASSX_GROUP_H diff --git a/src/core/MacPasteboard.cpp b/src/core/MacPasteboard.cpp new file mode 100644 index 00000000..98dc6f7a --- /dev/null +++ b/src/core/MacPasteboard.cpp @@ -0,0 +1,94 @@ +/* + * 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 . + */ + +#include "MacPasteboard.h" + +QString MacPasteboard::convertorName() { return QLatin1String("MacPasteboard"); } + +QString MacPasteboard::flavorFor(const QString& mimetype) { + if (mimetype == QLatin1String("text/plain")) { + return QLatin1String("public.utf8-plain-text"); + } else if (mimetype == QLatin1String("application/x-nspasteboard-concealed-type")) { + return QLatin1String("org.nspasteboard.ConcealedType"); + } + + int i = mimetype.indexOf(QLatin1String("charset=")); + + if (i >= 0) { + QString cs(mimetype.mid(i + 8).toLower()); + i = cs.indexOf(QLatin1Char(';')); + + if (i >= 0) { + cs = cs.left(i); + } + + if (cs == QLatin1String("system")) { + return QLatin1String("public.utf8-plain-text"); + } else if (cs == QLatin1String("iso-10646-ucs-2") || + cs == QLatin1String("utf16")) { + return QLatin1String("public.utf16-plain-text"); + } + } + return QString(); +} + +QString MacPasteboard::mimeFor(QString flavor) { + if (flavor == QLatin1String("public.utf8-plain-text")) + return QLatin1String("text/plain"); + if (flavor == QLatin1String("org.nspasteboard.ConcealedType")) + return QLatin1String("application/x-nspasteboard-concealed-type"); + if (flavor == QLatin1String("public.utf16-plain-text")) + return QLatin1String("text/plain;charset=utf16"); + return QString(); +} + +bool MacPasteboard::canConvert(const QString& mimetype, QString flavor) { + Q_UNUSED(mimetype); + Q_UNUSED(flavor); + return true; +} + +QVariant MacPasteboard::convertToMime(const QString& mimetype, QList data, QString flavor) { + if (data.count() > 1) + qWarning("QMime::convertToMime: Cannot handle multiple member data"); + const QByteArray& firstData = data.first(); + QVariant ret; + if (flavor == QLatin1String("public.utf8-plain-text")) { + ret = QString::fromUtf8(firstData); + } else if (flavor == QLatin1String("org.nspasteboard.ConcealedType")) { + ret = QString::fromUtf8(firstData); + } else if (flavor == QLatin1String("public.utf16-plain-text")) { + ret = QTextCodec::codecForName("UTF-16")->toUnicode(firstData); + } else { + qWarning("QMime::convertToMime: unhandled mimetype: %s", + qPrintable(mimetype)); + } + return ret; +} + +QList MacPasteboard::convertFromMime(const QString&, QVariant data, QString flavor) { + QList ret; + QString string = data.toString(); + if (flavor == QLatin1String("public.utf8-plain-text")) + ret.append(string.toUtf8()); + else if (flavor == QLatin1String("org.nspasteboard.ConcealedType")) + ret.append(string.toUtf8()); + else if (flavor == QLatin1String("public.utf16-plain-text")) + ret.append(QTextCodec::codecForName("UTF-16")->fromUnicode(string)); + return ret; +} + diff --git a/src/core/MacPasteboard.h b/src/core/MacPasteboard.h new file mode 100644 index 00000000..8461cbc5 --- /dev/null +++ b/src/core/MacPasteboard.h @@ -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 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_MACPASTEBOARD_H +#define KEEPASSXC_MACPASTEBOARD_H + +#include +#include + +class MacPasteboard : public QMacPasteboardMime +{ +public: + explicit MacPasteboard() : QMacPasteboardMime(MIME_ALL) {} + + QString convertorName() override; + bool canConvert(const QString &mime, QString flav) override; + QString mimeFor(QString flav) override; + QString flavorFor(const QString &mime) override; + QVariant convertToMime(const QString &mime, QList data, QString flav) override; + QList convertFromMime(const QString &mime, QVariant data, QString flav) override; +}; + +#endif // KEEPASSXC_MACPASTEBOARD_H diff --git a/src/core/Optional.h b/src/core/Optional.h deleted file mode 100644 index fb800198..00000000 --- a/src/core/Optional.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 OPTIONAL_H -#define OPTIONAL_H - -/* - * This utility class is for providing basic support for an option type. - * It can be replaced by std::optional (C++17) or - * std::experimental::optional (C++11) when they become fully supported - * by all the compilers. - */ - -template -class Optional -{ -public: - - // None - Optional() : - m_hasValue(false), - m_value() - { }; - - // Some T - Optional(const T& value) : - m_hasValue(true), - m_value(value) - { }; - - // Copy - Optional(const Optional& other) : - m_hasValue(other.m_hasValue), - m_value(other.m_value) - { }; - - const Optional& operator=(const Optional& other) - { - m_hasValue = other.m_hasValue; - m_value = other.m_value; - return *this; - } - - bool operator==(const Optional& other) const - { - if(m_hasValue) - return other.m_hasValue && m_value == other.m_value; - else - return !other.m_hasValue; - } - - bool operator!=(const Optional& other) const - { - return !(*this == other); - } - - bool hasValue() const - { - return m_hasValue; - } - - T valueOr(const T& other) const - { - return m_hasValue ? m_value : other; - } - - Optional static makeOptional(const T& value) - { - return Optional(value); - } - - -private: - - bool m_hasValue; - T m_value; -}; - -#endif // OPTIONAL_H diff --git a/src/core/ScreenLockListenerDBus.cpp b/src/core/ScreenLockListenerDBus.cpp index 1976b47e..6c1b7fe9 100644 --- a/src/core/ScreenLockListenerDBus.cpp +++ b/src/core/ScreenLockListenerDBus.cpp @@ -20,6 +20,7 @@ #include #include #include +#include ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget *parent): ScreenLockListenerPrivate(parent) @@ -34,7 +35,7 @@ ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget *parent): "ActiveChanged", // signal name this, //receiver SLOT(freedesktopScreenSaver(bool))); - + sessionBus.connect( "org.gnome.SessionManager", // service "/org/gnome/SessionManager/Presence", // path @@ -51,6 +52,15 @@ ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget *parent): this, //receiver SLOT(logindPrepareForSleep(bool))); + QString sessionId = QProcessEnvironment::systemEnvironment().value("XDG_SESSION_ID"); + systemBus.connect( + "", // service + QString("/org/freedesktop/login1/session/") + sessionId, // path + "org.freedesktop.login1.Session", // interface + "Lock", // signal name + this, //receiver + SLOT(unityLocked())); + sessionBus.connect( "com.canonical.Unity", // service "/com/canonical/Unity/Session", // path @@ -84,4 +94,4 @@ void ScreenLockListenerDBus::freedesktopScreenSaver(bool status) if (status) { emit screenLocked(); } -} \ No newline at end of file +} diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index 98d48196..016103b2 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -74,6 +74,11 @@ bool SymmetricCipher::reset() return m_backend->reset(); } +int SymmetricCipher::keySize() const +{ + return m_backend->keySize(); +} + int SymmetricCipher::blockSize() const { return m_backend->blockSize(); diff --git a/src/crypto/SymmetricCipher.h b/src/crypto/SymmetricCipher.h index b85c58b7..81e13f38 100644 --- a/src/crypto/SymmetricCipher.h +++ b/src/crypto/SymmetricCipher.h @@ -38,6 +38,7 @@ public: enum Mode { Cbc, + Ctr, Ecb, Stream }; @@ -69,6 +70,7 @@ public: } bool reset(); + int keySize() const; int blockSize() const; QString errorString() const; diff --git a/src/crypto/SymmetricCipherBackend.h b/src/crypto/SymmetricCipherBackend.h index 78ec60c6..dd493d2d 100644 --- a/src/crypto/SymmetricCipherBackend.h +++ b/src/crypto/SymmetricCipherBackend.h @@ -33,6 +33,7 @@ public: Q_REQUIRED_RESULT virtual bool processInPlace(QByteArray& data, quint64 rounds) = 0; virtual bool reset() = 0; + virtual int keySize() const = 0; virtual int blockSize() const = 0; virtual QString errorString() const = 0; diff --git a/src/crypto/SymmetricCipherGcrypt.cpp b/src/crypto/SymmetricCipherGcrypt.cpp index cd432406..ed031c00 100644 --- a/src/crypto/SymmetricCipherGcrypt.cpp +++ b/src/crypto/SymmetricCipherGcrypt.cpp @@ -26,7 +26,6 @@ SymmetricCipherGcrypt::SymmetricCipherGcrypt(SymmetricCipher::Algorithm algo, Sy , m_algo(gcryptAlgo(algo)) , m_mode(gcryptMode(mode)) , m_direction(direction) - , m_blockSize(-1) { } @@ -62,6 +61,9 @@ int SymmetricCipherGcrypt::gcryptMode(SymmetricCipher::Mode mode) case SymmetricCipher::Cbc: return GCRY_CIPHER_MODE_CBC; + case SymmetricCipher::Ctr: + return GCRY_CIPHER_MODE_CTR; + case SymmetricCipher::Stream: return GCRY_CIPHER_MODE_STREAM; @@ -92,14 +94,6 @@ bool SymmetricCipherGcrypt::init() return false; } - size_t blockSizeT; - error = gcry_cipher_algo_info(m_algo, GCRYCTL_GET_BLKLEN, nullptr, &blockSizeT); - if (error != 0) { - setErrorString(error); - return false; - } - - m_blockSize = blockSizeT; return true; } @@ -119,7 +113,13 @@ bool SymmetricCipherGcrypt::setKey(const QByteArray& key) bool SymmetricCipherGcrypt::setIv(const QByteArray& iv) { m_iv = iv; - gcry_error_t error = gcry_cipher_setiv(m_ctx, m_iv.constData(), m_iv.size()); + gcry_error_t error; + + if (m_mode == GCRY_CIPHER_MODE_CTR) { + error = gcry_cipher_setctr(m_ctx, m_iv.constData(), m_iv.size()); + } else { + error = gcry_cipher_setiv(m_ctx, m_iv.constData(), m_iv.size()); + } if (error != 0) { setErrorString(error); @@ -148,9 +148,10 @@ QByteArray SymmetricCipherGcrypt::process(const QByteArray& data, bool* ok) if (error != 0) { setErrorString(error); *ok = false; + } else { + *ok = true; } - *ok = true; return result; } @@ -227,9 +228,28 @@ bool SymmetricCipherGcrypt::reset() return true; } +int SymmetricCipherGcrypt::keySize() const +{ + gcry_error_t error; + size_t keySizeT; + + error = gcry_cipher_algo_info(m_algo, GCRYCTL_GET_KEYLEN, nullptr, &keySizeT); + if (error != 0) + return -1; + + return keySizeT; +} + int SymmetricCipherGcrypt::blockSize() const { - return m_blockSize; + gcry_error_t error; + size_t blockSizeT; + + error = gcry_cipher_algo_info(m_algo, GCRYCTL_GET_BLKLEN, nullptr, &blockSizeT); + if (error != 0) + return -1; + + return blockSizeT; } QString SymmetricCipherGcrypt::errorString() const diff --git a/src/crypto/SymmetricCipherGcrypt.h b/src/crypto/SymmetricCipherGcrypt.h index d3ad8d15..108bc14e 100644 --- a/src/crypto/SymmetricCipherGcrypt.h +++ b/src/crypto/SymmetricCipherGcrypt.h @@ -39,6 +39,7 @@ public: Q_REQUIRED_RESULT bool processInPlace(QByteArray& data, quint64 rounds); bool reset(); + int keySize() const; int blockSize() const; QString errorString() const; @@ -54,7 +55,6 @@ private: const SymmetricCipher::Direction m_direction; QByteArray m_key; QByteArray m_iv; - int m_blockSize; QString m_errorString; }; diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index d63151c8..f8f60f11 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -45,6 +45,7 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) m_error = false; m_errorStr.clear(); + QByteArray transformSeed = randomGen()->randomArray(32); QByteArray masterSeed = randomGen()->randomArray(32); QByteArray encryptionIV = randomGen()->randomArray(16); QByteArray protectedStreamKey = randomGen()->randomArray(32); @@ -52,7 +53,12 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) QByteArray endOfHeader = "\r\n\r\n"; if (db->challengeMasterSeed(masterSeed) == false) { - raiseError("Unable to issue challenge-response."); + raiseError(tr("Unable to issue challenge-response.")); + return; + } + + if (!db->transformKeyWithSeed(transformSeed)) { + raiseError(tr("Unable to calculate master key")); return; } diff --git a/src/format/KeePass2Writer.h b/src/format/KeePass2Writer.h index 1b3436dc..184aa1a7 100644 --- a/src/format/KeePass2Writer.h +++ b/src/format/KeePass2Writer.h @@ -18,6 +18,8 @@ #ifndef KEEPASSX_KEEPASS2WRITER_H #define KEEPASSX_KEEPASS2WRITER_H +#include + #include "format/KeePass2.h" #include "keys/CompositeKey.h" @@ -26,6 +28,8 @@ class QIODevice; class KeePass2Writer { + Q_DECLARE_TR_FUNCTIONS(KeePass2Writer) + public: KeePass2Writer(); void writeDatabase(QIODevice* device, Database* db); diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 7cbd648e..e89a7fdc 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -55,13 +55,17 @@ AboutDialog::AboutDialog(QWidget* parent) QString debugInfo = "KeePassXC - "; debugInfo.append(tr("Version %1\n").arg(KEEPASSX_VERSION)); if (!commitHash.isEmpty()) { - debugInfo.append(tr("Revision: %1").arg(commitHash).append("\n\n")); + debugInfo.append(tr("Revision: %1").arg(commitHash.left(7)).append("\n")); } - debugInfo.append(QString("%1\n- Qt %2\n- %3\n\n") - .arg(tr("Libraries:"), - QString::fromLocal8Bit(qVersion()), - Crypto::backendVersion())); +#ifdef KEEPASSXC_DIST + debugInfo.append(tr("Distribution: %1").arg(KEEPASSXC_DIST_TYPE).append("\n")); +#endif + + debugInfo.append("\n").append(QString("%1\n- Qt %2\n- %3\n\n") + .arg(tr("Libraries:")) + .arg(QString::fromLocal8Bit(qVersion())) + .arg(Crypto::backendVersion())); #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) debugInfo.append(tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4") @@ -83,6 +87,9 @@ AboutDialog::AboutDialog(QWidget* parent) #ifdef WITH_XC_YUBIKEY extensions += "\n- YubiKey"; #endif +#ifdef WITH_XC_SSHAGENT + extensions += "\n- SSH Agent"; +#endif if (extensions.isEmpty()) extensions = " None"; diff --git a/src/gui/Clipboard.cpp b/src/gui/Clipboard.cpp index bf4db8ff..78bad273 100644 --- a/src/gui/Clipboard.cpp +++ b/src/gui/Clipboard.cpp @@ -28,6 +28,9 @@ Clipboard* Clipboard::m_instance(nullptr); Clipboard::Clipboard(QObject* parent) : QObject(parent) , m_timer(new QTimer(this)) +#ifdef Q_OS_MAC + , m_pasteboard(new MacPasteboard) +#endif { m_timer->setSingleShot(true); connect(m_timer, SIGNAL(timeout()), SLOT(clearClipboard())); @@ -38,10 +41,17 @@ void Clipboard::setText(const QString& text) { QClipboard* clipboard = QApplication::clipboard(); +#ifdef Q_OS_MAC + QMimeData* mime = new QMimeData; + mime->setText(text); + mime->setData("application/x-nspasteboard-concealed-type", text.toUtf8()); + clipboard->setMimeData(mime, QClipboard::Clipboard); +#else clipboard->setText(text, QClipboard::Clipboard); if (clipboard->supportsSelection()) { clipboard->setText(text, QClipboard::Selection); } +#endif if (config()->get("security/clearclipboard").toBool()) { int timeout = config()->get("security/clearclipboardtimeout").toInt(); diff --git a/src/gui/Clipboard.h b/src/gui/Clipboard.h index e0a16d26..6f8ff9ac 100644 --- a/src/gui/Clipboard.h +++ b/src/gui/Clipboard.h @@ -19,6 +19,9 @@ #define KEEPASSX_CLIPBOARD_H #include +#ifdef Q_OS_MAC +#include "core/MacPasteboard.h" +#endif class QTimer; @@ -43,6 +46,9 @@ private: static Clipboard* m_instance; QTimer* m_timer; +#ifdef Q_OS_MAC + QScopedPointer m_pasteboard; +#endif QString m_lastCopied; }; diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index b8f8feb1..451dc597 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -95,11 +95,16 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event) m_ui->editPassword->setFocus(); #ifdef WITH_XC_YUBIKEY - connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection); - connect(YubiKey::instance(), SIGNAL(detectComplete()), SLOT(yubikeyDetectComplete()), Qt::QueuedConnection); - connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection); + // showEvent() may be called twice, so make sure we are only polling once + if (!m_yubiKeyBeingPolled) { + connect(YubiKey::instance(), SIGNAL(detected(int, bool)), SLOT(yubikeyDetected(int, bool)), + Qt::QueuedConnection); + connect(YubiKey::instance(), SIGNAL(detectComplete()), SLOT(yubikeyDetectComplete()), Qt::QueuedConnection); + connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection); - pollYubikey(); + pollYubikey(); + m_yubiKeyBeingPolled = true; + } #endif } @@ -110,6 +115,7 @@ void DatabaseOpenWidget::hideEvent(QHideEvent* event) #ifdef WITH_XC_YUBIKEY // Don't listen to any Yubikey events if we are hidden disconnect(YubiKey::instance(), 0, this, 0); + m_yubiKeyBeingPolled = false; #endif } @@ -162,7 +168,10 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile) void DatabaseOpenWidget::openDatabase() { KeePass2Reader reader; - CompositeKey masterKey = databaseKey(); + QSharedPointer masterKey = databaseKey(); + if (masterKey.isNull()) { + return; + } QFile file(m_filename); if (!file.open(QIODevice::ReadOnly)) { @@ -174,7 +183,7 @@ void DatabaseOpenWidget::openDatabase() delete m_db; } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - m_db = reader.readDatabase(&file, masterKey); + m_db = reader.readDatabase(&file, *masterKey); QApplication::restoreOverrideCursor(); if (m_db) { @@ -182,20 +191,19 @@ void DatabaseOpenWidget::openDatabase() m_ui->messageWidget->animatedHide(); } emit editFinished(true); - } - else { - m_ui->messageWidget->showMessage(tr("Unable to open the database.") - .append("\n").append(reader.errorString()), MessageWidget::Error); + } else { + m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(reader.errorString()), + MessageWidget::Error); m_ui->editPassword->clear(); } } -CompositeKey DatabaseOpenWidget::databaseKey() +QSharedPointer DatabaseOpenWidget::databaseKey() { - CompositeKey masterKey; + auto masterKey = QSharedPointer::create(); if (m_ui->checkPassword->isChecked()) { - masterKey.addKey(PasswordKey(m_ui->editPassword->text())); + masterKey->addKey(PasswordKey(m_ui->editPassword->text())); } QHash lastKeyFiles = config()->get("LastKeyFiles").toHash(); @@ -206,11 +214,11 @@ CompositeKey DatabaseOpenWidget::databaseKey() QString keyFilename = m_ui->comboKeyFile->currentText(); QString errorMsg; if (!key.load(keyFilename, &errorMsg)) { - m_ui->messageWidget->showMessage(tr("Can't open key file").append(":\n") - .append(errorMsg), MessageWidget::Error); - return CompositeKey(); + m_ui->messageWidget->showMessage(tr("Can't open key file").append(":\n").append(errorMsg), + MessageWidget::Error); + return QSharedPointer(); } - masterKey.addKey(key); + masterKey->addKey(key); lastKeyFiles[m_filename] = keyFilename; } else { lastKeyFiles.remove(m_filename); @@ -237,9 +245,9 @@ CompositeKey DatabaseOpenWidget::databaseKey() // read blocking mode from LSB and slot index number from second LSB bool blocking = comboPayload & 1; - int slot = comboPayload >> 1; - auto key = QSharedPointer(new YkChallengeResponseKey(slot, blocking)); - masterKey.addChallengeResponseKey(key); + int slot = comboPayload >> 1; + auto key = QSharedPointer(new YkChallengeResponseKey(slot, blocking)); + masterKey->addChallengeResponseKey(key); } #endif @@ -269,6 +277,9 @@ void DatabaseOpenWidget::activateChallengeResponse() void DatabaseOpenWidget::browseKeyFile() { QString filters = QString("%1 (*);;%2 (*.key)").arg(tr("All files"), tr("Key files")); + if (!config()->get("RememberLastKeyFiles").toBool()) { + fileDialog()->setNextForgetDialog(); + } QString filename = fileDialog()->getOpenFileName(this, tr("Select key file"), QString(), filters); if (!filename.isEmpty()) { @@ -309,10 +320,12 @@ void DatabaseOpenWidget::yubikeyDetectComplete() m_ui->checkChallengeResponse->setEnabled(true); m_ui->buttonRedetectYubikey->setEnabled(true); m_ui->yubikeyProgress->setVisible(false); + m_yubiKeyBeingPolled = false; } void DatabaseOpenWidget::noYubikeyFound() { m_ui->buttonRedetectYubikey->setEnabled(true); m_ui->yubikeyProgress->setVisible(false); + m_yubiKeyBeingPolled = false; } diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index d4b47364..aade111c 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -52,7 +52,7 @@ signals: protected: void showEvent(QShowEvent* event) override; void hideEvent(QHideEvent* event) override; - CompositeKey databaseKey(); + QSharedPointer databaseKey(); protected slots: virtual void openDatabase(); @@ -73,6 +73,7 @@ protected: QString m_filename; private: + bool m_yubiKeyBeingPolled = false; Q_DISABLE_COPY(DatabaseOpenWidget) }; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 67edd130..8cade696 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -129,12 +129,11 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw, while (i.hasNext()) { i.next(); if (i.value().canonicalFilePath == canonicalFilePath) { - if (pw.isEmpty() && keyFile.isEmpty()) { - setCurrentIndex(databaseIndex(i.key())); + if (!i.value().dbWidget->dbHasKey() && !(pw.isNull() && keyFile.isEmpty())) { + // If the database is locked and a pw or keyfile is provided, unlock it + i.value().dbWidget->switchToOpenDatabase(i.value().filePath, pw, keyFile); } else { - if (!i.key()->hasKey()) { - i.value().dbWidget->switchToOpenDatabase(canonicalFilePath, pw, keyFile); - } + setCurrentIndex(databaseIndex(i.key())); } return; } @@ -210,7 +209,7 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw, updateLastDatabases(dbStruct.filePath); - if (!pw.isNull() || !keyFile.isEmpty()) { + if (!(pw.isNull() && keyFile.isEmpty())) { dbStruct.dbWidget->switchToOpenDatabase(dbStruct.filePath, pw, keyFile); } else { @@ -299,8 +298,7 @@ bool DatabaseTabWidget::closeDatabase(Database* db) if (!saveDatabase(db)) { return false; } - } - else { + } else if (dbStruct.dbWidget->currentMode() != DatabaseWidget::LockedMode) { QMessageBox::StandardButton result = MessageBox::question( this, tr("Save changes?"), @@ -308,10 +306,9 @@ bool DatabaseTabWidget::closeDatabase(Database* db) QMessageBox::Yes | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Yes); if (result == QMessageBox::Yes) { if (!saveDatabase(db)) { - return false; + return false; } - } - else if (result == QMessageBox::Cancel) { + } else if (result == QMessageBox::Cancel) { return false; } } @@ -356,8 +353,13 @@ bool DatabaseTabWidget::saveDatabase(Database* db) { DatabaseManagerStruct& dbStruct = m_dbList[db]; - if (dbStruct.saveToFilename) { + if (dbStruct.dbWidget->currentMode() == DatabaseWidget::LockedMode) { + // Never allow saving a locked database; it causes corruption + // We return true since a save is not required + return true; + } + if (dbStruct.saveToFilename) { dbStruct.dbWidget->blockAutoReload(true); QString errorMessage = db->saveToFile(dbStruct.canonicalFilePath); dbStruct.dbWidget->blockAutoReload(false); @@ -376,7 +378,6 @@ bool DatabaseTabWidget::saveDatabase(Database* db) MessageWidget::Error); return false; } - } else { return saveDatabaseAs(db); } @@ -540,6 +541,16 @@ bool DatabaseTabWidget::readOnly(int index) return indexDatabaseManagerStruct(index).readOnly; } +bool DatabaseTabWidget::canSave(int index) +{ + if (index == -1) { + index = currentIndex(); + } + + const DatabaseManagerStruct& dbStruct = indexDatabaseManagerStruct(index); + return !dbStruct.saveToFilename || (dbStruct.modified && !dbStruct.readOnly); +} + bool DatabaseTabWidget::isModified(int index) { if (index == -1) { diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 847eaef0..db237d98 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -78,6 +78,7 @@ public slots: void changeMasterKey(); void changeDatabaseSettings(); bool readOnly(int index = -1); + bool canSave(int index = -1); bool isModified(int index = -1); void performGlobalAutoType(); void lockDatabases(); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index e5693e4f..5b274dc0 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -47,6 +47,7 @@ #include "gui/TotpDialog.h" #include "gui/DatabaseOpenWidget.h" #include "gui/DatabaseSettingsWidget.h" +#include "gui/DetailsWidget.h" #include "gui/KeePass1OpenWidget.h" #include "gui/MessageBox.h" #include "gui/UnlockDatabaseWidget.h" @@ -56,12 +57,19 @@ #include "gui/group/EditGroupWidget.h" #include "gui/group/GroupView.h" +#include "config-keepassx.h" + +#ifdef WITH_XC_SSHAGENT +#include "sshagent/SSHAgent.h" +#endif + DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) : QStackedWidget(parent) , m_db(db) , m_newGroup(nullptr) , m_newEntry(nullptr) , m_newParent(nullptr) + , m_importingCsv(false) { m_mainWidget = new QWidget(this); @@ -72,12 +80,15 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) QLayout* layout = new QHBoxLayout(); mainLayout->addWidget(m_messageWidget); mainLayout->addLayout(layout); - m_splitter = new QSplitter(m_mainWidget); - m_splitter->setChildrenCollapsible(false); + m_mainSplitter = new QSplitter(m_mainWidget); + m_mainSplitter->setChildrenCollapsible(false); + m_detailSplitter = new QSplitter(m_mainWidget); + m_detailSplitter->setOrientation(Qt::Vertical); + m_detailSplitter->setChildrenCollapsible(true); - QWidget* rightHandSideWidget = new QWidget(m_splitter); + QWidget* rightHandSideWidget = new QWidget(m_mainSplitter); - m_groupView = new GroupView(db, m_splitter); + m_groupView = new GroupView(db, m_mainSplitter); m_groupView->setObjectName("groupView"); m_groupView->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_groupView, SIGNAL(customContextMenuRequested(QPoint)), @@ -99,10 +110,18 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) "border: 2px solid rgb(190, 190, 190);" "border-radius: 5px;"); + m_detailsView = new DetailsWidget(this); + QVBoxLayout* vLayout = new QVBoxLayout(rightHandSideWidget); vLayout->setMargin(0); vLayout->addWidget(m_searchingLabel); - vLayout->addWidget(m_entryView); + vLayout->addWidget(m_detailSplitter); + + m_detailSplitter->addWidget(m_entryView); + m_detailSplitter->addWidget(m_detailsView); + + m_detailSplitter->setStretchFactor(0, 80); + m_detailSplitter->setStretchFactor(1, 20); m_searchingLabel->setVisible(false); @@ -110,13 +129,13 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) setTabOrder(m_entryView, m_groupView); - m_splitter->addWidget(m_groupView); - m_splitter->addWidget(rightHandSideWidget); + m_mainSplitter->addWidget(m_groupView); + m_mainSplitter->addWidget(rightHandSideWidget); - m_splitter->setStretchFactor(0, 30); - m_splitter->setStretchFactor(1, 70); + m_mainSplitter->setStretchFactor(0, 30); + m_mainSplitter->setStretchFactor(1, 70); - layout->addWidget(m_splitter); + layout->addWidget(m_mainSplitter); m_mainWidget->setLayout(mainLayout); m_editEntryWidget = new EditEntryWidget(); @@ -125,13 +144,14 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_editGroupWidget = new EditGroupWidget(); m_editGroupWidget->setObjectName("editGroupWidget"); m_changeMasterKeyWidget = new ChangeMasterKeyWidget(); + m_changeMasterKeyWidget->setObjectName("changeMasterKeyWidget"); m_changeMasterKeyWidget->headlineLabel()->setText(tr("Change master key")); - m_csvImportWizard = new CsvImportWizard(); - m_csvImportWizard->setObjectName("csvImportWizard"); QFont headlineLabelFont = m_changeMasterKeyWidget->headlineLabel()->font(); headlineLabelFont.setBold(true); headlineLabelFont.setPointSize(headlineLabelFont.pointSize() + 2); m_changeMasterKeyWidget->headlineLabel()->setFont(headlineLabelFont); + m_csvImportWizard = new CsvImportWizard(); + m_csvImportWizard->setObjectName("csvImportWizard"); m_databaseSettingsWidget = new DatabaseSettingsWidget(); m_databaseSettingsWidget->setObjectName("databaseSettingsWidget"); m_databaseOpenWidget = new DatabaseOpenWidget(); @@ -156,7 +176,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) addWidget(m_keepass1OpenWidget); addWidget(m_unlockDatabaseWidget); - connect(m_splitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged())); + connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(mainSplitterSizesChanged())); + connect(m_detailSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(detailSplitterSizesChanged())); connect(m_entryView->header(), SIGNAL(sectionResized(int,int,int)), SIGNAL(entryColumnSizesChanged())); connect(m_groupView, SIGNAL(groupChanged(Group*)), this, SLOT(onGroupChanged(Group*))); connect(m_groupView, SIGNAL(groupChanged(Group*)), SIGNAL(groupChanged())); @@ -180,6 +201,12 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(unblockAutoReload())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); + connect(m_groupView, SIGNAL(groupPressed(Group*)), SLOT(emitPressedGroup(Group*))); + connect(m_groupView, SIGNAL(groupChanged(Group*)), SLOT(emitPressedGroup(Group*))); + connect(m_entryView, SIGNAL(entryPressed(Entry*)), SLOT(emitPressedEntry(Entry*))); + connect(m_entryView, SIGNAL(entrySelectionChanged()), SLOT(emitPressedEntry())); + connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(emitPressedEntry())); + m_databaseModified = false; m_fileWatchTimer.setSingleShot(true); @@ -189,6 +216,13 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_searchCaseSensitive = false; m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool(); +#ifdef WITH_XC_SSHAGENT + if (config()->get("SSHAgent", false).toBool()) { + connect(this, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), SSHAgent::instance(), SLOT(databaseModeChanged(DatabaseWidget::Mode))); + connect(this, SIGNAL(closeRequest()), SSHAgent::instance(), SLOT(databaseModeChanged())); + } +#endif + setCurrentWidget(m_mainWidget); } @@ -233,14 +267,24 @@ bool DatabaseWidget::isEditWidgetModified() const } } -QList DatabaseWidget::splitterSizes() const +QList DatabaseWidget::mainSplitterSizes() const { - return m_splitter->sizes(); + return m_mainSplitter->sizes(); } -void DatabaseWidget::setSplitterSizes(const QList& sizes) +void DatabaseWidget::setMainSplitterSizes(const QList& sizes) { - m_splitter->setSizes(sizes); + m_mainSplitter->setSizes(sizes); +} + +QList DatabaseWidget::detailSplitterSizes() const +{ + return m_detailSplitter->sizes(); +} + +void DatabaseWidget::setDetailSplitterSizes(const QList &sizes) +{ + m_detailSplitter->setSizes(sizes); } QList DatabaseWidget::entryHeaderViewSizes() const @@ -373,6 +417,8 @@ void DatabaseWidget::setupTotp() setupTotpDialog->setSeed(currentEntry->totpSeed()); setupTotpDialog->setStep(currentEntry->totpStep()); setupTotpDialog->setDigits(currentEntry->totpDigits()); + // now that all settings are set, decide whether it's default, steam or custom + setupTotpDialog->setSettings(currentEntry->totpDigits()); } setupTotpDialog->open(); @@ -753,6 +799,12 @@ void DatabaseWidget::switchToGroupEdit(Group* group, bool create) void DatabaseWidget::updateMasterKey(bool accepted) { + if (m_importingCsv) { + setCurrentWidget(m_csvImportWizard); + m_csvImportWizard->keyFinished(accepted, m_changeMasterKeyWidget->newMasterKey()); + return; + } + if (accepted) { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); bool result = m_db->setKey(m_changeMasterKeyWidget->newMasterKey()); @@ -825,7 +877,7 @@ void DatabaseWidget::unlockDatabase(bool accepted) return; } - Database *db = Q_NULLPTR; + Database* db = nullptr; if (sender() == m_unlockDatabaseDialog) { db = m_unlockDatabaseDialog->database(); } else if (sender() == m_unlockDatabaseWidget) { @@ -886,6 +938,7 @@ void DatabaseWidget::switchToMasterKeyChange(bool disableCancel) m_changeMasterKeyWidget->clearForms(); m_changeMasterKeyWidget->setCancelEnabled(!disableCancel); setCurrentWidget(m_changeMasterKeyWidget); + m_importingCsv = false; } void DatabaseWidget::switchToDatabaseSettings() @@ -921,9 +974,11 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString void DatabaseWidget::switchToImportCsv(const QString& fileName) { updateFilename(fileName); - switchToMasterKeyChange(); m_csvImportWizard->load(fileName, m_db); - setCurrentWidget(m_csvImportWizard); + m_changeMasterKeyWidget->clearForms(); + m_changeMasterKeyWidget->setCancelEnabled(false); + setCurrentWidget(m_changeMasterKeyWidget); + m_importingCsv = true; } void DatabaseWidget::switchToOpenMergeDatabase(const QString& fileName) @@ -1050,6 +1105,32 @@ void DatabaseWidget::emitEntryContextMenuRequested(const QPoint& pos) emit entryContextMenuRequested(m_entryView->viewport()->mapToGlobal(pos)); } +void DatabaseWidget::emitPressedEntry() +{ + Entry* currentEntry = m_entryView->currentEntry(); + emitPressedEntry(currentEntry); +} + +void DatabaseWidget::emitPressedEntry(Entry* currentEntry) +{ + if (!currentEntry) { + // if no entry is pressed, leave in details the last entry + return; + } + + emit pressedEntry(currentEntry); +} + +void DatabaseWidget::emitPressedGroup(Group* currentGroup) +{ + if (!currentGroup) { + // if no group is pressed, leave in details the last group + return; + } + + emit pressedGroup(currentGroup); +} + bool DatabaseWidget::dbHasKey() const { return m_db->hasKey(); @@ -1134,8 +1215,13 @@ void DatabaseWidget::onWatchedFileChanged() void DatabaseWidget::reloadDatabaseFile() { - if (m_db == nullptr) + if (!m_db || currentMode() == DatabaseWidget::LockedMode) { return; + } + + if (currentMode() == DatabaseWidget::LockedMode) { + return; + } if (! config()->get("AutoReloadOnChange").toBool()) { // Ask if we want to reload the db @@ -1325,6 +1411,12 @@ void DatabaseWidget::showUnlockDialog() { m_unlockDatabaseDialog->clearForms(); m_unlockDatabaseDialog->setDBFilename(m_filename); + +#if defined(Q_OS_MAC) + autoType()->raiseWindow(); + Tools::wait(500); +#endif + m_unlockDatabaseDialog->show(); m_unlockDatabaseDialog->activateWindow(); } @@ -1334,9 +1426,10 @@ void DatabaseWidget::closeUnlockDialog() m_unlockDatabaseDialog->close(); } -void DatabaseWidget::showMessage(const QString& text, MessageWidget::MessageType type) +void DatabaseWidget::showMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton, int autoHideTimeout) { - m_messageWidget->showMessage(text, type); + m_messageWidget->setCloseButtonVisible(showClosebutton); + m_messageWidget->showMessage(text, type, autoHideTimeout); } void DatabaseWidget::hideMessage() diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 734e979e..e9428a70 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -47,6 +47,7 @@ class QSplitter; class QLabel; class UnlockDatabaseWidget; class MessageWidget; +class DetailsWidget; class UnlockDatabaseDialog; class QFileSystemWatcher; @@ -87,8 +88,10 @@ public: bool isGroupSelected() const; bool isInEditMode() const; bool isEditWidgetModified() const; - QList splitterSizes() const; - void setSplitterSizes(const QList& sizes); + QList mainSplitterSizes() const; + void setMainSplitterSizes(const QList& sizes); + QList detailSplitterSizes() const; + void setDetailSplitterSizes(const QList& sizes); QList entryHeaderViewSizes() const; void setEntryViewHeaderSizes(const QList& sizes); void clearAllWidgets(); @@ -115,12 +118,15 @@ signals: void databaseMerged(Database* mergedDb); void groupContextMenuRequested(const QPoint& globalPos); void entryContextMenuRequested(const QPoint& globalPos); + void pressedEntry(Entry* selectedEntry); + void pressedGroup(Group* selectedGroup); void unlockedDatabase(); void listModeAboutToActivate(); void listModeActivated(); void searchModeAboutToActivate(); void searchModeActivated(); - void splitterSizesChanged(); + void mainSplitterSizesChanged(); + void detailSplitterSizesChanged(); void entryColumnSizesChanged(); void updateSearch(QString text); @@ -166,7 +172,8 @@ public slots: void setSearchLimitGroup(bool state); void endSearch(); - void showMessage(const QString& text, MessageWidget::MessageType type); + void showMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true, + int autoHideTimeout = MessageWidget::DefaultAutoHideTimeout); void hideMessage(); private slots: @@ -178,6 +185,9 @@ private slots: void switchToGroupEdit(Group* entry, bool create); void emitGroupContextMenuRequested(const QPoint& pos); void emitEntryContextMenuRequested(const QPoint& pos); + void emitPressedEntry(); + void emitPressedEntry(Entry* currentEntry); + void emitPressedGroup(Group* currentGroup); void updateMasterKey(bool accepted); void openDatabase(bool accepted); void mergeDatabase(bool accepted); @@ -207,7 +217,8 @@ private: KeePass1OpenWidget* m_keepass1OpenWidget; UnlockDatabaseWidget* m_unlockDatabaseWidget; UnlockDatabaseDialog* m_unlockDatabaseDialog; - QSplitter* m_splitter; + QSplitter* m_mainSplitter; + QSplitter* m_detailSplitter; GroupView* m_groupView; EntryView* m_entryView; QLabel* m_searchingLabel; @@ -218,12 +229,16 @@ private: Uuid m_groupBeforeLock; Uuid m_entryBeforeLock; MessageWidget* m_messageWidget; + DetailsWidget* m_detailsView; // Search state QString m_lastSearchText; bool m_searchCaseSensitive; bool m_searchLimitGroup; + // CSV import state + bool m_importingCsv; + // Autoreload QFileSystemWatcher m_fileWatcher; QTimer m_fileWatchTimer; diff --git a/src/gui/DatabaseWidgetStateSync.cpp b/src/gui/DatabaseWidgetStateSync.cpp index 1510d844..57a3dcf8 100644 --- a/src/gui/DatabaseWidgetStateSync.cpp +++ b/src/gui/DatabaseWidgetStateSync.cpp @@ -25,14 +25,16 @@ DatabaseWidgetStateSync::DatabaseWidgetStateSync(QObject* parent) , m_activeDbWidget(nullptr) , m_blockUpdates(false) { - m_splitterSizes = variantToIntList(config()->get("GUI/SplitterState")); + m_mainSplitterSizes = variantToIntList(config()->get("GUI/SplitterState")); + m_detailSplitterSizes = variantToIntList(config()->get("GUI/DetailSplitterState")); m_columnSizesList = variantToIntList(config()->get("GUI/EntryListColumnSizes")); m_columnSizesSearch = variantToIntList(config()->get("GUI/EntrySearchColumnSizes")); } DatabaseWidgetStateSync::~DatabaseWidgetStateSync() { - config()->set("GUI/SplitterState", intListToVariant(m_splitterSizes)); + config()->set("GUI/SplitterState", intListToVariant(m_mainSplitterSizes)); + config()->set("GUI/DetailSplitterState", intListToVariant(m_detailSplitterSizes)); config()->set("GUI/EntryListColumnSizes", intListToVariant(m_columnSizesList)); config()->set("GUI/EntrySearchColumnSizes", intListToVariant(m_columnSizesSearch)); } @@ -48,17 +50,25 @@ void DatabaseWidgetStateSync::setActive(DatabaseWidget* dbWidget) if (m_activeDbWidget) { m_blockUpdates = true; - if (!m_splitterSizes.isEmpty()) - m_activeDbWidget->setSplitterSizes(m_splitterSizes); + if (!m_mainSplitterSizes.isEmpty()) { + m_activeDbWidget->setMainSplitterSizes(m_mainSplitterSizes); + } - if (m_activeDbWidget->isInSearchMode()) + if (!m_detailSplitterSizes.isEmpty()) { + m_activeDbWidget->setDetailSplitterSizes(m_detailSplitterSizes); + } + + if (m_activeDbWidget->isInSearchMode()) { restoreSearchView(); - else + } else { restoreListView(); + } m_blockUpdates = false; - connect(m_activeDbWidget, SIGNAL(splitterSizesChanged()), + connect(m_activeDbWidget, SIGNAL(mainSplitterSizesChanged()), + SLOT(updateSplitterSizes())); + connect(m_activeDbWidget, SIGNAL(detailSplitterSizesChanged()), SLOT(updateSplitterSizes())); connect(m_activeDbWidget, SIGNAL(entryColumnSizesChanged()), SLOT(updateColumnSizes())); @@ -102,7 +112,8 @@ void DatabaseWidgetStateSync::updateSplitterSizes() return; } - m_splitterSizes = m_activeDbWidget->splitterSizes(); + m_mainSplitterSizes = m_activeDbWidget->mainSplitterSizes(); + m_detailSplitterSizes = m_activeDbWidget->detailSplitterSizes(); } void DatabaseWidgetStateSync::updateColumnSizes() diff --git a/src/gui/DatabaseWidgetStateSync.h b/src/gui/DatabaseWidgetStateSync.h index 96ecd104..79a8ded3 100644 --- a/src/gui/DatabaseWidgetStateSync.h +++ b/src/gui/DatabaseWidgetStateSync.h @@ -46,7 +46,8 @@ private: DatabaseWidget* m_activeDbWidget; bool m_blockUpdates; - QList m_splitterSizes; + QList m_mainSplitterSizes; + QList m_detailSplitterSizes; QList m_columnSizesList; QList m_columnSizesSearch; }; diff --git a/src/gui/DetailsWidget.cpp b/src/gui/DetailsWidget.cpp new file mode 100644 index 00000000..23c3485a --- /dev/null +++ b/src/gui/DetailsWidget.cpp @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2012 Felix Geyer + * 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 . + */ + +#include "DetailsWidget.h" +#include "ui_DetailsWidget.h" + +#include +#include + +#include "core/Config.h" +#include "core/FilePath.h" +#include "core/TimeInfo.h" +#include "gui/Clipboard.h" +#include "gui/DatabaseWidget.h" + +DetailsWidget::DetailsWidget(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::DetailsWidget()) + , m_locked(false) + , m_currentEntry(nullptr) + , m_currentGroup(nullptr) + , m_timer(nullptr) + , m_attributesWidget(nullptr) + , m_autotypeWidget(nullptr) + , m_selectedTabEntry(0) + , m_selectedTabGroup(0) +{ + m_ui->setupUi(this); + + connect(parent, SIGNAL(pressedEntry(Entry*)), SLOT(getSelectedEntry(Entry*))); + connect(parent, SIGNAL(pressedGroup(Group*)), SLOT(getSelectedGroup(Group*))); + connect(parent, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), SLOT(setDatabaseMode(DatabaseWidget::Mode))); + + m_ui->totpButton->setIcon(filePath()->icon("actions", "chronometer")); + m_ui->closeButton->setIcon(filePath()->icon("actions", "dialog-close")); + + connect(m_ui->totpButton, SIGNAL(toggled(bool)), SLOT(showTotp(bool))); + connect(m_ui->closeButton, SIGNAL(toggled(bool)), SLOT(hideDetails())); + connect(m_ui->tabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndex(int))); + + this->hide(); +} + +DetailsWidget::~DetailsWidget() +{ +} + +void DetailsWidget::getSelectedEntry(Entry* selectedEntry) +{ + if (!selectedEntry) { + hideDetails(); + return; + } + + m_currentEntry = selectedEntry; + + if (!config()->get("GUI/HideDetailsView").toBool()) { + this->show(); + } + + m_ui->stackedWidget->setCurrentIndex(EntryPreview); + + if (m_ui->tabWidget->count() < 4) { + m_ui->tabWidget->insertTab(static_cast(AttributesTab), m_attributesWidget, "Attributes"); + m_ui->tabWidget->insertTab(static_cast(AutotypeTab), m_autotypeWidget, "Autotype"); + } + + m_ui->tabWidget->setTabEnabled(AttributesTab, false); + m_ui->tabWidget->setTabEnabled(NotesTab, false); + m_ui->tabWidget->setTabEnabled(AutotypeTab, false); + + m_ui->totpButton->hide(); + m_ui->totpWidget->hide(); + m_ui->totpButton->setChecked(false); + + auto icon = m_currentEntry->iconPixmap(); + if (icon.width() > 16 || icon.height() > 16) { + icon = icon.scaled(16, 16); + } + m_ui->entryIcon->setPixmap(icon); + + QString title = QString(" / "); + Group* entry_group = m_currentEntry->group(); + if (entry_group) { + QStringList hierarchy = entry_group->hierarchy(); + hierarchy.removeFirst(); + title += hierarchy.join(" / "); + if (hierarchy.size() > 0) { + title += " / "; + } + } + title.append(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title())); + m_ui->titleLabel->setText(title); + + m_ui->usernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username())); + + if (!config()->get("security/hidepassworddetails").toBool()) { + m_ui->passwordLabel->setText( + shortPassword(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password()))); + m_ui->passwordLabel->setToolTip(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password())); + } else { + m_ui->passwordLabel->setText("****"); + } + + QString url = m_currentEntry->webUrl(); + if (!url.isEmpty()) { + // URL is well formed and can be opened in a browser + // create a new display url that masks password placeholders + // the actual link will use the password + url = QString("%2").arg(url).arg(shortUrl(m_currentEntry->displayUrl())); + m_ui->urlLabel->setOpenExternalLinks(true); + } else { + // Fallback to the raw url string + url = shortUrl(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->url())); + m_ui->urlLabel->setOpenExternalLinks(false); + } + m_ui->urlLabel->setText(url); + + TimeInfo entryTime = m_currentEntry->timeInfo(); + if (entryTime.expires()) { + m_ui->expirationLabel->setText(entryTime.expiryTime().toString(Qt::DefaultLocaleShortDate)); + } else { + m_ui->expirationLabel->setText(tr("Never")); + } + + if (m_currentEntry->hasTotp()) { + m_step = m_currentEntry->totpStep(); + + if (nullptr != m_timer) { + m_timer->stop(); + } + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), this, SLOT(updateTotp())); + updateTotp(); + m_timer->start(m_step * 10); + m_ui->totpButton->show(); + } + + QString notes = m_currentEntry->notes(); + if (!notes.isEmpty()) { + m_ui->tabWidget->setTabEnabled(NotesTab, true); + m_ui->notesEdit->setText(m_currentEntry->resolveMultiplePlaceholders(notes)); + } + + QStringList customAttributes = m_currentEntry->attributes()->customKeys(); + if (customAttributes.size() > 0) { + m_ui->tabWidget->setTabEnabled(AttributesTab, true); + m_ui->attributesEdit->clear(); + + QString attributesText = QString(); + for (const QString& key : customAttributes) { + QString value = m_currentEntry->attributes()->value(key); + if (m_currentEntry->attributes()->isProtected(key)) { + value = "" + tr("[PROTECTED]") + ""; + } + attributesText.append(QString("%1: %2
").arg(key, value)); + } + m_ui->attributesEdit->setText(attributesText); + } + + m_ui->autotypeTree->clear(); + AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations(); + QList items; + for (auto assoc : autotypeAssociations->getAll()) { + QStringList association = QStringList() << assoc.window << assoc.sequence; + if (association.at(1).isEmpty()) { + association.replace(1, m_currentEntry->effectiveAutoTypeSequence()); + } + items.append(new QTreeWidgetItem(m_ui->autotypeTree, association)); + } + if (items.count() > 0) { + m_ui->autotypeTree->addTopLevelItems(items); + m_ui->tabWidget->setTabEnabled(AutotypeTab, true); + } + + if (m_ui->tabWidget->isTabEnabled(m_selectedTabEntry)) { + m_ui->tabWidget->setCurrentIndex(m_selectedTabEntry); + } +} + +void DetailsWidget::getSelectedGroup(Group* selectedGroup) +{ + if (!selectedGroup) { + hideDetails(); + return; + } + + m_currentGroup = selectedGroup; + + if (!config()->get("GUI/HideDetailsView").toBool()) { + this->show(); + } + + m_ui->stackedWidget->setCurrentIndex(GroupPreview); + + if (m_ui->tabWidget->count() > 2) { + m_autotypeWidget = m_ui->tabWidget->widget(AutotypeTab); + m_attributesWidget = m_ui->tabWidget->widget(AttributesTab); + m_ui->tabWidget->removeTab(AutotypeTab); + m_ui->tabWidget->removeTab(AttributesTab); + } + + m_ui->tabWidget->setTabEnabled(GroupNotesTab, false); + + m_ui->totpButton->hide(); + m_ui->totpWidget->hide(); + + auto icon = m_currentGroup->iconPixmap(); + if (icon.width() > 32 || icon.height() > 32) { + icon = icon.scaled(32, 32); + } + m_ui->entryIcon->setPixmap(icon); + + QString title = " / "; + QStringList hierarchy = m_currentGroup->hierarchy(); + hierarchy.removeFirst(); + title += hierarchy.join(" / "); + if (hierarchy.size() > 0) { + title += " / "; + } + m_ui->titleLabel->setText(title); + + QString notes = m_currentGroup->notes(); + if (!notes.isEmpty()) { + m_ui->tabWidget->setTabEnabled(GroupNotesTab, true); + m_ui->notesEdit->setText(notes); + } + + QString searching = tr("Disabled"); + if (m_currentGroup->resolveSearchingEnabled()) { + searching = tr("Enabled"); + } + m_ui->searchingLabel->setText(searching); + + QString autotype = tr("Disabled"); + if (m_currentGroup->resolveAutoTypeEnabled()) { + autotype = tr("Enabled"); + } + m_ui->autotypeLabel->setText(autotype); + + TimeInfo groupTime = m_currentGroup->timeInfo(); + if (groupTime.expires()) { + m_ui->groupExpirationLabel->setText(groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate)); + } else { + m_ui->groupExpirationLabel->setText(tr("Never")); + } + + if (m_ui->tabWidget->isTabEnabled(m_selectedTabGroup)) { + m_ui->tabWidget->setCurrentIndex(m_selectedTabGroup); + } +} + +void DetailsWidget::updateTotp() +{ + if (!m_locked) { + QString totpCode = m_currentEntry->totp(); + QString firstHalf = totpCode.left(totpCode.size() / 2); + QString secondHalf = totpCode.mid(totpCode.size() / 2); + m_ui->totpLabel->setText(firstHalf + " " + secondHalf); + } else if (nullptr != m_timer) { + m_timer->stop(); + } +} + +void DetailsWidget::showTotp(bool visible) +{ + if (visible) { + m_ui->totpWidget->show(); + } else { + m_ui->totpWidget->hide(); + } +} + +QString DetailsWidget::shortUrl(QString url) +{ + QString newurl = ""; + if (url.length() > 60) { + newurl.append(url.left(20)); + newurl.append("…"); + newurl.append(url.right(20)); + return newurl; + } + return url; +} + +QString DetailsWidget::shortPassword(QString password) +{ + QString newpassword = ""; + if (password.length() > 60) { + newpassword.append(password.left(50)); + newpassword.append("…"); + return newpassword; + } + return password; +} + +void DetailsWidget::hideDetails() +{ + this->hide(); +} + +void DetailsWidget::setDatabaseMode(DatabaseWidget::Mode mode) +{ + m_locked = false; + if (mode == DatabaseWidget::LockedMode) { + m_locked = true; + return; + } + if (mode == DatabaseWidget::ViewMode) { + if (m_ui->stackedWidget->currentIndex() == GroupPreview) { + getSelectedGroup(m_currentGroup); + } else { + getSelectedEntry(m_currentEntry); + } + } +} + +void DetailsWidget::updateTabIndex(int index) +{ + if (m_ui->stackedWidget->currentIndex() == GroupPreview) { + m_selectedTabGroup = index; + } else { + m_selectedTabEntry = index; + } +} diff --git a/src/gui/DetailsWidget.h b/src/gui/DetailsWidget.h new file mode 100644 index 00000000..780fe558 --- /dev/null +++ b/src/gui/DetailsWidget.h @@ -0,0 +1,75 @@ +/* + * 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 KEEPASSX_DETAILSWIDGET_H +#define KEEPASSX_DETAILSWIDGET_H + +#include "gui/DatabaseWidget.h" +#include + +namespace Ui { + class DetailsWidget; +} + +class DetailsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit DetailsWidget(QWidget* parent = nullptr); + ~DetailsWidget(); + + enum StackedWidgetIndex + { + EntryPreview = 0, + GroupPreview = 1, + }; + + enum TabWidgetIndex + { + GeneralTab = 0, + AttributesTab = 1, + GroupNotesTab = 1, + NotesTab = 2, + AutotypeTab = 3, + }; + +private slots: + void getSelectedEntry(Entry* selectedEntry); + void getSelectedGroup(Group* selectedGroup); + void showTotp(bool visible); + void updateTotp(); + void hideDetails(); + void setDatabaseMode(DatabaseWidget::Mode mode); + void updateTabIndex(int index); + +private: + const QScopedPointer m_ui; + bool m_locked; + Entry* m_currentEntry; + Group* m_currentGroup; + quint8 m_step; + QTimer* m_timer; + QWidget* m_attributesWidget; + QWidget* m_autotypeWidget; + quint8 m_selectedTabEntry; + quint8 m_selectedTabGroup; + QString shortUrl(QString url); + QString shortPassword(QString password); +}; + +#endif // KEEPASSX_DETAILSWIDGET_H diff --git a/src/gui/DetailsWidget.ui b/src/gui/DetailsWidget.ui new file mode 100644 index 00000000..8cece9a2 --- /dev/null +++ b/src/gui/DetailsWidget.ui @@ -0,0 +1,539 @@ + + + DetailsWidget + + + + 0 + 0 + 600 + 200 + + + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 12 + + + + Qt::AutoText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 10 + 75 + true + + + + + + + + + + + + + + Generate TOTP Token + + + + + + true + + + + + + + Close + + + + + + true + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + + + false + + + false + + + false + + + + General + + + + + + 0 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Password + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + + 9 + 75 + true + + + + URL + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Expiration + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Qt::LeftToRight + + + Username + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Autotype + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Searching + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Expiration + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + + + Attributes + + + + + + Qt::ClickFocus + + + true + + + + + + + + Notes + + + + + + Qt::ClickFocus + + + true + + + + + + + + Autotype + + + + + + QFrame::Sunken + + + true + + + true + + + false + + + 2 + + + false + + + 250 + + + 50 + + + true + + + + Window + + + + + Sequence + + + + + + + + + + + + Search + + + + + Clear + + + + + + diff --git a/src/gui/FileDialog.cpp b/src/gui/FileDialog.cpp index e293db2b..9f3caf6d 100644 --- a/src/gui/FileDialog.cpp +++ b/src/gui/FileDialog.cpp @@ -27,7 +27,7 @@ QString FileDialog::getOpenFileName(QWidget* parent, const QString& caption, QSt { if (!m_nextFileName.isEmpty()) { QString result = m_nextFileName; - m_nextFileName = ""; + m_nextFileName.clear(); return result; } else { @@ -43,11 +43,37 @@ QString FileDialog::getOpenFileName(QWidget* parent, const QString& caption, QSt parent->activateWindow(); } - if (!result.isEmpty()) { - config()->set("LastDir", QFileInfo(result).absolutePath()); + saveLastDir(result); + return result; + } +} + +QStringList FileDialog::getOpenFileNames(QWidget *parent, const QString &caption, QString dir, + const QString &filter, QString *selectedFilter, + QFileDialog::Options options) +{ + if (!m_nextFileNames.isEmpty()) { + QStringList results = m_nextFileNames; + m_nextFileNames.clear(); + return results; + } + else { + if (dir.isEmpty()) { + dir = config()->get("LastDir").toString(); } - return result; + QStringList results = QFileDialog::getOpenFileNames(parent, caption, dir, filter, + selectedFilter, options); + + // on Mac OS X the focus is lost after closing the native dialog + if (parent) { + parent->activateWindow(); + } + + if (!results.isEmpty()) { + saveLastDir(results[0]); + } + return results; } } @@ -57,7 +83,7 @@ QString FileDialog::getSaveFileName(QWidget* parent, const QString& caption, QSt { if (!m_nextFileName.isEmpty()) { QString result = m_nextFileName; - m_nextFileName = ""; + m_nextFileName.clear(); return result; } else { @@ -95,11 +121,33 @@ QString FileDialog::getSaveFileName(QWidget* parent, const QString& caption, QSt parent->activateWindow(); } - if (!result.isEmpty()) { - config()->set("LastDir", QFileInfo(result).absolutePath()); + saveLastDir(result); + return result; + } +} + +QString FileDialog::getExistingDirectory(QWidget *parent, const QString &caption, QString dir, + QFileDialog::Options options) +{ + if (!m_nextDirName.isEmpty()) { + QString result = m_nextDirName; + m_nextDirName.clear(); + return result; + } + else { + if (dir.isEmpty()) { + dir = config()->get("LastDir").toString(); } - return result; + dir = QFileDialog::getExistingDirectory(parent, caption, dir, options); + + // on Mac OS X the focus is lost after closing the native dialog + if (parent) { + parent->activateWindow(); + } + + saveLastDir(dir); + return dir; } } @@ -108,10 +156,33 @@ void FileDialog::setNextFileName(const QString& fileName) m_nextFileName = fileName; } +void FileDialog::setNextFileNames(const QStringList &fileNames) +{ + m_nextFileNames = fileNames; +} + +void FileDialog::setNextDirName(const QString &dirName) +{ + m_nextDirName = dirName; +} + +void FileDialog::setNextForgetDialog() +{ + m_forgetLastDir = true; +} + FileDialog::FileDialog() { } +void FileDialog::saveLastDir(QString dir) { + if (!dir.isEmpty() && !m_forgetLastDir) { + config()->set("LastDir", QFileInfo(dir).absolutePath()); + } + + m_forgetLastDir = false; +} + FileDialog* FileDialog::instance() { if (!m_instance) { diff --git a/src/gui/FileDialog.h b/src/gui/FileDialog.h index 9f8fbb54..9a57a921 100644 --- a/src/gui/FileDialog.h +++ b/src/gui/FileDialog.h @@ -26,22 +26,35 @@ public: QString getOpenFileName(QWidget* parent = nullptr, const QString& caption = QString(), QString dir = QString(), const QString& filter = QString(), QString* selectedFilter = nullptr, QFileDialog::Options options = 0); + QStringList getOpenFileNames(QWidget* parent = nullptr, const QString& caption = QString(), + QString dir = QString(), const QString& filter = QString(), + QString* selectedFilter = nullptr, QFileDialog::Options options = 0); QString getSaveFileName(QWidget* parent = nullptr, const QString& caption = QString(), QString dir = QString(), const QString& filter = QString(), QString* selectedFilter = nullptr, QFileDialog::Options options = 0, const QString& defaultExtension = QString()); + QString getExistingDirectory(QWidget* parent = nullptr, const QString& caption = QString(), + QString dir = QString(), QFileDialog::Options options = QFileDialog::ShowDirsOnly); + void setNextForgetDialog(); /** * Sets the result of the next get* method call. * Use only for testing. */ void setNextFileName(const QString& fileName); + void setNextFileNames(const QStringList& fileNames); + void setNextDirName(const QString& dirName); static FileDialog* instance(); private: FileDialog(); QString m_nextFileName; + QStringList m_nextFileNames; + QString m_nextDirName; + bool m_forgetLastDir = false; + + void saveLastDir(QString); static FileDialog* m_instance; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 14531a4f..fa16adb8 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -49,6 +49,11 @@ #include "http/OptionDialog.h" #endif +#ifdef WITH_XC_SSHAGENT +#include "sshagent/AgentSettingsPage.h" +#include "sshagent/SSHAgent.h" +#endif + #include "gui/SettingsWidget.h" #include "gui/PasswordGeneratorWidget.h" @@ -109,6 +114,8 @@ MainWindow::MainWindow() { m_ui->setupUi(this); + m_ui->toolBar->setContextMenuPolicy(Qt::PreventContextMenu); + // Setup the search widget in the toolbar SearchWidget *search = new SearchWidget(); search->connectSignals(m_actionMultiplexer); @@ -121,15 +128,13 @@ MainWindow::MainWindow() #ifdef WITH_XC_HTTP m_ui->settingsWidget->addSettingsPage(new HttpPlugin(m_ui->tabWidget)); #endif + #ifdef WITH_XC_SSHAGENT + SSHAgent::init(this); + m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); + #endif setWindowIcon(filePath()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); - QAction* toggleViewAction = m_ui->toolBar->toggleViewAction(); - toggleViewAction->setText(tr("Show toolbar")); - m_ui->menuView->addAction(toggleViewAction); - bool showToolbar = config()->get("ShowToolbar").toBool(); - m_ui->toolBar->setVisible(showToolbar); - connect(m_ui->toolBar, SIGNAL(visibilityChanged(bool)), this, SLOT(saveToolbarState(bool))); m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); @@ -460,7 +465,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); m_ui->actionChangeMasterKey->setEnabled(true); m_ui->actionChangeDatabaseSettings->setEnabled(true); - m_ui->actionDatabaseSave->setEnabled(true); + m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); m_ui->actionDatabaseSaveAs->setEnabled(true); m_ui->actionExportCsv->setEnabled(true); m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); @@ -566,6 +571,7 @@ void MainWindow::updateWindowTitle() if (m_ui->tabWidget->readOnly(tabWidgetIndex)) { customWindowTitlePart.append(QString(" [%1]").arg(tr("read-only"))); } + m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave(tabWidgetIndex)); } else if (stackedWidgetIndex == 1) { customWindowTitlePart = tr("Settings"); } @@ -732,7 +738,9 @@ void MainWindow::changeEvent(QEvent* event) void MainWindow::saveWindowInformation() { - config()->set("GUI/MainWindowGeometry", saveGeometry()); + if (isVisible()) { + config()->set("GUI/MainWindowGeometry", saveGeometry()); + } } bool MainWindow::saveLastDatabases() @@ -816,11 +824,6 @@ void MainWindow::showGroupContextMenu(const QPoint& globalPos) m_ui->menuGroups->popup(globalPos); } -void MainWindow::saveToolbarState(bool value) -{ - config()->set("ShowToolbar", value); -} - void MainWindow::setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback) { if (!QKeySequence::keyBindings(standard).isEmpty()) { @@ -863,6 +866,7 @@ void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) void MainWindow::hideWindow() { + saveWindowInformation(); #ifndef Q_OS_MAC setWindowState(windowState() | Qt::WindowMinimized); #endif @@ -884,7 +888,7 @@ void MainWindow::toggleWindow() raise(); activateWindow(); -#if defined(Q_OS_LINUX) && ! defined(QT_NO_DBUS) +#if defined(Q_OS_LINUX) && ! defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) // re-register global D-Bus menu (needed on Ubuntu with Unity) // see https://github.com/keepassxreboot/keepassxc/issues/271 // and https://bugreports.qt.io/browse/QTBUG-58723 @@ -952,16 +956,17 @@ bool MainWindow::isTrayIconEnabled() const && QSystemTrayIcon::isSystemTrayAvailable(); } -void MainWindow::displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton) +void MainWindow::displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton, + int autoHideTimeout) { m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); - m_ui->globalMessageWidget->showMessage(text, type); + m_ui->globalMessageWidget->showMessage(text, type, autoHideTimeout); } -void MainWindow::displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton) +void MainWindow::displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton, + int autoHideTimeout) { - m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); - m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type); + m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type, showClosebutton, autoHideTimeout); } void MainWindow::hideGlobalMessage() @@ -978,7 +983,8 @@ void MainWindow::hideTabMessage() void MainWindow::showYubiKeyPopup() { - displayGlobalMessage(tr("Please touch the button on your YubiKey!"), MessageWidget::Information, false); + displayGlobalMessage(tr("Please touch the button on your YubiKey!"), MessageWidget::Information, + false, MessageWidget::DisableAutoHide); setEnabled(false); } diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index caf3f585..ade339c5 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -54,8 +54,10 @@ public slots: void openDatabase(const QString& fileName, const QString& pw = QString(), const QString& keyFile = QString()); void appExit(); - void displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true); - void displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true); + void displayGlobalMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true, + int autoHideTimeout = MessageWidget::DefaultAutoHideTimeout); + void displayTabMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true, + int autoHideTimeout = MessageWidget::DefaultAutoHideTimeout); void hideGlobalMessage(); void showYubiKeyPopup(); void hideYubiKeyPopup(); @@ -85,7 +87,6 @@ private slots: void updateCopyAttributesMenu(); void showEntryContextMenu(const QPoint& globalPos); void showGroupContextMenu(const QPoint& globalPos); - void saveToolbarState(bool value); void rememberOpenDatabases(const QString& filePath); void applySettingsChanges(); void trayIconTriggered(QSystemTrayIcon::ActivationReason reason); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 258dd90c..d0cded22 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -260,15 +260,9 @@ - - - &View - - - @@ -563,7 +557,7 @@ - Empty recycle bin + E&mpty recycle bin false diff --git a/src/gui/MessageWidget.cpp b/src/gui/MessageWidget.cpp index de981b92..2be29805 100644 --- a/src/gui/MessageWidget.cpp +++ b/src/gui/MessageWidget.cpp @@ -18,20 +18,53 @@ #include "MessageWidget.h" -MessageWidget::MessageWidget(QWidget* parent) - :KMessageWidget(parent) -{ +#include "QTimer" +const int MessageWidget::DefaultAutoHideTimeout = 6000; +const int MessageWidget::DisableAutoHide = -1; + +MessageWidget::MessageWidget(QWidget* parent) + : KMessageWidget(parent) + , m_autoHideTimer(new QTimer(this)) + , m_autoHideTimeout(DefaultAutoHideTimeout) +{ + m_autoHideTimer->setSingleShot(true); + connect(m_autoHideTimer, SIGNAL(timeout()), this, SLOT(animatedHide())); + connect(this, SIGNAL(hideAnimationFinished()), m_autoHideTimer, SLOT(stop())); +} + +int MessageWidget::autoHideTimeout() const +{ + return m_autoHideTimeout; } void MessageWidget::showMessage(const QString& text, MessageWidget::MessageType type) +{ + showMessage(text, type, m_autoHideTimeout); +} + +void MessageWidget::showMessage(const QString &text, KMessageWidget::MessageType type, int autoHideTimeout) { setMessageType(type); setText(text); animatedShow(); + if (autoHideTimeout > 0) { + m_autoHideTimer->start(autoHideTimeout); + } else { + m_autoHideTimer->stop(); + } } void MessageWidget::hideMessage() { animatedHide(); + m_autoHideTimer->stop(); +} + +void MessageWidget::setAutoHideTimeout(int autoHideTimeout) +{ + m_autoHideTimeout = autoHideTimeout; + if (autoHideTimeout <= 0) { + m_autoHideTimer->stop(); + } } diff --git a/src/gui/MessageWidget.h b/src/gui/MessageWidget.h index 03ebee3e..c29c320b 100644 --- a/src/gui/MessageWidget.h +++ b/src/gui/MessageWidget.h @@ -21,6 +21,8 @@ #include "gui/KMessageWidget.h" +class QTimer; + class MessageWidget : public KMessageWidget { Q_OBJECT @@ -28,10 +30,20 @@ class MessageWidget : public KMessageWidget public: explicit MessageWidget(QWidget* parent = 0); + int autoHideTimeout() const; + + static const int DefaultAutoHideTimeout; + static const int DisableAutoHide; + public slots: void showMessage(const QString& text, MessageWidget::MessageType type); + void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout); void hideMessage(); + void setAutoHideTimeout(int autoHideTimeout); +private: + QTimer* m_autoHideTimer; + int m_autoHideTimeout; }; #endif // MESSAGEWIDGET_H diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp index 54b0ca28..ad736bf2 100644 --- a/src/gui/PasswordEdit.cpp +++ b/src/gui/PasswordEdit.cpp @@ -31,9 +31,18 @@ PasswordEdit::PasswordEdit(QWidget* parent) { setEchoMode(QLineEdit::Password); updateStylesheet(); - - // set font to system monospace font and increase letter spacing + + // use a monospace font for the password field QFont passwordFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); +#ifdef Q_OS_WIN + // try to use Consolas on Windows, because the default Courier New has too many similar characters + QFont consolasFont = QFontDatabase().font("Consolas", passwordFont.styleName(), passwordFont.pointSize()); + const QFont defaultFont; + if (passwordFont != defaultFont) { + passwordFont = consolasFont; + } +#endif + passwordFont.setLetterSpacing(QFont::PercentageSpacing, 110); setFont(passwordFont); } diff --git a/src/gui/SettingsWidget.cpp b/src/gui/SettingsWidget.cpp index 8385bf26..4ba81c6f 100644 --- a/src/gui/SettingsWidget.cpp +++ b/src/gui/SettingsWidget.cpp @@ -134,6 +134,7 @@ void SettingsWidget::loadSettings() m_generalUi->languageComboBox->setCurrentIndex(defaultIndex); } + m_generalUi->detailsHideCheckBox->setChecked(config()->get("GUI/HideDetailsView").toBool()); m_generalUi->systrayShowCheckBox->setChecked(config()->get("GUI/ShowTrayIcon").toBool()); m_generalUi->systrayMinimizeToTrayCheckBox->setChecked(config()->get("GUI/MinimizeToTray").toBool()); m_generalUi->systrayMinimizeOnCloseCheckBox->setChecked(config()->get("GUI/MinimizeOnClose").toBool()); @@ -160,7 +161,9 @@ void SettingsWidget::loadSettings() m_secUi->fallbackToGoogle->setChecked(config()->get("security/IconDownloadFallbackToGoogle").toBool()); m_secUi->passwordCleartextCheckBox->setChecked(config()->get("security/passwordscleartext").toBool()); + m_secUi->passwordDetailsCleartextCheckBox->setChecked(config()->get("security/hidepassworddetails").toBool()); m_secUi->passwordRepeatCheckBox->setChecked(config()->get("security/passwordsrepeat").toBool()); + m_secUi->hideNotesCheckBox->setChecked(config()->get("security/hidenotes").toBool()); for (const ExtraPage& page: asConst(m_extraPages)) { @@ -203,6 +206,7 @@ void SettingsWidget::saveSettings() config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString()); + config()->set("GUI/HideDetailsView", m_generalUi->detailsHideCheckBox->isChecked()); config()->set("GUI/ShowTrayIcon", m_generalUi->systrayShowCheckBox->isChecked()); config()->set("GUI/MinimizeToTray", m_generalUi->systrayMinimizeToTrayCheckBox->isChecked()); config()->set("GUI/MinimizeOnClose", m_generalUi->systrayMinimizeOnCloseCheckBox->isChecked()); @@ -226,7 +230,9 @@ void SettingsWidget::saveSettings() config()->set("security/IconDownloadFallbackToGoogle", m_secUi->fallbackToGoogle->isChecked()); config()->set("security/passwordscleartext", m_secUi->passwordCleartextCheckBox->isChecked()); + config()->set("security/hidepassworddetails", m_secUi->passwordDetailsCleartextCheckBox->isChecked()); config()->set("security/passwordsrepeat", m_secUi->passwordRepeatCheckBox->isChecked()); + config()->set("security/hidenotes", m_secUi->hideNotesCheckBox->isChecked()); // Security: clear storage if related settings are disabled if (!config()->get("RememberLastDatabases").toBool()) { @@ -235,6 +241,7 @@ void SettingsWidget::saveSettings() if (!config()->get("RememberLastKeyFiles").toBool()) { config()->set("LastKeyFiles", QVariant()); + config()->set("LastDir", ""); } for (const ExtraPage& page: asConst(m_extraPages)) { diff --git a/src/gui/SettingsWidgetGeneral.ui b/src/gui/SettingsWidgetGeneral.ui index 1e62104c..7dc4487c 100644 --- a/src/gui/SettingsWidgetGeneral.ui +++ b/src/gui/SettingsWidgetGeneral.ui @@ -153,6 +153,13 @@ + + + + Hide the Details view + + + diff --git a/src/gui/SettingsWidgetSecurity.ui b/src/gui/SettingsWidgetSecurity.ui index 4233fdbd..17d324f7 100644 --- a/src/gui/SettingsWidgetSecurity.ui +++ b/src/gui/SettingsWidgetSecurity.ui @@ -136,6 +136,20 @@ + + + + Hide passwords in the preview panel + + + + + + + Hide entry notes by default + + + diff --git a/src/gui/SetupTotpDialog.cpp b/src/gui/SetupTotpDialog.cpp index 5521773b..52d63f0e 100644 --- a/src/gui/SetupTotpDialog.cpp +++ b/src/gui/SetupTotpDialog.cpp @@ -35,7 +35,9 @@ SetupTotpDialog::SetupTotpDialog(DatabaseWidget* parent, Entry* entry) connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(setupTotp())); - connect(m_ui->customSettingsCheckBox, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool))); + connect(m_ui->radioDefault, SIGNAL(toggled(bool)), SLOT(toggleDefault(bool))); + connect(m_ui->radioSteam, SIGNAL(toggled(bool)), SLOT(toggleSteam(bool))); + connect(m_ui->radioCustom, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool))); } @@ -43,19 +45,37 @@ void SetupTotpDialog::setupTotp() { quint8 digits; - if (m_ui->radio8Digits->isChecked()) { + if (m_ui->radioSteam->isChecked()) { + digits = Totp::ENCODER_STEAM; + } else if (m_ui->radio8Digits->isChecked()) { digits = 8; } else { digits = 6; } quint8 step = m_ui->stepSpinBox->value(); - QString seed = QTotp::parseOtpString(m_ui->seedEdit->text(), digits, step); + QString seed = Totp::parseOtpString(m_ui->seedEdit->text(), digits, step); m_entry->setTotp(seed, step, digits); emit m_parent->entrySelectionChanged(); close(); } +void SetupTotpDialog::toggleDefault(bool status) +{ + if (status) { + setStep(Totp::defaultStep); + setDigits(Totp::defaultDigits); + } +} + +void SetupTotpDialog::toggleSteam(bool status) +{ + if (status) { + setStep(Totp::defaultStep); + setDigits(Totp::ENCODER_STEAM); + } +} + void SetupTotpDialog::toggleCustom(bool status) { m_ui->digitsLabel->setEnabled(status); @@ -72,13 +92,25 @@ void SetupTotpDialog::setSeed(QString value) m_ui->seedEdit->setText(value); } +void SetupTotpDialog::setSettings(quint8 digits) { + quint8 step = m_ui->stepSpinBox->value(); + + bool isDefault = ((step == Totp::defaultStep) && + (digits == Totp::defaultDigits)); + bool isSteam = (digits == Totp::ENCODER_STEAM); + + if (isSteam) { + m_ui->radioSteam->setChecked(true); + } else if (isDefault) { + m_ui->radioDefault->setChecked(true); + } else { + m_ui->radioCustom->setChecked(true); + } +} + void SetupTotpDialog::setStep(quint8 step) { m_ui->stepSpinBox->setValue(step); - - if (step != QTotp::defaultStep) { - m_ui->customSettingsCheckBox->setChecked(true); - } } void SetupTotpDialog::setDigits(quint8 digits) @@ -90,13 +122,8 @@ void SetupTotpDialog::setDigits(quint8 digits) m_ui->radio6Digits->setChecked(true); m_ui->radio8Digits->setChecked(false); } - - if (digits != QTotp::defaultDigits) { - m_ui->customSettingsCheckBox->setChecked(true); - } } - SetupTotpDialog::~SetupTotpDialog() { } diff --git a/src/gui/SetupTotpDialog.h b/src/gui/SetupTotpDialog.h index 243a05f9..9e90e968 100644 --- a/src/gui/SetupTotpDialog.h +++ b/src/gui/SetupTotpDialog.h @@ -39,8 +39,11 @@ public: void setSeed(QString value); void setStep(quint8 step); void setDigits(quint8 digits); + void setSettings(quint8 digits); private Q_SLOTS: + void toggleDefault(bool status); + void toggleSteam(bool status); void toggleCustom(bool status); void setupTotp(); diff --git a/src/gui/SetupTotpDialog.ui b/src/gui/SetupTotpDialog.ui index a6d80628..c3a83e21 100644 --- a/src/gui/SetupTotpDialog.ui +++ b/src/gui/SetupTotpDialog.ui @@ -7,7 +7,7 @@ 0 0 282 - 257 + 364 @@ -29,11 +29,38 @@ - - - Use custom settings - - + + + + + Default RFC 6238 token settings + + + settingsButtonGroup + + + + + + + Steam token settings + + + settingsButtonGroup + + + + + + + Use custom settings + + + settingsButtonGroup + + + + @@ -134,4 +161,7 @@ + + + diff --git a/src/gui/TotpDialog.cpp b/src/gui/TotpDialog.cpp index 17cc1120..474acf77 100644 --- a/src/gui/TotpDialog.cpp +++ b/src/gui/TotpDialog.cpp @@ -87,8 +87,8 @@ void TotpDialog::updateSeconds() void TotpDialog::updateTotp() { QString totpCode = m_entry->totp(); - QString firstHalf = totpCode.left(totpCode.size()/2); - QString secondHalf = totpCode.right(totpCode.size()/2); + QString firstHalf = totpCode.left(totpCode.size() / 2); + QString secondHalf = totpCode.mid(totpCode.size() / 2); m_ui->totpLabel->setText(firstHalf + " " + secondHalf); } diff --git a/src/gui/UnlockDatabaseDialog.h b/src/gui/UnlockDatabaseDialog.h index 9a42ed6e..ef2eabb3 100644 --- a/src/gui/UnlockDatabaseDialog.h +++ b/src/gui/UnlockDatabaseDialog.h @@ -31,7 +31,7 @@ class UnlockDatabaseDialog : public QDialog { Q_OBJECT public: - explicit UnlockDatabaseDialog(QWidget* parent = Q_NULLPTR); + explicit UnlockDatabaseDialog(QWidget* parent = nullptr); void setDBFilename(const QString& filename); void clearForms(); Database* database(); diff --git a/src/gui/csvImport/CsvImportWizard.cpp b/src/gui/csvImport/CsvImportWizard.cpp index eb4b2123..e9a8f498 100644 --- a/src/gui/csvImport/CsvImportWizard.cpp +++ b/src/gui/csvImport/CsvImportWizard.cpp @@ -28,19 +28,9 @@ CsvImportWizard::CsvImportWizard(QWidget *parent) : DialogyWidget(parent) { m_layout = new QGridLayout(this); - m_pages = new QStackedWidget(parent); - m_layout->addWidget(m_pages, 0, 0); + m_layout->addWidget(m_parse = new CsvImportWidget(this), 0, 0); - m_pages->addWidget(key = new ChangeMasterKeyWidget(m_pages)); - m_pages->addWidget(parse = new CsvImportWidget(m_pages)); - key->headlineLabel()->setText(tr("Import CSV file")); - QFont headLineFont = key->headlineLabel()->font(); - headLineFont.setBold(true); - headLineFont.setPointSize(headLineFont.pointSize() + 2); - key->headlineLabel()->setFont(headLineFont); - - connect(key, SIGNAL(editFinished(bool)), this, SLOT(keyFinished(bool))); - connect(parse, SIGNAL(editFinished(bool)), this, SLOT(parseFinished(bool))); + connect(m_parse, SIGNAL(editFinished(bool)), this, SLOT(parseFinished(bool))); } CsvImportWizard::~CsvImportWizard() @@ -49,21 +39,18 @@ CsvImportWizard::~CsvImportWizard() void CsvImportWizard::load(const QString& filename, Database* database) { m_db = database; - parse->load(filename, database); - key->clearForms(); + m_parse->load(filename, database); } -void CsvImportWizard::keyFinished(bool accepted) +void CsvImportWizard::keyFinished(bool accepted, CompositeKey key) { if (!accepted) { emit importFinished(false); return; } - m_pages->setCurrentIndex(m_pages->currentIndex()+1); - QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - bool result = m_db->setKey(key->newMasterKey()); + bool result = m_db->setKey(key); QApplication::restoreOverrideCursor(); if (!result) { diff --git a/src/gui/csvImport/CsvImportWizard.h b/src/gui/csvImport/CsvImportWizard.h index 317018d9..b6414c0c 100644 --- a/src/gui/csvImport/CsvImportWizard.h +++ b/src/gui/csvImport/CsvImportWizard.h @@ -38,19 +38,17 @@ public: explicit CsvImportWizard(QWidget *parent = nullptr); ~CsvImportWizard(); void load(const QString& filename, Database *database); + void keyFinished(bool accepted, CompositeKey key); signals: void importFinished(bool accepted); private slots: - void keyFinished(bool accepted); void parseFinished(bool accepted); private: Database* m_db; - CsvImportWidget* parse; - ChangeMasterKeyWidget* key; - QStackedWidget *m_pages; + CsvImportWidget* m_parse; QGridLayout *m_layout; }; diff --git a/src/gui/entry/AutoTypeAssociationsModel.cpp b/src/gui/entry/AutoTypeAssociationsModel.cpp index 4a76233b..442453de 100644 --- a/src/gui/entry/AutoTypeAssociationsModel.cpp +++ b/src/gui/entry/AutoTypeAssociationsModel.cpp @@ -17,9 +17,12 @@ #include "AutoTypeAssociationsModel.h" +#include "core/Entry.h" + AutoTypeAssociationsModel::AutoTypeAssociationsModel(QObject* parent) : QAbstractListModel(parent) , m_autoTypeAssociations(nullptr) + , m_entry(nullptr) { } @@ -46,6 +49,11 @@ void AutoTypeAssociationsModel::setAutoTypeAssociations(AutoTypeAssociations* au endResetModel(); } +void AutoTypeAssociationsModel::setEntry(const Entry *entry) +{ + m_entry = entry; +} + int AutoTypeAssociationsModel::rowCount(const QModelIndex& parent) const { if (!m_autoTypeAssociations || parent.isValid()) { @@ -86,7 +94,12 @@ QVariant AutoTypeAssociationsModel::data(const QModelIndex& index, int role) con if (role == Qt::DisplayRole) { if (index.column() == 0) { - return m_autoTypeAssociations->get(index.row()).window; + QString window = m_autoTypeAssociations->get(index.row()).window; + if (m_entry) { + window = m_entry->maskPasswordPlaceholders(window); + window = m_entry->resolveMultiplePlaceholders(window); + } + return window; } else { QString sequence = m_autoTypeAssociations->get(index.row()).sequence; diff --git a/src/gui/entry/AutoTypeAssociationsModel.h b/src/gui/entry/AutoTypeAssociationsModel.h index cef8bc66..1daa4a9c 100644 --- a/src/gui/entry/AutoTypeAssociationsModel.h +++ b/src/gui/entry/AutoTypeAssociationsModel.h @@ -19,10 +19,11 @@ #define KEEPASSX_AUTOTYPEASSOCIATIONSMODEL_H #include +#include #include "core/AutoTypeAssociations.h" -class EntryAttributes; +class Entry; class AutoTypeAssociationsModel : public QAbstractListModel { @@ -31,6 +32,7 @@ class AutoTypeAssociationsModel : public QAbstractListModel public: explicit AutoTypeAssociationsModel(QObject* parent = nullptr); void setAutoTypeAssociations(AutoTypeAssociations* autoTypeAssociations); + void setEntry(const Entry* entry); int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; @@ -47,6 +49,7 @@ public slots: private: AutoTypeAssociations* m_autoTypeAssociations; + QPointer m_entry; }; #endif // KEEPASSX_AUTOTYPEASSOCIATIONSMODEL_H diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 809ac95e..dda0ab1d 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -19,6 +19,7 @@ #include "EditEntryWidget.h" #include "ui_EditEntryWidgetAdvanced.h" #include "ui_EditEntryWidgetAutoType.h" +#include "ui_EditEntryWidgetSSHAgent.h" #include "ui_EditEntryWidgetHistory.h" #include "ui_EditEntryWidgetMain.h" @@ -36,10 +37,16 @@ #include "core/Metadata.h" #include "core/TimeDelta.h" #include "core/Tools.h" +#ifdef WITH_XC_SSHAGENT +#include "sshagent/KeeAgentSettings.h" +#include "sshagent/OpenSSHKey.h" +#include "sshagent/SSHAgent.h" +#endif #include "gui/EditWidgetIcons.h" #include "gui/EditWidgetProperties.h" #include "gui/FileDialog.h" #include "gui/MessageBox.h" +#include "gui/Clipboard.h" #include "gui/entry/AutoTypeAssociationsModel.h" #include "gui/entry/EntryAttachmentsModel.h" #include "gui/entry/EntryAttributesModel.h" @@ -51,11 +58,13 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) , m_mainUi(new Ui::EditEntryWidgetMain()) , m_advancedUi(new Ui::EditEntryWidgetAdvanced()) , m_autoTypeUi(new Ui::EditEntryWidgetAutoType()) + , m_sshAgentUi(new Ui::EditEntryWidgetSSHAgent()) , m_historyUi(new Ui::EditEntryWidgetHistory()) , m_mainWidget(new QWidget()) , m_advancedWidget(new QWidget()) , m_iconsWidget(new EditWidgetIcons()) , m_autoTypeWidget(new QWidget()) + , m_sshAgentWidget(new QWidget()) , m_editWidgetProperties(new EditWidgetProperties()) , m_historyWidget(new QWidget()) , m_entryAttachments(new EntryAttachments(this)) @@ -73,6 +82,14 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) setupAdvanced(); setupIcon(); setupAutoType(); +#ifdef WITH_XC_SSHAGENT + if (config()->get("SSHAgent", false).toBool()) { + setupSSHAgent(); + m_sshAgentEnabled = true; + } else { + m_sshAgentEnabled = false; + } +#endif setupProperties(); setupHistory(); @@ -99,6 +116,7 @@ void EditEntryWidget::setupMain() connect(m_mainUi->togglePasswordButton, SIGNAL(toggled(bool)), m_mainUi->passwordEdit, SLOT(setShowPassword(bool))); connect(m_mainUi->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); + connect(m_mainUi->notesEnabled, SIGNAL(toggled(bool)), this, SLOT(toggleHideNotes(bool))); m_mainUi->passwordRepeatEdit->enableVerifyMode(m_mainUi->passwordEdit); connect(m_mainUi->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); @@ -121,13 +139,14 @@ void EditEntryWidget::setupAdvanced() m_attachmentsModel->setEntryAttachments(m_entryAttachments); m_advancedUi->attachmentsView->setModel(m_attachmentsModel); + m_advancedUi->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection); connect(m_advancedUi->attachmentsView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(updateAttachmentButtonsEnabled(QModelIndex))); connect(m_advancedUi->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex))); - connect(m_advancedUi->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveCurrentAttachment())); - connect(m_advancedUi->openAttachmentButton, SIGNAL(clicked()), SLOT(openCurrentAttachment())); - connect(m_advancedUi->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachment())); - connect(m_advancedUi->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeCurrentAttachment())); + connect(m_advancedUi->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments())); + connect(m_advancedUi->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments())); + connect(m_advancedUi->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments())); + connect(m_advancedUi->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments())); m_attributesModel->setEntryAttributes(m_entryAttributes); m_advancedUi->attributesView->setModel(m_attributesModel); @@ -243,6 +262,239 @@ void EditEntryWidget::updateHistoryButtons(const QModelIndex& current, const QMo } } +#ifdef WITH_XC_SSHAGENT +void EditEntryWidget::setupSSHAgent() +{ + m_sshAgentUi->setupUi(m_sshAgentWidget); + + connect(m_sshAgentUi->privateKeyComboBox, SIGNAL(currentTextChanged(QString)), SLOT(updateSSHAgentKeyInfo())); + connect(m_sshAgentUi->browseButton, SIGNAL(clicked()), SLOT(browsePrivateKey())); + connect(m_sshAgentUi->addToAgentButton, SIGNAL(clicked()), SLOT(addKeyToAgent())); + connect(m_sshAgentUi->removeFromAgentButton, SIGNAL(clicked()), SLOT(removeKeyFromAgent())); + connect(m_sshAgentUi->decryptButton, SIGNAL(clicked()), SLOT(decryptPrivateKey())); + connect(m_sshAgentUi->copyToClipboardButton, SIGNAL(clicked()), SLOT(copyPublicKey())); + + addPage(tr("SSH Agent"), FilePath::instance()->icon("apps", "utilities-terminal"), m_sshAgentWidget); +} + +void EditEntryWidget::updateSSHAgent() +{ + // TODO: unsafe use of translations + QString prefix = tr("Attachment") + ": "; + KeeAgentSettings settings; + settings.fromXml(m_entryAttachments->value("KeeAgent.settings")); + + m_sshAgentUi->addKeyToAgentCheckBox->setChecked(settings.addAtDatabaseOpen()); + m_sshAgentUi->removeKeyFromAgentCheckBox->setChecked(settings.removeAtDatabaseClose()); + m_sshAgentUi->requireUserConfirmationCheckBox->setChecked(settings.useConfirmConstraintWhenAdding()); + m_sshAgentUi->lifetimeCheckBox->setChecked(settings.useLifetimeConstraintWhenAdding()); + m_sshAgentUi->lifetimeSpinBox->setValue(settings.lifetimeConstraintDuration()); + m_sshAgentUi->privateKeyComboBox->clear(); + m_sshAgentUi->addToAgentButton->setEnabled(false); + m_sshAgentUi->removeFromAgentButton->setEnabled(false); + m_sshAgentUi->copyToClipboardButton->setEnabled(false); + + for (QString fileName : m_entryAttachments->keys()) { + if (fileName == "KeeAgent.settings") { + continue; + } + + m_sshAgentUi->privateKeyComboBox->addItem(prefix + fileName); + } + + if (settings.selectedType() == "attachment") { + m_sshAgentUi->privateKeyComboBox->setCurrentText(prefix + settings.attachmentName()); + } else if (!settings.fileName().isEmpty()) { + m_sshAgentUi->privateKeyComboBox->addItem(settings.fileName()); + m_sshAgentUi->privateKeyComboBox->setCurrentText(settings.fileName()); + } else { + m_sshAgentUi->privateKeyComboBox->setCurrentText(""); + } + + m_sshAgentSettings = settings; +} + +void EditEntryWidget::updateSSHAgentKeyInfo() +{ + m_sshAgentUi->addToAgentButton->setEnabled(false); + m_sshAgentUi->removeFromAgentButton->setEnabled(false); + m_sshAgentUi->copyToClipboardButton->setEnabled(false); + m_sshAgentUi->fingerprintEdit->setText(""); + m_sshAgentUi->commentEdit->setText(""); + m_sshAgentUi->decryptButton->setEnabled(false); + m_sshAgentUi->publicKeyEdit->document()->setPlainText(""); + + if (m_sshAgentUi->privateKeyComboBox->currentText().isEmpty()) { + return; + } + + OpenSSHKey key; + + if (!getOpenSSHKey(key)) { + return; + } + + m_sshAgentUi->fingerprintEdit->setText(key.fingerprint()); + + if (key.encrypted()) { + m_sshAgentUi->commentEdit->setText(tr("(encrypted)")); + m_sshAgentUi->decryptButton->setEnabled(true); + } else { + m_sshAgentUi->commentEdit->setText(key.comment()); + } + + m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey()); + + // enable agent buttons only if we have an agent running + if (SSHAgent::instance()->isAgentRunning()) { + m_sshAgentUi->addToAgentButton->setEnabled(true); + m_sshAgentUi->removeFromAgentButton->setEnabled(true); + } + + m_sshAgentUi->copyToClipboardButton->setEnabled(true); +} + +void EditEntryWidget::saveSSHAgentConfig() +{ + KeeAgentSettings settings; + QString privateKeyPath = m_sshAgentUi->privateKeyComboBox->currentText(); + + settings.setAddAtDatabaseOpen(m_sshAgentUi->addKeyToAgentCheckBox->isChecked()); + settings.setRemoveAtDatabaseClose(m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked()); + settings.setUseConfirmConstraintWhenAdding(m_sshAgentUi->requireUserConfirmationCheckBox->isChecked()); + settings.setUseLifetimeConstraintWhenAdding(m_sshAgentUi->lifetimeCheckBox->isChecked()); + settings.setLifetimeConstraintDuration(m_sshAgentUi->lifetimeSpinBox->value()); + + // TODO: unsafe use of translations + QString prefix = tr("Attachment") + ": "; + if (privateKeyPath.startsWith(prefix)) { + settings.setSelectedType("attachment"); + settings.setAttachmentName(privateKeyPath.remove(0, prefix.length())); + settings.setFileName(""); + } else { + settings.setSelectedType("file"); + settings.setFileName(privateKeyPath); + settings.setAttachmentName(""); + } + + // we don't use this as we don't run an agent but for compatibility we set it if necessary + settings.setAllowUseOfSshKey(settings.addAtDatabaseOpen() || settings.removeAtDatabaseClose()); + + // we don't use this either but we don't want it to dirty flag the config + settings.setSaveAttachmentToTempFile(m_sshAgentSettings.saveAttachmentToTempFile()); + + if (settings.isDefault() && m_entryAttachments->hasKey("KeeAgent.settings")) { + m_entryAttachments->remove("KeeAgent.settings"); + } else if (settings != m_sshAgentSettings) { + m_entryAttachments->set("KeeAgent.settings", settings.toXml()); + } + + m_sshAgentSettings = settings; +} + +void EditEntryWidget::browsePrivateKey() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Select private key"), ""); + if (!fileName.isEmpty()) { + m_sshAgentUi->privateKeyComboBox->addItem(fileName); + m_sshAgentUi->privateKeyComboBox->setCurrentText(fileName); + } +} + +bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key) +{ + QString privateKeyPath = m_sshAgentUi->privateKeyComboBox->currentText(); + QByteArray privateKeyData; + + // TODO: unsafe use of translations + QString prefix = tr("Attachment") + ": "; + if (privateKeyPath.startsWith(prefix)) { + QString attachmentName = privateKeyPath.remove(0, prefix.length()); + privateKeyData = m_entryAttachments->value(attachmentName); + } else { + QFile localFile(privateKeyPath); + + if (localFile.size() > 1024 * 1024) { + showMessage(tr("File too large to be a private key"), MessageWidget::Error); + return false; + } + + if (!localFile.open(QIODevice::ReadOnly)) { + showMessage(tr("Failed to open private key"), MessageWidget::Error); + return false; + } + + privateKeyData = localFile.readAll(); + } + + if (!key.parse(privateKeyData)) { + showMessage(key.errorString(), MessageWidget::Error); + return false; + } + + return true; +} + +void EditEntryWidget::addKeyToAgent() +{ + OpenSSHKey key; + + if (!getOpenSSHKey(key)) { + return; + } + + if (!key.openPrivateKey(m_entry->password())) { + showMessage(key.errorString(), MessageWidget::Error); + } else { + m_sshAgentUi->commentEdit->setText(key.comment()); + m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey()); + } + + quint32 lifetime = 0; + bool confirm = m_sshAgentUi->requireUserConfirmationCheckBox->isChecked(); + + if (m_sshAgentUi->lifetimeCheckBox->isChecked()) { + lifetime = m_sshAgentUi->lifetimeSpinBox->value(); + } + + SSHAgent::instance()->addIdentity(key, lifetime, confirm); + + if (m_sshAgentUi->removeKeyFromAgentCheckBox->isChecked()) { + SSHAgent::instance()->removeIdentityAtLock(key, m_entry->uuid()); + } +} + +void EditEntryWidget::removeKeyFromAgent() +{ + OpenSSHKey key; + + if (getOpenSSHKey(key)) { + SSHAgent::instance()->removeIdentity(key); + } +} + +void EditEntryWidget::decryptPrivateKey() +{ + OpenSSHKey key; + + if (!getOpenSSHKey(key)) { + return; + } + + if (!key.openPrivateKey(m_entry->password())) { + showMessage(key.errorString(), MessageWidget::Error); + } else { + m_sshAgentUi->commentEdit->setText(key.comment()); + m_sshAgentUi->publicKeyEdit->document()->setPlainText(key.publicKey()); + } +} + +void EditEntryWidget::copyPublicKey() +{ + clipboard()->setText(m_sshAgentUi->publicKeyEdit->document()->toPlainText()); +} +#endif + void EditEntryWidget::useExpiryPreset(QAction* action) { m_mainUi->expireCheck->setChecked(true); @@ -261,6 +513,12 @@ void EditEntryWidget::updateAttachmentButtonsEnabled(const QModelIndex& current) m_advancedUi->removeAttachmentButton->setEnabled(enable && !m_history); } +void EditEntryWidget::toggleHideNotes(bool visible) +{ + m_mainUi->notesEdit->setVisible(visible); + m_mainUi->notesHint->setVisible(!visible); +} + QString EditEntryWidget::entryTitle() const { if (m_entry) { @@ -278,6 +536,7 @@ void EditEntryWidget::loadEntry(Entry* entry, bool create, bool history, const Q m_database = database; m_create = create; m_history = history; + m_saved = false; if (history) { setHeadline(QString("%1 > %2").arg(parentName, tr("Entry history"))); @@ -308,7 +567,10 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_mainUi->passwordRepeatEdit->setReadOnly(m_history); m_mainUi->expireCheck->setEnabled(!m_history); m_mainUi->expireDatePicker->setReadOnly(m_history); + m_mainUi->notesEnabled->setChecked(!config()->get("security/hidenotes").toBool()); m_mainUi->notesEdit->setReadOnly(m_history); + m_mainUi->notesEdit->setVisible(!config()->get("security/hidenotes").toBool()); + m_mainUi->notesHint->setVisible(config()->get("security/hidenotes").toBool()); m_mainUi->togglePasswordGeneratorButton->setChecked(false); m_mainUi->togglePasswordGeneratorButton->setDisabled(m_history); m_mainUi->passwordGenerator->reset(); @@ -378,6 +640,7 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_autoTypeUi->defaultWindowSequenceButton->setChecked(true); m_autoTypeUi->windowSequenceEdit->setText(""); m_autoTypeAssoc->copyDataFrom(entry->autoTypeAssociations()); + m_autoTypeAssocModel->setEntry(entry); if (m_autoTypeAssoc->size() != 0) { m_autoTypeUi->assocView->setCurrentIndex(m_autoTypeAssocModel->index(0, 0)); } @@ -386,6 +649,12 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) } updateAutoTypeEnabled(); +#ifdef WITH_XC_SSHAGENT + if (m_sshAgentEnabled) { + updateSSHAgent(); + } +#endif + m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid()); if (!m_history && !restore) { @@ -433,15 +702,28 @@ void EditEntryWidget::saveEntry() m_autoTypeAssoc->removeEmpty(); +#ifdef WITH_XC_SSHAGENT + if (m_sshAgentEnabled) { + saveSSHAgentConfig(); + } +#endif + if (!m_create) { m_entry->beginUpdate(); } updateEntryData(m_entry); + m_saved = true; if (!m_create) { m_entry->endUpdate(); } + +#ifdef WITH_XC_SSHAGENT + if (m_sshAgentEnabled) { + updateSSHAgent(); + } +#endif } void EditEntryWidget::acceptEntry() @@ -510,7 +792,7 @@ void EditEntryWidget::cancel() clear(); - emit editFinished(false); + emit editFinished(m_saved); } void EditEntryWidget::clear() @@ -631,7 +913,7 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) if (index.isValid()) { QString key = m_attributesModel->keyByIndex(index); if (showProtected) { - m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED] Press reveal to view or edit")); + m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED]") + " " + tr("Press reveal to view or edit")); m_advancedUi->attributesEdit->setEnabled(false); m_advancedUi->revealAttributeButton->setEnabled(true); m_advancedUi->protectAttributeButton->setChecked(true); @@ -661,6 +943,32 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) m_advancedUi->protectAttributeButton->blockSignals(false); } +bool EditEntryWidget::openAttachment(const QModelIndex &index, QString *errorMessage) +{ + const QString filename = m_attachmentsModel->keyByIndex(index); + const QByteArray attachmentData = m_entryAttachments->value(filename); + + // tmp file will be removed once the database (or the application) has been closed + const QString tmpFileTemplate = QDir::temp().absoluteFilePath(QString("XXXXXX.").append(filename)); + QTemporaryFile* tmpFile = new QTemporaryFile(tmpFileTemplate, this); + + const bool saveOk = tmpFile->open() + && tmpFile->write(attachmentData) == attachmentData.size() + && tmpFile->flush(); + if (!saveOk) { + if (errorMessage) { + *errorMessage = tr("Unable to save the attachment:\n").append(tmpFile->errorString()); + } + delete tmpFile; + return false; + } + + tmpFile->close(); + QDesktopServices::openUrl(QUrl::fromLocalFile(tmpFile->fileName())); + + return true; +} + void EditEntryWidget::protectCurrentAttribute(bool state) { QModelIndex index = m_advancedUi->attributesView->currentIndex(); @@ -691,7 +999,7 @@ void EditEntryWidget::revealCurrentAttribute() } } -void EditEntryWidget::insertAttachment() +void EditEntryWidget::insertAttachments() { Q_ASSERT(!m_history); @@ -699,53 +1007,115 @@ void EditEntryWidget::insertAttachment() if (defaultDir.isEmpty() || !QDir(defaultDir).exists()) { defaultDir = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0); } - QString filename = fileDialog()->getOpenFileName(this, tr("Select file"), defaultDir); - if (filename.isEmpty() || !QFile::exists(filename)) { + + const QStringList filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDir); + if (filenames.isEmpty()) { return; } - QFile file(filename); - if (!file.open(QIODevice::ReadOnly)) { - showMessage(tr("Unable to open file").append(":\n").append(file.errorString()), MessageWidget::Error); - return; + config()->set("LastAttachmentDir", QFileInfo(filenames.first()).absolutePath()); + + QStringList errors; + for (const QString &filename: filenames) { + const QFileInfo fInfo(filename); + QFile file(filename); + QByteArray data; + const bool readOk = file.open(QIODevice::ReadOnly) && Tools::readAllFromDevice(&file, data); + if (!readOk) { + errors.append(QString("%1 - %2").arg(fInfo.fileName(), file.errorString())); + continue; + } + + m_entryAttachments->set(fInfo.fileName(), data); } - QByteArray data; - if (!Tools::readAllFromDevice(&file, data)) { - showMessage(tr("Unable to open file").append(":\n").append(file.errorString()), MessageWidget::Error); - return; + if (!errors.isEmpty()) { + showMessage(tr("Unable to open files:\n%1").arg(errors.join('\n')), MessageWidget::Error); } - - m_entryAttachments->set(QFileInfo(filename).fileName(), data); } -void EditEntryWidget::saveCurrentAttachment() +void EditEntryWidget::saveSelectedAttachment() { - QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); + const QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); if (!index.isValid()) { return; } - QString filename = m_attachmentsModel->keyByIndex(index); + const QString filename = m_attachmentsModel->keyByIndex(index); QString defaultDirName = config()->get("LastAttachmentDir").toString(); if (defaultDirName.isEmpty() || !QDir(defaultDirName).exists()) { defaultDirName = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } - QDir dir(defaultDirName); - QString savePath = fileDialog()->getSaveFileName(this, tr("Save attachment"), - dir.filePath(filename)); + + const QString savePath = fileDialog()->getSaveFileName(this, tr("Save attachment"), + QDir(defaultDirName).filePath(filename)); if (!savePath.isEmpty()) { - QByteArray attachmentData = m_entryAttachments->value(filename); + config()->set("LastAttachmentDir", QFileInfo(savePath).absolutePath()); QFile file(savePath); - if (!file.open(QIODevice::WriteOnly)) { + const QByteArray attachmentData = m_entryAttachments->value(filename); + const bool saveOk = file.open(QIODevice::WriteOnly) && file.write(attachmentData) == attachmentData.size(); + if (!saveOk) { showMessage(tr("Unable to save the attachment:\n").append(file.errorString()), MessageWidget::Error); + } + } +} + +void EditEntryWidget::saveSelectedAttachments() +{ + const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + return; + } else if (indexes.count() == 1) { + saveSelectedAttachment(); + return; + } + + QString defaultDirName = config()->get("LastAttachmentDir").toString(); + if (defaultDirName.isEmpty() || !QDir(defaultDirName).exists()) { + defaultDirName = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + } + + const QString savePath = fileDialog()->getExistingDirectory(this, tr("Save attachments"), defaultDirName); + if (savePath.isEmpty()) { + return; + } + + QDir saveDir(savePath); + if (!saveDir.exists()) { + if (saveDir.mkpath(saveDir.absolutePath())) { + showMessage(tr("Unable to create the directory:\n").append(saveDir.absolutePath()), MessageWidget::Error); return; } - if (file.write(attachmentData) != attachmentData.size()) { - showMessage(tr("Unable to save the attachment:\n").append(file.errorString()), MessageWidget::Error); - return; + } + config()->set("LastAttachmentDir", QFileInfo(saveDir.absolutePath()).absolutePath()); + + QStringList errors; + for (const QModelIndex &index: indexes) { + const QString filename = m_attachmentsModel->keyByIndex(index); + const QString attachmentPath = saveDir.absoluteFilePath(filename); + + if (QFileInfo::exists(attachmentPath)) { + const QString question(tr("Are you sure you want to overwrite existing file \"%1\" with the attachment?")); + auto ans = MessageBox::question(this, tr("Confirm overwrite"), question.arg(filename), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (ans == QMessageBox::No) { + continue; + } else if (ans == QMessageBox::Cancel) { + return; + } } + + QFile file(attachmentPath); + const QByteArray attachmentData = m_entryAttachments->value(filename); + const bool saveOk = file.open(QIODevice::WriteOnly) && file.write(attachmentData) == attachmentData.size(); + if (!saveOk) { + errors.append(QString("%1 - %2").arg(filename, file.errorString())); + } + } + + if (!errors.isEmpty()) { + showMessage(tr("Unable to save the attachments:\n").append(errors.join('\n')), MessageWidget::Error); } } @@ -756,55 +1126,51 @@ void EditEntryWidget::openAttachment(const QModelIndex& index) return; } - QString filename = m_attachmentsModel->keyByIndex(index); - QByteArray attachmentData = m_entryAttachments->value(filename); - - // tmp file will be removed once the database (or the application) has been closed - QString tmpFileTemplate = QDir::temp().absoluteFilePath(QString("XXXXXX.").append(filename)); - QTemporaryFile* file = new QTemporaryFile(tmpFileTemplate, this); - - if (!file->open()) { - showMessage(tr("Unable to save the attachment:\n").append(file->errorString()), MessageWidget::Error); - return; + QString errorMessage; + if (!openAttachment(index, &errorMessage)) { + showMessage(errorMessage, MessageWidget::Error); } - - if (file->write(attachmentData) != attachmentData.size()) { - showMessage(tr("Unable to save the attachment:\n").append(file->errorString()), MessageWidget::Error); - return; - } - - if (!file->flush()) { - showMessage(tr("Unable to save the attachment:\n").append(file->errorString()), MessageWidget::Error); - return; - } - - file->close(); - - QDesktopServices::openUrl(QUrl::fromLocalFile(file->fileName())); } -void EditEntryWidget::openCurrentAttachment() +void EditEntryWidget::openSelectedAttachments() { - QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); + const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + return; + } - openAttachment(index); + QStringList errors; + for (const QModelIndex &index: indexes) { + QString errorMessage; + if (!openAttachment(index, &errorMessage)) { + const QString filename = m_attachmentsModel->keyByIndex(index); + errors.append(QString("%1 - %2").arg(filename, errorMessage)); + }; + } + + if (!errors.isEmpty()) { + showMessage(tr("Unable to open the attachments:\n").append(errors.join('\n')), MessageWidget::Error); + } } -void EditEntryWidget::removeCurrentAttachment() +void EditEntryWidget::removeSelectedAttachments() { Q_ASSERT(!m_history); - QModelIndex index = m_advancedUi->attachmentsView->currentIndex(); - if (!index.isValid()) { + const QModelIndexList indexes = m_advancedUi->attachmentsView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { return; } + const QString question = tr("Are you sure you want to remove %n attachments?", "", indexes.count()); QMessageBox::StandardButton ans = MessageBox::question(this, tr("Confirm Remove"), - tr("Are you sure you want to remove this attachment?"), - QMessageBox::Yes | QMessageBox::No); + question, QMessageBox::Yes | QMessageBox::No); if (ans == QMessageBox::Yes) { - QString key = m_attachmentsModel->keyByIndex(index); - m_entryAttachments->remove(key); + QStringList keys; + for (const QModelIndex &index: indexes) { + keys.append(m_attachmentsModel->keyByIndex(index)); + } + m_entryAttachments->remove(keys); } } diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 2888d43a..883e7e7f 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -23,6 +23,7 @@ #include #include "gui/EditWidget.h" +#include "config-keepassx.h" class AutoTypeAssociations; class AutoTypeAssociationsModel; @@ -39,10 +40,15 @@ class QButtonGroup; class QMenu; class QSortFilterProxyModel; class QStackedLayout; +#ifdef WITH_XC_SSHAGENT +#include "sshagent/KeeAgentSettings.h" +class OpenSSHKey; +#endif namespace Ui { class EditEntryWidgetAdvanced; class EditEntryWidgetAutoType; + class EditEntryWidgetSSHAgent; class EditEntryWidgetMain; class EditEntryWidgetHistory; class EditWidget; @@ -80,11 +86,12 @@ private slots: void updateCurrentAttribute(); void protectCurrentAttribute(bool state); void revealCurrentAttribute(); - void insertAttachment(); - void saveCurrentAttachment(); + void insertAttachments(); + void saveSelectedAttachment(); + void saveSelectedAttachments(); void openAttachment(const QModelIndex& index); - void openCurrentAttachment(); - void removeCurrentAttachment(); + void openSelectedAttachments(); + void removeSelectedAttachments(); void updateAutoTypeEnabled(); void insertAutoTypeAssoc(); void removeAutoTypeAssoc(); @@ -100,12 +107,25 @@ private slots: void updateHistoryButtons(const QModelIndex& current, const QModelIndex& previous); void useExpiryPreset(QAction* action); void updateAttachmentButtonsEnabled(const QModelIndex& current); + void toggleHideNotes(bool visible); +#ifdef WITH_XC_SSHAGENT + void updateSSHAgent(); + void updateSSHAgentKeyInfo(); + void browsePrivateKey(); + void addKeyToAgent(); + void removeKeyFromAgent(); + void decryptPrivateKey(); + void copyPublicKey(); +#endif private: void setupMain(); void setupAdvanced(); void setupIcon(); void setupAutoType(); +#ifdef WITH_XC_SSHAGENT + void setupSSHAgent(); +#endif void setupProperties(); void setupHistory(); @@ -113,22 +133,35 @@ private: void setForms(const Entry* entry, bool restore = false); QMenu* createPresetsMenu(); void updateEntryData(Entry* entry) const; +#ifdef WITH_XC_SSHAGENT + bool getOpenSSHKey(OpenSSHKey& key); + void saveSSHAgentConfig(); +#endif void displayAttribute(QModelIndex index, bool showProtected); + bool openAttachment(const QModelIndex& index, QString *errorMessage); + Entry* m_entry; Database* m_database; bool m_create; bool m_history; + bool m_saved; +#ifdef WITH_XC_SSHAGENT + bool m_sshAgentEnabled; + KeeAgentSettings m_sshAgentSettings; +#endif const QScopedPointer m_mainUi; const QScopedPointer m_advancedUi; const QScopedPointer m_autoTypeUi; + const QScopedPointer m_sshAgentUi; const QScopedPointer m_historyUi; QWidget* const m_mainWidget; QWidget* const m_advancedWidget; EditWidgetIcons* const m_iconsWidget; QWidget* const m_autoTypeWidget; + QWidget* const m_sshAgentWidget; EditWidgetProperties* const m_editWidgetProperties; QWidget* const m_historyWidget; EntryAttachments* const m_entryAttachments; diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui index afc1d4f2..dc07603a 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -79,10 +79,10 @@ - - + + - Notes: + Notes @@ -129,6 +129,19 @@ + + + + false + + + Toggle the checkbox to reveal the notes section. + + + Qt::AlignTop + + + diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui new file mode 100644 index 00000000..2d88327c --- /dev/null +++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui @@ -0,0 +1,207 @@ + + + EditEntryWidgetSSHAgent + + + + 0 + 0 + 471 + 480 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Remove key from agent when database is closed/locked + + + + + + + Fingerprint + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + + + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + Private key + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Public key + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + + Remove key from agent after + + + + + + + seconds + + + 999999999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Comment + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Add key to agent when database is opened/unlocked + + + + + + + + 0 + 0 + + + + true + + + + + + + Browse... + + + + + + + Require user confirmation when this key is used + + + + + + + Copy to clipboard + + + + + + + + + Add to agent + + + + + + + Remove from agent + + + + + + + + + true + + + + + + + Decrypt + + + + + + + + diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 2f79f02d..4d357806 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -151,8 +151,7 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } return result; case Url: - result = entry->maskPasswordPlaceholders(entry->url()); - result = entry->resolveMultiplePlaceholders(result); + result = entry->displayUrl(); if (attr->isReference(EntryAttributes::URLKey)) { result.prepend(tr("Ref: ","Reference abbreviation")); } diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 1bdd4fbc..ac8e6678 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -49,6 +49,8 @@ EntryView::EntryView(QWidget* parent) connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SIGNAL(entrySelectionChanged())); connect(m_model, SIGNAL(switchedToEntryListMode()), SLOT(switchToEntryListMode())); connect(m_model, SIGNAL(switchedToGroupMode()), SLOT(switchToGroupMode())); + + connect(this, SIGNAL(clicked(QModelIndex)), SLOT(emitEntryPressed(QModelIndex))); } void EntryView::keyPressEvent(QKeyEvent* event) @@ -99,6 +101,11 @@ void EntryView::emitEntryActivated(const QModelIndex& index) emit entryActivated(entry, static_cast(m_sortModel->mapToSource(index).column())); } +void EntryView::emitEntryPressed(const QModelIndex& index) +{ + emit entryPressed(entryFromIndex(index)); +} + void EntryView::setModel(QAbstractItemModel* model) { Q_UNUSED(model); diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index 6a545f62..14c6b7cc 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -47,6 +47,7 @@ public slots: signals: void entryActivated(Entry* entry, EntryModel::ModelColumn column); + void entryPressed(Entry* entry); void entrySelectionChanged(); protected: @@ -54,6 +55,7 @@ protected: private slots: void emitEntryActivated(const QModelIndex& index); + void emitEntryPressed(const QModelIndex& index); void switchToEntryListMode(); void switchToGroupMode(); diff --git a/src/gui/group/GroupView.cpp b/src/gui/group/GroupView.cpp index e9649e44..82d01e31 100644 --- a/src/gui/group/GroupView.cpp +++ b/src/gui/group/GroupView.cpp @@ -41,6 +41,8 @@ GroupView::GroupView(Database* db, QWidget* parent) connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(emitGroupChanged())); + connect(this, SIGNAL(clicked(QModelIndex)), SLOT(emitGroupPressed(QModelIndex))); + modelReset(); setDragEnabled(true); @@ -126,6 +128,11 @@ void GroupView::emitGroupChanged() emit groupChanged(currentGroup()); } +void GroupView::emitGroupPressed(const QModelIndex& index) +{ + emit groupPressed(m_model->groupFromIndex(index)); +} + void GroupView::syncExpandedState(const QModelIndex& parent, int start, int end) { for (int row = start; row <= end; row++) { diff --git a/src/gui/group/GroupView.h b/src/gui/group/GroupView.h index eaa29072..0a084425 100644 --- a/src/gui/group/GroupView.h +++ b/src/gui/group/GroupView.h @@ -38,11 +38,13 @@ public: signals: void groupChanged(Group* group); + void groupPressed(Group* group); private slots: void expandedChanged(const QModelIndex& index); void emitGroupChanged(const QModelIndex& index); void emitGroupChanged(); + void emitGroupPressed(const QModelIndex& index); void syncExpandedState(const QModelIndex& parent, int start, int end); void modelReset(); diff --git a/src/http/OptionDialog.cpp b/src/http/OptionDialog.cpp index 9fb66bd6..03ae6e9c 100644 --- a/src/http/OptionDialog.cpp +++ b/src/http/OptionDialog.cpp @@ -35,6 +35,7 @@ OptionDialog::OptionDialog(QWidget *parent) : 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->warningWidget->setAutoHideTimeout(MessageWidget::DisableAutoHide); m_ui->tabWidget->setEnabled(m_ui->enableHttpServer->isChecked()); connect(m_ui->enableHttpServer, SIGNAL(toggled(bool)), m_ui->tabWidget, SLOT(setEnabled(bool))); diff --git a/src/http/Service.cpp b/src/http/Service.cpp index 768a5774..7f74a3ae 100644 --- a/src/http/Service.cpp +++ b/src/http/Service.cpp @@ -259,13 +259,16 @@ Service::Access Service::checkAccess(const Entry *entry, const QString & host, c KeepassHttpProtocol::Entry Service::prepareEntry(const Entry* entry) { - KeepassHttpProtocol::Entry res(entry->resolvePlaceholder(entry->title()), entry->resolvePlaceholder(entry->username()), entry->resolvePlaceholder(entry->password()), entry->uuid().toHex()); + KeepassHttpProtocol::Entry res(entry->resolveMultiplePlaceholders(entry->title()), + entry->resolveMultiplePlaceholders(entry->username()), + entry->resolveMultiplePlaceholders(entry->password()), + entry->uuid().toHex()); if (HttpSettings::supportKphFields()) { const EntryAttributes * attr = entry->attributes(); const auto keys = attr->keys(); for (const QString& key: keys) { if (key.startsWith(QLatin1String("KPH: "))) { - res.addStringField(key, attr->value(key)); + res.addStringField(key, entry->resolveMultiplePlaceholders(attr->value(key))); } } } @@ -545,7 +548,7 @@ void Service::removeSharedEncryptionKeys() const int count = keysToRemove.count(); QMessageBox::information(0, tr("KeePassXC: Removed keys from database"), - tr("Successfully removed %1 encryption-%2 from KeePassX/Http Settings.").arg(count).arg(count ? "keys" : "key"), + tr("Successfully removed %n encryption-key(s) from KeePassX/Http Settings.", "", count), QMessageBox::Ok); } else { QMessageBox::information(0, tr("KeePassXC: No keys found"), @@ -589,7 +592,7 @@ void Service::removeStoredPermissions() if (counter > 0) { QMessageBox::information(0, tr("KeePassXC: Removed permissions"), - tr("Successfully removed permissions from %1 %2.").arg(counter).arg(counter ? "entries" : "entry"), + tr("Successfully removed permissions from %n entries.", "", counter), QMessageBox::Ok); } else { QMessageBox::information(0, tr("KeePassXC: No entry with permissions found!"), diff --git a/src/http/qhttp/http-parser/http_parser.c b/src/http/qhttp/http-parser/http_parser.c index 895bf0c7..ab48d0cb 100644 --- a/src/http/qhttp/http-parser/http_parser.c +++ b/src/http/qhttp/http-parser/http_parser.c @@ -1815,6 +1815,9 @@ reexecute: case 2: parser->upgrade = 1; +#if __GNUC__ >= 7 + __attribute__ ((fallthrough)); +#endif case 1: parser->flags |= F_SKIPBODY; @@ -2374,6 +2377,9 @@ http_parser_parse_url(const char *buf, size_t buflen, int is_connect, case s_req_server_with_at: found_at = 1; +#if __GNUC__ >= 7 + __attribute__ ((fallthrough)); +#endif /* FALLTROUGH */ case s_req_server: diff --git a/src/sshagent/AgentSettingsPage.cpp b/src/sshagent/AgentSettingsPage.cpp new file mode 100644 index 00000000..70fa04bd --- /dev/null +++ b/src/sshagent/AgentSettingsPage.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 . + */ + +#include "AgentSettingsPage.h" +#include "AgentSettingsWidget.h" +#include "core/FilePath.h" + +AgentSettingsPage::AgentSettingsPage(DatabaseTabWidget* tabWidget) +{ + Q_UNUSED(tabWidget); +} + +AgentSettingsPage::~AgentSettingsPage() +{ + +} + +QString AgentSettingsPage::name() +{ + return QObject::tr("SSH Agent"); +} + +QIcon AgentSettingsPage::icon() +{ + return FilePath::instance()->icon("apps", "utilities-terminal"); +} + +QWidget* AgentSettingsPage::createWidget() +{ + return new AgentSettingsWidget(); +} + +void AgentSettingsPage::loadSettings(QWidget* widget) +{ + AgentSettingsWidget* agentWidget = reinterpret_cast(widget); + agentWidget->loadSettings(); +} + +void AgentSettingsPage::saveSettings(QWidget* widget) +{ + AgentSettingsWidget* agentWidget = reinterpret_cast(widget); + agentWidget->saveSettings(); +} diff --git a/src/sshagent/AgentSettingsPage.h b/src/sshagent/AgentSettingsPage.h new file mode 100644 index 00000000..f9d1be3f --- /dev/null +++ b/src/sshagent/AgentSettingsPage.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 AGENTSETTINGSPAGE_H +#define AGENTSETTINGSPAGE_H + +#include "gui/SettingsWidget.h" +#include "gui/DatabaseTabWidget.h" + +class AgentSettingsPage : public ISettingsPage +{ +public: + AgentSettingsPage(DatabaseTabWidget* tabWidget); + ~AgentSettingsPage() override; + + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget) override; + void saveSettings(QWidget* widget) override; + +private: +}; + +#endif // AGENTSETTINGSPAGE_H diff --git a/src/sshagent/AgentSettingsWidget.cpp b/src/sshagent/AgentSettingsWidget.cpp new file mode 100644 index 00000000..e8bc75ae --- /dev/null +++ b/src/sshagent/AgentSettingsWidget.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 . + */ + +#include "AgentSettingsWidget.h" +#include "core/Config.h" + +AgentSettingsWidget::AgentSettingsWidget(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::AgentSettingsWidget()) +{ + m_ui->setupUi(this); +} + +void AgentSettingsWidget::loadSettings() +{ + m_ui->enableSSHAgentCheckBox->setChecked(config()->get("SSHAgent", false).toBool()); +} + +void AgentSettingsWidget::saveSettings() +{ + config()->set("SSHAgent", m_ui->enableSSHAgentCheckBox->isChecked()); +} diff --git a/src/sshagent/AgentSettingsWidget.h b/src/sshagent/AgentSettingsWidget.h new file mode 100644 index 00000000..b6462daa --- /dev/null +++ b/src/sshagent/AgentSettingsWidget.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 AGENTSETTINGSWIDGET_H +#define AGENTSETTINGSWIDGET_H + +#include +#include +#include "ui_AgentSettingsWidget.h" + +namespace Ui { + class AgentSettingsWidget; +} + +class AgentSettingsWidget : public QWidget +{ + Q_OBJECT +public: + explicit AgentSettingsWidget(QWidget* parent = nullptr); + +signals: + +public slots: + void loadSettings(); + void saveSettings(); + +private: + QScopedPointer m_ui; +}; + +#endif // AGENTSETTINGSWIDGET_H diff --git a/src/sshagent/AgentSettingsWidget.ui b/src/sshagent/AgentSettingsWidget.ui new file mode 100644 index 00000000..e97ee87b --- /dev/null +++ b/src/sshagent/AgentSettingsWidget.ui @@ -0,0 +1,53 @@ + + + AgentSettingsWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Enable SSH Agent (requires restart) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/sshagent/BinaryStream.cpp b/src/sshagent/BinaryStream.cpp new file mode 100644 index 00000000..b9ed236f --- /dev/null +++ b/src/sshagent/BinaryStream.cpp @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 . + */ + +#include "BinaryStream.h" +#include + +BinaryStream::BinaryStream(QObject* parent) + : QObject(parent) + , m_timeout(-1) +{ + +} + +BinaryStream::BinaryStream(QIODevice* device) + : QObject(device) + , m_timeout(-1) + , m_device(device) +{ + +} + +BinaryStream::BinaryStream(QByteArray* ba, QObject* parent) + : QObject(parent) + , m_timeout(-1) +{ + setData(ba); +} + +BinaryStream::~BinaryStream() +{ +} + +const QString BinaryStream::errorString() const +{ + return m_error; +} + +QIODevice* BinaryStream::device() const +{ + return m_device; +} + +void BinaryStream::setDevice(QIODevice* device) +{ + m_device = device; +} + +void BinaryStream::setData(QByteArray* ba) +{ + m_buffer.reset(new QBuffer(ba)); + m_buffer->open(QIODevice::ReadWrite); + + m_device = m_buffer.data(); +} + +void BinaryStream::setTimeout(int timeout) +{ + m_timeout = timeout; +} + +bool BinaryStream::read(char* ptr, qint64 size) +{ + qint64 pos = 0; + + while (pos < size) { + if (m_device->bytesAvailable() == 0) { + if (!m_device->waitForReadyRead(m_timeout)) { + m_error = m_device->errorString(); + return false; + } + } + + qint64 nread = m_device->read(ptr + pos, size - pos); + + if (nread == -1) { + m_error = m_device->errorString(); + return false; + } + + pos += nread; + } + + return true; +} + +bool BinaryStream::read(QByteArray& ba) +{ + return read(ba.data(), ba.length()); +} + +bool BinaryStream::read(quint32& i) +{ + if (read(reinterpret_cast(&i), sizeof(i))) { + i = qFromBigEndian(i); + return true; + } + + return false; +} + +bool BinaryStream::read(quint16& i) +{ + if (read(reinterpret_cast(&i), sizeof(i))) { + i = qFromBigEndian(i); + return true; + } + + return false; +} + +bool BinaryStream::read(quint8& i) +{ + return read(reinterpret_cast(&i), sizeof(i)); +} + +bool BinaryStream::readString(QByteArray& ba) +{ + quint32 length; + + if (!read(length)) { + return false; + } + + ba.resize(length); + + if (!read(ba.data(), ba.length())) { + return false; + } + + return true; +} + +bool BinaryStream::readString(QString& str) +{ + QByteArray ba; + + if (!readString(ba)) { + return false; + } + + str = str.fromLatin1(ba); + return true; +} + + +bool BinaryStream::write(const char* ptr, qint64 size) +{ + if (m_device->write(ptr, size) < 0) { + m_error = m_device->errorString(); + return false; + } + + return true; +} + +bool BinaryStream::flush() +{ + if (!m_device->waitForBytesWritten(m_timeout)) { + m_error = m_device->errorString(); + return false; + } + + return true; +} + +bool BinaryStream::write(const QByteArray& ba) +{ + return write(ba.data(), ba.length()); +} + +bool BinaryStream::write(quint32 i) +{ + i = qToBigEndian(i); + return write(reinterpret_cast(&i), sizeof(i)); +} + +bool BinaryStream::write(quint16 i) +{ + i = qToBigEndian(i); + return write(reinterpret_cast(&i), sizeof(i)); +} + +bool BinaryStream::write(quint8 i) +{ + return write(reinterpret_cast(&i), sizeof(i)); +} + +bool BinaryStream::writeString(const QByteArray& ba) +{ + if (!write(static_cast(ba.length()))) { + return false; + } + + if (!write(ba)) { + return false; + } + + return true; +} + +bool BinaryStream::writeString(const QString& s) +{ + return writeString(s.toLatin1()); +} diff --git a/src/sshagent/BinaryStream.h b/src/sshagent/BinaryStream.h new file mode 100644 index 00000000..c6101018 --- /dev/null +++ b/src/sshagent/BinaryStream.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 BINARYSTREAM_H +#define BINARYSTREAM_H + +#include +#include +#include + +class BinaryStream : QObject +{ + Q_OBJECT +public: + BinaryStream(QObject* parent = nullptr); + BinaryStream(QIODevice* device); + BinaryStream(QByteArray* ba, QObject* parent = nullptr); + ~BinaryStream(); + + const QString errorString() const; + QIODevice* device() const; + void setDevice(QIODevice* device); + void setData(QByteArray* ba); + void setTimeout(int timeout); + + bool read(QByteArray& ba); + bool read(quint32& i); + bool read(quint16& i); + bool read(quint8& i); + bool readString(QByteArray& ba); + bool readString(QString& s); + + bool write(const QByteArray& ba); + bool write(quint32 i); + bool write(quint16 i); + bool write(quint8 i); + bool writeString(const QByteArray& ba); + bool writeString(const QString& s); + + bool flush(); + +protected: + bool read(char* ptr, qint64 len); + bool write(const char* ptr, qint64 len); + +private: + int m_timeout; + QString m_error; + QIODevice* m_device; + QScopedPointer m_buffer; +}; + +#endif // BINARYSTREAM_H diff --git a/src/sshagent/CMakeLists.txt b/src/sshagent/CMakeLists.txt new file mode 100644 index 00000000..1733e21b --- /dev/null +++ b/src/sshagent/CMakeLists.txt @@ -0,0 +1,15 @@ +if(WITH_XC_SSHAGENT) + set(sshagent_SOURCES + bcrypt_pbkdf.cpp + blowfish.c + AgentSettingsPage.cpp + AgentSettingsWidget.cpp + BinaryStream.cpp + KeeAgentSettings.cpp + OpenSSHKey.cpp + SSHAgent.cpp + ) + + add_library(sshagent STATIC ${sshagent_SOURCES}) + target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES}) +endif() diff --git a/src/sshagent/KeeAgentSettings.cpp b/src/sshagent/KeeAgentSettings.cpp new file mode 100644 index 00000000..218e98ac --- /dev/null +++ b/src/sshagent/KeeAgentSettings.cpp @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 . + */ + +#include "KeeAgentSettings.h" + +KeeAgentSettings::KeeAgentSettings() + : m_allowUseOfSshKey(false) + , m_addAtDatabaseOpen(false) + , m_removeAtDatabaseClose(false) + , m_useConfirmConstraintWhenAdding(false) + , m_useLifetimeConstraintWhenAdding(false) + , m_lifetimeConstraintDuration(600) + , m_selectedType(QString("file")) + , m_attachmentName(QString()) + , m_saveAttachmentToTempFile(false) + , m_fileName(QString()) +{ + +} + +bool KeeAgentSettings::operator==(KeeAgentSettings& other) +{ + return (m_allowUseOfSshKey == other.m_allowUseOfSshKey + && m_addAtDatabaseOpen == other.m_addAtDatabaseOpen + && m_removeAtDatabaseClose == other.m_removeAtDatabaseClose + && m_useConfirmConstraintWhenAdding == other.m_useConfirmConstraintWhenAdding + && m_useLifetimeConstraintWhenAdding == other.m_useLifetimeConstraintWhenAdding + && m_lifetimeConstraintDuration == other.m_lifetimeConstraintDuration + && m_selectedType == other.m_selectedType + && m_attachmentName == other.m_attachmentName + && m_saveAttachmentToTempFile == other.m_saveAttachmentToTempFile + && m_fileName == other.m_fileName); +} + +bool KeeAgentSettings::operator!=(KeeAgentSettings& other) +{ + return !(*this == other); +} + +bool KeeAgentSettings::isDefault() +{ + KeeAgentSettings defaultSettings; + return (*this == defaultSettings); +} + +bool KeeAgentSettings::allowUseOfSshKey() const +{ + return m_allowUseOfSshKey; +} + +bool KeeAgentSettings::addAtDatabaseOpen() const +{ + return m_addAtDatabaseOpen; +} + +bool KeeAgentSettings::removeAtDatabaseClose() const +{ + return m_removeAtDatabaseClose; +} + +bool KeeAgentSettings::useConfirmConstraintWhenAdding() const +{ + return m_useConfirmConstraintWhenAdding; +} + +bool KeeAgentSettings::useLifetimeConstraintWhenAdding() const +{ + return m_useLifetimeConstraintWhenAdding; +} + +int KeeAgentSettings::lifetimeConstraintDuration() const +{ + return m_lifetimeConstraintDuration; +} + +const QString KeeAgentSettings::selectedType() const +{ + return m_selectedType; +} + +const QString KeeAgentSettings::attachmentName() const +{ + return m_attachmentName; +} + +bool KeeAgentSettings::saveAttachmentToTempFile() const +{ + return m_saveAttachmentToTempFile; +} + +const QString KeeAgentSettings::fileName() const +{ + return m_fileName; +} + +void KeeAgentSettings::setAllowUseOfSshKey(bool allowUseOfSshKey) +{ + m_allowUseOfSshKey = allowUseOfSshKey; +} + +void KeeAgentSettings::setAddAtDatabaseOpen(bool addAtDatabaseOpen) +{ + m_addAtDatabaseOpen = addAtDatabaseOpen; +} + +void KeeAgentSettings::setRemoveAtDatabaseClose(bool removeAtDatabaseClose) +{ + m_removeAtDatabaseClose = removeAtDatabaseClose; +} + +void KeeAgentSettings::setUseConfirmConstraintWhenAdding(bool useConfirmConstraintWhenAdding) +{ + m_useConfirmConstraintWhenAdding = useConfirmConstraintWhenAdding; +} + +void KeeAgentSettings::setUseLifetimeConstraintWhenAdding(bool useLifetimeConstraintWhenAdding) +{ + m_useLifetimeConstraintWhenAdding = useLifetimeConstraintWhenAdding; +} + +void KeeAgentSettings::setLifetimeConstraintDuration(int lifetimeConstraintDuration) +{ + m_lifetimeConstraintDuration = lifetimeConstraintDuration; +} + +void KeeAgentSettings::setSelectedType(const QString& selectedType) +{ + m_selectedType = selectedType; +} + +void KeeAgentSettings::setAttachmentName(const QString& attachmentName) +{ + m_attachmentName = attachmentName; +} + +void KeeAgentSettings::setSaveAttachmentToTempFile(bool saveAttachmentToTempFile) +{ + m_saveAttachmentToTempFile = saveAttachmentToTempFile; +} + +void KeeAgentSettings::setFileName(const QString& fileName) +{ + m_fileName = fileName; +} + +bool KeeAgentSettings::readBool(QXmlStreamReader& reader) +{ + reader.readNext(); + bool ret = (reader.text().startsWith("t", Qt::CaseInsensitive)); + reader.readNext(); // tag end + return ret; +} + +int KeeAgentSettings::readInt(QXmlStreamReader& reader) +{ + reader.readNext(); + int ret = reader.text().toInt(); + reader.readNext(); // tag end + return ret; +} + +bool KeeAgentSettings::fromXml(const QByteArray& ba) +{ + QXmlStreamReader reader; + reader.addData(ba); + + if (reader.error() || !reader.readNextStartElement()) { + return false; + } + + if (reader.qualifiedName() != "EntrySettings") { + return false; + } + + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "AllowUseOfSshKey") { + m_allowUseOfSshKey = readBool(reader); + } else if (reader.name() == "AddAtDatabaseOpen") { + m_addAtDatabaseOpen = readBool(reader); + } else if (reader.name() == "RemoveAtDatabaseClose") { + m_removeAtDatabaseClose = readBool(reader); + } else if (reader.name() == "UseConfirmConstraintWhenAdding") { + m_useConfirmConstraintWhenAdding = readBool(reader); + } else if (reader.name() == "UseLifetimeConstraintWhenAdding") { + m_useLifetimeConstraintWhenAdding = readBool(reader); + } else if (reader.name() == "LifetimeConstraintDuration") { + m_lifetimeConstraintDuration = readInt(reader); + } else if (reader.name() == "Location") { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "SelectedType") { + reader.readNext(); + m_selectedType = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "AttachmentName") { + reader.readNext(); + m_attachmentName = reader.text().toString(); + reader.readNext(); + } else if (reader.name() == "SaveAttachmentToTempFile") { + m_saveAttachmentToTempFile = readBool(reader); + } else if (reader.name() == "FileName") { + reader.readNext(); + m_fileName = reader.text().toString(); + reader.readNext(); + } else { + qWarning() << "Skipping location element" << reader.name(); + reader.skipCurrentElement(); + } + } + } else { + qWarning() << "Skipping element" << reader.name(); + reader.skipCurrentElement(); + } + } + + return true; +} + +QByteArray KeeAgentSettings::toXml() +{ + QByteArray ba; + QXmlStreamWriter writer(&ba); + + // real KeeAgent can only read UTF-16 + writer.setCodec(QTextCodec::codecForName("UTF-16")); + writer.setAutoFormatting(true); + writer.setAutoFormattingIndent(2); + + writer.writeStartDocument(); + + writer.writeStartElement("EntrySettings"); + writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); + writer.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + + writer.writeTextElement("AllowUseOfSshKey", m_allowUseOfSshKey ? "true" : "false"); + writer.writeTextElement("AddAtDatabaseOpen", m_addAtDatabaseOpen ? "true" : "false"); + writer.writeTextElement("RemoveAtDatabaseClose", m_removeAtDatabaseClose ? "true" : "false"); + writer.writeTextElement("UseConfirmConstraintWhenAdding", m_useConfirmConstraintWhenAdding ? "true" : "false"); + writer.writeTextElement("UseLifetimeConstraintWhenAdding", m_useLifetimeConstraintWhenAdding ? "true" : "false"); + writer.writeTextElement("LifetimeConstraintDuration", QString::number(m_lifetimeConstraintDuration)); + + writer.writeStartElement("Location"); + writer.writeTextElement("SelectedType", m_selectedType); + + if (!m_attachmentName.isEmpty()) { + writer.writeTextElement("AttachmentName", m_attachmentName); + } else { + writer.writeEmptyElement("AttachmentName"); + } + + writer.writeTextElement("SaveAttachmentToTempFile", m_saveAttachmentToTempFile ? "true" : "false"); + + if (!m_fileName.isEmpty()) { + writer.writeTextElement("FileName", m_fileName); + } else { + writer.writeEmptyElement("FileName"); + } + + writer.writeEndElement(); // Location + writer.writeEndElement(); // EntrySettings + writer.writeEndDocument(); + + return ba; +} diff --git a/src/sshagent/KeeAgentSettings.h b/src/sshagent/KeeAgentSettings.h new file mode 100644 index 00000000..4022750d --- /dev/null +++ b/src/sshagent/KeeAgentSettings.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 KEEAGENTSETTINGS_H +#define KEEAGENTSETTINGS_H + +#include +#include + +class KeeAgentSettings +{ +public: + KeeAgentSettings(); + + bool operator==(KeeAgentSettings& other); + bool operator!=(KeeAgentSettings& other); + bool isDefault(); + + bool fromXml(const QByteArray &ba); + QByteArray toXml(); + + bool allowUseOfSshKey() const; + bool addAtDatabaseOpen() const; + bool removeAtDatabaseClose() const; + bool useConfirmConstraintWhenAdding() const; + bool useLifetimeConstraintWhenAdding() const; + int lifetimeConstraintDuration() const; + + const QString selectedType() const; + const QString attachmentName() const; + bool saveAttachmentToTempFile() const; + const QString fileName() const; + + void setAllowUseOfSshKey(bool allowUseOfSshKey); + void setAddAtDatabaseOpen(bool addAtDatabaseOpen); + void setRemoveAtDatabaseClose(bool removeAtDatabaseClose); + void setUseConfirmConstraintWhenAdding(bool useConfirmConstraintWhenAdding); + void setUseLifetimeConstraintWhenAdding(bool useLifetimeConstraintWhenAdding); + void setLifetimeConstraintDuration(int lifetimeConstraintDuration); + + void setSelectedType(const QString& type); + void setAttachmentName(const QString& attachmentName); + void setSaveAttachmentToTempFile(bool); + void setFileName(const QString& fileName); + +private: + bool readBool(QXmlStreamReader& reader); + int readInt(QXmlStreamReader& reader); + + bool m_allowUseOfSshKey; + bool m_addAtDatabaseOpen; + bool m_removeAtDatabaseClose; + bool m_useConfirmConstraintWhenAdding; + bool m_useLifetimeConstraintWhenAdding; + int m_lifetimeConstraintDuration; + + // location + QString m_selectedType; + QString m_attachmentName; + bool m_saveAttachmentToTempFile; + QString m_fileName; +}; + +#endif // KEEAGENTSETTINGS_H diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp new file mode 100644 index 00000000..2c51ee45 --- /dev/null +++ b/src/sshagent/OpenSSHKey.cpp @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 . + */ + +#include "OpenSSHKey.h" +#include +#include +#include +#include "crypto/SymmetricCipher.h" + +// bcrypt_pbkdf.cpp +int bcrypt_pbkdf(const QByteArray& pass, const QByteArray& salt, QByteArray& key, quint32 rounds); + +OpenSSHKey::OpenSSHKey(QObject *parent) + : QObject(parent) + , m_type(QString()) + , m_cipherName(QString("none")) + , m_kdfName(QString("none")) + , m_kdfOptions(QByteArray()) + , m_rawPrivateData(QByteArray()) + , m_publicData(QList()) + , m_privateData(QList()) + , m_comment(QString()) + , m_error(QString()) +{ + +} + +OpenSSHKey::OpenSSHKey(const OpenSSHKey& other) + : QObject(nullptr) + , m_type(other.m_type) + , m_cipherName(other.m_cipherName) + , m_kdfName(other.m_kdfName) + , m_kdfOptions(other.m_kdfOptions) + , m_rawPrivateData(other.m_rawPrivateData) + , m_publicData(other.m_publicData) + , m_privateData(other.m_privateData) + , m_comment(other.m_comment) + , m_error(other.m_error) +{ + +} + +bool OpenSSHKey::operator==(const OpenSSHKey& other) const +{ + // close enough for now + return (fingerprint() == other.fingerprint()); +} + +const QString OpenSSHKey::cipherName() const +{ + return m_cipherName; +} + +const QString OpenSSHKey::type() const +{ + return m_type; +} + +int OpenSSHKey::keyLength() const +{ + if (m_type == "ssh-dss" && m_publicData.length() == 4) { + return (m_publicData[0].length() - 1) * 8; + } else if (m_type == "ssh-rsa" && m_publicData.length() == 2) { + return (m_publicData[1].length() - 1) * 8; + } else if (m_type.startsWith("ecdsa-sha2-") && m_publicData.length() == 2) { + return (m_publicData[1].length() - 1) * 4; + } else if (m_type == "ssh-ed25519" && m_publicData.length() == 1) { + return m_publicData[0].length() * 8; + } + + return 0; +} + +const QString OpenSSHKey::fingerprint() const +{ + QByteArray publicKey; + BinaryStream stream(&publicKey); + + stream.writeString(m_type); + + for (QByteArray ba : m_publicData) { + stream.writeString(ba); + } + + QByteArray rawHash = QCryptographicHash::hash(publicKey, QCryptographicHash::Sha256); + + return "SHA256:" + QString::fromLatin1(rawHash.toBase64(QByteArray::OmitTrailingEquals)); +} + +const QString OpenSSHKey::comment() const +{ + return m_comment; +} + +const QString OpenSSHKey::publicKey() const +{ + QByteArray publicKey; + BinaryStream stream(&publicKey); + + stream.writeString(m_type); + + for (QByteArray ba : m_publicData) { + stream.writeString(ba); + } + + return m_type + " " + QString::fromLatin1(publicKey.toBase64()) + " " + m_comment; +} + +const QString OpenSSHKey::errorString() const +{ + return m_error; +} + +void OpenSSHKey::setType(const QString& type) +{ + m_type = type; +} + +void OpenSSHKey::setPublicData(const QList& data) +{ + m_publicData = data; +} + +void OpenSSHKey::setPrivateData(const QList& data) +{ + m_privateData = data; +} + +void OpenSSHKey::setComment(const QString& comment) +{ + m_comment = comment; +} + +void OpenSSHKey::clearPrivate() +{ + m_rawPrivateData.clear(); + m_privateData.clear(); +} + +bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out) +{ + QString pem = QString::fromLatin1(in); + QStringList rows = pem.split(QRegularExpression("(?:\r?\n|\r)"), QString::SkipEmptyParts); + + if (rows.length() < 3) { + m_error = tr("Invalid key file, expecting an OpenSSH key"); + return false; + } + + QString begin = rows.first(); + QString end = rows.last(); + + QRegularExpressionMatch beginMatch = QRegularExpression("^-----BEGIN ([^\\-]+)-----$").match(begin); + QRegularExpressionMatch endMatch = QRegularExpression("^-----END ([^\\-]+)-----$").match(end); + + if (!beginMatch.hasMatch() || !endMatch.hasMatch()) { + m_error = tr("Invalid key file, expecting an OpenSSH key"); + return false; + } + + if (beginMatch.captured(1) != endMatch.captured(1)) { + m_error = tr("PEM boundary mismatch"); + return false; + } + + if (beginMatch.captured(1) != "OPENSSH PRIVATE KEY") { + m_error = tr("This is not an OpenSSH key, only modern keys are supported"); + return false; + } + + rows.removeFirst(); + rows.removeLast(); + + out = QByteArray::fromBase64(rows.join("").toLatin1()); + + if (out.isEmpty()) { + m_error = tr("Base64 decoding failed"); + return false; + } + + return true; +} + +bool OpenSSHKey::parse(const QByteArray& in) +{ + QByteArray data; + + if (!parsePEM(in, data)) { + return false; + } + + BinaryStream stream(&data); + + QByteArray magic; + magic.resize(15); + + if (!stream.read(magic)) { + m_error = tr("Key file way too small."); + return false; + } + + if (QString::fromLatin1(magic) != "openssh-key-v1") { + m_error = tr("Key file magic header id invalid"); + return false; + } + + stream.readString(m_cipherName); + stream.readString(m_kdfName); + stream.readString(m_kdfOptions); + + quint32 numberOfKeys; + stream.read(numberOfKeys); + + if (numberOfKeys == 0) { + m_error = tr("Found zero keys"); + return false; + } + + for (quint32 i = 0; i < numberOfKeys; ++i) { + QByteArray publicKey; + if (!stream.readString(publicKey)) { + m_error = tr("Failed to read public key."); + return false; + } + + if (i == 0) { + BinaryStream publicStream(&publicKey); + if (!readPublic(publicStream)) { + return false; + } + } + } + + // padded list of keys + if (!stream.readString(m_rawPrivateData)) { + m_error = tr("Corrupted key file, reading private key failed"); + return false; + } + + // load private if no encryption + if (!encrypted()) { + return openPrivateKey(); + } + + return true; +} + +bool OpenSSHKey::encrypted() const +{ + return (m_cipherName != "none"); +} + +bool OpenSSHKey::openPrivateKey(const QString& passphrase) +{ + QScopedPointer cipher; + + if (!m_privateData.isEmpty()) { + return true; + } + + if (m_rawPrivateData.isEmpty()) { + m_error = tr("No private key payload to decrypt"); + return false; + } + + if (m_cipherName == "aes256-cbc") { + cipher.reset(new SymmetricCipher(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt)); + } else if (m_cipherName == "aes256-ctr") { + cipher.reset(new SymmetricCipher(SymmetricCipher::Aes256, SymmetricCipher::Ctr, SymmetricCipher::Decrypt)); + } else if (m_cipherName != "none") { + m_error = tr("Unknown cipher: ") + m_cipherName; + return false; + } + + if (m_kdfName == "bcrypt") { + if (!cipher) { + m_error = tr("Trying to run KDF without cipher"); + return false; + } + + if (passphrase.length() == 0) { + m_error = tr("Passphrase is required to decrypt this key"); + return false; + } + + BinaryStream optionStream(&m_kdfOptions); + + QByteArray salt; + quint32 rounds; + + optionStream.readString(salt); + optionStream.read(rounds); + + QByteArray decryptKey; + decryptKey.fill(0, cipher->keySize() + cipher->blockSize()); + + QByteArray phraseData = passphrase.toLatin1(); + if (bcrypt_pbkdf(phraseData, salt, decryptKey, rounds) < 0) { + m_error = tr("Key derivation failed, key file corrupted?"); + return false; + } + + QByteArray keyData, ivData; + keyData.setRawData(decryptKey.data(), cipher->keySize()); + ivData.setRawData(decryptKey.data() + cipher->keySize(), cipher->blockSize()); + + cipher->init(keyData, ivData); + } else if (m_kdfName != "none") { + m_error = tr("Unknown KDF: ") + m_kdfName; + return false; + } + + QByteArray rawPrivateData = m_rawPrivateData; + + if (cipher && cipher->isInitalized()) { + bool ok = false; + rawPrivateData = cipher->process(rawPrivateData, &ok); + if (!ok) { + m_error = tr("Decryption failed, wrong passphrase?"); + return false; + } + } + + BinaryStream keyStream(&rawPrivateData); + + quint32 checkInt1; + quint32 checkInt2; + + keyStream.read(checkInt1); + keyStream.read(checkInt2); + + if (checkInt1 != checkInt2) { + m_error = tr("Decryption failed, wrong passphrase?"); + return false; + } + + return readPrivate(keyStream); +} + +bool OpenSSHKey::readPublic(BinaryStream& stream) +{ + m_publicData.clear(); + + if (!stream.readString(m_type)) { + m_error = tr("Unexpected EOF while reading public key"); + return false; + } + + int keyParts; + if (m_type == "ssh-dss") { + keyParts = 4; + } else if (m_type == "ssh-rsa") { + keyParts = 2; + } else if (m_type.startsWith("ecdsa-sha2-")) { + keyParts = 2; + } else if (m_type == "ssh-ed25519") { + keyParts = 1; + } else { + m_error = tr("Unknown key type: ") + m_type; + return false; + } + + for (int i = 0; i < keyParts; ++i) { + QByteArray t; + + if (!stream.readString(t)) { + m_error = tr("Unexpected EOF while reading public key"); + return false; + } + + m_publicData.append(t); + } + + return true; +} + +bool OpenSSHKey::readPrivate(BinaryStream& stream) +{ + m_privateData.clear(); + + if (!stream.readString(m_type)) { + m_error = tr("Unexpected EOF while reading private key"); + return false; + } + + int keyParts; + if (m_type == "ssh-dss") { + keyParts = 5; + } else if (m_type == "ssh-rsa") { + keyParts = 6; + } else if (m_type.startsWith("ecdsa-sha2-")) { + keyParts = 3; + } else if (m_type == "ssh-ed25519") { + keyParts = 2; + } else { + m_error = tr("Unknown key type: ") + m_type; + return false; + } + + for (int i = 0; i < keyParts; ++i) { + QByteArray t; + + if (!stream.readString(t)) { + m_error = tr("Unexpected EOF while reading private key"); + return false; + } + + m_privateData.append(t); + } + + if (!stream.readString(m_comment)) { + m_error = tr("Unexpected EOF while reading private key"); + return false; + } + + return true; +} + +bool OpenSSHKey::writePublic(BinaryStream& stream) +{ + if (m_publicData.isEmpty()) { + m_error = tr("Can't write public key as it is empty"); + return false; + } + + if (!stream.writeString(m_type)) { + m_error = tr("Unexpected EOF when writing public key"); + return false; + } + + for (QByteArray t : m_publicData) { + if (!stream.writeString(t)) { + m_error = tr("Unexpected EOF when writing public key"); + return false; + } + } + + return true; +} + +bool OpenSSHKey::writePrivate(BinaryStream& stream) +{ + if (m_privateData.isEmpty()) { + m_error = tr("Can't write private key as it is empty"); + return false; + } + + if (!stream.writeString(m_type)) { + m_error = tr("Unexpected EOF when writing private key"); + return false; + } + + for (QByteArray t : m_privateData) { + if (!stream.writeString(t)) { + m_error = tr("Unexpected EOF when writing private key"); + return false; + } + } + + if (!stream.writeString(m_comment)) { + m_error = tr("Unexpected EOF when writing private key"); + return false; + } + + return true; +} + +uint qHash(const OpenSSHKey& key) +{ + return qHash(key.fingerprint()); +} diff --git a/src/sshagent/OpenSSHKey.h b/src/sshagent/OpenSSHKey.h new file mode 100644 index 00000000..eca6c9ed --- /dev/null +++ b/src/sshagent/OpenSSHKey.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 OPENSSHKEY_H +#define OPENSSHKEY_H + +#include +#include "BinaryStream.h" + +class OpenSSHKey : QObject +{ + Q_OBJECT +public: + explicit OpenSSHKey(QObject* parent = nullptr); + OpenSSHKey(const OpenSSHKey& other); + bool operator==(const OpenSSHKey& other) const; + + bool parse(const QByteArray& in); + bool encrypted() const; + bool openPrivateKey(const QString& passphrase = QString()); + + const QString cipherName() const; + const QString type() const; + int keyLength() const; + const QString fingerprint() const; + const QString comment() const; + const QString publicKey() const; + const QString errorString() const; + + void setType(const QString& type); + void setPublicData(const QList& data); + void setPrivateData(const QList& data); + void setComment(const QString& comment); + + void clearPrivate(); + + bool readPublic(BinaryStream& stream); + bool readPrivate(BinaryStream& stream); + bool writePublic(BinaryStream& stream); + bool writePrivate(BinaryStream& stream); + +private: + bool parsePEM(const QByteArray& in, QByteArray& out); + + QString m_type; + QString m_cipherName; + QString m_kdfName; + QByteArray m_kdfOptions; + QByteArray m_rawPrivateData; + QList m_publicData; + QList m_privateData; + QString m_comment; + QString m_error; +}; + +uint qHash(const OpenSSHKey& key); + +#endif // OPENSSHKEY_H diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp new file mode 100644 index 00000000..7969fe0c --- /dev/null +++ b/src/sshagent/SSHAgent.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 . + */ + +#include "SSHAgent.h" +#include "BinaryStream.h" +#include "KeeAgentSettings.h" + +#ifndef Q_OS_WIN +#include +#else +#include +#endif + +SSHAgent* SSHAgent::m_instance; + +SSHAgent::SSHAgent(QObject* parent) : QObject(parent) +{ +#ifndef Q_OS_WIN + m_socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); +#endif +} + +SSHAgent::~SSHAgent() +{ + for (QSet keys : m_keys.values()) { + for (OpenSSHKey key : keys) { + removeIdentity(key); + } + } +} + +SSHAgent* SSHAgent::instance() +{ + if (m_instance == nullptr) { + qFatal("Race condition: instance wanted before it was initialized, this is a bug."); + } + + return m_instance; +} + +void SSHAgent::init(QObject* parent) +{ + m_instance = new SSHAgent(parent); +} + +bool SSHAgent::isAgentRunning() const +{ +#ifndef Q_OS_WIN + return !m_socketPath.isEmpty(); +#else + return (FindWindowA("Pageant", "Pageant") != nullptr); +#endif +} + +bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out) const +{ +#ifndef Q_OS_WIN + QLocalSocket socket; + BinaryStream stream(&socket); + + socket.connectToServer(m_socketPath); + if (!socket.waitForConnected(500)) { + return false; + } + + stream.writeString(in); + stream.flush(); + + if (!stream.readString(out)) { + return false; + } + + socket.close(); + + return true; +#else + HWND hWnd = FindWindowA("Pageant", "Pageant"); + + if (!hWnd) { + return false; + } + + if (static_cast(in.length()) > AGENT_MAX_MSGLEN - 4) { + return false; + } + + QByteArray mapName = (QString("SSHAgentRequest") + reinterpret_cast(QThread::currentThreadId())).toLatin1(); + + HANDLE handle = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapName.data()); + + if (!handle) { + return false; + } + + LPVOID ptr = MapViewOfFile(handle, FILE_MAP_WRITE, 0, 0, 0); + + if (!ptr) { + CloseHandle(handle); + return false; + } + + quint32 *requestLength = reinterpret_cast(ptr); + void *requestData = reinterpret_cast(reinterpret_cast(ptr) + 4); + + *requestLength = qToBigEndian(in.length()); + memcpy(requestData, in.data(), in.length()); + + COPYDATASTRUCT data; + data.dwData = AGENT_COPYDATA_ID; + data.cbData = mapName.length() + 1; + data.lpData = reinterpret_cast(mapName.data()); + + LRESULT res = SendMessageA(hWnd, WM_COPYDATA, 0, reinterpret_cast(&data)); + + if (res) { + quint32 responseLength = qFromBigEndian(*requestLength); + if (responseLength <= AGENT_MAX_MSGLEN) { + out.resize(responseLength); + memcpy(out.data(), requestData, responseLength); + } + } + + UnmapViewOfFile(ptr); + CloseHandle(handle); + + return (res > 0); +#endif +} + + +bool SSHAgent::addIdentity(OpenSSHKey& key, quint32 lifetime, bool confirm) const +{ + QByteArray requestData; + BinaryStream request(&requestData); + + request.write((lifetime > 0 || confirm) ? SSH_AGENTC_ADD_ID_CONSTRAINED : SSH_AGENTC_ADD_IDENTITY); + key.writePrivate(request); + + if (lifetime > 0) { + request.write(SSH_AGENT_CONSTRAIN_LIFETIME); + request.write(lifetime); + } + + if (confirm) { + request.write(SSH_AGENT_CONSTRAIN_CONFIRM); + } + + QByteArray responseData; + sendMessage(requestData, responseData); + + if (responseData.length() < 1 || static_cast(responseData[0]) != SSH_AGENT_SUCCESS) { + return false; + } + + return true; +} + +bool SSHAgent::removeIdentity(OpenSSHKey& key) const +{ + QByteArray requestData; + BinaryStream request(&requestData); + + QByteArray keyData; + BinaryStream keyStream(&keyData); + key.writePublic(keyStream); + + request.write(SSH_AGENTC_REMOVE_IDENTITY); + request.writeString(keyData); + + QByteArray responseData; + sendMessage(requestData, responseData); + + if (responseData.length() < 1 || static_cast(responseData[0]) != SSH_AGENT_SUCCESS) { + return false; + } + + return true; +} + +void SSHAgent::removeIdentityAtLock(const OpenSSHKey& key, const Uuid& uuid) +{ + OpenSSHKey copy = key; + copy.clearPrivate(); + m_keys[uuid.toHex()].insert(copy); +} + +void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode) +{ + DatabaseWidget* widget = qobject_cast(sender()); + + if (widget == nullptr) { + return; + } + + Uuid uuid = widget->database()->uuid(); + + if (mode == DatabaseWidget::LockedMode && m_keys.contains(uuid.toHex())) { + QSet keys = m_keys.take(uuid.toHex()); + for (OpenSSHKey key : keys) { + removeIdentity(key); + } + } else if (mode == DatabaseWidget::ViewMode && !m_keys.contains(uuid.toHex())) { + for (Entry* e : widget->database()->rootGroup()->entriesRecursive()) { + + if (!e->attachments()->hasKey("KeeAgent.settings")) + continue; + + KeeAgentSettings settings; + settings.fromXml(e->attachments()->value("KeeAgent.settings")); + + if (!settings.allowUseOfSshKey()) { + continue; + } + + QByteArray keyData; + if (settings.selectedType() == "attachment") { + keyData = e->attachments()->value(settings.attachmentName()); + } else if (!settings.fileName().isEmpty()) { + QFile file(settings.fileName()); + + if (file.size() > 1024 * 1024) { + continue; + } + + if (!file.open(QIODevice::ReadOnly)) { + continue; + } + + keyData = file.readAll(); + } + + if (keyData.isEmpty()) { + continue; + } + + OpenSSHKey key; + + if (!key.parse(keyData)) { + continue; + } + + if (settings.removeAtDatabaseClose()) { + removeIdentityAtLock(key, uuid); + } + + if (settings.addAtDatabaseOpen() && key.openPrivateKey(e->password())) { + int lifetime = 0; + + if (settings.useLifetimeConstraintWhenAdding()) { + lifetime = settings.lifetimeConstraintDuration(); + } + + addIdentity(key, lifetime, settings.useConfirmConstraintWhenAdding()); + } + } + } +} diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h new file mode 100644 index 00000000..078ff7b0 --- /dev/null +++ b/src/sshagent/SSHAgent.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 Toni Spets + * 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 AGENTCLIENT_H +#define AGENTCLIENT_H + +#include +#include +#include "OpenSSHKey.h" + +#include "gui/DatabaseWidget.h" + +class SSHAgent : public QObject +{ + Q_OBJECT + +public: + static SSHAgent* instance(); + static void init(QObject* parent); + + bool isAgentRunning() const; + bool addIdentity(OpenSSHKey& key, quint32 lifetime = 0, bool confirm = false) const; + bool removeIdentity(OpenSSHKey& key) const; + void removeIdentityAtLock(const OpenSSHKey& key, const Uuid& uuid); + +public slots: + void databaseModeChanged(DatabaseWidget::Mode mode = DatabaseWidget::LockedMode); + +private: + const quint8 SSH_AGENT_FAILURE = 5; + const quint8 SSH_AGENT_SUCCESS = 6; + const quint8 SSH_AGENTC_REQUEST_IDENTITIES = 11; + const quint8 SSH_AGENT_IDENTITIES_ANSWER = 12; + const quint8 SSH_AGENTC_ADD_IDENTITY = 17; + const quint8 SSH_AGENTC_REMOVE_IDENTITY = 18; + const quint8 SSH_AGENTC_ADD_ID_CONSTRAINED = 25; + + const quint8 SSH_AGENT_CONSTRAIN_LIFETIME = 1; + const quint8 SSH_AGENT_CONSTRAIN_CONFIRM = 2; + + explicit SSHAgent(QObject* parent = nullptr); + ~SSHAgent(); + + bool sendMessage(const QByteArray& in, QByteArray& out) const; + + static SSHAgent* m_instance; + +#ifndef Q_OS_WIN + QString m_socketPath; +#else + const quint32 AGENT_MAX_MSGLEN = 8192; + const quint32 AGENT_COPYDATA_ID = 0x804e50ba; +#endif + + QMap> m_keys; +}; + +#endif // AGENTCLIENT_H diff --git a/src/sshagent/bcrypt_pbkdf.cpp b/src/sshagent/bcrypt_pbkdf.cpp new file mode 100644 index 00000000..fed4cdb2 --- /dev/null +++ b/src/sshagent/bcrypt_pbkdf.cpp @@ -0,0 +1,172 @@ +/* $OpenBSD: bcrypt_pbkdf.c,v 1.13 2015/01/12 03:20:04 tedu Exp $ */ +/* + * Copyright (c) 2013 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +extern "C" { +#include "blf.h" +} + +#define MINIMUM(a,b) (((a) < (b)) ? (a) : (b)) + +/* + * pkcs #5 pbkdf2 implementation using the "bcrypt" hash + * + * The bcrypt hash function is derived from the bcrypt password hashing + * function with the following modifications: + * 1. The input password and salt are preprocessed with SHA512. + * 2. The output length is expanded to 256 bits. + * 3. Subsequently the magic string to be encrypted is lengthened and modifed + * to "OxychromaticBlowfishSwatDynamite" + * 4. The hash function is defined to perform 64 rounds of initial state + * expansion. (More rounds are performed by iterating the hash.) + * + * Note that this implementation pulls the SHA512 operations into the caller + * as a performance optimization. + * + * One modification from official pbkdf2. Instead of outputting key material + * linearly, we mix it. pbkdf2 has a known weakness where if one uses it to + * generate (e.g.) 512 bits of key material for use as two 256 bit keys, an + * attacker can merely run once through the outer loop, but the user + * always runs it twice. Shuffling output bytes requires computing the + * entirety of the key material to assemble any subkey. This is something a + * wise caller could do; we just do it for you. + */ + +#define BCRYPT_WORDS 8 +#define BCRYPT_HASHSIZE (BCRYPT_WORDS * 4) +#define SHA512_DIGEST_LENGTH 64 + +// FIXME: explicit_bzero exists to ensure bzero is not optimized out +#define explicit_bzero bzero + +static void +bcrypt_hash(const quint8* sha2pass, const quint8* sha2salt, quint8* out) +{ + blf_ctx state; + quint8 ciphertext[BCRYPT_HASHSIZE] = // "OxychromaticBlowfishSwatDynamite" + { 0x4f, 0x78, 0x79, 0x63, 0x68, 0x72, 0x6f, 0x6d, + 0x61, 0x74, 0x69, 0x63, 0x42, 0x6c, 0x6f, 0x77, + 0x66, 0x69, 0x73, 0x68, 0x53, 0x77, 0x61, 0x74, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x74, 0x65 }; + quint32 cdata[BCRYPT_WORDS]; + int i; + quint16 j; + size_t shalen = SHA512_DIGEST_LENGTH; + + /* key expansion */ + Blowfish_initstate(&state); + Blowfish_expandstate(&state, sha2salt, shalen, sha2pass, shalen); + for (i = 0; i < 64; i++) { + Blowfish_expand0state(&state, sha2salt, shalen); + Blowfish_expand0state(&state, sha2pass, shalen); + } + + /* encryption */ + j = 0; + for (i = 0; i < BCRYPT_WORDS; i++) + cdata[i] = Blowfish_stream2word(ciphertext, sizeof(ciphertext), + &j); + for (i = 0; i < 64; i++) + blf_enc(&state, cdata, sizeof(cdata) / sizeof(uint64_t)); + + /* copy out */ + for (i = 0; i < BCRYPT_WORDS; i++) { + out[4 * i + 3] = (cdata[i] >> 24) & 0xff; + out[4 * i + 2] = (cdata[i] >> 16) & 0xff; + out[4 * i + 1] = (cdata[i] >> 8) & 0xff; + out[4 * i + 0] = cdata[i] & 0xff; + } + + /* zap */ + explicit_bzero(ciphertext, sizeof(ciphertext)); + explicit_bzero(cdata, sizeof(cdata)); + explicit_bzero(&state, sizeof(state)); +} + +int bcrypt_pbkdf(const QByteArray& pass, const QByteArray& salt, QByteArray& key, quint32 rounds) +{ + QCryptographicHash ctx(QCryptographicHash::Sha512); + QByteArray sha2pass; + QByteArray sha2salt; + quint8 out[BCRYPT_HASHSIZE]; + quint8 tmpout[BCRYPT_HASHSIZE]; + quint8 countsalt[4]; + + /* nothing crazy */ + if (rounds < 1) { + return -1; + } + + if (pass.isEmpty() || salt.isEmpty() || key.isEmpty() || + static_cast(key.length()) > sizeof(out) * sizeof(out)) { + return -1; + } + + quint32 stride = (key.length() + sizeof(out) - 1) / sizeof(out); + quint32 amt = (key.length() + stride - 1) / stride; + + /* collapse password */ + ctx.reset(); + ctx.addData(pass); + sha2pass = ctx.result(); + + /* generate key, sizeof(out) at a time */ + for (quint32 count = 1, keylen = key.length(); keylen > 0; count++) { + countsalt[0] = (count >> 24) & 0xff; + countsalt[1] = (count >> 16) & 0xff; + countsalt[2] = (count >> 8) & 0xff; + countsalt[3] = count & 0xff; + + /* first round, salt is salt */ + ctx.reset(); + ctx.addData(salt); + ctx.addData(reinterpret_cast(countsalt), sizeof(countsalt)); + sha2salt = ctx.result(); + + bcrypt_hash(reinterpret_cast(sha2pass.data()), reinterpret_cast(sha2salt.data()), tmpout); + memcpy(out, tmpout, sizeof(out)); + + for (quint32 i = 1; i < rounds; i++) { + /* subsequent rounds, salt is previous output */ + ctx.reset(); + ctx.addData(reinterpret_cast(tmpout), sizeof(tmpout)); + sha2salt = ctx.result(); + bcrypt_hash(reinterpret_cast(sha2pass.data()), reinterpret_cast(sha2salt.data()), tmpout); + for (quint32 j = 0; j < sizeof(out); j++) + out[j] ^= tmpout[j]; + } + + /* + * pbkdf2 deviation: output the key material non-linearly. + */ + amt = MINIMUM(amt, keylen); + quint32 i; + for (i = 0; i < amt; i++) { + int dest = i * stride + (count - 1); + if (dest >= key.length()) + break; + key.data()[dest] = out[i]; + } + keylen -= i; + } + + /* zap */ + explicit_bzero(out, sizeof(out)); + + return 0; +} diff --git a/src/sshagent/blf.h b/src/sshagent/blf.h new file mode 100644 index 00000000..4878e558 --- /dev/null +++ b/src/sshagent/blf.h @@ -0,0 +1,98 @@ +/* $OpenBSD: blf.h,v 1.7 2007/03/14 17:59:41 grunk Exp $ */ +/* + * Blowfish - a fast block cipher designed by Bruce Schneier + * + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by Niels Provos. + * 4. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _BLF_H_ +#define _BLF_H_ + +#ifdef _WIN32 + +#include + +typedef uint32_t u_int32_t; +typedef uint16_t u_int16_t; +typedef uint8_t u_int8_t; + +#define bzero(p,s) memset(p, 0, s) + +#endif + +#if !defined(HAVE_BCRYPT_PBKDF) && !defined(HAVE_BLH_H) + +/* Schneier specifies a maximum key length of 56 bytes. + * This ensures that every key bit affects every cipher + * bit. However, the subkeys can hold up to 72 bytes. + * Warning: For normal blowfish encryption only 56 bytes + * of the key affect all cipherbits. + */ + +#define BLF_N 16 /* Number of Subkeys */ +#define BLF_MAXKEYLEN ((BLF_N-2)*4) /* 448 bits */ +#define BLF_MAXUTILIZED ((BLF_N+2)*4) /* 576 bits */ + +/* Blowfish context */ +typedef struct BlowfishContext { + u_int32_t S[4][256]; /* S-Boxes */ + u_int32_t P[BLF_N + 2]; /* Subkeys */ +} blf_ctx; + +/* Raw access to customized Blowfish + * blf_key is just: + * Blowfish_initstate( state ) + * Blowfish_expand0state( state, key, keylen ) + */ + +void Blowfish_encipher(blf_ctx *, u_int32_t *, u_int32_t *); +void Blowfish_decipher(blf_ctx *, u_int32_t *, u_int32_t *); +void Blowfish_initstate(blf_ctx *); +void Blowfish_expand0state(blf_ctx *, const u_int8_t *, u_int16_t); +void Blowfish_expandstate +(blf_ctx *, const u_int8_t *, u_int16_t, const u_int8_t *, u_int16_t); + +/* Standard Blowfish */ + +void blf_key(blf_ctx *, const u_int8_t *, u_int16_t); +void blf_enc(blf_ctx *, u_int32_t *, u_int16_t); +void blf_dec(blf_ctx *, u_int32_t *, u_int16_t); + +void blf_ecb_encrypt(blf_ctx *, u_int8_t *, u_int32_t); +void blf_ecb_decrypt(blf_ctx *, u_int8_t *, u_int32_t); + +void blf_cbc_encrypt(blf_ctx *, u_int8_t *, u_int8_t *, u_int32_t); +void blf_cbc_decrypt(blf_ctx *, u_int8_t *, u_int8_t *, u_int32_t); + +/* Converts u_int8_t to u_int32_t */ +u_int32_t Blowfish_stream2word(const u_int8_t *, u_int16_t , u_int16_t *); + +#endif /* !defined(HAVE_BCRYPT_PBKDF) && !defined(HAVE_BLH_H) */ +#endif /* _BLF_H */ + diff --git a/src/sshagent/blowfish.c b/src/sshagent/blowfish.c new file mode 100644 index 00000000..02e9ac0b --- /dev/null +++ b/src/sshagent/blowfish.c @@ -0,0 +1,696 @@ +/* $OpenBSD: blowfish.c,v 1.18 2004/11/02 17:23:26 hshoexer Exp $ */ +/* + * Blowfish block cipher for OpenBSD + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Implementation advice by David Mazieres . + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by Niels Provos. + * 4. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * This code is derived from section 14.3 and the given source + * in section V of Applied Cryptography, second edition. + * Blowfish is an unpatented fast block cipher designed by + * Bruce Schneier. + */ + +#define HAVE_BLF_H + +#if !defined(HAVE_BCRYPT_PBKDF) && (!defined(HAVE_BLOWFISH_INITSTATE) || \ + !defined(HAVE_BLOWFISH_EXPAND0STATE) || !defined(HAVE_BLF_ENC)) + +#if 0 +#include /* used for debugging */ +#include +#endif + +#include +#ifdef HAVE_BLF_H +#include "blf.h" +#endif + +#undef inline +#ifdef __GNUC__ +#define inline __inline +#else /* !__GNUC__ */ +#define inline +#endif /* !__GNUC__ */ + +/* Function for Feistel Networks */ + +#define F(s, x) ((((s)[ (((x)>>24)&0xFF)] \ + + (s)[0x100 + (((x)>>16)&0xFF)]) \ + ^ (s)[0x200 + (((x)>> 8)&0xFF)]) \ + + (s)[0x300 + ( (x) &0xFF)]) + +#define BLFRND(s,p,i,j,n) (i ^= F(s,j) ^ (p)[n]) + +void +Blowfish_encipher(blf_ctx *c, u_int32_t *xl, u_int32_t *xr) +{ + u_int32_t Xl; + u_int32_t Xr; + u_int32_t *s = c->S[0]; + u_int32_t *p = c->P; + + Xl = *xl; + Xr = *xr; + + Xl ^= p[0]; + BLFRND(s, p, Xr, Xl, 1); BLFRND(s, p, Xl, Xr, 2); + BLFRND(s, p, Xr, Xl, 3); BLFRND(s, p, Xl, Xr, 4); + BLFRND(s, p, Xr, Xl, 5); BLFRND(s, p, Xl, Xr, 6); + BLFRND(s, p, Xr, Xl, 7); BLFRND(s, p, Xl, Xr, 8); + BLFRND(s, p, Xr, Xl, 9); BLFRND(s, p, Xl, Xr, 10); + BLFRND(s, p, Xr, Xl, 11); BLFRND(s, p, Xl, Xr, 12); + BLFRND(s, p, Xr, Xl, 13); BLFRND(s, p, Xl, Xr, 14); + BLFRND(s, p, Xr, Xl, 15); BLFRND(s, p, Xl, Xr, 16); + + *xl = Xr ^ p[17]; + *xr = Xl; +} + +void +Blowfish_decipher(blf_ctx *c, u_int32_t *xl, u_int32_t *xr) +{ + u_int32_t Xl; + u_int32_t Xr; + u_int32_t *s = c->S[0]; + u_int32_t *p = c->P; + + Xl = *xl; + Xr = *xr; + + Xl ^= p[17]; + BLFRND(s, p, Xr, Xl, 16); BLFRND(s, p, Xl, Xr, 15); + BLFRND(s, p, Xr, Xl, 14); BLFRND(s, p, Xl, Xr, 13); + BLFRND(s, p, Xr, Xl, 12); BLFRND(s, p, Xl, Xr, 11); + BLFRND(s, p, Xr, Xl, 10); BLFRND(s, p, Xl, Xr, 9); + BLFRND(s, p, Xr, Xl, 8); BLFRND(s, p, Xl, Xr, 7); + BLFRND(s, p, Xr, Xl, 6); BLFRND(s, p, Xl, Xr, 5); + BLFRND(s, p, Xr, Xl, 4); BLFRND(s, p, Xl, Xr, 3); + BLFRND(s, p, Xr, Xl, 2); BLFRND(s, p, Xl, Xr, 1); + + *xl = Xr ^ p[0]; + *xr = Xl; +} + +void +Blowfish_initstate(blf_ctx *c) +{ + /* P-box and S-box tables initialized with digits of Pi */ + + static const blf_ctx initstate = + { { + { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a}, + { + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7}, + { + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0}, + { + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6} + }, + { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + } }; + + *c = initstate; +} + +u_int32_t +Blowfish_stream2word(const u_int8_t *data, u_int16_t databytes, + u_int16_t *current) +{ + u_int8_t i; + u_int16_t j; + u_int32_t temp; + + temp = 0x00000000; + j = *current; + + for (i = 0; i < 4; i++, j++) { + if (j >= databytes) + j = 0; + temp = (temp << 8) | data[j]; + } + + *current = j; + return temp; +} + +void +Blowfish_expand0state(blf_ctx *c, const u_int8_t *key, u_int16_t keybytes) +{ + u_int16_t i; + u_int16_t j; + u_int16_t k; + u_int32_t temp; + u_int32_t datal; + u_int32_t datar; + + j = 0; + for (i = 0; i < BLF_N + 2; i++) { + /* Extract 4 int8 to 1 int32 from keystream */ + temp = Blowfish_stream2word(key, keybytes, &j); + c->P[i] = c->P[i] ^ temp; + } + + j = 0; + datal = 0x00000000; + datar = 0x00000000; + for (i = 0; i < BLF_N + 2; i += 2) { + Blowfish_encipher(c, &datal, &datar); + + c->P[i] = datal; + c->P[i + 1] = datar; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + Blowfish_encipher(c, &datal, &datar); + + c->S[i][k] = datal; + c->S[i][k + 1] = datar; + } + } +} + + +void +Blowfish_expandstate(blf_ctx *c, const u_int8_t *data, u_int16_t databytes, + const u_int8_t *key, u_int16_t keybytes) +{ + u_int16_t i; + u_int16_t j; + u_int16_t k; + u_int32_t temp; + u_int32_t datal; + u_int32_t datar; + + j = 0; + for (i = 0; i < BLF_N + 2; i++) { + /* Extract 4 int8 to 1 int32 from keystream */ + temp = Blowfish_stream2word(key, keybytes, &j); + c->P[i] = c->P[i] ^ temp; + } + + j = 0; + datal = 0x00000000; + datar = 0x00000000; + for (i = 0; i < BLF_N + 2; i += 2) { + datal ^= Blowfish_stream2word(data, databytes, &j); + datar ^= Blowfish_stream2word(data, databytes, &j); + Blowfish_encipher(c, &datal, &datar); + + c->P[i] = datal; + c->P[i + 1] = datar; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + datal ^= Blowfish_stream2word(data, databytes, &j); + datar ^= Blowfish_stream2word(data, databytes, &j); + Blowfish_encipher(c, &datal, &datar); + + c->S[i][k] = datal; + c->S[i][k + 1] = datar; + } + } + +} + +void +blf_key(blf_ctx *c, const u_int8_t *k, u_int16_t len) +{ + /* Initialize S-boxes and subkeys with Pi */ + Blowfish_initstate(c); + + /* Transform S-boxes and subkeys with key */ + Blowfish_expand0state(c, k, len); +} + +void +blf_enc(blf_ctx *c, u_int32_t *data, u_int16_t blocks) +{ + u_int32_t *d; + u_int16_t i; + + d = data; + for (i = 0; i < blocks; i++) { + Blowfish_encipher(c, d, d + 1); + d += 2; + } +} + +void +blf_dec(blf_ctx *c, u_int32_t *data, u_int16_t blocks) +{ + u_int32_t *d; + u_int16_t i; + + d = data; + for (i = 0; i < blocks; i++) { + Blowfish_decipher(c, d, d + 1); + d += 2; + } +} + +void +blf_ecb_encrypt(blf_ctx *c, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int32_t i; + + for (i = 0; i < len; i += 8) { + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_encipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + data += 8; + } +} + +void +blf_ecb_decrypt(blf_ctx *c, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int32_t i; + + for (i = 0; i < len; i += 8) { + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_decipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + data += 8; + } +} + +void +blf_cbc_encrypt(blf_ctx *c, u_int8_t *iv, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int32_t i, j; + + for (i = 0; i < len; i += 8) { + for (j = 0; j < 8; j++) + data[j] ^= iv[j]; + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_encipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + iv = data; + data += 8; + } +} + +void +blf_cbc_decrypt(blf_ctx *c, u_int8_t *iva, u_int8_t *data, u_int32_t len) +{ + u_int32_t l, r; + u_int8_t *iv; + u_int32_t i, j; + + iv = data + len - 16; + data = data + len - 8; + for (i = len - 8; i >= 8; i -= 8) { + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_decipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + for (j = 0; j < 8; j++) + data[j] ^= iv[j]; + iv -= 8; + data -= 8; + } + l = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + r = data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]; + Blowfish_decipher(c, &l, &r); + data[0] = l >> 24 & 0xff; + data[1] = l >> 16 & 0xff; + data[2] = l >> 8 & 0xff; + data[3] = l & 0xff; + data[4] = r >> 24 & 0xff; + data[5] = r >> 16 & 0xff; + data[6] = r >> 8 & 0xff; + data[7] = r & 0xff; + for (j = 0; j < 8; j++) + data[j] ^= iva[j]; +} + +#if 0 +void +report(u_int32_t data[], u_int16_t len) +{ + u_int16_t i; + for (i = 0; i < len; i += 2) + printf("Block %0hd: %08lx %08lx.\n", + i / 2, data[i], data[i + 1]); +} +void +main(void) +{ + + blf_ctx c; + char key[] = "AAAAA"; + char key2[] = "abcdefghijklmnopqrstuvwxyz"; + + u_int32_t data[10]; + u_int32_t data2[] = + {0x424c4f57l, 0x46495348l}; + + u_int16_t i; + + /* First test */ + for (i = 0; i < 10; i++) + data[i] = i; + + blf_key(&c, (u_int8_t *) key, 5); + blf_enc(&c, data, 5); + blf_dec(&c, data, 1); + blf_dec(&c, data + 2, 4); + printf("Should read as 0 - 9.\n"); + report(data, 10); + + /* Second test */ + blf_key(&c, (u_int8_t *) key2, strlen(key2)); + blf_enc(&c, data2, 1); + printf("\nShould read as: 0x324ed0fe 0xf413a203.\n"); + report(data2, 2); + blf_dec(&c, data2, 1); + report(data2, 2); +} +#endif + +#endif /* !defined(HAVE_BCRYPT_PBKDF) && (!defined(HAVE_BLOWFISH_INITSTATE) || \ + !defined(HAVE_BLOWFISH_EXPAND0STATE) || !defined(HAVE_BLF_ENC)) */ + diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp index 26716c09..f102335a 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -18,24 +18,57 @@ #include "totp.h" #include "core/Base32.h" -#include -#include -#include -#include #include +#include #include +#include #include #include +#include +#include +#include +const quint8 Totp::defaultStep = 30; +const quint8 Totp::defaultDigits = 6; -const quint8 QTotp::defaultStep = 30; -const quint8 QTotp::defaultDigits = 6; +/** + * Custom encoder types. Each should be unique and >= 128 and < 255 + * Values have no meaning outside of keepassxc + */ +/** + * Encoder for Steam Guard TOTP + */ +const quint8 Totp::ENCODER_STEAM = 254; -QTotp::QTotp() +const Totp::Encoder Totp::defaultEncoder = { "", "", "0123456789", 0, 0, false }; +const QMap Totp::encoders{ + { Totp::ENCODER_STEAM, { "steam", "S", "23456789BCDFGHJKMNPQRTVWXY", 5, 30, true } }, +}; + +/** + * These map the second field of the "TOTP Settings" field to our internal encoder number + * that overloads the digits field. Make sure that the key matches the shortName value + * in the corresponding Encoder + * NOTE: when updating this map, a corresponding edit to the settings regex must be made + * in Entry::totpSeed() + */ +const QMap Totp::shortNameToEncoder{ + { "S", Totp::ENCODER_STEAM }, +}; +/** + * These map the "encoder=" URL parameter of the "otp" field to our internal encoder number + * that overloads the digits field. Make sure that the key matches the name value + * in the corresponding Encoder + */ +const QMap Totp::nameToEncoder{ + { "steam", Totp::ENCODER_STEAM }, +}; + +Totp::Totp() { } -QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step) +QString Totp::parseOtpString(QString key, quint8& digits, quint8& step) { QUrl url(key); @@ -57,8 +90,10 @@ QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step) if (q_step > 0 && q_step <= 60) { step = q_step; } - - + QString encName = query.queryItemValue("encoder"); + if (!encName.isEmpty() && nameToEncoder.contains(encName)) { + digits = nameToEncoder[encName]; + } } else { // Compatibility with "KeeOtp" plugin string format QRegExp rx("key=(.+)", Qt::CaseInsensitive, QRegExp::RegExp); @@ -93,30 +128,73 @@ QString QTotp::parseOtpString(QString key, quint8 &digits, quint8 &step) return seed; } -QString QTotp::generateTotp(const QByteArray key, quint64 time, - const quint8 numDigits = defaultDigits, const quint8 step = defaultStep) +QString Totp::generateTotp(const QByteArray key, + quint64 time, + const quint8 numDigits = defaultDigits, + const quint8 step = defaultStep) { quint64 current = qToBigEndian(time / step); - Optional secret = Base32::decode(key); - if (!secret.hasValue()) { + QVariant secret = Base32::decode(Base32::sanitizeInput(key)); + if (secret.isNull()) { return "Invalid TOTP secret key"; } QMessageAuthenticationCode code(QCryptographicHash::Sha1); - code.setKey(secret.valueOr("")); + code.setKey(secret.toByteArray()); code.addData(QByteArray(reinterpret_cast(¤t), sizeof(current))); QByteArray hmac = code.result(); int offset = (hmac[hmac.length() - 1] & 0xf); + + // clang-format off int binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); + // clang-format on - quint32 digitsPower = pow(10, numDigits); + const Encoder& encoder = encoders.value(numDigits, defaultEncoder); + // if encoder.digits is 0, we need to use the passed-in number of digits (default encoder) + quint8 digits = encoder.digits == 0 ? numDigits : encoder.digits; + int direction = -1; + int startpos = digits - 1; + if (encoder.reverse) { + direction = 1; + startpos = 0; + } + quint32 digitsPower = pow(encoder.alphabet.size(), digits); quint64 password = binary % digitsPower; - return QString("%1").arg(password, numDigits, 10, QChar('0')); + QString retval(int(digits), encoder.alphabet[0]); + for (quint8 pos = startpos; password > 0; pos += direction) { + retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())]; + password /= encoder.alphabet.size(); + } + return retval; +} + +// See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format +QUrl Totp::generateOtpString(const QString& secret, + const QString& type, + const QString& issuer, + const QString& username, + const QString& algorithm, + quint8 digits, + quint8 step) +{ + QUrl keyUri; + keyUri.setScheme("otpauth"); + keyUri.setHost(type); + keyUri.setPath(QString("/%1:%2").arg(issuer).arg(username)); + QUrlQuery parameters; + parameters.addQueryItem("secret", secret); + parameters.addQueryItem("issuer", issuer); + parameters.addQueryItem("algorithm", algorithm); + parameters.addQueryItem("digits", QString::number(digits)); + parameters.addQueryItem("period", QString::number(step)); + keyUri.setQuery(parameters); + + return keyUri; } diff --git a/src/totp/totp.h b/src/totp/totp.h index 642b4f9a..7d4c78c1 100644 --- a/src/totp/totp.h +++ b/src/totp/totp.h @@ -20,15 +20,41 @@ #define QTOTP_H #include +#include +#include -class QTotp +class QUrl; + +class Totp { public: - QTotp(); - static QString parseOtpString(QString rawSecret, quint8 &digits, quint8 &step); + Totp(); + static QString parseOtpString(QString rawSecret, quint8& digits, quint8& step); static QString generateTotp(const QByteArray key, quint64 time, const quint8 numDigits, const quint8 step); + static QUrl generateOtpString(const QString& secret, + const QString& type, + const QString& issuer, + const QString& username, + const QString& algorithm, + quint8 digits, + quint8 step); static const quint8 defaultStep; static const quint8 defaultDigits; + struct Encoder + { + QString name; + QString shortName; + QString alphabet; + quint8 digits; + quint8 step; + bool reverse; + }; + static const Encoder defaultEncoder; + // custom encoder values that overload the digits field + static const quint8 ENCODER_STEAM; + static const QMap encoders; + static const QMap shortNameToEncoder; + static const QMap nameToEncoder; }; #endif // QTOTP_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c1f1adf4..c36eefd4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR}/../src) add_definitions(-DQT_TEST_LIB) @@ -155,6 +155,11 @@ if(WITH_XC_AUTOTYPE) set_target_properties(testautotype PROPERTIES ENABLE_EXPORTS ON) endif() +if(WITH_XC_SSHAGENT) + add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp + LIBS sshagent ${TEST_LIBRARIES}) +endif() + add_unit_test(NAME testentry SOURCES TestEntry.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestAutoType.cpp b/tests/TestAutoType.cpp index be73efd4..acc2df61 100644 --- a/tests/TestAutoType.cpp +++ b/tests/TestAutoType.cpp @@ -96,6 +96,9 @@ void TestAutoType::init() m_entry4->setGroup(m_group); m_entry4->setPassword("custom_attr"); m_entry4->attributes()->set("CUSTOM","Attribute",false); + m_entry4->attributes()->set("CustomAttrFirst","AttrValueFirst",false); + m_entry4->attributes()->set("CustomAttrSecond","AttrValueSecond",false); + m_entry4->attributes()->set("CustomAttrThird","AttrValueThird",false); association.window = "//^CustomAttr1$//"; association.sequence = "{PASSWORD}:{S:CUSTOM}"; m_entry4->autoTypeAssociations()->add(association); @@ -105,7 +108,16 @@ void TestAutoType::init() association.window = "//^CustomAttr3$//"; association.sequence = "{PaSSworD}"; m_entry4->autoTypeAssociations()->add(association); - + association.window = "//^{S:CustomAttrFirst}$//"; + association.sequence = "custom_attr_first"; + m_entry4->autoTypeAssociations()->add(association); + association.window = "//{S:CustomAttrFirst}And{S:CustomAttrSecond}//"; + association.sequence = "custom_attr_first_and_second"; + m_entry4->autoTypeAssociations()->add(association); + association.window = "//{S:CustomAttrThird}//"; + association.sequence = "custom_attr_third"; + m_entry4->autoTypeAssociations()->add(association); + m_entry5 = new Entry(); m_entry5->setGroup(m_group); m_entry5->setPassword("example5"); @@ -253,4 +265,20 @@ void TestAutoType::testGlobalAutoTypeRegExp() m_autoType->performGlobalAutoType(m_dbList); QCOMPARE(m_test->actionChars(), QString("custom_attr")); m_test->clearActions(); + + // with resolve placeholders in window association title + m_test->setActiveWindowTitle("AttrValueFirst"); + m_autoType->performGlobalAutoType(m_dbList); + QCOMPARE(m_test->actionChars(), QString("custom_attr_first")); + m_test->clearActions(); + + m_test->setActiveWindowTitle("lorem AttrValueFirstAndAttrValueSecond ipsum"); + m_autoType->performGlobalAutoType(m_dbList); + QCOMPARE(m_test->actionChars(), QString("custom_attr_first_and_second")); + m_test->clearActions(); + + m_test->setActiveWindowTitle("lorem AttrValueThird ipsum"); + m_autoType->performGlobalAutoType(m_dbList); + QCOMPARE(m_test->actionChars(), QString("custom_attr_third")); + m_test->clearActions(); } diff --git a/tests/TestBase32.cpp b/tests/TestBase32.cpp index 7268e927..1d30e781 100644 --- a/tests/TestBase32.cpp +++ b/tests/TestBase32.cpp @@ -23,88 +23,136 @@ QTEST_GUILESS_MAIN(TestBase32) void TestBase32::testDecode() { - // 3 quantums, all upper case + padding + // 3 quanta, all upper case + padding QByteArray encodedData = "JBSWY3DPEB3W64TMMQXC4LQ="; - auto data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("Hello world...")); + QVariant data = Base32::decode(encodedData); + QString expectedData = "Hello world..."; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); - // 4 quantums, all upper case + // 4 quanta, all upper case encodedData = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("12345678901234567890")); + expectedData = "12345678901234567890"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); - // 4 quantums, all lower case + // 4 quanta, all lower case encodedData = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("12345678901234567890")); + expectedData = "12345678901234567890"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); - // 4 quantums, mixed upper and lower case + // 4 quanta, mixed upper and lower case encodedData = "Gezdgnbvgy3tQojqgezdGnbvgy3tQojQ"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("12345678901234567890")); + expectedData = "12345678901234567890"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); // 1 pad characters encodedData = "ORSXG5A="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("test")); + expectedData = "test"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); // 3 pad characters encodedData = "L5PV6==="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("___")); + expectedData = "___"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); // 4 pad characters encodedData = "MZXW6IDCMFZA===="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foo bar")); + expectedData = "foo bar"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); // six pad characters encodedData = "MZXW6YTBOI======"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foobar")); + expectedData = "foobar"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "IA======"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("@")); + expectedData = "@"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); // error: illegal character encodedData = "1MZXW6YTBOI====="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("ERROR")); + QVERIFY(data.isNull()); // error: missing pad character encodedData = "MZXW6YTBOI====="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("ERROR")); + QVERIFY(data.isNull()); // RFC 4648 test vectors encodedData = ""; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("")); + expectedData = ""; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "MY======"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("f")); + expectedData = "f"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "MZXQ===="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("fo")); + expectedData = "fo"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "MZXW6==="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foo")); + QVERIFY(!data.isNull()); + expectedData = "foo"; + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "MZXW6YQ="; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foob")); + expectedData = "foob"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "MZXW6YTB"; + expectedData = "fooba"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("fooba")); + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); encodedData = "MZXW6YTBOI======"; data = Base32::decode(encodedData); - QCOMPARE(QString::fromLatin1(data.valueOr("ERROR")), QString("foobar")); + expectedData = "foobar"; + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), expectedData); + QVERIFY(data.value().size() == expectedData.size()); } void TestBase32::testEncode() @@ -115,7 +163,7 @@ void TestBase32::testEncode() data = "12345678901234567890"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")); + QCOMPARE(encodedData, QByteArray("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ")); data = "012345678901234567890"; encodedData = Base32::encode(data); @@ -123,46 +171,160 @@ void TestBase32::testEncode() data = "test"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("ORSXG5A=")); + QCOMPARE(encodedData, QByteArray("ORSXG5A=")); data = "___"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("L5PV6===")); + QCOMPARE(encodedData, QByteArray("L5PV6===")); data = "foo bar"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("MZXW6IDCMFZA====")); + QCOMPARE(encodedData, QByteArray("MZXW6IDCMFZA====")); data = "@"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("IA======")); + QCOMPARE(encodedData, QByteArray("IA======")); // RFC 4648 test vectors data = ""; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("")); + QCOMPARE(encodedData, QByteArray("")); data = "f"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("MY======")); + QCOMPARE(encodedData, QByteArray("MY======")); data = "fo"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("MZXQ====")); + QCOMPARE(encodedData, QByteArray("MZXQ====")); data = "foo"; encodedData = Base32::encode(data); QCOMPARE(encodedData, QByteArray("MZXW6===")); - + data = "foob"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("MZXW6YQ=")); + QCOMPARE(encodedData, QByteArray("MZXW6YQ=")); data = "fooba"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("MZXW6YTB")); + QCOMPARE(encodedData, QByteArray("MZXW6YTB")); data = "foobar"; encodedData = Base32::encode(data); - QCOMPARE(encodedData, QByteArray("MZXW6YTBOI======")); + QCOMPARE(encodedData, QByteArray("MZXW6YTBOI======")); +} + +void TestBase32::testAddPadding() +{ + // Empty. Invalid, returns input. + QByteArray data = ""; + QByteArray paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // One byte of encoded data. Invalid, returns input. + data = "B"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // Two bytes of encoded data. + data = "BB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BB======")); + + // Three bytes of encoded data. Invalid, returns input. + data = "BBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // Four bytes of encoded data. + data = "BBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BBBB====")); + + // Five bytes of encoded data. + data = "BBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BBBBB===")); + + // Six bytes of encoded data. Invalid, returns input. + data = "BBBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // Seven bytes of encoded data. + data = "BBBBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("BBBBBBB=")); + + // Eight bytes of encoded data. Valid, but returns same as input. + data = "BBBBBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, data); + + // More than eight bytes (8+5). + data = "AAAAAAAABBBBB"; + paddedData = Base32::addPadding(data); + QCOMPARE(paddedData, QByteArray("AAAAAAAABBBBB===")); +} + +void TestBase32::testRemovePadding() +{ + QByteArray data = ""; + QByteArray unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, data); + + data = "AAAAAAAABB======"; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("AAAAAAAABB")); + + data = "BBBB===="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("BBBB")); + + data = "AAAAAAAABBBBB==="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("AAAAAAAABBBBB")); + + data = "BBBBBBB="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, QByteArray("BBBBBBB")); + + // Invalid: 7 bytes of data. Returns same as input. + data = "IIIIIII"; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, data); + + // Invalid: more padding than necessary. Returns same as input. + data = "AAAAAAAABBBB====="; + unpaddedData = Base32::removePadding(data); + QCOMPARE(unpaddedData, data); +} + +void TestBase32::testSanitizeInput() +{ + // sanitize input (white space + missing padding) + QByteArray encodedData = "JBSW Y3DP EB3W 64TM MQXC 4LQA"; + auto data = Base32::decode(Base32::sanitizeInput(encodedData)); + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), QString("Hello world...")); + + // sanitize input (typo + missing padding) + encodedData = "J8SWY3DPE83W64TMMQXC4LQA"; + data = Base32::decode(Base32::sanitizeInput(encodedData)); + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), QString("Hello world...")); + + // sanitize input (other illegal characters) + encodedData = "J8SWY3D[PE83W64TMMQ]XC!4LQA"; + data = Base32::decode(Base32::sanitizeInput(encodedData)); + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), QString("Hello world...")); + + // sanitize input (NUL character) + encodedData = "J8SWY3DPE83W64TMMQXC4LQA"; + encodedData.insert(3, '\0'); + data = Base32::decode(Base32::sanitizeInput(encodedData)); + QVERIFY(!data.isNull()); + QCOMPARE(data.toString(), QString("Hello world...")); } diff --git a/tests/TestBase32.h b/tests/TestBase32.h index 6d4de34a..cf7cf092 100644 --- a/tests/TestBase32.h +++ b/tests/TestBase32.h @@ -29,6 +29,9 @@ class TestBase32 : public QObject private slots: void testEncode(); void testDecode(); + void testAddPadding(); + void testRemovePadding(); + void testSanitizeInput(); }; #endif // KEEPASSX_TESTBASE32_H diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 84ad0318..1e863dbe 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -20,7 +20,9 @@ #include +#include "core/Database.h" #include "core/Entry.h" +#include "core/Group.h" #include "crypto/Crypto.h" QTEST_GUILESS_MAIN(TestEntry) @@ -158,3 +160,105 @@ void TestEntry::testResolveUrl() delete entry; } + +void TestEntry::testResolveUrlPlaceholders() +{ + Entry entry; + entry.setUrl("https://user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment"); + + QString rmvscm("//user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment"); // Entry URL without scheme name. + QString scm("https"); // Scheme name of the entry URL. + QString host("keepassxc.org"); // Host component of the entry URL. + QString port("80"); // Port number of the entry URL. + QString path("/path/example.php"); // Path component of the entry URL. + QString query("q=e&s=t+2"); // Query information of the entry URL. + QString userinfo("user:pw"); // User information of the entry URL. + QString username("user"); // User name of the entry URL. + QString password("pw"); // Password of the entry URL. + QString fragment("fragment"); // Fragment of the entry URL. + + QCOMPARE(entry.resolvePlaceholder("{URL:RMVSCM}"), rmvscm); + QCOMPARE(entry.resolvePlaceholder("{URL:WITHOUTSCHEME}"), rmvscm); + QCOMPARE(entry.resolvePlaceholder("{URL:SCM}"), scm); + QCOMPARE(entry.resolvePlaceholder("{URL:SCHEME}"), scm); + QCOMPARE(entry.resolvePlaceholder("{URL:HOST}"), host); + QCOMPARE(entry.resolvePlaceholder("{URL:PORT}"), port); + QCOMPARE(entry.resolvePlaceholder("{URL:PATH}"), path); + QCOMPARE(entry.resolvePlaceholder("{URL:QUERY}"), query); + QCOMPARE(entry.resolvePlaceholder("{URL:USERINFO}"), userinfo); + QCOMPARE(entry.resolvePlaceholder("{URL:USERNAME}"), username); + QCOMPARE(entry.resolvePlaceholder("{URL:PASSWORD}"), password); + QCOMPARE(entry.resolvePlaceholder("{URL:FRAGMENT}"), fragment); +} + +void TestEntry::testResolveRecursivePlaceholders() +{ + Database db; + Group* root = db.rootGroup(); + + Entry* entry1 = new Entry(); + entry1->setGroup(root); + entry1->setUuid(Uuid::random()); + entry1->setTitle("{USERNAME}"); + entry1->setUsername("{PASSWORD}"); + entry1->setPassword("{URL}"); + entry1->setUrl("{S:CustomTitle}"); + entry1->attributes()->set("CustomTitle", "RecursiveValue"); + QCOMPARE(entry1->resolveMultiplePlaceholders(entry1->title()), QString("RecursiveValue")); + + Entry* entry2 = new Entry(); + entry2->setGroup(root); + entry2->setUuid(Uuid::random()); + entry2->setTitle("Entry2Title"); + entry2->setUsername("{S:CustomUserNameAttribute}"); + entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuid().toHex())); + entry2->setUrl("http://{S:IpAddress}:{S:Port}/{S:Uri}"); + entry2->attributes()->set("CustomUserNameAttribute", "CustomUserNameValue"); + entry2->attributes()->set("IpAddress", "127.0.0.1"); + entry2->attributes()->set("Port", "1234"); + entry2->attributes()->set("Uri", "uri/path"); + + Entry* entry3 = new Entry(); + entry3->setGroup(root); + entry3->setUuid(Uuid::random()); + entry3->setTitle(QString("{REF:T@I:%1}").arg(entry2->uuid().toHex())); + entry3->setUsername(QString("{REF:U@I:%1}").arg(entry2->uuid().toHex())); + entry3->setPassword(QString("{REF:P@I:%1}").arg(entry2->uuid().toHex())); + entry3->setUrl(QString("{REF:A@I:%1}").arg(entry2->uuid().toHex())); + + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->title()), QString("Entry2Title")); + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->username()), QString("CustomUserNameValue")); + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->password()), QString("RecursiveValue")); + QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), QString("http://127.0.0.1:1234/uri/path")); + + Entry* entry4 = new Entry(); + entry4->setGroup(root); + entry4->setUuid(Uuid::random()); + entry4->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())); + entry4->setUsername(QString("{REF:U@I:%1}").arg(entry3->uuid().toHex())); + entry4->setPassword(QString("{REF:P@I:%1}").arg(entry3->uuid().toHex())); + entry4->setUrl(QString("{REF:A@I:%1}").arg(entry3->uuid().toHex())); + + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->title()), QString("Entry2Title")); + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->username()), QString("CustomUserNameValue")); + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->password()), QString("RecursiveValue")); + QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->url()), QString("http://127.0.0.1:1234/uri/path")); + + Entry* entry5 = new Entry(); + entry5->setGroup(root); + entry5->setUuid(Uuid::random()); + entry5->attributes()->set("Scheme", "http"); + entry5->attributes()->set("Host", "host.org"); + entry5->attributes()->set("Port", "2017"); + entry5->attributes()->set("Path", "/some/path"); + entry5->attributes()->set("UserName", "username"); + entry5->attributes()->set("Password", "password"); + entry5->attributes()->set("Query", "q=e&t=s"); + entry5->attributes()->set("Fragment", "fragment"); + entry5->setUrl("{S:Scheme}://{S:UserName}:{S:Password}@{S:Host}:{S:Port}{S:Path}?{S:Query}#{S:Fragment}"); + entry5->setTitle("title+{URL:Path}+{URL:Fragment}+title"); + + const QString url("http://username:password@host.org:2017/some/path?q=e&t=s#fragment"); + QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->url()), url); + QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->title()), QString("title+/some/path+fragment+title")); +} diff --git a/tests/TestEntry.h b/tests/TestEntry.h index 3f6d20ee..50fec57a 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -32,6 +32,8 @@ private slots: void testCopyDataFrom(); void testClone(); void testResolveUrl(); + void testResolveUrlPlaceholders(); + void testResolveRecursivePlaceholders(); }; #endif // KEEPASSX_TESTENTRY_H diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 5a809670..2d478bd1 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -391,6 +391,22 @@ void TestGroup::testClone() QCOMPARE(clonedGroupKeepUuid->entries().at(0)->uuid(), originalGroupEntry->uuid()); QCOMPARE(clonedGroupKeepUuid->children().at(0)->entries().at(0)->uuid(), subGroupEntry->uuid()); + Group* clonedGroupNoFlags = originalGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags); + QCOMPARE(clonedGroupNoFlags->entries().size(), 0); + QVERIFY(clonedGroupNoFlags->uuid() == originalGroup->uuid()); + + Group* clonedGroupNewUuid = originalGroup->clone(Entry::CloneNoFlags, Group::CloneNewUuid); + QCOMPARE(clonedGroupNewUuid->entries().size(), 0); + QVERIFY(clonedGroupNewUuid->uuid() != originalGroup->uuid()); + + // Making sure the new modification date is not the same. + QTest::qSleep(1); + + Group* clonedGroupResetTimeInfo = originalGroup->clone(Entry::CloneNoFlags, Group::CloneNewUuid | Group::CloneResetTimeInfo); + QCOMPARE(clonedGroupResetTimeInfo->entries().size(), 0); + QVERIFY(clonedGroupResetTimeInfo->uuid() != originalGroup->uuid()); + QVERIFY(clonedGroupResetTimeInfo->timeInfo().lastModificationTime() != originalGroup->timeInfo().lastModificationTime()); + delete clonedGroup; delete clonedGroupKeepUuid; delete db; diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp index c8f15f29..d68c4f10 100644 --- a/tests/TestMerge.cpp +++ b/tests/TestMerge.cpp @@ -48,6 +48,8 @@ void TestMerge::testMergeIntoNew() QCOMPARE(dbDestination->rootGroup()->children().size(), 2); QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2); + // Test for retention of history + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false); delete dbDestination; delete dbSource; @@ -62,7 +64,7 @@ void TestMerge::testMergeNoChanges() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); QCOMPARE(dbSource->rootGroup()->entriesRecursive().size(), 2); @@ -90,7 +92,7 @@ void TestMerge::testResolveConflictNewer() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); // sanity check Group* group1 = dbSource->rootGroup()->findChildByName("group1"); @@ -139,7 +141,7 @@ void TestMerge::testResolveConflictOlder() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); // sanity check Group* group1 = dbSource->rootGroup()->findChildByName("group1"); @@ -195,7 +197,7 @@ void TestMerge::testResolveConflictKeepBoth() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneIncludeHistory, Group::CloneIncludeEntries)); // sanity check QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2); @@ -212,9 +214,11 @@ void TestMerge::testResolveConflictKeepBoth() // one entry is duplicated because of mode QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 3); + QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().at(0)->historyItems().isEmpty(), false); // the older entry was merged from the other db as last in the group Entry* olderEntry = dbDestination->rootGroup()->children().at(0)->entries().at(2); QVERIFY2(olderEntry->attributes()->hasKey("merged"), "older entry is marked with an attribute \"merged\""); + QCOMPARE(olderEntry->historyItems().isEmpty(), false); QVERIFY2(olderEntry->uuid().toHex() != updatedEntry->uuid().toHex(), "KeepBoth should not reuse the UUIDs when cloning."); @@ -232,7 +236,7 @@ void TestMerge::testMoveEntry() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); QVERIFY(entry1 != nullptr); @@ -266,7 +270,7 @@ void TestMerge::testMoveEntryPreserveChanges() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); QVERIFY(entry1 != nullptr); @@ -303,11 +307,12 @@ void TestMerge::testCreateNewGroups() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); QTest::qSleep(1); Group* group3 = new Group(); group3->setName("group3"); + group3->setUuid(Uuid::random()); group3->setParent(dbSource->rootGroup()); dbDestination->merge(dbSource); @@ -325,11 +330,12 @@ void TestMerge::testMoveEntryIntoNewGroup() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); QTest::qSleep(1); Group* group3 = new Group(); group3->setName("group3"); + group3->setUuid(Uuid::random()); group3->setParent(dbSource->rootGroup()); Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); @@ -361,10 +367,11 @@ void TestMerge::testUpdateEntryDifferentLocation() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); Group* group3 = new Group(); group3->setName("group3"); + group3->setUuid(Uuid::random()); group3->setParent(dbDestination->rootGroup()); Entry* entry1 = dbDestination->rootGroup()->findEntry("entry1"); @@ -395,6 +402,84 @@ void TestMerge::testUpdateEntryDifferentLocation() delete dbSource; } +/** + * Groups should be updated using the uuids. + */ +void TestMerge::testUpdateGroup() +{ + Database* dbDestination = createTestDatabase(); + + Database* dbSource = new Database(); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + QTest::qSleep(1); + + Group* group2 = dbSource->rootGroup()->findChildByName("group2"); + group2->setName("group2 renamed"); + group2->setNotes("updated notes"); + Uuid customIconId = Uuid::random(); + QImage customIcon; + dbSource->metadata()->addCustomIcon(customIconId, customIcon); + group2->setIcon(customIconId); + + Entry* entry1 = dbSource->rootGroup()->findEntry("entry1"); + QVERIFY(entry1 != nullptr); + entry1->setGroup(group2); + entry1->setTitle("entry1 renamed"); + Uuid uuidBeforeSyncing = entry1->uuid(); + + dbDestination->merge(dbSource); + + QCOMPARE(dbDestination->rootGroup()->entriesRecursive().size(), 2); + + entry1 = dbDestination->rootGroup()->findEntry("entry1 renamed"); + QVERIFY(entry1 != nullptr); + QVERIFY(entry1->group() != nullptr); + QCOMPARE(entry1->group()->name(), QString("group2 renamed")); + QCOMPARE(uuidBeforeSyncing, entry1->uuid()); + + group2 = dbDestination->rootGroup()->findChildByName("group2 renamed"); + QCOMPARE(group2->notes(), QString("updated notes")); + QCOMPARE(group2->iconUuid(), customIconId); + + delete dbDestination; + delete dbSource; +} + +void TestMerge::testUpdateGroupLocation() +{ + Database* dbDestination = createTestDatabase(); + Group* group3 = new Group(); + Uuid group3Uuid = Uuid::random(); + group3->setUuid(group3Uuid); + group3->setName("group3"); + group3->setParent(dbDestination->rootGroup()->findChildByName("group1")); + + Database* dbSource = new Database(); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + // Sanity check + group3 = dbSource->rootGroup()->findChildByUuid(group3Uuid); + QVERIFY(group3 != nullptr); + + QTest::qSleep(1); + + group3->setParent(dbSource->rootGroup()->findChildByName("group2")); + + dbDestination->merge(dbSource); + group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid); + QVERIFY(group3 != nullptr); + QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2")); + + dbDestination->merge(dbSource); + group3 = dbDestination->rootGroup()->findChildByUuid(group3Uuid); + QVERIFY(group3 != nullptr); + QCOMPARE(group3->parent(), dbDestination->rootGroup()->findChildByName("group2")); + + delete dbDestination; + delete dbSource; +} + /** * The first merge should create new entries, the * second should only sync them, since they have @@ -443,24 +528,71 @@ void TestMerge::testMergeCustomIcons() delete dbSource; } +/** + * If the group is updated in the source database, and the + * destination database after, the group should remain the + * same. + */ +void TestMerge::testResolveGroupConflictOlder() +{ + Database* dbDestination = createTestDatabase(); + + Database* dbSource = new Database(); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneNoFlags, Group::CloneIncludeEntries)); + + // sanity check + Group* group1 = dbSource->rootGroup()->findChildByName("group1"); + QVERIFY(group1 != nullptr); + + // Make sure the two changes have a different timestamp. + QTest::qSleep(1); + group1->setName("group1 updated in source"); + + // Make sure the two changes have a different timestamp. + QTest::qSleep(1); + + group1 = dbDestination->rootGroup()->findChildByName("group1"); + group1->setName("group1 updated in destination"); + + dbDestination->merge(dbSource); + + // sanity check + group1 = dbDestination->rootGroup()->findChildByName("group1 updated in destination"); + QVERIFY(group1 != nullptr); + + delete dbDestination; + delete dbSource; +} + + Database* TestMerge::createTestDatabase() { Database* db = new Database(); Group* group1 = new Group(); group1->setName("group1"); + group1->setUuid(Uuid::random()); + Group* group2 = new Group(); group2->setName("group2"); + group2->setUuid(Uuid::random()); Entry* entry1 = new Entry(); Entry* entry2 = new Entry(); + // Give Entry 1 a history + entry1->beginUpdate(); entry1->setGroup(group1); entry1->setUuid(Uuid::random()); entry1->setTitle("entry1"); + entry1->endUpdate(); + + // Give Entry 2 a history + entry2->beginUpdate(); entry2->setGroup(group1); entry2->setUuid(Uuid::random()); entry2->setTitle("entry2"); + entry2->endUpdate(); group1->setParent(db->rootGroup()); group2->setParent(db->rootGroup()); diff --git a/tests/TestMerge.h b/tests/TestMerge.h index 0b3ec618..3588cfd5 100644 --- a/tests/TestMerge.h +++ b/tests/TestMerge.h @@ -31,12 +31,15 @@ private slots: void testMergeNoChanges(); void testResolveConflictNewer(); void testResolveConflictOlder(); + void testResolveGroupConflictOlder(); void testResolveConflictKeepBoth(); void testMoveEntry(); void testMoveEntryPreserveChanges(); void testMoveEntryIntoNewGroup(); void testCreateNewGroups(); void testUpdateEntryDifferentLocation(); + void testUpdateGroup(); + void testUpdateGroupLocation(); void testMergeAndSync(); void testMergeCustomIcons(); diff --git a/tests/TestOpenSSHKey.cpp b/tests/TestOpenSSHKey.cpp new file mode 100644 index 00000000..949c708c --- /dev/null +++ b/tests/TestOpenSSHKey.cpp @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2017 Toni Spets + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestOpenSSHKey.h" +#include "crypto/Crypto.h" +#include "sshagent/OpenSSHKey.h" +#include + +QTEST_GUILESS_MAIN(TestOpenSSHKey) + +void TestOpenSSHKey::initTestCase() +{ + QVERIFY(Crypto::init()); +} + +void TestOpenSSHKey::testParse() +{ + // mixed line endings and missing ones are intentional, we only require 3 lines total + const QString keyString = QString( + "\r\n\r" + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW" + "QyNTUxOQAAACDdlO5F2kF2WzedrBAHBi9wBHeISzXZ0IuIqrp0EzeazAAAAKjgCfj94An4" + "/QAAAAtzc2gtZWQyNTUxOQAAACDdlO5F2kF2WzedrBAHBi9wBHeISzXZ0IuIqrp0EzeazA" + "AAAEBe1iilZFho8ZGAliiSj5URvFtGrgvmnEKdiLZow5hOR92U7kXaQXZbN52sEAcGL3AE" + "d4hLNdnQi4iqunQTN5rMAAAAH29wZW5zc2hrZXktdGVzdC1wYXJzZUBrZWVwYXNzeGMBAg" + "MEBQY=\r" + "-----END OPENSSH PRIVATE KEY-----\r\n\r" + ); + + const QByteArray keyData = keyString.toLatin1(); + + OpenSSHKey key; + QVERIFY(key.parse(keyData)); + QVERIFY(!key.encrypted()); + QCOMPARE(key.cipherName(), QString("none")); + QCOMPARE(key.type(), QString("ssh-ed25519")); + QCOMPARE(key.comment(), QString("opensshkey-test-parse@keepassxc")); + + QByteArray publicKey, privateKey; + BinaryStream publicStream(&publicKey), privateStream(&privateKey); + + QVERIFY(key.writePublic(publicStream)); + QVERIFY(key.writePrivate(privateStream)); + + QVERIFY(publicKey.length() == 51); + QVERIFY(privateKey.length() == 154); +} + +void TestOpenSSHKey::testDecryptAES256CBC() +{ + const QString keyString = QString( + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABD2A0agtd\n" + "oGtJiI9JvIxYbTAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIDPvDXmi0w1rdMoX\n" + "fOeyZ0Q/v+wqq/tPFgJwxnW5ADtfAAAAsC3UPsf035hrF5SgZ48p55iDFPiyGfZC/C3vQx\n" + "+THzpQo8DTUmFokdPn8wvDYGQoIcr9q0RzJuKV87eMQf3zzvZfJthtLYBlt330Deivv9AQ\n" + "MbKdhPZ4SfwRvv0grgT2EVId3GQAPgSVBhXYQTOf2CdmbXV4kieFLTmSsBMy+v6Qn5Rqur\n" + "PDWBwuLQgamcVDZuhrkUEqIVJZU2zAiRU2oAXsw/XOgFV6+Y5UZmLwWJQZ\n" + "-----END OPENSSH PRIVATE KEY-----\n" + ); + + const QByteArray keyData = keyString.toLatin1(); + + OpenSSHKey key; + QVERIFY(key.parse(keyData)); + QVERIFY(key.encrypted()); + QCOMPARE(key.cipherName(), QString("aes256-cbc")); + QVERIFY(!key.openPrivateKey("incorrectpassphrase")); + QVERIFY(key.openPrivateKey("correctpassphrase")); + QCOMPARE(key.type(), QString("ssh-ed25519")); + QCOMPARE(key.comment(), QString("opensshkey-test-aes256cbc@keepassxc")); + + QByteArray publicKey, privateKey; + BinaryStream publicStream(&publicKey), privateStream(&privateKey); + + QVERIFY(key.writePublic(publicStream)); + QVERIFY(key.writePrivate(privateStream)); + + QVERIFY(publicKey.length() == 51); + QVERIFY(privateKey.length() == 158); +} + +void TestOpenSSHKey::testDecryptAES256CTR() +{ + const QString keyString = QString( + "-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAMhIAypt\n" + "WP4tZJBmMwq0tTAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIErNsS8ROy43XoWC\n" + "nO9Sn2lEFBJYcDVtRPM1t6WB7W7OAAAAsFKXMOlPILoTmMj2JmcqzjaYAhaCezx18HDp76\n" + "VrNxaZTd0T28EGFSkzrReeewpJWy/bWlhLoXR5fRyOSSto+iMg/pibIvIJMrD5sqxlxr/e\n" + "c5lSeSZUzIK8Rv+ou/3EFDcY5jp8hVXqA4qNtoM/3fV52vmwlNje5d1V5Gsr4U8443+i+p\n" + "swqksozfatkynk51uR/9QFoOJKlsL/Z3LkK1S/apYz/K331iU1f5ozFELf\n" + "-----END OPENSSH PRIVATE KEY-----\n" + ); + + const QByteArray keyData = keyString.toLatin1(); + + OpenSSHKey key; + QVERIFY(key.parse(keyData)); + QVERIFY(key.encrypted()); + QCOMPARE(key.cipherName(), QString("aes256-ctr")); + QVERIFY(!key.openPrivateKey("incorrectpassphrase")); + QVERIFY(key.openPrivateKey("correctpassphrase")); + QCOMPARE(key.type(), QString("ssh-ed25519")); + QCOMPARE(key.comment(), QString("opensshkey-test-aes256ctr@keepassxc")); + + QByteArray publicKey, privateKey; + BinaryStream publicStream(&publicKey), privateStream(&privateKey); + + QVERIFY(key.writePublic(publicStream)); + QVERIFY(key.writePrivate(privateStream)); + + QVERIFY(publicKey.length() == 51); + QVERIFY(privateKey.length() == 158); +} diff --git a/tests/TestOpenSSHKey.h b/tests/TestOpenSSHKey.h new file mode 100644 index 00000000..f2d6d1fb --- /dev/null +++ b/tests/TestOpenSSHKey.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2017 Toni Spets + * + * 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 TESTOPENSSHKEY_H +#define TESTOPENSSHKEY_H + +#include + +class OpenSSHKey; + +class TestOpenSSHKey : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testParse(); + void testDecryptAES256CBC(); + void testDecryptAES256CTR(); +}; + +#endif // TESTOPENSSHKEY_H diff --git a/tests/TestSymmetricCipher.cpp b/tests/TestSymmetricCipher.cpp index 4f78693d..74f720a7 100644 --- a/tests/TestSymmetricCipher.cpp +++ b/tests/TestSymmetricCipher.cpp @@ -124,6 +124,46 @@ void TestSymmetricCipher::testAes256CbcDecryption() plainText); } +void TestSymmetricCipher::testAes256CtrEncryption() +{ + // http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf + + QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); + QByteArray ctr = QByteArray::fromHex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); + plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); + QByteArray cipherText = QByteArray::fromHex("601ec313775789a5b7a7f504bbf3d228"); + cipherText.append(QByteArray::fromHex("f443e3ca4d62b59aca84e990cacaf5c5")); + bool ok; + + SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Ctr, SymmetricCipher::Encrypt); + QVERIFY(cipher.init(key, ctr)); + QCOMPARE(cipher.blockSize(), 16); + + QCOMPARE(cipher.process(plainText, &ok), + cipherText); + QVERIFY(ok); +} + +void TestSymmetricCipher::testAes256CtrDecryption() +{ + QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); + QByteArray ctr = QByteArray::fromHex("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + QByteArray cipherText = QByteArray::fromHex("601ec313775789a5b7a7f504bbf3d228"); + cipherText.append(QByteArray::fromHex("f443e3ca4d62b59aca84e990cacaf5c5")); + QByteArray plainText = QByteArray::fromHex("6bc1bee22e409f96e93d7e117393172a"); + plainText.append(QByteArray::fromHex("ae2d8a571e03ac9c9eb76fac45af8e51")); + bool ok; + + SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Ctr, SymmetricCipher::Decrypt); + QVERIFY(cipher.init(key, ctr)); + QCOMPARE(cipher.blockSize(), 16); + + QCOMPARE(cipher.process(cipherText, &ok), + plainText); + QVERIFY(ok); +} + void TestSymmetricCipher::testTwofish256CbcEncryption() { // NIST MCT Known-Answer Tests (cbc_e_m.txt) diff --git a/tests/TestSymmetricCipher.h b/tests/TestSymmetricCipher.h index 00998950..cad13841 100644 --- a/tests/TestSymmetricCipher.h +++ b/tests/TestSymmetricCipher.h @@ -29,6 +29,8 @@ private slots: void initTestCase(); void testAes256CbcEncryption(); void testAes256CbcDecryption(); + void testAes256CtrEncryption(); + void testAes256CtrDecryption(); void testTwofish256CbcEncryption(); void testTwofish256CbcDecryption(); void testSalsa20(); diff --git a/tests/TestTotp.cpp b/tests/TestTotp.cpp index 7a3b2e3c..06e36014 100644 --- a/tests/TestTotp.cpp +++ b/tests/TestTotp.cpp @@ -18,11 +18,11 @@ #include "TestTotp.h" -#include -#include #include -#include +#include #include +#include +#include #include "crypto/Crypto.h" #include "totp/totp.h" @@ -34,27 +34,28 @@ void TestTotp::initTestCase() QVERIFY(Crypto::init()); } - void TestTotp::testParseSecret() { quint8 digits = 0; quint8 step = 0; - QString secret = "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"; - QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); + QString secret = "otpauth://totp/" + "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=" + "SHA1&digits=6&period=30"; + QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); QCOMPARE(digits, quint8(6)); QCOMPARE(step, quint8(30)); - digits = QTotp::defaultDigits; - step = QTotp::defaultStep; + digits = Totp::defaultDigits; + step = Totp::defaultStep; secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8"; - QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRBY=")); + QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("HXDMVJECJJWSRBY=")); QCOMPARE(digits, quint8(8)); QCOMPARE(step, quint8(25)); digits = 0; step = 0; secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; - QCOMPARE(QTotp::parseOtpString(secret, digits, step), QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); + QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); QCOMPARE(digits, quint8(6)); QCOMPARE(step, quint8(30)); } @@ -67,18 +68,104 @@ void TestTotp::testTotpCode() QByteArray seed = QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ").toLatin1(); quint64 time = 1234567890; - QString output = QTotp::generateTotp(seed, time, 6, 30); + QString output = Totp::generateTotp(seed, time, 6, 30); QCOMPARE(output, QString("005924")); time = 1111111109; - output = QTotp::generateTotp(seed, time, 6, 30); + output = Totp::generateTotp(seed, time, 6, 30); QCOMPARE(output, QString("081804")); time = 1111111111; - output = QTotp::generateTotp(seed, time, 8, 30); + output = Totp::generateTotp(seed, time, 8, 30); QCOMPARE(output, QString("14050471")); time = 2000000000; - output = QTotp::generateTotp(seed, time, 8, 30); + output = Totp::generateTotp(seed, time, 8, 30); QCOMPARE(output, QString("69279037")); } + +void TestTotp::testEncoderData() +{ + for (quint8 key: Totp::encoders.keys()) { + const Totp::Encoder& enc = Totp::encoders.value(key); + QVERIFY2(enc.digits != 0, + qPrintable(QString("Custom encoders cannot have zero-value for digits field: %1(%2)") + .arg(enc.name) + .arg(key))); + QVERIFY2(!enc.name.isEmpty(), + qPrintable(QString("Custom encoders must have a name: %1(%2)") + .arg(enc.name) + .arg(key))); + QVERIFY2(!enc.shortName.isEmpty(), + qPrintable(QString("Custom encoders must have a shortName: %1(%2)") + .arg(enc.name) + .arg(key))); + QVERIFY2(Totp::shortNameToEncoder.contains(enc.shortName), + qPrintable(QString("No shortNameToEncoder entry found for custom encoder: %1(%2) %3") + .arg(enc.name) + .arg(key) + .arg(enc.shortName))); + QVERIFY2(Totp::shortNameToEncoder[enc.shortName] == key, + qPrintable(QString("shortNameToEncoder doesn't reference this custome encoder: %1(%2) %3") + .arg(enc.name) + .arg(key) + .arg(enc.shortName))); + QVERIFY2(Totp::nameToEncoder.contains(enc.name), + qPrintable(QString("No nameToEncoder entry found for custom encoder: %1(%2) %3") + .arg(enc.name) + .arg(key) + .arg(enc.shortName))); + QVERIFY2(Totp::nameToEncoder[enc.name] == key, + qPrintable(QString("nameToEncoder doesn't reference this custome encoder: %1(%2) %3") + .arg(enc.name) + .arg(key) + .arg(enc.shortName))); + } + + for (const QString & key: Totp::nameToEncoder.keys()) { + quint8 value = Totp::nameToEncoder.value(key); + QVERIFY2(Totp::encoders.contains(value), + qPrintable(QString("No custom encoder found for encoder named %1(%2)") + .arg(value) + .arg(key))); + QVERIFY2(Totp::encoders[value].name == key, + qPrintable(QString("nameToEncoder doesn't reference the right custom encoder: %1(%2)") + .arg(value) + .arg(key))); + } + + for (const QString & key: Totp::shortNameToEncoder.keys()) { + quint8 value = Totp::shortNameToEncoder.value(key); + QVERIFY2(Totp::encoders.contains(value), + qPrintable(QString("No custom encoder found for short-name encoder %1(%2)") + .arg(value) + .arg(key))); + QVERIFY2(Totp::encoders[value].shortName == key, + qPrintable(QString("shortNameToEncoder doesn't reference the right custom encoder: %1(%2)") + .arg(value) + .arg(key))); + } +} + +void TestTotp::testSteamTotp() +{ + quint8 digits = 0; + quint8 step = 0; + QString secret = "otpauth://totp/" + "test:test@example.com?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm=" + "SHA1&digits=5&period=30&encoder=steam"; + QCOMPARE(Totp::parseOtpString(secret, digits, step), QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK")); + QCOMPARE(digits, quint8(Totp::ENCODER_STEAM)); + QCOMPARE(step, quint8(30)); + + + QByteArray seed = QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK").toLatin1(); + + // These time/value pairs were created by running the Steam Guard function of the + // Steam mobile app with a throw-away steam account. The above secret was extracted + // from the Steam app's data for use in testing here. + quint64 time = 1511200518; + QCOMPARE(Totp::generateTotp(seed, time, Totp::ENCODER_STEAM, 30), QString("FR8RV")); + time = 1511200714; + QCOMPARE(Totp::generateTotp(seed, time, Totp::ENCODER_STEAM, 30), QString("9P3VP")); +} diff --git a/tests/TestTotp.h b/tests/TestTotp.h index 785a9f52..3bf2de93 100644 --- a/tests/TestTotp.h +++ b/tests/TestTotp.h @@ -31,6 +31,8 @@ private slots: void initTestCase(); void testParseSecret(); void testTotpCode(); + void testEncoderData(); + void testSteamTotp(); }; #endif // KEEPASSX_TESTTOTP_H diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 75ce3cc5..664bfd65 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -49,6 +49,7 @@ #include "gui/DatabaseTabWidget.h" #include "gui/DatabaseWidget.h" #include "gui/CloneDialog.h" +#include "gui/PasswordEdit.h" #include "gui/TotpDialog.h" #include "gui/SetupTotpDialog.h" #include "gui/FileDialog.h" @@ -119,6 +120,45 @@ void TestGui::cleanup() m_dbWidget = nullptr; } +void TestGui::testCreateDatabase() +{ + QTemporaryFile tmpFile; + QVERIFY(tmpFile.open()); + QString tmpFileName = tmpFile.fileName(); + tmpFile.remove(); + + fileDialog()->setNextFileName(tmpFileName); + triggerAction("actionDatabaseNew"); + + DatabaseWidget* dbWidget = m_tabWidget->currentDatabaseWidget(); + + QWidget* databaseNewWidget = dbWidget->findChild("changeMasterKeyWidget"); + QList databaseNewWidgets = dbWidget->findChildren("changeMasterKeyWidget"); + PasswordEdit* editPassword = databaseNewWidget->findChild("enterPasswordEdit"); + QVERIFY(editPassword->isVisible()); + + QLineEdit* editPasswordRepeat = databaseNewWidget->findChild("repeatPasswordEdit"); + QVERIFY(editPasswordRepeat->isVisible()); + + m_tabWidget->currentDatabaseWidget()->setCurrentWidget(databaseNewWidget); + + QTest::keyClicks(editPassword, "test"); + QTest::keyClicks(editPasswordRepeat, "test"); + QTest::keyClick(editPasswordRepeat, Qt::Key_Enter); + + QTRY_VERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).contains("*")); + + m_db = m_tabWidget->currentDatabaseWidget()->database(); + + // there is a new empty db + QCOMPARE(m_db->rootGroup()->children().size(), 0); + + // close the new database + MessageBox::setNextAnswer(QMessageBox::No); + triggerAction("actionDatabaseClose"); + Tools::wait(100); +} + void TestGui::testMergeDatabase() { // It is safe to ignore the warning this line produces @@ -332,15 +372,27 @@ void TestGui::testAddEntry() QTest::keyClicks(passwordRepeatEdit, "something 2"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); - // Add entry "something 3" + // Add entry "something 3" using the apply button then click ok QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "something 3"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + // Add entry "something 4" using the apply button then click cancel + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QTest::keyClicks(titleEdit, "something 4"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton); + + // Add entry "something 5" but click cancel button (does NOT add entry) + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QTest::keyClicks(titleEdit, "something 5"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton); + QApplication::processEvents(); - // Confirm that 4 entries now exist - QTRY_COMPARE(entryView->model()->rowCount(), 4); + // Confirm that 5 entries now exist + QTRY_COMPARE(entryView->model()->rowCount(), 5); } void TestGui::testPasswordEntryEntropy() @@ -513,7 +565,7 @@ void TestGui::testTotp() void TestGui::testSearch() { // Add canned entries for consistent testing - testAddEntry(); + Q_UNUSED(addCannedEntries()); QToolBar* toolBar = m_mainWindow->findChild("toolBar"); @@ -629,7 +681,7 @@ void TestGui::testSearch() void TestGui::testDeleteEntry() { // Add canned entries for consistent testing - testAddEntry(); + Q_UNUSED(addCannedEntries()); GroupView* groupView = m_dbWidget->findChild("groupView"); EntryView* entryView = m_dbWidget->findChild("entryView"); @@ -905,6 +957,42 @@ void TestGui::cleanupTestCase() delete m_mainWindow; } +int TestGui::addCannedEntries() +{ + int entries_added = 0; + + // Find buttons + QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); + EditEntryWidget* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); + QLineEdit* passwordEdit = editEntryWidget->findChild("passwordEdit"); + QLineEdit* passwordRepeatEdit = editEntryWidget->findChild("passwordRepeatEdit"); + + // Add entry "test" and confirm added + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QTest::keyClicks(titleEdit, "test"); + QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + ++entries_added; + + // Add entry "something 2" + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QTest::keyClicks(titleEdit, "something 2"); + QTest::keyClicks(passwordEdit, "something 2"); + QTest::keyClicks(passwordRepeatEdit, "something 2"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + ++entries_added; + + // Add entry "something 3" + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QTest::keyClicks(titleEdit, "something 3"); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + ++entries_added; + + return entries_added; +} + void TestGui::checkDatabase(QString dbFileName) { if (dbFileName.isEmpty()) diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 904e5f21..5ec8237b 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -40,6 +40,7 @@ private slots: void cleanup(); void cleanupTestCase(); + void testCreateDatabase(); void testMergeDatabase(); void testAutoreloadDatabase(); void testTabs(); @@ -61,6 +62,7 @@ private slots: void testDatabaseLocking(); private: + int addCannedEntries(); void checkDatabase(QString dbFileName = ""); void triggerAction(const QString& name); void dragAndDropGroup(const QModelIndex& sourceIndex, const QModelIndex& targetIndex, int row, diff --git a/utils/fix_mac.sh b/utils/fix_mac.sh deleted file mode 100755 index 2e4e84e5..00000000 --- a/utils/fix_mac.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# Canonical path to qt5 directory -QT="/usr/local/Cellar/qt" -if [ ! -d "$QT" ]; then - # Alternative (old) path to qt5 directory - QT+="5" - if [ ! -d "$QT" ]; then - echo "Qt/Qt5 not found!" - exit - fi -fi -QT5_DIR="$QT/$(ls $QT | sort -r | head -n1)" -echo $QT5_DIR - -# Change qt5 framework ids -for framework in $(find "$QT5_DIR/lib" -regex ".*/\(Qt[a-zA-Z]*\)\.framework/Versions/5/\1"); do - echo "$framework" - install_name_tool -id "$framework" "$framework" -done