diff --git a/.travis.yml b/.travis.yml index df7f8ed58..942bb426d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,8 @@ compiler: - gcc env: - - CONFIG=Release ASAN_OPTIONS=detect_odr_violation=1:leak_check_at_exit=0 - - CONFIG=Debug ASAN_OPTIONS=detect_odr_violation=1:leak_check_at_exit=0 + - CONFIG=Release ASAN_OPTIONS=detect_odr_violation=1 + - CONFIG=Debug ASAN_OPTIONS=detect_odr_violation=1 git: depth: 3 @@ -37,7 +37,7 @@ script: - cmake -DCMAKE_BUILD_TYPE=${CONFIG} -DWITH_GUI_TESTS=ON -DWITH_ASAN=ON -DWITH_XC_HTTP=ON -DWITH_XC_AUTOTYPE=ON -DWITH_XC_YUBIKEY=ON $CMAKE_ARGS .. - make -j2 - if [ "$TRAVIS_OS_NAME" = "linux" ]; then make test ARGS+="-E testgui --output-on-failure"; fi - - if [ "$TRAVIS_OS_NAME" = "linux" ]; then xvfb-run -a --server-args="-screen 0 800x600x24" make test ARGS+="-R testgui --output-on-failure"; fi + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ASAN_OPTIONS=${ASAN_OPTIONS}:leak_check_at_exit=0 xvfb-run -a --server-args="-screen 0 800x600x24" make test ARGS+="-R testgui --output-on-failure"; fi - if [ "$TRAVIS_OS_NAME" = "osx" ]; then make test ARGS+="--output-on-failure"; fi # Generate snapcraft build when merging into master/develop branches diff --git a/AppImage-Recipe.sh b/AppImage-Recipe.sh index 4cb5ceda1..f8d7e105f 100755 --- a/AppImage-Recipe.sh +++ b/AppImage-Recipe.sh @@ -87,14 +87,15 @@ else fi EOF chmod +x ./usr/bin/keepassxc_env -sed -i 's/Exec=keepassxc/Exec=keepassxc_env/' org.${LOWERAPP}.desktop -get_desktopintegration "org.${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 ../KeePassXC-${VERSION}-${ARCH}.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 3719f8e4c..24e935548 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +2.2.4 (2017-12-13) +========================= + +- Prevent database corruption when locked [#1219] +- Fixes apply button not saving new entries [#1141] +- Switch to Consolas font on Windows for password edit [#1229] +- Multiple fixes to AppImage deployment [#1115, #1228] +- Fixes multiple memory leaks [#1213] +- Resize message close to 16x16 pixels [#1253] + 2.2.2 (2017-10-22) ========================= diff --git a/CMakeLists.txt b/CMakeLists.txt index c5cc9e41b..a92f018f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,16 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +cmake_minimum_required(VERSION 3.1.0) + +project(KeePassXC) + if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Choose the type of build, options are: None Debug Release RelWithDebInfo Debug DebugFull Profile MinSizeRel." FORCE) endif() -project(KeePassXC) - -cmake_minimum_required(VERSION 3.1.0) - set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) # Support Visual Studio Code @@ -43,13 +43,14 @@ 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 "2") +set(KEEPASSXC_VERSION_PATCH "4") set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION_MAJOR}.${KEEPASSXC_VERSION_MINOR}.${KEEPASSXC_VERSION_PATCH}") # Distribution info @@ -96,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") diff --git a/COPYING b/COPYING index e12f4534c..7aa9c0333 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 @@ -179,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 ad9a2f442..a5966be39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,8 @@ RUN set -x \ && apt-get -y install software-properties-common RUN set -x \ - && add-apt-repository ppa:beineri/opt-qt${QT5_PPA_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 -y \ @@ -42,7 +43,9 @@ RUN set -x \ zlib1g-dev \ libxi-dev \ libxtst-dev \ - mesa-common-dev + mesa-common-dev \ + libyubikey-dev \ + libykpers-1-dev ENV CMAKE_PREFIX_PATH=/opt/qt${QT5_VERSION}/lib/cmake ENV LD_LIBRARY_PATH=/opt/qt${QT5_VERSION}/lib @@ -52,34 +55,8 @@ RUN set -x \ # AppImage dependencies RUN set -x \ && apt-get install -y \ - wget \ - libfuse2 - -# build libyubikey -ENV YUBIKEY_VERSION=1.13 -RUN set -x \ - && wget "https://developers.yubico.com/yubico-c/Releases/libyubikey-${YUBIKEY_VERSION}.tar.gz" \ - && tar xf libyubikey-${YUBIKEY_VERSION}.tar.gz \ - && cd libyubikey-${YUBIKEY_VERSION} \ - && ./configure --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu \ - && make \ - && make install \ - && cd .. \ - && rm -Rf libyubikey-${YUBIKEY_VERSION}* - -# build libykpers-1 -ENV YKPERS_VERSION=1.18.0 -RUN set -x \ - && apt-get install -y libusb-dev -RUN set -x \ - && wget "https://developers.yubico.com/yubikey-personalization/Releases/ykpers-${YKPERS_VERSION}.tar.gz" \ - && tar xf ykpers-${YKPERS_VERSION}.tar.gz \ - && cd ykpers-${YKPERS_VERSION} \ - && ./configure --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu \ - && make \ - && make install \ - && cd .. \ - && rm -Rf ykpers-${YKPERS_VERSION}* + libfuse2 \ + wget VOLUME /keepassxc/src VOLUME /keepassxc/out diff --git a/INSTALL.md b/INSTALL.md index 45efa7a21..2d3f7cb2b 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 e379123c1..16f382b03 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) @@ -19,67 +29,39 @@ KeePass Cross-platform Community Edition - Using website favicons as entry icons - Merging of databases - Automatic reload when the database changed on disk -- KeePassHTTP support for use 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. +- 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](./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 / macOS only) (default: OFF) - -DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF) -``` - -: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). - -### 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 000000000..de4b39261 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 000000000..989294a4e 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 000000000..55b0f3d49 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 000000000..b4b2d38ca --- /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 8ad547468..a3813fd93 100755 --- a/release-tool +++ b/release-tool @@ -50,14 +50,14 @@ printUsage() { local cmd if [ "" == "$1" ] || [ "help" == "$1" ]; then cmd="COMMAND" - elif [ "check" == "$1" ] || [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "sign" == "$1" ]; then + elif [ "check" == "$1" ] || [ "merge" == "$1" ] || [ "build" == "$1" ] || [ "gpgsign" == "$1" ] || [ "appsign" == "$1" ]; then cmd="$1" else logError "Unknown command: '$1'\n" cmd="COMMAND" fi - - printf "\e[1mUsage:\e[0m $(basename $0) $cmd [--version x.y.z] [options]\n" + + printf "\e[1mUsage:\e[0m $(basename $0) $cmd [options]\n" if [ "COMMAND" == "$cmd" ]; then cat << EOF @@ -66,7 +66,8 @@ Commands: check Perform a dry-run check, nothing is changed merge Merge release branch into main branch and create release tags build Build and package binary release from sources - sign Sign previously compiled release packages + gpgsign Sign previously compiled release packages with GPG + appsign Sign binaries with code signing certificates on Windows and macOS help Show help for the given command EOF elif [ "merge" == "$cmd" ]; then @@ -113,16 +114,25 @@ Options: -n, --no-source-tarball Don't build source tarball -h, --help Show this help EOF - elif [ "sign" == "$cmd" ]; then + elif [ "gpgsign" == "$cmd" ]; then cat << EOF -Sign previously compiled release packages +Sign previously compiled release packages with GPG Options: -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 + elif [ "appsign" == "$cmd" ]; then + cat << EOF + +Sign binaries with code signing certificates on Windows and macOS + +Options: + -f, --files Files to sign (required) + -k, --signtool-key Key to be used with signtool (required for Windows EXE) + -i, --identity Apple Developer ID to be used with codesign (required for macOS APP and DMG) -h, --help Show this help EOF fi @@ -264,13 +274,13 @@ checkChangeLog() { } checkAppStreamInfo() { - if [ ! -f share/linux/org.keepassxc.appdata.xml ]; then + if [ ! -f share/linux/org.keepassxc.KeePassXC.appdata.xml ]; then exitError "No AppStream info file found!" fi - grep -qPzo "" share/linux/org.keepassxc.appdata.xml + grep -qPzo "" share/linux/org.keepassxc.KeePassXC.appdata.xml if [ $? -ne 0 ]; then - exitError "'share/linux/org.keepassxc.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!" + exitError "'share/linux/org.keepassxc.KeePassXC.appdata.xml' has not been updated to the '${RELEASE_NAME}' release!" fi } @@ -289,7 +299,28 @@ checkSnapcraft() { checkTransifexCommandExists() { command -v tx > /dev/null if [ 0 -ne $? ]; then - exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'" + exitError "Transifex tool 'tx' not installed! Please install it using 'pip install transifex-client'." + fi +} + +checkOsslsigncodeCommandExists() { + command -v osslsigncode > /dev/null + if [ 0 -ne $? ]; then + exitError "osslsigncode command not found on the PATH! Please install it using 'pacman -S mingw-w64-osslsigncode'." + fi +} + +checkCodesignCommandExists() { + command -v codesign > /dev/null + if [ 0 -ne $? ]; then + exitError "codesign command not found on the PATH! Please check that you have correctly installed Xcode." + fi +} + +checkCreateDMGCommandExists() { + command -v create-dmg > /dev/null + if [ 0 -ne $? ]; then + exitError "create-dmg command not found on the PATH! Please install it using 'npm install --global create-dmg'." fi } @@ -665,81 +696,54 @@ build() { # ----------------------------------------------------------------------- -# sign command +# gpgsign command # ----------------------------------------------------------------------- -sign() { - SIGN_FILES=() - SIGNTOOL="signtool" - SIGNTOOL_KEY="" - +gpgsign() { + local sign_files=() + while [ $# -ge 1 ]; do local arg="$1" case "$arg" in -f|--files) while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do - SIGN_FILES+=("$2") + sign_files+=("$2") shift done ;; - + -g|--gpg-key) GPG_KEY="$2" shift ;; - --signtool) - SIGNTOOL="$2" - shift ;; - - --signtool-key) - SIGNTOOL_KEY="$2" - shift ;; - -h|--help) - printUsage "sign" + printUsage "gpgsign" exit ;; - + *) logError "Unknown option '$arg'\n" - printUsage "sign" + printUsage "gpgsign" exit 1 ;; esac shift done - if [ -z "$SIGN_FILES" ]; then + if [ -z "${sign_files}" ]; then logError "Missing arguments, --files is required!\n" - printUsage "sign" + printUsage "gpgsign" 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 + for f in "${sign_files[@]}"; do if [ ! -f "$f" ]; then - exitError "File '${f}' does not exist!" + exitError "File '${f}' does not exist or is not a file!" 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}' using release key..." gpg --output "${f}.sig" --armor --local-user "$GPG_KEY" --detach-sig "$f" if [ 0 -ne $? ]; then exitError "Signing failed!" fi - + logInfo "Creating digest for file '${f}'..." local rp="$(realpath "$f")" local bname="$(basename "$f")" @@ -750,6 +754,143 @@ sign() { } + +# ----------------------------------------------------------------------- +# appsign command +# ----------------------------------------------------------------------- +appsign() { + local sign_files=() + local signtool_key + local codesign_identity + + while [ $# -ge 1 ]; do + local arg="$1" + case "$arg" in + -f|--files) + while [ "${2:0:1}" != "-" ] && [ $# -ge 2 ]; do + sign_files+=("$2") + shift + done ;; + + -k|--signtool-key) + signtool_key="$2" + shift ;; + + -i|--identity) + codesign_identity="$2" + shift ;; + + -h|--help) + printUsage "appsign" + exit ;; + + *) + logError "Unknown option '$arg'\n" + printUsage "appsign" + exit 1 ;; + esac + shift + done + + if [ -z "${sign_files}" ]; then + logError "Missing arguments, --files is required!\n" + printUsage "appsign" + exit 1 + fi + + for f in "${sign_files[@]}"; do + if [ ! -f "${f}" ]; then + exitError "File '${f}' does not exist or is not a file!" + fi + done + + if [ "$(uname -s)" == "Darwin" ]; then + if [ -z "${codesign_identity}" ]; then + logError "Missing arguments, --identity is required on macOS!\n" + printUsage "appsign" + exit 1 + fi + + checkCodesignCommandExists + checkCreateDMGCommandExists + + local orig_dir="$(pwd)" + for f in "${sign_files[@]}"; do + if [[ ${f: -4} == '.dmg' ]]; then + logInfo "Unpacking disk image '${f}'..." + local tmp_dir="/tmp/KeePassXC_${RANDOM}" + mkdir -p ${tmp_dir}/{mnt,app} + hdiutil attach -quiet -noautoopen -mountpoint ${tmp_dir}/mnt "${f}" + cd ${tmp_dir} + cp -a ./mnt/KeePassXC.app ./app + hdiutil detach -quiet ${tmp_dir}/mnt + + if [ ! -d ./app/KeePassXC.app ]; then + cd "${orig_dir}" + exitError "Unpacking failed!" + fi + + logInfo "Signing app using codesign..." + codesign --sign "${codesign_identity}" --verbose --deep ./app/KeePassXC.app + + if [ 0 -ne $? ]; then + cd "${orig_dir}" + exitError "Signing failed!" + fi + + logInfo "Repacking and signing disk image..." + create-dmg ./app/KeePassXC.app + cd "${orig_dir}" + cp -f ${tmp_dir}/KeePassXC-*.dmg "${f}" + rm -Rf ${tmp_dir} + else + logInfo "Skipping non-DMG file '${f}'..." + fi + done + + elif [ "$(uname -o)" == "Msys" ]; then + if [ -z "${signtool_key}" ]; then + logError "Missing arguments, --signtool-key is required on Windows!\n" + printUsage "appsign" + exit 1 + fi + + checkOsslsigncodeCommandExists + + if [[ ! -f "${signtool_key}" ]]; then + exitError "Key file was not found!" + fi + + read -s -p "Key password: " password + echo + + for f in "${sign_files[@]}"; do + if [[ ${f: -4} == '.exe' ]]; then + logInfo "Signing file '${f}' using osslsigncode..." + # output a signed exe; we have to use a different name due to osslsigntool limitations + osslsigncode sign -pkcs12 "${signtool_key}" -pass "${password}" \ + -t "http://timestamp.comodoca.com/authenticode" -in "${f}" -out "${f}.signed" + + if [ 0 -ne $? ]; then + rm -f "${f}.signed" + exitError "Signing failed!" + fi + + # overwrite the original exe with the signed exe + mv -f "${f}.signed" "${f}" + else + logInfo "Skipping non-EXE file '${f}'..." + fi + done + + else + exitError "Unsupported platform for code signing!\n" + fi + + logInfo "All done!" +} + + # ----------------------------------------------------------------------- # parse global command line # ----------------------------------------------------------------------- @@ -762,8 +903,8 @@ if [ "" == "$MODE" ]; then elif [ "help" == "$MODE" ]; then printUsage "$1" exit -elif [ "check" == "$MODE" ] || [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] || [ "sign" == "$MODE" ]; then - $MODE "$@" +elif [ "check" == "$MODE" ] || [ "merge" == "$MODE" ] || [ "build" == "$MODE" ] || [ "gpgsign" == "$MODE" ] || [ "appsign" == "$MODE" ]; then + ${MODE} "$@" else printUsage "$MODE" fi diff --git a/share/CMakeLists.txt b/share/CMakeLists.txt index 6323ece88..81bb26938 100644 --- a/share/CMakeLists.txt +++ b/share/CMakeLists.txt @@ -30,8 +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/org.keepassxc.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) - install(FILES linux/org.keepassxc.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) + 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/16x16/actions/message-close.png b/share/icons/application/16x16/actions/message-close.png index 4b2f9ca4d..b3a44a232 100644 Binary files a/share/icons/application/16x16/actions/message-close.png and b/share/icons/application/16x16/actions/message-close.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 000000000..3e4d324c0 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 000000000..e913402f5 Binary files /dev/null and b/share/icons/svg/utilities-terminal.svgz differ diff --git a/share/keepassxc.ini b/share/keepassxc.ini index f7ff52cbc..c6f0654fe 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/org.keepassxc.appdata.xml b/share/linux/org.keepassxc.KeePassXC.appdata.xml similarity index 85% rename from share/linux/org.keepassxc.appdata.xml rename to share/linux/org.keepassxc.KeePassXC.appdata.xml index 563450a4f..e15d24f03 100644 --- a/share/linux/org.keepassxc.appdata.xml +++ b/share/linux/org.keepassxc.KeePassXC.appdata.xml @@ -1,16 +1,15 @@ - + - org.keepassxc + org.keepassxc.KeePassXC.desktop KeePassXC CC-BY-3.0 GPL-3.0+ - keepassxc - https://keepassxc.org 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 @@ -19,56 +18,53 @@

- org.keepassxc.desktop + 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 - - - https://keepassxc.org/images/screenshots/linux/screen_005.png + Icon Selection for Entry https://keepassxc.org/images/screenshots/linux/screen_006.png - - - https://keepassxc.org/images/screenshots/linux/screen_007.png - - - https://keepassxc.org/images/screenshots/linux/screen_008.png - - - https://keepassxc.org/images/screenshots/linux/screen_009.png - - - https://keepassxc.org/images/screenshots/linux/screen_010.png - - - https://keepassxc.org/images/screenshots/linux/screen_011.png - - - https://keepassxc.org/images/screenshots/linux/screen_012.png - - - https://keepassxc.org/images/screenshots/linux/screen_013.png - - - https://keepassxc.org/images/screenshots/linux/screen_014.png + Password Generator + + +
    +
  • Prevent database corruption when locked [#1219]
  • +
  • Fixes apply button not saving new entries [#1141]
  • +
  • Switch to Consolas font on Windows for password edit [#1229]
  • +
  • Multiple fixes to AppImage deployment [#1115, #1228]
  • +
  • Fixes multiple memory leaks [#1213]
  • +
  • Resize message close to 16x16 pixels [#1253]
  • +
+
+
+

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]
  • @@ -85,8 +81,10 @@
  • 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]
  • @@ -103,6 +101,7 @@ +

    Changes included in this release:

    • Added YubiKey 2FA integration for unlocking databases [#127]
    • Added TOTP support [#519]
    • @@ -138,6 +137,7 @@ +

      Changes included in this release:

      • Bumped KeePassHTTP version to 1.8.4.2
      • KeePassHTTP confirmation window comes to foreground [#466]
      • @@ -146,6 +146,7 @@ +

        Changes included in this release:

        • Fix possible overflow in zxcvbn library [#363]
        • Revert HiDPI setting to avoid problems on laptop screens [#332]
        • @@ -160,6 +161,7 @@ +

          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]
          • @@ -177,6 +179,7 @@ +

            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]
            • @@ -189,6 +192,7 @@ +

              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]
              • diff --git a/share/linux/org.keepassxc.desktop b/share/linux/org.keepassxc.KeePassXC.desktop similarity index 94% rename from share/linux/org.keepassxc.desktop rename to share/linux/org.keepassxc.KeePassXC.desktop index d3b007bdc..8a0169800 100644 --- a/share/linux/org.keepassxc.desktop +++ b/share/linux/org.keepassxc.KeePassXC.desktop @@ -9,6 +9,7 @@ Comment=Community-driven port of the Windows application “KeePass Password Saf Exec=keepassxc %f TryExec=keepassxc Icon=keepassxc +StartupWMClass=keepassxc Terminal=false Type=Application Version=1.0 diff --git a/share/translations/keepassx_ca.ts b/share/translations/keepassx_ca.ts new file mode 100644 index 000000000..8d837c563 --- /dev/null +++ b/share/translations/keepassx_ca.ts @@ -0,0 +1,2395 @@ + + + AboutDialog + + About KeePassXC + Sobre KeePassXC + + + About + Sobre + + + Contributors + Contribuïdors + + + Debug Info + Informació de depuració + + + Copy to clipboard + Copia al porta-retalls + + + Version %1 + + Versió %1 + + + + Revision: %1 + Revisió: %1 + + + Libraries: + Llibreries + + + Operating system: %1 +CPU architecture: %2 +Kernel: %3 %4 + Sistema operatiu: %1 +Arquitectura de la CPU: %2 +Nucli: %3 %4 + + + Enabled extensions: + Extensions habilitades: + + + Report bugs at: <a href="https://github.com/keepassxreboot/keepassxc/issues" style="text-decoration: underline;">https://github.com</a> + Reportar errors a: <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. + KeepassXC es distribueix sota els termes de la Llicència Pública General GNU (GPL) versió 2 o (segons la vostra opció) versió 3. + + + Project Maintainers: + Mantenidors del projecte: + + + <a href="https://github.com/keepassxreboot/keepassxc/graphs/contributors">See Contributions on GitHub</a> + <a href="https://github.com/keepassxreboot/keepassxc/graphs/contributors">Veure els contribuïdors a GitHub</a> + + + Include the following information whenever you report a bug: + Inclou la següent informació a l'hora de reportar un error: + + + Distribution: %1 + Distribució: %1 + + + + AccessControlDialog + + Remember this decision + Recorda aquesta decisió + + + Allow + Permet + + + Deny + Denega + + + %1 has requested access to passwords for the following item(s). +Please select whether you want to allow access. + + + + KeePassXC HTTP Confirm Access + KeePassXC HTTP Confirmeu l'accés + + + + AutoType + + Couldn't find an entry that matches the window title: + No hem trobat una entrada que coincidesca amb el títol de la finestra: + + + Auto-Type - KeePassXC + + + + + AutoTypeAssociationsModel + + Window + Finestra + + + Sequence + Seqüència + + + Default sequence + Seqüència per omissió + + + + AutoTypeSelectDialog + + Select entry to Auto-Type: + + + + Auto-Type - KeePassXC + + + + + ChangeMasterKeyWidget + + Password + Contrasenya + + + Enter password: + Introduïu la contrasenya: + + + Repeat password: + Repetiu la contrasenya: + + + Browse + Navegar + + + Create + Crea + + + Key files + Fitxers de clau + + + All files + Tots els fitxers + + + Create Key File... + Crea un arxiu clau... + + + Unable to create Key File : + + + + Select a key file + Seleccioneu un arxiu clau + + + Do you really want to use an empty string as password? + Realment voleu utilitzar una cadena buida com a contrasenya? + + + Different passwords supplied. + Les contrasenyes no coincideixen. + + + Failed to set %1 as the Key file: +%2 + No ha pogut definir %1 com a arxiu clau: %2 + + + &Key file + Arxiu clau + + + Cha&llenge Response + + + + Refresh + L'actualitza + + + Empty password + Contrasenya buida + + + Changing master key failed: no YubiKey inserted. + + + + + CloneDialog + + Clone Options + Clona les opcions + + + Replace username and password with references + Substituir el nom d'usuari i contrasenya amb referències + + + Copy history + Còpia el historial + + + Append ' - Clone' to title + Afegeix '-clon' al títol + + + + CsvImportWidget + + Import CSV fields + Importació de fitxer CSV + + + filename + nom del fitxer + + + size, rows, columns + mida, files, columnes + + + Encoding + Codificació + + + Codec + Còdec + + + Text is qualified by + Text està qualificat per + + + Fields are separated by + Els camps estan separats per + + + Comments start with + Els comentaris comencen amb + + + First record has field names + El primer registre conté els noms de camp + + + Number of headers line to discard + Número de línies amb capçaleres per a descartar + + + Consider '\' an escape character + Considera ' \' com un caràcter d'escapada + + + Preview + Visualització prèvia + + + Column layout + Columnes + + + Not present in CSV file + + + + Empty fieldname + Camp de nom buit + + + column + columna + + + Imported from CSV file + Importats d'un fitxer CSV + + + Original data: + Dades originals: + + + Error(s) detected in CSV file ! + S'ha(n) detectat error(s) al fitxer CSV ! + + + more messages skipped] + + + + Error + Error + + + CSV import: writer has errors: + + + + + + CsvImportWizard + + Import CSV file + + + + Error + Error + + + Unable to calculate master key + No es pot calcular la clau mestra + + + + CsvParserModel + + byte, + byte, + + + rows, + files, + + + columns + columnes + + + + DatabaseOpenWidget + + Enter master key + Introduïu la clau mestra + + + Key File: + Fitxer clau: + + + Password: + Contrasenya: + + + Browse + Navega + + + Unable to open the database. + No es pot obrir la base de dades. + + + Can't open key file + No es pot obrir el fitxer de clau + + + All files + Tots els fitxers + + + Key files + Arxius de clau + + + Select key file + Seleccioneu el fitxer de clau + + + Refresh + Actualitza + + + Challenge Response: + + + + + DatabaseRepairWidget + + Repair database + Repara la base de dades + + + Error + Error + + + Can't open key file + No es pot obrir el fitxer de clau + + + Database opened fine. Nothing to do. + + + + Unable to open the database. + No es pot obrir la base de dades. + + + Success + + + + The database has been successfully repaired +You can now save it. + + + + Unable to repair the database. + No es pot reparar la base de dades. + + + + DatabaseSettingsWidget + + Database name: + Nom de base de dades: + + + Database description: + Descripció de la base de dades: + + + Transform rounds: + + + + Default username: + Usuari per defecte: + + + MiB + MiB + + + Benchmark + + + + Max. history items: + N. max. d'elements al historial: + + + Max. history size: + Mida màx. del historial: + + + Use recycle bin + Utilitza la paperera + + + AES: 256 Bit (default) + AES: 256 bits (per defecte) + + + Twofish: 256 Bit + Twofish: 256 bits + + + Algorithm: + Algoritme: + + + + DatabaseTabWidget + + Root + Arrel + + + KeePass 2 Database + Base de dades de KeePass 2 + + + All files + Tots els fitxers + + + Open database + Obre la base de dades + + + File not found! + No s'ha trobat el fitxer! + + + Open KeePass 1 database + Obre base de dades de KeePass 1 + + + KeePass 1 database + Base de dades de KeePass 1 + + + All files (*) + Tots els arxius (*) + + + Close? + Voleu tancar? + + + Save changes? + Voleu desar els canvis? + + + "%1" was modified. +Save changes? + "%1" ha canviat. +Voleu desar els canvis? + + + Writing the database failed. + Ha fallat l'escriptura en la base de dades. + + + Save database as + Desa la base de dades com a + + + New database + Nova base de dades + + + locked + bloquejat + + + Lock database + Bloqueja la base de dades + + + Can't lock the database as you are currently editing it. +Please press cancel to finish your changes or discard them. + No es pot bloquejar la base de dades actualment en ús. +Per favor, feu clic a cancel·la per a finalitzar els canvis o descarteu-los. + + + This database has never been saved. +You can save the database or stop locking it. + Aquesta base de dades no s'ha desat. +Podeu desar la base de dades o deixar de bloquejar-la. + + + This database has been modified. +Do you want to save the database before locking it? +Otherwise your changes are lost. + Aquesta base de dades ha estat modificada. +Voleu desar la base de dades abans de tancar-la? +En cas contrari, es perderan els canvis. + + + "%1" is in edit mode. +Discard changes and close anyway? + "%1" està en mode d'edició. +Voleu descartar els canvis i tancar de totes maneres? + + + Export database to CSV file + Exporta la base de dades a un fitxer CSV + + + CSV file + Fitxer CSV + + + Writing the CSV file failed. + Ha fallat l'escriptura al fitxer CSV. + + + Unable to open the database. + No es pot obrir la base de dades. + + + Merge database + Fusiona la base de dades + + + The database you are trying to save as is locked by another instance of KeePassXC. +Do you want to save it anyway? + La base de dades que està intentant salvar està bloquejada per una altra instància de KeePassXC. +Voleu desar-la igualment? + + + Passwords + Contrasenyes + + + Database already opened + La base de dades ja està oberta + + + The database you are trying to open is locked by another instance of KeePassXC. + +Do you want to open it anyway? + La base de dades que està intentant obrir està bloquejada per una altra instància de KeePassXC. + +Voleu obrir-la igualment? + + + Open read-only + Obre en mode només de lectura + + + File opened in read only mode. + Arxiu obert en mode de només lectura. + + + Open CSV file + Obre arxiu CSV + + + + DatabaseWidget + + Change master key + Canvia la clau mestra + + + Delete entry? + Suprimir l'entrada? + + + Do you really want to delete the entry "%1" for good? + Realment voleu suprimir l'entrada "%1" per sempre? + + + Delete entries? + Suprimir les entrades? + + + Do you really want to delete %1 entries for good? + Realment voleu suprimir %1 entrades per sempre? + + + Move entries to recycle bin? + Moure les entrades a la paperera? + + + Do you really want to move %n entry(s) to the recycle bin? + Realment voleu moure %n entry(s) a la Paperera de reciclatge?Realment voleu moure %n entrada(es) a la paperera? + + + Delete group? + Voleu suprimir el grup? + + + Do you really want to delete the group "%1" for good? + Realment voleu suprimir el grup "%1" per sempre? + + + Unable to calculate master key + No es pot calcular la clau mestra + + + Move entry to recycle bin? + Moure l'entrada a la Paperera? + + + Do you really want to move entry "%1" to the recycle bin? + Realment voleu moure l'entrada "%1" a la paperera? + + + Searching... + Cercant... + + + No current database. + Cap base de dades actual. + + + No source database, nothing to do. + + + + Search Results (%1) + Resultats de la cerca (%1) + + + No Results + No hi ha resultats + + + Execute command? + Execute l'ordre? + + + Do you really want to execute the following command?<br><br>%1<br> + Voleu executar la següent ordre? <br><br>%1<br> + + + Remember my choice + Recordar la meva elecció + + + Autoreload Request + Tona a carregar la petició + + + The database file has changed. Do you want to load the changes? + El fitxer de base de dades ha canviat. Voleu carregar els canvis? + + + Merge Request + + + + The database file has changed and you have unsaved changes.Do you want to merge your changes? + El fitxer de base de dades ha canviat i té canvis no desats. Voleu combinar els canvis? + + + Could not open the new database file while attempting to autoreload this database. + + + + Empty recycle bin? + Buida la paperera? + + + Are you sure you want to permanently delete everything from your recycle bin? + Esteu segur que voleu suprimir permanentment tot el contingut de la paperera? + + + + EditEntryWidget + + Entry + Entrada + + + Advanced + Avançat + + + Icon + Icona + + + Auto-Type + + + + Properties + Propietats + + + History + Historial + + + Entry history + Historial de l'entrada + + + Add entry + Afegiu una entrada + + + Edit entry + Edita l'entrada + + + Different passwords supplied. + Les contrasenyes no coincideixen. + + + New attribute + Nou atribut + + + Select file + Seleccioneu el fitxer + + + Unable to open file + No es pot obrir el fitxer + + + Save attachment + Deseu el fitxer adjunt + + + Unable to save the attachment: + + No es por desar l'arxiu adjunt: + + + + Tomorrow + Demà + + + %n week(s) + %n setmanes%n setmana(es) + + + %n month(s) + %n mes (OS)%n mes(os) + + + 1 year + 1 any + + + Confirm Remove + Confirma la supressió + + + Are you sure you want to remove this attribute? + Esteu segur que voleu suprimir aquest atribut? + + + [PROTECTED] Press reveal to view or edit + [PROTEGIT] Premeu revelar per veure o editar + + + Are you sure you want to remove this attachment? + Esteu segur que voleu suprimir aquest fitxer adjunt? + + + + EditEntryWidgetAdvanced + + Additional attributes + Atributs addicionals + + + Add + Afegiu + + + Remove + Suprimiu + + + Attachments + Fitxers adjunts + + + Save + Desa + + + Open + Obre + + + Edit Name + Edita el nom + + + Protect + Protegeix + + + Reveal + Revela + + + + EditEntryWidgetAutoType + + Enable Auto-Type for this entry + + + + + + + + + + - + - + + + Window title: + Títol de la finestra: + + + Inherit default Auto-Type sequence from the &group + + + + &Use custom Auto-Type sequence: + + + + Use default se&quence + + + + Set custo&m sequence: + + + + Window Associations + + + + + EditEntryWidgetHistory + + Show + Mostra + + + Restore + Restaura + + + Delete + Suprimeix + + + Delete all + Suprimeix tots + + + + EditEntryWidgetMain + + Title: + Títol: + + + Username: + Nom d'usuari: + + + Password: + Contrasenya: + + + Repeat: + Repeteix: + + + URL: + URL: + + + Expires + Expira + + + Presets + Configuracions + + + Notes: + Notes: + + + + EditGroupWidget + + Group + Grup + + + Icon + Icona + + + Properties + Propietats + + + Add group + Afegeix un grup + + + Edit group + Edita el grup + + + Enable + Habilita + + + Disable + Inhabilita + + + Inherit from parent group (%1) + Hereta de grup pare (%1) + + + + EditGroupWidgetMain + + Name + Nom + + + Notes + Notes + + + Expires + Expira + + + Search + Cerca + + + Auto-Type + + + + &Use default Auto-Type sequence of parent group + + + + Set default Auto-Type se&quence + + + + + EditWidgetIcons + + Add custom icon + Afegeix una icona personalitzada + + + Delete custom icon + Suprimeix la icona personalitzada + + + Images + Imatges + + + All files + Tots els fitxers + + + Select Image + Seleccioneu la imatge + + + Download favicon + Descarregua el favicon + + + Unable to fetch favicon. + No es pot descarregar el favicon. + + + Can't read icon + No es pot llegir la icona + + + &Use default icon + + + + Use custo&m icon + + + + Confirm Delete + Confirma la supressió + + + This icon is used by %1 entries, and will be replaced by the default icon. Are you sure you want to delete it? + Aquesta icona s'utilitza en %1 entrades i serà substituïda per la icona per defecte. Esteu segur que voleu suprimir-la? + + + Hint: You can enable Google as a fallback under Tools>Settings>Security + Consell: Podeu activar Google com a recurs alternatiu a Eines > Configuració > Seguretat + + + Custom icon already exists + Ja existeix una icona personalitzada + + + + EditWidgetProperties + + Created: + Creat: + + + Modified: + Modificat: + + + Accessed: + Accedit: + + + Uuid: + UUID: + + + + Entry + + - Clone + -Clon + + + + EntryAttributesModel + + Name + Nom + + + + EntryHistoryModel + + Last modified + Darrera modificació + + + Title + Títol + + + Username + Nom d'usuari + + + URL + URL + + + + EntryModel + + Group + Grup + + + Title + Títol + + + Username + Nom d'usuari + + + URL + URL + + + Ref: + Reference abbreviation + Ref: + + + + Group + + Recycle Bin + Paperera + + + + HttpPasswordGeneratorWidget + + Length: + Longitud: + + + Character Types + Tipus de caràcter + + + Upper Case Letters + Lletra majúscula + + + A-Z + A-Z + + + Lower Case Letters + Lletra minúscula + + + a-z + a-z + + + Numbers + Números + + + 0-9 + + + + Special Characters + + + + /*_& ... + + + + Exclude look-alike characters + + + + Ensure that the password contains characters from every group + + + + + KMessageWidget + + &Close + + + + Close message + + + + + KeePass1OpenWidget + + Import KeePass1 database + + + + Unable to open the database. + No es pot obrir la base de dades. + + + + KeePass1Reader + + Unable to read keyfile. + + + + Not a KeePass database. + + + + Unsupported encryption algorithm. + + + + Unsupported KeePass database version. + + + + Root + Arrel + + + Unable to calculate master key + No es pot calcular la clau mestra + + + Wrong key or database file is corrupt. + + + + + KeePass2Reader + + Not a KeePass database. + + + + Unsupported KeePass database version. + + + + Wrong key or database file is corrupt. + + + + Unable to calculate master key + No es pot calcular la clau mestra + + + Unable to issue challenge-response. + + + + The selected file is an old KeePass 1 database (.kdb). + +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. + + + + + KeePass2Writer + + Unable to issue challenge-response. + + + + Unable to calculate master key + No es pot calcular la clau mestra + + + + Main + + Fatal error while testing the cryptographic functions. + + + + KeePassXC - Error + + + + The lock file could not be created. Single-instance mode disabled. + + + + Another instance of KeePassXC is already running. + + + + Existing single-instance lock file is invalid. Launching new instance. + + + + + MainWindow + + Open database + Obre la base de dades + + + Database settings + + + + Copy username to clipboard + + + + Copy password to clipboard + + + + Settings + + + + Show toolbar + + + + read-only + + + + Toggle window + + + + KeePass 2 Database + Base de dades de KeePass 2 + + + All files + Tots els fitxers + + + Save repaired database + + + + Writing the database failed. + Ha fallat l'escriptura en la base de dades. + + + &Recent databases + + + + E&ntries + + + + Copy att&ribute to clipboard + + + + &Groups + + + + &View + + + + &Quit + + + + &About + + + + &Save database + + + + &Close database + + + + &New database + + + + Merge from KeePassX database + + + + &Add new entry + + + + &View/Edit entry + + + + &Delete entry + + + + &Add new group + + + + &Edit group + + + + &Delete group + + + + &Database settings + + + + &Clone entry + + + + Timed one-time password + + + + Copy &TOTP + + + + Show TOTP + + + + &Find + + + + Copy &username + + + + Cop&y password + + + + &Settings + + + + &Perform Auto-Type + + + + &Open URL + + + + &Lock databases + + + + &Title + + + + &URL + + + + &Notes + + + + Password Generator + + + + Clear history + + + + &Database + + + + Import + + + + &Tools + + + + Empty recycle bin + + + + Access error for config file %1 + + + + Quit KeePassXC + + + + Please touch the button on your YubiKey! + + + + &Help + + + + &Open database... + + + + Sa&ve database as... + + + + Change &master key... + + + + &Export to CSV file... + + + + Import KeePass 1 database... + + + + Import CSV file... + + + + Re&pair database... + + + + Set up TOTP... + + + + + OptionDialog + + Dialog + + + + General + + + + Sh&ow a notification when credentials are requested + + + + Sort matching entries by &username + + + + Re&move all stored permissions from entries in active database + + + + Advanced + Avançat + + + Always allow &access to entries + + + + Always allow &updating entries + + + + Searc&h in all opened databases for matching entries + + + + HTTP Port: + + + + Default port: 19455 + + + + Re&quest to unlock the database if it is locked + + + + Sort &matching entries by title + + + + KeePassXC will listen to this port on 127.0.0.1 + + + + Cannot bind to privileged ports + + + + Cannot bind to privileged ports below 1024! +Using default port 19455. + + + + R&emove all shared encryption keys from active database + + + + &Return advanced string fields which start with "KPH: " + + + + Automatically creating or updating string fields is not supported. + + + + This is required for accessing your databases from ChromeIPass or PassIFox + + + + Enable KeePassHTTP server + + + + Only returns the best matches for a specific URL instead of all entries for the whole domain. + + + + &Return only best matching entries + + + + Only entries with the same scheme (http://, https://, ftp://, ...) are returned. + + + + &Match URL schemes + + + + Password Generator + + + + Only the selected database has to be connected with a client. + + + + The following options can be dangerous! +Change them only if you know what you are doing. + + + + + PasswordGeneratorWidget + + Password: + Contrasenya: + + + Character Types + Tipus de caràcter + + + Upper Case Letters + Lletra majúscula + + + Lower Case Letters + Lletra minúscula + + + Numbers + Números + + + Special Characters + + + + Exclude look-alike characters + + + + Accept + + + + %p% + + + + strength + + + + entropy + + + + &Length: + + + + Pick characters from every group + + + + Generate + + + + Close + + + + Apply + + + + Entropy: %1 bit + + + + Password Quality: %1 + + + + Poor + + + + Weak + + + + Good + + + + Excellent + + + + Password + Contrasenya + + + Extended ASCII + + + + Passphrase + + + + Wordlist: + + + + Word Count: + + + + Word Separator: + + + + Copy + + + + + QObject + + NULL device + + + + error reading from device + + + + file empty ! + + + + + malformed string + + + + missing closing quote + + + + INTERNAL - unget lower bound exceeded + + + + Group + Grup + + + Title + Títol + + + Username + Nom d'usuari + + + Password + Contrasenya + + + URL + URL + + + Notes + Notes + + + Browser Integration + + + + YubiKey[%1] Challenge Response - Slot %2 - %3 + + + + Press + + + + Passive + + + + + QtIOCompressor + + Internal zlib error when compressing: + + + + Error writing to underlying device: + + + + Error opening underlying device: + + + + Error reading data from underlying device: + + + + Internal zlib error when decompressing: + + + + + QtIOCompressor::open + + The gzip format not supported in this version of zlib. + + + + Internal zlib error: + + + + + SearchWidget + + Case Sensitive + + + + Search + Cerca + + + Clear + + + + Search... + + + + Limit search to selected group + + + + + Service + + A shared encryption-key with the name "%1" already exists. +Do you want to overwrite it? + + + + Do you want to update the information in %1 - %2? + + + + The active database is locked! +Please unlock the selected database or choose another one which is unlocked. + + + + Successfully removed %1 encryption-%2 from KeePassX/Http Settings. + + + + No shared encryption-keys found in KeePassHttp Settings. + + + + The active database does not contain an entry of KeePassHttp Settings. + + + + Removing stored permissions... + + + + Abort + + + + Successfully removed permissions from %1 %2. + + + + The active database does not contain an entry with permissions. + + + + KeePassXC: New key association request + + + + You have received an association request for the above key. +If you would like to allow it access to your KeePassXC database +give it a unique name to identify and accept it. + + + + KeePassXC: Overwrite existing key? + + + + KeePassXC: Update Entry + + + + KeePassXC: Database locked! + + + + KeePassXC: Removed keys from database + + + + KeePassXC: No keys found + + + + KeePassXC: Settings not available! + + + + KeePassXC: Removed permissions + + + + KeePassXC: No entry with permissions found! + + + + + SettingsWidget + + Application Settings + + + + General + + + + Security + + + + Access error for config file %1 + + + + + SettingsWidgetGeneral + + Remember last databases + + + + Automatically save on exit + + + + Automatically save after every change + + + + Minimize when copying to clipboard + + + + Use group icon on entry creation + + + + Global Auto-Type shortcut + + + + Language + + + + Show a system tray icon + + + + Hide window to system tray when minimized + + + + Load previous databases on startup + + + + Automatically reload the database when modified externally + + + + Hide window to system tray instead of app exit + + + + Minimize window at application startup + + + + Basic Settings + + + + Remember last key files and security dongles + + + + Don't mark database as modified for non-data changes (e.g., expanding groups) + + + + Auto-Type + + + + Use entry title and URL to match windows for global Auto-Type + + + + Always ask before performing Auto-Type + + + + Auto-Type delay + + + + ms + + + + Start only a single instance of KeePassXC + + + + + SettingsWidgetSecurity + + Clear clipboard after + + + + sec + + + + Lock databases after inactivity of + + + + Show passwords in cleartext by default + + + + Lock databases after minimizing the window + + + + Don't require password repeat when it is visible + + + + Timeouts + + + + Convenience + + + + Lock databases when session is locked or lid is closed + + + + Privacy + + + + Use Google as fallback for downloading website icons + + + + + SetupTotpDialog + + Setup TOTP + + + + Key: + + + + Use custom settings + + + + Note: Change these settings only if you know what you are doing. + + + + Time step: + + + + 8 digits + + + + 6 digits + + + + Code size: + + + + sec + + + + + TotpDialog + + Timed Password + + + + 000000 + + + + Copy + + + + Expires in + + + + seconds + + + + + UnlockDatabaseWidget + + Unlock database + + + + + WelcomeWidget + + Welcome to KeePassXC + + + + Start storing your passwords securely in a KeePassXC database + + + + Create new database + + + + Open existing database + + + + Import from KeePass 1 + + + + Import from CSV + + + + Recent databases + + + + + main + + path to a custom config file + + + + key file of the database + + + + KeePassXC - cross-platform password manager + + + + read password of the database from stdin + + + + filenames of the password databases to open (*.kdbx) + + + + Copy a password to the clipboard + + + + Path of the database. + + + + Use a GUI prompt unlocking the database. + + + + Name of the entry to clip. + + + + Extract and print the content of a database. + + + + Path of the database to extract. + + + + Name of the command to execute. + + + + List database entries. + + + + Path of the group to list. Default is / + + + + Print the UUIDs of the entries and groups. + + + + Merge two databases. + + + + Path of the database to merge into. + + + + Path of the database to merge from. + + + + Use the same password for both database files. + + + + Show a password. + + + + Name of the entry to show. + + + + \ No newline at end of file diff --git a/share/translations/keepassx_de.ts b/share/translations/keepassx_de.ts index 2fd20f04a..f3bd0e39f 100644 --- a/share/translations/keepassx_de.ts +++ b/share/translations/keepassx_de.ts @@ -571,7 +571,7 @@ Sie können sie speichern oder die Sperre freigeben. Do you want to save the database before locking it? Otherwise your changes are lost. Dieses Datenbank wurde geändert. -Soll sie gespeichert werden bevor sie gesperrt wirt? +Soll sie gespeichert werden bevor sie gesperrt wird? Anderenfalls gehen Ihre Änderungen verloren. diff --git a/share/translations/keepassx_es.ts b/share/translations/keepassx_es.ts index f8176b807..479284898 100644 --- a/share/translations/keepassx_es.ts +++ b/share/translations/keepassx_es.ts @@ -69,7 +69,7 @@ Núcleo: %3 %4 Distribution: %1 - + Distribución: %1 @@ -250,7 +250,7 @@ Por favor seleccione si desea autorizar su acceso. Codec - Codificación + Códec Text is qualified by @@ -302,11 +302,11 @@ Por favor seleccione si desea autorizar su acceso. Original data: - Datos originales: + Dato original: Error(s) detected in CSV file ! - ¡Se detectaron errores en el archivo CSV! + ¡Error(es) detectado(s) en el archivo CSV! more messages skipped] @@ -728,7 +728,7 @@ Do you want to open it anyway? Merge Request - Solicitud de unión + Solicitud de Unión The database file has changed and you have unsaved changes.Do you want to merge your changes? @@ -744,7 +744,7 @@ Do you want to open it anyway? Are you sure you want to permanently delete everything from your recycle bin? - ¿Está seguro(a) que quiere permanentemente eliminar todo de su papelera de reciclaje? + ¿Está seguro que quiere eliminar permanentemente todo de su papelera de reciclaje? @@ -795,7 +795,7 @@ Do you want to open it anyway? Select file - Seleccionar archivo llave + Seleccionar archivo Unable to open file @@ -817,11 +817,11 @@ Do you want to open it anyway? %n week(s) - %n semana%n semanas + %n semana%n semana(s) %n month(s) - %n mes%n meses + %n mes%n mes(es) 1 year @@ -969,7 +969,7 @@ Do you want to open it anyway? Presets - Predeterminado + Programar Notes: @@ -1098,7 +1098,7 @@ Do you want to open it anyway? Custom icon already exists - + El icono personalizado ya existe @@ -1352,7 +1352,7 @@ Esta migración es en único sentido. No podrá abrir la base de datos importada Existing single-instance lock file is invalid. Launching new instance. - + El archivo de bloqueo de instancia única existente no es válido. Lanzando nueva instancia. @@ -1483,7 +1483,7 @@ Esta migración es en único sentido. No podrá abrir la base de datos importada Timed one-time password - Contraseña programada de único uso (TOTP) + Contraseña programada de único uso Copy &TOTP diff --git a/share/translations/keepassx_fr.ts b/share/translations/keepassx_fr.ts index 90580e146..f3fa01920 100644 --- a/share/translations/keepassx_fr.ts +++ b/share/translations/keepassx_fr.ts @@ -29,27 +29,27 @@ Revision: %1 - Revision: %1 + Révision : %1 Libraries: - Bibliothèques: + Bibliothèques : Operating system: %1 CPU architecture: %2 Kernel: %3 %4 - Système d'exploitation: %1 -Architecture CPU: %2 -Kernel: %3 %4 + Système d'exploitation : %1 +Architecture CPU : %2 +Kernel : %3 %4 Enabled extensions: - Extensions activées: + Extensions activées : Report bugs at: <a href="https://github.com/keepassxreboot/keepassxc/issues" style="text-decoration: underline;">https://github.com</a> - Signaler les bugs sur: <a href="https://github.com/keepassxreboot/keepassxc/issues" style="text-decoration: underline;">https://github.com</a> + Signaler les bugs sur : <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: - Mainteneurs du projet: + Mainteneurs du projet : <a href="https://github.com/keepassxreboot/keepassxc/graphs/contributors">See Contributions on GitHub</a> @@ -65,11 +65,11 @@ Kernel: %3 %4 Include the following information whenever you report a bug: - Inclure l'information suivante lorsque vous signaler un bug: + Inclure l'information suivante lorsque vous signaler un bug : Distribution: %1 - + Distribution : %1 @@ -302,7 +302,7 @@ Veuillez sélectionner si vous souhaitez autoriser l’accès. Original data: - Données originales: + Données originales : Error(s) detected in CSV file ! @@ -397,7 +397,7 @@ Veuillez sélectionner si vous souhaitez autoriser l’accès. Challenge Response: - Challenge-réponse: + Challenge-réponse : @@ -477,15 +477,15 @@ Vous pouvez maintenant la sauvegarder. AES: 256 Bit (default) - AES: 256 Bits (par défault) + AES : 256 Bits (par défaut) Twofish: 256 Bit - Twofish: 256 bits + Twofish : 256 bits Algorithm: - Algorithme: + Algorithme : @@ -737,7 +737,7 @@ Voulez vous l'ouvrir quand même ? Could not open the new database file while attempting to autoreload this database. - La nouvelle base de données ne peux être ouverte pendant qu'un rafraîchissement automatique de l'actuelle est en cours. + La nouvelle base de données ne peut être ouverte pendant qu'un rafraîchissement automatique de l'actuelle est en cours. Empty recycle bin? @@ -1095,11 +1095,11 @@ Voulez vous l'ouvrir quand même ? Hint: You can enable Google as a fallback under Tools>Settings>Security - Astuce: Vous pouvez activer Google en tant que repli sous Outils>Paramètres>Sécurité + Astuce : Vous pouvez activer Google en tant que repli sous Outils>Paramètres>Sécurité Custom icon already exists - + L'icône personnalisée existe déjà @@ -1175,7 +1175,7 @@ Voulez vous l'ouvrir quand même ? Ref: Reference abbreviation - Réf: + Réf : @@ -1353,7 +1353,7 @@ Il s'agit d'une migration à sens unique. Vous ne pourrez pas ouvrir l Existing single-instance lock file is invalid. Launching new instance. - + Le fichier de verrouillage de l’instance unique existant n’est pas valide. Lancement d'une nouvelle instance. @@ -1647,11 +1647,11 @@ Il s'agit d'une migration à sens unique. Vous ne pourrez pas ouvrir l HTTP Port: - Port HTTP: + Port HTTP : Default port: 19455 - Port par défaut: 19455 + Port par défaut : 19455 Re&quest to unlock the database if it is locked @@ -1681,7 +1681,7 @@ Restauration du port 19455 par défaut. &Return advanced string fields which start with "KPH: " - & Retourne les champs avancés de type chaîne qui commencent par "KPH:" + & Retourne les champs avancés de type chaîne qui commencent par "KPH :" Automatically creating or updating string fields is not supported. @@ -1774,7 +1774,7 @@ Ne les changez que si vous savez ce que vous faites. &Length: - &Longueur: + &Longueur : Pick characters from every group @@ -1794,11 +1794,11 @@ Ne les changez que si vous savez ce que vous faites. Entropy: %1 bit - Entropie: %1 bit + Entropie : %1 bit Password Quality: %1 - Qualité du mot de passe: %1 + Qualité du mot de passe : %1 Poor @@ -1830,15 +1830,15 @@ Ne les changez que si vous savez ce que vous faites. Wordlist: - Liste de mots: + Liste de mots : Word Count: - Nombre de mots: + Nombre de mots : Word Separator: - Séparateur de mot: + Séparateur de mot : Copy @@ -2019,7 +2019,7 @@ Veuillez déverrouiller la base de données sélectionnée ou en choisir une qui KeePassXC: New key association request - KeePassXC: nouvelle demande d'association + KeePassXC : nouvelle demande d'association You have received an association request for the above key. @@ -2031,23 +2031,23 @@ attribuez lui un nom unique pour l'identifier et acceptez la. KeePassXC: Overwrite existing key? - KeePassXC: Écraser la clé existante ? + KeePassXC : Écraser la clé existante ? KeePassXC: Update Entry - KeePassXC: Mettre à jour l'entrée + KeePassXC : Mettre à jour l'entrée KeePassXC: Database locked! - KeePassXC: Base de données verrouillée ! + KeePassXC : Base de données verrouillée ! KeePassXC: Removed keys from database - KeePassXC: Les clés ont été effacées de la base de donnée + KeePassXC : Les clés ont été effacées de la base de donnée KeePassXC: No keys found - KeePassXC: Aucune clé trouvée + KeePassXC : Aucune clé trouvée KeePassXC: Settings not available! @@ -2055,11 +2055,11 @@ attribuez lui un nom unique pour l'identifier et acceptez la. KeePassXC: Removed permissions - KeePassXC: Permissions retirées + KeePassXC : Permissions retirées KeePassXC: No entry with permissions found! - KeePassXC: Aucune entrée avec permissions trouvée ! + KeePassXC : Aucune entrée avec permissions trouvée ! @@ -2145,7 +2145,7 @@ attribuez lui un nom unique pour l'identifier et acceptez la. Don't mark database as modified for non-data changes (e.g., expanding groups) - Ne pas indiquer la base de données comme modifiée pour les changements hors-données (par exemple: groupes développés) + Ne pas indiquer la base de données comme modifiée pour les changements hors-données (par exemple : groupes développés) Auto-Type @@ -2227,7 +2227,7 @@ attribuez lui un nom unique pour l'identifier et acceptez la. Key: - Clé: + Clé : Use custom settings @@ -2235,11 +2235,11 @@ attribuez lui un nom unique pour l'identifier et acceptez la. Note: Change these settings only if you know what you are doing. - Attention: modifiez ces paramètres seulement si vous savez ce que vous faites. + Attention : modifiez ces paramètres seulement si vous savez ce que vous faites. Time step: - Période de temps: + Période de temps : 8 digits @@ -2251,7 +2251,7 @@ attribuez lui un nom unique pour l'identifier et acceptez la. Code size: - Taille du code: + Taille du code : sec @@ -2375,7 +2375,7 @@ attribuez lui un nom unique pour l'identifier et acceptez la. Path of the group to list. Default is / - Chemin du groupe à lister. Par défaut: / + Chemin du groupe à lister. Par défaut : / Print the UUIDs of the entries and groups. diff --git a/share/translations/keepassx_id.ts b/share/translations/keepassx_id.ts index f7db658d2..1e04aeb4c 100644 --- a/share/translations/keepassx_id.ts +++ b/share/translations/keepassx_id.ts @@ -69,7 +69,7 @@ Kernel: %3 %4 Distribution: %1 - + Distribusi: %1 diff --git a/share/translations/keepassx_it.ts b/share/translations/keepassx_it.ts index 5125a8b63..582d18970 100644 --- a/share/translations/keepassx_it.ts +++ b/share/translations/keepassx_it.ts @@ -131,7 +131,7 @@ Seleziona se vuoi consentire l'accesso. Auto-Type - KeePassXC - KeePassXC - Auto completamento + KeePassXC - Auto completamento @@ -170,7 +170,7 @@ Seleziona se vuoi consentire l'accesso. Unable to create Key File : - Impossibile creare file chiave: + Impossibile creare file chiave: Select a key file @@ -290,11 +290,11 @@ Seleziona se vuoi consentire l'accesso. Empty fieldname - Nome campo vuoto + Nome campo vuoto column - colonna + colonna Imported from CSV file @@ -302,7 +302,7 @@ Seleziona se vuoi consentire l'accesso. Original data: - Dati originali: + Dati originali: Error(s) detected in CSV file ! @@ -1917,23 +1917,23 @@ Modificale solo se sai quello che stai facendo. QtIOCompressor Internal zlib error when compressing: - Errore interno di zlib durante la compressione: + Errore interno di zlib durante la compressione: Error writing to underlying device: - Errore durante la scrittura nel dispositivo: + Errore durante la scrittura nel dispositivo: Error opening underlying device: - Errore durante l'apertura dal dispositivo: + Errore durante l'apertura dal dispositivo: Error reading data from underlying device: - Errore durante la lettura dal dispositivo: + Errore durante la lettura dal dispositivo: Internal zlib error when decompressing: - Errore interno di zlib durante la decompressione: + Errore interno di zlib durante la decompressione: @@ -1944,7 +1944,7 @@ Modificale solo se sai quello che stai facendo. Internal zlib error: - Errore interno di zlib: + Errore interno di zlib: @@ -2018,7 +2018,7 @@ Sblocca il database selezionato o scegli un altro database sbloccato. KeePassXC: New key association request - KeePassXC: Nuova richiesta di associazione chiave + KeePassXC: Nuova richiesta di associazione chiave You have received an association request for the above key. @@ -2164,7 +2164,7 @@ imposta un nome unico per identificarla ed accettarla. ms - ms + ms Start only a single instance of KeePassXC @@ -2179,7 +2179,7 @@ imposta un nome unico per identificarla ed accettarla. sec - sec + sec Lock databases after inactivity of @@ -2254,7 +2254,7 @@ imposta un nome unico per identificarla ed accettarla. sec - sec + sec diff --git a/share/translations/keepassx_ja.ts b/share/translations/keepassx_ja.ts index 28b7c5336..dd349640f 100644 --- a/share/translations/keepassx_ja.ts +++ b/share/translations/keepassx_ja.ts @@ -69,7 +69,7 @@ CPU アーキテクチャ: %2 Distribution: %1 - + 配布形式: %1 diff --git a/share/translations/keepassx_ko.ts b/share/translations/keepassx_ko.ts index 0b528474d..9b00bbc2f 100644 --- a/share/translations/keepassx_ko.ts +++ b/share/translations/keepassx_ko.ts @@ -69,7 +69,7 @@ CPU 아키텍처: %2 Distribution: %1 - + 배포판: %1 @@ -1096,7 +1096,7 @@ Do you want to open it anyway? Custom icon already exists - + 사용자 정의 아이콘이 이미 존재함 @@ -1350,7 +1350,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_ru.ts b/share/translations/keepassx_ru.ts index 938d5eefd..3058bcf14 100644 --- a/share/translations/keepassx_ru.ts +++ b/share/translations/keepassx_ru.ts @@ -69,7 +69,7 @@ Kernel: %3 %4 Distribution: %1 - + Дистрибутив: %1 @@ -219,7 +219,7 @@ Please select whether you want to allow access. Replace username and password with references - Заменить имя пользователя и пароль к ссылкам + Использовать ссылки для имени пользователя и пароля Copy history @@ -227,7 +227,7 @@ Please select whether you want to allow access. Append ' - Clone' to title - Добавить' - Клонировать' в заголовок + Добавить к названию « - клон» @@ -579,7 +579,7 @@ Otherwise your changes are lost. "%1" is in edit mode. Discard changes and close anyway? - «%1» в режиме редактирования. + «%1» в режиме правки. Отменить изменения и всё равно закрыть? @@ -741,7 +741,7 @@ Do you want to open it anyway? Empty recycle bin? - Корзина пустая? + Очистить корзину? Are you sure you want to permanently delete everything from your recycle bin? @@ -784,7 +784,7 @@ Do you want to open it anyway? Edit entry - Редактировать запись + Править запись Different passwords supplied. @@ -838,7 +838,7 @@ Do you want to open it anyway? [PROTECTED] Press reveal to view or edit - [Защищён] Нажмите для открытия просмотра или редактирования + [Защищён] Нажмите для открытия просмотра или правки Are you sure you want to remove this attachment? @@ -853,7 +853,7 @@ Do you want to open it anyway? Add - Добавить + Создать Remove @@ -946,7 +946,7 @@ Do you want to open it anyway? EditEntryWidgetMain Title: - Заголовок: + Название: Username: @@ -997,7 +997,7 @@ Do you want to open it anyway? Edit group - Редактировать группу + Править группу Enable @@ -1095,22 +1095,22 @@ Do you want to open it anyway? Hint: You can enable Google as a fallback under Tools>Settings>Security - Подсказка: вы можете включить Google в качестве резервного копирования в меню «Инструменты»> «Настройки»> «Безопасность» + Подсказка: в качестве резервного варианта для получения значков сайтов возможно использовать Google. Включите этот параметр в меню «Инструменты» -> «Настройки» -> «Безопасность» Custom icon already exists - + Пользовательский значок уже существует EditWidgetProperties Created: - Создано: + Создание: Modified: - Изменено: + Изменение: Accessed: @@ -1125,7 +1125,7 @@ Do you want to open it anyway? Entry - Clone - - Клонировать + - клон @@ -1143,7 +1143,7 @@ Do you want to open it anyway? Title - Заголовок + Имя записи Username @@ -1162,7 +1162,7 @@ Do you want to open it anyway? Title - Заголовок + Имя записи Username @@ -1353,7 +1353,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. - + Запускается новый экземпляр программы, т.к. файл блокировки запуска повреждён. @@ -1452,11 +1452,11 @@ This is a one-way migration. You won't be able to open the imported databas &Add new entry - &Добавить новую запись + &Создать запись &View/Edit entry - &Посмотреть/редактировать запись + &Открыть/править запись &Delete entry @@ -1464,11 +1464,11 @@ This is a one-way migration. You won't be able to open the imported databas &Add new group - &Добавить новую группу + &Создать группу &Edit group - &Редактировать группу + &Править группу &Delete group @@ -1524,7 +1524,7 @@ This is a one-way migration. You won't be able to open the imported databas &Title - &Заголовок + &Имя записи &URL @@ -1556,7 +1556,7 @@ This is a one-way migration. You won't be able to open the imported databas Empty recycle bin - Корзина пустая + Очистить корзину Access error for config file %1 @@ -1701,15 +1701,15 @@ Using default port 19455. &Return only best matching entries - &Возврат только наиболее совпадающих записей + &Показывать только лучшие совпадения Only entries with the same scheme (http://, https://, ftp://, ...) are returned. - Возвращаются только записи с той же схемой (http: //, https: //, ftp: //, ...). + Будут отобраны только записи с совпадающим протоколом (http://, https://, ftp://, ...). &Match URL schemes - &Совпадения схем адресов + &Проверять протокол Password Generator @@ -1879,7 +1879,7 @@ Change them only if you know what you are doing. Title - Заголовок + Имя записи Username @@ -2085,7 +2085,7 @@ give it a unique name to identify and accept it. SettingsWidgetGeneral Remember last databases - Помнить последнюю базу данных + Запоминать последнюю базу данных Automatically save on exit @@ -2117,7 +2117,7 @@ give it a unique name to identify and accept it. Hide window to system tray when minimized - При сворачивании прятать окно в системный лоток + При сворачивании скрывать окно в системный лоток Load previous databases on startup @@ -2129,7 +2129,7 @@ give it a unique name to identify and accept it. Hide window to system tray instead of app exit - Прятать окно в системный лоток вместо выхода + Скрывать окно в системный лоток вместо выхода Minimize window at application startup @@ -2141,11 +2141,11 @@ give it a unique name to identify and accept it. Remember last key files and security dongles - Помнить последние ключевые файлы и ключи безопасности + Запоминать последние использованные файлы ключей и устройства Don't mark database as modified for non-data changes (e.g., expanding groups) - Не помечать базу данных как измененную без изменения данных (например, для расширения групп) + Не помечать базу данных изменённой при действиях, не связанных с изменением данных (например, при распахивании групп) Auto-Type @@ -2153,7 +2153,7 @@ give it a unique name to identify and accept it. Use entry title and URL to match windows for global Auto-Type - Использовать URL и заголовок записи при сопоставлении окон для глобального автоввода + Использовать для поиска URL и название записи Always ask before performing Auto-Type @@ -2184,7 +2184,7 @@ give it a unique name to identify and accept it. Lock databases after inactivity of - Заблокировать базу данных после неактивности длительностью + Блокировать базу данных при отсутствии активности длительностью Show passwords in cleartext by default @@ -2192,7 +2192,7 @@ give it a unique name to identify and accept it. Lock databases after minimizing the window - Блокировать базу данных после сворачивания окна + Блокировать базу данных при сворачивания окна Don't require password repeat when it is visible @@ -2216,7 +2216,7 @@ give it a unique name to identify and accept it. Use Google as fallback for downloading website icons - Использовать Google как резерв для загрузки значков веб-сайтов + Использовать Google в качестве резервного варианта для получения значков веб-сайтов diff --git a/share/translations/keepassx_zh_TW.ts b/share/translations/keepassx_zh_TW.ts index 0dbabb3ab..54614e5cc 100644 --- a/share/translations/keepassx_zh_TW.ts +++ b/share/translations/keepassx_zh_TW.ts @@ -69,7 +69,7 @@ Kernel: %3 %4 Distribution: %1 - + 散佈:%1 @@ -196,7 +196,7 @@ Please select whether you want to allow access. Cha&llenge Response - + 挑戰回應 Refresh @@ -208,7 +208,7 @@ Please select whether you want to allow access. Changing master key failed: no YubiKey inserted. - + 挑戰主金鑰失敗:沒有插入 YubiKey @@ -397,7 +397,7 @@ Please select whether you want to allow access. Challenge Response: - + 挑戰驗證: @@ -1098,7 +1098,7 @@ Do you want to open it anyway? Custom icon already exists - + 自訂圖示已經存在 @@ -2140,7 +2140,7 @@ give it a unique name to identify and accept it. Remember last key files and security dongles - 記住最近的金鑰檔案與安全加密狗 + 記住最近的金鑰檔案與安全鎖 Don't mark database as modified for non-data changes (e.g., expanding groups) diff --git a/snapcraft.yaml b/snapcraft.yaml index 0d69941ac..fd2ca7b8d 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: keepassxc -version: 2.2.2 +version: 2.2.4 grade: stable summary: Community-driven port of the Windows application “KeePass Password Safe” description: | @@ -12,7 +12,7 @@ apps: keepassxc: command: desktop-launch keepassxc plugs: [unity7, x11, opengl, gsettings, home, network, network-bind, removable-media, raw-usb] - desktop: usr/share/applications/org.keepassxc.desktop + desktop: usr/share/applications/org.keepassxc.KeePassXC.desktop cli: command: keepassxc-cli plugs: [gsettings, home, removable-media, raw-usb] @@ -41,8 +41,10 @@ parts: - libxtst-dev - libyubikey-dev - libykpers-1-dev + stage-packages: + - dbus install: | - sed -i 's|Icon=keepassxc|Icon=${SNAP}/usr/share/icons/hicolor/256x256/apps/keepassxc.png|g' $SNAPCRAFT_PART_INSTALL/usr/share/applications/org.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 @@ -52,19 +54,12 @@ parts: - ttf-ubuntu-font-family - dmz-cursor-theme - light-themes + - adwaita-icon-theme + - gnome-themes-standard - shared-mime-info - libqt5gui5 - libgdk-pixbuf2.0-0 - libqt5svg5 # for loading icon themes which are svg - locales-all + - xdg-user-dirs - # Overcome limitation in snapd to support URL loading (CTRL+U) - # client needs to install "snapd-xdg-open" on their system - snapd-xdg-open: - source: https://github.com/ubuntu-core/snapd-xdg-open.git - source-depth: 1 - plugin: nil - install: | - install -D -t $SNAPCRAFT_PART_INSTALL/usr/bin/ data/xdg-open - stage-packages: - - dbus diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 89e3ebcbf..42e1bdbd0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -105,6 +105,7 @@ set(keepassx_SOURCES gui/EditWidgetIcons.cpp gui/EditWidgetProperties.cpp gui/FileDialog.cpp + gui/Font.cpp gui/IconModels.cpp gui/KeePass1OpenWidget.cpp gui/KMessageWidget.cpp @@ -155,6 +156,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 +180,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 +190,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 +231,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 diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 2519b9dcb..0d01a8311 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) { diff --git a/src/autotype/AutoType.h b/src/autotype/AutoType.h index 6f4a815f8..e881975ac 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/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index e9f50d1a8..4c8620d55 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -22,8 +22,8 @@ set(cli_SOURCES Command.h Edit.cpp Edit.h - EntropyMeter.cpp - EntropyMeter.h + Estimate.cpp + Estimate.h Extract.cpp Extract.h List.cpp diff --git a/src/cli/Clip.cpp b/src/cli/Clip.cpp index d50b32f0c..dd51d7f0b 100644 --- a/src/cli/Clip.cpp +++ b/src/cli/Clip.cpp @@ -55,9 +55,7 @@ int Clip::execute(QStringList arguments) parser.addOption(keyFile); parser.addPositionalArgument("entry", QObject::tr("Path of the entry to clip.")); parser.addPositionalArgument( - "timeout", - QObject::tr("Timeout in seconds before clearing the clipboard."), - QString("[timeout]")); + "timeout", QObject::tr("Timeout in seconds before clearing the clipboard."), QString("[timeout]")); parser.process(arguments); const QStringList args = parser.positionalArguments(); diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index c7eff6b08..6ec07b7af 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -23,9 +23,9 @@ #include "Command.h" #include "Add.h" -#include "Edit.h" #include "Clip.h" -#include "EntropyMeter.h" +#include "Edit.h" +#include "Estimate.h" #include "Extract.h" #include "List.h" #include "Locate.h" @@ -62,7 +62,7 @@ void populateCommands() commands.insert(QString("add"), new Add()); commands.insert(QString("clip"), new Clip()); commands.insert(QString("edit"), new Edit()); - commands.insert(QString("entropy-meter"), new EntropyMeter()); + commands.insert(QString("estimate"), new Estimate()); commands.insert(QString("extract"), new Extract()); commands.insert(QString("locate"), new Locate()); commands.insert(QString("ls"), new List()); diff --git a/src/cli/EntropyMeter.cpp b/src/cli/EntropyMeter.cpp deleted file mode 100644 index 1632fe234..000000000 --- a/src/cli/EntropyMeter.cpp +++ /dev/null @@ -1,141 +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 . - */ - -#include "EntropyMeter.h" - -#include -#include -#include -#include - -/* For pre-compiled headers under windows */ -#ifdef _WIN32 -#ifndef __MINGW32__ -#include "stdafx.h" -#endif -#endif - -EntropyMeter::EntropyMeter() -{ - this->name = QString("entropy-meter"); - this->description = QObject::tr("Calculate password entropy."); -} - -EntropyMeter::~EntropyMeter() -{ -} - -static void calculate(const char *pwd, int advanced) -{ - double e; - int len = strlen(pwd); - if (advanced == 0){ - e = ZxcvbnMatch(pwd, 0, 0); - printf("Pass '%s' \tLength %d\tEntropy %.3f\tLog10 %.3f\n", pwd, len, e, e * 0.301029996); - } else { - int ChkLen; - ZxcMatch_t *info, *p; - double m = 0.0; - e = ZxcvbnMatch(pwd, 0, &info); - for(p = info; p; p = p->Next) { - m += p->Entrpy; - } - m = e - m; - printf("Pass '%s' \tLength %d\tEntropy %.3f\tLog10 %.3f\n Multi-word extra bits %.1f\n", pwd, len, e, e * 0.301029996, m); - p = info; - ChkLen = 0; - while(p) { - int n; - switch(static_cast(p->Type)) - { - case BRUTE_MATCH: printf(" Type: Bruteforce "); break; - case DICTIONARY_MATCH: printf(" Type: Dictionary "); break; - case DICT_LEET_MATCH: printf(" Type: Dict+Leet "); break; - case USER_MATCH: printf(" Type: User Words "); break; - case USER_LEET_MATCH: printf(" Type: User+Leet "); break; - case REPEATS_MATCH: printf(" Type: Repeated "); break; - case SEQUENCE_MATCH: printf(" Type: Sequence "); break; - case SPATIAL_MATCH: printf(" Type: Spatial "); break; - case DATE_MATCH: printf(" Type: Date "); break; - case BRUTE_MATCH+MULTIPLE_MATCH: printf(" Type: Bruteforce(Rep)"); break; - case DICTIONARY_MATCH+MULTIPLE_MATCH: printf(" Type: Dictionary(Rep)"); break; - case DICT_LEET_MATCH+MULTIPLE_MATCH: printf(" Type: Dict+Leet(Rep) "); break; - case USER_MATCH+MULTIPLE_MATCH: printf(" Type: User Words(Rep)"); break; - case USER_LEET_MATCH+MULTIPLE_MATCH: printf(" Type: User+Leet(Rep) "); break; - case REPEATS_MATCH+MULTIPLE_MATCH: printf(" Type: Repeated(Rep) "); break; - case SEQUENCE_MATCH+MULTIPLE_MATCH: printf(" Type: Sequence(Rep) "); break; - case SPATIAL_MATCH+MULTIPLE_MATCH: printf(" Type: Spatial(Rep) "); break; - case DATE_MATCH+MULTIPLE_MATCH: printf(" Type: Date(Rep) "); break; - - default: printf(" Type: Unknown%d ", p->Type); break; - } - ChkLen += p->Length; - printf(" Length %d Entropy %6.3f (%.2f) ", p->Length, p->Entrpy, p->Entrpy * 0.301029996); - for(n = 0; n < p->Length; ++n, ++pwd) { - printf("%c", *pwd); - } - printf("\n"); - p = p->Next; - } - ZxcvbnFreeInfo(info); - if (ChkLen != len) - printf("*** Password length (%d) != sum of length of parts (%d) ***\n", len, ChkLen); - } -} - -int EntropyMeter::execute(QStringList arguments) -{ - printf("KeePassXC Entropy Meter, based on zxcvbn-c.\nEnter your password below or pass it as argv\n"); - printf(" Usage: entropy-meter [-a] [pwd1 pwd2 ...]\n> "); - int i, advanced = 0; - if (arguments.size() > 1 && arguments.at(1) == "-a") - { - advanced = 1; - arguments.removeAt(1); - } - i = 1; - if (i >= arguments.size()) - { - /* No test passwords on command line, so get them from stdin */ - char line[500]; - while(fgets(line, sizeof line, stdin)) - { - /* Drop the trailing newline character */ - for(i = 0; i < static_cast(sizeof line - 1); ++i) - { - if (line[i] < ' ') - { - line[i] = 0; - break; - } - } - if (line[0]) { - calculate(line,advanced); - printf("> "); - } - } - } - else - { - /* Do the test passwords on the command line */ - for(; i < arguments.size(); ++i) - { - calculate(arguments.at(i).toLatin1(), advanced); - } - } - return 0; -} diff --git a/src/cli/Estimate.cpp b/src/cli/Estimate.cpp new file mode 100644 index 000000000..80226a27e --- /dev/null +++ b/src/cli/Estimate.cpp @@ -0,0 +1,170 @@ +/* + * 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 "Estimate.h" + +#include +#include + +#include +#include +#include +#include + +/* For pre-compiled headers under windows */ +#ifdef _WIN32 +#ifndef __MINGW32__ +#include "stdafx.h" +#endif +#endif + +Estimate::Estimate() +{ + this->name = QString("estimate"); + this->description = QObject::tr("Estimate the entropy of a password."); +} + +Estimate::~Estimate() +{ +} + +static void estimate(const char* pwd, bool advanced) +{ + double e; + int len = strlen(pwd); + if (!advanced) { + e = ZxcvbnMatch(pwd, 0, 0); + printf("Length %d\tEntropy %.3f\tLog10 %.3f\n", len, e, e * 0.301029996); + } else { + int ChkLen; + ZxcMatch_t *info, *p; + double m = 0.0; + e = ZxcvbnMatch(pwd, 0, &info); + for (p = info; p; p = p->Next) { + m += p->Entrpy; + } + m = e - m; + printf("Length %d\tEntropy %.3f\tLog10 %.3f\n Multi-word extra bits %.1f\n", len, e, e * 0.301029996, m); + p = info; + ChkLen = 0; + while (p) { + int n; + switch (static_cast(p->Type)) { + case BRUTE_MATCH: + printf(" Type: Bruteforce "); + break; + case DICTIONARY_MATCH: + printf(" Type: Dictionary "); + break; + case DICT_LEET_MATCH: + printf(" Type: Dict+Leet "); + break; + case USER_MATCH: + printf(" Type: User Words "); + break; + case USER_LEET_MATCH: + printf(" Type: User+Leet "); + break; + case REPEATS_MATCH: + printf(" Type: Repeated "); + break; + case SEQUENCE_MATCH: + printf(" Type: Sequence "); + break; + case SPATIAL_MATCH: + printf(" Type: Spatial "); + break; + case DATE_MATCH: + printf(" Type: Date "); + break; + case BRUTE_MATCH + MULTIPLE_MATCH: + printf(" Type: Bruteforce(Rep)"); + break; + case DICTIONARY_MATCH + MULTIPLE_MATCH: + printf(" Type: Dictionary(Rep)"); + break; + case DICT_LEET_MATCH + MULTIPLE_MATCH: + printf(" Type: Dict+Leet(Rep) "); + break; + case USER_MATCH + MULTIPLE_MATCH: + printf(" Type: User Words(Rep)"); + break; + case USER_LEET_MATCH + MULTIPLE_MATCH: + printf(" Type: User+Leet(Rep) "); + break; + case REPEATS_MATCH + MULTIPLE_MATCH: + printf(" Type: Repeated(Rep) "); + break; + case SEQUENCE_MATCH + MULTIPLE_MATCH: + printf(" Type: Sequence(Rep) "); + break; + case SPATIAL_MATCH + MULTIPLE_MATCH: + printf(" Type: Spatial(Rep) "); + break; + case DATE_MATCH + MULTIPLE_MATCH: + printf(" Type: Date(Rep) "); + break; + + default: + printf(" Type: Unknown%d ", p->Type); + break; + } + ChkLen += p->Length; + printf(" Length %d Entropy %6.3f (%.2f) ", p->Length, p->Entrpy, p->Entrpy * 0.301029996); + for (n = 0; n < p->Length; ++n, ++pwd) { + printf("%c", *pwd); + } + printf("\n"); + p = p->Next; + } + ZxcvbnFreeInfo(info); + if (ChkLen != len) { + printf("*** Password length (%d) != sum of length of parts (%d) ***\n", len, ChkLen); + } + } +} + +int Estimate::execute(QStringList arguments) +{ + QTextStream inputTextStream(stdin, QIODevice::ReadOnly); + QTextStream outputTextStream(stdout, QIODevice::WriteOnly); + + QCommandLineParser parser; + parser.setApplicationDescription(this->description); + parser.addPositionalArgument("password", QObject::tr("Password for which to estimate the entropy."), "[password]"); + QCommandLineOption advancedOption(QStringList() << "a" + << "advanced", + QObject::tr("Perform advanced analysis on the password.")); + parser.addOption(advancedOption); + parser.process(arguments); + + const QStringList args = parser.positionalArguments(); + if (args.size() > 1) { + outputTextStream << parser.helpText().replace("keepassxc-cli", "keepassxc-cli estimate"); + return EXIT_FAILURE; + } + + QString password; + if (args.size() == 1) { + password = args.at(0); + } else { + password = inputTextStream.readLine(); + } + + estimate(password.toLatin1(), parser.isSet(advancedOption)); + return EXIT_SUCCESS; +} diff --git a/src/cli/EntropyMeter.h b/src/cli/Estimate.h similarity index 81% rename from src/cli/EntropyMeter.h rename to src/cli/Estimate.h index 614e2de35..2cbe49104 100644 --- a/src/cli/EntropyMeter.h +++ b/src/cli/Estimate.h @@ -15,17 +15,17 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_ENTROPYMETER_H -#define KEEPASSXC_ENTROPYMETER_H +#ifndef KEEPASSXC_ESTIMATE_H +#define KEEPASSXC_ESTIMATE_H #include "Command.h" -class EntropyMeter : public Command +class Estimate : public Command { public: - EntropyMeter(); - ~EntropyMeter(); + Estimate(); + ~Estimate(); int execute(QStringList arguments); }; -#endif // KEEPASSXC_ENTROPYMETER_H +#endif // KEEPASSXC_ESTIMATE_H diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index 2d519f9a1..73879f67d 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -85,7 +85,6 @@ int Extract::execute(QStringList arguments) compositeKey.addKey(fileKey); } - QString databaseFilename = args.at(0); QFile dbFile(databaseFilename); if (!dbFile.exists()) { diff --git a/src/cli/List.cpp b/src/cli/List.cpp index 63e48ee22..73830cab8 100644 --- a/src/cli/List.cpp +++ b/src/cli/List.cpp @@ -44,8 +44,7 @@ int List::execute(QStringList arguments) QCommandLineParser parser; parser.setApplicationDescription(this->description); parser.addPositionalArgument("database", QObject::tr("Path of the database.")); - parser.addPositionalArgument( - "group", QObject::tr("Path of the group to list. Default is /"), QString("[group]")); + parser.addPositionalArgument("group", QObject::tr("Path of the group to list. Default is /"), QString("[group]")); QCommandLineOption keyFile(QStringList() << "k" << "key-file", QObject::tr("Key file of the database."), diff --git a/src/cli/Locate.cpp b/src/cli/Locate.cpp index bb9831be1..83a3c5ce0 100644 --- a/src/cli/Locate.cpp +++ b/src/cli/Locate.cpp @@ -83,5 +83,4 @@ int Locate::locateEntry(Database* database, QString searchTerm) outputTextStream << result << endl; } return EXIT_SUCCESS; - } diff --git a/src/cli/Merge.cpp b/src/cli/Merge.cpp index a713cbb33..5df6b0188 100644 --- a/src/cli/Merge.cpp +++ b/src/cli/Merge.cpp @@ -43,10 +43,9 @@ int Merge::execute(QStringList arguments) parser.addPositionalArgument("database1", QObject::tr("Path of the database to merge into.")); parser.addPositionalArgument("database2", QObject::tr("Path of the database to merge from.")); - QCommandLineOption samePasswordOption( - QStringList() << "s" - << "same-credentials", - QObject::tr("Use the same credentials for both database files.")); + QCommandLineOption samePasswordOption(QStringList() << "s" + << "same-credentials", + QObject::tr("Use the same credentials for both database files.")); QCommandLineOption keyFile(QStringList() << "k" << "key-file", @@ -55,8 +54,8 @@ int Merge::execute(QStringList arguments) parser.addOption(keyFile); QCommandLineOption keyFileFrom(QStringList() << "f" << "key-file-from", - QObject::tr("Key file of the database to merge from."), - QObject::tr("path")); + QObject::tr("Key file of the database to merge from."), + QObject::tr("path")); parser.addOption(keyFileFrom); parser.addOption(samePasswordOption); @@ -68,7 +67,6 @@ int Merge::execute(QStringList arguments) return EXIT_FAILURE; } - Database* db1 = Database::unlockFromStdin(args.at(0), parser.value(keyFile)); if (db1 == nullptr) { return EXIT_FAILURE; diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index d137f74df..66225c56a 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -49,6 +49,13 @@ int Show::execute(QStringList arguments) QObject::tr("Key file of the database."), QObject::tr("path")); parser.addOption(keyFile); + QCommandLineOption attributes(QStringList() << "a" + << "attributes", + QObject::tr("Names of the attributes to show. " + "This option can be specified more than once, with each attribute shown one-per-line in the given order. " + "If no attributes are specified, a summary of the default attributes is given."), + QObject::tr("attribute")); + parser.addOption(attributes); parser.addPositionalArgument("entry", QObject::tr("Name of the entry to show.")); parser.process(arguments); @@ -63,10 +70,10 @@ int Show::execute(QStringList arguments) return EXIT_FAILURE; } - return this->showEntry(db, args.at(1)); + return this->showEntry(db, parser.values(attributes), args.at(1)); } -int Show::showEntry(Database* database, QString entryPath) +int Show::showEntry(Database* database, QStringList attributes, QString entryPath) { QTextStream inputTextStream(stdin, QIODevice::ReadOnly); @@ -78,10 +85,24 @@ int Show::showEntry(Database* database, QString entryPath) return EXIT_FAILURE; } - outputTextStream << " title: " << entry->title() << endl; - outputTextStream << "username: " << entry->username() << endl; - outputTextStream << "password: " << entry->password() << endl; - outputTextStream << " URL: " << entry->url() << endl; - outputTextStream << " Notes: " << entry->notes() << endl; - return EXIT_SUCCESS; + // If no attributes specified, output the default attribute set. + bool showAttributeNames = attributes.isEmpty(); + if (attributes.isEmpty()) { + attributes = EntryAttributes::DefaultAttributes; + } + + // Iterate over the attributes and output them line-by-line. + bool sawUnknownAttribute = false; + for (QString attribute : attributes) { + if (!entry->attributes()->contains(attribute)) { + sawUnknownAttribute = true; + qCritical("ERROR: unknown attribute '%s'.", qPrintable(attribute)); + continue; + } + if (showAttributeNames) { + outputTextStream << attribute << ": "; + } + outputTextStream << entry->resolveMultiplePlaceholders(entry->attributes()->value(attribute)) << endl; + } + return sawUnknownAttribute ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/src/cli/Show.h b/src/cli/Show.h index f3bc55e60..f2caefdbf 100644 --- a/src/cli/Show.h +++ b/src/cli/Show.h @@ -26,7 +26,7 @@ public: Show(); ~Show(); int execute(QStringList arguments); - int showEntry(Database* database, QString entryPath); + int showEntry(Database* database, QStringList attributes, QString entryPath); }; #endif // KEEPASSXC_SHOW_H diff --git a/src/cli/Utils.cpp b/src/cli/Utils.cpp index b9866a1a2..f42095cbb 100644 --- a/src/cli/Utils.cpp +++ b/src/cli/Utils.cpp @@ -27,7 +27,6 @@ #include #include - void Utils::setStdinEcho(bool enable = true) { #ifdef Q_OS_WIN diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 940b3d4cf..a9145b27e 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -22,8 +22,8 @@ Copies the password of a database entry to the clipboard. If multiple entries wi .IP "edit [options] " Edits a database entry. A password can be generated (\fI-g\fP option), or a prompt can be displayed to input the password (\fI-p\fP option). -.IP "entropy-meter [-a pwd1 pwd2 ...]" -Calculates the entropy of a single, or multiple passwords specified using the \fI-a\fP option. If no passwords are specified, the program will run in interactive mode and prompt the user to enter a password. +.IP "estimate [options] [password]" +Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input. .IP "extract [options] " Extracts and prints the contents of a database to standard output in XML format. @@ -90,6 +90,20 @@ Specify the length of the password to generate. Specify the title of the entry. +.SS "Estimate options" + +.IP "-a, --advanced" +Perform advanced analysis on the password. + + +.SS "Show options" + +.IP "-a, --attributes ..." +Names of the attributes to show. This option can be specified more than once, +with each attribute shown one-per-line in the given order. If no attributes are +specified, a summary of the default attributes is given. + + .SH REPORTING BUGS Bugs and feature requests can be reported on GitHub at https://github.com/keepassxreboot/keepassxc/issues. diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index e06c69382..b06e702a9 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -15,6 +15,7 @@ #cmakedefine WITH_XC_HTTP #cmakedefine WITH_XC_AUTOTYPE #cmakedefine WITH_XC_YUBIKEY +#cmakedefine WITH_XC_SSHAGENT #cmakedefine KEEPASSXC_DIST #cmakedefine KEEPASSXC_DIST_TYPE "@KEEPASSXC_DIST_TYPE@" diff --git a/src/core/Base32.cpp b/src/core/Base32.cpp index 13228a37f..78448ffb5 100644 --- a/src/core/Base32.cpp +++ b/src/core/Base32.cpp @@ -52,8 +52,9 @@ QVariant Base32::decode(const QByteArray& encodedData) int nPads = 0; for (int i = -1; i > -7; --i) { - if ('=' == encodedData[encodedData.size() + i]) + if ('=' == encodedData[encodedData.size() + i]) { ++nPads; + } } int specialOffset; @@ -95,11 +96,12 @@ QVariant Base32::decode(const QByteArray& encodedData) int nQuantumBytes = 5; for (int n = 0; n < 8; ++n) { - quint8 ch = static_cast(encodedData[i++]); + 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 (ASCII_2 <= ch && ch <= ASCII_7) { ch -= ASCII_2; @@ -126,8 +128,8 @@ QVariant Base32::decode(const QByteArray& encodedData) const int offset = (nQuantumBytes - 1) * 8; quint64 mask = quint64(0xFF) << offset; for (int n = offset; n >= 0 && o < nBytes; n -= 8) { - data[o++] = static_cast((quantum & mask) >> n); - mask >>= 8; + data[o++] = static_cast((quantum & mask) >> n); + mask >>= 8; } } @@ -147,7 +149,7 @@ QByteArray Base32::encode(const QByteArray& data) const int rBits = nBits % 40; // in {0, 8, 16, 24, 32} const int nQuanta = nBits / 40 + (rBits > 0 ? 1 : 0); const int nBytes = nQuanta * 8; - QByteArray encodedData(nQuanta * 8, Qt::Uninitialized); + QByteArray encodedData(nBytes, Qt::Uninitialized); int i = 0; int o = 0; @@ -166,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; } @@ -173,10 +176,11 @@ 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 @@ -195,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; @@ -203,6 +207,7 @@ 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; diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 6899015a6..2047919b1 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); diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 3913f8090..cb28ee211 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -95,10 +95,15 @@ const Metadata* Database::metadata() const Entry* Database::resolveEntry(const Uuid& uuid) { - return recFindEntry(uuid, m_rootGroup); + return findEntryRecursive(uuid, m_rootGroup); } -Entry* Database::recFindEntry(const Uuid& uuid, Group* group) +Entry* Database::resolveEntry(const QString& text, EntryReferenceType referenceType) +{ + return findEntryRecursive(text, referenceType, m_rootGroup); +} + +Entry* Database::findEntryRecursive(const Uuid& uuid, Group* group) { const QList entryList = group->entries(); for (Entry* entry : entryList) { @@ -109,7 +114,57 @@ Entry* Database::recFindEntry(const Uuid& uuid, Group* group) const QList children = group->children(); for (Group* child : children) { - Entry* result = recFindEntry(uuid, child); + Entry* result = findEntryRecursive(uuid, child); + if (result) { + return result; + } + } + + return nullptr; +} + +Entry* Database::findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group) +{ + Q_ASSERT_X(referenceType != EntryReferenceType::Unknown, "Database::findEntryRecursive", + "Can't search entry with \"referenceType\" parameter equal to \"Unknown\""); + + bool found = false; + const QList entryList = group->entries(); + for (Entry* entry : entryList) { + switch (referenceType) { + case EntryReferenceType::Unknown: + return nullptr; + case EntryReferenceType::Title: + found = entry->title() == text; + break; + case EntryReferenceType::UserName: + found = entry->username() == text; + break; + case EntryReferenceType::Password: + found = entry->password() == text; + break; + case EntryReferenceType::Url: + found = entry->url() == text; + break; + case EntryReferenceType::Notes: + found = entry->notes() == text; + break; + case EntryReferenceType::Uuid: + found = entry->uuid() == Uuid::fromHex(text); + break; + case EntryReferenceType::CustomAttributes: + found = entry->attributes()->containsValue(text); + break; + } + + if (found) { + return entry; + } + } + + const QList children = group->children(); + for (Group* child : children) { + Entry* result = findEntryRecursive(text, referenceType, child); if (result) { return result; } @@ -120,10 +175,10 @@ Entry* Database::recFindEntry(const Uuid& uuid, Group* group) Group* Database::resolveGroup(const Uuid& uuid) { - return recFindGroup(uuid, m_rootGroup); + return findGroupRecursive(uuid, m_rootGroup); } -Group* Database::recFindGroup(const Uuid& uuid, Group* group) +Group* Database::findGroupRecursive(const Uuid& uuid, Group* group) { if (group->uuid() == uuid) { return group; @@ -131,7 +186,7 @@ Group* Database::recFindGroup(const Uuid& uuid, Group* group) const QList children = group->children(); for (Group* child : children) { - Group* result = recFindGroup(uuid, child); + Group* result = findGroupRecursive(uuid, child); if (result) { return result; } diff --git a/src/core/Database.h b/src/core/Database.h index 26b150253..b20f897fe 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -27,6 +27,7 @@ #include "keys/CompositeKey.h" class Entry; +enum class EntryReferenceType; class Group; class Metadata; class QTimer; @@ -81,6 +82,7 @@ public: Metadata* metadata(); const Metadata* metadata() const; Entry* resolveEntry(const Uuid& uuid); + Entry* resolveEntry(const QString& text, EntryReferenceType referenceType); Group* resolveGroup(const Uuid& uuid); QList deletedObjects(); void addDeletedObject(const DeletedObject& delObj); @@ -141,8 +143,9 @@ private slots: void startModifiedTimer(); private: - Entry* recFindEntry(const Uuid& uuid, Group* group); - Group* recFindGroup(const Uuid& uuid, Group* group); + Entry* findEntryRecursive(const Uuid& uuid, Group* group); + Entry* findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group); + Group* findGroupRecursive(const Uuid& uuid, Group* group); void createRecycleBin(); diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 91d757a91..6169c1093 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -25,6 +25,8 @@ #include "core/Metadata.h" #include "totp/totp.h" +#include + const int Entry::DefaultIconNumber = 0; const int Entry::ResolveMaximumDepth = 10; @@ -40,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())); @@ -90,6 +92,29 @@ void Entry::setUpdateTimeinfo(bool value) m_updateTimeinfo = value; } +EntryReferenceType Entry::referenceType(const QString& referenceStr) +{ + const QString referenceLowerStr = referenceStr.toLower(); + EntryReferenceType result = EntryReferenceType::Unknown; + if (referenceLowerStr == QLatin1String("t")) { + result = EntryReferenceType::Title; + } else if (referenceLowerStr == QLatin1String("u")) { + result = EntryReferenceType::UserName; + } else if (referenceLowerStr == QLatin1String("p")) { + result = EntryReferenceType::Password; + } else if (referenceLowerStr == QLatin1String("a")) { + result = EntryReferenceType::Url; + } else if (referenceLowerStr == QLatin1String("n")) { + result = EntryReferenceType::Notes; + } else if (referenceLowerStr == QLatin1String("i")) { + result = EntryReferenceType::Uuid; + } else if (referenceLowerStr == QLatin1String("o")) { + result = EntryReferenceType::CustomAttributes; + } + + return result; +} + Uuid Entry::uuid() const { return m_uuid; @@ -315,7 +340,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 { @@ -326,18 +351,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); } } @@ -351,19 +388,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 @@ -680,7 +722,7 @@ void Entry::updateModifiedSinceBegin() m_modifiedSinceBegin = true; } -QString Entry::resolveMultiplePlaceholdersRecursive(const QString &str, int maxDepth) const +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())); @@ -704,15 +746,12 @@ QString Entry::resolveMultiplePlaceholdersRecursive(const QString &str, int maxD return result; } -QString Entry::resolvePlaceholderRecursive(const QString &placeholder, int maxDepth) const +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(); @@ -743,45 +782,64 @@ QString Entry::resolvePlaceholderRecursive(const QString &placeholder, int maxDe 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; - } + case PlaceholderType::Reference: + return resolveReferencePlaceholderRecursive(placeholder, maxDepth); } return placeholder; } +QString Entry::resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const +{ + // resolving references in format: {REF:@:} + // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing + + QRegularExpressionMatch match = EntryAttributes::matchReference(placeholder); + if (!match.hasMatch()) { + return placeholder; + } + + QString result; + const QString searchIn = match.captured(EntryAttributes::SearchInGroupName); + const QString searchText = match.captured(EntryAttributes::SearchTextGroupName); + + const EntryReferenceType searchInType = Entry::referenceType(searchIn); + const Entry* refEntry = m_group->database()->resolveEntry(searchText, searchInType); + + if (refEntry) { + const QString wantedField = match.captured(EntryAttributes::WantedFieldGroupName); + result = refEntry->referenceFieldValue(Entry::referenceType(wantedField)); + + // 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; +} + +QString Entry::referenceFieldValue(EntryReferenceType referenceType) const +{ + switch (referenceType) { + case EntryReferenceType::Title: + return title(); + case EntryReferenceType::UserName: + return username(); + case EntryReferenceType::Password: + return password(); + case EntryReferenceType::Url: + return url(); + case EntryReferenceType::Notes: + return notes(); + case EntryReferenceType::Uuid: + return uuid().toHex(); + default: + break; + } + return QString(); +} + Group* Entry::group() { return m_group; @@ -839,7 +897,7 @@ const Database* Entry::database() const } } -QString Entry::maskPasswordPlaceholders(const QString &str) const +QString Entry::maskPasswordPlaceholders(const QString& str) const { QString result = str; result.replace(QRegExp("(\\{PASSWORD\\})", Qt::CaseInsensitive, QRegExp::RegExp2), "******"); @@ -856,7 +914,7 @@ QString Entry::resolvePlaceholder(const QString& placeholder) const return resolvePlaceholderRecursive(placeholder, ResolveMaximumDepth); } -QString Entry::resolveUrlPlaceholder(const QString &str, Entry::PlaceholderType placeholderType) const +QString Entry::resolveUrlPlaceholder(const QString& str, Entry::PlaceholderType placeholderType) const { if (str.isEmpty()) return QString(); @@ -892,7 +950,7 @@ QString Entry::resolveUrlPlaceholder(const QString &str, Entry::PlaceholderType return QString(); } -Entry::PlaceholderType Entry::placeholderType(const QString &placeholder) const +Entry::PlaceholderType Entry::placeholderType(const QString& placeholder) const { if (!placeholder.startsWith(QLatin1Char('{')) || !placeholder.endsWith(QLatin1Char('}'))) { return PlaceholderType::NotPlaceholder; diff --git a/src/core/Entry.h b/src/core/Entry.h index d4d2b9034..266254e65 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -36,6 +36,17 @@ class Database; class Group; +enum class EntryReferenceType { + Unknown, + Title, + UserName, + Password, + Url, + Notes, + Uuid, + CustomAttributes +}; + struct EntryData { int iconNumber; @@ -170,7 +181,7 @@ 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; + QString resolveUrlPlaceholder(const QString& str, PlaceholderType placeholderType) const; PlaceholderType placeholderType(const QString& placeholder) const; QString resolveUrl(const QString& url) const; @@ -203,6 +214,10 @@ private slots: private: QString resolveMultiplePlaceholdersRecursive(const QString& str, int maxDepth) const; QString resolvePlaceholderRecursive(const QString& placeholder, int maxDepth) const; + QString resolveReferencePlaceholderRecursive(const QString& placeholder, int maxDepth) const; + QString referenceFieldValue(EntryReferenceType referenceType) const; + + static EntryReferenceType referenceType(const QString& referenceStr); 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 d700ed443..bab110576 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 04c22cb34..8fa7c7179 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/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index 8ffdaa8f5..8cc7f2f0a 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -25,11 +25,15 @@ const QString EntryAttributes::URLKey = "URL"; const QString EntryAttributes::NotesKey = "Notes"; const QStringList EntryAttributes::DefaultAttributes(QStringList() << TitleKey << UserNameKey << PasswordKey << URLKey << NotesKey); + +const QString EntryAttributes::WantedFieldGroupName = "WantedField"; +const QString EntryAttributes::SearchInGroupName = "SearchIn"; +const QString EntryAttributes::SearchTextGroupName = "SearchText"; + const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD"; EntryAttributes::EntryAttributes(QObject* parent) : QObject(parent) - , m_referenceRegExp("\\{REF:([TUPAN])@I:([^}]+)\\}", Qt::CaseInsensitive, QRegExp::RegExp2) { clear(); } @@ -61,11 +65,16 @@ QString EntryAttributes::value(const QString& key) const return m_attributes.value(key); } -bool EntryAttributes::contains(const QString &key) const +bool EntryAttributes::contains(const QString& key) const { return m_attributes.contains(key); } +bool EntryAttributes::containsValue(const QString& value) const +{ + return m_attributes.values().contains(value); +} + bool EntryAttributes::isProtected(const QString& key) const { return m_protectedAttributes.contains(key); @@ -78,16 +87,8 @@ bool EntryAttributes::isReference(const QString& key) const return false; } - QString data = value(key); - if (m_referenceRegExp.indexIn(data) != -1) { - return true; - } - return false; -} - -QRegExp* EntryAttributes::referenceRegExp() -{ - return &m_referenceRegExp; + const QString data = value(key); + return matchReference(data).hasMatch(); } void EntryAttributes::set(const QString& key, const QString& value, bool protect) @@ -258,6 +259,15 @@ bool EntryAttributes::operator!=(const EntryAttributes& other) const || m_protectedAttributes != other.m_protectedAttributes); } +QRegularExpressionMatch EntryAttributes::matchReference(const QString& text) +{ + static QRegularExpression referenceRegExp( + "\\{REF:(?[TUPANI])@(?[TUPANIO]):(?[^}]+)\\}", + QRegularExpression::CaseInsensitiveOption); + + return referenceRegExp.match(text); +} + void EntryAttributes::clear() { emit aboutToBeReset(); diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h index 40fc5dec4..f483b8a9b 100644 --- a/src/core/EntryAttributes.h +++ b/src/core/EntryAttributes.h @@ -21,6 +21,7 @@ #include #include +#include #include #include @@ -35,9 +36,9 @@ public: QList customKeys(); QString value(const QString& key) const; bool contains(const QString& key) const; + bool containsValue(const QString& value) const; bool isProtected(const QString& key) const; bool isReference(const QString& key) const; - QRegExp* referenceRegExp(); void set(const QString& key, const QString& value, bool protect = false); void remove(const QString& key); void rename(const QString& oldKey, const QString& newKey); @@ -49,6 +50,8 @@ public: bool operator==(const EntryAttributes& other) const; bool operator!=(const EntryAttributes& other) const; + static QRegularExpressionMatch matchReference(const QString& text); + static const QString TitleKey; static const QString UserNameKey; static const QString PasswordKey; @@ -58,6 +61,10 @@ public: static const QString RememberCmdExecAttr; static bool isDefaultAttribute(const QString& key); + static const QString WantedFieldGroupName; + static const QString SearchInGroupName; + static const QString SearchTextGroupName; + signals: void modified(); void defaultKeyModified(); @@ -74,7 +81,6 @@ signals: private: QMap m_attributes; QSet m_protectedAttributes; - QRegExp m_referenceRegExp; }; #endif // KEEPASSX_ENTRYATTRIBUTES_H diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 6f70db347..c7c12bfea 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; } } @@ -682,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)) { @@ -725,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); @@ -743,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; } @@ -908,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(); @@ -946,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 2a9d2b18c..57d503a85 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("")); @@ -119,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); @@ -160,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(); @@ -183,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 000000000..98dc6f7ab --- /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 000000000..8461cbc5d --- /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 fb800198a..000000000 --- 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 6c1b7fe96..03eed58ad 100644 --- a/src/core/ScreenLockListenerDBus.cpp +++ b/src/core/ScreenLockListenerDBus.cpp @@ -36,6 +36,14 @@ ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget *parent): this, //receiver SLOT(freedesktopScreenSaver(bool))); + sessionBus.connect( + "org.gnome.ScreenSaver", // service + "/org/gnome/ScreenSaver", // path + "org.gnome.ScreenSaver", // interface + "ActiveChanged", // signal name + this, //receiver + SLOT(freedesktopScreenSaver(bool))); + sessionBus.connect( "org.gnome.SessionManager", // service "/org/gnome/SessionManager/Presence", // path diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index 98d481969..016103b27 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 b85c58b7c..81e13f385 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 78ec60c60..dd493d2df 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 e600a7edb..0b291e693 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; @@ -86,20 +88,14 @@ bool SymmetricCipherGcrypt::init() gcry_error_t error; + if(m_ctx != nullptr) + gcry_cipher_close(m_ctx); error = gcry_cipher_open(&m_ctx, m_algo, m_mode, 0); if (error != 0) { setErrorString(error); 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 +115,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); @@ -228,9 +230,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 d3ad8d15b..108bc14e4 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/KeePass2Repair.cpp b/src/format/KeePass2Repair.cpp index 81ada2fda..8d18457d4 100644 --- a/src/format/KeePass2Repair.cpp +++ b/src/format/KeePass2Repair.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 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 @@ -18,36 +19,29 @@ #include "KeePass2Repair.h" #include +#include #include #include "format/KeePass2RandomStream.h" #include "format/KeePass2Reader.h" #include "format/KeePass2XmlReader.h" -KeePass2Repair::KeePass2Repair() - : m_db(nullptr) +KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device, const CompositeKey& key) { -} - -KeePass2Repair::RepairResult KeePass2Repair::repairDatabase(QIODevice* device, const CompositeKey& key) -{ - m_db = nullptr; m_errorStr.clear(); KeePass2Reader reader; reader.setSaveXml(true); - Database* db = reader.readDatabase(device, key, true); + QScopedPointer db(reader.readDatabase(device, key, true)); if (!reader.hasError()) { - delete db; - return NothingTodo; + return qMakePair(NothingTodo, nullptr); } QByteArray xmlData = reader.xmlData(); if (!db || xmlData.isEmpty()) { - delete db; m_errorStr = reader.errorString(); - return UnableToOpen; + return qMakePair(UnableToOpen, nullptr); } bool repairAction = false; @@ -59,8 +53,7 @@ KeePass2Repair::RepairResult KeePass2Repair::repairDatabase(QIODevice* device, c && encodingRegExp.cap(1).compare("utf8", Qt::CaseInsensitive) != 0) { // database is not utf-8 encoded, we don't support repairing that - delete db; - return RepairFailed; + return qMakePair(RepairFailed, nullptr); } } @@ -75,8 +68,7 @@ KeePass2Repair::RepairResult KeePass2Repair::repairDatabase(QIODevice* device, c if (!repairAction) { // we were unable to find the problem - delete db; - return RepairFailed; + return qMakePair(RepairFailed, nullptr); } KeePass2RandomStream randomStream; @@ -84,23 +76,16 @@ KeePass2Repair::RepairResult KeePass2Repair::repairDatabase(QIODevice* device, c KeePass2XmlReader xmlReader; QBuffer buffer(&xmlData); buffer.open(QIODevice::ReadOnly); - xmlReader.readDatabase(&buffer, db, &randomStream); + xmlReader.readDatabase(&buffer, db.data(), &randomStream); if (xmlReader.hasError()) { - delete db; - return RepairFailed; + return qMakePair(RepairFailed, nullptr); } else { - m_db = db; - return RepairSuccess; + return qMakePair(RepairSuccess, db.take()); } } -Database* KeePass2Repair::database() const -{ - return m_db; -} - QString KeePass2Repair::errorString() const { return m_errorStr; diff --git a/src/format/KeePass2Repair.h b/src/format/KeePass2Repair.h index fe2f9dbfe..e7f2c8435 100644 --- a/src/format/KeePass2Repair.h +++ b/src/format/KeePass2Repair.h @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 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 @@ -20,6 +21,7 @@ #include #include +#include #include "core/Database.h" #include "keys/CompositeKey.h" @@ -36,14 +38,12 @@ public: RepairSuccess, RepairFailed }; + using RepairOutcome = QPair; - KeePass2Repair(); - RepairResult repairDatabase(QIODevice* device, const CompositeKey& key); - Database* database() const; + RepairOutcome repairDatabase(QIODevice* device, const CompositeKey& key); QString errorString() const; private: - Database* m_db; QString m_errorStr; }; diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 58387335c..e89a7fdcb 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -87,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 bf4db8ff5..78bad2731 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 e0a16d26d..6f8ff9ace 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 e487f97ca..451dc597a 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -277,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()) { diff --git a/src/gui/DatabaseRepairWidget.cpp b/src/gui/DatabaseRepairWidget.cpp index 2b0039408..d3dddf14f 100644 --- a/src/gui/DatabaseRepairWidget.cpp +++ b/src/gui/DatabaseRepairWidget.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2016 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 @@ -69,7 +70,8 @@ void DatabaseRepairWidget::openDatabase() delete m_db; } QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - KeePass2Repair::RepairResult repairResult = repair.repairDatabase(&file, masterKey); + auto repairOutcome = repair.repairDatabase(&file, masterKey); + KeePass2Repair::RepairResult repairResult = repairOutcome.first; QApplication::restoreOverrideCursor(); switch (repairResult) { @@ -83,7 +85,7 @@ void DatabaseRepairWidget::openDatabase() emit editFinished(false); return; case KeePass2Repair::RepairSuccess: - m_db = repair.database(); + m_db = repairOutcome.second; MessageBox::warning(this, tr("Success"), tr("The database has been successfully repaired\nYou can now save it.")); emit editFinished(true); return; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 8510279b5..8cade6965 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -90,12 +90,12 @@ void DatabaseTabWidget::newDatabase() Database* db = new Database(); db->rootGroup()->setName(tr("Root")); dbStruct.dbWidget = new DatabaseWidget(db, this); - + CompositeKey emptyKey; db->setKey(emptyKey); insertDatabase(db, dbStruct); - + if (!saveDatabaseAs(db)) { closeDatabase(db); return; @@ -298,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?"), @@ -307,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; } } @@ -355,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); @@ -375,7 +378,6 @@ bool DatabaseTabWidget::saveDatabase(Database* db) MessageWidget::Error); return false; } - } else { return saveDatabaseAs(db); } @@ -622,6 +624,36 @@ void DatabaseTabWidget::updateTabNameFromDbWidgetSender() DatabaseWidget* dbWidget = static_cast(sender()); updateTabName(databaseFromDatabaseWidget(dbWidget)); + + Database* db = dbWidget->database(); + Group *autoload = db->rootGroup()->findChildByName("AutoOpen"); + if (autoload) { + const DatabaseManagerStruct& dbStruct = m_dbList.value(db); + QFileInfo dbpath(dbStruct.canonicalFilePath); + QDir dbFolder(dbpath.canonicalPath()); + for (auto entry : autoload->entries()) { + if (entry->url().isEmpty() || entry->password().isEmpty()) { + continue; + } + QFileInfo filepath; + if (entry->url().startsWith("file://")) { + QUrl url(entry->url()); + filepath.setFile(url.toLocalFile()); + } + else { + filepath.setFile(entry->url()); + if (filepath.isRelative()) { + filepath.setFile(dbFolder, entry->url()); + } + } + + if (!filepath.isFile()) { + continue; + } + + openDatabase(filepath.canonicalFilePath(), entry->password(), ""); + } + } } int DatabaseTabWidget::databaseIndex(Database* db) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 6571668c8..b851839ae 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -57,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); @@ -73,15 +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)), @@ -122,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(); @@ -137,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(); @@ -168,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())); @@ -207,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); } @@ -251,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 @@ -391,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(); @@ -771,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()); @@ -904,6 +938,7 @@ void DatabaseWidget::switchToMasterKeyChange(bool disableCancel) m_changeMasterKeyWidget->clearForms(); m_changeMasterKeyWidget->setCancelEnabled(!disableCancel); setCurrentWidget(m_changeMasterKeyWidget); + m_importingCsv = false; } void DatabaseWidget::switchToDatabaseSettings() @@ -915,8 +950,13 @@ void DatabaseWidget::switchToDatabaseSettings() void DatabaseWidget::switchToOpenDatabase(const QString& fileName) { updateFilename(fileName); - m_databaseOpenWidget->load(fileName); - setCurrentWidget(m_databaseOpenWidget); + if (m_databaseOpenWidget) { + m_databaseOpenWidget->load(fileName); + setCurrentWidget(m_databaseOpenWidget); + } else if (m_unlockDatabaseWidget) { + m_unlockDatabaseWidget->load(fileName); + setCurrentWidget(m_unlockDatabaseWidget); + } } void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString& password, @@ -924,15 +964,21 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString { updateFilename(fileName); switchToOpenDatabase(fileName); - m_databaseOpenWidget->enterKey(password, keyFile); + if (m_databaseOpenWidget) { + m_databaseOpenWidget->enterKey(password, keyFile); + } else if (m_unlockDatabaseWidget) { + m_unlockDatabaseWidget->enterKey(password, keyFile); + } } 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) @@ -1169,7 +1215,7 @@ void DatabaseWidget::onWatchedFileChanged() void DatabaseWidget::reloadDatabaseFile() { - if (m_db == nullptr) { + if (!m_db || currentMode() == DatabaseWidget::LockedMode) { return; } @@ -1201,7 +1247,7 @@ void DatabaseWidget::reloadDatabaseFile() if (m_databaseModified) { // Ask if we want to merge changes into new database QMessageBox::StandardButton mb = MessageBox::question(this, tr("Merge Request"), - tr("The database file has changed and you have unsaved changes." + tr("The database file has changed and you have unsaved changes.\n" "Do you want to merge your changes?"), QMessageBox::Yes | QMessageBox::No); @@ -1365,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(); } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 404adb528..e9428a702 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -88,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(); @@ -123,7 +125,8 @@ signals: void listModeActivated(); void searchModeAboutToActivate(); void searchModeActivated(); - void splitterSizesChanged(); + void mainSplitterSizesChanged(); + void detailSplitterSizesChanged(); void entryColumnSizesChanged(); void updateSearch(QString text); @@ -214,7 +217,7 @@ 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; @@ -233,6 +236,9 @@ private: 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 1510d8440..57a3dcf86 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 96ecd104a..79a8ded38 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 index 48077bbb2..23c3485ad 100644 --- a/src/gui/DetailsWidget.cpp +++ b/src/gui/DetailsWidget.cpp @@ -20,6 +20,7 @@ #include "ui_DetailsWidget.h" #include +#include #include "core/Config.h" #include "core/FilePath.h" @@ -33,6 +34,7 @@ DetailsWidget::DetailsWidget(QWidget* parent) , m_locked(false) , m_currentEntry(nullptr) , m_currentGroup(nullptr) + , m_timer(nullptr) , m_attributesWidget(nullptr) , m_autotypeWidget(nullptr) , m_selectedTabEntry(0) @@ -58,7 +60,7 @@ DetailsWidget::~DetailsWidget() { } -void DetailsWidget::getSelectedEntry(Entry* selectedEntry) +void DetailsWidget::getSelectedEntry(Entry* selectedEntry) { if (!selectedEntry) { hideDetails(); @@ -108,7 +110,8 @@ void DetailsWidget::getSelectedEntry(Entry* selectedEntry) 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->setText( + shortPassword(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password()))); m_ui->passwordLabel->setToolTip(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password())); } else { m_ui->passwordLabel->setText("****"); @@ -136,14 +139,16 @@ void DetailsWidget::getSelectedEntry(Entry* selectedEntry) } if (m_currentEntry->hasTotp()) { - m_ui->totpButton->show(); - updateTotp(); - 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(); @@ -188,7 +193,7 @@ void DetailsWidget::getSelectedEntry(Entry* selectedEntry) } } -void DetailsWidget::getSelectedGroup(Group* selectedGroup) +void DetailsWidget::getSelectedGroup(Group* selectedGroup) { if (!selectedGroup) { hideDetails(); @@ -212,7 +217,6 @@ void DetailsWidget::getSelectedGroup(Group* selectedGroup) m_ui->tabWidget->setTabEnabled(GroupNotesTab, false); - m_ui->totpButton->hide(); m_ui->totpWidget->hide(); @@ -248,7 +252,7 @@ void DetailsWidget::getSelectedGroup(Group* selectedGroup) 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)); @@ -263,23 +267,23 @@ void DetailsWidget::getSelectedGroup(Group* selectedGroup) void DetailsWidget::updateTotp() { - if (m_locked) { + 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(); - return; } - QString totpCode = m_currentEntry->totp(); - QString firstHalf = totpCode.left(totpCode.size()/2); - QString secondHalf = totpCode.right(totpCode.size()/2); - m_ui->totpLabel->setText(firstHalf + " " + secondHalf); } void DetailsWidget::showTotp(bool visible) -{ - if (visible){ +{ + if (visible) { m_ui->totpWidget->show(); } else { m_ui->totpWidget->hide(); - } + } } QString DetailsWidget::shortUrl(QString url) @@ -326,7 +330,8 @@ void DetailsWidget::setDatabaseMode(DatabaseWidget::Mode mode) } } -void DetailsWidget::updateTabIndex(int index) { +void DetailsWidget::updateTabIndex(int index) +{ if (m_ui->stackedWidget->currentIndex() == GroupPreview) { m_selectedTabGroup = index; } else { diff --git a/src/gui/DetailsWidget.h b/src/gui/DetailsWidget.h index 5bc026000..780fe5588 100644 --- a/src/gui/DetailsWidget.h +++ b/src/gui/DetailsWidget.h @@ -18,9 +18,8 @@ #ifndef KEEPASSX_DETAILSWIDGET_H #define KEEPASSX_DETAILSWIDGET_H -#include - #include "gui/DatabaseWidget.h" +#include namespace Ui { class DetailsWidget; diff --git a/src/gui/DetailsWidget.ui b/src/gui/DetailsWidget.ui index fb31409f8..8cece9a2f 100644 --- a/src/gui/DetailsWidget.ui +++ b/src/gui/DetailsWidget.ui @@ -6,22 +6,10 @@ 0 0 - 630 + 600 200 - - - 0 - 0 - - - - - 16777215 - 200 - - @@ -36,12 +24,6 @@ 0 - - - 20 - 16777215 - - @@ -123,7 +105,7 @@ - Generate TOTP Token + Close @@ -166,85 +148,19 @@ - - - 0 - 0 - - 0 - + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + - - - - - - - 0 - 0 - - - - PointingHandCursor - - - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Expiration - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 9 - 75 - true - - - - URL - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - @@ -267,6 +183,102 @@ + + + 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 + + + + + + + @@ -291,41 +303,11 @@ - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - 0 - 0 - - - - - + + @@ -347,7 +329,30 @@ - + + + + + 0 + 0 + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + @@ -357,7 +362,7 @@ - + @@ -379,7 +384,7 @@ - + @@ -389,7 +394,7 @@ - + @@ -411,15 +416,18 @@ - - + + - Qt::Vertical + Qt::Horizontal + + + QSizePolicy::Fixed 20 - 40 + 20 @@ -434,27 +442,9 @@ Attributes - - + + - - - 0 - 1 - - - - - 0 - 80 - - - - - 16777215 - 80 - - Qt::ClickFocus @@ -469,27 +459,9 @@ Notes - - + + - - - 0 - 1 - - - - - 0 - 80 - - - - - 16777215 - 80 - - Qt::ClickFocus @@ -504,8 +476,8 @@ Autotype - - + + QFrame::Sunken diff --git a/src/gui/FileDialog.cpp b/src/gui/FileDialog.cpp index e293db2ba..9f3caf6da 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 9f8fbb547..9a57a9218 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/Font.cpp b/src/gui/Font.cpp new file mode 100644 index 000000000..3583622dd --- /dev/null +++ b/src/gui/Font.cpp @@ -0,0 +1,36 @@ +/* + * 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 "Font.h" + +#include + +QFont Font::fixedFont() +{ + QFont fixedFont = 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", fixedFont.styleName(), fixedFont.pointSize()); + const QFont defaultFont; + if (fixedFont != defaultFont) { + fixedFont = consolasFont; + } +#endif + + return fixedFont; +} diff --git a/src/gui/Font.h b/src/gui/Font.h new file mode 100644 index 000000000..bfc3d7d36 --- /dev/null +++ b/src/gui/Font.h @@ -0,0 +1,31 @@ +/* + * 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_FONT_H +#define KEEPASSX_FONT_H + +#include + +class Font +{ +public: + static QFont fixedFont(); +private: + Font() {} +}; + +#endif // KEEPASSX_FONT_H diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c82ea1751..fa16adb80 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,6 +128,10 @@ 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); @@ -727,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() @@ -811,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()) { @@ -858,6 +866,7 @@ void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) void MainWindow::hideWindow() { + saveWindowInformation(); #ifndef Q_OS_MAC setWindowState(windowState() | Qt::WindowMinimized); #endif diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index ea17107ad..ade339c56 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -87,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 6824904ab..d0cded220 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -557,7 +557,7 @@ - Empty recycle bin + E&mpty recycle bin false diff --git a/src/gui/PasswordEdit.cpp b/src/gui/PasswordEdit.cpp index 54b0ca288..b084f9cf0 100644 --- a/src/gui/PasswordEdit.cpp +++ b/src/gui/PasswordEdit.cpp @@ -19,8 +19,7 @@ #include "PasswordEdit.h" #include "core/Config.h" - -#include +#include "gui/Font.h" const QColor PasswordEdit::CorrectSoFarColor = QColor(255, 205, 15); const QColor PasswordEdit::ErrorColor = QColor(255, 125, 125); @@ -31,9 +30,9 @@ PasswordEdit::PasswordEdit(QWidget* parent) { setEchoMode(QLineEdit::Password); updateStylesheet(); - - // set font to system monospace font and increase letter spacing - QFont passwordFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + + // use a monospace font for the password field + QFont passwordFont = Font::fixedFont(); passwordFont.setLetterSpacing(QFont::PercentageSpacing, 110); setFont(passwordFont); } diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp index d77634415..70375a048 100644 --- a/src/gui/PasswordGeneratorWidget.cpp +++ b/src/gui/PasswordGeneratorWidget.cpp @@ -21,6 +21,7 @@ #include #include +#include #include "core/Config.h" #include "core/PasswordGenerator.h" @@ -149,6 +150,19 @@ void PasswordGeneratorWidget::setStandaloneMode(bool standalone) } } +void PasswordGeneratorWidget::keyPressEvent(QKeyEvent* e) +{ + if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { + if (e->key() == Qt::Key_Escape && m_standalone == true) { + emit dialogTerminated(); + } else { + e->ignore(); + } + } else { + e->ignore(); + } +} + void PasswordGeneratorWidget::regeneratePassword() { if (m_ui->tabWidget->currentIndex() == Password) { diff --git a/src/gui/PasswordGeneratorWidget.h b/src/gui/PasswordGeneratorWidget.h index 130106461..3d6d27a55 100644 --- a/src/gui/PasswordGeneratorWidget.h +++ b/src/gui/PasswordGeneratorWidget.h @@ -81,6 +81,9 @@ private: const QScopedPointer m_passwordGenerator; const QScopedPointer m_dicewareGenerator; const QScopedPointer m_ui; + +protected: + void keyPressEvent(QKeyEvent* e) override; }; #endif // KEEPASSX_PASSWORDGENERATORWIDGET_H diff --git a/src/gui/PasswordGeneratorWidget.ui b/src/gui/PasswordGeneratorWidget.ui index a04719c33..a7e485817 100644 --- a/src/gui/PasswordGeneratorWidget.ui +++ b/src/gui/PasswordGeneratorWidget.ui @@ -318,7 +318,7 @@ QProgressBar::chunk { Extended ASCII - Extended ASCII + Extended ASCII true diff --git a/src/gui/SettingsWidget.cpp b/src/gui/SettingsWidget.cpp index a8b0e9424..4ba81c6f7 100644 --- a/src/gui/SettingsWidget.cpp +++ b/src/gui/SettingsWidget.cpp @@ -241,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/SetupTotpDialog.cpp b/src/gui/SetupTotpDialog.cpp index 5521773bd..52d63f0ef 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 243a05f9f..9e90e9686 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 a6d806287..c3a83e21d 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 17cc1120f..474acf773 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/csvImport/CsvImportWizard.cpp b/src/gui/csvImport/CsvImportWizard.cpp index eb4b21236..e9a8f4984 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 317018d99..b6414c0c9 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/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 8457c5af3..db35b2f8f 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,17 @@ #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/Font.h" #include "gui/entry/AutoTypeAssociationsModel.h" #include "gui/entry/EntryAttachmentsModel.h" #include "gui/entry/EntryAttributesModel.h" @@ -51,11 +59,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 +83,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(); @@ -122,13 +140,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); @@ -244,6 +263,244 @@ void EditEntryWidget::updateHistoryButtons(const QModelIndex& current, const QMo } } +#ifdef WITH_XC_SSHAGENT +void EditEntryWidget::setupSSHAgent() +{ + m_sshAgentUi->setupUi(m_sshAgentWidget); + + QFont fixedFont = Font::fixedFont(); + m_sshAgentUi->fingerprintTextLabel->setFont(fixedFont); + m_sshAgentUi->commentTextLabel->setFont(fixedFont); + m_sshAgentUi->publicKeyEdit->setFont(fixedFont); + + connect(m_sshAgentUi->attachmentRadioButton, SIGNAL(clicked(bool)), SLOT(updateSSHAgentKeyInfo())); + connect(m_sshAgentUi->attachmentComboBox, SIGNAL(currentIndexChanged(int)), SLOT(updateSSHAgentKeyInfo())); + connect(m_sshAgentUi->externalFileRadioButton, SIGNAL(clicked(bool)), SLOT(updateSSHAgentKeyInfo())); + connect(m_sshAgentUi->externalFileEdit, SIGNAL(textChanged(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() +{ + 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->attachmentComboBox->clear(); + m_sshAgentUi->addToAgentButton->setEnabled(false); + m_sshAgentUi->removeFromAgentButton->setEnabled(false); + m_sshAgentUi->copyToClipboardButton->setEnabled(false); + + m_sshAgentUi->attachmentComboBox->addItem(""); + + for (QString fileName : m_entryAttachments->keys()) { + if (fileName == "KeeAgent.settings") { + continue; + } + + m_sshAgentUi->attachmentComboBox->addItem(fileName); + } + + m_sshAgentUi->attachmentComboBox->setCurrentText(settings.attachmentName()); + m_sshAgentUi->externalFileEdit->setText(settings.fileName()); + + if (settings.selectedType() == "attachment") { + m_sshAgentUi->attachmentRadioButton->setChecked(true); + } else { + m_sshAgentUi->externalFileRadioButton->setChecked(true); + } + + m_sshAgentSettings = settings; + + updateSSHAgentKeyInfo(); +} + +void EditEntryWidget::updateSSHAgentKeyInfo() +{ + m_sshAgentUi->addToAgentButton->setEnabled(false); + m_sshAgentUi->removeFromAgentButton->setEnabled(false); + m_sshAgentUi->copyToClipboardButton->setEnabled(false); + m_sshAgentUi->fingerprintTextLabel->setText(tr("n/a")); + m_sshAgentUi->commentTextLabel->setText(tr("n/a")); + m_sshAgentUi->decryptButton->setEnabled(false); + m_sshAgentUi->publicKeyEdit->document()->setPlainText(""); + + OpenSSHKey key; + + if (!getOpenSSHKey(key)) { + return; + } + + m_sshAgentUi->fingerprintTextLabel->setText(key.fingerprint()); + + if (key.encrypted()) { + m_sshAgentUi->commentTextLabel->setText(tr("(encrypted)")); + m_sshAgentUi->decryptButton->setEnabled(true); + } else { + m_sshAgentUi->commentTextLabel->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->attachmentComboBox->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()); + + if (m_sshAgentUi->attachmentRadioButton->isChecked()) { + settings.setSelectedType("attachment"); + } else { + settings.setSelectedType("file"); + } + settings.setAttachmentName(m_sshAgentUi->attachmentComboBox->currentText()); + settings.setFileName(m_sshAgentUi->externalFileEdit->text()); + + // 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->externalFileEdit->setText(fileName); + } +} + +bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key) +{ + QByteArray privateKeyData; + + if (m_sshAgentUi->attachmentRadioButton->isChecked()) { + privateKeyData = m_entryAttachments->value(m_sshAgentUi->attachmentComboBox->currentText()); + } else { + QFile localFile(m_sshAgentUi->externalFileEdit->text()); + + if (localFile.fileName().isEmpty()) { + return false; + } + + 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 (privateKeyData.length() == 0) { + return false; + } + + 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->commentTextLabel->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->commentTextLabel->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); @@ -285,6 +542,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"))); @@ -397,6 +655,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) { @@ -444,15 +708,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() @@ -521,7 +798,7 @@ void EditEntryWidget::cancel() clear(); - emit editFinished(false); + emit editFinished(m_saved); } void EditEntryWidget::clear() @@ -672,6 +949,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(); @@ -702,7 +1005,7 @@ void EditEntryWidget::revealCurrentAttribute() } } -void EditEntryWidget::insertAttachment() +void EditEntryWidget::insertAttachments() { Q_ASSERT(!m_history); @@ -710,53 +1013,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); } } @@ -767,55 +1132,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 74da2c46f..883e7e7fb 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(); @@ -101,12 +108,24 @@ private slots: 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(); @@ -114,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 2c9aa59b7..dc07603aa 100644 --- a/src/gui/entry/EditEntryWidgetMain.ui +++ b/src/gui/entry/EditEntryWidgetMain.ui @@ -127,12 +127,6 @@ 100 - - - Monospace - 10 - - diff --git a/src/gui/entry/EditEntryWidgetSSHAgent.ui b/src/gui/entry/EditEntryWidgetSSHAgent.ui new file mode 100644 index 000000000..34c8fae82 --- /dev/null +++ b/src/gui/entry/EditEntryWidgetSSHAgent.ui @@ -0,0 +1,275 @@ + + + EditEntryWidgetSSHAgent + + + + 0 + 0 + 471 + 480 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Remove key from agent after + + + + + + + seconds + + + 999999999 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Fingerprint + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Remove key from agent when database is closed/locked + + + + + + + Public key + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + Add key to agent when database is opened/unlocked + + + + + + + Comment + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Decrypt + + + + + + + + + + Monospace + + + + n/a + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Copy to clipboard + + + + + + + Private key + + + + + + External file + + + + + + + Browse... + + + + + + + Attachment + + + true + + + + + + + + + + + + Add to agent + + + + + + + Remove from agent + + + + + + + + + + 0 + 0 + + + + false + + + + + + + + + + Require user confirmation when this key is used + + + + + + + + Monospace + + + + true + + + + + + + + + + Monospace + + + + n/a + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/src/http/AccessControlDialog.cpp b/src/http/AccessControlDialog.cpp index ef02215a3..30ab3f21c 100644 --- a/src/http/AccessControlDialog.cpp +++ b/src/http/AccessControlDialog.cpp @@ -44,7 +44,9 @@ void AccessControlDialog::setUrl(const QString &url) void AccessControlDialog::setItems(const QList &items) { for (Entry* entry: items) { - ui->itemsList->addItem(entry->title() + " - " + entry->username()); + QString title = entry->resolveMultiplePlaceholders(entry->title()); + QString username = entry->resolveMultiplePlaceholders(entry->username()); + ui->itemsList->addItem(title + " - " + username); } } diff --git a/src/http/Service.cpp b/src/http/Service.cpp index e68a7bfd9..7f74a3ae8 100644 --- a/src/http/Service.cpp +++ b/src/http/Service.cpp @@ -548,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"), @@ -592,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 895bf0c73..ab48d0cb5 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 000000000..70fa04bde --- /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 000000000..f9d1be3f9 --- /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 000000000..e8bc75ae1 --- /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 000000000..b6462daa0 --- /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 000000000..e97ee87bb --- /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 000000000..b9ed236fd --- /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 000000000..c61010180 --- /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 000000000..1733e21b1 --- /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 000000000..218e98acb --- /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 000000000..4022750d1 --- /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 000000000..2c51ee459 --- /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 000000000..eca6c9edd --- /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 000000000..7969fe0cc --- /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 000000000..078ff7b0d --- /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 000000000..fed4cdb29 --- /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 000000000..4878e5588 --- /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 000000000..02e9ac0bd --- /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 7a584def2..f102335aa 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -28,14 +28,47 @@ #include #include -const quint8 QTotp::defaultStep = 30; -const quint8 QTotp::defaultDigits = 6; +const quint8 Totp::defaultStep = 30; +const quint8 Totp::defaultDigits = 6; -QTotp::QTotp() +/** + * 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; + +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,7 +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); @@ -92,7 +128,7 @@ QString QTotp::parseOtpString(QString key, quint8& digits, quint8& step) return seed; } -QString QTotp::generateTotp(const QByteArray key, +QString Totp::generateTotp(const QByteArray key, quint64 time, const quint8 numDigits = defaultDigits, const quint8 step = defaultStep) @@ -119,20 +155,34 @@ QString QTotp::generateTotp(const QByteArray key, | (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 QTotp::generateOtpString(const QString& secret, +QUrl Totp::generateOtpString(const QString& secret, const QString& type, const QString& issuer, const QString& username, const QString& algorithm, - const quint8& digits, - const quint8& step) + quint8 digits, + quint8 step) { QUrl keyUri; keyUri.setScheme("otpauth"); diff --git a/src/totp/totp.h b/src/totp/totp.h index d5d8aa679..7d4c78c1d 100644 --- a/src/totp/totp.h +++ b/src/totp/totp.h @@ -20,13 +20,15 @@ #define QTOTP_H #include +#include +#include class QUrl; -class QTotp +class Totp { public: - QTotp(); + 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, @@ -34,10 +36,25 @@ public: const QString& issuer, const QString& username, const QString& algorithm, - const quint8& digits, - const quint8& step); + 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 c1f1adf41..c36eefd4a 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/TestCsvParser.cpp b/tests/TestCsvParser.cpp index 57bc683a2..a292b56bb 100644 --- a/tests/TestCsvParser.cpp +++ b/tests/TestCsvParser.cpp @@ -24,17 +24,12 @@ QTEST_GUILESS_MAIN(TestCsvParser) void TestCsvParser::initTestCase() { - parser = new CsvParser(); -} - -void TestCsvParser::cleanupTestCase() -{ - delete parser; + parser.reset(new CsvParser()); } void TestCsvParser::init() { - file = new QTemporaryFile(); + file.reset(new QTemporaryFile()); if (not file->open()) QFAIL("Cannot open file!"); parser->setBackslashSyntax(false); @@ -51,20 +46,20 @@ void TestCsvParser::cleanup() /****************** TEST CASES ******************/ void TestCsvParser::testMissingQuote() { parser->setTextQualifier(':'); - QTextStream out(file); + QTextStream out(file.data()); out << "A,B\n:BM,1"; QEXPECT_FAIL("", "Bad format", Continue); - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QWARN(parser->getStatus().toLatin1()); } void TestCsvParser::testMalformed() { parser->setTextQualifier(':'); - QTextStream out(file); + QTextStream out(file.data()); out << "A,B,C\n:BM::,1,:2:"; QEXPECT_FAIL("", "Bad format", Continue); - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QWARN(parser->getStatus().toLatin1()); } @@ -72,14 +67,14 @@ void TestCsvParser::testMalformed() { void TestCsvParser::testBackslashSyntax() { parser->setBackslashSyntax(true); parser->setTextQualifier(QChar('X')); - QTextStream out(file); + QTextStream out(file.data()); //attended result: one"\t\"wo out << "Xone\\\"\\\\t\\\\\\\"w\noX\n" << "X13X,X2\\X,X,\"\"3\"X\r" << "3,X\"4\"X,,\n" << "XX\n" << "\\"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.at(0).at(0) == "one\"\\t\\\"w\no"); QVERIFY(t.at(1).at(0) == "13"); @@ -94,10 +89,10 @@ void TestCsvParser::testBackslashSyntax() { } void TestCsvParser::testQuoted() { - QTextStream out(file); + QTextStream out(file.data()); out << "ro,w,\"end, of \"\"\"\"\"\"row\"\"\"\"\"\n" << "2\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.at(0).at(0) == "ro"); QVERIFY(t.at(0).at(1) == "w"); @@ -107,41 +102,41 @@ void TestCsvParser::testQuoted() { } void TestCsvParser::testEmptySimple() { - QTextStream out(file); + QTextStream out(file.data()); out <<""; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 0); } void TestCsvParser::testEmptyQuoted() { - QTextStream out(file); + QTextStream out(file.data()); out <<"\"\""; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 0); } void TestCsvParser::testEmptyNewline() { - QTextStream out(file); + QTextStream out(file.data()); out <<"\"\n\""; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 0); } void TestCsvParser::testEmptyFile() { - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 0); } void TestCsvParser::testNewline() { - QTextStream out(file); + QTextStream out(file.data()); out << "1,2\n\n\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 1); QVERIFY(t.at(0).at(0) == "1"); @@ -150,9 +145,9 @@ void TestCsvParser::testNewline() void TestCsvParser::testCR() { - QTextStream out(file); + QTextStream out(file.data()); out << "1,2\r3,4"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 2); QVERIFY(t.at(0).at(0) == "1"); @@ -163,9 +158,9 @@ void TestCsvParser::testCR() void TestCsvParser::testLF() { - QTextStream out(file); + QTextStream out(file.data()); out << "1,2\n3,4"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 2); QVERIFY(t.at(0).at(0) == "1"); @@ -176,9 +171,9 @@ void TestCsvParser::testLF() void TestCsvParser::testCRLF() { - QTextStream out(file); + QTextStream out(file.data()); out << "1,2\r\n3,4"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 2); QVERIFY(t.at(0).at(0) == "1"); @@ -189,13 +184,13 @@ void TestCsvParser::testCRLF() void TestCsvParser::testComments() { - QTextStream out(file); + QTextStream out(file.data()); out << " #one\n" << " \t # two, three \r\n" << " #, sing\t with\r" << " #\t me!\n" << "useful,text #1!"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 1); QVERIFY(t.at(0).at(0) == "useful"); @@ -203,21 +198,21 @@ void TestCsvParser::testComments() } void TestCsvParser::testColumns() { - QTextStream out(file); + QTextStream out(file.data()); out << "1,2\n" << ",,,,,,,,,a\n" << "a,b,c,d\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(parser->getCsvCols() == 10); } void TestCsvParser::testSimple() { - QTextStream out(file); + QTextStream out(file.data()); out << ",,2\r,2,3\n" << "A,,B\"\n" << " ,,\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 4); QVERIFY(t.at(0).at(0) == ""); @@ -236,11 +231,11 @@ void TestCsvParser::testSimple() { void TestCsvParser::testSeparator() { parser->setFieldSeparator('\t'); - QTextStream out(file); + QTextStream out(file.data()); out << "\t\t2\r\t2\t3\n" << "A\t\tB\"\n" << " \t\t\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 4); QVERIFY(t.at(0).at(0) == ""); @@ -260,10 +255,10 @@ void TestCsvParser::testSeparator() { void TestCsvParser::testMultiline() { parser->setTextQualifier(QChar(':')); - QTextStream out(file); + QTextStream out(file.data()); out << ":1\r\n2a::b:,:3\r4:\n" << "2\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.at(0).at(0) == "1\n2a:b"); QVERIFY(t.at(0).at(1) == "3\n4"); @@ -281,10 +276,10 @@ void TestCsvParser::testEmptyReparsing() void TestCsvParser::testReparsing() { - QTextStream out(file); + QTextStream out(file.data()); out << ":te\r\nxt1:,:te\rxt2:,:end of \"this\n string\":\n" << "2\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QEXPECT_FAIL("", "Wrong qualifier", Continue); @@ -303,10 +298,10 @@ void TestCsvParser::testReparsing() void TestCsvParser::testQualifier() { parser->setTextQualifier(QChar('X')); - QTextStream out(file); + QTextStream out(file.data()); out << "X1X,X2XX,X,\"\"3\"\"\"X\r" << "3,X\"4\"X,,\n"; - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 2); QVERIFY(t.at(0).at(0) == "1"); @@ -324,10 +319,10 @@ void TestCsvParser::testUnicode() { //CORRECT QChar g(0x20AC); //ERROR QChar g("\u20AC"); parser->setFieldSeparator(QChar('A')); - QTextStream out(file); + QTextStream out(file.data()); out << QString("€1A2śA\"3śAż\"Ażac"); - QVERIFY(parser->parse(file)); + QVERIFY(parser->parse(file.data())); t = parser->getCsvTable(); QVERIFY(t.size() == 1); QVERIFY(t.at(0).at(0) == "€1"); diff --git a/tests/TestCsvParser.h b/tests/TestCsvParser.h index 0cf8b94d3..f8c327d63 100644 --- a/tests/TestCsvParser.h +++ b/tests/TestCsvParser.h @@ -22,6 +22,7 @@ #include #include #include +#include #include "core/CsvParser.h" @@ -37,7 +38,6 @@ private slots: void init(); void cleanup(); void initTestCase(); - void cleanupTestCase(); void testUnicode(); void testLF(); @@ -62,8 +62,8 @@ private slots: void testColumns(); private: - QTemporaryFile* file; - CsvParser* parser; + QScopedPointer file; + QScopedPointer parser; CsvTable t; void dumpRow(CsvTable table, int row); }; diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 1e863dbeb..47082d12f 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -262,3 +262,244 @@ void TestEntry::testResolveRecursivePlaceholders() QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->url()), url); QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->title()), QString("title+/some/path+fragment+title")); } + +void TestEntry::testResolveReferencePlaceholders() +{ + Database db; + Group* root = db.rootGroup(); + + Entry* entry1 = new Entry(); + entry1->setGroup(root); + entry1->setUuid(Uuid::random()); + entry1->setTitle("Title1"); + entry1->setUsername("Username1"); + entry1->setPassword("Password1"); + entry1->setUrl("Url1"); + entry1->setNotes("Notes1"); + entry1->attributes()->set("CustomAttribute1", "CustomAttributeValue1"); + + Group* group = new Group(); + group->setParent(root); + Entry* entry2 = new Entry(); + entry2->setGroup(group); + entry2->setUuid(Uuid::random()); + entry2->setTitle("Title2"); + entry2->setUsername("Username2"); + entry2->setPassword("Password2"); + entry2->setUrl("Url2"); + entry2->setNotes("Notes2"); + entry2->attributes()->set("CustomAttribute2", "CustomAttributeValue2"); + + Entry* entry3 = new Entry(); + entry3->setGroup(group); + entry3->setUuid(Uuid::random()); + entry3->setTitle("{S:AttributeTitle}"); + entry3->setUsername("{S:AttributeUsername}"); + entry3->setPassword("{S:AttributePassword}"); + entry3->setUrl("{S:AttributeUrl}"); + entry3->setNotes("{S:AttributeNotes}"); + entry3->attributes()->set("AttributeTitle", "TitleValue"); + entry3->attributes()->set("AttributeUsername", "UsernameValue"); + entry3->attributes()->set("AttributePassword", "PasswordValue"); + entry3->attributes()->set("AttributeUrl", "UrlValue"); + entry3->attributes()->set("AttributeNotes", "NotesValue"); + + Entry* tstEntry = new Entry(); + tstEntry->setGroup(root); + tstEntry->setUuid(Uuid::random()); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuid().toHex())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry1->password())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry1->url())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry1->notes())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))), entry1->title()); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuid().toHex())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())), entry1->username()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry1->password())), entry1->password()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), entry1->url()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), entry1->notes()); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry2->uuid().toHex())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry2->username())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry2->password())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry2->url())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry2->notes())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@O:%1}").arg(entry2->attributes()->value("CustomAttribute2"))), entry2->title()); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry2->username())), entry2->username()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry2->password())), entry2->password()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry2->url())), entry2->url()); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry2->notes())), entry2->notes()); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex())), entry3->attributes()->value("AttributeTitle")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuid().toHex())), entry3->attributes()->value("AttributeUsername")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuid().toHex())), entry3->attributes()->value("AttributePassword")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuid().toHex())), entry3->attributes()->value("AttributeUrl")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuid().toHex())), entry3->attributes()->value("AttributeNotes")); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuid().toHex().toUpper())), entry3->attributes()->value("AttributeTitle")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuid().toHex().toUpper())), entry3->attributes()->value("AttributeUsername")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuid().toHex().toUpper())), entry3->attributes()->value("AttributePassword")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuid().toHex().toUpper())), entry3->attributes()->value("AttributeUrl")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuid().toHex().toUpper())), entry3->attributes()->value("AttributeNotes")); + + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:t@i:%1}").arg(entry3->uuid().toHex().toLower())), entry3->attributes()->value("AttributeTitle")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:u@i:%1}").arg(entry3->uuid().toHex().toLower())), entry3->attributes()->value("AttributeUsername")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:p@i:%1}").arg(entry3->uuid().toHex().toLower())), entry3->attributes()->value("AttributePassword")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:a@i:%1}").arg(entry3->uuid().toHex().toLower())), entry3->attributes()->value("AttributeUrl")); + QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:n@i:%1}").arg(entry3->uuid().toHex().toLower())), entry3->attributes()->value("AttributeNotes")); +} + +void TestEntry::testResolveNonIdPlaceholdersToUuid() +{ + Database db; + auto* root = db.rootGroup(); + + Entry referencedEntryTitle; + referencedEntryTitle.setGroup(root); + referencedEntryTitle.setTitle("myTitle"); + referencedEntryTitle.setUuid(Uuid::random()); + + Entry referencedEntryUsername; + referencedEntryUsername.setGroup(root); + referencedEntryUsername.setUsername("myUser"); + referencedEntryUsername.setUuid(Uuid::random()); + + Entry referencedEntryPassword; + referencedEntryPassword.setGroup(root); + referencedEntryPassword.setPassword("myPassword"); + referencedEntryPassword.setUuid(Uuid::random()); + + Entry referencedEntryUrl; + referencedEntryUrl.setGroup(root); + referencedEntryUrl.setUrl("myUrl"); + referencedEntryUrl.setUuid(Uuid::random()); + + Entry referencedEntryNotes; + referencedEntryNotes.setGroup(root); + referencedEntryNotes.setNotes("myNotes"); + referencedEntryNotes.setUuid(Uuid::random()); + + const QList placeholders{'T', 'U', 'P', 'A', 'N'}; + for (const QChar searchIn : placeholders) { + const Entry* referencedEntry = nullptr; + QString newEntryNotesRaw("{REF:I@%1:%2}"); + + switch(searchIn.toLatin1()) { + case 'T': + referencedEntry = &referencedEntryTitle; + newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->title()); + break; + case 'U': + referencedEntry = &referencedEntryUsername; + newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->username()); + break; + case 'P': + referencedEntry = &referencedEntryPassword; + newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->password()); + break; + case 'A': + referencedEntry = &referencedEntryUrl; + newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->url()); + break; + case 'N': + referencedEntry = &referencedEntryNotes; + newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->notes()); + break; + default: + break; + } + + Entry newEntry; + newEntry.setGroup(root); + newEntry.setNotes(newEntryNotesRaw); + + const auto newEntryNotesResolved = + newEntry.resolveMultiplePlaceholders(newEntry.notes()); + QCOMPARE(newEntryNotesResolved, QString(referencedEntry->uuid().toHex())); + } +} + +void TestEntry::testResolveClonedEntry() +{ + Database db; + Group* root = db.rootGroup(); + + Entry* original = new Entry(); + original->setGroup(root); + original->setUuid(Uuid::random()); + original->setTitle("Title"); + original->setUsername("SomeUsername"); + original->setPassword("SomePassword"); + + QCOMPARE(original->resolveMultiplePlaceholders(original->username()), original->username()); + QCOMPARE(original->resolveMultiplePlaceholders(original->password()), original->password()); + + // Top-level clones. + Entry* clone1 = original->clone(Entry::CloneNewUuid); + clone1->setGroup(root); + Entry* clone2 = original->clone(Entry::CloneUserAsRef | Entry::CloneNewUuid); + clone2->setGroup(root); + Entry* clone3 = original->clone(Entry::ClonePassAsRef | Entry::CloneNewUuid); + clone3->setGroup(root); + Entry* clone4 = original->clone(Entry::CloneUserAsRef | Entry::ClonePassAsRef | Entry::CloneNewUuid); + clone4->setGroup(root); + + QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->username()), original->username()); + QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->password()), original->password()); + QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->username()), original->username()); + QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->password()), original->password()); + QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->username()), original->username()); + QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->password()), original->password()); + QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->username()), original->username()); + QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->password()), original->password()); + + // Second-level clones. + Entry* cclone1 = clone4->clone(Entry::CloneNewUuid); + cclone1->setGroup(root); + Entry* cclone2 = clone4->clone(Entry::CloneUserAsRef | Entry::CloneNewUuid); + cclone2->setGroup(root); + Entry* cclone3 = clone4->clone(Entry::ClonePassAsRef | Entry::CloneNewUuid); + cclone3->setGroup(root); + Entry* cclone4 = clone4->clone(Entry::CloneUserAsRef | Entry::ClonePassAsRef | Entry::CloneNewUuid); + cclone4->setGroup(root); + + QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->username()), original->username()); + QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->password()), original->password()); + QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->username()), original->username()); + QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->password()), original->password()); + QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->username()), original->username()); + QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->password()), original->password()); + QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->username()), original->username()); + QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password()); + + // Change the original's attributes and make sure that the changes are tracked. + QString oldUsername = original->username(); + QString oldPassword = original->password(); + original->setUsername("DifferentUsername"); + original->setPassword("DifferentPassword"); + + QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->username()), oldUsername); + QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->password()), oldPassword); + QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->username()), original->username()); + QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->password()), oldPassword); + QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->username()), oldUsername); + QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->password()), original->password()); + QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->username()), original->username()); + QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->password()), original->password()); + + QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->username()), original->username()); + QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->password()), original->password()); + QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->username()), original->username()); + QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->password()), original->password()); + QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->username()), original->username()); + QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->password()), original->password()); + QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->username()), original->username()); + QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password()); +} diff --git a/tests/TestEntry.h b/tests/TestEntry.h index 50fec57a5..7c2350861 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -34,6 +34,9 @@ private slots: void testResolveUrl(); void testResolveUrlPlaceholders(); void testResolveRecursivePlaceholders(); + void testResolveReferencePlaceholders(); + void testResolveNonIdPlaceholdersToUuid(); + void testResolveClonedEntry(); }; #endif // KEEPASSX_TESTENTRY_H diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 5a809670f..24bdfeb5a 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -18,9 +18,10 @@ #include "TestGroup.h" -#include -#include #include +#include +#include +#include #include #include "core/Database.h" @@ -75,6 +76,7 @@ void TestGroup::testParenting() QCOMPARE(g3->children().size(), 1); QCOMPARE(g4->children().size(), 0); + QVERIFY(rootGroup->children().at(0) == g1); QVERIFY(rootGroup->children().at(0) == g1); QVERIFY(g1->children().at(0) == g2); QVERIFY(g1->children().at(1) == g3); @@ -99,7 +101,6 @@ void TestGroup::testParenting() g3->setIcon(Uuid::random()); g1->setIcon(2); QCOMPARE(spy.count(), 6); - delete db; QVERIFY(rootGroup.isNull()); @@ -107,7 +108,6 @@ void TestGroup::testParenting() QVERIFY(g2.isNull()); QVERIFY(g3.isNull()); QVERIFY(g4.isNull()); - delete tmpRoot; } @@ -117,18 +117,18 @@ void TestGroup::testSignals() Database* db2 = new Database(); QPointer root = db->rootGroup(); - QSignalSpy spyAboutToAdd(db, SIGNAL(groupAboutToAdd(Group*,int))); + QSignalSpy spyAboutToAdd(db, SIGNAL(groupAboutToAdd(Group*, int))); QSignalSpy spyAdded(db, SIGNAL(groupAdded())); QSignalSpy spyAboutToRemove(db, SIGNAL(groupAboutToRemove(Group*))); QSignalSpy spyRemoved(db, SIGNAL(groupRemoved())); - QSignalSpy spyAboutToMove(db, SIGNAL(groupAboutToMove(Group*,Group*,int))); + QSignalSpy spyAboutToMove(db, SIGNAL(groupAboutToMove(Group*, Group*, int))); QSignalSpy spyMoved(db, SIGNAL(groupMoved())); - QSignalSpy spyAboutToAdd2(db2, SIGNAL(groupAboutToAdd(Group*,int))); + QSignalSpy spyAboutToAdd2(db2, SIGNAL(groupAboutToAdd(Group*, int))); QSignalSpy spyAdded2(db2, SIGNAL(groupAdded())); QSignalSpy spyAboutToRemove2(db2, SIGNAL(groupAboutToRemove(Group*))); QSignalSpy spyRemoved2(db2, SIGNAL(groupRemoved())); - QSignalSpy spyAboutToMove2(db2, SIGNAL(groupAboutToMove(Group*,Group*,int))); + QSignalSpy spyAboutToMove2(db2, SIGNAL(groupAboutToMove(Group*, Group*, int))); QSignalSpy spyMoved2(db2, SIGNAL(groupMoved())); Group* g1 = new Group(); @@ -251,7 +251,7 @@ void TestGroup::testEntries() void TestGroup::testDeleteSignals() { - Database* db = new Database(); + QScopedPointer db(new Database()); Group* groupRoot = db->rootGroup(); Group* groupChild = new Group(); Group* groupChildChild = new Group(); @@ -260,15 +260,13 @@ void TestGroup::testDeleteSignals() groupChildChild->setObjectName("groupChildChild"); groupChild->setParent(groupRoot); groupChildChild->setParent(groupChild); - QSignalSpy spyAboutToRemove(db, SIGNAL(groupAboutToRemove(Group*))); - QSignalSpy spyRemoved(db, SIGNAL(groupRemoved())); + QSignalSpy spyAboutToRemove(db.data(), SIGNAL(groupAboutToRemove(Group*))); + QSignalSpy spyRemoved(db.data(), SIGNAL(groupRemoved())); delete groupChild; QVERIFY(groupRoot->children().isEmpty()); QCOMPARE(spyAboutToRemove.count(), 2); QCOMPARE(spyRemoved.count(), 2); - delete db; - Group* group = new Group(); Entry* entry = new Entry(); @@ -282,7 +280,7 @@ void TestGroup::testDeleteSignals() QCOMPARE(spyEntryRemoved.count(), 1); delete group; - Database* db2 = new Database(); + QScopedPointer db2(new Database()); Group* groupRoot2 = db2->rootGroup(); Group* group2 = new Group(); group2->setParent(groupRoot2); @@ -294,12 +292,11 @@ void TestGroup::testDeleteSignals() delete group2; QCOMPARE(spyEntryAboutToRemove2.count(), 1); QCOMPARE(spyEntryRemoved2.count(), 1); - delete db2; } void TestGroup::testCopyCustomIcon() { - Database* dbSource = new Database(); + QScopedPointer dbSource(new Database()); Uuid groupIconUuid = Uuid::random(); QImage groupIcon(16, 16, QImage::Format_RGB32); @@ -321,7 +318,7 @@ void TestGroup::testCopyCustomIcon() entry->setIcon(entryIconUuid); QCOMPARE(entry->icon(), entryIcon); - Database* dbTarget = new Database(); + QScopedPointer dbTarget(new Database()); group->setParent(dbTarget->rootGroup()); QVERIFY(dbTarget->metadata()->containsCustomIcon(groupIconUuid)); @@ -332,37 +329,34 @@ void TestGroup::testCopyCustomIcon() QVERIFY(dbTarget->metadata()->containsCustomIcon(entryIconUuid)); QCOMPARE(dbTarget->metadata()->customIcon(entryIconUuid), entryIcon); QCOMPARE(entry->icon(), entryIcon); - - delete dbSource; - delete dbTarget; } void TestGroup::testClone() { - Database* db = new Database(); + QScopedPointer db(new Database()); - Group* originalGroup = new Group(); + QScopedPointer originalGroup(new Group()); originalGroup->setParent(db->rootGroup()); originalGroup->setName("Group"); originalGroup->setIcon(42); - Entry* originalGroupEntry = new Entry(); - originalGroupEntry->setGroup(originalGroup); + QScopedPointer originalGroupEntry(new Entry()); + originalGroupEntry->setGroup(originalGroup.data()); originalGroupEntry->setTitle("GroupEntryOld"); originalGroupEntry->setIcon(43); originalGroupEntry->beginUpdate(); originalGroupEntry->setTitle("GroupEntry"); originalGroupEntry->endUpdate(); - Group* subGroup = new Group(); - subGroup->setParent(originalGroup); + QScopedPointer subGroup(new Group()); + subGroup->setParent(originalGroup.data()); subGroup->setName("SubGroup"); - Entry* subGroupEntry = new Entry(); - subGroupEntry->setGroup(subGroup); + QScopedPointer subGroupEntry(new Entry()); + subGroupEntry->setGroup(subGroup.data()); subGroupEntry->setTitle("SubGroupEntry"); - Group* clonedGroup = originalGroup->clone(); + QScopedPointer clonedGroup(originalGroup->clone()); QVERIFY(!clonedGroup->parentGroup()); QVERIFY(!clonedGroup->database()); QVERIFY(clonedGroup->uuid() != originalGroup->uuid()); @@ -387,19 +381,32 @@ void TestGroup::testClone() QVERIFY(clonedSubGroupEntry->uuid() != subGroupEntry->uuid()); QCOMPARE(clonedSubGroupEntry->title(), QString("SubGroupEntry")); - Group* clonedGroupKeepUuid = originalGroup->clone(Entry::CloneNoFlags); + QScopedPointer clonedGroupKeepUuid(originalGroup->clone(Entry::CloneNoFlags)); QCOMPARE(clonedGroupKeepUuid->entries().at(0)->uuid(), originalGroupEntry->uuid()); QCOMPARE(clonedGroupKeepUuid->children().at(0)->entries().at(0)->uuid(), subGroupEntry->uuid()); - delete clonedGroup; - delete clonedGroupKeepUuid; - delete db; + QScopedPointer clonedGroupNoFlags(originalGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags)); + QCOMPARE(clonedGroupNoFlags->entries().size(), 0); + QVERIFY(clonedGroupNoFlags->uuid() == originalGroup->uuid()); + + QScopedPointer 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); + + QScopedPointer 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()); } void TestGroup::testCopyCustomIcons() { - Database* dbSource = new Database(); - Database* dbTarget = new Database(); + QScopedPointer dbSource(new Database()); + QScopedPointer dbTarget(new Database()); QImage iconImage1(1, 1, QImage::Format_RGB32); iconImage1.setPixel(0, 0, qRgb(1, 2, 3)); @@ -407,20 +414,20 @@ void TestGroup::testCopyCustomIcons() QImage iconImage2(1, 1, QImage::Format_RGB32); iconImage2.setPixel(0, 0, qRgb(4, 5, 6)); - Group* group1 = new Group(); + QScopedPointer group1(new Group()); group1->setParent(dbSource->rootGroup()); Uuid group1Icon = Uuid::random(); dbSource->metadata()->addCustomIcon(group1Icon, iconImage1); group1->setIcon(group1Icon); - Group* group2 = new Group(); - group2->setParent(group1); + QScopedPointer group2(new Group()); + group2->setParent(group1.data()); Uuid group2Icon = Uuid::random(); dbSource->metadata()->addCustomIcon(group2Icon, iconImage1); group2->setIcon(group2Icon); - Entry* entry1 = new Entry(); - entry1->setGroup(group2); + QScopedPointer entry1(new Entry()); + entry1->setGroup(group2.data()); Uuid entry1IconOld = Uuid::random(); dbSource->metadata()->addCustomIcon(entry1IconOld, iconImage1); entry1->setIcon(entry1IconOld); @@ -447,14 +454,11 @@ void TestGroup::testCopyCustomIcons() QCOMPARE(metaTarget->customIcon(group1Icon).pixel(0, 0), qRgb(1, 2, 3)); QCOMPARE(metaTarget->customIcon(group2Icon).pixel(0, 0), qRgb(4, 5, 6)); - - delete dbTarget; - delete dbSource; } void TestGroup::testFindEntry() { - Database* db = new Database(); + QScopedPointer db(new Database()); Entry* entry1 = new Entry(); entry1->setTitle(QString("entry1")); @@ -525,13 +529,11 @@ void TestGroup::testFindEntry() // An invalid UUID. entry = db->rootGroup()->findEntry(QString("febfb01ebcdf9dbd90a3f1579dc")); QVERIFY(entry == nullptr); - - delete db; } void TestGroup::testFindGroupByPath() { - Database* db = new Database(); + QScopedPointer db(new Database()); Group* group1 = new Group(); group1->setName("group1"); @@ -589,13 +591,11 @@ void TestGroup::testFindGroupByPath() group = db->rootGroup()->findGroupByPath("invalid"); QVERIFY(group == nullptr); - - delete db; } void TestGroup::testPrint() { - Database* db = new Database(); + QScopedPointer db(new Database()); QString output = db->rootGroup()->print(); QCOMPARE(output, QString("[empty]\n")); @@ -635,8 +635,6 @@ void TestGroup::testPrint() output = group1->print(); QVERIFY(!output.contains(QString("group1/\n"))); QVERIFY(output.contains(QString("entry2\n"))); - - delete db; } void TestGroup::testLocate() diff --git a/tests/TestKeePass2Writer.cpp b/tests/TestKeePass2Writer.cpp index 9f0c87be7..f6d3f58ad 100644 --- a/tests/TestKeePass2Writer.cpp +++ b/tests/TestKeePass2Writer.cpp @@ -148,13 +148,15 @@ void TestKeePass2Writer::testRepair() KeePass2Repair repair; QFile file(brokenDbFilename); file.open(QIODevice::ReadOnly); - QCOMPARE(repair.repairDatabase(&file, key), KeePass2Repair::RepairSuccess); - Database* dbRepaired = repair.database(); + auto result = repair.repairDatabase(&file, key); + QCOMPARE(result.first, KeePass2Repair::RepairSuccess); + Database* dbRepaired = result.second; QVERIFY(dbRepaired); QCOMPARE(dbRepaired->rootGroup()->entries().size(), 1); QCOMPARE(dbRepaired->rootGroup()->entries().at(0)->username(), QString("testuser").append(QChar(0x20AC))); QCOMPARE(dbRepaired->rootGroup()->entries().at(0)->password(), QString("testpw")); + delete dbRepaired; } void TestKeePass2Writer::cleanupTestCase() diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp index 4f8daa068..d68c4f102 100644 --- a/tests/TestMerge.cpp +++ b/tests/TestMerge.cpp @@ -64,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); @@ -92,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"); @@ -141,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"); @@ -197,7 +197,7 @@ void TestMerge::testResolveConflictKeepBoth() Database* dbDestination = createTestDatabase(); Database* dbSource = new Database(); - dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneIncludeHistory)); + dbSource->setRootGroup(dbDestination->rootGroup()->clone(Entry::CloneIncludeHistory, Group::CloneIncludeEntries)); // sanity check QCOMPARE(dbDestination->rootGroup()->children().at(0)->entries().size(), 2); @@ -236,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); @@ -270,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); @@ -307,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); @@ -329,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"); @@ -365,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"); @@ -399,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 @@ -447,14 +528,54 @@ 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(); diff --git a/tests/TestMerge.h b/tests/TestMerge.h index 0b3ec618e..3588cfd53 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 000000000..949c708cf --- /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 000000000..f2d6d1fb9 --- /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 4f78693d6..c1e947063 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) @@ -162,7 +202,7 @@ void TestSymmetricCipher::testTwofish256CbcEncryption() bool ok; for (int i = 0; i < keys.size(); ++i) { - cipher.init(keys[i], ivs[i]); + QVERIFY(cipher.init(keys[i], ivs[i])); QByteArray ptNext = plainTexts[i]; QByteArray ctPrev = ivs[i]; QByteArray ctCur; diff --git a/tests/TestSymmetricCipher.h b/tests/TestSymmetricCipher.h index 009989500..cad13841a 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 48ff88144..06e360148 100644 --- a/tests/TestTotp.cpp +++ b/tests/TestTotp.cpp @@ -41,21 +41,21 @@ void TestTotp::testParseSecret() 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")); + 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)); } @@ -68,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 785a9f522..3bf2de93f 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 75ce3cc59..664bfd654 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 904e5f21e..5ec8237b3 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,