From eca9c658f4d0a8e956d49ce2e9eea81704e1de9b Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Mon, 1 Oct 2018 10:26:24 -0400 Subject: [PATCH 01/23] Add sharing of groups between databases * Add source folder keeshare for sharing with corresponding define WITH_XC_KEESHARE * Move common crypto parts to src/crypto/ssh * Extended OpenSSHKey * Move filewatching to own file (currently in two related classes DelayedFileWatcher and BulkFileWatcher) * Small improvements for style and code in several classes * Sharing is secured using RSA-Keys which are generated on demand * Publisher signs the container using their private key * Client can verify the signed container and choose to decline an import, import only once or trust the publisher and automatically import all data of this source henceforth * Integration of settings into Group-Settings, Database-Settings and Application-Settings * Introduced dependency QuaZip as dependency to allow combined export of key container and the (custom format) certificate --- CMakeLists.txt | 15 + Dockerfile | 2 + INSTALL.md | 5 +- README.md | 1 + ci/trusty/Dockerfile | 2 + cmake/CLangFormat.cmake | 6 +- cmake/FindQuaZip.cmake | 23 + docs/QUICKSTART.md | 62 +- src/CMakeLists.txt | 23 +- src/config-keepassx.h.cmake | 1 + src/core/Clock.cpp | 4 +- src/core/Config.cpp | 9 + src/core/Config.h | 3 + src/core/Database.cpp | 11 +- src/core/Database.h | 5 +- src/core/DatabaseIcons.cpp | 2 + src/core/DatabaseIcons.h | 2 + src/core/Entry.cpp | 14 + src/core/Entry.h | 1 + src/core/FileWatcher.cpp | 238 +++++++ src/core/FileWatcher.h | 92 +++ src/core/TimeInfo.cpp | 6 +- src/core/Tools.cpp | 27 + src/core/Tools.h | 27 + src/crypto/CryptoHash.cpp | 6 +- src/crypto/CryptoHash.h | 2 +- src/crypto/Random.cpp | 20 +- src/crypto/Random.h | 13 +- src/crypto/SymmetricCipher.cpp | 2 +- src/crypto/SymmetricCipherBackend.h | 2 +- src/crypto/SymmetricCipherGcrypt.cpp | 27 +- src/crypto/SymmetricCipherGcrypt.h | 6 +- src/{sshagent => crypto/ssh}/ASN1Key.cpp | 47 +- src/{sshagent => crypto/ssh}/ASN1Key.h | 9 +- src/{sshagent => crypto/ssh}/BinaryStream.cpp | 0 src/{sshagent => crypto/ssh}/BinaryStream.h | 6 +- src/crypto/ssh/CMakeLists.txt | 14 + src/{sshagent => crypto/ssh}/OpenSSHKey.cpp | 355 ++++++++-- src/{sshagent => crypto/ssh}/OpenSSHKey.h | 51 +- src/{sshagent => crypto/ssh}/bcrypt_pbkdf.cpp | 0 src/{sshagent => crypto/ssh}/blf.h | 0 src/{sshagent => crypto/ssh}/blowfish.c | 0 src/{sshagent => crypto/ssh}/includes.h | 0 src/format/Kdbx4Reader.cpp | 6 +- src/format/KdbxXmlWriter.cpp | 2 +- src/format/KdbxXmlWriter.h | 6 +- src/gui/AboutDialog.cpp | 3 + src/gui/ApplicationSettingsWidget.cpp | 10 +- src/gui/DatabaseTabWidget.cpp | 50 +- src/gui/DatabaseTabWidget.h | 3 +- src/gui/DatabaseWidget.cpp | 62 +- src/gui/DatabaseWidget.h | 12 +- src/gui/DetailsWidget.cpp | 18 +- src/gui/DetailsWidget.h | 4 + src/gui/DetailsWidget.ui | 284 +++++--- src/gui/EditWidgetProperties.cpp | 41 +- src/gui/EditWidgetProperties.h | 13 +- src/gui/FileDialog.cpp | 78 ++- src/gui/FileDialog.h | 11 +- src/gui/MainWindow.cpp | 17 +- src/gui/MainWindow.ui | 17 +- src/gui/dbsettings/DatabaseSettingsDialog.cpp | 47 ++ src/gui/dbsettings/DatabaseSettingsDialog.h | 17 + src/gui/entry/EditEntryWidget.cpp | 18 +- src/gui/entry/EditEntryWidget.h | 5 + src/gui/group/EditGroupWidget.cpp | 98 ++- src/gui/group/EditGroupWidget.h | 23 +- src/gui/group/GroupModel.cpp | 18 +- src/keeshare/CMakeLists.txt | 19 + src/keeshare/DatabaseSettingsPageKeeShare.cpp | 53 ++ src/keeshare/DatabaseSettingsPageKeeShare.h | 37 + .../DatabaseSettingsWidgetKeeShare.cpp | 72 ++ src/keeshare/DatabaseSettingsWidgetKeeShare.h | 51 ++ .../DatabaseSettingsWidgetKeeShare.ui | 74 ++ src/keeshare/KeeShare.cpp | 234 +++++++ src/keeshare/KeeShare.h | 79 +++ src/keeshare/KeeShareSettings.cpp | 463 +++++++++++++ src/keeshare/KeeShareSettings.h | 161 +++++ src/keeshare/SettingsPageKeeShare.cpp | 67 ++ src/keeshare/SettingsPageKeeShare.h | 43 ++ src/keeshare/SettingsWidgetKeeShare.cpp | 214 ++++++ src/keeshare/SettingsWidgetKeeShare.h | 72 ++ src/keeshare/SettingsWidgetKeeShare.ui | 249 +++++++ src/keeshare/ShareObserver.cpp | 637 ++++++++++++++++++ src/keeshare/ShareObserver.h | 112 +++ src/keeshare/Signature.cpp | 260 +++++++ src/keeshare/Signature.h | 34 + src/keeshare/group/EditGroupPageKeeShare.cpp | 55 ++ src/keeshare/group/EditGroupPageKeeShare.h | 37 + .../group/EditGroupWidgetKeeShare.cpp | 228 +++++++ src/keeshare/group/EditGroupWidgetKeeShare.h | 59 ++ src/keeshare/group/EditGroupWidgetKeeShare.ui | 139 ++++ src/sshagent/CMakeLists.txt | 7 +- src/sshagent/SSHAgent.cpp | 10 +- src/sshagent/SSHAgent.h | 8 +- tests/CMakeLists.txt | 22 +- tests/TestOpenSSHKey.cpp | 58 +- tests/TestOpenSSHKey.h | 1 + ...TestRandom.cpp => TestRandomGenerator.cpp} | 48 +- tests/{TestRandom.h => TestRandomGenerator.h} | 23 +- tests/TestSharing.cpp | 306 +++++++++ tests/TestSharing.h | 47 ++ tests/TestSignature.cpp | 198 ++++++ tests/TestSignature.h | 40 ++ tests/stub/TestRandom.cpp | 62 ++ tests/stub/TestRandom.h | 48 ++ 106 files changed, 5828 insertions(+), 503 deletions(-) create mode 100644 cmake/FindQuaZip.cmake create mode 100644 src/core/FileWatcher.cpp create mode 100644 src/core/FileWatcher.h rename src/{sshagent => crypto/ssh}/ASN1Key.cpp (81%) rename src/{sshagent => crypto/ssh}/ASN1Key.h (81%) rename src/{sshagent => crypto/ssh}/BinaryStream.cpp (100%) rename src/{sshagent => crypto/ssh}/BinaryStream.h (94%) create mode 100644 src/crypto/ssh/CMakeLists.txt rename src/{sshagent => crypto/ssh}/OpenSSHKey.cpp (57%) rename src/{sshagent => crypto/ssh}/OpenSSHKey.h (64%) rename src/{sshagent => crypto/ssh}/bcrypt_pbkdf.cpp (100%) rename src/{sshagent => crypto/ssh}/blf.h (100%) rename src/{sshagent => crypto/ssh}/blowfish.c (100%) rename src/{sshagent => crypto/ssh}/includes.h (100%) create mode 100644 src/keeshare/CMakeLists.txt create mode 100644 src/keeshare/DatabaseSettingsPageKeeShare.cpp create mode 100644 src/keeshare/DatabaseSettingsPageKeeShare.h create mode 100644 src/keeshare/DatabaseSettingsWidgetKeeShare.cpp create mode 100644 src/keeshare/DatabaseSettingsWidgetKeeShare.h create mode 100644 src/keeshare/DatabaseSettingsWidgetKeeShare.ui create mode 100644 src/keeshare/KeeShare.cpp create mode 100644 src/keeshare/KeeShare.h create mode 100644 src/keeshare/KeeShareSettings.cpp create mode 100644 src/keeshare/KeeShareSettings.h create mode 100644 src/keeshare/SettingsPageKeeShare.cpp create mode 100644 src/keeshare/SettingsPageKeeShare.h create mode 100644 src/keeshare/SettingsWidgetKeeShare.cpp create mode 100644 src/keeshare/SettingsWidgetKeeShare.h create mode 100644 src/keeshare/SettingsWidgetKeeShare.ui create mode 100644 src/keeshare/ShareObserver.cpp create mode 100644 src/keeshare/ShareObserver.h create mode 100644 src/keeshare/Signature.cpp create mode 100644 src/keeshare/Signature.h create mode 100644 src/keeshare/group/EditGroupPageKeeShare.cpp create mode 100644 src/keeshare/group/EditGroupPageKeeShare.h create mode 100644 src/keeshare/group/EditGroupWidgetKeeShare.cpp create mode 100644 src/keeshare/group/EditGroupWidgetKeeShare.h create mode 100644 src/keeshare/group/EditGroupWidgetKeeShare.ui rename tests/{TestRandom.cpp => TestRandomGenerator.cpp} (75%) rename tests/{TestRandom.h => TestRandomGenerator.h} (68%) create mode 100644 tests/TestSharing.cpp create mode 100644 tests/TestSharing.h create mode 100644 tests/TestSignature.cpp create mode 100644 tests/TestSignature.h create mode 100644 tests/stub/TestRandom.cpp create mode 100644 tests/stub/TestRandom.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0edd766ff..23fb82663 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ option(WITH_XC_NETWORKING "Include networking code (e.g. for downlading website option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF) option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) +option(WITH_XC_KEESHARE "Include sharing support with KeeShare." OFF) if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() @@ -58,11 +59,18 @@ if(WITH_XC_ALL) set(WITH_XC_BROWSER ON) set(WITH_XC_YUBIKEY ON) set(WITH_XC_SSHAGENT ON) + set(WITH_XC_KEESHARE ON) if(APPLE) set(WITH_XC_TOUCHID ON) endif() endif() +if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE) + set(WITH_XC_CRYPTO_SSH ON) +else() + set(WITH_XC_CRYPTO_SSH OFF) +endif() + set(KEEPASSXC_VERSION_MAJOR "2") set(KEEPASSXC_VERSION_MINOR "4") set(KEEPASSXC_VERSION_PATCH "0") @@ -347,6 +355,13 @@ endif() include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) +# Optional +if(WITH_XC_KEESHARE) + find_package(QuaZip REQUIRED) + + include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) +endif() + # Optional if(WITH_XC_YUBIKEY) find_package(YubiKey REQUIRED) diff --git a/Dockerfile b/Dockerfile index 89ee04464..95c3ed99f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,8 @@ RUN set -x \ zlib1g-dev \ libxi-dev \ libxtst-dev \ + libquazip5-headers \ + libquazip5-dev \ mesa-common-dev \ libyubikey-dev \ libykpers-1-dev diff --git a/INSTALL.md b/INSTALL.md index a7e1f3a5c..c13263913 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -98,8 +98,9 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct -DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF) -DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF) -DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (favicon download) (default: OFF) - -DWITH_XC_SSHAGENT=[ON|OFF] Include SSH agent support. (default: OFF) - + -DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF) + -DWITH_XC_SHARING=[ON|OFF] Enable/Disable Sharing extension (default: OFF) + -DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF) -DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF) -DWITH_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON) diff --git a/README.md b/README.md index fc63beda2..3de0f38f6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ so please check out your distribution's package list to see if KeePassXC is avai [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb), and [passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. [[See note about KeePassHTTP]](#note-about-keepasshttp) - Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk) +- Sharing of passwords using KeeShare. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details. - Many bug fixes For a full list of features and changes, read the [CHANGELOG](CHANGELOG) document. diff --git a/ci/trusty/Dockerfile b/ci/trusty/Dockerfile index 04aee25a5..55c604e75 100644 --- a/ci/trusty/Dockerfile +++ b/ci/trusty/Dockerfile @@ -52,6 +52,8 @@ RUN set -x \ zlib1g-dev \ libyubikey-dev \ libykpers-1-dev \ + libquazip5-headers \ + libquazip5-dev \ libxi-dev \ libxtst-dev \ xvfb diff --git a/cmake/CLangFormat.cmake b/cmake/CLangFormat.cmake index 8c26db93b..68d28311b 100644 --- a/cmake/CLangFormat.cmake +++ b/cmake/CLangFormat.cmake @@ -29,9 +29,9 @@ set(EXCLUDED_FILES gui/KMessageWidget.cpp gui/MainWindowAdaptor.h gui/MainWindowAdaptor.cpp - sshagent/bcrypt_pbkdf.cpp - sshagent/blf.h - sshagent/blowfish.c + crypto/ssh/bcrypt_pbkdf.cpp + crypto/ssh/blf.h + crypto/ssh/blowfish.c tests/modeltest.cpp tests/modeltest.h # objective-c files diff --git a/cmake/FindQuaZip.cmake b/cmake/FindQuaZip.cmake new file mode 100644 index 000000000..58244f4df --- /dev/null +++ b/cmake/FindQuaZip.cmake @@ -0,0 +1,23 @@ +# Copyright (C) 2018 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 . + +find_path(QUAZIP_INCLUDE_DIR quazip5/quazip.h) +find_library(QUAZIP_LIBRARIES quazip5) + +mark_as_advanced(QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +include_directories(${QUAZIP_INCLUDE_DIR}) +find_package_handle_standard_args(QuaZip DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 8b694888e..0f40958bb 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -30,12 +30,12 @@ Leave the other options at their defaults. * *In your default web browser,* install the KeePassXC Browser extension/add-on. Instructions for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) or [Chrome](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk) * 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 +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 +KeePassXC accept connection dialog ## Using Browser Integration @@ -45,4 +45,60 @@ or select it and type Ctrl+U (Cmd+U on macOS). * 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 +KeePassCX Confirm Access dialog + +## Using Sharing + +Sharing allows you to share a subset of your credentials with others and vice versa. + +### Enable Sharing + +To use sharing, you need to enable it on a database. + +1. Go to Database → Database Settings +1. Check _Allow import_ if you want to import shared credentials +1. Check _Allow export_ if you want to share credentials + +KeePassXC Databse Sharing Settings + +### Sharing Credentials + +If you checked _Allow export_ in the Sharing settings you now are good to go to share some passwords with others. Sharing always is defined on a group. If you enable sharing on a group, every entry under this group or it's children is shared. If you enable sharing on the root node, **every password** inside your database gets shared! + +1. Open the edit sheet on a group you want to share +1. Select the sharing section +1. Choose _Export to path_ as the sharing method +1. Choose a path to store the shared credentials to +1. Generate a password for this share database + +The export file will not be generated automatically. Instead, each time the database is saved, the file gets written. If an old file is present, the old file will be overwritten! The file should be written to a location that is accessible by others. An easy setup is a network share or storing the file inside the cloud. + +KeePassXC Group Sharing Settings + +### Using Shared Credentials + +Checking _Allow import_ in the Sharing settings of the database enables you to receive credentials from others. KeePass will watch sharing sources and import any changes immoderately into your database using the synchronization feature. + +1. Create a group for import +1. Open the edit sheet on that group +1. Select the sharing section +1. Choose _Import from path_ as the sharing method +1. Choose a database that is shared with you +1. Enter the password for the shared database + +KeePassXC Group Import Settings + +### Using Synchronized Credentials + +Instead of using different groups for sharing and importing you can use a single group that acts as both. This way you can synchronize a number of credentials easily across many users without a lot of hassle. + +1. Open the edit sheet on a group you want to synchronize +1. Select the sharing section +1. Choose _Synchronize with path_ as the sharing method +1. Choose a database that you want to use a synchronization file +1. Enter the password for the database + +KeePassXC Group Synchronization Settings + +## Technical Details of Sharing +Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3621067e8..b39017718 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -51,6 +51,7 @@ set(keepassx_SOURCES core/EntryAttributes.cpp core/EntrySearcher.cpp core/FilePath.cpp + core/FileWatcher.cpp core/Global.h core/Group.cpp core/InactivityTimer.cpp @@ -211,6 +212,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") +add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") if(APPLE) add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration") @@ -226,6 +228,17 @@ endif() add_subdirectory(autotype) add_subdirectory(cli) +add_subdirectory(crypto/ssh) +if(WITH_XC_CRYPTO_SSH) + set(crypto_ssh_LIB crypto_ssh) +endif() + + +add_subdirectory(keeshare) +if(WITH_XC_KEESHARE) + set(keeshare_LIB keeshare) +endif() + add_subdirectory(sshagent) if(WITH_XC_SSHAGENT) set(sshagent_LIB sshagent) @@ -269,7 +282,6 @@ set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUIL target_link_libraries(keepassx_core autotype ${keepassxcbrowser_LIB} - ${sshagent_LIB} Qt5::Core Qt5::Network Qt5::Concurrent @@ -280,7 +292,14 @@ target_link_libraries(keepassx_core ${ARGON2_LIBRARIES} ${GCRYPT_LIBRARIES} ${GPGERROR_LIBRARIES} - ${ZLIB_LIBRARIES}) + ${ZLIB_LIBRARIES}) + +if(WITH_XC_SSHAGENT) + target_link_libraries(keepassx_core sshagent) +endif() +if(WITH_XC_KEESHARE) + target_link_libraries(keepassx_core keeshare) +endif() if(APPLE) target_link_libraries(keepassx_core "-framework Foundation") diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index d1f0723b4..b863af91c 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -17,6 +17,7 @@ #cmakedefine WITH_XC_BROWSER #cmakedefine WITH_XC_YUBIKEY #cmakedefine WITH_XC_SSHAGENT +#cmakedefine WITH_XC_KEESHARE #cmakedefine WITH_XC_TOUCHID #cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@" diff --git a/src/core/Clock.cpp b/src/core/Clock.cpp index 02c2ae1bc..88ac4fb77 100644 --- a/src/core/Clock.cpp +++ b/src/core/Clock.cpp @@ -16,7 +16,7 @@ */ #include "Clock.h" -QSharedPointer Clock::m_instance = QSharedPointer(); +QSharedPointer Clock::m_instance; QDateTime Clock::currentDateTimeUtc() { @@ -92,7 +92,7 @@ QDateTime Clock::currentDateTimeImpl() const void Clock::resetInstance() { - m_instance.clear(); + m_instance.reset(); } void Clock::setInstance(Clock* clock) diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 8f1dc55fd..8fd2faad9 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -48,7 +48,16 @@ QString Config::getFileName() void Config::set(const QString& key, const QVariant& value) { + if (m_settings->contains(key) && m_settings->value(key) == value) { + return; + } + const bool surpressSignal = !m_settings->contains(key) && m_defaults.value(key) == value; + m_settings->setValue(key, value); + + if (!surpressSignal) { + emit changed(key); + } } /** diff --git a/src/core/Config.h b/src/core/Config.h index fcb27e2ca..347350754 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -43,6 +43,9 @@ public: static void createConfigFromFile(const QString& file); static void createTempFileInstance(); +signals: + void changed(const QString& key); + private: Config(const QString& fileName, QObject* parent); explicit Config(QObject* parent); diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 5b7a3c07d..607ecc93f 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -111,7 +111,7 @@ void Database::setFilePath(const QString& filePath) m_filePath = filePath; } -Entry* Database::resolveEntry(const QUuid& uuid) +Entry* Database::resolveEntry(const QUuid& uuid) const { return findEntryRecursive(uuid, m_rootGroup); } @@ -121,7 +121,7 @@ Entry* Database::resolveEntry(const QString& text, EntryReferenceType referenceT return findEntryRecursive(text, referenceType, m_rootGroup); } -Entry* Database::findEntryRecursive(const QUuid& uuid, Group* group) +Entry* Database::findEntryRecursive(const QUuid& uuid, Group* group) const { const QList entryList = group->entries(); for (Entry* entry : entryList) { @@ -289,8 +289,11 @@ QByteArray Database::challengeResponseKey() const bool Database::challengeMasterSeed(const QByteArray& masterSeed) { - m_data.masterSeed = masterSeed; - return m_data.key->challenge(masterSeed, m_data.challengeResponseKey); + if (m_data.key) { + m_data.masterSeed = masterSeed; + return m_data.key->challenge(masterSeed, m_data.challengeResponseKey); + } + return true; } void Database::setCipher(const QUuid& cipher) diff --git a/src/core/Database.h b/src/core/Database.h index a5ae3effa..9253cb9ea 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -23,6 +23,7 @@ #include #include +#include "config-keepassx.h" #include "crypto/kdf/Kdf.h" #include "keys/CompositeKey.h" @@ -88,7 +89,7 @@ public: const Metadata* metadata() const; QString filePath() const; void setFilePath(const QString& filePath); - Entry* resolveEntry(const QUuid& uuid); + Entry* resolveEntry(const QUuid& uuid) const; Entry* resolveEntry(const QString& text, EntryReferenceType referenceType); Group* resolveGroup(const QUuid& uuid); QList deletedObjects(); @@ -149,7 +150,7 @@ private slots: void startModifiedTimer(); private: - Entry* findEntryRecursive(const QUuid& uuid, Group* group); + Entry* findEntryRecursive(const QUuid& uuid, Group* group) const; Entry* findEntryRecursive(const QString& text, EntryReferenceType referenceType, Group* group); Group* findGroupRecursive(const QUuid& uuid, Group* group); diff --git a/src/core/DatabaseIcons.cpp b/src/core/DatabaseIcons.cpp index ddb4e9106..6219d41f5 100644 --- a/src/core/DatabaseIcons.cpp +++ b/src/core/DatabaseIcons.cpp @@ -22,6 +22,8 @@ DatabaseIcons* DatabaseIcons::m_instance(nullptr); const int DatabaseIcons::IconCount(69); const int DatabaseIcons::ExpiredIconIndex(45); +const int DatabaseIcons::SharedIconIndex(1); +const int DatabaseIcons::UnsharedIconIndex(45); // clang-format off const char* const DatabaseIcons::m_indexToName[] = { diff --git a/src/core/DatabaseIcons.h b/src/core/DatabaseIcons.h index 43a6df216..ecd38fd8a 100644 --- a/src/core/DatabaseIcons.h +++ b/src/core/DatabaseIcons.h @@ -33,6 +33,8 @@ public: static const int IconCount; static const int ExpiredIconIndex; + static const int SharedIconIndex; + static const int UnsharedIconIndex; private: DatabaseIcons(); diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 929447f9c..f3391189c 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -955,6 +955,20 @@ QString Entry::maskPasswordPlaceholders(const QString& str) const return result; } +Entry* Entry::resolveReference(const QString& str) const +{ + QRegularExpressionMatch match = EntryAttributes::matchReference(str); + if (!match.hasMatch()) { + return nullptr; + } + + const QString searchIn = match.captured(EntryAttributes::SearchInGroupName); + const QString searchText = match.captured(EntryAttributes::SearchTextGroupName); + + const EntryReferenceType searchInType = Entry::referenceType(searchIn); + return m_group->database()->resolveEntry(searchText, searchInType); +} + QString Entry::resolveMultiplePlaceholders(const QString& str) const { return resolveMultiplePlaceholdersRecursive(str, ResolveMaximumDepth); diff --git a/src/core/Entry.h b/src/core/Entry.h index 05ed30bc0..94649444b 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -195,6 +195,7 @@ public: Entry* clone(CloneFlags flags) const; void copyDataFrom(const Entry* other); QString maskPasswordPlaceholders(const QString& str) const; + Entry* resolveReference(const QString& str) const; QString resolveMultiplePlaceholders(const QString& str) const; QString resolvePlaceholder(const QString& str) const; QString resolveUrlPlaceholder(const QString& str, PlaceholderType placeholderType) const; diff --git a/src/core/FileWatcher.cpp b/src/core/FileWatcher.cpp new file mode 100644 index 000000000..ac44174bd --- /dev/null +++ b/src/core/FileWatcher.cpp @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2011 Felix Geyer + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FileWatcher.h" + +#include "core/Clock.h" +#include + +#ifdef Q_OS_LINUX +#include +#endif + +namespace +{ + const int FileChangeDelay = 500; + const int TimerResolution = 100; +} + +DelayingFileWatcher::DelayingFileWatcher(QObject* parent) + : QObject(parent) + , m_ignoreFileChange(false) +{ + connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); + connect(&m_fileUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges())); + connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), SIGNAL(fileChanged())); + + m_fileChangeDelayTimer.setSingleShot(true); + m_fileUnblockTimer.setSingleShot(true); +} + +void DelayingFileWatcher::restart() +{ + m_fileWatcher.addPath(m_filePath); +} + +void DelayingFileWatcher::stop() +{ + m_fileWatcher.removePath(m_filePath); +} + +void DelayingFileWatcher::start(const QString& filePath) +{ + if (!m_filePath.isEmpty()) { + m_fileWatcher.removePath(m_filePath); + } + +#if defined(Q_OS_LINUX) + struct statfs statfsBuf; + bool forcePolling = false; + const auto NFS_SUPER_MAGIC = 0x6969; + + if (!statfs(filePath.toLocal8Bit().constData(), &statfsBuf)) { + forcePolling = (statfsBuf.f_type == NFS_SUPER_MAGIC); + } else { + // if we can't get the fs type let's fall back to polling + forcePolling = true; + } + auto objectName = forcePolling ? QLatin1String("_qt_autotest_force_engine_poller") : QLatin1String(""); + m_fileWatcher.setObjectName(objectName); +#endif + + m_fileWatcher.addPath(filePath); + + if (!filePath.isEmpty()) { + m_filePath = filePath; + } +} + +void DelayingFileWatcher::ignoreFileChanges() +{ + m_ignoreFileChange = true; + m_fileChangeDelayTimer.stop(); +} + +void DelayingFileWatcher::observeFileChanges(bool delayed) +{ + int timeout = 0; + if (delayed) { + timeout = FileChangeDelay; + } else { + m_ignoreFileChange = false; + start(m_filePath); + } + if (timeout > 0 && !m_fileUnblockTimer.isActive()) { + m_fileUnblockTimer.start(timeout); + } +} + +void DelayingFileWatcher::onWatchedFileChanged() +{ + if (m_ignoreFileChange) { + // the client forcefully silenced us + return; + } + if (m_fileChangeDelayTimer.isActive()) { + // we are waiting to fire the delayed fileChanged event, so nothing + // to do here + return; + } + + m_fileChangeDelayTimer.start(FileChangeDelay); +} + +BulkFileWatcher::BulkFileWatcher(QObject* parent) + : QObject(parent) +{ + connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileChanged(QString))); + connect(&m_fileWatcher, SIGNAL(directoryChanged(QString)), SLOT(handleDirectoryChanged(QString))); + connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges())); + m_fileWatchUnblockTimer.setSingleShot(true); +} + +void BulkFileWatcher::clear() +{ + for (const QString& path : m_fileWatcher.files() + m_fileWatcher.directories()) { + const QFileInfo info(path); + m_fileWatcher.removePath(info.absoluteFilePath()); + m_fileWatcher.removePath(info.absolutePath()); + } + m_filePaths.clear(); + m_watchedFilesInDirectory.clear(); + m_ignoreFilesChangess.clear(); +} + +void BulkFileWatcher::removePath(const QString& path) +{ + const QFileInfo info(path); + m_fileWatcher.removePath(info.absoluteFilePath()); + m_fileWatcher.removePath(info.absolutePath()); + m_filePaths.remove(info.absoluteFilePath()); + m_filePaths.remove(info.absolutePath()); + m_watchedFilesInDirectory[info.absolutePath()].remove(info.absoluteFilePath()); +} + +void BulkFileWatcher::addPath(const QString& path) +{ + const QFileInfo info(path); + m_fileWatcher.addPath(info.absoluteFilePath()); + m_fileWatcher.addPath(info.absolutePath()); + m_filePaths.insert(info.absoluteFilePath()); + m_filePaths.insert(info.absolutePath()); + m_watchedFilesInDirectory[info.absolutePath()][info.absoluteFilePath()] = info.exists(); +} + +void BulkFileWatcher::restart(const QString& path) +{ + const QFileInfo info(path); + Q_ASSERT(m_filePaths.contains(info.absoluteFilePath())); + Q_ASSERT(m_filePaths.contains(info.absolutePath())); + m_fileWatcher.addPath(info.absoluteFilePath()); + m_fileWatcher.addPath(info.absolutePath()); +} + +void BulkFileWatcher::handleFileChanged(const QString& path) +{ + addPath(path); + + const QFileInfo info(path); + if (m_ignoreFilesChangess[info.canonicalFilePath()] > Clock::currentDateTimeUtc()) { + // changes are blocked + return; + } + + emit fileChanged(path); +} + +void BulkFileWatcher::handleDirectoryChanged(const QString& path) +{ + qDebug("Directory changed %s", qPrintable(path)); + const QFileInfo directory(path); + const QMap& watchedFiles = m_watchedFilesInDirectory[directory.absolutePath()]; + for (const QString& file : watchedFiles.keys()) { + const QFileInfo info(file); + const bool existed = watchedFiles[info.absoluteFilePath()]; + if (!info.exists() && existed) { + qDebug("Remove watch file %s", qPrintable(info.absoluteFilePath())); + m_fileWatcher.removePath(info.absolutePath()); + emit fileRemoved(info.absoluteFilePath()); + } + if (!existed && info.exists()) { + qDebug("Add watch file %s", qPrintable(info.absoluteFilePath())); + m_fileWatcher.addPath(info.absolutePath()); + emit fileCreated(info.absoluteFilePath()); + } + if (existed && info.exists()) { + qDebug("Refresh watch file %s", qPrintable(info.absoluteFilePath())); + m_fileWatcher.removePath(info.absolutePath()); + m_fileWatcher.addPath(info.absolutePath()); + emit fileChanged(info.absoluteFilePath()); + } + m_watchedFilesInDirectory[info.absolutePath()][info.absoluteFilePath()] = info.exists(); + } +} + +void BulkFileWatcher::ignoreFileChanges(const QString& path) +{ + const QFileInfo info(path); + m_ignoreFilesChangess[info.canonicalFilePath()] = Clock::currentDateTimeUtc().addMSecs(FileChangeDelay); +} + +void BulkFileWatcher::observeFileChanges(bool delayed) +{ + int timeout = 0; + if (delayed) { + timeout = TimerResolution; + } else { + const QDateTime current = Clock::currentDateTimeUtc(); + for (const QString& key : m_ignoreFilesChangess.keys()) { + if (m_ignoreFilesChangess[key] < current) { + // We assume that there was no concurrent change of the database + // during our block - so no need to reimport + qDebug("Remove block from %s", qPrintable(key)); + m_ignoreFilesChangess.remove(key); + continue; + } + qDebug("Keep block from %s", qPrintable(key)); + timeout = static_cast(current.msecsTo(m_ignoreFilesChangess[key])); + } + } + if (timeout > 0 && !m_fileWatchUnblockTimer.isActive()) { + m_fileWatchUnblockTimer.start(timeout); + } +} diff --git a/src/core/FileWatcher.h b/src/core/FileWatcher.h new file mode 100644 index 000000000..de7dbb1c2 --- /dev/null +++ b/src/core/FileWatcher.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 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_FILEWATCHER_H +#define KEEPASSXC_FILEWATCHER_H + +#include +#include +#include +#include + +class DelayingFileWatcher : public QObject +{ + Q_OBJECT + +public: + explicit DelayingFileWatcher(QObject* parent = nullptr); + + void blockAutoReload(bool block); + void start(const QString& path); + + void restart(); + void stop(); + void ignoreFileChanges(); + +signals: + void fileChanged(); + +public slots: + void observeFileChanges(bool delayed = false); + +private slots: + void onWatchedFileChanged(); + +private: + QString m_filePath; + QFileSystemWatcher m_fileWatcher; + QTimer m_fileChangeDelayTimer; + QTimer m_fileUnblockTimer; + bool m_ignoreFileChange; +}; + +class BulkFileWatcher : public QObject +{ + Q_OBJECT +public: + explicit BulkFileWatcher(QObject* parent = nullptr); + + void clear(); + + void removePath(const QString& path); + void addPath(const QString& path); + + void restart(const QString& path); + + void ignoreFileChanges(const QString& path); + +signals: + void fileCreated(QString); + void fileChanged(QString); + void fileRemoved(QString); + +public slots: + void observeFileChanges(bool delayed = false); + +private slots: + void handleFileChanged(const QString& path); + void handleDirectoryChanged(const QString& path); + +private: + QSet m_filePaths; + QMap m_ignoreFilesChangess; + QFileSystemWatcher m_fileWatcher; + QMap> m_watchedFilesInDirectory; + QTimer m_fileWatchUnblockTimer; // needed for Import/Export-References +}; + +#endif // KEEPASSXC_FILEWATCHER_H diff --git a/src/core/TimeInfo.cpp b/src/core/TimeInfo.cpp index c774a7c81..b48ad42ea 100644 --- a/src/core/TimeInfo.cpp +++ b/src/core/TimeInfo.cpp @@ -124,8 +124,7 @@ bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const if (::compare(m_creationTime, other.m_creationTime, options) != 0) { return false; } - if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options) - != 0) { + if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_lastAccessTime, other.m_lastAccessTime, options) != 0) { return false; } if (::compare(m_expires, m_expiryTime, other.m_expires, other.expiryTime(), options) != 0) { @@ -134,8 +133,7 @@ bool TimeInfo::equals(const TimeInfo& other, CompareItemOptions options) const if (::compare(!options.testFlag(CompareItemIgnoreStatistics), m_usageCount, other.m_usageCount, options) != 0) { return false; } - if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options) - != 0) { + if (::compare(!options.testFlag(CompareItemIgnoreLocation), m_locationChanged, other.m_locationChanged, options) != 0) { return false; } return true; diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 458d42988..2dc75873b 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -346,4 +346,31 @@ namespace Tools return bSuccess; } + + Buffer::Buffer() + : raw(nullptr) + , size(0) + { + + } + + Buffer::~Buffer() + { + clear(); + } + + void Buffer::clear() + { + if(size > 0){ + free(raw); + } + raw = nullptr; size = 0; + } + + QByteArray Buffer::content() const + { + return QByteArray(reinterpret_cast(raw), size ); + } + + } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 4f75b750b..c1814f756 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -56,6 +56,33 @@ namespace Tools } } + template struct Map + { + QMap values; + Value& operator[](const Key index) + { + return values[index]; + } + + ~Map() + { + for (Value m : values) { + deleter(m); + } + } + }; + + struct Buffer + { + unsigned char* raw; + size_t size; + + Buffer(); + ~Buffer(); + + void clear(); + QByteArray content() const; + }; } // namespace Tools #endif // KEEPASSX_TOOLS_H diff --git a/src/crypto/CryptoHash.cpp b/src/crypto/CryptoHash.cpp index 12c6bf791..986326af5 100644 --- a/src/crypto/CryptoHash.cpp +++ b/src/crypto/CryptoHash.cpp @@ -58,8 +58,7 @@ CryptoHash::CryptoHash(Algorithm algo, bool hmac) gcry_error_t error = gcry_md_open(&d->ctx, algoGcrypt, flagsGcrypt); if (error != GPG_ERR_NO_ERROR) { - qWarning("Gcrypt error (ctor): %s", gcry_strerror(error)); - qWarning("Gcrypt error (ctor): %s", gcry_strsource(error)); + qWarning("Gcrypt error (ctor): %s\n %s", gcry_strerror(error), gcry_strsource(error)); } Q_ASSERT(error == 0); // TODO: error handling @@ -92,8 +91,7 @@ void CryptoHash::setKey(const QByteArray& data) gcry_error_t error = gcry_md_setkey(d->ctx, data.constData(), static_cast(data.size())); if (error) { - qWarning("Gcrypt error (setKey): %s", gcry_strerror(error)); - qWarning("Gcrypt error (setKey): %s", gcry_strsource(error)); + qWarning("Gcrypt error (setKey): %s\n %s", gcry_strerror(error), gcry_strsource(error)); } Q_ASSERT(error == 0); } diff --git a/src/crypto/CryptoHash.h b/src/crypto/CryptoHash.h index bd312121a..0d806af42 100644 --- a/src/crypto/CryptoHash.h +++ b/src/crypto/CryptoHash.h @@ -35,8 +35,8 @@ public: ~CryptoHash(); void addData(const QByteArray& data); void reset(); - QByteArray result() const; void setKey(const QByteArray& data); + QByteArray result() const; static QByteArray hash(const QByteArray& data, Algorithm algo); static QByteArray hmac(const QByteArray& data, const QByteArray& key, Algorithm algo); diff --git a/src/crypto/Random.cpp b/src/crypto/Random.cpp index 69c786306..4203b6c0c 100644 --- a/src/crypto/Random.cpp +++ b/src/crypto/Random.cpp @@ -28,7 +28,7 @@ public: void randomize(void* data, int len) override; }; -Random* Random::m_instance(nullptr); +QSharedPointer Random::m_instance; void Random::randomize(QByteArray& ba) { @@ -70,18 +70,20 @@ quint32 Random::randomUIntRange(quint32 min, quint32 max) Random* Random::instance() { if (!m_instance) { - m_instance = new Random(new RandomBackendGcrypt()); + m_instance.reset(new Random(new RandomBackendGcrypt())); } - return m_instance; + return m_instance.data(); } -void Random::createWithBackend(RandomBackend* backend) +void Random::resetInstance() { - Q_ASSERT(backend); - Q_ASSERT(!m_instance); + m_instance.reset(); +} - m_instance = new Random(backend); +void Random::setInstance(RandomBackend* backend) +{ + m_instance.reset(new Random(backend)); } Random::Random(RandomBackend* backend) @@ -95,3 +97,7 @@ void RandomBackendGcrypt::randomize(void* data, int len) gcry_randomize(data, len, GCRY_STRONG_RANDOM); } + +RandomBackend::~RandomBackend() +{ +} diff --git a/src/crypto/Random.h b/src/crypto/Random.h index 1a36c4107..bdf7b9aca 100644 --- a/src/crypto/Random.h +++ b/src/crypto/Random.h @@ -20,14 +20,13 @@ #include #include +#include class RandomBackend { public: virtual void randomize(void* data, int len) = 0; - virtual ~RandomBackend() - { - } + virtual ~RandomBackend(); }; class Random @@ -47,13 +46,17 @@ public: quint32 randomUIntRange(quint32 min, quint32 max); static Random* instance(); - static void createWithBackend(RandomBackend* backend); + +protected: + static void resetInstance(); + static void setInstance(RandomBackend* backend); private: + static QSharedPointer m_instance; + explicit Random(RandomBackend* backend); QScopedPointer m_backend; - static Random* m_instance; Q_DISABLE_COPY(Random) }; diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index 0467ad7c2..108eebd84 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -89,7 +89,7 @@ int SymmetricCipher::blockSize() const QString SymmetricCipher::errorString() const { - return m_backend->errorString(); + return m_backend->error(); } SymmetricCipher::Algorithm SymmetricCipher::cipherToAlgorithm(const QUuid& cipher) diff --git a/src/crypto/SymmetricCipherBackend.h b/src/crypto/SymmetricCipherBackend.h index 27a39177e..649e68313 100644 --- a/src/crypto/SymmetricCipherBackend.h +++ b/src/crypto/SymmetricCipherBackend.h @@ -38,7 +38,7 @@ public: virtual int keySize() const = 0; virtual int blockSize() const = 0; - virtual QString errorString() const = 0; + virtual QString error() const = 0; }; #endif // KEEPASSX_SYMMETRICCIPHERBACKEND_H diff --git a/src/crypto/SymmetricCipherGcrypt.cpp b/src/crypto/SymmetricCipherGcrypt.cpp index c7a5e6a07..0b533a882 100644 --- a/src/crypto/SymmetricCipherGcrypt.cpp +++ b/src/crypto/SymmetricCipherGcrypt.cpp @@ -80,13 +80,12 @@ int SymmetricCipherGcrypt::gcryptMode(SymmetricCipher::Mode mode) } } -void SymmetricCipherGcrypt::setErrorString(gcry_error_t err) +void SymmetricCipherGcrypt::setError(const gcry_error_t& err) { const char* gcryptError = gcry_strerror(err); const char* gcryptErrorSource = gcry_strsource(err); - m_errorString = - QString("%1/%2").arg(QString::fromLocal8Bit(gcryptErrorSource), QString::fromLocal8Bit(gcryptError)); + m_error = QString("%1/%2").arg(QString::fromLocal8Bit(gcryptErrorSource), QString::fromLocal8Bit(gcryptError)); } bool SymmetricCipherGcrypt::init() @@ -99,7 +98,7 @@ bool SymmetricCipherGcrypt::init() gcry_cipher_close(m_ctx); error = gcry_cipher_open(&m_ctx, m_algo, m_mode, 0); if (error != 0) { - setErrorString(error); + setError(error); return false; } @@ -112,7 +111,7 @@ bool SymmetricCipherGcrypt::setKey(const QByteArray& key) gcry_error_t error = gcry_cipher_setkey(m_ctx, m_key.constData(), m_key.size()); if (error != 0) { - setErrorString(error); + setError(error); return false; } @@ -131,7 +130,7 @@ bool SymmetricCipherGcrypt::setIv(const QByteArray& iv) } if (error != 0) { - setErrorString(error); + setError(error); return false; } @@ -154,7 +153,7 @@ QByteArray SymmetricCipherGcrypt::process(const QByteArray& data, bool* ok) } if (error != 0) { - setErrorString(error); + setError(error); *ok = false; } else { *ok = true; @@ -176,7 +175,7 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data) } if (error != 0) { - setErrorString(error); + setError(error); return false; } @@ -197,7 +196,7 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data, quint64 rounds) error = gcry_cipher_decrypt(m_ctx, rawData, size, nullptr, 0); if (error != 0) { - setErrorString(error); + setError(error); return false; } } @@ -206,7 +205,7 @@ bool SymmetricCipherGcrypt::processInPlace(QByteArray& data, quint64 rounds) error = gcry_cipher_encrypt(m_ctx, rawData, size, nullptr, 0); if (error != 0) { - setErrorString(error); + setError(error); return false; } } @@ -221,13 +220,13 @@ bool SymmetricCipherGcrypt::reset() error = gcry_cipher_reset(m_ctx); if (error != 0) { - setErrorString(error); + setError(error); return false; } error = gcry_cipher_setiv(m_ctx, m_iv.constData(), m_iv.size()); if (error != 0) { - setErrorString(error); + setError(error); return false; } @@ -258,7 +257,7 @@ int SymmetricCipherGcrypt::blockSize() const return blockSizeT; } -QString SymmetricCipherGcrypt::errorString() const +QString SymmetricCipherGcrypt::error() const { - return m_errorString; + return m_error; } diff --git a/src/crypto/SymmetricCipherGcrypt.h b/src/crypto/SymmetricCipherGcrypt.h index 6f806b90b..0c5c00099 100644 --- a/src/crypto/SymmetricCipherGcrypt.h +++ b/src/crypto/SymmetricCipherGcrypt.h @@ -43,12 +43,12 @@ public: int keySize() const; int blockSize() const; - QString errorString() const; + QString error() const; private: static int gcryptAlgo(SymmetricCipher::Algorithm algo); static int gcryptMode(SymmetricCipher::Mode mode); - void setErrorString(gcry_error_t err); + void setError(const gcry_error_t& err); gcry_cipher_hd_t m_ctx; const int m_algo; @@ -56,7 +56,7 @@ private: const SymmetricCipher::Direction m_direction; QByteArray m_key; QByteArray m_iv; - QString m_errorString; + QString m_error; }; #endif // KEEPASSX_SYMMETRICCIPHERGCRYPT_H diff --git a/src/sshagent/ASN1Key.cpp b/src/crypto/ssh/ASN1Key.cpp similarity index 81% rename from src/sshagent/ASN1Key.cpp rename to src/crypto/ssh/ASN1Key.cpp index dc6da2adc..5a83bcee9 100644 --- a/src/sshagent/ASN1Key.cpp +++ b/src/crypto/ssh/ASN1Key.cpp @@ -17,6 +17,8 @@ */ #include "ASN1Key.h" +#include "crypto/ssh/BinaryStream.h" + #include namespace @@ -53,7 +55,17 @@ namespace return true; } - bool parseHeader(BinaryStream& stream, quint8 wantedType) + bool parsePublicHeader(BinaryStream& stream) + { + quint8 tag; + quint32 len; + + nextTag(stream, tag, len); + + return (tag == TAG_SEQUENCE); + } + + bool parsePrivateHeader(BinaryStream& stream, quint8 wantedType) { quint8 tag; quint32 len; @@ -118,7 +130,7 @@ bool ASN1Key::parseDSA(QByteArray& ba, OpenSSHKey& key) { BinaryStream stream(&ba); - if (!parseHeader(stream, KEY_ZERO)) { + if (!parsePrivateHeader(stream, KEY_ZERO)) { return false; } @@ -149,11 +161,38 @@ bool ASN1Key::parseDSA(QByteArray& ba, OpenSSHKey& key) return true; } -bool ASN1Key::parseRSA(QByteArray& ba, OpenSSHKey& key) +bool ASN1Key::parsePublicRSA(QByteArray& ba, OpenSSHKey& key) { BinaryStream stream(&ba); - if (!parseHeader(stream, KEY_ZERO)) { + if (!parsePublicHeader(stream)) { + return false; + } + + QByteArray n, e; + readInt(stream, n); + readInt(stream, e); + + QList publicData; + publicData.append(e); + publicData.append(n); + + QList privateData; + privateData.append(n); + privateData.append(e); + + key.setType("ssh-rsa"); + key.setPublicData(publicData); + key.setPrivateData(privateData); + key.setComment(""); + return true; +} + +bool ASN1Key::parsePrivateRSA(QByteArray& ba, OpenSSHKey& key) +{ + BinaryStream stream(&ba); + + if (!parsePrivateHeader(stream, KEY_ZERO)) { return false; } diff --git a/src/sshagent/ASN1Key.h b/src/crypto/ssh/ASN1Key.h similarity index 81% rename from src/sshagent/ASN1Key.h rename to src/crypto/ssh/ASN1Key.h index 59f8d4e81..54a2bde4e 100644 --- a/src/sshagent/ASN1Key.h +++ b/src/crypto/ssh/ASN1Key.h @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -#ifndef ASN1KEY_H -#define ASN1KEY_H +#ifndef KEEPASSXC_ASN1KEY_H +#define KEEPASSXC_ASN1KEY_H #include "OpenSSHKey.h" #include @@ -25,7 +25,8 @@ namespace ASN1Key { bool parseDSA(QByteArray& ba, OpenSSHKey& key); - bool parseRSA(QByteArray& ba, OpenSSHKey& key); + bool parsePrivateRSA(QByteArray& ba, OpenSSHKey& key); + bool parsePublicRSA(QByteArray& ba, OpenSSHKey& key); } -#endif // ASN1KEY_H +#endif // KEEPASSXC_ASN1KEY_H diff --git a/src/sshagent/BinaryStream.cpp b/src/crypto/ssh/BinaryStream.cpp similarity index 100% rename from src/sshagent/BinaryStream.cpp rename to src/crypto/ssh/BinaryStream.cpp diff --git a/src/sshagent/BinaryStream.h b/src/crypto/ssh/BinaryStream.h similarity index 94% rename from src/sshagent/BinaryStream.h rename to src/crypto/ssh/BinaryStream.h index fa9ded81a..8f4155b65 100644 --- a/src/sshagent/BinaryStream.h +++ b/src/crypto/ssh/BinaryStream.h @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -#ifndef BINARYSTREAM_H -#define BINARYSTREAM_H +#ifndef KEEPASSXC_BINARYSTREAM_H +#define KEEPASSXC_BINARYSTREAM_H #include #include @@ -65,4 +65,4 @@ private: QScopedPointer m_buffer; }; -#endif // BINARYSTREAM_H +#endif // KEEPASSXC_BINARYSTREAM_H diff --git a/src/crypto/ssh/CMakeLists.txt b/src/crypto/ssh/CMakeLists.txt new file mode 100644 index 000000000..709dd2f95 --- /dev/null +++ b/src/crypto/ssh/CMakeLists.txt @@ -0,0 +1,14 @@ +if(WITH_XC_CRYPTO_SSH) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + + set(crypto_ssh_SOURCES + bcrypt_pbkdf.cpp + blowfish.c + ASN1Key.cpp + BinaryStream.cpp + OpenSSHKey.cpp + ) + + add_library(crypto_ssh STATIC ${crypto_ssh_SOURCES}) + target_link_libraries(crypto_ssh Qt5::Core ${GCRYPT_LIBRARIES}) +endif() diff --git a/src/sshagent/OpenSSHKey.cpp b/src/crypto/ssh/OpenSSHKey.cpp similarity index 57% rename from src/sshagent/OpenSSHKey.cpp rename to src/crypto/ssh/OpenSSHKey.cpp index 44684d620..91f641401 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/crypto/ssh/OpenSSHKey.cpp @@ -17,29 +17,202 @@ */ #include "OpenSSHKey.h" -#include "ASN1Key.h" + +#include "core/Tools.h" #include "crypto/SymmetricCipher.h" +#include "crypto/ssh/ASN1Key.h" +#include "crypto/ssh/BinaryStream.h" + #include #include #include -const QString OpenSSHKey::TYPE_DSA = "DSA PRIVATE KEY"; -const QString OpenSSHKey::TYPE_RSA = "RSA PRIVATE KEY"; -const QString OpenSSHKey::TYPE_OPENSSH = "OPENSSH PRIVATE KEY"; +#include + +const QString OpenSSHKey::TYPE_DSA_PRIVATE = "DSA PRIVATE KEY"; +const QString OpenSSHKey::TYPE_RSA_PRIVATE = "RSA PRIVATE KEY"; +const QString OpenSSHKey::TYPE_RSA_PUBLIC = "RSA PUBLIC KEY"; +const QString OpenSSHKey::TYPE_OPENSSH_PRIVATE = "OPENSSH PRIVATE KEY"; + +namespace +{ + QPair> binaryDeserialize(const QByteArray& serialized) + { + if (serialized.isEmpty()) { + return {}; + } + QBuffer buffer; + buffer.setData(serialized); + buffer.open(QBuffer::ReadOnly); + BinaryStream stream(&buffer); + QString type; + stream.readString(type); + QByteArray temp; + QList data; + while (stream.readString(temp)) { + data << temp; + } + return ::qMakePair(type, data); + } + + QByteArray binarySerialize(const QString& type, const QList& data) + { + if (type.isEmpty() && data.isEmpty()) { + return {}; + } + QByteArray buffer; + BinaryStream stream(&buffer); + stream.writeString(type); + for (const QByteArray& part : data) { + stream.writeString(part); + } + return buffer; + } +} // bcrypt_pbkdf.cpp int bcrypt_pbkdf(const QByteArray& pass, const QByteArray& salt, QByteArray& key, quint32 rounds); +OpenSSHKey OpenSSHKey::generate(bool secure) +{ + enum Index + { + Params, + CombinedKey, + PrivateKey, + PublicKey, + + Private_N, + Private_E, + Private_D, + Private_P, + Private_Q, + Private_U, // private key + Public_N, + Public_E, + }; + + Tools::Map mpi; + Tools::Map sexp; + gcry_error_t rc = GPG_ERR_NO_ERROR; + rc = gcry_sexp_build(&sexp[Params], NULL, secure ? "(genkey (rsa (nbits 4:2048)))" : "(genkey (rsa (transient-key) (nbits 4:2048)))"); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not create ssh key" << gcry_err_code(rc); + return OpenSSHKey(); + } + + rc = gcry_pk_genkey(&sexp[CombinedKey], sexp[Params]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not create ssh key" << gcry_err_code(rc); + return OpenSSHKey(); + } + + sexp[PrivateKey] = gcry_sexp_find_token(sexp[CombinedKey], "private-key", 0); + sexp[PublicKey] = gcry_sexp_find_token(sexp[CombinedKey], "public-key", 0); + + sexp[Private_N] = gcry_sexp_find_token(sexp[PrivateKey], "n", 1); + mpi[Private_N] = gcry_sexp_nth_mpi(sexp[Private_N], 1, GCRYMPI_FMT_USG); + sexp[Private_E] = gcry_sexp_find_token(sexp[PrivateKey], "e", 1); + mpi[Private_E] = gcry_sexp_nth_mpi(sexp[Private_E], 1, GCRYMPI_FMT_USG); + sexp[Private_D] = gcry_sexp_find_token(sexp[PrivateKey], "d", 1); + mpi[Private_D] = gcry_sexp_nth_mpi(sexp[Private_D], 1, GCRYMPI_FMT_USG); + sexp[Private_Q] = gcry_sexp_find_token(sexp[PrivateKey], "q", 1); + mpi[Private_Q] = gcry_sexp_nth_mpi(sexp[Private_Q], 1, GCRYMPI_FMT_USG); + sexp[Private_P] = gcry_sexp_find_token(sexp[PrivateKey], "p", 1); + mpi[Private_P] = gcry_sexp_nth_mpi(sexp[Private_P], 1, GCRYMPI_FMT_USG); + sexp[Private_U] = gcry_sexp_find_token(sexp[PrivateKey], "u", 1); + mpi[Private_U] = gcry_sexp_nth_mpi(sexp[Private_U], 1, GCRYMPI_FMT_USG); + + sexp[Public_N] = gcry_sexp_find_token(sexp[PublicKey], "n", 1); + mpi[Public_N] = gcry_sexp_nth_mpi(sexp[Public_N], 1, GCRYMPI_FMT_USG); + sexp[Public_E] = gcry_sexp_find_token(sexp[PublicKey], "e", 1); + mpi[Public_E] = gcry_sexp_nth_mpi(sexp[Public_E], 1, GCRYMPI_FMT_USG); + + QList publicParts; + QList privateParts; + Tools::Buffer buffer; + gcry_mpi_format format = GCRYMPI_FMT_USG; + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_N]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract private key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + privateParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_E]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract private key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + privateParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_D]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract private key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + privateParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_U]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract private key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + privateParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_P]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract private key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + privateParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Private_Q]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract private key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + privateParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Public_E]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract public key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + publicParts << buffer.content(); + + buffer.clear(); + rc = gcry_mpi_aprint(format, &buffer.raw, &buffer.size, mpi[Public_N]); + if (rc != GPG_ERR_NO_ERROR) { + qWarning() << "Could not extract public key part" << gcry_err_code(rc); + return OpenSSHKey(); + } + publicParts << buffer.content(); + OpenSSHKey key; + key.m_rawType = OpenSSHKey::TYPE_RSA_PRIVATE; + key.setType("ssh-rsa"); + key.setPublicData(publicParts); + key.setPrivateData(privateParts); + key.setComment(""); + return key; +} + 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_privateType(QString()) + , m_rawType(QString()) + , m_rawData(QByteArray()) + , m_rawPublicData(QList()) + , m_rawPrivateData(QList()) , m_comment(QString()) , m_error(QString()) { @@ -51,9 +224,10 @@ OpenSSHKey::OpenSSHKey(const OpenSSHKey& other) , m_cipherName(other.m_cipherName) , m_kdfName(other.m_kdfName) , m_kdfOptions(other.m_kdfOptions) + , m_rawType(other.m_rawType) + , m_rawData(other.m_rawData) + , m_rawPublicData(other.m_rawPublicData) , 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) { @@ -77,22 +251,21 @@ const QString OpenSSHKey::type() const 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; + if (m_type == "ssh-dss" && m_rawPublicData.length() == 4) { + return (m_rawPublicData[0].length() - 1) * 8; + } else if (m_type == "ssh-rsa" && m_rawPublicData.length() == 2) { + return (m_rawPublicData[1].length() - 1) * 8; + } else if (m_type.startsWith("ecdsa-sha2-") && m_rawPublicData.length() == 2) { + return (m_rawPublicData[1].length() - 1) * 4; + } else if (m_type == "ssh-ed25519" && m_rawPublicData.length() == 1) { + return m_rawPublicData[0].length() * 8; } - return 0; } const QString OpenSSHKey::fingerprint(QCryptographicHash::Algorithm algo) const { - if (m_publicData.isEmpty()) { + if (m_rawPublicData.isEmpty()) { return {}; } @@ -101,7 +274,7 @@ const QString OpenSSHKey::fingerprint(QCryptographicHash::Algorithm algo) const stream.writeString(m_type); - for (QByteArray ba : m_publicData) { + for (const QByteArray& ba : m_rawPublicData) { stream.writeString(ba); } @@ -126,9 +299,27 @@ const QString OpenSSHKey::comment() const return m_comment; } +const QString OpenSSHKey::privateKey() const +{ + if (m_rawPrivateData.isEmpty()) { + return {}; + } + + QByteArray privateKey; + BinaryStream stream(&privateKey); + + stream.writeString(m_type); + + for (QByteArray ba : m_rawPrivateData) { + stream.writeString(ba); + } + + return m_type + " " + QString::fromLatin1(privateKey.toBase64()) + " " + m_comment; +} + const QString OpenSSHKey::publicKey() const { - if (m_publicData.isEmpty()) { + if (m_rawPublicData.isEmpty()) { return {}; } @@ -137,7 +328,7 @@ const QString OpenSSHKey::publicKey() const stream.writeString(m_type); - for (QByteArray ba : m_publicData) { + for (QByteArray ba : m_rawPublicData) { stream.writeString(ba); } @@ -156,12 +347,12 @@ void OpenSSHKey::setType(const QString& type) void OpenSSHKey::setPublicData(const QList& data) { - m_publicData = data; + m_rawPublicData = data; } void OpenSSHKey::setPrivateData(const QList& data) { - m_privateData = data; + m_rawPrivateData = data; } void OpenSSHKey::setComment(const QString& comment) @@ -171,11 +362,11 @@ void OpenSSHKey::setComment(const QString& comment) void OpenSSHKey::clearPrivate() { + m_rawData.clear(); m_rawPrivateData.clear(); - m_privateData.clear(); } -bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out) +bool OpenSSHKey::extractPEM(const QByteArray& in, QByteArray& out) { QString pem = QString::fromLatin1(in); QStringList rows = pem.split(QRegularExpression("(?:\r?\n|\r)"), QString::SkipEmptyParts); @@ -201,7 +392,7 @@ bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out) return false; } - m_privateType = beginMatch.captured(1); + m_rawType = beginMatch.captured(1); rows.removeFirst(); rows.removeLast(); @@ -237,17 +428,17 @@ bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out) return true; } -bool OpenSSHKey::parse(const QByteArray& in) +bool OpenSSHKey::parsePKCS1PEM(const QByteArray& in) { QByteArray data; - if (!parsePEM(in, data)) { + if (!extractPEM(in, data)) { return false; } - if (m_privateType == TYPE_DSA || m_privateType == TYPE_RSA) { - m_rawPrivateData = data; - } else if (m_privateType == TYPE_OPENSSH) { + if (m_rawType == TYPE_DSA_PRIVATE || m_rawType == TYPE_RSA_PRIVATE || m_rawType == TYPE_RSA_PUBLIC) { + m_rawData = data; + } else if (m_rawType == TYPE_OPENSSH_PRIVATE) { BinaryStream stream(&data); QByteArray magic; @@ -291,18 +482,18 @@ bool OpenSSHKey::parse(const QByteArray& in) } // padded list of keys - if (!stream.readString(m_rawPrivateData)) { + if (!stream.readString(m_rawData)) { m_error = tr("Corrupted key file, reading private key failed"); return false; } } else { - m_error = tr("Unsupported key type: %1").arg(m_privateType); + m_error = tr("Unsupported key type: %1").arg(m_rawType); return false; } // load private if no encryption if (!encrypted()) { - return openPrivateKey(); + return openKey(); } return true; @@ -313,15 +504,15 @@ bool OpenSSHKey::encrypted() const return (m_cipherName != "none"); } -bool OpenSSHKey::openPrivateKey(const QString& passphrase) +bool OpenSSHKey::openKey(const QString& passphrase) { QScopedPointer cipher; - if (!m_privateData.isEmpty()) { + if (!m_rawPrivateData.isEmpty()) { return true; } - if (m_rawPrivateData.isEmpty()) { + if (m_rawData.isEmpty()) { m_error = tr("No private key payload to decrypt"); return false; } @@ -390,7 +581,7 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase) hash.addData(m_cipherIV.data(), 8); mdBuf = hash.result(); keyData.append(mdBuf); - } while(keyData.size() < cipher->keySize()); + } while (keyData.size() < cipher->keySize()); if (keyData.size() > cipher->keySize()) { // If our key size isn't a multiple of 16 (e.g. AES-192 or something), @@ -407,33 +598,38 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase) return false; } - QByteArray rawPrivateData = m_rawPrivateData; + QByteArray rawData = m_rawData; if (cipher && cipher->isInitalized()) { bool ok = false; - rawPrivateData = cipher->process(rawPrivateData, &ok); + rawData = cipher->process(rawData, &ok); if (!ok) { m_error = tr("Decryption failed, wrong passphrase?"); return false; } } - if (m_privateType == TYPE_DSA) { - if (!ASN1Key::parseDSA(rawPrivateData, *this)) { + if (m_rawType == TYPE_DSA_PRIVATE) { + if (!ASN1Key::parseDSA(rawData, *this)) { m_error = tr("Decryption failed, wrong passphrase?"); return false; } return true; - } else if (m_privateType == TYPE_RSA) { - if (!ASN1Key::parseRSA(rawPrivateData, *this)) { + } else if (m_rawType == TYPE_RSA_PRIVATE) { + if (!ASN1Key::parsePrivateRSA(rawData, *this)) { m_error = tr("Decryption failed, wrong passphrase?"); return false; } - return true; - } else if (m_privateType == TYPE_OPENSSH) { - BinaryStream keyStream(&rawPrivateData); + } else if (m_rawType == TYPE_RSA_PUBLIC) { + if (!ASN1Key::parsePublicRSA(rawData, *this)) { + m_error = tr("Decryption failed, wrong passphrase?"); + return false; + } + return true; + } else if (m_rawType == TYPE_OPENSSH_PRIVATE) { + BinaryStream keyStream(&rawData); quint32 checkInt1; quint32 checkInt2; @@ -449,13 +645,13 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase) return readPrivate(keyStream); } - m_error = tr("Unsupported key type: %1").arg(m_privateType); + m_error = tr("Unsupported key type: %1").arg(m_rawType); return false; } bool OpenSSHKey::readPublic(BinaryStream& stream) { - m_publicData.clear(); + m_rawPublicData.clear(); if (!stream.readString(m_type)) { m_error = tr("Unexpected EOF while reading public key"); @@ -484,7 +680,7 @@ bool OpenSSHKey::readPublic(BinaryStream& stream) return false; } - m_publicData.append(t); + m_rawPublicData.append(t); } return true; @@ -492,7 +688,7 @@ bool OpenSSHKey::readPublic(BinaryStream& stream) bool OpenSSHKey::readPrivate(BinaryStream& stream) { - m_privateData.clear(); + m_rawPrivateData.clear(); if (!stream.readString(m_type)) { m_error = tr("Unexpected EOF while reading private key"); @@ -521,7 +717,7 @@ bool OpenSSHKey::readPrivate(BinaryStream& stream) return false; } - m_privateData.append(t); + m_rawPrivateData.append(t); } if (!stream.readString(m_comment)) { @@ -534,7 +730,7 @@ bool OpenSSHKey::readPrivate(BinaryStream& stream) bool OpenSSHKey::writePublic(BinaryStream& stream) { - if (m_publicData.isEmpty()) { + if (m_rawPublicData.isEmpty()) { m_error = tr("Can't write public key as it is empty"); return false; } @@ -544,7 +740,7 @@ bool OpenSSHKey::writePublic(BinaryStream& stream) return false; } - for (QByteArray t : m_publicData) { + for (QByteArray t : m_rawPublicData) { if (!stream.writeString(t)) { m_error = tr("Unexpected EOF when writing public key"); return false; @@ -556,7 +752,7 @@ bool OpenSSHKey::writePublic(BinaryStream& stream) bool OpenSSHKey::writePrivate(BinaryStream& stream) { - if (m_privateData.isEmpty()) { + if (m_rawPrivateData.isEmpty()) { m_error = tr("Can't write private key as it is empty"); return false; } @@ -566,7 +762,7 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream) return false; } - for (QByteArray t : m_privateData) { + for (QByteArray t : m_rawPrivateData) { if (!stream.writeString(t)) { m_error = tr("Unexpected EOF when writing private key"); return false; @@ -581,6 +777,49 @@ bool OpenSSHKey::writePrivate(BinaryStream& stream) return true; } +QList OpenSSHKey::publicParts() const +{ + return m_rawPublicData; +} + +QList OpenSSHKey::privateParts() const +{ + return m_rawPrivateData; +} + +const QString& OpenSSHKey::privateType() const +{ + return m_rawType; +} + +OpenSSHKey OpenSSHKey::restoreFromBinary(Type type, const QByteArray& serialized) +{ + OpenSSHKey key; + auto data = binaryDeserialize(serialized); + key.setType(data.first); + switch (type) { + case Public: + key.setPublicData(data.second); + break; + case Private: + key.setPrivateData(data.second); + break; + } + return key; +} + +QByteArray OpenSSHKey::serializeToBinary(Type type, const OpenSSHKey& key) +{ + Q_ASSERT(!key.encrypted()); + switch (type) { + case Public: + return binarySerialize(key.type(), key.publicParts()); + case Private: + return binarySerialize(key.type(), key.privateParts()); + } + return {}; +} + uint qHash(const OpenSSHKey& key) { return qHash(key.fingerprint()); diff --git a/src/sshagent/OpenSSHKey.h b/src/crypto/ssh/OpenSSHKey.h similarity index 64% rename from src/sshagent/OpenSSHKey.h rename to src/crypto/ssh/OpenSSHKey.h index 406f390ea..85c288b9f 100644 --- a/src/sshagent/OpenSSHKey.h +++ b/src/crypto/ssh/OpenSSHKey.h @@ -16,23 +16,26 @@ * along with this program. If not, see . */ -#ifndef OPENSSHKEY_H -#define OPENSSHKEY_H +#ifndef KEEPASSXC_OPENSSHKEY_H +#define KEEPASSXC_OPENSSHKEY_H -#include "BinaryStream.h" #include -class OpenSSHKey : QObject +class BinaryStream; + +class OpenSSHKey : public QObject { Q_OBJECT public: + static OpenSSHKey generate(bool secure = true); + explicit OpenSSHKey(QObject* parent = nullptr); OpenSSHKey(const OpenSSHKey& other); bool operator==(const OpenSSHKey& other) const; - bool parse(const QByteArray& in); + bool parsePKCS1PEM(const QByteArray& in); bool encrypted() const; - bool openPrivateKey(const QString& passphrase = QString()); + bool openKey(const QString& passphrase = QString()); const QString cipherName() const; const QString type() const; @@ -40,6 +43,7 @@ public: const QString fingerprint(QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256) const; const QString comment() const; const QString publicKey() const; + const QString privateKey() const; const QString errorString() const; void setType(const QString& type); @@ -54,26 +58,41 @@ public: bool writePublic(BinaryStream& stream); bool writePrivate(BinaryStream& stream); -private: - static const QString TYPE_DSA; - static const QString TYPE_RSA; - static const QString TYPE_OPENSSH; + QList publicParts() const; + QList privateParts() const; + const QString& privateType() const; - bool parsePEM(const QByteArray& in, QByteArray& out); + static const QString TYPE_DSA_PRIVATE; + static const QString TYPE_RSA_PRIVATE; + static const QString TYPE_RSA_PUBLIC; + static const QString TYPE_OPENSSH_PRIVATE; + + enum Type + { + Public, + Private + }; + + static OpenSSHKey restoreFromBinary(Type eType, const QByteArray& serialized); + static QByteArray serializeToBinary(Type eType, const OpenSSHKey& key); + +private: + bool extractPEM(const QByteArray& in, QByteArray& out); QString m_type; QString m_cipherName; QByteArray m_cipherIV; QString m_kdfName; QByteArray m_kdfOptions; - QByteArray m_rawPrivateData; - QList m_publicData; - QList m_privateData; - QString m_privateType; + + QString m_rawType; + QByteArray m_rawData; + QList m_rawPublicData; + QList m_rawPrivateData; QString m_comment; QString m_error; }; uint qHash(const OpenSSHKey& key); -#endif // OPENSSHKEY_H +#endif // KEEPASSXC_OPENSSHKEY_H diff --git a/src/sshagent/bcrypt_pbkdf.cpp b/src/crypto/ssh/bcrypt_pbkdf.cpp similarity index 100% rename from src/sshagent/bcrypt_pbkdf.cpp rename to src/crypto/ssh/bcrypt_pbkdf.cpp diff --git a/src/sshagent/blf.h b/src/crypto/ssh/blf.h similarity index 100% rename from src/sshagent/blf.h rename to src/crypto/ssh/blf.h diff --git a/src/sshagent/blowfish.c b/src/crypto/ssh/blowfish.c similarity index 100% rename from src/sshagent/blowfish.c rename to src/crypto/ssh/blowfish.c diff --git a/src/sshagent/includes.h b/src/crypto/ssh/includes.h similarity index 100% rename from src/sshagent/includes.h rename to src/crypto/ssh/includes.h diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp index 7b94d34f8..30f642464 100644 --- a/src/format/Kdbx4Reader.cpp +++ b/src/format/Kdbx4Reader.cpp @@ -69,8 +69,7 @@ Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device, } QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, m_db->transformedMasterKey()); - if (headerHmac - != CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) { + if (headerHmac != CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) { raiseError(tr("Wrong key or database file is corrupt. (HMAC mismatch)")); return nullptr; } @@ -85,8 +84,7 @@ Database* Kdbx4Reader::readDatabaseImpl(QIODevice* device, raiseError(tr("Unknown cipher")); return nullptr; } - SymmetricCipherStream cipherStream( - &hmacStream, cipher, SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt); + SymmetricCipherStream cipherStream(&hmacStream, cipher, SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt); if (!cipherStream.init(finalKey, m_encryptionIV)) { raiseError(cipherStream.errorString()); return nullptr; diff --git a/src/format/KdbxXmlWriter.cpp b/src/format/KdbxXmlWriter.cpp index 5ad1e34ae..cff2283e1 100644 --- a/src/format/KdbxXmlWriter.cpp +++ b/src/format/KdbxXmlWriter.cpp @@ -34,7 +34,7 @@ KdbxXmlWriter::KdbxXmlWriter(quint32 version) } void KdbxXmlWriter::writeDatabase(QIODevice* device, - Database* db, + const Database* db, KeePass2RandomStream* randomStream, const QByteArray& headerHash) { diff --git a/src/format/KdbxXmlWriter.h b/src/format/KdbxXmlWriter.h index 51a803497..1e00732fe 100644 --- a/src/format/KdbxXmlWriter.h +++ b/src/format/KdbxXmlWriter.h @@ -37,7 +37,7 @@ public: explicit KdbxXmlWriter(quint32 version); void writeDatabase(QIODevice* device, - Database* db, + const Database *db, KeePass2RandomStream* randomStream = nullptr, const QByteArray& headerHash = QByteArray()); void writeDatabase(const QString& filename, Database* db); @@ -82,8 +82,8 @@ private: const quint32 m_kdbxVersion; QXmlStreamWriter m_xml; - QPointer m_db; - QPointer m_meta; + QPointer m_db; + QPointer m_meta; KeePass2RandomStream* m_randomStream = nullptr; QHash m_idMap; QByteArray m_headerHash; diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index c7c75a11e..b987815d8 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -89,6 +89,9 @@ AboutDialog::AboutDialog(QWidget* parent) #ifdef WITH_XC_SSHAGENT extensions += "\n- " + tr("SSH Agent"); #endif +#ifdef WITH_XC_KEESHARE + extensions += "\n- " + tr("KeeShare"); +#endif #ifdef WITH_XC_YUBIKEY extensions += "\n- " + tr("YubiKey"); #endif diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 285462042..ce854f244 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -20,8 +20,9 @@ #include "ui_ApplicationSettingsWidgetGeneral.h" #include "ui_ApplicationSettingsWidgetSecurity.h" -#include "autotype/AutoType.h" #include "config-keepassx.h" + +#include "autotype/AutoType.h" #include "core/Config.h" #include "core/FilePath.h" #include "core/Global.h" @@ -77,12 +78,10 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) connect(this, SIGNAL(apply()), SLOT(saveSettings())); connect(this, SIGNAL(rejected()), SLOT(reject())); - connect( - m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableAutoSaveOnExit(bool))); + connect(m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableAutoSaveOnExit(bool))); connect(m_generalUi->systrayShowCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableSystray(bool))); - connect( - m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool))); + connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool))); connect(m_secUi->lockDatabaseIdleCheckBox, SIGNAL(toggled(bool)), m_secUi->lockDatabaseIdleSpinBox, @@ -120,7 +119,6 @@ void ApplicationSettingsWidget::addSettingsPage(ISettingsPage* page) void ApplicationSettingsWidget::loadSettings() { - if (config()->hasAccessError()) { showMessage(tr("Access error for config file %1").arg(config()->getFileName()), MessageWidget::Error); } diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 71bd74814..e5c906fbb 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -40,6 +40,9 @@ #include "gui/entry/EntryView.h" #include "gui/group/GroupView.h" #include "gui/wizard/NewDatabaseWizard.h" +#include "keeshare/KeeShare.h" + +#include "config-keepassx.h" DatabaseManagerStruct::DatabaseManagerStruct() : dbWidget(nullptr) @@ -375,6 +378,11 @@ bool DatabaseTabWidget::saveDatabase(Database* db, QString filePath) dbStruct.saveAttempts = 0; dbStruct.fileInfo = QFileInfo(filePath); dbStruct.dbWidget->databaseSaved(); +#ifdef WITH_XC_KEESHARE + // TODO HNH: This is hacky - we need to remove the logic from the ui at this point to allow a proper + // architecture + KeeShare::instance()->handleDatabaseSaved(db); +#endif updateTabName(db); emit messageDismissTab(); return true; @@ -429,7 +437,14 @@ bool DatabaseTabWidget::saveDatabaseAs(Database* db) // Failed to save, try again continue; } - +#ifdef WITH_XC_KEESHARE + // Since we change to the saved database we should also export + // TODO HNH: This is hacky - we need to remove the logic from the ui at this point to allow a proper + // architecture + KeeShare::instance()->handleDatabaseSaved(db); +#endif + // changes of the current database + // SaveAs for non-existing datbase doesn't matter since one has to set the path while creation dbStruct.dbWidget->updateFilePath(dbStruct.fileInfo.absoluteFilePath()); updateLastDatabases(dbStruct.fileInfo.absoluteFilePath()); return true; @@ -628,9 +643,9 @@ void DatabaseTabWidget::updateTabNameFromDbWidgetSender() } } -int DatabaseTabWidget::databaseIndex(Database* db) +int DatabaseTabWidget::databaseIndex(const Database* db) { - QWidget* dbWidget = m_dbList.value(db).dbWidget; + QWidget* dbWidget = m_dbList.value(const_cast(db)).dbWidget; return indexOf(dbWidget); } @@ -865,6 +880,35 @@ void DatabaseTabWidget::connectDatabase(Database* newDb, Database* oldDb) connect(newDb, SIGNAL(nameTextChanged()), SLOT(updateTabNameFromDbSender())); connect(newDb, SIGNAL(modified()), SLOT(modified())); newDb->setEmitModified(true); + +#ifdef WITH_XC_KEESHARE + KeeShare::instance()->connectDatabase(newDb, oldDb); + connect(KeeShare::instance(), + SIGNAL(sharingMessage(Database*, QString, MessageWidget::MessageType)), + this, + SLOT(handleDatabaseMessage(Database*, QString, MessageWidget::MessageType)), + Qt::UniqueConnection); + KeeShare::instance()->handleDatabaseOpened(newDb); +#endif +} + +void DatabaseTabWidget::handleDatabaseMessage(Database* db, QString message, MessageWidget::MessageType type) +{ + auto* databaseWidget = currentDatabaseWidget(); + if (!databaseWidget) { + return; + } + auto* currentDb = currentDatabaseWidget()->database(); + if (!currentDb) { + return; + } + if (currentDb != db) { + auto index = databaseIndex(db); + emit messageGlobal(tr("Update in background database %1:\n%2").arg(tabText(index)).arg(message), type); + + } else { + emit messageTab(message, type); + } } void DatabaseTabWidget::performGlobalAutoType() diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index dcb4a62da..d761f6efb 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -101,6 +101,7 @@ private slots: void changeDatabase(Database* newDb, bool unsavedChanges); void emitActivateDatabaseChanged(); void emitDatabaseUnlockedFromDbWidgetSender(); + void handleDatabaseMessage(Database* db, QString message, MessageWidget::MessageType type); private: Database* execNewDatabaseWizard(); @@ -108,7 +109,7 @@ private: bool saveDatabaseAs(Database* db); bool closeDatabase(Database* db); void deleteDatabase(Database* db); - int databaseIndex(Database* db); + int databaseIndex(const Database* db); Database* indexDatabase(int index); DatabaseManagerStruct indexDatabaseManagerStruct(int index); Database* databaseFromDatabaseWidget(DatabaseWidget* dbWidget); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index aae6527a1..e840f075e 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -35,6 +35,7 @@ #include "core/Config.h" #include "core/EntrySearcher.h" #include "core/FilePath.h" +#include "core/FileWatcher.h" #include "core/Group.h" #include "core/Merger.h" #include "core/Metadata.h" @@ -73,6 +74,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) , m_newGroup(nullptr) , m_newEntry(nullptr) , m_newParent(nullptr) + , m_importingCsv(false) + , m_fileWatcher(new DelayingFileWatcher(this)) { m_mainWidget = new QWidget(this); @@ -198,9 +201,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool))); connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool))); - connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); - connect(&m_fileWatchTimer, SIGNAL(timeout()), this, SLOT(reloadDatabaseFile())); - connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(unblockAutoReload())); + connect(m_fileWatcher.data(), SIGNAL(fileChanged()), this, SLOT(reloadDatabaseFile())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); connect(m_groupView, SIGNAL(groupPressed(Group*)), SLOT(emitPressedGroup(Group*))); @@ -211,10 +212,6 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_databaseModified = false; - m_fileWatchTimer.setSingleShot(true); - m_fileWatchUnblockTimer.setSingleShot(true); - m_ignoreAutoReload = false; - m_searchCaseSensitive = false; m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool(); @@ -817,9 +814,9 @@ void DatabaseWidget::openDatabase(bool accepted) m_databaseOpenWidget = nullptr; delete m_keepass1OpenWidget; m_keepass1OpenWidget = nullptr; - m_fileWatcher.addPath(m_filePath); + m_fileWatcher->restart(); } else { - m_fileWatcher.removePath(m_filePath); + m_fileWatcher->stop(); if (m_databaseOpenWidget->database()) { delete m_databaseOpenWidget->database(); } @@ -1175,26 +1172,7 @@ void DatabaseWidget::lock() void DatabaseWidget::updateFilePath(const QString& filePath) { - if (!m_filePath.isEmpty()) { - m_fileWatcher.removePath(m_filePath); - } - -#if defined(Q_OS_LINUX) - struct statfs statfsBuf; - bool forcePolling = false; - const auto NFS_SUPER_MAGIC = 0x6969; - - if (!statfs(filePath.toLocal8Bit().constData(), &statfsBuf)) { - forcePolling = (statfsBuf.f_type == NFS_SUPER_MAGIC); - } else { - // if we can't get the fs type let's fall back to polling - forcePolling = true; - } - auto objectName = forcePolling ? QLatin1String("_qt_autotest_force_engine_poller") : QLatin1String(""); - m_fileWatcher.setObjectName(objectName); -#endif - - m_fileWatcher.addPath(filePath); + m_fileWatcher->start(filePath); m_filePath = filePath; m_db->setFilePath(filePath); } @@ -1202,30 +1180,12 @@ void DatabaseWidget::updateFilePath(const QString& filePath) void DatabaseWidget::blockAutoReload(bool block) { if (block) { - m_ignoreAutoReload = true; - m_fileWatchTimer.stop(); + m_fileWatcher->ignoreFileChanges(); } else { - m_fileWatchUnblockTimer.start(500); + m_fileWatcher->observeFileChanges(true); } } -void DatabaseWidget::unblockAutoReload() -{ - m_ignoreAutoReload = false; - updateFilePath(m_filePath); -} - -void DatabaseWidget::onWatchedFileChanged() -{ - if (m_ignoreAutoReload) { - return; - } - if (m_fileWatchTimer.isActive()) - return; - - m_fileWatchTimer.start(500); -} - void DatabaseWidget::reloadDatabaseFile() { if (!m_db || currentMode() == DatabaseWidget::LockedMode) { @@ -1249,7 +1209,7 @@ void DatabaseWidget::reloadDatabaseFile() m_db->markAsModified(); m_databaseModified = true; // Rewatch the database file - m_fileWatcher.addPath(m_filePath); + m_fileWatcher->restart(); return; } } @@ -1307,7 +1267,7 @@ void DatabaseWidget::reloadDatabaseFile() } // Rewatch the database file - m_fileWatcher.addPath(m_filePath); + m_fileWatcher->restart(); } int DatabaseWidget::numberOfSelectedEntries() const diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 896703eb6..b8e20f019 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -37,6 +37,7 @@ class EditEntryWidget; class EditGroupWidget; class Entry; class EntryView; +class DelayingFileWatcher; class Group; class GroupView; class KeePass1OpenWidget; @@ -48,7 +49,6 @@ class UnlockDatabaseWidget; class MessageWidget; class DetailsWidget; class UnlockDatabaseDialog; -class QFileSystemWatcher; namespace Ui { @@ -200,10 +200,8 @@ private slots: void unlockDatabase(bool accepted); void emitCurrentModeChanged(); // Database autoreload slots - void onWatchedFileChanged(); void reloadDatabaseFile(); void restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& EntryUuid); - void unblockAutoReload(); private: void setClipboardTextAndMinimize(const QString& text); @@ -227,11 +225,12 @@ private: QSplitter* m_detailSplitter; GroupView* m_groupView; EntryView* m_entryView; + QString m_filePath; QLabel* m_searchingLabel; Group* m_newGroup; Entry* m_newEntry; Group* m_newParent; - QString m_filePath; + QUuid m_groupBeforeLock; QUuid m_entryBeforeLock; MessageWidget* m_messageWidget; @@ -246,10 +245,7 @@ private: bool m_importingCsv; // Autoreload - QFileSystemWatcher m_fileWatcher; - QTimer m_fileWatchTimer; - QTimer m_fileWatchUnblockTimer; - bool m_ignoreAutoReload; + QPointer m_fileWatcher; bool m_databaseModified; }; diff --git a/src/gui/DetailsWidget.cpp b/src/gui/DetailsWidget.cpp index ff8861172..4e8da2623 100644 --- a/src/gui/DetailsWidget.cpp +++ b/src/gui/DetailsWidget.cpp @@ -27,6 +27,9 @@ #include "core/FilePath.h" #include "entry/EntryAttachmentsModel.h" #include "gui/Clipboard.h" +#ifdef WITH_XC_KEESHARE +#include "keeshare/KeeShare.h" +#endif namespace { @@ -102,7 +105,9 @@ void DetailsWidget::setGroup(Group* selectedGroup) updateGroupHeaderLine(); updateGroupGeneralTab(); updateGroupNotesTab(); - +#ifdef WITH_XC_KEESHARE + updateGroupSharingTab(); +#endif setVisible(!config()->get("GUI/HideDetailsView").toBool()); m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup); @@ -267,6 +272,17 @@ void DetailsWidget::updateGroupNotesTab() m_ui->groupNotesEdit->setText(notes); } +#ifdef WITH_XC_KEESHARE +void DetailsWidget::updateGroupSharingTab() +{ + Q_ASSERT(m_currentGroup); + setTabEnabled(m_ui->groupTabWidget, m_ui->groupShareTab, KeeShare::isShared(m_currentGroup)); + auto reference = KeeShare::referenceOf(m_currentGroup); + m_ui->groupShareTypeLabel->setText(KeeShare::referenceTypeLabel(reference)); + m_ui->groupSharePathLabel->setText(reference.path); +} +#endif + void DetailsWidget::updateTotpLabel() { if (!m_locked && m_currentEntry && m_currentEntry->hasTotp()) { diff --git a/src/gui/DetailsWidget.h b/src/gui/DetailsWidget.h index ba42e5278..513bb9556 100644 --- a/src/gui/DetailsWidget.h +++ b/src/gui/DetailsWidget.h @@ -18,6 +18,7 @@ #ifndef KEEPASSX_DETAILSWIDGET_H #define KEEPASSX_DETAILSWIDGET_H +#include "config-keepassx.h" #include "gui/DatabaseWidget.h" #include @@ -55,6 +56,9 @@ private slots: void updateGroupHeaderLine(); void updateGroupGeneralTab(); void updateGroupNotesTab(); +#ifdef WITH_XC_KEESHARE + void updateGroupSharingTab(); +#endif void updateTotpLabel(); void updateTabIndexes(); diff --git a/src/gui/DetailsWidget.ui b/src/gui/DetailsWidget.ui index 38906150e..27c3c1d2c 100644 --- a/src/gui/DetailsWidget.ui +++ b/src/gui/DetailsWidget.ui @@ -2,6 +2,14 @@ DetailsWidget + + + 0 + 0 + 652 + 274 + + 0 @@ -38,7 +46,7 @@ 0 - + QLayout::SetDefaultConstraint @@ -76,6 +84,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -167,47 +188,6 @@ 0 - - - - - 0 - 0 - - - - - 100 - 0 - - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - PointingHandCursor - - - - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse - - - @@ -252,6 +232,54 @@ + + + + + 100 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + URL + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 100 + 0 + + + + PointingHandCursor + + + + + + @@ -284,75 +312,15 @@ - - - - - 0 - 0 - - - - - 75 - true - - - - URL - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Password - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + Qt::Vertical - 0 - 0 + 20 + 10 @@ -473,7 +441,7 @@ 0 - + QLayout::SetDefaultConstraint @@ -511,6 +479,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -673,10 +654,16 @@ - + Qt::Vertical + + + 20 + 10 + + @@ -701,6 +688,89 @@ + + + Share + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + <path> + + + + + + + + 75 + true + + + + <type> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + 20 + 147 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + diff --git a/src/gui/EditWidgetProperties.cpp b/src/gui/EditWidgetProperties.cpp index 93e3b0ae8..422e32bb7 100644 --- a/src/gui/EditWidgetProperties.cpp +++ b/src/gui/EditWidgetProperties.cpp @@ -22,10 +22,12 @@ #include "MessageBox.h" #include "ui_EditWidgetProperties.h" +#include "core/CustomData.h" +#include "core/TimeInfo.h" + EditWidgetProperties::EditWidgetProperties(QWidget* parent) : QWidget(parent) , m_ui(new Ui::EditWidgetProperties()) - , m_customData(new CustomData(this)) , m_customDataModel(new QStandardItemModel(this)) { m_ui->setupUi(this); @@ -51,17 +53,19 @@ void EditWidgetProperties::setFields(const TimeInfo& timeInfo, const QUuid& uuid m_ui->uuidEdit->setText(uuid.toRfc4122().toHex()); } -void EditWidgetProperties::setCustomData(const CustomData* customData) +void EditWidgetProperties::setCustomData(CustomData* customData) { - Q_ASSERT(customData); - m_customData->copyDataFrom(customData); + if (m_customData) { + m_customData->disconnect(this); + } - updateModel(); -} + m_customData = customData; -const CustomData* EditWidgetProperties::customData() const -{ - return m_customData; + if (m_customData) { + connect(m_customData, SIGNAL(modified()), SLOT(update())); + } + + update(); } void EditWidgetProperties::removeSelectedPluginData() @@ -81,7 +85,7 @@ void EditWidgetProperties::removeSelectedPluginData() const QString key = index.data().toString(); m_customData->remove(key); } - updateModel(); + update(); } } @@ -90,16 +94,17 @@ void EditWidgetProperties::toggleRemoveButton(const QItemSelection& selected) m_ui->removeCustomDataButton->setEnabled(!selected.isEmpty()); } -void EditWidgetProperties::updateModel() +void EditWidgetProperties::update() { m_customDataModel->clear(); - m_customDataModel->setHorizontalHeaderLabels({tr("Key"), tr("Value")}); - - for (const QString& key : m_customData->keys()) { - m_customDataModel->appendRow(QList() << new QStandardItem(key) - << new QStandardItem(m_customData->value(key))); + if (!m_customData) { + m_ui->removeCustomDataButton->setEnabled(false); + } else { + for (const QString& key : m_customData->keys()) { + m_customDataModel->appendRow(QList() << new QStandardItem(key) + << new QStandardItem(m_customData->value(key))); + } + m_ui->removeCustomDataButton->setEnabled(!m_customData->isEmpty()); } - - m_ui->removeCustomDataButton->setEnabled(false); } diff --git a/src/gui/EditWidgetProperties.h b/src/gui/EditWidgetProperties.h index 6fad1f866..30a983e98 100644 --- a/src/gui/EditWidgetProperties.h +++ b/src/gui/EditWidgetProperties.h @@ -23,8 +23,9 @@ #include #include -#include "core/CustomData.h" -#include "core/TimeInfo.h" +class CustomData; +class TimeInfo; +class QUuid; namespace Ui { @@ -40,21 +41,19 @@ public: ~EditWidgetProperties(); void setFields(const TimeInfo& timeInfo, const QUuid& uuid); - void setCustomData(const CustomData* customData); - - const CustomData* customData() const; + void setCustomData(CustomData* customData); private slots: + void update(); void removeSelectedPluginData(); void toggleRemoveButton(const QItemSelection& selected); private: - void updateModel(); - const QScopedPointer m_ui; QPointer m_customData; QPointer m_customDataModel; + Q_DISABLE_COPY(EditWidgetProperties) }; diff --git a/src/gui/FileDialog.cpp b/src/gui/FileDialog.cpp index d58f52928..f404d8fc8 100644 --- a/src/gui/FileDialog.cpp +++ b/src/gui/FileDialog.cpp @@ -79,13 +79,14 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent, } } -QString FileDialog::getSaveFileName(QWidget* parent, - const QString& caption, - QString dir, - const QString& filter, - QString* selectedFilter, - QFileDialog::Options options, - const QString& defaultExtension) +QString FileDialog::getFileName(QWidget* parent, + const QString& caption, + QString dir, + const QString& filter, + QString* selectedFilter, + QFileDialog::Options options, + const QString& defaultExtension, + const QString& defaultName) { if (!m_nextFileName.isEmpty()) { QString result = m_nextFileName; @@ -98,6 +99,63 @@ QString FileDialog::getSaveFileName(QWidget* parent, QString result; #if defined(Q_OS_MAC) || defined(Q_OS_WIN) + Q_UNUSED(defaultName); + Q_UNUSED(defaultExtension); + // the native dialogs on these platforms already append the file extension + result = QFileDialog::getSaveFileName(parent, caption, dir, filter, selectedFilter, options); +#else + QFileDialog dialog(parent, caption, dir, filter); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setAcceptMode(QFileDialog::AcceptSave); + if (selectedFilter) { + dialog.selectNameFilter(*selectedFilter); + } + if (!defaultName.isEmpty()) { + dialog.selectFile(defaultName); + } + dialog.setOptions(options); + dialog.setDefaultSuffix(defaultExtension); + dialog.setLabelText(QFileDialog::Accept, QFileDialog::tr("Select")); + QStringList results; + if (dialog.exec()) { + results = dialog.selectedFiles(); + if (!results.isEmpty()) { + result = results[0]; + } + } +#endif + + // on Mac OS X the focus is lost after closing the native dialog + if (parent) { + parent->activateWindow(); + } + + saveLastDir(result); + return result; + } +} + +QString FileDialog::getSaveFileName(QWidget* parent, + const QString& caption, + QString dir, + const QString& filter, + QString* selectedFilter, + QFileDialog::Options options, + const QString& defaultExtension, + const QString& defaultName) +{ + if (!m_nextFileName.isEmpty()) { + QString result = m_nextFileName; + m_nextFileName.clear(); + return result; + } else { + if (dir.isEmpty()) { + dir = config()->get("LastDir").toString(); + } + + QString result; +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + Q_UNUSED(defaultName); Q_UNUSED(defaultExtension); // the native dialogs on these platforms already append the file extension result = QFileDialog::getSaveFileName(parent, caption, dir, filter, selectedFilter, options); @@ -108,6 +166,9 @@ QString FileDialog::getSaveFileName(QWidget* parent, if (selectedFilter) { dialog.selectNameFilter(*selectedFilter); } + if (!defaultName.isEmpty()) { + dialog.selectFile(defaultName); + } dialog.setOptions(options); dialog.setDefaultSuffix(defaultExtension); @@ -130,8 +191,7 @@ QString FileDialog::getSaveFileName(QWidget* parent, } } -QString -FileDialog::getExistingDirectory(QWidget* parent, const QString& caption, QString dir, QFileDialog::Options options) +QString FileDialog::getExistingDirectory(QWidget* parent, const QString& caption, QString dir, QFileDialog::Options options) { if (!m_nextDirName.isEmpty()) { QString result = m_nextDirName; diff --git a/src/gui/FileDialog.h b/src/gui/FileDialog.h index 4862dcfda..8cc1d138e 100644 --- a/src/gui/FileDialog.h +++ b/src/gui/FileDialog.h @@ -35,13 +35,22 @@ public: const QString& filter = QString(), QString* selectedFilter = nullptr, QFileDialog::Options options = 0); + QString getFileName(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(), + const QString& defaultName = QString()); 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()); + const QString& defaultExtension = QString(), + const QString& defaultName = QString()); QString getExistingDirectory(QWidget* parent = nullptr, const QString& caption = QString(), QString dir = QString(), diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 733ac0163..dd9894c37 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -40,7 +40,10 @@ #include "sshagent/AgentSettingsPage.h" #include "sshagent/SSHAgent.h" #endif - +#ifdef WITH_XC_KEESHARE +#include "keeshare/KeeShare.h" +#include "keeshare/SettingsPageKeeShare.h" +#endif #ifdef WITH_XC_BROWSER #include "browser/BrowserOptionDialog.h" #include "browser/BrowserSettings.h" @@ -141,22 +144,26 @@ MainWindow::MainWindow() m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); + #ifdef WITH_XC_BROWSER m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); #endif + #ifdef WITH_XC_SSHAGENT SSHAgent::init(this); connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif +#ifdef WITH_XC_KEESHARE + KeeShare::init(this); + m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); +#endif setWindowIcon(filePath()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); - connect( - m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); - connect( - m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); + connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); + connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 9f6041312..90908f92c 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -608,8 +608,19 @@ Report a &bug + + + Share entry + + + + PasswordGeneratorWidget + QWidget +
gui/PasswordGeneratorWidget.h
+ 1 +
MessageWidget QWidget @@ -634,12 +645,6 @@
gui/WelcomeWidget.h
1
- - PasswordGeneratorWidget - QWidget -
gui/PasswordGeneratorWidget.h
- 1 -
diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index b2624a425..c95e14409 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -21,11 +21,36 @@ #include "DatabaseSettingsWidgetGeneral.h" #include "DatabaseSettingsWidgetEncryption.h" #include "DatabaseSettingsWidgetMasterKey.h" +#ifdef WITH_XC_KEESHARE +#include "keeshare/DatabaseSettingsPageKeeShare.h" +#endif +#include "core/Global.h" #include "core/Config.h" #include "core/FilePath.h" #include "core/Database.h" +class DatabaseSettingsDialog::ExtraPage +{ +public: + ExtraPage(IDatabaseSettingsPage* page, QWidget* widget) + : settingsPage(page) + , widget(widget) + { + } + void loadSettings(Database* db) const + { + settingsPage->loadSettings(widget, db); + } + void saveSettings() const + { + settingsPage->saveSettings(widget); + } +private: + QSharedPointer settingsPage; + QWidget* widget; +}; + DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) : DialogyWidget(parent) , m_ui(new Ui::DatabaseSettingsDialog()) @@ -47,6 +72,10 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key")); m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings")); +#ifdef WITH_XC_KEESHARE + addSettingsPage(new DatabaseSettingsPageKeeShare()); +#endif + m_ui->stackedWidget->setCurrentIndex(0); m_securityTabWidget->setCurrentIndex(0); @@ -67,10 +96,24 @@ void DatabaseSettingsDialog::load(Database* db) m_generalWidget->load(db); m_masterKeyWidget->load(db); m_encryptionWidget->load(db); + for (const ExtraPage& page : asConst(m_extraPages)) { + page.loadSettings(db); + } m_ui->advancedSettingsToggle->setChecked(config()->get("GUI/AdvancedSettings", false).toBool()); m_db = db; } +void DatabaseSettingsDialog::addSettingsPage(IDatabaseSettingsPage* page) +{ + const int category = m_ui->categoryList->currentCategory(); + QWidget* widget = page->createWidget(); + widget->setParent(this); + m_extraPages.append(ExtraPage(page, widget)); + m_ui->stackedWidget->addWidget(widget); + m_ui->categoryList->addCategory(page->name(), page->icon()); + m_ui->categoryList->setCurrentCategory(category); +} + /** * Show page and tab with database master key settings. */ @@ -94,6 +137,10 @@ void DatabaseSettingsDialog::save() return; } + for (const ExtraPage& extraPage : asConst(m_extraPages)) { + extraPage.saveSettings(); + } + #ifdef WITH_XC_TOUCHID TouchID::getInstance().reset(m_db ? m_db->filePath() : ""); #endif diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.h b/src/gui/dbsettings/DatabaseSettingsDialog.h index 50fec32d6..81c295975 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.h +++ b/src/gui/dbsettings/DatabaseSettingsDialog.h @@ -34,6 +34,19 @@ namespace Ui class DatabaseSettingsDialog; } +class IDatabaseSettingsPage +{ +public: + virtual ~IDatabaseSettingsPage() + { + } + virtual QString name() = 0; + virtual QIcon icon() = 0; + virtual QWidget* createWidget() = 0; + virtual void loadSettings(QWidget* widget, Database* db) = 0; + virtual void saveSettings(QWidget* widget) = 0; +}; + class DatabaseSettingsDialog : public DialogyWidget { Q_OBJECT @@ -44,6 +57,7 @@ public: Q_DISABLE_COPY(DatabaseSettingsDialog); void load(Database* db); + void addSettingsPage(IDatabaseSettingsPage* page); void showMasterKeySettings(); signals: @@ -68,6 +82,9 @@ private: QPointer m_securityTabWidget; QPointer m_masterKeyWidget; QPointer m_encryptionWidget; + + class ExtraPage; + QList m_extraPages; }; #endif // KEEPASSX_DATABASESETTINGSWIDGET_H diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index f15ca5328..23cc9c0bc 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -44,8 +44,8 @@ #include "core/TimeDelta.h" #include "core/Tools.h" #ifdef WITH_XC_SSHAGENT +#include "crypto/ssh/OpenSSHKey.h" #include "sshagent/KeeAgentSettings.h" -#include "sshagent/OpenSSHKey.h" #include "sshagent/SSHAgent.h" #endif #include "gui/Clipboard.h" @@ -67,11 +67,14 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) , m_autoTypeUi(new Ui::EditEntryWidgetAutoType()) , m_sshAgentUi(new Ui::EditEntryWidgetSSHAgent()) , m_historyUi(new Ui::EditEntryWidgetHistory()) + , m_customData(new CustomData()) , m_mainWidget(new QWidget()) , m_advancedWidget(new QWidget()) , m_iconsWidget(new EditWidgetIcons()) , m_autoTypeWidget(new QWidget()) +#ifdef WITH_XC_SSHAGENT , m_sshAgentWidget(new QWidget()) +#endif , m_editWidgetProperties(new EditWidgetProperties()) , m_historyWidget(new QWidget()) , m_entryAttributes(new EntryAttributes(this)) @@ -87,6 +90,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) setupAdvanced(); setupIcon(); setupAutoType(); + #ifdef WITH_XC_SSHAGENT if (config()->get("SSHAgent", false).toBool()) { setupSSHAgent(); @@ -95,6 +99,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) m_sshAgentEnabled = false; } #endif + setupProperties(); setupHistory(); setupEntryUpdate(); @@ -108,6 +113,8 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) connect(m_iconsWidget, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage())); m_mainUi->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); + + m_editWidgetProperties->setCustomData(m_customData.data()); } EditEntryWidget::~EditEntryWidget() @@ -522,13 +529,13 @@ bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key, bool decrypt) return false; } - if (!key.parse(privateKeyData)) { + if (!key.parsePKCS1PEM(privateKeyData)) { showMessage(key.errorString(), MessageWidget::Error); return false; } if (key.encrypted() && (decrypt || key.publicKey().isEmpty())) { - if (!key.openPrivateKey(m_entry->password())) { + if (!key.openKey(m_entry->password())) { showMessage(key.errorString(), MessageWidget::Error); return false; } @@ -669,6 +676,8 @@ void EditEntryWidget::loadEntry(Entry* entry, bool create, bool history, const Q void EditEntryWidget::setForms(const Entry* entry, bool restore) { + m_customData->copyDataFrom(entry->customData()); + m_mainUi->titleEdit->setReadOnly(m_history); m_mainUi->usernameEdit->setReadOnly(m_history); m_mainUi->urlEdit->setReadOnly(m_history); @@ -764,7 +773,6 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) #endif m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid()); - m_editWidgetProperties->setCustomData(entry->customData()); if (!m_history && !restore) { m_historyModel->setEntries(entry->historyItems()); @@ -873,7 +881,7 @@ void EditEntryWidget::updateEntryData(Entry* entry) const entry->attributes()->copyCustomKeysFrom(m_entryAttributes); entry->attachments()->copyDataFrom(m_advancedUi->attachmentsWidget->entryAttachments()); - entry->customData()->copyDataFrom(m_editWidgetProperties->customData()); + entry->customData()->copyDataFrom(m_customData.data()); entry->setTitle(m_mainUi->titleEdit->text().replace(newLineRegex, " ")); entry->setUsername(m_mainUi->usernameEdit->text().replace(newLineRegex, " ")); entry->setUrl(m_mainUi->urlEdit->text().replace(newLineRegex, " ")); diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index b3c313b19..c8eabb8a9 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -28,6 +28,7 @@ class AutoTypeAssociations; class AutoTypeAssociationsModel; +class CustomData; class Database; class EditWidgetIcons; class EditWidgetProperties; @@ -153,11 +154,15 @@ private: const QScopedPointer m_autoTypeUi; const QScopedPointer m_sshAgentUi; const QScopedPointer m_historyUi; + const QScopedPointer m_customData; + QWidget* const m_mainWidget; QWidget* const m_advancedWidget; EditWidgetIcons* const m_iconsWidget; QWidget* const m_autoTypeWidget; +#ifdef WITH_XC_SSHAGENT QWidget* const m_sshAgentWidget; +#endif EditWidgetProperties* const m_editWidgetProperties; QWidget* const m_historyWidget; EntryAttributes* const m_entryAttributes; diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 2abfa5544..e74cc6cfd 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -23,14 +23,40 @@ #include "gui/EditWidgetIcons.h" #include "gui/EditWidgetProperties.h" +#ifdef WITH_XC_KEESHARE +#include "keeshare/group/EditGroupPageKeeShare.h" +#endif + +class EditGroupWidget::ExtraPage +{ +public: + ExtraPage(IEditGroupPage* page, QWidget* widget) + : editPage(page) + , widget(widget) + { + } + + void set(Group* temporaryGroup) const + { + editPage->set(widget, temporaryGroup); + } + + void assign() const + { + editPage->assign(widget); + } + +private: + QSharedPointer editPage; + QWidget* widget; +}; + EditGroupWidget::EditGroupWidget(QWidget* parent) : EditWidget(parent) , m_mainUi(new Ui::EditGroupWidgetMain()) , m_editGroupWidgetMain(new QWidget()) , m_editGroupWidgetIcons(new EditWidgetIcons()) , m_editWidgetProperties(new EditWidgetProperties()) - , m_group(nullptr) - , m_database(nullptr) { m_mainUi->setupUi(m_editGroupWidgetMain); @@ -52,6 +78,10 @@ EditGroupWidget::EditGroupWidget(QWidget* parent) SIGNAL(messageEditEntry(QString, MessageWidget::MessageType)), SLOT(showMessage(QString, MessageWidget::MessageType))); connect(m_editGroupWidgetIcons, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage())); + +#ifdef WITH_XC_KEESHARE + addEditPage(new EditGroupPageKeeShare(this)); +#endif } EditGroupWidget::~EditGroupWidget() @@ -63,6 +93,8 @@ void EditGroupWidget::loadGroup(Group* group, bool create, Database* database) m_group = group; m_database = database; + m_temporaryGroup.reset(group->clone(Entry::CloneNoFlags, Group::CloneNoFlags)); + if (create) { setHeadline(tr("Add group")); } else { @@ -91,12 +123,15 @@ void EditGroupWidget::loadGroup(Group* group, bool create, Database* database) m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence()); IconStruct iconStruct; - iconStruct.uuid = group->iconUuid(); - iconStruct.number = group->iconNumber(); - m_editGroupWidgetIcons->load(group->uuid(), database, iconStruct); + iconStruct.uuid = m_temporaryGroup->iconUuid(); + iconStruct.number = m_temporaryGroup->iconNumber(); + m_editGroupWidgetIcons->load(m_temporaryGroup->uuid(), m_database, iconStruct); + m_editWidgetProperties->setFields(m_temporaryGroup->timeInfo(), m_temporaryGroup->uuid()); + m_editWidgetProperties->setCustomData(m_temporaryGroup->customData()); - m_editWidgetProperties->setFields(group->timeInfo(), group->uuid()); - m_editWidgetProperties->setCustomData(group->customData()); + for (const ExtraPage& page : asConst(m_extraPages)) { + page.set(m_temporaryGroup.data()); + } setCurrentPage(0); @@ -112,50 +147,61 @@ void EditGroupWidget::save() void EditGroupWidget::apply() { - m_group->setName(m_mainUi->editName->text()); - m_group->setNotes(m_mainUi->editNotes->toPlainText()); - m_group->setExpires(m_mainUi->expireCheck->isChecked()); - m_group->setExpiryTime(m_mainUi->expireDatePicker->dateTime().toUTC()); + m_temporaryGroup->setName(m_mainUi->editName->text()); + m_temporaryGroup->setNotes(m_mainUi->editNotes->toPlainText()); + m_temporaryGroup->setExpires(m_mainUi->expireCheck->isChecked()); + m_temporaryGroup->setExpiryTime(m_mainUi->expireDatePicker->dateTime().toUTC()); - m_group->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex())); - m_group->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex())); - - m_group->customData()->copyDataFrom(m_editWidgetProperties->customData()); + m_temporaryGroup->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex())); + m_temporaryGroup->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex())); if (m_mainUi->autoTypeSequenceInherit->isChecked()) { - m_group->setDefaultAutoTypeSequence(QString()); + m_temporaryGroup->setDefaultAutoTypeSequence(QString()); } else { - m_group->setDefaultAutoTypeSequence(m_mainUi->autoTypeSequenceCustomEdit->text()); + m_temporaryGroup->setDefaultAutoTypeSequence(m_mainUi->autoTypeSequenceCustomEdit->text()); } IconStruct iconStruct = m_editGroupWidgetIcons->state(); if (iconStruct.number < 0) { - m_group->setIcon(Group::DefaultIconNumber); + m_temporaryGroup->setIcon(Group::DefaultIconNumber); } else if (iconStruct.uuid.isNull()) { - m_group->setIcon(iconStruct.number); + m_temporaryGroup->setIcon(iconStruct.number); } else { - m_group->setIcon(iconStruct.uuid); + m_temporaryGroup->setIcon(iconStruct.uuid); } + + for (const ExtraPage& page : asConst(m_extraPages)) { + page.assign(); + } + + // Icons add/remove are applied globally outside the transaction! + m_group->copyDataFrom(m_temporaryGroup.data()); } void EditGroupWidget::cancel() { - if (!m_group->iconUuid().isNull() && !m_database->metadata()->containsCustomIcon(m_group->iconUuid())) { - m_group->setIcon(Entry::DefaultIconNumber); - } - clear(); emit editFinished(false); } void EditGroupWidget::clear() { - m_group = nullptr; - m_database = nullptr; + m_temporaryGroup.reset(nullptr); + m_database.clear(); + m_group.clear(); m_editGroupWidgetIcons->reset(); } +void EditGroupWidget::addEditPage(IEditGroupPage* page) +{ + QWidget* widget = page->createWidget(); + widget->setParent(this); + + m_extraPages.append(ExtraPage(page, widget)); + addPage(page->name(), page->icon(), widget); +} + void EditGroupWidget::addTriStateItems(QComboBox* comboBox, bool inheritDefault) { QString inheritDefaultString; diff --git a/src/gui/group/EditGroupWidget.h b/src/gui/group/EditGroupWidget.h index 87271871d..fcba1e5fc 100644 --- a/src/gui/group/EditGroupWidget.h +++ b/src/gui/group/EditGroupWidget.h @@ -24,6 +24,7 @@ #include "core/Group.h" #include "gui/EditWidget.h" +class CustomData; class EditWidgetIcons; class EditWidgetProperties; @@ -33,6 +34,19 @@ namespace Ui class EditWidget; } +class IEditGroupPage +{ +public: + virtual ~IEditGroupPage() + { + } + virtual QString name() = 0; + virtual QIcon icon() = 0; + virtual QWidget* createWidget() = 0; + virtual void set(QWidget* widget, Group* tempoaryGroup) = 0; + virtual void assign(QWidget* widget) = 0; +}; + class EditGroupWidget : public EditWidget { Q_OBJECT @@ -44,6 +58,8 @@ public: void loadGroup(Group* group, bool create, Database* database); void clear(); + void addEditPage(IEditGroupPage* page); + signals: void editFinished(bool accepted); void messageEditEntry(QString, MessageWidget::MessageType); @@ -60,12 +76,17 @@ private: Group::TriState triStateFromIndex(int index); const QScopedPointer m_mainUi; + QPointer m_editGroupWidgetMain; QPointer m_editGroupWidgetIcons; QPointer m_editWidgetProperties; - QPointer m_group; + QScopedPointer m_temporaryGroup; QPointer m_database; + QPointer m_group; + + class ExtraPage; + QList m_extraPages; Q_DISABLE_COPY(EditGroupWidget) }; diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index e8f51909f..690463724 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -25,6 +25,7 @@ #include "core/Group.h" #include "core/Metadata.h" #include "core/Tools.h" +#include "keeshare/KeeShare.h" GroupModel::GroupModel(Database* db, QObject* parent) : QAbstractItemModel(parent) @@ -125,13 +126,18 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const Group* group = groupFromIndex(index); if (role == Qt::DisplayRole) { - return group->name(); + QString nameTemplate = tr("%1", "Template for name without annotation"); +#ifdef WITH_XC_KEESHARE + nameTemplate = KeeShare::indicatorSuffix(group, nameTemplate); +#endif + return nameTemplate.arg(group->name()); } else if (role == Qt::DecorationRole) { - if (group->isExpired()) { - return databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex); - } else { - return group->iconScaledPixmap(); - } + QPixmap pixmap = group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex) + : group->iconScaledPixmap(); +#ifdef WITH_XC_KEESHARE + pixmap = KeeShare::indicatorBadge(group, pixmap); +#endif + return pixmap; } else if (role == Qt::FontRole) { QFont font; if (group->isExpired()) { diff --git a/src/keeshare/CMakeLists.txt b/src/keeshare/CMakeLists.txt new file mode 100644 index 000000000..30a6bc4e1 --- /dev/null +++ b/src/keeshare/CMakeLists.txt @@ -0,0 +1,19 @@ +if(WITH_XC_KEESHARE) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + + set(keeshare_SOURCES + SettingsPageKeeShare.cpp + SettingsWidgetKeeShare.cpp + DatabaseSettingsPageKeeShare.cpp + DatabaseSettingsWidgetKeeShare.cpp + group/EditGroupWidgetKeeShare.cpp + group/EditGroupPageKeeShare.cpp + KeeShare.cpp + KeeShareSettings.cpp + ShareObserver.cpp + Signature.cpp + ) + + add_library(keeshare STATIC ${keeshare_SOURCES}) + target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${QUAZIP_LIBRARIES} ${crypto_ssh_LIB}) +endif() diff --git a/src/keeshare/DatabaseSettingsPageKeeShare.cpp b/src/keeshare/DatabaseSettingsPageKeeShare.cpp new file mode 100644 index 000000000..1bd117327 --- /dev/null +++ b/src/keeshare/DatabaseSettingsPageKeeShare.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2018 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 "DatabaseSettingsPageKeeShare.h" + +#include "core/Database.h" +#include "core/FilePath.h" +#include "core/Group.h" +#include "keeshare/DatabaseSettingsWidgetKeeShare.h" +#include "keeshare/KeeShare.h" + +#include + +QString DatabaseSettingsPageKeeShare::name() +{ + return QApplication::tr("KeeShare"); +} + +QIcon DatabaseSettingsPageKeeShare::icon() +{ + return FilePath::instance()->icon("apps", "preferences-system-network-sharing"); +} + +QWidget* DatabaseSettingsPageKeeShare::createWidget() +{ + return new DatabaseSettingsWidgetKeeShare(); +} + +void DatabaseSettingsPageKeeShare::loadSettings(QWidget* widget, Database* db) +{ + DatabaseSettingsWidgetKeeShare* settingsWidget = reinterpret_cast(widget); + settingsWidget->loadSettings(db); +} + +void DatabaseSettingsPageKeeShare::saveSettings(QWidget* widget) +{ + DatabaseSettingsWidgetKeeShare* settingsWidget = reinterpret_cast(widget); + settingsWidget->saveSettings(); +} diff --git a/src/keeshare/DatabaseSettingsPageKeeShare.h b/src/keeshare/DatabaseSettingsPageKeeShare.h new file mode 100644 index 000000000..23a4d5f8a --- /dev/null +++ b/src/keeshare/DatabaseSettingsPageKeeShare.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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_DATABASESETTINGSPAGEKEESHARE_H +#define KEEPASSXC_DATABASESETTINGSPAGEKEESHARE_H + +#include +#include +#include + +#include "gui/dbsettings/DatabaseSettingsDialog.h" + +class DatabaseSettingsPageKeeShare : public IDatabaseSettingsPage +{ +public: + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget, Database* db) override; + void saveSettings(QWidget* widget) override; +}; + +#endif // KEEPASSXC_DATABASESETTINGSPAGEKEESHARE_H diff --git a/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp b/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp new file mode 100644 index 000000000..522aaa603 --- /dev/null +++ b/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp @@ -0,0 +1,72 @@ + +/* + * Copyright (C) 2018 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 "DatabaseSettingsWidgetKeeShare.h" +#include "ui_DatabaseSettingsWidgetKeeShare.h" + +#include "core/Database.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "keeshare/KeeShare.h" +#include "keeshare/KeeShareSettings.h" + +#include +#include + +DatabaseSettingsWidgetKeeShare::DatabaseSettingsWidgetKeeShare(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::DatabaseSettingsWidgetKeeShare()) +{ + m_ui->setupUi(this); +} + +DatabaseSettingsWidgetKeeShare::~DatabaseSettingsWidgetKeeShare() +{ +} + +void DatabaseSettingsWidgetKeeShare::loadSettings(Database* db) +{ + m_db = db; + + m_referencesModel.reset(new QStandardItemModel()); + + m_referencesModel->setHorizontalHeaderLabels( + QStringList() << tr("Breadcrumb") << tr("Type") << tr("Path") << tr("Last Signer") << tr("Certificates")); + const QList groups = db->rootGroup()->groupsRecursive(true); + for (const Group* group : groups) { + if (!KeeShare::isShared(group)) { + continue; + } + const KeeShareSettings::Reference reference = KeeShare::referenceOf(group); + + QStringList hierarchy = group->hierarchy(); + hierarchy.removeFirst(); + QList row = QList(); + row << new QStandardItem(hierarchy.join(" > ")); + row << new QStandardItem(KeeShare::referenceTypeLabel(reference)); + row << new QStandardItem(reference.path); + m_referencesModel->appendRow(row); + } + + m_ui->sharedGroupsView->setModel(m_referencesModel.data()); +} + +void DatabaseSettingsWidgetKeeShare::saveSettings() +{ + // nothing to do - the tab is passive +} diff --git a/src/keeshare/DatabaseSettingsWidgetKeeShare.h b/src/keeshare/DatabaseSettingsWidgetKeeShare.h new file mode 100644 index 000000000..80ece51a4 --- /dev/null +++ b/src/keeshare/DatabaseSettingsWidgetKeeShare.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 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_DATABASESETTINGSWIDGETKEESHARE_H +#define KEEPASSXC_DATABASESETTINGSWIDGETKEESHARE_H + +#include +#include +#include + +class Database; + +class QStandardItemModel; + +namespace Ui +{ + class DatabaseSettingsWidgetKeeShare; +} + +class DatabaseSettingsWidgetKeeShare : public QWidget +{ + Q_OBJECT +public: + explicit DatabaseSettingsWidgetKeeShare(QWidget* parent = nullptr); + ~DatabaseSettingsWidgetKeeShare(); + + void loadSettings(Database* db); + void saveSettings(); + +private: + QScopedPointer m_ui; + + QScopedPointer m_referencesModel; + QPointer m_db; +}; + +#endif // KEEPASSXC_DATABASESETTINGSWIDGETKEESHARE_H diff --git a/src/keeshare/DatabaseSettingsWidgetKeeShare.ui b/src/keeshare/DatabaseSettingsWidgetKeeShare.ui new file mode 100644 index 000000000..85bf2083a --- /dev/null +++ b/src/keeshare/DatabaseSettingsWidgetKeeShare.ui @@ -0,0 +1,74 @@ + + + DatabaseSettingsWidgetKeeShare + + + + 0 + 0 + 327 + 379 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Sharing + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + Qt::ElideMiddle + + + true + + + true + + + false + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp new file mode 100644 index 000000000..b2c7fd143 --- /dev/null +++ b/src/keeshare/KeeShare.cpp @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2018 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 "KeeShare.h" +#include "core/Config.h" +#include "core/CustomData.h" +#include "core/Database.h" +#include "core/DatabaseIcons.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "crypto/ssh/OpenSSHKey.h" +#include "keeshare/ShareObserver.h" +#include "keeshare/Signature.h" + +#include +#include +#include + +namespace +{ + static const QString KeeShare_Reference("KeeShare/Reference"); + static const QString KeeShare_Own("KeeShare/Settings.own"); + static const QString KeeShare_Foreign("KeeShare/Settings.foreign"); + static const QString KeeShare_Active("KeeShare/Settings.active"); +} + +KeeShare* KeeShare::m_instance = nullptr; + +KeeShare* KeeShare::instance() +{ + if (!m_instance) { + qFatal("Race condition: instance wanted before it was initialized, this is a bug."); + } + + return m_instance; +} + +void KeeShare::init(QObject* parent) +{ + Q_ASSERT(!m_instance); + m_instance = new KeeShare(parent); +} + +KeeShareSettings::Own KeeShare::own() +{ + return KeeShareSettings::Own::deserialize(config()->get(KeeShare_Own).toString()); +} + +KeeShareSettings::Active KeeShare::active() +{ + return KeeShareSettings::Active::deserialize(config()->get(KeeShare_Active).toString()); +} + +KeeShareSettings::Foreign KeeShare::foreign() +{ + return KeeShareSettings::Foreign::deserialize(config()->get(KeeShare_Foreign).toString()); +} + +void KeeShare::setForeign(const KeeShareSettings::Foreign& foreign) +{ + config()->set(KeeShare_Foreign, KeeShareSettings::Foreign::serialize(foreign)); +} + +void KeeShare::setActive(const KeeShareSettings::Active& active) +{ + config()->set(KeeShare_Active, KeeShareSettings::Active::serialize(active)); +} + +void KeeShare::setOwn(const KeeShareSettings::Own& own) +{ + config()->set(KeeShare_Own, KeeShareSettings::Own::serialize(own)); +} + +bool KeeShare::isShared(const Group* group) +{ + return group->customData()->contains(KeeShare_Reference); +} + +KeeShareSettings::Reference KeeShare::referenceOf(const Group* group) +{ + static const KeeShareSettings::Reference s_emptyReference; + const CustomData* customData = group->customData(); + if (!customData->contains(KeeShare_Reference)) { + return s_emptyReference; + } + const auto encoded = customData->value(KeeShare_Reference); + const auto serialized = QString::fromUtf8(QByteArray::fromBase64(encoded.toLatin1())); + KeeShareSettings::Reference reference = KeeShareSettings::Reference::deserialize(serialized); + if (reference.isNull()) { + qWarning("Invalid sharing reference detected - sharing disabled"); + return s_emptyReference; + } + return reference; +} + +void KeeShare::setReferenceTo(Group* group, const KeeShareSettings::Reference& reference) +{ + CustomData* customData = group->customData(); + if (reference.isNull()) { + customData->remove(KeeShare_Reference); + return; + } + const auto serialized = KeeShareSettings::Reference::serialize(reference); + const auto encoded = serialized.toUtf8().toBase64(); + customData->set(KeeShare_Reference, encoded); +} + +QPixmap KeeShare::indicatorBadge(const Group* group, QPixmap pixmap) +{ + if (!isShared(group)) { + return pixmap; + } + const auto reference = KeeShare::referenceOf(group); + const auto active = KeeShare::active(); + const bool enabled = (reference.isImporting() && active.in) || (reference.isExporting() && active.out); + const QPixmap badge = enabled ? databaseIcons()->iconPixmap(DatabaseIcons::SharedIconIndex) + : databaseIcons()->iconPixmap(DatabaseIcons::UnsharedIconIndex); + QImage canvas = pixmap.toImage(); + const QRectF target(canvas.width() * 0.4, canvas.height() * 0.4, canvas.width() * 0.6, canvas.height() * 0.6); + QPainter painter(&canvas); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.drawPixmap(target, badge, badge.rect()); + pixmap.convertFromImage(canvas); + return pixmap; +} + +QString KeeShare::referenceTypeLabel(const KeeShareSettings::Reference& reference) +{ + switch (reference.type) { + case KeeShareSettings::Inactive: + return tr("Disabled share"); + case KeeShareSettings::ImportFrom: + return tr("Import from"); + case KeeShareSettings::ExportTo: + return tr("Export to"); + case KeeShareSettings::SynchronizeWith: + return tr("Synchronize with"); + } + return ""; +} + +QString KeeShare::indicatorSuffix(const Group* group, const QString& text) +{ + // we not adjust the display name for now - it's just an alternative to the icon + Q_UNUSED(group); + return text; +} + +void KeeShare::connectDatabase(Database* newDb, Database* oldDb) +{ + if (oldDb && m_observersByDatabase.contains(oldDb)) { + QPointer observer = m_observersByDatabase.take(oldDb); + if (observer) { + delete observer; + } + } + + if (newDb && !m_observersByDatabase.contains(newDb)) { + QPointer observer(new ShareObserver(newDb, newDb)); + m_observersByDatabase[newDb] = observer; + connect(observer.data(), + SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), + this, + SLOT(emitSharingMessage(QString, MessageWidget::MessageType))); + } +} + +void KeeShare::handleDatabaseOpened(Database* db) +{ + QPointer observer = m_observersByDatabase.value(db); + if (observer) { + observer->handleDatabaseOpened(); + } +} + +void KeeShare::handleDatabaseSaved(Database* db) +{ + QPointer observer = m_observersByDatabase.value(db); + if (observer) { + observer->handleDatabaseSaved(); + } +} + +void KeeShare::emitSharingMessage(const QString& message, KMessageWidget::MessageType type) +{ + QObject* observer = sender(); + Database* db = m_databasesByObserver.value(observer); + if (db) { + emit sharingMessage(db, message, type); + } +} + +void KeeShare::handleDatabaseDeleted(QObject* db) +{ + auto observer = m_observersByDatabase.take(db); + if (observer) { + m_databasesByObserver.remove(observer); + } +} + +void KeeShare::handleObserverDeleted(QObject* observer) +{ + auto database = m_databasesByObserver.take(observer); + if (database) { + m_observersByDatabase.remove(database); + } +} + +void KeeShare::handleSettingsChanged(const QString& key) +{ + if (key == KeeShare_Active) { + emit activeChanged(); + } +} + +KeeShare::KeeShare(QObject* parent) + : QObject(parent) +{ + connect(config(), SIGNAL(changed(QString)), this, SLOT(handleSettingsChanged(QString))); +} diff --git a/src/keeshare/KeeShare.h b/src/keeshare/KeeShare.h new file mode 100644 index 000000000..cd4d538f0 --- /dev/null +++ b/src/keeshare/KeeShare.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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_KEESHARE_H +#define KEEPASSXC_KEESHARE_H + +#include +#include + +#include "gui/MessageWidget.h" +#include "keeshare/KeeShareSettings.h" + +class Group; +class Database; +class ShareObserver; +class QXmlStreamWriter; +class QXmlStreamReader; + +class KeeShare : public QObject +{ + Q_OBJECT +public: + static KeeShare* instance(); + static void init(QObject* parent); + + static QString indicatorSuffix(const Group* group, const QString& text); + static QPixmap indicatorBadge(const Group* group, QPixmap pixmap); + + static bool isShared(const Group* group); + + static KeeShareSettings::Own own(); + static KeeShareSettings::Active active(); + static KeeShareSettings::Foreign foreign(); + static void setForeign(const KeeShareSettings::Foreign& foreign); + static void setActive(const KeeShareSettings::Active& active); + static void setOwn(const KeeShareSettings::Own& own); + + static KeeShareSettings::Reference referenceOf(const Group* group); + static void setReferenceTo(Group* group, const KeeShareSettings::Reference& reference); + static QString referenceTypeLabel(const KeeShareSettings::Reference& reference); + + void connectDatabase(Database* newDb, Database* oldDb); + void handleDatabaseOpened(Database* db); + void handleDatabaseSaved(Database* db); + +signals: + void activeChanged(); + void sharingMessage(Database*, QString, MessageWidget::MessageType); + +private slots: + void emitSharingMessage(const QString&, MessageWidget::MessageType); + void handleDatabaseDeleted(QObject*); + void handleObserverDeleted(QObject*); + void handleSettingsChanged(const QString&); + +private: + static KeeShare* m_instance; + + explicit KeeShare(QObject* parent); + + QMap> m_observersByDatabase; + QMap> m_databasesByObserver; +}; + +#endif // KEEPASSXC_KEESHARE_H diff --git a/src/keeshare/KeeShareSettings.cpp b/src/keeshare/KeeShareSettings.cpp new file mode 100644 index 000000000..a1fcfac37 --- /dev/null +++ b/src/keeshare/KeeShareSettings.cpp @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2018 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 "KeeShareSettings.h" +#include "core/CustomData.h" +#include "core/Database.h" +#include "core/DatabaseIcons.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "crypto/ssh/OpenSSHKey.h" +#include "keeshare/Signature.h" + +#include +#include +#include + +namespace KeeShareSettings +{ + namespace + { + Certificate packCertificate(const OpenSSHKey& key, bool verified, const QString& signer) + { + KeeShareSettings::Certificate extracted; + extracted.trusted = verified; + extracted.signer = signer; + Q_ASSERT(key.type() == "ssh-rsa"); + extracted.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key); + return extracted; + } + + Key packKey(const OpenSSHKey& key) + { + KeeShareSettings::Key extracted; + Q_ASSERT(key.type() == "ssh-rsa"); + extracted.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Private, key); + return extracted; + } + + OpenSSHKey unpackKey(const Key& sign) + { + if (sign.key.isEmpty()) { + return OpenSSHKey(); + } + OpenSSHKey key = OpenSSHKey::restoreFromBinary(OpenSSHKey::Private, sign.key); + Q_ASSERT(key.type() == "ssh-rsa"); + return key; + } + + OpenSSHKey unpackCertificate(const Certificate& certificate) + { + if (certificate.key.isEmpty()) { + return OpenSSHKey(); + } + OpenSSHKey key = OpenSSHKey::restoreFromBinary(OpenSSHKey::Public, certificate.key); + Q_ASSERT(key.type() == "ssh-rsa"); + return key; + } + + QString xmlSerialize(std::function specific) + { + QString buffer; + QXmlStreamWriter writer(&buffer); + + writer.setCodec(QTextCodec::codecForName("UTF-8")); + writer.setAutoFormatting(true); + writer.setAutoFormattingIndent(2); + + writer.writeStartDocument(); + writer.writeStartElement("KeeShare"); + writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema"); + writer.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); + specific(writer); + writer.writeEndElement(); + writer.writeEndElement(); + writer.writeEndDocument(); + return buffer; + } + + void xmlDeserialize(const QString& raw, std::function specific) + { + QXmlStreamReader reader(raw); + if (!reader.readNextStartElement() || reader.qualifiedName() != "KeeShare") { + return; + } + specific(reader); + } + } + + void Certificate::serialize(QXmlStreamWriter& writer, const Certificate& certificate) + { + if (certificate.isNull()) { + return; + } + writer.writeStartElement("Signer"); + writer.writeCharacters(certificate.signer); + writer.writeEndElement(); + writer.writeStartElement("Trusted"); + writer.writeCharacters(certificate.trusted ? "True" : "False"); + writer.writeEndElement(); + writer.writeStartElement("Key"); + writer.writeCharacters(certificate.key.toBase64()); + writer.writeEndElement(); + } + + bool Certificate::operator==(const Certificate& other) const + { + return trusted == other.trusted && key == other.key && signer == other.signer; + } + + bool Certificate::operator!=(const Certificate& other) const + { + return !operator==(other); + } + + bool Certificate::isNull() const + { + return !trusted && key.isEmpty() && signer.isEmpty(); + } + + QString Certificate::fingerprint() const + { + if (isNull()) { + return {}; + } + return sshKey().fingerprint(); + } + + OpenSSHKey Certificate::sshKey() const + { + return unpackCertificate(*this); + } + + QString Certificate::publicKey() const + { + if (isNull()) { + return {}; + } + return sshKey().publicKey(); + } + + Certificate Certificate::deserialize(QXmlStreamReader& reader) + { + Certificate certificate; + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Signer") { + certificate.signer = reader.readElementText(); + } else if (reader.name() == "Trusted") { + certificate.trusted = reader.readElementText() == "True"; + } else if (reader.name() == "Key") { + certificate.key = QByteArray::fromBase64(reader.readElementText().toLatin1()); + } + } + return certificate; + } + + bool Key::operator==(const Key& other) const + { + return key == other.key; + } + + bool Key::operator!=(const Key& other) const + { + return !operator==(other); + } + + bool Key::isNull() const + { + return key.isEmpty(); + } + + QString Key::privateKey() const + { + if (isNull()) { + return {}; + } + return sshKey().privateKey(); + } + + OpenSSHKey Key::sshKey() const + { + return unpackKey(*this); + } + + void Key::serialize(QXmlStreamWriter& writer, const Key& key) + { + if (key.isNull()) { + return; + } + writer.writeCharacters(key.key.toBase64()); + } + + Key Key::deserialize(QXmlStreamReader& reader) + { + Key key; + key.key = QByteArray::fromBase64(reader.readElementText().toLatin1()); + return key; + } + + Own Own::generate() + { + OpenSSHKey key = OpenSSHKey::generate(false); + key.openKey(QString()); + Own own; + own.key = packKey(key); + const QString name = qgetenv("USER"); // + "@" + QHostInfo::localHostName(); + own.certificate = packCertificate(key, true, name); + return own; + } + + QString Active::serialize(const Active& active) + { + return xmlSerialize([&](QXmlStreamWriter& writer) { + writer.writeStartElement("Active"); + if (active.in) { + writer.writeEmptyElement("Import"); + } + if (active.out) { + writer.writeEmptyElement("Export"); + } + writer.writeEndElement(); + }); + } + + Active Active::deserialize(const QString& raw) + { + Active active; + xmlDeserialize(raw, [&](QXmlStreamReader& reader) { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Active") { + while (reader.readNextStartElement()) { + if (reader.name() == "Import") { + active.in = true; + reader.skipCurrentElement(); + } else if (reader.name() == "Export") { + active.out = true; + reader.skipCurrentElement(); + } else { + break; + } + } + } else { + ::qWarning() << "Unknown KeeShareSettings element" << reader.name(); + reader.skipCurrentElement(); + } + } + }); + return active; + } + + bool Own::operator==(const Own& other) const + { + return key == other.key && certificate == other.certificate; + } + + bool Own::operator!=(const Own& other) const + { + return !operator==(other); + } + + QString Own::serialize(const Own& own) + { + return xmlSerialize([&](QXmlStreamWriter& writer) { + writer.writeStartElement("PrivateKey"); + Key::serialize(writer, own.key); + writer.writeEndElement(); + writer.writeStartElement("PublicKey"); + Certificate::serialize(writer, own.certificate); + writer.writeEndElement(); + }); + } + + Own Own::deserialize(const QString& raw) + { + Own own; + xmlDeserialize(raw, [&](QXmlStreamReader& reader) { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "PrivateKey") { + own.key = Key::deserialize(reader); + } else if (reader.name() == "PublicKey") { + own.certificate = Certificate::deserialize(reader); + } else { + ::qWarning() << "Unknown KeeShareSettings element" << reader.name(); + reader.skipCurrentElement(); + } + } + }); + return own; + } + + QString Foreign::serialize(const Foreign& foreign) + { + return xmlSerialize([&](QXmlStreamWriter& writer) { + writer.writeStartElement("Foreign"); + for (const Certificate& certificate : foreign.certificates) { + writer.writeStartElement("Certificate"); + Certificate::serialize(writer, certificate); + writer.writeEndElement(); + } + writer.writeEndElement(); + }); + } + + Foreign Foreign::deserialize(const QString& raw) + { + Foreign foreign; + xmlDeserialize(raw, [&](QXmlStreamReader& reader) { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Foreign") { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Certificate") { + foreign.certificates << Certificate::deserialize(reader); + } else { + ::qWarning() << "Unknown Cerificates element" << reader.name(); + reader.skipCurrentElement(); + } + } + } else { + ::qWarning() << "Unknown KeeShareSettings element" << reader.name(); + reader.skipCurrentElement(); + } + } + }); + return foreign; + } + + Reference::Reference() + : type(Inactive) + , uuid(QUuid::createUuid()) + { + } + + bool Reference::isNull() const + { + return type == Inactive && path.isEmpty() && password.isEmpty(); + } + + bool Reference::isValid() const + { + return type != Inactive && !path.isEmpty(); + } + + bool Reference::isExporting() const + { + return (type & ExportTo) != 0 && !path.isEmpty(); + } + + bool Reference::isImporting() const + { + return (type & ImportFrom) != 0 && !path.isEmpty(); + } + + bool Reference::operator<(const Reference& other) const + { + if (type != other.type) { + return type < other.type; + } + return path < other.path; + } + + bool Reference::operator==(const Reference& other) const + { + return path == other.path && uuid == other.uuid && password == other.password && type == other.type; + } + + QString Reference::serialize(const Reference& reference) + { + return xmlSerialize([&](QXmlStreamWriter& writer) { + writer.writeStartElement("Type"); + if ((reference.type & ImportFrom) == ImportFrom) { + writer.writeEmptyElement("Import"); + } + if ((reference.type & ExportTo) == ExportTo) { + writer.writeEmptyElement("Export"); + } + writer.writeEndElement(); + writer.writeStartElement("Group"); + writer.writeCharacters(reference.uuid.toRfc4122().toBase64()); + writer.writeEndElement(); + writer.writeStartElement("Path"); + writer.writeCharacters(reference.path.toUtf8().toBase64()); + writer.writeEndElement(); + writer.writeStartElement("Password"); + writer.writeCharacters(reference.password.toUtf8().toBase64()); + writer.writeEndElement(); + }); + } + + Reference Reference::deserialize(const QString& raw) + { + Reference reference; + xmlDeserialize(raw, [&](QXmlStreamReader& reader) { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Type") { + while (reader.readNextStartElement()) { + if (reader.name() == "Import") { + reference.type |= ImportFrom; + reader.skipCurrentElement(); + } else if (reader.name() == "Export") { + reference.type |= ExportTo; + reader.skipCurrentElement(); + } else { + break; + } + } + } else if (reader.name() == "Group") { + reference.uuid = QUuid::fromRfc4122(QByteArray::fromBase64(reader.readElementText().toLatin1())); + } else if (reader.name() == "Path") { + reference.path = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1())); + } else if (reader.name() == "Password") { + reference.password = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1())); + } else { + ::qWarning() << "Unknown Reference element" << reader.name(); + reader.skipCurrentElement(); + } + } + }); + return reference; + } + + QString Sign::serialize(const Sign& sign) + { + return xmlSerialize([&](QXmlStreamWriter& writer) { + writer.writeStartElement("Signature"); + writer.writeCharacters(sign.signature); + writer.writeEndElement(); + writer.writeStartElement("Certificate"); + Certificate::serialize(writer, sign.certificate); + writer.writeEndElement(); + }); + } + + Sign Sign::deserialize(const QString& raw) + { + Sign sign; + xmlDeserialize(raw, [&](QXmlStreamReader& reader) { + while (!reader.error() && reader.readNextStartElement()) { + if (reader.name() == "Signature") { + sign.signature = reader.readElementText(); + } else if (reader.name() == "Certificate") { + sign.certificate = KeeShareSettings::Certificate::deserialize(reader); + } else { + ::qWarning() << "Unknown Sign element" << reader.name(); + reader.skipCurrentElement(); + } + } + }); + return sign; + } +} diff --git a/src/keeshare/KeeShareSettings.h b/src/keeshare/KeeShareSettings.h new file mode 100644 index 000000000..94d67d1ca --- /dev/null +++ b/src/keeshare/KeeShareSettings.h @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018 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_KEESHARESETTINGS_H +#define KEEPASSXC_KEESHARESETTINGS_H + +#include +#include + +#include "crypto/ssh/OpenSSHKey.h" + +class CustomData; +class QXmlStreamWriter; +class QXmlStreamReader; + +namespace KeeShareSettings +{ + struct Certificate + { + QByteArray key; + QString signer; + bool trusted; + + bool operator==(const Certificate& other) const; + bool operator!=(const Certificate& other) const; + + Certificate() + : trusted(false) + { + } + + bool isNull() const; + QString fingerprint() const; + QString publicKey() const; + OpenSSHKey sshKey() const; + + static void serialize(QXmlStreamWriter& writer, const Certificate& certificate); + static Certificate deserialize(QXmlStreamReader& reader); + }; + + struct Key + { + QByteArray key; + + bool operator==(const Key& other) const; + bool operator!=(const Key& other) const; + + bool isNull() const; + QString privateKey() const; + OpenSSHKey sshKey() const; + + static void serialize(QXmlStreamWriter& writer, const Key& key); + static Key deserialize(QXmlStreamReader& reader); + }; + + struct Active + { + bool in; + bool out; + Active() + : in(false) + , out(false) + { + } + bool isNull() const + { + return !in && !out; + } + + static QString serialize(const Active& active); + static Active deserialize(const QString& raw); + }; + + struct Own + { + Key key; + Certificate certificate; + + bool operator==(const Own& other) const; + bool operator!=(const Own& other) const; + bool isNull() const + { + return key.isNull() && certificate.isNull(); + } + + static QString serialize(const Own& own); + static Own deserialize(const QString& raw); + static Own generate(); + }; + + struct Foreign + { + QList certificates; + + bool isNull() const + { + return certificates.isEmpty(); + } + + static QString serialize(const Foreign& foreign); + static Foreign deserialize(const QString& raw); + }; + + struct Sign + { + QString signature; + Certificate certificate; + + bool isNull() const + { + return signature.isEmpty() && certificate.isNull(); + } + + static QString serialize(const Sign& sign); + static Sign deserialize(const QString& raw); + }; + + enum TypeFlag + { + Inactive = 0, + ImportFrom = 1 << 0, + ExportTo = 1 << 1, + SynchronizeWith = ImportFrom | ExportTo + }; + Q_DECLARE_FLAGS(Type, TypeFlag) + + struct Reference + { + Type type; + QUuid uuid; + QString path; + QString password; + + Reference(); + bool isNull() const; + bool isValid() const; + bool isExporting() const; + bool isImporting() const; + bool operator<(const Reference& other) const; + bool operator==(const Reference& other) const; + + static QString serialize(const Reference& reference); + static Reference deserialize(const QString& raw); + }; +}; + +#endif // KEEPASSXC_KEESHARESETTINGS_H diff --git a/src/keeshare/SettingsPageKeeShare.cpp b/src/keeshare/SettingsPageKeeShare.cpp new file mode 100644 index 000000000..04a0f1058 --- /dev/null +++ b/src/keeshare/SettingsPageKeeShare.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 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 "SettingsPageKeeShare.h" + +#include "core/Database.h" +#include "core/FilePath.h" +#include "core/Group.h" +#include "gui/DatabaseTabWidget.h" +#include "gui/MessageWidget.h" +#include "keeshare/KeeShare.h" +#include "keeshare/SettingsWidgetKeeShare.h" +#include +#include + +SettingsPageKeeShare::SettingsPageKeeShare(DatabaseTabWidget* tabWidget) + : m_tabWidget(tabWidget) +{ +} + +QString SettingsPageKeeShare::name() +{ + return QApplication::tr("KeeShare"); +} + +QIcon SettingsPageKeeShare::icon() +{ + return FilePath::instance()->icon("apps", "preferences-system-network-sharing"); +} + +QWidget* SettingsPageKeeShare::createWidget() +{ + auto* widget = new SettingsWidgetKeeShare(); + QObject::connect(widget, + SIGNAL(settingsMessage(QString, MessageWidget::MessageType)), + m_tabWidget, + SIGNAL(messageGlobal(QString, MessageWidget::MessageType))); + return widget; +} + +void SettingsPageKeeShare::loadSettings(QWidget* widget) +{ + Q_UNUSED(widget); + SettingsWidgetKeeShare* settingsWidget = reinterpret_cast(widget); + settingsWidget->loadSettings(); +} + +void SettingsPageKeeShare::saveSettings(QWidget* widget) +{ + Q_UNUSED(widget); + SettingsWidgetKeeShare* settingsWidget = reinterpret_cast(widget); + return settingsWidget->saveSettings(); +} diff --git a/src/keeshare/SettingsPageKeeShare.h b/src/keeshare/SettingsPageKeeShare.h new file mode 100644 index 000000000..975f64393 --- /dev/null +++ b/src/keeshare/SettingsPageKeeShare.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 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_SETTINGSPAGEKEESHARE_H +#define KEEPASSXC_SETTINGSPAGEKEESHARE_H + +#include +#include +#include + +#include "gui/ApplicationSettingsWidget.h" + +class DatabaseTabWidget; + +class SettingsPageKeeShare : public ISettingsPage +{ +public: + SettingsPageKeeShare(DatabaseTabWidget* tabWidget); + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget) override; + void saveSettings(QWidget* widget) override; + +private: + QPointer m_tabWidget; +}; + +#endif // KEEPASSXC_SETTINGSPAGEKEESHARE_H diff --git a/src/keeshare/SettingsWidgetKeeShare.cpp b/src/keeshare/SettingsWidgetKeeShare.cpp new file mode 100644 index 000000000..7da48c4df --- /dev/null +++ b/src/keeshare/SettingsWidgetKeeShare.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2018 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 "SettingsWidgetKeeShare.h" +#include "ui_SettingsWidgetKeeShare.h" + +#include "core/Config.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "gui/FileDialog.h" +#include "keeshare/KeeShare.h" +#include "keeshare/KeeShareSettings.h" + +#include +#include + +SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::SettingsWidgetKeeShare()) +{ + m_ui->setupUi(this); + + connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString))); + + connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate())); + connect(m_ui->importOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(importCertificate())); + connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate())); + + connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates())); + connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates())); + connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates())); +} + +SettingsWidgetKeeShare::~SettingsWidgetKeeShare() +{ +} + +void SettingsWidgetKeeShare::loadSettings() +{ + const auto active = KeeShare::active(); + m_ui->enableExportCheckBox->setChecked(active.out); + m_ui->enableImportCheckBox->setChecked(active.in); + + m_own = KeeShare::own(); + updateOwnCertificate(); + + m_foreign = KeeShare::foreign(); + updateForeignCertificates(); +} + +void SettingsWidgetKeeShare::updateForeignCertificates() +{ + m_importedCertificateModel.reset(new QStandardItemModel()); + m_importedCertificateModel->setHorizontalHeaderLabels( + QStringList() << tr("Signer") << tr("Status") << tr("Fingerprint") << tr("Certificate")); + + for (const KeeShareSettings::Certificate& certificate : m_foreign.certificates) { + QStandardItem* signer = new QStandardItem(certificate.signer); + QStandardItem* verified = new QStandardItem(certificate.trusted ? tr("trusted") : tr("untrusted")); + QStandardItem* fingerprint = new QStandardItem(certificate.fingerprint()); + QStandardItem* key = new QStandardItem(certificate.publicKey()); + m_importedCertificateModel->appendRow(QList() << signer << verified << fingerprint << key); + } + + m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data()); +} + +void SettingsWidgetKeeShare::updateOwnCertificate() +{ + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); + m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); + m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); +} + +void SettingsWidgetKeeShare::saveSettings() +{ + KeeShareSettings::Active active; + active.out = m_ui->enableExportCheckBox->isChecked(); + active.in = m_ui->enableImportCheckBox->isChecked(); + // TODO HNH: This depends on the order of saving new data - a better model would be to + // store changes to the settings in a temporary object and check on the final values + // of this object (similar scheme to Entry) - this way we could validate the settings before save + if (active.in) { + emit settingsMessage(tr("Make sure to have a history size greater than 2 to prevent data loss when importing!"), MessageWidget::Warning); + } + + KeeShare::setOwn(m_own); + KeeShare::setForeign(m_foreign); + KeeShare::setActive(active); +} + +void SettingsWidgetKeeShare::setVerificationExporter(const QString& signer) +{ + m_own.certificate.signer = signer; + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); +} + +void SettingsWidgetKeeShare::generateCertificate() +{ + m_own = KeeShareSettings::Own::generate(); + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); + m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); + m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); +} + +void SettingsWidgetKeeShare::importCertificate() +{ + QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + const auto filetype = tr("key.share", "Filetype for KeeShare key"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); + QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, 0); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + file.open(QIODevice::ReadOnly); + QTextStream stream(&file); + m_own = KeeShareSettings::Own::deserialize(stream.readAll()); + file.close(); + config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); + + updateOwnCertificate(); +} + +void SettingsWidgetKeeShare::exportCertificate() +{ + if (KeeShare::own() != m_own) { + QMessageBox warning; + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(tr("Exporting changed certificate")); + warning.setText(tr("The exported certificate is not the same as the one in use. Do you want to export the current certificate?")); + auto yes = warning.addButton(QMessageBox::StandardButton::Yes); + auto no = warning.addButton(QMessageBox::StandardButton::No); + warning.setDefaultButton(no); + warning.exec(); + if (warning.clickedButton() != yes) { + return; + } + } + QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + const auto filetype = tr("key.share", "Filetype for KeeShare key"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); + QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype); + filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, 0, filetype, filename); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + file.open(QIODevice::Truncate | QIODevice::WriteOnly); + QTextStream stream(&file); + stream << KeeShareSettings::Own::serialize(m_own); + stream.flush(); + file.close(); + config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); +} + +void SettingsWidgetKeeShare::trustSelectedCertificates() +{ + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trusted = true; + } + + updateForeignCertificates(); +} + +void SettingsWidgetKeeShare::untrustSelectedCertificates() +{ + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trusted = false; + } + + updateForeignCertificates(); +} + +void SettingsWidgetKeeShare::removeSelectedCertificates() +{ + QList certificates = m_foreign.certificates; + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + certificates.removeOne(m_foreign.certificates[index.row()]); + } + m_foreign.certificates = certificates; + + updateForeignCertificates(); +} diff --git a/src/keeshare/SettingsWidgetKeeShare.h b/src/keeshare/SettingsWidgetKeeShare.h new file mode 100644 index 000000000..f68b76792 --- /dev/null +++ b/src/keeshare/SettingsWidgetKeeShare.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 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_SETTINGSWIDGETKEESHARE_H +#define KEEPASSXC_SETTINGSWIDGETKEESHARE_H + +#include +#include +#include + +#include "gui/MessageWidget.h" +#include "keeshare/KeeShareSettings.h" + +class Database; + +class QStandardItemModel; + +namespace Ui +{ + class SettingsWidgetKeeShare; +} + +class SettingsWidgetKeeShare : public QWidget +{ + Q_OBJECT +public: + explicit SettingsWidgetKeeShare(QWidget* parent = nullptr); + ~SettingsWidgetKeeShare(); + + void loadSettings(); + void saveSettings(); + +signals: + void settingsMessage(const QString&, MessageWidget::MessageType type); + +private slots: + void setVerificationExporter(const QString& signer); + + void generateCertificate(); + void importCertificate(); + void exportCertificate(); + + void trustSelectedCertificates(); + void untrustSelectedCertificates(); + void removeSelectedCertificates(); + +private: + void updateOwnCertificate(); + void updateForeignCertificates(); + + QScopedPointer m_ui; + + KeeShareSettings::Own m_own; + KeeShareSettings::Foreign m_foreign; + QScopedPointer m_importedCertificateModel; +}; + +#endif // KEEPASSXC_SETTINGSWIDGETKEESHARE_H diff --git a/src/keeshare/SettingsWidgetKeeShare.ui b/src/keeshare/SettingsWidgetKeeShare.ui new file mode 100644 index 000000000..c736bdedd --- /dev/null +++ b/src/keeshare/SettingsWidgetKeeShare.ui @@ -0,0 +1,249 @@ + + + SettingsWidgetKeeShare + + + + 0 + 0 + 327 + 423 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Active + + + + + + Allow export + + + + + + + Allow import + + + + + + + + + + + 0 + 0 + + + + Own certificate + + + + + + Fingerprint: + + + + + + + true + + + + + + + Certificate: + + + + + + + Signer + + + + + + + Key: + + + + + + + true + + + + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Generate + + + + + + + Import + + + + + + + Export + + + + + + + + + + + + Imported certificates + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + true + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + true + + + true + + + false + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Trust + + + + + + + Untrust + + + + + + + Remove + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp new file mode 100644 index 000000000..fb0b419fa --- /dev/null +++ b/src/keeshare/ShareObserver.cpp @@ -0,0 +1,637 @@ +/* + * Copyright (C) 2018 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 "ShareObserver.h" +#include "core/Clock.h" +#include "core/Config.h" +#include "core/CustomData.h" +#include "core/Database.h" +#include "core/DatabaseIcons.h" +#include "core/Entry.h" +#include "core/FilePath.h" +#include "core/FileWatcher.h" +#include "core/Group.h" +#include "core/Merger.h" +#include "core/Metadata.h" +#include "format/KeePass2Reader.h" +#include "format/KeePass2Writer.h" +#include "keeshare/KeeShare.h" +#include "keeshare/KeeShareSettings.h" +#include "keeshare/Signature.h" +#include "keys/PasswordKey.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ + static const QString KeeShare_Signature("container.share.signature"); + static const QString KeeShare_Container("container.share.kdbx"); + + enum Trust + { + None, + Invalid, + Single, + Lasting, + Known, + Own + }; + + QPair check(QByteArray& data, + const KeeShareSettings::Reference& reference, + const KeeShareSettings::Certificate& ownCertificate, + const QList& knownCertificates, + const KeeShareSettings::Sign& sign) + { + if (sign.signature.isEmpty()) { + QMessageBox warning; + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature")); + warning.setText(ShareObserver::tr("Do you want to import from unsigned container %1").arg(reference.path)); + auto yes = warning.addButton(ShareObserver::tr("Import once"), QMessageBox::ButtonRole::YesRole); + auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); + warning.setDefaultButton(no); + warning.exec(); + const auto trust = warning.clickedButton() == yes ? Single : None; + return qMakePair(trust, KeeShareSettings::Certificate()); + } + auto key = sign.certificate.sshKey(); + key.openKey(QString()); + const Signature signer; + if (!signer.verify(data, sign.signature, key)) { + const QFileInfo info(reference.path); + qCritical("Invalid signature for sharing container %s.", qPrintable(info.absoluteFilePath())); + return qMakePair(Invalid, KeeShareSettings::Certificate()); + } + if (ownCertificate.key == sign.certificate.key) { + return qMakePair(Own, ownCertificate); + } + for (const auto& certificate : knownCertificates) { + if (certificate.key == certificate.key && certificate.trusted) { + return qMakePair(Known, certificate); + } + } + + QMessageBox warning; + warning.setIcon(QMessageBox::Question); + warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2") + .arg(sign.certificate.signer) + .arg(sign.certificate.fingerprint())); + auto yes = warning.addButton(ShareObserver::tr("Import and trust"), QMessageBox::ButtonRole::YesRole); + auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); + warning.setDefaultButton(no); + warning.exec(); + if (warning.clickedButton() != yes) { + qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer)); + return qMakePair(None, sign.certificate); + } + return qMakePair(Lasting, sign.certificate); + } +} + +ShareObserver::ShareObserver(Database* db, QObject* parent) + : QObject(parent) + , m_db(db) + , m_fileWatcher(new BulkFileWatcher(this)) +{ + connect(KeeShare::instance(), SIGNAL(activeChanged()), this, SLOT(handleDatabaseChanged())); + + connect(m_db, SIGNAL(modified()), this, SLOT(handleDatabaseChanged())); + + connect(m_fileWatcher, SIGNAL(fileCreated(QString)), this, SLOT(handleFileCreated(QString))); + connect(m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(handleFileChanged(QString))); + connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), this, SLOT(handleFileRemoved(QString))); +} + +ShareObserver::~ShareObserver() +{ +} + +void ShareObserver::deinitialize() +{ + m_fileWatcher->clear(); + m_groupToReference.clear(); + m_referenceToGroup.clear(); +} + +void ShareObserver::reinitialize() +{ + struct Update + { + Group* group; + KeeShareSettings::Reference oldReference; + KeeShareSettings::Reference newReference; + }; + const auto active = KeeShare::active(); + QList updated; + QList groups = m_db->rootGroup()->groupsRecursive(true); + for (Group* group : groups) { + Update couple{group, m_groupToReference.value(group), KeeShare::referenceOf(group)}; + if (couple.oldReference == couple.newReference) { + continue; + } + m_groupToReference.remove(couple.group); + m_referenceToGroup.remove(couple.oldReference); + m_shareToGroup.remove(couple.oldReference.path); + if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting()) + || (active.out && couple.newReference.isExporting()))) { + m_groupToReference[couple.group] = couple.newReference; + m_referenceToGroup[couple.newReference] = couple.group; + m_shareToGroup[couple.newReference.path] = couple.group; + } + updated << couple; + } + + QStringList success; + QStringList warning; + QStringList error; + for (Update update : updated) { + if (!update.oldReference.path.isEmpty()) { + m_fileWatcher->removePath(update.oldReference.path); + } + if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { + m_fileWatcher->addPath(update.newReference.path); + } + + if (update.newReference.isImporting()) { + const Result result = this->importFromReferenceContainer(update.newReference.path); + if (!result.isValid()) { + // tolerable result - blocked import or missing source + continue; + } + + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + } + } + notifyAbout(success, warning, error); +} + +void ShareObserver::notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error) +{ + if (error.isEmpty() && warning.isEmpty() && success.isEmpty()) { + return; + } + + MessageWidget::MessageType type = MessageWidget::Positive; + if (!warning.isEmpty()) { + type = MessageWidget::Warning; + } + if (!error.isEmpty()) { + type = MessageWidget::Error; + } + emit sharingMessage((success + warning + error).join("\n"), type); +} + +void ShareObserver::handleDatabaseChanged() +{ + if (!m_db) { + Q_ASSERT(m_db); + return; + } + const auto active = KeeShare::active(); + if (!active.out && !active.in) { + deinitialize(); + } else { + reinitialize(); + } +} + +void ShareObserver::handleFileUpdated(const QString& path, Change change) +{ + switch (change) { + case Creation: + qDebug("File created %s", qPrintable(path)); + break; + case Update: + qDebug("File changed %s", qPrintable(path)); + break; + case Deletion: + qDebug("File deleted %s", qPrintable(path)); + break; + } + + const Result result = this->importFromReferenceContainer(path); + if (!result.isValid()) { + return; + } + QStringList success; + QStringList warning; + QStringList error; + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + notifyAbout(success, warning, error); +} + +void ShareObserver::handleFileCreated(const QString& path) +{ + handleFileUpdated(path, Creation); +} + +void ShareObserver::handleFileChanged(const QString& path) +{ + handleFileUpdated(path, Update); +} + +void ShareObserver::handleFileRemoved(const QString& path) +{ + handleFileUpdated(path, Deletion); +} + +ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +{ + const QFileInfo info(reference.path); + if (!info.exists()) { + qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); + return {reference.path, Result::Warning, tr("File does not exist")}; + } + QuaZip zip(info.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) { + qCritical("Unable to open file %s.", qPrintable(info.absoluteFilePath())); + return {reference.path, Result::Error, tr("File is not readable")}; + } + const auto expected = QSet() << KeeShare_Signature << KeeShare_Container; + const auto files = zip.getFileInfoList(); + QSet actual; + for (const auto& file : files) { + actual << file.name; + } + if (expected != actual) { + qCritical("Invalid sharing container %s.", qPrintable(info.absoluteFilePath())); + return {reference.path, Result::Error, tr("Invalid sharing container")}; + } + + zip.setCurrentFile(KeeShare_Signature); + QuaZipFile signatureFile(&zip); + signatureFile.open(QuaZipFile::ReadOnly); + QTextStream stream(&signatureFile); + + const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll()); + signatureFile.close(); + + zip.setCurrentFile(KeeShare_Container); + QuaZipFile databaseFile(&zip); + databaseFile.open(QuaZipFile::ReadOnly); + auto payload = databaseFile.readAll(); + databaseFile.close(); + QBuffer buffer(&payload); + buffer.open(QIODevice::ReadOnly); + + KeePass2Reader reader; + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + auto* sourceDb = reader.readDatabase(&buffer, key); + if (reader.hasError()) { + qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); + return {reference.path, Result::Error, reader.errorString()}; + } + auto foreign = KeeShare::foreign(); + auto own = KeeShare::own(); + auto trusted = check(payload, reference, own.certificate, foreign.certificates, sign); + switch (trusted.first) { + case None: + qWarning("Prevent untrusted import"); + return {reference.path, Result::Warning, tr("Untrusted import prevented")}; + + case Invalid: + qCritical("Prevent untrusted import"); + return {reference.path, Result::Error, tr("Untrusted import prevented")}; + + case Known: + case Lasting: { + bool found = false; + for (KeeShareSettings::Certificate& knownCertificate : foreign.certificates) { + if (knownCertificate.key == trusted.second.key) { + knownCertificate.signer = trusted.second.signer; + knownCertificate.trusted = true; + found = true; + } + } + if (!found) { + foreign.certificates << trusted.second; + // we need to update with the new signer + KeeShare::setForeign(foreign); + } + } + [[gnu::fallthrough]]; + case Single: + case Own: { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful import")}; + } + return {}; + } + default: + Q_ASSERT(false); + return {}; + } +} + +ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString& path) +{ + if (!KeeShare::active().in) { + return {}; + } + auto shareGroup = m_shareToGroup.value(path); + if (!shareGroup) { + qWarning("Source for %s does not exist", qPrintable(path)); + Q_ASSERT(shareGroup); + return {}; + } + const auto reference = KeeShare::referenceOf(shareGroup); + if (reference.type == KeeShareSettings::Inactive) { + qDebug("Ignore change of inactive reference %s", qPrintable(reference.path)); + return {}; + } + if (reference.type == KeeShareSettings::ExportTo) { + qDebug("Ignore change of export reference %s", qPrintable(reference.path)); + return {}; + } + Q_ASSERT(shareGroup->database() == m_db); + Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid())); + return importContainerInto(reference, shareGroup); +} + +void ShareObserver::resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb) +{ + for (const auto& attribute : EntryAttributes::DefaultAttributes) { + const auto standardValue = targetEntry->attributes()->value(attribute); + const auto type = targetEntry->placeholderType(standardValue); + if (type != Entry::PlaceholderType::Reference) { + // No reference to resolve + continue; + } + const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue); + if (referencedTargetEntry) { + // References is within scope, no resolving needed + continue; + } + // We could do more sophisticated **** trying to point the reference to the next in-scope reference + // but those cases with high propability constructed examples and very rare in real usage + const auto* sourceReference = sourceDb->resolveEntry(targetEntry->uuid()); + const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue); + targetEntry->setUpdateTimeinfo(false); + targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute)); + targetEntry->setUpdateTimeinfo(true); + } +} + +Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot) +{ + const auto* sourceDb = sourceRoot->database(); + auto* targetDb = new Database(); + targetDb->metadata()->setRecycleBinEnabled(false); + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + + // Copy the source root as the root of the export database, memory manage the old root node + auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags); + const bool updateTimeinfo = targetRoot->canUpdateTimeinfo(); + targetRoot->setUpdateTimeinfo(false); + KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference()); + targetRoot->setUpdateTimeinfo(updateTimeinfo); + const auto sourceEntries = sourceRoot->entriesRecursive(false); + for (const Entry* sourceEntry : sourceEntries) { + auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); + const bool updateTimeinfo = targetEntry->canUpdateTimeinfo(); + targetEntry->setUpdateTimeinfo(false); + targetEntry->setGroup(targetRoot); + targetEntry->setUpdateTimeinfo(updateTimeinfo); + const auto iconUuid = targetEntry->iconUuid(); + if (!iconUuid.isNull()) { + targetDb->metadata()->addCustomIcon(iconUuid, sourceEntry->icon()); + } + } + + targetDb->setKey(key); + auto* obsoleteRoot = targetDb->rootGroup(); + targetDb->setRootGroup(targetRoot); + delete obsoleteRoot; + + targetDb->metadata()->setName(sourceRoot->name()); + + // Push all deletions of the source database to the target + // simple moving out of a share group will not trigger a deletion in the + // target - a more elaborate mechanism may need the use of another custom + // attribute to share unshared entries from the target db + for (const auto& object : sourceDb->deletedObjects()) { + targetDb->addDeletedObject(object); + } + for (auto* targetEntry : targetRoot->entriesRecursive(false)) { + if (targetEntry->hasReferences()) { + resolveReferenceAttributes(targetEntry, sourceDb); + } + } + return targetDb; +} + +const Database* ShareObserver::database() const +{ + return m_db; +} + +Database* ShareObserver::database() +{ + return m_db; +} + +void ShareObserver::handleDatabaseOpened() +{ + if (!m_db) { + Q_ASSERT(m_db); + return; + } + const auto active = KeeShare::active(); + if (!active.in && !active.out) { + deinitialize(); + } else { + reinitialize(); + } +} + +QList ShareObserver::exportIntoReferenceContainers() +{ + QList results; + const auto own = KeeShare::own(); + const auto groups = m_db->rootGroup()->groupsRecursive(true); + for (const auto* group : groups) { + const auto reference = KeeShare::referenceOf(group); + if (!reference.isExporting()) { + continue; + } + + m_fileWatcher->ignoreFileChanges(reference.path); + QScopedPointer targetDb(exportIntoContainer(reference, group)); + QByteArray bytes; + { + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + KeePass2Writer writer; + writer.writeDatabase(&buffer, targetDb.data()); + if (writer.hasError()) { + qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); + results << Result{reference.path, Result::Error, writer.errorString()}; + m_fileWatcher->observeFileChanges(true); + continue; + } + } + QuaZip zip(reference.path); + zip.setFileNameCodec("UTF-8"); + const bool zipOpened = zip.open(QuaZip::mdCreate); + if (!zipOpened) { + ::qWarning("Opening export file failed: %d", zip.getZipError()); + results << Result{reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; + m_fileWatcher->observeFileChanges(true); + continue; + } + { + QuaZipFile file(&zip); + const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); + if (!signatureOpened) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + results << Result{reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + m_fileWatcher->observeFileChanges(true); + continue; + } + QTextStream stream(&file); + KeeShareSettings::Sign sign; + auto sshKey = own.key.sshKey(); + sshKey.openKey(QString()); + const Signature signer; + sign.signature = signer.create(bytes, sshKey); + sign.certificate = own.certificate; + stream << KeeShareSettings::Sign::serialize(sign); + stream.flush(); + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + results << Result{reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + m_fileWatcher->observeFileChanges(true); + continue; + } + file.close(); + } + { + QuaZipFile file(&zip); + const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); + if (!dbOpened) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + results << Result{reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + m_fileWatcher->observeFileChanges(true); + continue; + } + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + results << Result{reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + m_fileWatcher->observeFileChanges(true); + continue; + } + file.write(bytes); + file.close(); + } + zip.close(); + + m_fileWatcher->observeFileChanges(true); + results << Result{reference.path}; + } + return results; +} + +void ShareObserver::handleDatabaseSaved() +{ + if (!KeeShare::active().out) { + return; + } + QStringList error; + QStringList warning; + QStringList success; + const auto results = exportIntoReferenceContainers(); + for (const Result& result : results) { + if (!result.isValid()) { + Q_ASSERT(result.isValid()); + continue; + } + if (result.isError()) { + error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Export to %1").arg(result.path); + } + } + notifyAbout(success, warning, error); +} + +ShareObserver::Result::Result(const QString& path, ShareObserver::Result::Type type, const QString& message) + : path(path) + , type(type) + , message(message) +{ +} + +bool ShareObserver::Result::isValid() const +{ + return !path.isEmpty() || !message.isEmpty() || !message.isEmpty() || !message.isEmpty(); +} + +bool ShareObserver::Result::isError() const +{ + return !message.isEmpty() && type == Error; +} + +bool ShareObserver::Result::isInfo() const +{ + return !message.isEmpty() && type == Info; +} + +bool ShareObserver::Result::isWarning() const +{ + return !message.isEmpty() && type == Warning; +} diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h new file mode 100644 index 000000000..ae7734ea0 --- /dev/null +++ b/src/keeshare/ShareObserver.h @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 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_SHAREOBSERVER_H +#define KEEPASSXC_SHAREOBSERVER_H + +#include +#include +#include +#include +#include + +#include "gui/MessageWidget.h" +#include "keeshare/KeeShareSettings.h" + +class BulkFileWatcher; +class Entry; +class Group; +class CustomData; +class Database; + +class ShareObserver : public QObject +{ + Q_OBJECT + +public: + explicit ShareObserver(Database* db, QObject* parent = nullptr); + ~ShareObserver(); + + void handleDatabaseSaved(); + void handleDatabaseOpened(); + + const Database* database() const; + Database* database(); + +signals: + void sharingMessage(QString, MessageWidget::MessageType); + +public slots: + void handleDatabaseChanged(); + +private slots: + void handleFileCreated(const QString& path); + void handleFileChanged(const QString& path); + void handleFileRemoved(const QString& path); + +private: + enum Change + { + Creation, + Update, + Deletion + }; + + struct Result + { + enum Type + { + Success, + Info, + Warning, + Error + }; + + QString path; + Type type; + QString message; + + Result(const QString& path = QString(), Type type = Success, const QString& message = QString()); + + bool isValid() const; + bool isError() const; + bool isWarning() const; + bool isInfo() const; + }; + + static void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb); + + static Database* exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot); + static Result importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); + + Result importFromReferenceContainer(const QString& path); + QList exportIntoReferenceContainers(); + void deinitialize(); + void reinitialize(); + void handleFileUpdated(const QString& path, Change change); + void notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error); + +private: + Database* const m_db; + QMap> m_referenceToGroup; + QMap, KeeShareSettings::Reference> m_groupToReference; + QMap> m_shareToGroup; + + BulkFileWatcher* m_fileWatcher; +}; + +#endif // KEEPASSXC_SHAREOBSERVER_H diff --git a/src/keeshare/Signature.cpp b/src/keeshare/Signature.cpp new file mode 100644 index 000000000..fdc0481fb --- /dev/null +++ b/src/keeshare/Signature.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2018 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 "Signature.h" +#include "core/Tools.h" +#include "crypto/Crypto.h" +#include "crypto/CryptoHash.h" +#include "crypto/ssh/OpenSSHKey.h" + +#include +#include + +struct RSASigner +{ + gcry_error_t rc; + QString error; + + void raiseError(const QString& message = QString()) + { + if (message.isEmpty()) { + error = QString("%1/%2").arg(QString::fromLocal8Bit(gcry_strsource(rc)), + QString::fromLocal8Bit(gcry_strerror(rc))); + } else { + error = message; + } + } + + RSASigner() + : rc(GPG_ERR_NO_ERROR) + { + } + + QString sign(const QByteArray& data, const OpenSSHKey& key) + { + enum Index + { + N, + E, + D, + P, + Q, + U, // private key + R, + S, // signature + + Data, + Key, + Sig + }; + + const QList parts = key.privateParts(); + if (parts.count() != 6) { + raiseError("Unsupported signing key"); + return QString(); + } + + const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256); + + Tools::Map mpi; + Tools::Map sexp; + const gcry_mpi_format format = GCRYMPI_FMT_USG; + rc = gcry_mpi_scan(&mpi[N], format, parts[0].data(), parts[0].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + rc = gcry_mpi_scan(&mpi[E], format, parts[1].data(), parts[1].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + rc = gcry_mpi_scan(&mpi[D], format, parts[2].data(), parts[2].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + rc = gcry_mpi_scan(&mpi[U], format, parts[3].data(), parts[3].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + rc = gcry_mpi_scan(&mpi[P], format, parts[4].data(), parts[4].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + rc = gcry_mpi_scan(&mpi[Q], format, parts[5].data(), parts[5].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + if (gcry_mpi_cmp(mpi[P], mpi[Q]) > 0) { + // see https://www.gnupg.org/documentation/manuals/gcrypt/RSA-key-parameters.html#RSA-key-parameters + gcry_mpi_swap(mpi[P], mpi[Q]); + gcry_mpi_invm(mpi[U], mpi[P], mpi[Q]); + } + rc = gcry_sexp_build(&sexp[Key], + NULL, + "(private-key (rsa (n %m) (e %m) (d %m) (p %m) (q %m) (u %m)))", + mpi[N], + mpi[E], + mpi[D], + mpi[P], + mpi[Q], + mpi[U]); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + + rc = gcry_pk_testkey(sexp[Key]); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + + rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags pkcs1) (hash sha256 %b))", block.size(), block.data()); + // rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags raw) (value %b))", data.size(), data.data()); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + rc = gcry_pk_sign(&sexp[Sig], sexp[Data], sexp[Key]); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + sexp[S] = gcry_sexp_find_token(sexp[Sig], "s", 1); + mpi[S] = gcry_sexp_nth_mpi(sexp[S], 1, GCRYMPI_FMT_USG); + Tools::Buffer buffer; + rc = gcry_mpi_aprint(GCRYMPI_FMT_STD, &buffer.raw, &buffer.size, mpi[S]); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return QString(); + } + return QString("rsa|%1").arg(QString::fromLatin1(buffer.content().toHex())); + } + + bool verify(const QByteArray& data, const OpenSSHKey& key, const QString& signature) + { + const gcry_mpi_format format = GCRYMPI_FMT_USG; + enum MPI + { + N, + E, // public key + R, + S // signature + }; + enum SEXP + { + Data, + Key, + Sig + }; + + const QList parts = key.publicParts(); + if (parts.count() != 2) { + raiseError("Unsupported verification key"); + return false; + } + + const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256); + + Tools::Map mpi; + Tools::Map sexp; + + rc = gcry_mpi_scan(&mpi[E], format, parts[0].data(), parts[0].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return false; + } + rc = gcry_mpi_scan(&mpi[N], format, parts[1].data(), parts[1].size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return false; + } + rc = gcry_sexp_build(&sexp[Key], NULL, "(public-key (rsa (n %m) (e %m)))", mpi[N], mpi[E]); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return false; + } + + QRegExp extractor("rsa\\|([a-f0-9]+)", Qt::CaseInsensitive); + if (!extractor.exactMatch(signature) || extractor.captureCount() != 1) { + raiseError("Could not unpack signature parts"); + return false; + } + const QByteArray sig_s = QByteArray::fromHex(extractor.cap(1).toLatin1()); + + rc = gcry_mpi_scan(&mpi[S], GCRYMPI_FMT_STD, sig_s.data(), sig_s.size(), nullptr); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return false; + } + rc = gcry_sexp_build(&sexp[Sig], NULL, "(sig-val (rsa (s %m)))", mpi[S]); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return false; + } + rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags pkcs1) (hash sha256 %b))", block.size(), block.data()); + // rc = gcry_sexp_build(&sexp[Data], NULL, "(data (flags raw) (value %b))", data.size(), data.data()); + if (rc != GPG_ERR_NO_ERROR) { + raiseError(); + return false; + } + rc = gcry_pk_verify(sexp[Sig], sexp[Data], sexp[Key]); + if (rc != GPG_ERR_NO_ERROR && rc != GPG_ERR_BAD_SIGNATURE) { + raiseError(); + return false; + } + return rc != GPG_ERR_BAD_SIGNATURE; + } +}; + +QString Signature::create(const QByteArray& data, const OpenSSHKey& key) +{ + // TODO HNH: currently we publish the signature in our own non-standard format - it would + // be better to use a standard format (like ASN1 - but this would be more easy + // when we integrate a proper library) + // Even more, we could publish standard self signed certificates with the container + // instead of the custom certificates + if (key.type() == "ssh-rsa") { + RSASigner signer; + QString result = signer.sign(data, key); + if (signer.rc != GPG_ERR_NO_ERROR) { + ::qWarning() << signer.error; + } + return result; + } + ::qWarning() << "Unsupported Public/Private key format"; + return QString(); +} + +bool Signature::verify(const QByteArray& data, const QString& signature, const OpenSSHKey& key) +{ + if (key.type() == "ssh-rsa") { + RSASigner signer; + bool result = signer.verify(data, key, signature); + if (signer.rc != GPG_ERR_NO_ERROR) { + ::qWarning() << signer.error; + } + return result; + } + ::qWarning() << "Unsupported Public/Private key format"; + return false; +} diff --git a/src/keeshare/Signature.h b/src/keeshare/Signature.h new file mode 100644 index 000000000..59c32339f --- /dev/null +++ b/src/keeshare/Signature.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018 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_SIGNATURE_H +#define KEEPASSXC_SIGNATURE_H + +#include +#include + +class QByteArray; +class OpenSSHKey; + +class Signature +{ +public: + static QString create(const QByteArray& data, const OpenSSHKey& key); + static bool verify(const QByteArray& data, const QString& signature, const OpenSSHKey& key); +}; + +#endif // KEEPASSXC_SIGNATURE_H diff --git a/src/keeshare/group/EditGroupPageKeeShare.cpp b/src/keeshare/group/EditGroupPageKeeShare.cpp new file mode 100644 index 000000000..6d2eabb92 --- /dev/null +++ b/src/keeshare/group/EditGroupPageKeeShare.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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 "EditGroupPageKeeShare.h" + +#include "core/FilePath.h" +#include "keeshare/group/EditGroupWidgetKeeShare.h" + +#include + +EditGroupPageKeeShare::EditGroupPageKeeShare(EditGroupWidget* widget) +{ + Q_UNUSED(widget); +} + +QString EditGroupPageKeeShare::name() +{ + return QApplication::tr("KeeShare"); +} + +QIcon EditGroupPageKeeShare::icon() +{ + return FilePath::instance()->icon("apps", "preferences-system-network-sharing"); +} + +QWidget* EditGroupPageKeeShare::createWidget() +{ + return new EditGroupWidgetKeeShare(); +} + +void EditGroupPageKeeShare::set(QWidget* widget, Group* temporaryGroup) +{ + EditGroupWidgetKeeShare* settingsWidget = reinterpret_cast(widget); + settingsWidget->setGroup(temporaryGroup); +} + +void EditGroupPageKeeShare::assign(QWidget* widget) +{ + Q_UNUSED(widget); + // everything is saved directly +} diff --git a/src/keeshare/group/EditGroupPageKeeShare.h b/src/keeshare/group/EditGroupPageKeeShare.h new file mode 100644 index 000000000..786c43435 --- /dev/null +++ b/src/keeshare/group/EditGroupPageKeeShare.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 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_EDITGROUPPAGEKEESHARE_H +#define KEEPASSXC_EDITGROUPPAGEKEESHARE_H + +#include "gui/group/EditGroupWidget.h" + +class Group; +class Database; + +class EditGroupPageKeeShare : public IEditGroupPage +{ +public: + EditGroupPageKeeShare(EditGroupWidget* widget); + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void set(QWidget* widget, Group* temporaryGroup) override; + void assign(QWidget* widget) override; +}; + +#endif // KEEPASSXC_EDITGROUPPAGEKEESHARE_H diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp new file mode 100644 index 000000000..a9299bf7f --- /dev/null +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2018 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 "EditGroupWidgetKeeShare.h" +#include "ui_EditGroupWidgetKeeShare.h" + +#include "core/Config.h" +#include "core/CustomData.h" +#include "core/FilePath.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "crypto/ssh/OpenSSHKey.h" +#include "gui/FileDialog.h" +#include "keeshare/KeeShare.h" + +#include +#include + +EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::EditGroupWidgetKeeShare()) +{ + m_ui->setupUi(this); + + m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); + m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); + + m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); + m_ui->passwordGenerator->hide(); + m_ui->passwordGenerator->reset(); + + m_ui->messageWidget->hide(); + + connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); + connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); + connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); + connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); + connect(m_ui->pathEdit, SIGNAL(textChanged(QString)), SLOT(setPath(QString))); + connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(selectPath())); + connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); + + connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); + + const auto types = QList() << KeeShareSettings::Inactive + << KeeShareSettings::ImportFrom + << KeeShareSettings::ExportTo + << KeeShareSettings::SynchronizeWith; + for (const auto& type : types) { + QString name; + switch (type) { + case KeeShareSettings::Inactive: + name = tr("Inactive"); + break; + case KeeShareSettings::ImportFrom: + name = tr("Import from path"); + break; + case KeeShareSettings::ExportTo: + name = tr("Export to path"); + break; + case KeeShareSettings::SynchronizeWith: + name = tr("Synchronize with path"); + break; + } + m_ui->typeComboBox->insertItem(type, name, static_cast(type)); + } +} + +EditGroupWidgetKeeShare::~EditGroupWidgetKeeShare() +{ +} + +void EditGroupWidgetKeeShare::setGroup(Group* temporaryGroup) +{ + if (m_temporaryGroup) { + m_temporaryGroup->disconnect(this); + } + + m_temporaryGroup = temporaryGroup; + + if (m_temporaryGroup) { + connect(m_temporaryGroup, SIGNAL(modified()), SLOT(update())); + } + update(); +} + +void EditGroupWidgetKeeShare::showSharingState() +{ + if (!m_temporaryGroup) { + return; + } + const auto active = KeeShare::active(); + if (!active.in && !active.out) { + m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information); + } + if (active.in && !active.out) { + m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information); + } + if (!active.in && active.out) { + m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information); + } +} + +void EditGroupWidgetKeeShare::update() +{ + if (!m_temporaryGroup) { + m_ui->passwordEdit->clear(); + m_ui->pathEdit->clear(); + m_ui->passwordGenerator->hide(); + m_ui->togglePasswordGeneratorButton->setChecked(false); + + } else { + const auto reference = KeeShare::referenceOf(m_temporaryGroup); + + m_ui->typeComboBox->setCurrentIndex(reference.type); + m_ui->passwordEdit->setText(reference.password); + m_ui->pathEdit->setText(reference.path); + + showSharingState(); + } +} + +void EditGroupWidgetKeeShare::togglePasswordGeneratorButton(bool checked) +{ + m_ui->passwordGenerator->regeneratePassword(); + m_ui->passwordGenerator->setVisible(checked); +} + +void EditGroupWidgetKeeShare::setGeneratedPassword(const QString& password) +{ + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.password = password; + KeeShare::setReferenceTo(m_temporaryGroup, reference); + m_ui->togglePasswordGeneratorButton->setChecked(false); +} + +void EditGroupWidgetKeeShare::setPath(const QString& path) +{ + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.path = path; + KeeShare::setReferenceTo(m_temporaryGroup, reference); +} + +void EditGroupWidgetKeeShare::selectPath() +{ + if (!m_temporaryGroup) { + return; + } + QString defaultDirPath = config()->get("KeeShare/LastShareDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + const auto filetype = tr("kdbx.share", "Filetype for KeeShare container"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare Container"), tr("All files")); + auto filename = reference.path; + if (filename.isEmpty()) { + filename = tr("%1.%2", "Template for KeeShare container").arg(m_temporaryGroup->name()).arg(filetype); + } + switch (reference.type) { + case KeeShareSettings::ImportFrom: + filename = fileDialog()->getFileName(this, + tr("Select import source"), + defaultDirPath, + filters, + nullptr, + QFileDialog::DontConfirmOverwrite, + filetype, + filename); + break; + case KeeShareSettings::ExportTo: + filename = fileDialog()->getFileName( + this, tr("Select export target"), defaultDirPath, filters, nullptr, 0, filetype, filename); + break; + case KeeShareSettings::SynchronizeWith: + case KeeShareSettings::Inactive: + filename = fileDialog()->getFileName( + this, tr("Select import/export file"), defaultDirPath, filters, nullptr, 0, filetype, filename); + break; + } + + if (filename.isEmpty()) { + return; + } + + setPath(filename); + config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); +} + +void EditGroupWidgetKeeShare::selectPassword() +{ + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.password = m_ui->passwordEdit->text(); + KeeShare::setReferenceTo(m_temporaryGroup, reference); +} + +void EditGroupWidgetKeeShare::selectType() +{ + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.type = static_cast(m_ui->typeComboBox->currentData().toInt()); + KeeShare::setReferenceTo(m_temporaryGroup, reference); +} diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.h b/src/keeshare/group/EditGroupWidgetKeeShare.h new file mode 100644 index 000000000..b01bada44 --- /dev/null +++ b/src/keeshare/group/EditGroupWidgetKeeShare.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 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_EDITGROUPWIDGETKEESHARE_H +#define KEEPASSXC_EDITGROUPWIDGETKEESHARE_H + +#include +#include +#include + +class Group; +class Database; + +namespace Ui +{ + class EditGroupWidgetKeeShare; +} + +class EditGroupWidgetKeeShare : public QWidget +{ + Q_OBJECT +public: + explicit EditGroupWidgetKeeShare(QWidget* parent = nullptr); + ~EditGroupWidgetKeeShare(); + + void setGroup(Group* temporaryGroup); + +private slots: + void showSharingState(); + +private slots: + void update(); + void selectType(); + void selectPassword(); + void selectPath(); + void setPath(const QString& path); + void setGeneratedPassword(const QString& password); + void togglePasswordGeneratorButton(bool checked); + +private: + QScopedPointer m_ui; + QPointer m_temporaryGroup; +}; + +#endif // KEEPASSXC_EDITGROUPWIDGETKEESHARE_H diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.ui b/src/keeshare/group/EditGroupWidgetKeeShare.ui new file mode 100644 index 000000000..02361f92d --- /dev/null +++ b/src/keeshare/group/EditGroupWidgetKeeShare.ui @@ -0,0 +1,139 @@ + + + EditGroupWidgetKeeShare + + + + 0 + 0 + 342 + 378 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + Type: + + + + + + + + + + Path: + + + + + + + + + + + + ... + + + + + + + + + Password: + + + + + + + + + QLineEdit::Password + + + + + + + true + + + + + + + true + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + PasswordGeneratorWidget + QWidget +
gui/PasswordGeneratorWidget.h
+ 1 +
+ + PasswordEdit + QLineEdit +
gui/PasswordEdit.h
+ 1 +
+ + MessageWidget + QWidget +
gui/MessageWidget.h
+ 1 +
+
+ + +
diff --git a/src/sshagent/CMakeLists.txt b/src/sshagent/CMakeLists.txt index a612ff076..ff14356ec 100644 --- a/src/sshagent/CMakeLists.txt +++ b/src/sshagent/CMakeLists.txt @@ -2,17 +2,12 @@ if(WITH_XC_SSHAGENT) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) set(sshagent_SOURCES - bcrypt_pbkdf.cpp - blowfish.c AgentSettingsPage.cpp AgentSettingsWidget.cpp - BinaryStream.cpp KeeAgentSettings.cpp - OpenSSHKey.cpp - ASN1Key.cpp SSHAgent.cpp ) add_library(sshagent STATIC ${sshagent_SOURCES}) - target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES}) + target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB}) endif() diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 758c86851..84281bf7d 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -17,8 +17,10 @@ */ #include "SSHAgent.h" -#include "BinaryStream.h" -#include "KeeAgentSettings.h" + +#include "crypto/ssh/OpenSSHKey.h" +#include "crypto/ssh/BinaryStream.h" +#include "sshagent/KeeAgentSettings.h" #ifndef Q_OS_WIN #include @@ -306,11 +308,11 @@ void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode) OpenSSHKey key; - if (!key.parse(keyData)) { + if (!key.parsePKCS1PEM(keyData)) { continue; } - if (!key.openPrivateKey(e->password())) { + if (!key.openKey(e->password())) { continue; } diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index acef6d62e..e6564d572 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -16,14 +16,14 @@ * along with this program. If not, see . */ -#ifndef AGENTCLIENT_H -#define AGENTCLIENT_H +#ifndef KEEPASSXC_SSHAGENT_H +#define KEEPASSXC_SSHAGENT_H -#include "OpenSSHKey.h" #include #include #include "gui/DatabaseWidget.h" +#include "crypto/ssh/OpenSSHKey.h" class SSHAgent : public QObject { @@ -75,4 +75,4 @@ private: QString m_error; }; -#endif // AGENTCLIENT_H +#endif // KEEPASSXC_SSHAGENT_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 73262bae0..22e6fa450 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -95,7 +95,7 @@ set(TEST_LIBRARIES ${ZLIB_LIBRARIES} ) -set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp stub/TestClock.cpp) +set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp stub/TestClock.cpp stub/TestRandom.cpp) add_library(testsupport STATIC ${testsupport_SOURCES}) target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test) @@ -130,6 +130,11 @@ add_unit_test(NAME testcryptohash SOURCES TestCryptoHash.cpp add_unit_test(NAME testsymmetriccipher SOURCES TestSymmetricCipher.cpp LIBS ${TEST_LIBRARIES}) +if(WITH_XC_KEESHARE) + add_unit_test(NAME testsignature SOURCES TestSignature.cpp + LIBS ${TEST_LIBRARIES}) +endif() + add_unit_test(NAME testhashedblockstream SOURCES TestHashedBlockStream.cpp LIBS testsupport ${TEST_LIBRARIES}) @@ -154,9 +159,9 @@ 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}) +if(WITH_XC_CRYPTO_SSH) +add_unit_test(NAME testopensshkey SOURCES TestOpenSSHKey.cpp + LIBS ${TEST_LIBRARIES}) endif() add_unit_test(NAME testentry SOURCES TestEntry.cpp @@ -174,8 +179,8 @@ add_unit_test(NAME testbase32 SOURCES TestBase32.cpp add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp LIBS ${TEST_LIBRARIES}) -add_unit_test(NAME testrandom SOURCES TestRandom.cpp - LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testrandomgenerator SOURCES TestRandomGenerator.cpp + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testentrysearcher SOURCES TestEntrySearcher.cpp LIBS ${TEST_LIBRARIES}) @@ -187,6 +192,11 @@ add_unit_test(NAME testykchallengeresponsekey SOURCES TestYkChallengeResponseKey.cpp LIBS ${TEST_LIBRARIES}) +if(WITH_XC_KEESHARE) + add_unit_test(NAME testsharing SOURCES TestSharing.cpp + LIBS testsupport ${TEST_LIBRARIES}) +endif() + add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestOpenSSHKey.cpp b/tests/TestOpenSSHKey.cpp index 40fb9c302..222a50f1e 100644 --- a/tests/TestOpenSSHKey.cpp +++ b/tests/TestOpenSSHKey.cpp @@ -18,7 +18,8 @@ #include "TestOpenSSHKey.h" #include "TestGlobal.h" #include "crypto/Crypto.h" -#include "sshagent/OpenSSHKey.h" +#include "crypto/ssh/BinaryStream.h" +#include "crypto/ssh/OpenSSHKey.h" QTEST_GUILESS_MAIN(TestOpenSSHKey) @@ -43,7 +44,7 @@ void TestOpenSSHKey::testParse() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(!key.encrypted()); QCOMPARE(key.cipherName(), QString("none")); QCOMPARE(key.type(), QString("ssh-ed25519")); @@ -79,7 +80,7 @@ void TestOpenSSHKey::testParseDSA() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(!key.encrypted()); QCOMPARE(key.cipherName(), QString("none")); QCOMPARE(key.type(), QString("ssh-dss")); @@ -125,11 +126,11 @@ void TestOpenSSHKey::testDecryptRSAAES128CBC() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(key.encrypted()); QCOMPARE(key.cipherName(), QString("AES-128-CBC")); - QVERIFY(!key.openPrivateKey("incorrectpassphrase")); - QVERIFY(key.openPrivateKey("correctpassphrase")); + QVERIFY(!key.openKey("incorrectpassphrase")); + QVERIFY(key.openKey("correctpassphrase")); QCOMPARE(key.type(), QString("ssh-rsa")); QCOMPARE(key.comment(), QString("")); QCOMPARE(key.fingerprint(), QString("SHA256:1Hsebt2WWnmc72FERsUOgvaajIGHkrMONxXylcmk87U")); @@ -168,7 +169,7 @@ void TestOpenSSHKey::testParseRSA() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(!key.encrypted()); QCOMPARE(key.cipherName(), QString("none")); QCOMPARE(key.type(), QString("ssh-rsa")); @@ -246,8 +247,8 @@ void TestOpenSSHKey::testParseRSACompare() QByteArray oldPrivateKey, newPrivateKey; BinaryStream oldPrivateStream(&oldPrivateKey), newPrivateStream(&newPrivateKey); - QVERIFY(oldKey.parse(oldKeyData)); - QVERIFY(newKey.parse(newKeyData)); + QVERIFY(oldKey.parsePKCS1PEM(oldKeyData)); + QVERIFY(newKey.parsePKCS1PEM(newKeyData)); // comment is not part of the old format and writePrivate() includes it oldKey.setComment("id_rsa"); @@ -274,11 +275,11 @@ void TestOpenSSHKey::testDecryptOpenSSHAES256CBC() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(key.encrypted()); QCOMPARE(key.cipherName(), QString("aes256-cbc")); - QVERIFY(!key.openPrivateKey("incorrectpassphrase")); - QVERIFY(key.openPrivateKey("correctpassphrase")); + QVERIFY(!key.openKey("incorrectpassphrase")); + QVERIFY(key.openKey("correctpassphrase")); QCOMPARE(key.type(), QString("ssh-ed25519")); QCOMPARE(key.comment(), QString("opensshkey-test-aes256cbc@keepassxc")); @@ -330,11 +331,11 @@ void TestOpenSSHKey::testDecryptRSAAES256CBC() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(key.encrypted()); QCOMPARE(key.cipherName(), QString("AES-256-CBC")); - QVERIFY(!key.openPrivateKey("incorrectpassphrase")); - QVERIFY(key.openPrivateKey("correctpassphrase")); + QVERIFY(!key.openKey("incorrectpassphrase")); + QVERIFY(key.openKey("correctpassphrase")); QCOMPARE(key.type(), QString("ssh-rsa")); QCOMPARE(key.comment(), QString("")); QCOMPARE(key.fingerprint(), QString("SHA256:1Hsebt2WWnmc72FERsUOgvaajIGHkrMONxXylcmk87U")); @@ -354,11 +355,11 @@ void TestOpenSSHKey::testDecryptOpenSSHAES256CTR() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(key.encrypted()); QCOMPARE(key.cipherName(), QString("aes256-ctr")); - QVERIFY(!key.openPrivateKey("incorrectpassphrase")); - QVERIFY(key.openPrivateKey("correctpassphrase")); + QVERIFY(!key.openKey("incorrectpassphrase")); + QVERIFY(key.openKey("correctpassphrase")); QCOMPARE(key.type(), QString("ssh-ed25519")); QCOMPARE(key.comment(), QString("opensshkey-test-aes256ctr@keepassxc")); @@ -410,11 +411,11 @@ void TestOpenSSHKey::testDecryptRSAAES256CTR() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(key.encrypted()); QCOMPARE(key.cipherName(), QString("AES-256-CTR")); - QVERIFY(!key.openPrivateKey("incorrectpassphrase")); - QVERIFY(key.openPrivateKey("correctpassphrase")); + QVERIFY(!key.openKey("incorrectpassphrase")); + QVERIFY(key.openKey("correctpassphrase")); QCOMPARE(key.type(), QString("ssh-rsa")); QCOMPARE(key.comment(), QString("")); QCOMPARE(key.fingerprint(), QString("SHA256:1Hsebt2WWnmc72FERsUOgvaajIGHkrMONxXylcmk87U")); @@ -436,12 +437,21 @@ void TestOpenSSHKey::testDecryptUTF8() const QByteArray keyData = keyString.toLatin1(); OpenSSHKey key; - QVERIFY(key.parse(keyData)); + QVERIFY(key.parsePKCS1PEM(keyData)); QVERIFY(key.encrypted()); QCOMPARE(key.cipherName(), QString("aes256-ctr")); - QVERIFY(!key.openPrivateKey("incorrectpassphrase")); - QVERIFY(key.openPrivateKey("äåéëþüúíóö")); + QVERIFY(!key.openKey("incorrectpassphrase")); + QVERIFY(key.openKey("äåéëþüúíóö")); QCOMPARE(key.fingerprint(), QString("SHA256:EfUXwvH4rOoys+AlbznCqjMwzIVW8KuhoWu9uT03FYA")); QCOMPARE(key.type(), QString("ssh-ed25519")); QCOMPARE(key.comment(), QString("opensshkey-test-utf8@keepassxc")); } + +void TestOpenSSHKey::testGenerateRSA() +{ + OpenSSHKey key = OpenSSHKey::generate(false); + QVERIFY(!key.encrypted()); + QCOMPARE(key.cipherName(), QString("none")); + QCOMPARE(key.type(), QString("ssh-rsa")); + QCOMPARE(key.comment(), QString("")); +} diff --git a/tests/TestOpenSSHKey.h b/tests/TestOpenSSHKey.h index 214de8942..64516b204 100644 --- a/tests/TestOpenSSHKey.h +++ b/tests/TestOpenSSHKey.h @@ -38,6 +38,7 @@ private slots: void testDecryptOpenSSHAES256CTR(); void testDecryptRSAAES256CTR(); void testDecryptUTF8(); + void testGenerateRSA(); }; #endif // TESTOPENSSHKEY_H diff --git a/tests/TestRandom.cpp b/tests/TestRandomGenerator.cpp similarity index 75% rename from tests/TestRandom.cpp rename to tests/TestRandomGenerator.cpp index 07d1c683a..02f183d07 100644 --- a/tests/TestRandom.cpp +++ b/tests/TestRandomGenerator.cpp @@ -15,21 +15,31 @@ * along with this program. If not, see . */ -#include "TestRandom.h" +#include "TestRandomGenerator.h" #include "TestGlobal.h" #include "core/Endian.h" #include "core/Global.h" +#include "stub/TestRandom.h" -QTEST_GUILESS_MAIN(TestRandom) +#include -void TestRandom::initTestCase() +QTEST_GUILESS_MAIN(TestRandomGenerator) + +void TestRandomGenerator::initTestCase() { - m_backend = new RandomBackendTest(); + m_backend = new RandomBackendPreset(); - Random::createWithBackend(m_backend); + TestRandom::setup(m_backend); } -void TestRandom::testUInt() +void TestRandomGenerator::cleanupTestCase() +{ + TestRandom::teardown(); + + m_backend = nullptr; +} + +void TestRandomGenerator::testUInt() { QByteArray nextBytes; @@ -60,7 +70,7 @@ void TestRandom::testUInt() QCOMPARE(randomGen()->randomUInt((QUINT32_MAX / 2U) + 1U), QUINT32_MAX / 2U); } -void TestRandom::testUIntRange() +void TestRandomGenerator::testUIntRange() { QByteArray nextBytes; @@ -68,27 +78,3 @@ void TestRandom::testUIntRange() m_backend->setNextBytes(nextBytes); QCOMPARE(randomGen()->randomUIntRange(100, 200), 142U); } - -RandomBackendTest::RandomBackendTest() - : m_bytesIndex(0) -{ -} - -void RandomBackendTest::randomize(void* data, int len) -{ - QVERIFY(len <= (m_nextBytes.size() - m_bytesIndex)); - - char* charData = reinterpret_cast(data); - - for (int i = 0; i < len; i++) { - charData[i] = m_nextBytes[m_bytesIndex + i]; - } - - m_bytesIndex += len; -} - -void RandomBackendTest::setNextBytes(const QByteArray& nextBytes) -{ - m_nextBytes = nextBytes; - m_bytesIndex = 0; -} diff --git a/tests/TestRandom.h b/tests/TestRandomGenerator.h similarity index 68% rename from tests/TestRandom.h rename to tests/TestRandomGenerator.h index 323d6b613..addcb0250 100644 --- a/tests/TestRandom.h +++ b/tests/TestRandomGenerator.h @@ -15,36 +15,27 @@ * along with this program. If not, see . */ -#ifndef KEEPASSX_TESTRANDOM_H -#define KEEPASSX_TESTRANDOM_H +#ifndef KEEPASSX_TESTRANDOMGENERATOR_H +#define KEEPASSX_TESTRANDOMGENERATOR_H #include "crypto/Random.h" #include -class RandomBackendTest : public RandomBackend -{ -public: - RandomBackendTest(); - void randomize(void* data, int len) override; - void setNextBytes(const QByteArray& nextBytes); +class RandomBackendPreset; -private: - QByteArray m_nextBytes; - int m_bytesIndex; -}; - -class TestRandom : public QObject +class TestRandomGenerator : public QObject { Q_OBJECT private slots: void initTestCase(); + void cleanupTestCase(); void testUInt(); void testUIntRange(); private: - RandomBackendTest* m_backend; + RandomBackendPreset* m_backend; }; -#endif // KEEPASSX_TESTRANDOM_H +#endif // KEEPASSX_TESTRANDOMGENERATOR_H diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp new file mode 100644 index 000000000..f6500ca8b --- /dev/null +++ b/tests/TestSharing.cpp @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2018 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 "TestSharing.h" +#include "TestGlobal.h" +#include "stub/TestRandom.h" + +#include +#include +#include +#include +#include + +#include "config-keepassx-tests.h" +#include "core/Metadata.h" +#include "crypto/Crypto.h" +#include "crypto/Random.h" +#include "crypto/ssh/OpenSSHKey.h" +#include "format/KeePass2Writer.h" +#include "keeshare/KeeShareSettings.h" +#include "keys/PasswordKey.h" + +#include + +QTEST_GUILESS_MAIN(TestSharing) + +Q_DECLARE_METATYPE(KeeShareSettings::Type) +Q_DECLARE_METATYPE(KeeShareSettings::Key) +Q_DECLARE_METATYPE(KeeShareSettings::Certificate) +Q_DECLARE_METATYPE(QList) + +void TestSharing::initTestCase() +{ + QVERIFY(Crypto::init()); +} + +void TestSharing::cleanupTestCase() +{ + TestRandom::teardown(); +} + +void TestSharing::testIdempotentDatabaseWriting() +{ + QScopedPointer db(new Database()); + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create("password")); + db->setKey(key); + + Group* sharingGroup = new Group(); + sharingGroup->setName("SharingGroup"); + sharingGroup->setUuid(QUuid::createUuid()); + sharingGroup->setParent(db->rootGroup()); + + Entry* entry1 = new Entry(); + entry1->setUuid(QUuid::createUuid()); + entry1->beginUpdate(); + entry1->setTitle("Entry1"); + entry1->endUpdate(); + entry1->setGroup(sharingGroup); + + Entry* entry2 = new Entry(); + entry2->setUuid(QUuid::createUuid()); + entry2->beginUpdate(); + entry2->setTitle("Entry2"); + entry2->endUpdate(); + entry2->setGroup(sharingGroup); + + // prevent from changes introduced by randomization + TestRandom::setup(new RandomBackendNull()); + + QByteArray bufferOriginal; + { + QBuffer device(&bufferOriginal); + device.open(QIODevice::ReadWrite); + KeePass2Writer writer; + writer.writeDatabase(&device, db.data()); + } + + QByteArray bufferCopy; + { + QBuffer device(&bufferCopy); + device.open(QIODevice::ReadWrite); + KeePass2Writer writer; + writer.writeDatabase(&device, db.data()); + } + + QCOMPARE(bufferCopy, bufferOriginal); +} + +void TestSharing::testNullObjects() +{ + const QString empty; + QXmlStreamReader reader(empty); + + const KeeShareSettings::Key nullKey; + QVERIFY(nullKey.isNull()); + const KeeShareSettings::Key xmlKey = KeeShareSettings::Key::deserialize(reader); + QVERIFY(xmlKey.isNull()); + + const KeeShareSettings::Certificate certificate; + QVERIFY(certificate.isNull()); + const KeeShareSettings::Certificate xmlCertificate = KeeShareSettings::Certificate::deserialize(reader); + QVERIFY(xmlCertificate.isNull()); + + const KeeShareSettings::Own own; + QVERIFY(own.isNull()); + const KeeShareSettings::Own xmlOwn = KeeShareSettings::Own::deserialize(empty); + QVERIFY(xmlOwn.isNull()); + + const KeeShareSettings::Active active; + QVERIFY(active.isNull()); + const KeeShareSettings::Active xmlActive = KeeShareSettings::Active::deserialize(empty); + QVERIFY(xmlActive.isNull()); + + const KeeShareSettings::Foreign foreign; + QVERIFY(foreign.isNull()); + const KeeShareSettings::Foreign xmlForeign = KeeShareSettings::Foreign::deserialize(empty); + QVERIFY(xmlForeign.isNull()); + + const KeeShareSettings::Reference reference; + QVERIFY(reference.isNull()); + const KeeShareSettings::Reference xmlReference = KeeShareSettings::Reference::deserialize(empty); + QVERIFY(xmlReference.isNull()); +} + +void TestSharing::testCertificateSerialization() +{ + QFETCH(bool, trusted); + const OpenSSHKey& key = stubkey(); + KeeShareSettings::Certificate original; + original.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key); + original.signer = "Some &#_\"\" weird string"; + original.trusted = trusted; + + QString buffer; + QXmlStreamWriter writer(&buffer); + writer.writeStartDocument(); + writer.writeStartElement("Certificate"); + KeeShareSettings::Certificate::serialize(writer, original); + writer.writeEndElement(); + writer.writeEndDocument(); + QXmlStreamReader reader(buffer); + reader.readNextStartElement(); + QVERIFY(reader.name() == "Certificate"); + KeeShareSettings::Certificate restored = KeeShareSettings::Certificate::deserialize(reader); + + QCOMPARE(restored.key, original.key); + QCOMPARE(restored.signer, original.signer); + QCOMPARE(restored.trusted, original.trusted); + + QCOMPARE(restored.sshKey().publicParts(), key.publicParts()); +} + +void TestSharing::testCertificateSerialization_data() +{ + QTest::addColumn("trusted"); + QTest::newRow("Trusted") << true; + QTest::newRow("Untrusted") << false; +} + +void TestSharing::testKeySerialization() +{ + const OpenSSHKey& key = stubkey(); + KeeShareSettings::Key original; + original.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Private, key); + + QString buffer; + QXmlStreamWriter writer(&buffer); + writer.writeStartDocument(); + writer.writeStartElement("Key"); + KeeShareSettings::Key::serialize(writer, original); + writer.writeEndElement(); + writer.writeEndDocument(); + QXmlStreamReader reader(buffer); + reader.readNextStartElement(); + QVERIFY(reader.name() == "Key"); + KeeShareSettings::Key restored = KeeShareSettings::Key::deserialize(reader); + + QCOMPARE(restored.key, original.key); + QCOMPARE(restored.sshKey().privateParts(), key.privateParts()); + QCOMPARE(restored.sshKey().type(), key.type()); +} + +void TestSharing::testReferenceSerialization() +{ + QFETCH(QString, password); + QFETCH(QString, path); + QFETCH(QUuid, uuid); + QFETCH(int, type); + KeeShareSettings::Reference original; + original.password = password; + original.path = path; + original.uuid = uuid; + original.type = static_cast(type); + + const QString serialized = KeeShareSettings::Reference::serialize(original); + const KeeShareSettings::Reference restored = KeeShareSettings::Reference::deserialize(serialized); + + QCOMPARE(restored.password, original.password); + QCOMPARE(restored.path, original.path); + QCOMPARE(restored.uuid, original.uuid); + QCOMPARE(int(restored.type), int(original.type)); +} + +void TestSharing::testReferenceSerialization_data() +{ + QTest::addColumn("password"); + QTest::addColumn("path"); + QTest::addColumn("uuid"); + QTest::addColumn("type"); + QTest::newRow("1") << "Password" << "/some/path" << QUuid::createUuid() << int(KeeShareSettings::Inactive); + QTest::newRow("2") << "" << "" << QUuid() << int(KeeShareSettings::SynchronizeWith); + QTest::newRow("3") << "" << "/some/path" << QUuid() << int(KeeShareSettings::ExportTo); + +} + +void TestSharing::testSettingsSerialization() +{ + QFETCH(bool, importing); + QFETCH(bool, exporting); + QFETCH(KeeShareSettings::Certificate, ownCertificate); + QFETCH(KeeShareSettings::Key, ownKey); + QFETCH(QList, foreignCertificates); + + KeeShareSettings::Own originalOwn; + KeeShareSettings::Foreign originalForeign; + KeeShareSettings::Active originalActive; + originalActive.in = importing; + originalActive.out = exporting; + originalOwn.certificate = ownCertificate; + originalOwn.key = ownKey; + originalForeign.certificates = foreignCertificates; + + const QString serializedActive = KeeShareSettings::Active::serialize(originalActive); + KeeShareSettings::Active restoredActive = KeeShareSettings::Active::deserialize(serializedActive); + + const QString serializedOwn = KeeShareSettings::Own::serialize(originalOwn); + KeeShareSettings::Own restoredOwn = KeeShareSettings::Own::deserialize(serializedOwn); + + const QString serializedForeign = KeeShareSettings::Foreign::serialize(originalForeign); + KeeShareSettings::Foreign restoredForeign = KeeShareSettings::Foreign::deserialize(serializedForeign); + + QCOMPARE(restoredActive.in, importing); + QCOMPARE(restoredActive.out, exporting); + QCOMPARE(restoredOwn.certificate.key, ownCertificate.key); + QCOMPARE(restoredOwn.certificate.trusted, ownCertificate.trusted); + QCOMPARE(restoredOwn.key.key, ownKey.key); + QCOMPARE(restoredForeign.certificates.count(), foreignCertificates.count()); + for (int i = 0; i < foreignCertificates.count(); ++i) { + QCOMPARE(restoredForeign.certificates[i].key, foreignCertificates[i].key); + } +} + +void TestSharing::testSettingsSerialization_data() +{ + const OpenSSHKey& sshKey0 = stubkey(0); + KeeShareSettings::Certificate certificate0; + certificate0.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey0); + certificate0.signer = "Some &#_\"\" weird string"; + certificate0.trusted = true; + + KeeShareSettings::Key key0; + key0.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Private, sshKey0); + + const OpenSSHKey& sshKey1 = stubkey(1); + KeeShareSettings::Certificate certificate1; + certificate1.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey1); + certificate1.signer = "Another "; + certificate1.trusted = true; + + QTest::addColumn("importing"); + QTest::addColumn("exporting"); + QTest::addColumn("ownCertificate"); + QTest::addColumn("ownKey"); + QTest::addColumn>("foreignCertificates"); + QTest::newRow("1") << false << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); + QTest::newRow("2") << true << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); + QTest::newRow("3") << true << true << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList({ certificate0, certificate1 }); + QTest::newRow("4") << false << true << certificate0 << key0 << QList(); + QTest::newRow("5") << false << false << certificate0 << key0 << QList({ certificate1 }); +} + +const OpenSSHKey& TestSharing::stubkey(int index) +{ + static QMap keys; + if (!keys.contains(index)) { + OpenSSHKey* key = new OpenSSHKey(OpenSSHKey::generate(false)); + key->setParent(this); + keys[index] = key; + } + return *keys[index]; +} diff --git a/tests/TestSharing.h b/tests/TestSharing.h new file mode 100644 index 000000000..ebf85fd4e --- /dev/null +++ b/tests/TestSharing.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 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_TESTSHARING_H +#define KEEPASSXC_TESTSHARING_H + +#include +#include + +class OpenSSHKey; + +class TestSharing : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanupTestCase(); + void testIdempotentDatabaseWriting(); + void testNullObjects(); + void testCertificateSerialization(); + void testCertificateSerialization_data(); + void testKeySerialization(); + void testReferenceSerialization(); + void testReferenceSerialization_data(); + void testSettingsSerialization(); + void testSettingsSerialization_data(); + +private: + const OpenSSHKey& stubkey(int iIndex = 0); +}; + +#endif // KEEPASSXC_TESTSHARING_H diff --git a/tests/TestSignature.cpp b/tests/TestSignature.cpp new file mode 100644 index 000000000..887143a85 --- /dev/null +++ b/tests/TestSignature.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2010 Felix Geyer + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestSignature.h" +#include "TestGlobal.h" + +#include + +#include "crypto/Crypto.h" +#include "crypto/ssh/OpenSSHKey.h" +#include "keeshare/Signature.h" + +QTEST_GUILESS_MAIN(TestSignature) +static const char* rsa_2_private = "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEowIBAAKCAQEAwGdladnqFfcDy02Gubx4sdBT8NYEg2YKXfcKLSwca5gV4X7I\n" + "z/+QR51LAfPCkj+QjWpj3DD1/6P7s6jOJ4BNd6CSDukv18DOsIsFn2D+zLmVoir2\n" + "lki3sTsmiEz65KvHE8EnQ8IzZCqZDC40tZOcz2bnkZrmcsEibKoxYsmQJk95NwdR\n" + "teFymp1qH3zq85xdNWw2u6c5CKzLgI5BjInttO98iSxL0KuY/JmzMx0gTbRiqc0x\n" + "22EODtdVsBoNL/pt8v6Q+WLpRg4/Yq7YurAngxk4h38NWvufj2vJvbcRqX4cupcu\n" + "92T9SWwSwZmd4Xy3bt+AUlq4XRMa1MlKfPvXmwIDAQABAoIBAGbWnRD/xaup1OBU\n" + "dr9N6qD3/fXLHqxw3PeudEUCv8oOhxt43bK3IZH1k8LeXFA5I3VCuU9W6BWUu5Mi\n" + "ldXtMPrQ22CW6NiEGLWqCP5QJMCeLUl5d0WKZoyXVhgiNTQGUKjRY8BGy5stXZJy\n" + "HAA1fuooUXu09Jm/ezvjl/P6Uk722nZns4g6cc8aUSQDSVoeuCvwDaix5o4Z4RGY\n" + "4biIKGj5qYxoe+rbgYH/2zlEcAiSJIuqjYY+Xk4IdB89DYZBYnO/xPkRaDeiY2xl\n" + "QM7Inr7PQC8PWJc9zYYvlGnnmIRCkO15mWau70N1Y1rUAsyW61g2GyFhdsIIODH/\n" + "878Kc9ECgYEA+2JaUqRWr6dqE+uVPpGkbGiAaRQ79olTcRmxXCnM+Y3c88z9G7kC\n" + "2S5UKPRDl7EzwmMJqqb8BZbdSWoAxO4++F6ylSz7TqowPw+13Wxwm3wApvr2Q1Mo\n" + "rkq4ltgyHMR+iXvKqOYa2GqZNmRwh7JGLIJ7Y0Z77nwBkkgDc/3ey8MCgYEAw+/N\n" + "fxv2t+r6VKxEtjdy3sfn8LLjiWqghPngJzcYH9NdB8fmMN1WHqX075hbKjI9TyJw\n" + "77p8onjZI0opLexHHUmepEa6Ijo1zynJJ7XPXnyruiTXXqz49io6lFOLcXi/i+DZ\n" + "B2vQcMGWA4qwJxz7KA6EZ/HimjuysV1guvlKf0kCgYA6+JGTvXWQc0eRMLysFuJp\n" + "hAJLpDGE3iYy7AINSskI6dyhXL8rl7UxWYroqJSKq0knGrCT1eRdM0zqAfH4QKOJ\n" + "BD4EfK7ff1EeGgNh1CR+dRJ6GXlXxdRPPrwattDaqsW8Xsvl30UA69DRT7KOQqXv\n" + "nxRu74P3KCP+OuKEfVOcnQKBgQC+/2r1Zj/5huBhW9BbQ/ABBSOuueMeGEfDeIUu\n" + "FQG6PGKqbA2TQp9pnuMGECGGH5UuJ+epeMN36Y/ZW7iKoJGuFg7EGoHlTZMYj6Yb\n" + "xJoRhDwuZy1eiATkicOyxUHf6hHme9dz6YA1+i+O4knWxuR5ZrVhUiRPrrQBO4JI\n" + "oSwiqQKBgHblgOVfOJrG3HDg6bo+qmxQsGFRCD0mehsg9YpokuZVX0UJtGx/RJHU\n" + "vIBL00An6YcNfPTSlNJeG83APtk/tdgsXvQd3pmIkeY78x6xWKSieZXv4lyDv7lX\n" + "r28lCTj2Ez2gEzEohZgf4V1uzBvTdJefarpQ00ep34UZ9FsNfUwD\n" + "-----END RSA PRIVATE KEY-----\n"; +static const char* rsa_2_public = "-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAwGdladnqFfcDy02Gubx4sdBT8NYEg2YKXfcKLSwca5gV4X7Iz/+Q\n" + "R51LAfPCkj+QjWpj3DD1/6P7s6jOJ4BNd6CSDukv18DOsIsFn2D+zLmVoir2lki3\n" + "sTsmiEz65KvHE8EnQ8IzZCqZDC40tZOcz2bnkZrmcsEibKoxYsmQJk95NwdRteFy\n" + "mp1qH3zq85xdNWw2u6c5CKzLgI5BjInttO98iSxL0KuY/JmzMx0gTbRiqc0x22EO\n" + "DtdVsBoNL/pt8v6Q+WLpRg4/Yq7YurAngxk4h38NWvufj2vJvbcRqX4cupcu92T9\n" + "SWwSwZmd4Xy3bt+AUlq4XRMa1MlKfPvXmwIDAQAB\n" + "-----END RSA PUBLIC KEY-----\n"; +static const char* rsa_2_sign = "4fda1b39f93f174cdc79ac2bd6118155359830c90e2c39b60a1a548852f2c87a" + "cd61b2a378259a38befad35dbf208a2c1332ab74faf2cee2ff2e8be49c4c5f41" + "dc10e5a5fafb53d3c54e2b7640d7bfee6bb0f24c5a1fb934150a144c2b465fe4" + "8a1579e666a097fb1609ae9abc5760f6e6d6e73acb610fb11dd1c409ca284a72" + "0be64dd56a28ab257e8721f5bade58816382581ac08d932098dd200d836fe897" + "f78a5f02095ac3b21cca2a47b2afd282ce075c6450cba8c85b08b58c5bacb75d" + "e1a73bdec4321193d4a3ce653d8e3aa8a4f2beac6a44497328f8855f7e28e15d" + "f63b21f8bc7204bf6e202c9cb08be050379be5ad88d8e695a38440a50e75dfdf"; +static const char* rsa_1_private = "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpAIBAAKCAQEAsCHtJicDPWnvHSIKbnTZaJkIB9vgE0pmLdK580JUqBuonVbB\n" + "y1QTy0ZQ7/TtqvLPgwPK88TR46OLO/QGCzo2+XxgJ85uy0xfuyUYRmSuw0drsErN\n" + "mH8vU91lSBxsGDp9LtBbgHKoR23vMWZ34IxFRc55XphrIH48ijsMaL6bXBwF/3tD\n" + "9T3lm2MpP1huyVNnIY9+GRRWCy4f9LMj/UGu/n4RtwwfpOZBBRwYkq5QkzA9lPm/\n" + "VzF3MP1rKTMkvAw+Nfb383mkmc6MRnsa6uh6iDa9aVB7naegM13UJQX/PY1Ks6pO\n" + "XDpy/MQ7iCh+HmYNq5dRmARyaNl9xIXJNhz1cQIDAQABAoIBAQCnEUc1LUQxeM5K\n" + "wANNCqE+SgoIClPdeHC7fmrLh1ttqe6ib6ybBUFRS31yXs0hnfefunVEDKlaV8K2\n" + "N52UAMAsngFHQNRvGh6kEWeZPd9Xc+N98TZbNCjcT+DGKc+Om8wqH5DrodZlCq4c\n" + "GaoT4HnE4TjWtZTH2XXrWF9I66PKFWf070R44nvyVcvaZi4pC2YmURRPuGF6K1iK\n" + "dH8zM6HHG1UGu2W6hLNn+K01IulG0Lb8eWNaNYMmtQWaxyp7I2IWkkecUs3nCuiR\n" + "byFOoomCjdh8r9yZFvwxjGUhgtkALN9GCU0Mwve+s11IB2gevruN+q9/Qejbyfdm\n" + "IlgLAeTRAoGBANRcVzW9CYeobCf+U9hKJFEOur8XO+J2mTMaELA0EjWpTJFAeIT7\n" + "KeRpCRG4/vOSklxxRF6vP1EACA4Z+5BlN+FTipHHs+bSEgqkPZiiANDH7Zot5Iqv\n" + "1q0fRyldNRZNZK7DWp08BPNVWGA/EnEuKJiURxnxBaxNXbUyMCdjxvMvAoGBANRT\n" + "utbrqS/bAa/DcHKn3V6DRqBl3TDOfvCNjiKC84a67F2uXgzLIdMktr4d1NyCZVJd\n" + "7/zVgWORLIdg1eAi6rYGoOvNV39wwga7CF+m9sBY0wAaKYCELe6L26r4aQHVCX6n\n" + "rnIgUv+4o4itmU2iP0r3wlmDC9pDRQP82vfvQPlfAoGASwhleANW/quvq2HdViq8\n" + "Mje2HBalfhrRfpDTHK8JUBSFjTzuWG42GxJRtgVbb8x2ElujAKGDCaetMO5VSGu7\n" + "Fs5hw6iAFCpdXY0yhl+XUi2R8kwM2EPQ4lKO3jqkq0ClNmqn9a5jQWcCVt9yMLNS\n" + "fLbHeI8EpiCf34ngIcrLXNkCgYEAzlcEZuKkC46xB+dNew8pMTUwSKZVm53BfPKD\n" + "44QRN6imFbBjU9mAaJnwQbfp6dWKs834cGPolyM4++MeVfB42iZ88ksesgmZdUMD\n" + "szkl6O0pOJs0I+HQZVdjRbadDZvD22MHQ3+oST1dJ3FVXz3Cdo9qPuT8esMO6f4r\n" + "qfDH2s8CgYAXC/lWWHQ//PGP0pH4oiEXisx1K0X1u0xMGgrChxBRGRiKZUwNMIvJ\n" + "TqUu7IKizK19cLHF/NBvxHYHFw+m7puNjn6T1RtRCUjRZT7Dx1VHfVosL9ih5DA8\n" + "tpbZA5KGKcvHtB5DDgT0MHwzBZnb4Q//Rhovzn+HXZPsJTTgHHy3NQ==\n" + "-----END RSA PRIVATE KEY-----\n"; +static const char* rsa_1_public = "-----BEGIN RSA PUBLIC KEY-----\n" + "MIIBCgKCAQEAsCHtJicDPWnvHSIKbnTZaJkIB9vgE0pmLdK580JUqBuonVbBy1QT\n" + "y0ZQ7/TtqvLPgwPK88TR46OLO/QGCzo2+XxgJ85uy0xfuyUYRmSuw0drsErNmH8v\n" + "U91lSBxsGDp9LtBbgHKoR23vMWZ34IxFRc55XphrIH48ijsMaL6bXBwF/3tD9T3l\n" + "m2MpP1huyVNnIY9+GRRWCy4f9LMj/UGu/n4RtwwfpOZBBRwYkq5QkzA9lPm/VzF3\n" + "MP1rKTMkvAw+Nfb383mkmc6MRnsa6uh6iDa9aVB7naegM13UJQX/PY1Ks6pOXDpy\n" + "/MQ7iCh+HmYNq5dRmARyaNl9xIXJNhz1cQIDAQAB\n" + "-----END RSA PUBLIC KEY-----\n"; + +static QByteArray data("Some trivial test with a longer .... ................................. longer text"); + +void TestSignature::initTestCase() +{ + QVERIFY(Crypto::init()); +} + +void TestSignature::testSigningOpenSSH_RSA_PrivateOnly() +{ + OpenSSHKey privateKey; + privateKey.parsePKCS1PEM(rsa_2_private); + privateKey.openKey(QString()); + QCOMPARE(privateKey.fingerprint(), QString("SHA256:ZAQ/W1QdW59OaIh/0hs3ePl2og5TjXnGX5L0iN7WtNA")); + Signature signer; + const QString sign = signer.create(data, privateKey); + QVERIFY(!sign.isEmpty()); + + QCOMPARE(sign, QString("rsa|%1").arg(QString::fromLatin1(rsa_2_sign))); + + Signature verifier; + const bool verified = verifier.verify(data, sign, privateKey); + QCOMPARE(verified, true); +} + +void TestSignature::testSigningOpenSSH_RSA() +{ + OpenSSHKey privateKey; + privateKey.parsePKCS1PEM(rsa_2_private); + privateKey.openKey(QString()); + QCOMPARE(privateKey.fingerprint(), QString("SHA256:ZAQ/W1QdW59OaIh/0hs3ePl2og5TjXnGX5L0iN7WtNA")); + Signature signer; + const QString sign = signer.create(data, privateKey); + QVERIFY(!sign.isEmpty()); + + OpenSSHKey publicKey; + publicKey.parsePKCS1PEM(rsa_2_public); + publicKey.openKey(QString()); + QCOMPARE(publicKey.fingerprint(), QString("SHA256:ZAQ/W1QdW59OaIh/0hs3ePl2og5TjXnGX5L0iN7WtNA")); + + Signature verifier; + const bool verified = verifier.verify(data, sign, publicKey); + QCOMPARE(verified, true); +} + +void TestSignature::testSigningGenerated_RSA_PrivateOnly() +{ + OpenSSHKey privateKey = OpenSSHKey::generate(false); + privateKey.openKey(QString()); + + Signature signer; + const QString sign = signer.create(data, privateKey); + QVERIFY(!sign.isEmpty()); + + Signature verifier; + const bool verified = verifier.verify(data, sign, privateKey); + QCOMPARE(verified, true); +} + +void TestSignature::testSigningTest_RSA_PrivateOnly() +{ + OpenSSHKey privateKey; + privateKey.parsePKCS1PEM(rsa_1_private); + privateKey.openKey(QString()); + QCOMPARE(privateKey.fingerprint(), QString("SHA256:DYdaZciYNxCejr+/8x+OKYxeTU1D5UsuIFUG4PWRFkk")); + Signature signer; + const QString sign = signer.create(data, privateKey); + QVERIFY(!sign.isEmpty()); + + Signature verifier; + const bool verified = verifier.verify(data, sign, privateKey); + QCOMPARE(verified, true); +} + +void TestSignature::testSigningTest_RSA() +{ + OpenSSHKey privateKey; + privateKey.parsePKCS1PEM(rsa_1_private); + privateKey.openKey(QString()); + QCOMPARE(privateKey.fingerprint(), QString("SHA256:DYdaZciYNxCejr+/8x+OKYxeTU1D5UsuIFUG4PWRFkk")); + Signature signer; + const QString sign = signer.create(data, privateKey); + QVERIFY(!sign.isEmpty()); + + OpenSSHKey publicKey; + publicKey.parsePKCS1PEM(rsa_1_public); + publicKey.openKey(QString()); + QCOMPARE(publicKey.fingerprint(), QString("SHA256:DYdaZciYNxCejr+/8x+OKYxeTU1D5UsuIFUG4PWRFkk")); + Signature verifier; + const bool verified = verifier.verify(data, sign, publicKey); + QCOMPARE(verified, true); +} diff --git a/tests/TestSignature.h b/tests/TestSignature.h new file mode 100644 index 000000000..38e576462 --- /dev/null +++ b/tests/TestSignature.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 Felix Geyer + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_TESTSIGNATURE_H +#define KEEPASSXC_TESTSIGNATURE_H + +#include + +class TestSignature : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testSigningOpenSSH_RSA_PrivateOnly(); + void testSigningOpenSSH_RSA(); + + void testSigningGenerated_RSA_PrivateOnly(); + + void testSigningTest_RSA_PrivateOnly(); + void testSigningTest_RSA(); +}; + +#endif // KEEPASSX_TESTSIGNATURE_H diff --git a/tests/stub/TestRandom.cpp b/tests/stub/TestRandom.cpp new file mode 100644 index 000000000..d33b1c9b8 --- /dev/null +++ b/tests/stub/TestRandom.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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 "TestRandom.h" +#include "TestGlobal.h" + +RandomBackendPreset::RandomBackendPreset() + : m_bytesIndex(0) +{ +} + +void RandomBackendPreset::randomize(void* data, int len) +{ + QVERIFY(len <= (m_nextBytes.size() - m_bytesIndex)); + + char* charData = reinterpret_cast(data); + + for (int i = 0; i < len; i++) { + charData[i] = m_nextBytes[m_bytesIndex + i]; + } + + m_bytesIndex += len; +} + +void RandomBackendPreset::setNextBytes(const QByteArray& nextBytes) +{ + m_nextBytes = nextBytes; + m_bytesIndex = 0; +} + +void TestRandom::setup(RandomBackend* backend) +{ + Random::setInstance(backend); +} + +void TestRandom::teardown() +{ + Random::resetInstance(); +} + +void RandomBackendNull::randomize(void* data, int len) +{ + char* charData = reinterpret_cast(data); + + for (int i = 0; i < len; i++) { + charData[i] = '\0'; + } +} diff --git a/tests/stub/TestRandom.h b/tests/stub/TestRandom.h new file mode 100644 index 000000000..ec5afb59d --- /dev/null +++ b/tests/stub/TestRandom.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 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_TESTRANDOM_H +#define KEEPASSXC_TESTRANDOM_H + +#include "crypto/Random.h" + +class RandomBackendPreset : public RandomBackend +{ +public: + RandomBackendPreset(); + void randomize(void* data, int len) override; + void setNextBytes(const QByteArray& nextBytes); + +private: + QByteArray m_nextBytes; + int m_bytesIndex; +}; + +class RandomBackendNull : public RandomBackend +{ +public: + void randomize(void* data, int len) override; +}; + +class TestRandom : public Random +{ +public: + static void setup(RandomBackend* backend); + static void teardown(); +}; + +#endif // KEEPASSXC_TESTRANDOM_H From 1a42c589753ad7904b7cda00180e0f54f4e1586a Mon Sep 17 00:00:00 2001 From: Michael Starke Date: Wed, 16 May 2018 16:00:17 +0200 Subject: [PATCH 02/23] Add documentation for sharing groups * Add Sharing section to Quickstart Guide * Add pictures and text in quickstart to match extended functionality --- README.md | 3 + docs/KeePassHTTP/KeePassXC-Accept-Button.png | Bin 0 -> 32135 bytes docs/KeePassHTTP/KeePassXC-Confirm.png | Bin 0 -> 18606 bytes docs/KeePassHTTP/KeePassXC-Connect.png | Bin 0 -> 25412 bytes docs/KeePassXC-Accept-Button.png | Bin 98966 -> 0 bytes docs/KeePassXC-Confirm.png | Bin 72759 -> 0 bytes docs/KeePassXC-Connect.png | Bin 103252 -> 0 bytes docs/KeeShare/AppSettings.png | Bin 0 -> 42163 bytes docs/KeeShare/DatabaseSettings.png | Bin 0 -> 27801 bytes docs/KeeShare/GroupSettings_Export.png | Bin 0 -> 30784 bytes docs/KeeShare/GroupSettings_Import.png | Bin 0 -> 30826 bytes docs/KeeShare/GroupSettings_Sync.png | Bin 0 -> 30856 bytes docs/QUICKSTART.md | 57 +++++++++++++----- .../preferences-system-network-sharing.png | Bin 0 -> 13033 bytes 14 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 docs/KeePassHTTP/KeePassXC-Accept-Button.png create mode 100644 docs/KeePassHTTP/KeePassXC-Confirm.png create mode 100644 docs/KeePassHTTP/KeePassXC-Connect.png delete mode 100644 docs/KeePassXC-Accept-Button.png delete mode 100644 docs/KeePassXC-Confirm.png delete mode 100644 docs/KeePassXC-Connect.png create mode 100644 docs/KeeShare/AppSettings.png create mode 100644 docs/KeeShare/DatabaseSettings.png create mode 100644 docs/KeeShare/GroupSettings_Export.png create mode 100644 docs/KeeShare/GroupSettings_Import.png create mode 100644 docs/KeeShare/GroupSettings_Sync.png create mode 100644 share/icons/application/128x128/apps/preferences-system-network-sharing.png diff --git a/README.md b/README.md index 3de0f38f6..67cb66973 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(id:KeepassXC_TeamCityCi\)/statusIcon?guest=1)](https://ci.keepassxc.org/viewType.html?buildTypeId=KeepassXC_TeamCityCi&guest=1) [![Build status](https://ci.appveyor.com/api/projects/status/qmcar8rnqjh4oxof?svg=true)](https://ci.appveyor.com/project/droidmonkey/keepassxc) [![codecov](https://codecov.io/gh/keepassxreboot/keepassxc/branch/develop/graph/badge.svg)](https://codecov.io/gh/keepassxreboot/keepassxc) +## Note +This is a feature fork to introduce the concept of Sharing to KeepPassXC. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details. + ## About KeePassXC [KeePassXC](https://keepassxc.org) is a cross-platform community fork of [KeePassX](https://www.keepassx.org/). diff --git a/docs/KeePassHTTP/KeePassXC-Accept-Button.png b/docs/KeePassHTTP/KeePassXC-Accept-Button.png new file mode 100644 index 0000000000000000000000000000000000000000..8c736c2134e5a11472314b830d1c7046d05ef9db GIT binary patch literal 32135 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
  • OpD7u6j3G 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 literal 0 HcmV?d00001 diff --git a/docs/KeePassXC-Accept-Button.png b/docs/KeePassXC-Accept-Button.png deleted file mode 100644 index de4b39261a9179902f6f9e700bb510c6c47e0e71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98966 zcmb@tbyQrzvpm1^G=w$0>-^J3*udfsGUAMJc=Y|r0^|F0oKoIxk}zF zq8?oZDDr)<=f5cZBT-h2bB47WQI7FH1TA|T&5OUO(ftD5d_bT7-=yz5?n6Z?{ea#R z*8elKX5$J;&MmhZ;J^8ZRKsla1($Yvx`r<;pOHOZf<#hArJs3&Y^r0Wpr|g4QD<(X zZLe-={%-q>5Kbs1#Klpr+i+3^WAKbIvOgV3X!uXxd%k6N;ks}F4xfwc&oGTdYpew` zs+-+p9Bi!c7f+AIQ4Qyi@W0_@V)l0mbj{sv+UWWF8XYw+U!3VH5F=UF%xfx*d?#U;b{LlTmkgr!6b3H<6$U$7WsLjm`T&5}xFioXA~ z6;`8%diDmp&c;BM6jsHNC`>4t=o~vFjBHnL!HV>={v<1&C>boP$#BnJI)~IGf#@!8`&rSR8xXSmbaY<&5zGJzV%qO`UNg6e7E{@ z^f{v*vP=sUx+(ezp;67D)k6Ng29A=&^M^4W({@KOf_hNH|)wfv(k z1e-BVq;mmocZEqXS<375*-w1&y&LHreD{AGiPd|52-YIf!Td}loEw>*HoeMlayFJ6 z-6wz0+3B?{3jGjr{LLeDL!}2QLZi;9uQ^7q?WQsJJ@xi}KKK2hu~~3N8&(J3Mv%Lj z0~pNB#y<9#MMOSzfyxX-!E5r*(-^b+@i`H&8kcsHU?f!)ZV$C6G899bYK9l=|2Ly& z&#NcQm3bPi$dF@K2a_Z}5f+bCcA-GvHx$TmY4&#xnz#xG-g zV1(^aXttFw8P+^0PexHr$ly!yC_Xt<(eMjycHZ&(UEFVCj`-2(SD{8CjYV;3k)ez- z^^}7b4OrOjkVYdm>FhU2*=@?&S(%MzDW6A9Gc-s!JoqB^QX+;fs*o=Dk9r| zoqLZ5TyQzl@^eAlyjhuU+`YaE<-9QM1O^Ghmx8gBE}q{INSDuC(TTV2rw#bBI*FpX z6G=0M5VGFQ>bFxIY&{6YE@rQm!uHOkD$dWpQw5YQkt%K$!mIvUj($wQd2p{U*K&CV zj3>|&m#?x1_S6umEUL)PGK^mG(VgRD0_Z}OaXU?0J4$^IgHiO}w@1VJCbKlUsko(a zS(R-stnp$$$j;8bo}86&O~zgx5Z&eJpl=ZE%o2&Q<#?t&ZrC^)m%%M4F=KM#Czrw}n-#k<&Ho~EkOsj6hVlKv z&{0_JlLM%GYGC~-?`XVX4lXTH zhWce|18i}XT($Tk_PA18H7I{gZ2TC%Cs||VFI{?#*BPrMV~ewYUbnAAe3R!KURnC_ zqFMK8ImUn4!T+0j?GLBDnUMRVi^*aKN2|A61eiADvZN;J12%5Vs}+Ur2a%Ok&e+OI zZ&I$p=FfBAni2jOSBMQ0H1CJz@QQlyUjKa#{sPDcVV)DEr^4fwi22bJU$Ced`Fxt`eq23_c z{@jpnr3(Ei4#6W?;Xk;9shW~3logzc>c+Tc&+X*)OX*pamHW~sfDjC2hSW)*Y>vSQ z?&4A~N85r#f60%89}CeTs#6h$bF;sLhQ0=)6XgakBV$6%nslU7K^+Vikj8$S)#sij zIBg2jxUVxOTNhOx+(-@9XIrrDWSc(J7CW-VH;*a*7D6*xqb99vO^_{?`hDP@%BRxo zOWg7|)O(V7Bt?kpIZdp9Z^{v3LS~{GPd`O%B=`gse0&{pT_8B~sijyxx1B7()u}^~ zDkFpI&JU%zp8}kWCRpzhQ-*OI&DSi_>?z6z?Wq*FEVQl^obRuRRsTjqvF@|?)p{i5 zgtU0n(Hkh;zLY*iH%+RVEcL4%G!%#Ij(?oaP+bGWD*)4GijrIY)J(g0nJ9`8629)- z(8M@e!^fB4zFNowjet-@30<+`djw4^{+y0Ia8=6BAh`Etp}HzsZYid zIX|6SchkX(u~svwbN~(Lq^Ie;^P6-NuBS><^dd#%FML~HO5c|!R=wxmZglnJ4cmH6 z)y8H5qPJKk9q-rRn=gXx?{k|>o!zAeJnZX5&Z^xLzBI%>2W)r*$Y!=-f9JRFUowy~ zXO#yi%h;fJ(hD8IP7~pbhzG1cxSevsmz2stUnNQ~_NL3kio!A!A^duNbXraYpnm!` z1G@aoMkAOTJf0;VFh^*iiRI6^pd-5X&nuUb+r~@@Iz8nfFyPMepC5NhkX1XJ*Jnk4 zs^^N-kT&ZS($L35O}Z5*fw)5<6ez(GvU+MssQZJL2$RUN1@!(H(y%&mf}#U=kFr=J zn`%@d^ZZGV(&km|WS@39r@h{SVma-N1@By`C}Q|oqaM$uqn5?hXI|GmPFoqZCUCDy zZ!-*x^iN3}v2vuU!%s z&85IUcpsJ}t<>(ZB-3V(4$C;3KV@H&f8`>bXC z2&toA=_Q@e#faf!sw8I=mGvHhq`pp1R)%R42=w)A|E9%@MG#W;^T;i)af_66(H{Q( zNnAK5=twG|j3w5^punh~zov4(gvxu5D z(T?CA3{fhVf!6;LAY9u<5Tn`{zW1bGK8 zrxYUa*C~7UYibF{&OjLBY2q=FUHodjK6k8(+`~B{MzV5pOl?uD5qdI;>1#I?nTvL! z`dq%wDD6VY3W&fA)l>TMIe}e@%vsaw`(W-%5*c}HQ8G4gAZMzc?ejm8L?jgfqkmE* z?svIHgHOMUGH56G^VCOvuHy)cuPM9jNgUMkbQN{68gRy<_i`4H`{Qj`WFh*;zTk@`>o9+0smiPZU`tHbqq%>4kpe~e9Hzz*IWgpC|TJ5SXHHe;r9 z8cWbln`98y?}{sx>27z(;$VLpfR_0MLMj0LeW%1CF9S`>9Y{YRtg*-=t4l{K1Um^kgC@cDue)+gy&biX726 zlh1 zv0^{4?t7-qv;rPAWnvp400qbg?0ij~HJ8EeKVHFjjY*HRwb|}SU;B{)i~~sh zsnMu>E_;NC|F{pq`5PGrd_$xk-gTEE9}jeCq>QCbFdR)Flul`DWa^BoFvPah(@fCZ>GI^0Xw&T>t5kXjSL@#fR!$#W->xMgQG?qSUbo?6<1UY_krdZRx92*(ey! zZa*sw6=~JwGW5rIuyYmVR^X#aWHmqYo|wOAfSo!IbT&=Mr`kiN5vF4?)n^T1<|=y| zIR4POz5W@R*tJyg%&}v`q(65e2l<%_J8RF@H|4ZmV?D~0m}ryTsgL68lY9J%)7aH* z+j&k#u)zMM*HVW4P88GqrdqILtYYS+ADG+&?5*#wta_@?TsWPUGElY9l&PFS!0nEx zRJu>5KxOCG`Pr0!gFD{zp}5bD%vHv=8k7@`eY`_eR>-H8$N!^&vbjOXHa>E-Gc{p& ztDk}2r|sH}zkIIn3}hbr%xh%9TdCx*sMW5bscW5_5iXyv8qW8}xw&vHq}er3`X_Cz ziLxY~vY|MtggWOUnFxHDrI3V{;`&Kq%n-qyq82IqFII4ztVKJ_=WGep(@I70CQ07p zjKrg#(ch&|LluMFD0bq&vJ^)tCLUcx;>Rd;)uAX<_{(7%?sIWf&Y#TZ=X!sPod~`* z%Xn@OQ@?J#q2QjUXkUY0|K2-Thp0oCjvL>e5O5g{2gj#kYu)n`KtbmUi{*fj{=e(#`SVbvC0G{d?`iz0tuN63E{Z4oK z!oV1)DbgW=;D1hTk%kghKt4gnZkjNZym)2xMRAeZ5-d^h&=y;5@_iAT&-6moq`B?; z>A(`u+y3eOs|F;Tuoi@yrt*Bq#JG_W_*OPIA!Q>@{C382&7y-eF{!g@S^~`hS8iT4u z={S&m)eD26ZMuolBiGmA13#^B>05d-Jeg%IOG?;tLfMHV+<5g@EGEhlU9qJIRbeaH zT@_Rb%gd|w8KFh|<4R)kk)aG8(xZI^5;7e$MDQsod+KNiADlgTO;W5jfkgnvpr9`VFH>me%(D@9%RGB8rZyFJ@nIeA$=_ zt7k3?rlkQ$zPkS}2V*fl^)j~4c(t6s;xqew0OcqEyewtVS*`EcB}nT`kxMH`Fr zLZ+m*m9YJWf+ui>$#aDG>mXdCs_*G>q4lZN~AGVSPn8yE9(pU+Vv?SS!@=KS_SNshN& z?>~2Vw><6B$i1JAo*3Ecx(+BynC*_9fWLDLS17FH%Eg@KazguUw_5=2=7I)#(Q!O730YaCCw7=V`jTaVJ9(A74o7LMN0&(vrdIH5Rd#*lw@2MnNzJ~>o z7k@i%cRN!zmOas1Z{>WQ5r*2k+b?D^1z%oWH~Gb@&nDlG*KnIZMYj0&%#P+v2y{h$ zuY|p&RXYuqSQ}R+#=6waLH|D1Q;ZXVN4t|tKE&5*KM}rgyu+vL^+3q+T?Hp`d2<SV8%eWo5>i!zesT z5yQkymV>lpe_un`U-~$UwQAzGr-#XYdGyD&rJf`CUcI;0Hlsy%I=lU?r111{x^3PR zXgcc%cwN{eCV0uDaI+45-z)AG5KLF*5B=4`vGtk%+@s_lOXrQBxNrzPnhv->Z-(=e zxY3^EhH*DBpY8oJ%U9vHf**vpv(h&_+Ze9H;_$Sf-Az%&XvTVAFsYci=~ahVGxqQ! z9L*#oo549rnJ1LUYns=I1E$jZD-zW7oGek$oG66z#t~0Fv^A79P7pg3vudsYytQKH zmY+jOkK}}#kzm~CDmqNbReey}pwfm$f3-_qpu!aUexu!yD>To@ufhd^^@dRDIACkhT;FZLD#=WF1z&AaWP3Pv&V;4D2(8+6~{; zhHXG(HWmP4@=Igdb+4HyNo9fY1~VE3v!<_@loy4-(E2@t+ozi0il`rHgr1@_CJIQ3Z7j6ke#dtp?*{r8)pq`z}@%+jT zF|inmOSs(%4PP3{pabXK1InOv%nP+TZ>Hj+k70yGLo{Nke#1`>7bwL=)Ca%*9h)1w zEl6Lp62^jSq&(7m#@8XjTMr1hRgCnDuo6j>d|T;U^L%0IxJpd)z1`s2kIsXxI=mSC zxTL1`{kVwfez`bg+RVUkX1EcCm$Y}IB`ZM{Cr<;4H?b`*UBpi*s#P@!)_epIe1l!yjl_fLY^&!r3aT zY~M&|->5lH*-JDBS+o*z+KW0WnQR~u*oitfmW9R>!)NF%c;8|6{hqcNT5h|~?b_Xqn$094cgMlb7A+de4v=LMKfxG;;mu~U zOrC_LrAR35j8t}R(Po>LXA#nb$?bBKPS(V%LeZ>y6{Zrfrj)G-DYT`n>}PIHTE=a z+b>fW;NADRpL)OT66bQ9&SWEc&V9Gw}HQ+c)+czeNo<`J;u6HR$zlKRr-y zoUko}BGu!%$jyPJ>ro{u>L1kNyO508+qa^BZue6Xy%{K4P zy6(-o?z6h?vAW81w^e)HEAYQab!&eAv|1}NiR|qplZVm@aFV&XtLe10@JGF~ag>o~ zwJDvAf^=y>-f_29QqucjVa%ta8yyOy4FzaHr#4p(%}*xX{tO`JOKH=&8s=OvubEY| zC^a9&l^aL*hrw%NW7m|osM?snGI1dZ zY+iyp>ErYoYb3@gc7;!)iBG@=+IF>K^-;Pu(PC9?MgsO}eHw;p;e|t&j%WV`jQPD!9tc zV7>Wfwp3>s>5$`0Q{M`uSY$g(RJ5Tc=UEu-3>D4!T(Q)x0F?+?C8*K$lx0m$n$Gcb zMWRDlNsq5y_fu3Z$&ZD*v$4r5_1aTe*W83?p{y`JzA&%8k1pfw1r+a7=lBL`L;+}~Opu5E`9SU_nBW%34;v()eW}XHTC>=pD<>}#H(rh|1XUB_ zQLa+vR0m3*As}LJ-Q&RF&lxoLzQe-P}nJ$qNDDoTDec_sNm%;$Ew)oU~q z;3e$)k$S)MzH{83utO7JL{kNMioq+CmC+4sg`l-U##D6-lV|#8W|P@!Z?ncP&Sy{7 zpmDcmOsM7ylRcj_#I%wy%EfHmBh@+-%Y$HH4k}9w;MnKO%^7P!8^sSl43l*Q_4fk~ zTt1ApdIJ&uYv2Y+Mz`l@Vatd2ozhkDffQ^ly51lL0!*27=wvG%!0C0&&KicL=5LVp z8I^>`F zl?9qX|CwIk;MAw50G~6cTF~!D2H`r&VI`AM1wxq{owOs_LHEU_N2oRi#!l*U<}t0Z zKYb|)_=HK@O67AdoZ5mFGf*gY4aMydtT0FPkU}WjS`E+?1TGqMGX?~p*b}Vo)*APCR`<=9@NCQCVB#m20a7&qnd*?l<|JmD zj z$-`8Y3d;c?J*6_t0*ZlneD7FAd`;bB5@usu-I5As9MDJ!PI?f*-wEX z{HG*JRv3K!WWo$3k#ge@)XvYC$(of);s0x1=7AcJ&R~kgBGX9adL^GGFxFf`5@m`P zb#^TnNxO-lO2V~|x%{?8Q6sG&9ME(^$r{O`?04#1jd4O&15)>h9F${BwHJaaOv2wv zWSQrz0YxD2ujg+gw*ih;!iT%mA-3IbP0Ln{^z>}_(VA34WZcp@wRWeBTOf>X7~K~w)JO@G&@7Onvi zz}nJeQ{6mTlKz>Q0Hd9Zn)@R)ol+Ia6pm5@(xQkIBJU9or#g(zraaZKo9OTlf!Eh{ zyIlWPj24IekyJgOCvJH3y|^q&`FPyh%d`K{Wj21e3J;uWcwAQa5AWw$tY!kpz{W-dl%u-yOmHkHyV90N zt_+=Z)w^wUNgJaeLCOr{xPh0+0GvVmOspVXsY-_x5fT!zvZ6^GD_Jm&jDo_-!lIvMu%&;^9`PNw_zKh?|U8b6su1|&5ApyX?c68rU=40c4_ zi3`WX8IY|tu%*iSMCg5KV=!*1S++PcbLwt)7MW$PC{6Ec9e64VE(=sFn%o68GR410 z)Ih>Qs{F)dJ+A4R=4wm=eoGF!1(8P!7b+Tnc|Lp;q=CtG>J|UW+#DFj zUqM5I_ABCvm@`IgVQUwj)fUPll1qT7(Du{KVS0KxY4{H+Pp0fry=TaO>i$il3m6zZ zM8imC2$JHm!i!f>lqggj`mxlSsW~tsj;eo>5AU?Jb*sF6(m*WZtmz6^hiS8AbeQo6 zf!qgwQ?=P8@qj(Pz_)aXL+)EZIttRf53)}OZMJQs>rlxVT+d_xKM-p2nJ9=zVBHv} zI7wIf7YibIBzx`p>TvV}7aTxZ(%7g%9&g7*2$K_+l93XhBTSVTERv^Gnnbs+;;e$* zQRkqG_Xy(?kiGR)K}uc*VZ<+8NFBDE!jHuDStpRhqe4A0B~pQYPkSl}LUmZQ^a6G-MU7-oOK; z?*!I~;JNsL$K*f@MQP5mLLG9oNRStLAa$Yr&lSaR*jLeLEhqMS_3$__?u1jYv(tqTClcLNmG^Rq?~@@MJg%|=fi+^z{h=xzd5*s%?PwTmu!fD&bnW!T8)Ae zpXE+_kN>2spxQBlNK`6B>?5e{dE(L=P+<1AF~e-SSj>TrtAIIy#jKUI)U)85tS> z6pIsy9XZg+X8AvWA$8YFRiO3&^Jw9x#7JN&n8VL)B^;8}O@@%X?Nk3{nAX>X?<_=Fc$TXJa_4K}N07Nd2g)%L8s+xeH$;B$-J zo^)k_QZ#Y0x%QG`X%=b$hW`k;f;k-M>EBeh@Do80LNtV65$J#5pJRC%<1)4)KOpPQ zsoo`v2}|_zaAQ9kxw|bNPNTFTQft;zV3O*m{8O-_;ibyZjWdPO0uokbNw3%(ZU1lZ zz#_VxG&eM0VPm(px67xqn9P+Zs;a7rhN8V~hmvGvWijfs^&7JYy5PO2LY*%yEsbSy zv$C^egT-wV{$H}FR-{D@{zEYxgz-5Zs+~@8B+MzhJpr0PyI5S#2|7hg(-q=T*`AP- z(w48(?)Y%(53sI*%8qok{cLB;`o~)cJd?4}R*=9^f??%7%V-Mog%%ndz%7l)6hB&p z-e&;wnC;bur{V@gQ4{V{fdZ4q^2+C2Whx+b*!iJ^`%!Pza4F%1Gw62~c5o3sjE0Mm zL@Z3f7b>nu9mK^)k+Z?7^fIJM0ovZMhzB^2Thw0j)RMG0a>Xez?Ex)HBD9&~=L4Rf z^=UO^98q({Ofb{5Yddry7S_Hb3UmNjdTjtY>_q79NQUMHikttBJy80uqAV$KoMbF) z9Bdpc92_jHU)Lyz>W0{o`Bn?SyVep-wwV(uXiRx@{q%(AlqOElGEf;2k({HcLsJa1 z)VR{Ni?S4-U^xi1R*qpTEI`BiD3r2iK!-FY5hs7K*57bPx{or%1!9I4#ZIu)!q!Q| zH_ztiYrPBhTC4ZfoGDYB9~QIHL<$Fs7)2I3Z}B{jrwd=OH~T!1&MYiYVIe0bB=jiz zZuR(!hJ0pbWqmowwC#F-KE3FAu{oZB(9+VPqN3*7f8(RaLHRW}GjqL_CwhE*+}PO2 ze+v8Q7krX1M%-7*kMH|dl7QdiU@T;{px?L!GFJd&a$N!)Bnbdc6lm_J^`4Cl_URp8 zi<}~C!)cb&XZm>;5kOkXGk)^(l$tb&7luSFqrkccthO0=M%LGI>Ysvt_}PG^NvQn( zl;-u=w>g*e_p2z_W1A>)cM~P|0FwT#@Hr{EeUycHNMEUUz z4+axo5NqXmG(lEb7vfp>>sZHyse0xYcoR-e=H?t`JDXo;;T^bHe>V8%8}5Xym!CFo`dI$_>G5}|R;^l>orA;YZ^C_Y*uc)e zUDZ#ZU{HNBgO!?rLFehd7Vv+ze$uBGv z9Mf1Lw3oZ%4grfgF3c{6TNK>Q?H|ue9XZjO1&N}{iuzvjaqZn#68Q_f2_@*iNL@>% zDxW&<$2Uta^GV40ydLW>;;j~?y?!5Y2!kXi2d57lkwh1@PCi7nMpl}XvUeUquXxbh z3Yu_V7DM~K%&M!&R0mNev_Q%y2r3=*VCKDABo7ApU#J1y=r(Q9tEG**E^+Dhz1Z?o+tF}K6sqMmQ3L0?E(TAFK2qgPu#t8l;}+Gn8! zqt1>PztD`{k8;lq=>GxkW#IVALS8~6PIBCm2qjJ+`Bua*&*b^F%cPAdH31=Eq|#y4 z9Cus7mMj=UI&(|fu1>B1lp9!`Fy>dCAVlC*SFhz-tO%$qNDztz&7EXKfEa+&jV0hJ zame4z01z;_thF_LayK!I(u+3cPhKHU6VnjM5DPmx{jZPKFZj(!)dsZ;XAhB@VYTf=l~3fl55SixANcQ z>Hj|HsO%Lv0|a>6o+?_-VL(%D-%l`0a>{Whf$?=AI5fTQ647O0Pb^Nc>x{+NgQ9hG zQw(tn(z@^`5vi^VwpZ$*;euQwZQfi(x5p?>NciC*1`)dalEvARZaBm#6On^umYEyw z%YAnW!Du|J6Q;vZ1?WY-nGFjI{H5eonhZaWD`rcb^QCJsO6~*mXC(PviUHl@#tYVK za_|=E9V!LqoIT!#Fz0()rdoP>Uj}>rf%bdDwp|N0ooW;@|H{AsfSf#F^3Zq9;l9zF znTAMz>YJZ%{UH?P$xibeOe{6TU)1Q9>&`}b{Uv#^05lmtlmJ617YU+-kvmS9Mi|}!U0uge?@bG&=&z`fv`9%kLtkTnsLvPJcP_IuPZfm+Nn%juia#Mj@R zCb>F1Han_xnQ`CQn(?*47gr6o@hO@ZoK_>d1-l&CayM>;K!h+@d$JSSTcU?#OPiH5 z+7UC(Qx%gR*vBFp`zA3I3}21ZY`Vu|O8t4F>uK2;N*R*nbFdIGnLEWHAg9OlaLYw6 z?iGU(y;Pg0ivZ{6r#jS=u!;$4t$u6G7gr~(X^-4+ zl%cr%*vw&+l_2BPxrEZHVwkL0M_RCx`!~{m2Yp=H&D4S6viw^irWj||VcvI;ph z#N?oC)g#uYm6hpJw4n0{!*2ajN}ULVkyUaGb{c5vyPcCoath$wrpNw5HM)Rg)`QNK z2$wj|1s$-=Dun0s^?s zSllljykj)q*a)kCimfnL^rMT@QbYAQk6v!uS+UBFk;5~$>F%Vu9Wc>_EiUb%MKR7w zUoL3R@`*Z}8yeWM|Mmz)q!Q)hwehsJfxU}H&Db_t9Nx}H&>PrR+E^N)qZu$cDvGnW zkE?%uO~a)mTiKyfn%jEML1a)#sAREsnc3iv@O?>sdxxA6EwlcVH6*Q`f0ZVCqW5u_+<#Zr%Xo!T2 zhz=xLih+W`^Q6W*t=JHi-v{`?ReWt#g>y7N3xAKyAfv@FmL)cqHQ;qluWDvMIs`1= z7TJeODGjfl+-cl^6kO5*@bX2-T0xZ=jO;Lt(mkzZcJ6>7&WV9k*#7wD;h(&oM7_R} z7B#QG>)LQG5816No=2Ac#(B4o`Dj09N0(o$E4L;M8k0ear1NZ4M}q?3UgNO!<1`hZ;HH*n+)=&yh0%sCzVQeT-#}Re~tR5AU&>O)Raf zWTd72i}-Gq>&?c;$9=P)7&S$;c%TZdFsYuid6G-8FF(D)@%HBF!ehBz{5w$L%)!M; z*~2wd6nCS~2xFR#1|BPf4^;T6aZSqx%ra)vZPl*g2JUuoBUMqDHXk{6_z6frCg58h zETc}dq0wAU|G6zLQl+fG-U85=3pgK)GO~#YoS&cP_kHJMV3&oFQ`2)NJKJlkfzVY= zYC5=QoFyO2RP{hLoP><`hPDSdGV%>j$8kNT;CzxF3wLu~e|rx}7IuWDgl1QNx@L>B z^M0(0JTB(COC}YKV;CaL+BrL1%*SAvvyJL}agD6{cYCGV*(M_UaIWzXw$9>{Y+OUR z02Dr$;o8e@{YR#-gwc4k?6@zXdBdow^ACF2q3O)AjUr3}8A~j`VluF%&bPe}MsS2p zfKY3mCiz*4cmILEBXuT0rh3K~j>fFEek-y4mVxS+Rr~d)}C8y49{XA&o5x?Y0w$L1%Y#Q3-&>DU`$dPsNplOCZpD&`+#xD(Z z|EihjPFHf7!S0LVywgRe%=kT(wU@|lX&BJdxM4Qx?&EdjB(nR}x<~EfeUfwYN5j*~ z-9%YCSKDXm0_t_=>#2i3LkY(c>YmD)Q)a-H!oslm(8=-Dr=t+tqv%e@LpgR97G`#4 z0zupP2)d_3W?+{AZ0+a5{lvtA1!8{g@%^3N`J9)9Sdv`q;@|E_!wCN2aw31B1$K8TqnLQ0rju0<44&wySd@<6+ zLqz?h#HpAW$LTQHY9sn1+lp__qrYZbbyMrzayY8hXM_6|9DNT>4;lV^nsx2dJ26G0 zMG8ZLM+o&buN<1Ol6-#AXin*6oA)h|7SFmZ6g3?G*v+#1>UVC4)@LFPkN#%SKXR`( zNqvbva`H@xJ(H?o69YjyE4O-s(;qo}zt?7Fevdu2OW&xrZIrdhs>m2u@MtfoEON%W zhyyO8wuA;GnGeyc1j*?o7~i@Xq&XfeX^Dy?RZ!=?%6e^e{yf@v!mJiI;ATq`@F)Sp z2MN5MWUcys&GFUq;EW(O_o4$H%jQz&hMlvP*SO+RlHhe3kBYY|7xH;z;s_%qiwnO~ zIPdb9e1Uuy_*n?X8xcaCY~tRHtdKEI-b09qS>~U+QaV`ptn%sa(_D$a!#6zY`%JPF zuTC!+b7@4qaoA5kcZDiO(O`4wbV7nSYz_##FprTi@YEjTUZ=)AuhTRn z5NFx9M{Sj*cBo_tWc-oNRC6GXI5~X5w9!<8G3z$&3ob3z`V03~*brg8Lj`{E>_!GU z{pqOzAJHZgzKW7mR|1uUD{1U_H1DbBLM^Z}Din%%E1@?d@07uKKT_X^fxw(qS~8?| zpaPN^`rWJ`VGD~=X`Z;XXAasv9ey0>`OiL-DLj>t$O>!dWW`HLs3UtDY_Q znWOtd^~=z7+0;M2-6y9H2+h{A7A-yg)$AIze$V}SzsW<~A|LStJ63>&2&bs1c)CzY z@Gq(Sc(n`7sz`$cQF{7^S#+_yIBF^?7^&yc%`O;%|KtfsYNd1(kvMvi^*2I5gx+H| zF3|?yGUf)1HIhlSV_EPC3aqvo7{q@z4gU<`Ax}? z&V18j)4p)Z-&xml-nCVJajI~~$-@7YcTiHwYwx3bIg^ zBa6iTQCGgOq8+8P?LZF61}c@)(!!_|u2G!zh|{QRlixvyjfm^iOsnGVO341%PB zuZc|sZdciT=2ybIwY?^2zb=ZJ&;%(Cjnmy=CuQJVNCRginj;OvO+UP=MK`rF)Rv7P(?7=5tAmAjBMSV&gEL{#Ii zH?Jr=;YmA1NXLf(Gli+8V$DrZ-JUvZk=P894wON)O7&PfioK8%$`2z1T$vqT|D z?A;#=)G1e3?3F-NFaQ`CX8+Ao|96W@JH`6%VQKClr9EZC=Xiv7#?i{f@m%|7ZB6Gq z{iYqK&)ex%6j#i}U-mH3Cs2J8t zjIUR0_DK#K&R@LMauVzDEz>H3UZ=c?t5NN42lqTOT2x^T%HkK_Dh}FEwH?{ocJg2n zgx3BsyNHyBcCS1XL`Yj+r8Zv!Ej|Bcx;r7j>d%M43d=ORV<>lt2 zjI1chgKolS%-`OKaLv{_mPTd-u&paVAGKm<&uaLn_f_H`)8$VJwG8R>VGM{8V|aSn z{`P2^POXa1_uWfFW7&+*z0T|@J3nCqO0AH!$HF`La%lpdYe}s%5&HbDL}@&;1Z?&- zxgVK<`c2U*SpX5}%+X_y4`edQUc+jfqqU0JjZQBYF$<8dv5wN-xp9-fjAUg#y5z$b z0-y^)OvN41DIWBHt7gXps=7NbWyvazGMZ(>Y1~af6-z=;1ba%1?-!w|1jibLM<5hBh9rTdGZN@>^va!ai{WIDBra^u@PN&ctWY_h{ zKr4iYfaf`c&3sP2v{X_)TCc|bi0VQA2cojh@{G^BUL=TR2}<&BGPE48sZreqbrE?f zc7mt~?f!NkcO;qyuMJvncBK`%D4f0Y<*_0UI3F z7gg^VT}c2+qP}nwmY_c^PY3QA9sxUQKQD%wP($ms)=V^ zi7P5OIrq$CnhJf0(QR0D?%SCK+4eUTL?d_Mq~J&xY%R=ufqs_6CR?|v66-i+on_U% zFs7?Ir9E!Gk2I|eUx?4-ogKR^F*z)7lWFhVCD=NgRsouFkF2M4ug$zKY^^>;t;b^U z=@~Vip364+$r0gVw}GJcEE;BT8u4r`+h#xEZ)I>7e?binu?eB4TH~>K=<_f88#qV# zFAridCSlQjm-|mcUd`b*gP@qO$>-BX)j8wWbKFp{DPJ$cz2DbCH(|i7^Qf#`VaQzF z3JvolKe4{b@c5NG$d9#6986h%aX(mw-nc&pJFV78LP=c74YqlS2iIME)`MzIgei~{19UXlNXti}r zT(1DFq?8ukE1zj9&0}vpkT&qL_Bv~>9u{EiI@xTgr>*%}y&%cBeUZepHk}h$PSy7Yb$Hpy?Tbm`b=~KnELWLLQn+~6YR~{pWC8p zA&RpT3~aYKA5hBo0UM%Qz4b_VKG*DcIFT7Fwih*g)*P>(Smw%n3s;&;6!AVe;aju* zi}A$fUd{$HK4MTrp+CCKB*2?RyLCA7{mS-l*rFf4zULb&S3W@nNy}SiQkSj1L zX!v}tE1egCei^+^?S8}J+A#bCdu|WCd{e3Pr+#9B_F94QC0AF zHoOiGdXmTsJ-;z{y$0c92FFp_E2DgJ99+9D<&X;mm`t-qxdIh?o?7MabJNMa7T-iv zLZV%eic-g;G}GN@A5uRr!0%mtYhf`g{LY#(cRhJ&f?#)!5FNIyHcr?+UH)7*Z>JV0 zW?5U@OM2eIceick&E&j<4_*pUinqyZ(K#_`?<0%OrZ!g`Wva+_zjD{4qVBrw(uF{E z?&2KqO)0}rKCy-D?Js4hwr5h}Y#m$%jBe<}sT+c|RL%4Z)P3uSj=c>8+OVvHB z;D`btR|bviViM4BpF>)mYT0BqlKGmA?h=~sfO+Slsof>IW>}+!#C@x!BQiyVNIB?z zpL!Mu4kClgr!H{tq2r>|0n#8S{eFu$J__`Y{;m=%5}9U-bYLX6iY6%%E$& z^Jy|6v81>?hW&th`;+xbuuOy6NB(otcSf9RBd$Z1n@*Mm2y^QnC99CeXe>Wr;L%Pu zSM;kC6h)rHAKHfp^4V_MS_X|g7DIFI#dXJprVfQavc{zERxGD^jR%`VlGCcCA_(eg zUXPQhr{|Egj$ImVX+>zgW*|3IIa%Jr3kGprVGP9L0(P0zjCU8hXASYrej|h>=bkbjeq8KdQId-{*l=#JT}lJ-%v6XE(|g zU#X?UapJRP%=N#1vaOCoOA@V~J~KQmn+@8?4A@rFExR)`&&hAa7jRQt*qTW4gOhQi zSU;&-U8O>usoM=i>ym{ucv+PzZqhIxxBDFwv5j}fs7DF-(Nva1|TE0ki zre`0{1<6%hX;-@BRk2Ncyvl;5f9eRQ4ew7Fh`L`|g3H?49_=dj213-;;hGxj)0_JZ zTsQx11mlc*Z!6B=9#q1ee7Lp^O{YrRw~nhQ5uL6l!7J5cS@14S&Kr#H_I04wM97v} zRnF}g^}#iX4AGRu+{>(Y*sUkp>!zh;z%|s;<33&Hh{~p({d-M)tVE2Tm}Q}p$IlOm!zJj~?Xcr@cEP*JG>`c1jzo3OHAt!w|&Rva=&Rr2WYN zi7y2j6}CG9`5;tA4b8@7@hvH9aAl@wuJb$zeR{1W;`iM+Ns+r1&l{pBMPKnbq=KjD zq`W`F8mX9IEjC&>LwRFel3D2$)4}K(-7>li=M#0J<;LqV8jCkyDBfywEUB-*Gi2x! zOWg6S^^?U-;w=Q0DaBO_Q1!L6Ve4h_1_RBc1tZB4Jyy9-U2-BOdWJts_sj&1tNx#r(4hWPBG!+zlN!$fO%Y z-M+l_K_@tO5gbPCkk#n3OjwY+DFCU9&3vVvfrwj-+S@K=POgFpuW5<@vF^~Urb%d- zVA92N#~nva!za&*nysY5wkNXV@gy*!b zV(Wc1-c!7bOC^-j2Uh0V(k^-~hM-%ep<6Rw`7~{{ezo7J+lUz$|LZv6z=W^G)iwJM zedEz*X8hnhuU#drIBlXJF~x)wEvQXuGHX;v@kf= zI<9t~p~p}Xt!Dk1qVuv%qJLkG+MB=scmYQKx&d8aZEsWkm4u-P zaEJqmjg1d1&eXx}a(xWZT+PWzPU6`L?7D1!GuuC_u<}yHFU!{a_L!-PaWg%qvs*)* z0vM5_27q}~K5Hlj-pJ<%#$?3}k=^;wP`14c&czd3ly%C*y7Sl-_idl=KS!pM2NzyL zCN&m4GH+l;-%&u63Zo^1or;z3kN2quy>Z`KDcSXog4>Wc|j;8_!ue6lzxu!Q=*DEMK2vZTDdu{d=3{5kNqOmxb!Uu^FkEo@$3iW=>wI}$Y;?bW>#@xj#A{E z9XqU+r``h|1{$z9TF9oL*Up>RM`K8;j!!nW?Xqc;iuU!pU4%quLWzu0owDQ`=4^K2|ez4Y=dZ9%uOM<0cTY)@3^*w zuMnpF#Xw|FgR4p{=VL!l%Ic7bs-YqeoX(bIj=i}&#~9j4HO~bb(|<7OtFx`qXi2l^ zGO`%mCcQnX)4IG1&OK{bIc?j|$aT2>66ca6`&KfA1a8#6yR zJ3OL`l<2z~=_+wH`r_3wAjko@d&Dkvi5TjLO|BWc*OfQEJmTI`rExLsXMCFjllr|u zTbABa$qtjhrp#ZQnlvHeO{uxP@>y@n=pP=I?21C1H$HKquP<9GJ`+R=<=-J~coXQ_ zotlG&81%i?H-BklgGM$qSnf2e7s=6ZSfD}Z4DVFRXepJSdEQ19a^#qci;GFA(K2#! zWJweHx;}pg19C?DNpsjKy>UP{#U7RCp-!mEW)u|_7laArjX`M|dwpg50V^l&w64bi zQ#?K*dc~&9q~tN#!)Wq-!Ky1t??G;S^o~2#FGsc0S#A8U$HF5S!Ttnoa>O9+{;-~N z;9Pk~7yJMTkQUo01!-w9aO!*KY?jTv`9{T6ReIV|7KHRH?~fb31#sW&&RM#d7Qrp+ zD62FG!TI_a{0oT^+$7F?=X3nuTf*Z3zsO)}+TpVHt@m%J(`vf7gib;-dq0;QtwXE$ zaKr#+0ozWs%0iWm^HdFi+@@1pw~HJyGL?a_I9qI%SK*0|5{#r^I;nFc*;KaG&5k{3 zu1GZ|KyC_k1oggOMO9Q)G3(PY(xnLZO){IYsg*MK;ym>FiA8HWWL0ZUJ(OkDj!HI{ z{iI72vT{?hI?GLZ`Q#El72F=j%UhpwB!*>@GpO1vj%mEp`!oN(@#yD0DmS);mzg*U z{pdGS$f8pp3c_VzfRu}4RB{^Ro)HYqR;71V*?mhNday@P7OUja8y>5#YlW(hJd!RK34j5trv zf>G8PAiH+m#PQ?5*a+nZvvOI@3F{bEafIb`de%PooK8E=sN?n5rJyRMxp`z{xcWyP zg=D0?$!5^D^{d5?+9MibkwZ5K9wy7S+qm;_zd=3J?#yOI`55B?+Z$BBcT56KD6SMmvNh{DBF6fz142*vA4V>zn%r01}mpQB78{X+(5d+ zsPI@J2elk$mP;r)9ge4osr4?hsI~JsP^E$~^U4wbl`HS+?BsI3-A)w^|5l4)@OSK1 z-VT;BXZE2S9dL7f;;a`F*vGTJbjw?2Cbc{KezP~A*#PkEHC(`b#Mk|z^Qf49sOk`;x<9Ny^EIbq3WqS#=d<{Mhc(L(8eVro;1pdln}Af7#8I^ps1C6x{?#lEH*T6Yb-cXRU)qVfe#1SK z6(kX7$_SELh?vhRxcX_OXdXO>?IR*;M_OSH1&2>FOv&vrYW8PNR-&|fB+caPUzr;K z!@&#v^XCy&;$@*n>3&uLOsLRts(u1V^U+GvSO4iavE?eL@wEg}1BdcI@*ge98?Cmw1*y+0JV5fL|xiYg!r>Z<+tF%pMNZ8HV^I^uX;hvoG#`Lu1`@6md|Jk+8veAKUP z{v{^qA2&mOblvoC@1vI+2d6VAy9@(KayCO;QEW;BtE$G_`MId9Q_OYiI+wnu5o#>J z{;9P{dl?<7=mh-3R-UK+oDr+Grc66m1pCw;*DxFfeY?^C&Hez(ZnXO?sg{=_)?o(= zz%{ib2jlSGal*uhQvSGW@7Ng$GMmm+oafJR#a+> z&B5OJYh?gh>G>PI_vYIVpK98^3~Z}wp$*1Q;_NGiozs0{$WV$Bi(iKzE*w+cHs_uM zaMaa`v{ltrvi!Mu4*EII!D&m9aV5NOn1AbP1yM7jQ1_Z`&9RFrNGH^DXL>y!uT8)v z?-={ykYI*{01#;z-PR|wl9^mzws^CJe1*@vYyD`8qN0da9Cz0&Ud{q6$vU1YwyAwS zShLXJ2CHd0P&>x;YR+4q)(jlfE(|KX2ddDnV0F>uzvxqfeADDm+HU9321|Vjq(W^s zaA(}ML1vLhrubYRGEQxLwXZOh@e2IXJ=Yr+nf}_fAR>8jmJaICcXoENTCLjM?u+nC z!oh{;LhvymMhxx3eg5?I{lDx4Y{d2F^%D|vz^VSR*> zon$Pa=*{{ep^w1sU2gjD}1aJQ?mmHgOmxJ7L5iPlb*}TszAu z&cIV4>pVqmb1bdecIo%crXAs>B&Am)*1n#e3PjVq&8^H07@Sg%YX1!XOE~sE_n6@Q zD~QHsspMol7A)oK*-_1T-{aiXQEuthHUDl4Cjn{auMyR0Z~iFkdCwQa%EpNG9JrS6 zYu!)53G;V+wwDcs>=VfiH9VKVDzo5AFQKjnUv)F_@Virs{cY zmYpsY=UDpq5@h`he}m+t*;$>c zKH0)fcUG(5hC=R^ykdLOf8Pq;?wzls5+W2^YW3gdX|e3odh@IHN7zAqAGwi-+~z`+ zTMIktyacyKYDU%Kv5t85)m=PfdjvOgw~!0_cjL3lF*S6SeAG`)n%#DXhbk^m@VI$> z{#|zcV5#CWenYI8{Ch0tIV!AloEwdnj5ZMJ#pW`fD19z_41o$LlfG1BQ0`m~Y262l z0ZkyC#Gto((1UTLa%4KN`9r=JYtiX!5bewIpi3t-_d-E~qe&fouPZglbx&=@=ke-mxgc6&ONo?a^L*FR z*BB*fGj~=zy!>!0W|J%mo8>OTH`SMI)Qa1nJn9**gU2)F6T8kQwZfJA6S4DY+B6z~ z0sAi$-{)mQ_p4s7FF-PdR#{cm_X5fL*N=ez^tUuNHvY_kXMfo=V95bl@M97KLkZmd zR-FEA>ZfP{X9tv&2$a;0y{q@|YUG_nj1QT#Xwf7^dSJ)?lOl~0T)DAm%OAJ0xTA5> z<6oZE&1S!n(b~^weq~@l8k(jwFeXh2$r0bUh-*DeA*=A~r+F~Xg=LrMt)c+AZVwjG zqE)3$c{b?!lLnE06LyOj0@1DvH<=DeWysT$(>?FG+=osPiAK^%4OI>A{Q6PO(M(!= zKuz=0m|i)_iT5{BVbvY1bHyOIbmF-E3`nW{(hX89#YLwv?>Ns{8JX_4^mO@7UtesqvnS=_-$!Y-m}1zDCF^fI9S zOp4Zv3wqZ_DXyMgsV7Oy#o6xO;5@A5CXlRO?^7TqG^LYJyxm6cPb4FO4OI7<&hm5E zELE=;nFQCtn$6S?@Z%=ORDT9Y8`vb>h@u|z;aJN`Enw(m(okfQ1gMt*!!{NJvXx9u@NazY&h>bELDwz&r)RoV@w} z!@k;jbwobKguLm=zjR8+kLQOegf|bQ5R=HrG)9jMx9A2tm$ljRhT?hqDI| z$_*g~wHDM6zlqfcGSuhtoAb7GUaOzh#;tH9xjd_-9xJ{E)W=1^8d#m*-KTx(H?1cf zEv>&V@m_aamx8y}p&w8_nE=0q(bfeZStcXh!dB z_|!X+rAKdVxwIbOHr8TVBJJaNt_mx_H$fmn%Pk}2q3fp-ay2x78j%ua>rat|cAu7b zymw;NND!)K91Z94Wq*cD6=Fc5vk%Vb0uy$O1kLCQ1!o3B8Pf7jA6`H-WC z8F?zae|jRDgL#gh6U$k8_rCbvR~syHz%*uz5W75$APLpp#SkAx(uo?OOu^2g6k}$z zB3CD!T7_~MRmf()?|_=6ia68)k5(TLaN%A(3}naj?)OfDzI=_Vc;M0dwVt;?*#sR0 zSK=Ym=KLnMHn1l92TlJvz|Y5|mF=X_00Pv+8NzcwNazs0c7mq-$|i}#_BCPW55W9_ z7ck!_9c*Y2K2FGn>ZruM>B-R1`99j9`?1$i!PnWYV$lbmHr>0PdrUw*W7po-2$h(c%T--}Kgo>DSk0 zJ`LY$?KhS8J+av`Zn&_V22#Y(iEiD*Q93aj3_lqziYp2Gn9l3t-!X9f?u&v-l#amFIG`DaLWtNRIO)Y%G9?;S-Bad2 z#vdT1@0_~%r-SZ{7;{rZb1ukaR>*Z!-!Wa;f@H(B`mq^Hu>ek+KYk^dMoSKV)kGl@Z_Iq_*d@L3ix~^@|<~;-GH=Al=7bu^lj)cfYAPJ z>y*j)$q7HEc$7I#5Yui^KQQTGNnkg9`Yfym#tlT43%W+$ql%@cCs*`A!ae+3Za|S@ zm%w;2+2G0p&HI<@+2;z|hv%m!NOn|pz6Uf$pn=CWupDO-LJ~B}?(pd_u(G@#_WaIH zjG!zz%AH4-&Ac^ZnuwsinC3`T@-p^{Z9ddgV~cZc@CijDhz49$ra19mvk^G{vDzxl zfaT@rF3G2<_vy;(Pdkmz4LJOW>GB0(*lG_Mq@jmS&|D{rJRnHDoO z{S0&iDU~sz?5>t3j*1j1^?__IpdS41a=rW?*}$T1&r4*Y?;c_| z`@!h{xurwXq6$<#($6W-usV%!(KuzVyqw<;(jbt97!ArU3owiR(9;Zy=`vVwRqbCinBa9-NPlK$Gnh#*D;B zq`5**dP)xau4?y@qOJSoF4h*Cr*ixmj#`uR$C;uaMucWCvrX(l9u%(HaIUa?q;y94 z@I{SZ&M_io*Yamf@~7;70j>Q>Z%#@@^`C0z1Jn@|s@qwK&1*Sy@Lv(592PPfRzXZ@9URI=hj(2@7J zOHKyPn6>Nmhw7A7lLgQ>m3&Voo5kLCdT`D~C#hKkVT=xt^gu~ElR5dvtlmm`CP0}S zCPwrhmr8+73L|BvxE1ZT)z4u@mVQuQfl}b43eb16wIV&!n*kRC%&2f1QG`DC{tF$+ zbe;?uK_;@^?)IPkJJecjHm5&t)V5N$t(k$TtIgo?tcWVP`)$IQZpQ6kr6T5|V9S9h zlTpk;HHn*-tgPMXc>kSiTc;=gHs(CW0ls;4Xqh0+x*{dePSp)N_oTBN znNDPTpD&1%3{C}HeUvh{jBT3Vq`d3>$Io8z6R@l(fK|iQ8FlX`{{Ny#ys9o7H;2Cd zp5}SpZh+^*bxb3Mm2dnj1+9-FQkk2-T!va@UG>PWj`W+YP7n2%wo++Ot-Lhpe^#LIN_qA!yJ8ll;9RAfeCed|iq;9NXo!~0NiVoOUTQcA&DZCX$w5jD;SWJ_?UUe#`Fa*9Vlkk9Lb-Fc z#%(Y6XIqn>ZBF8(_>&T|%K?UvcGF+Pp=*N2%9(s0+jom{3IzKSj{C>fGrre^kH=`J zsvBjs-^KVo)$@*BO+sah;)$GbZJH*&dG-&SCC)KoNlpstLvx;?3j4?4hynXh*wFEJ z?lGyYl;MA|zzUH+ZXEybFT)?o=9%GlhbsWAQE94jkDtkvsjq6j^Ii&X(~sz<$8B^& zpeFL|-Op=Oimu-uV_%4gCtIYe9}?Vt3!@^|nI6zhjsCAelx2t~FB%p7M#>nAglVaQ z`l{Tm?b<1_8#Vl<`$KpNo7pyDfW~})ZM5R2` zrSd$*qI3$tceTx0(PZ+DW^RMd_*n5TIBD=$DLtzqrO%6n=Tor=R2d9W|G)qUNZ>DE z;{M8DCUjzv_=E%G`LPJr;G!dUUoBkMvMv>)>@&9i+!+v1ASR37fupb>UOqs$4)GdAAYeN^3b}jhXyZB{L<0l+{;lvOVD^d_m%fJ`w#erN^1X+BiCaJ5X{p|iDxI~; zxMU0QKSVTrgnBoVHow zvVsVmT3ypWb4kr{COopxa9AkTX<05>sbT%%@U%x-TTAKRNyu0ZqK32rXEI#dDwn73`zE6T1*BR z`wuxVFifBuSODxBA>bP)r?&+}=ol`9zbD|1{R6D!2%5D% zSlCjVh5{ckd_WK8C7ilyU4>H@ONM%DY``3OZh5OhG+j8{(Rl<>wUBvJMLL(o z%0kuwmrw%FoiXr~iDTwmV0?8on9tb=QTdKzd4n}a2ig6^7+D3#`j@Sulm4xscRsO2&*0#6%8Vp|T@XA}mgituX4Pnhb7(0IQxHvIm8{MYqN5yY zsVYGv&%@(#Jvx7Qf{@cI!YeU?{99$m;QWX*LTkooaht>U@T5EH+Ga*d3z3hEnLP}F zK82o+h}n4J@8iby#P;n~F%QL>Y3K7IO?zn&(9bqX|2?|CGE|u;1RP?q30gwG@xqH% zh+n9yJ5m2&QkdIn8oN`11O<8%m9RyT+5+udDZTal=%YrkP6bY`G-(qmP1I84XhQQq zz7UKud8Za(Fj{+a$1;4&2xMiI_?{)5am$2r2-L0}@)`ZJoO;r+LkbBUX?{bZIE6ZP zb=^G)9XPJ{r-T|`g6RCyuCD8SvWe}alv;Aq8!K-Zqvh#cQbbCjvC;LGOSnUGQK+F^ zS-eJM`Mi7zB?(cvl5<2^T;4+aBU12>U{%miRkyaDPs%vJt}l1R))LbF>!sWI(0XdD zFK=x8aFd^!ERZrg{~w8+F){;El`UA5OOGutQNTKFBtdB--DS{tr7odw zx-BHsN)3Xx%8>}>NMOoVEU4OXlT1LgCv9Apesf_mkTENx5!K4JixB)cZJLn0!1&QU zY)RIJ0)KUvy9Q$kd*OJ#IA%}#<$vd!Fn1{+??B4)PJQ@Bqa2`%wNjuTIb zu8J%Lr3yzh#TsWWb>UJ($vjgXp6o0P++hgu{Htyhx0+9&?WxmZ=OvsNX#c%3pzMj- zawSRY>LO;g*-~j(52g%(y?hCrGYJr0MRL7}S$)Y*{Y=5-kRyA-r1`2Ss9WNweil!O zaB-w72{a;wRx%E4NKG>?YO_BP;Wb@B(f*+yp>30cCRbKQ$vK?0)D(D%Me41TV>-?2 zkYIKi9F8c;(2){k$I3n$uaU*LBz_fwN)rF70gT;+hqF_G6j1kw%71fir-Or-8at$0 zDSyTigAfz{qja|o7Q2^)(R}KsxDg?7RC@|St2k=flJ~iVvlu(*l~#yqsftvFa%FQE zxjnlnUE*M=+8;DLzf%_!g~T5->b!niTqNKv`GSRuv9`4`#_VJ*2CeG}bzxYp5Zcxe z{P=LuXFMh${GhR^PNAMrZBHu>Q4z00W#hSu-diO-rPku4?g7~RC~Z4&1s^gY#;f6! zTN1+8PErF^mKe05nx=W>DEeAIj$<=~crej_T{OZSY{? zi0jS~aIjj&X1|C{g<8hT#6&-Ea99n;+~MJ~>JB*}kIxRS&Uc5S`#EF>+G4c&Wry=% zR(Qw$?CDf{=0>{!X2|S;+G)P*G$g3!%7V(L(C32Z6Dk{iK+|i>GzQJ| zYfVl9JYpK^KHn(^r=bnvh<}KCN+Vx7$3c~6-jvN>bc%q>iA9u7=g0zewjsMEG$Z;AHt=hSZ*J*!(QmDSCpzv8XAL9&-{dh z3|ZT=?JY{w{QQdbIm``Cb-Ge!iZEU zx1&=-?Dwvm<{8Vg+Tca#3ZKTC1co_Y+#&j{OD`Y3b-Q)miB2T1>R9Q&79DNH+9VV% zY{eo&WcXcr<#F6X07CD7&b_x$uMp_R7kaz)j)}If3iH`Xj_tRD!uPBWM(bKJzQDI4 z(z4oX2XNI%$_y6(#6Op5tt)xz_RdiYF_6aSnCEQQ#tIFiTEO9zXZVG%PNX!zLtRPE zN37=a#}tZOyCvC|9I*e_9~6TgsF1XrJ1p6rwQsH{70H9P{v@|Oj2D~A0FJi!TA2tO z8!_x7F|FJNTPr!q7Cuc~^R1o=JGgeMMC~rwxpvZhtSi;?*KoT(aT6_ZX5MDV|61E( zP(v^~>k}=Y4;AD+zYv>?PHfg+z0ygOM5RbRIT9%ulyFzF4ifR27;IOMZpvd;Z0SX= zST+~aK3tS2TpzNLT0IXZ1DOue!Hao~-zFIG-FY1VMy$_Jt*lBD4H5+wo9^QDBU5zx5Vg>3>1+>bkj1 z7On?N;1m0Ihb$7?NG?;doF+8U3~31+Kr<=6&V<6{R8!+$dLwE@GOcEIB7byds5_Fv zy&~cL_>&S}!O1Xo?-+t|{C#0m$yLtu--uM{8zQ8Sf3H1B^#s<6XY07<+iPj*pGnWz z=_2SGd+l2PqmcZ=h`r%23R+2p)~i7_F)+KLxB?t{!||4r^R}FD9cARt@7|c$s7FD<7B*av?cqb zhPjkYl7$~!s4ILM^_1o^2n@Y3>;r6eA-1EiJELj-Wj=MD-(PzRFCfnUzc5X`ys!?V zNwEmLdoMr-=rT$uzF2NMt?u5f~5V5Q;oubZW#=X-zu zpdVnSzCKuu=`YDoK{YmRs4wd)5Z4@o=GSxX-u3Qwz$c4u(fb?I^_3rdzAr$C%5hE3dr_#`!=6kp43KV~YA z`3CrcB;-k!uuqiJ{U}e{ZzaSHA?&;@*_6-D<};)fjJagV^XuHDVO@B#6vc%8YR>Gk z*lA>F8avfxSJY2bcBRv5C3(QLlKnAmvTSmQ3saQMOJ=3dmLp*S!Id=>Xi1D@)k-4P zXizgXw^W7EXk*q;bT>Zwj3;ZsL)mHnfjC^L7g9a{h}pH?TDS+xR5!8g86bZ(S$VQ; zzHV3Sk8L7~k$y~gV*}TL(Q7wM+z0}nq|#&@)%Sg``TlzM{UTxJbe>uZX+X)4dpG$C zWR)nnQ#^RhQJ?V*@%`1q=Vy7XT0{Z8iy(^?iU+6t+c9d4-nCc12a|3vAyT{RO@`Xr zk$Yv$(`i(7#L-%eO~JwvUEvaqO946%&RVHoS;7#hBFfq#P|g~Gyiweq7`m_)F>yct zZ447FcRtE$Rxvl{=10*%6k5zy<#LjKU#$3 zNVf_Z5H~{l{yS}$fUhB)Sc4;Hz27zuaHvsmI`t_9Q8Ogoxm`iOdu-k&XxdDWL(%Z? z+k@pBN6JpNpRjB}`}JjbIjmOCx2DJK`?2Pmfy(4|T|$ZE`FKEq@LO)9k}%`qBhlM} z``;xo@_4Q`}hCk6||w$6xEZf&(2P(uTD=jlwxs(hrH3z<$%{fM=AIJj^`8n zBFrTW$mK;45sCHQas6|l9~y)p{>wiI0VF7vpTNIUr`XfPqqxUiLQM!nTt7u4Gymi6 zs4LU+dTsmrisN~^CON@LbN{~n=aaYoJ0ZtAFn3<}MNYym;j0sfQu@O^Y$_1Y&;TH^ zf;P#DGXMmBIF}LlV4>IiAXN&*_y+xT`cE?HTie5=%L`1XBpoe>5n9Xh0$;Y*veTbv zmEvIc>xJN>tV#J;c*X|%Av&CI@bwSI6VQLeY6M{=1cv#t*sYh$t#9;KGBQG<62^+i z^--N3P_1pBZTH=o^qkQ1tpoNaQoV)1;=NTMN=Wx<_buN?pHngv z(n3W&GD1as-zz6MU%b#?)`;SzqCu%AquFv>7jA5*nxhfW6-Mk# z%yW?Q`x^B?Og#KE+`JvmvEA!5UfawJy+x*mdo`KW-R)drHdoDe?#z)JjnczA?(wv5tIx955TpH zYHYj_a&#G`C{@l79uLwLe5`SQAHXp7^I#w!+-W`<#YyGJS_QydBO*%tBffjUvuuJD-* z_T9r949DD1cK!=}`e8l`O@wD^0}8uo1bX_YiLavW@9;cCyT+arFmwG>hGF$&BDhg#h_##yaf<%8hlI>Wcq!=!HL2++=~CK^(%C8=bjXWVT3ESne+eR%2yVE^2=rfwU$ng2 z{QGki4@*;&GU`#%tgbc_mZb4x%>9lasairHF6hf!F{d!*q65i;s4D16da@Z7)5@{%3wEnamdR|+1T_w)XSTpb~g>9u6 zQv2z_0L)%)?)O9Y^mN}7D^gKI_G(C}NbY(1s4%Hfy`)`62-Zb717%ZDSyg9ebA3Ep z>s@_11WNkkeu^ADDTX%KAqf0{)roW(7Y8E)r*P3M1t}sDdXv3 z+v0mYIAGS{UIZArjG<^@Y zuLJ2U@tb&@_O}m4Ck3S>z1iBjTuURE!EP~)VX#maD$H}RGU~gGCmfS#nk$=;+Cybo zqLjr6)9L9aFAN#UadEk0iX)z!))68m*rxTL_Pbd*XaOZ#qc#xU0bFBe7?#k~(A||x zBm(a(Jyi3=BnY{V5N=8eg|`AWMOT($kw)2i24ROD?t<1HC|V(A~SNQ z0jf5Vq`%#&MZa7^lZ1=zo71@3{t*L!;HO z?i$x0%(nR@f#=%a3B;Zw_L>*03#`G#J0Qi+LpD~~5br9C?jksmfzD7qjo$P6XgHs@ zM={nrFgiGFkg!_d2EOuCo!A=5+NHf}w+U2};cL%F$cQT5eC&gN@vUwrXNqTq$~Flp zW|(8>DAnNczr3E{^4t~-s$f!3sH29%VTr+cfl!Vu$Bks^CNXrkp3 zdH>|U62JCfkA!=rzU!@}+1k=AJK=%Nf5kqEcRCr;f$zNY%r$iVb+gn{_5L*iM=2I# z=IO4<9nNiu?K}giv&F(n+N{dN``BGUxjb<%blBF8)`1rv7?~IlklESUF9YZPS%SuU zx>PD`Bovu3!DagV0Uej^bL@?YhlTsV67cuNlVCCVE3xJ_yQ9EJyae^W(M9Uc$uCdW zeTbi~sHXPh%09-sM{$4bX5`xUMdvl_iQ$?%=2?o{dYJW(q|LQRH}uMm?W=9i_RErO z&(|4W_eQPF=hFdSDIe>@Wcf91e@hTI_s_^-ozI2=Qg+hPpUYa+sV-}|Ym*`9*Tqo; z-VQUBlJg#2vNDH5zpJcs`sc2pFCUE|LHAn1lZ>OSSw2@ajt%>VlW*{ z16#LonKuXTKhZ^Owydn@2bn>VT*$D1i#xmXU;RBNh?7>w@IyP2RO6$g;x@0hs$#B} zt_?E-#Y_~L{^VAidbzy5MFaa(1a8rZv+0+HRp&lVD;&r7o|)4VIGZKpP5Ob8*F7pK zYi%nmGk@@{f7kz|dt3#tU#6q(G7?#-Y*kh(A4X_A;d9vBBVcXt^M9kcli@_DK5_=AikhLo1~Ry>ix$ zb^ONZu)5EuL>bD?=F2$!1NJXp-W*SK#I~F=ol~QNUF~|c+Q?Cujv7`-IFQHNxqlO- zr1g(ZlU=*}J*LsLoR?7jXO_^r?58NexS+E!Gcs{7##0tkE6l<}n{8GHM;k`VnVlnN z;I@J_FUl6XBh>C%qaOHE(Juu41`knP{$*8E;Kyo@uy0T?XN;zSjnEgcEUbTIWd=0< zzz`APUOJL#LWr#9 zs7;4Xrv)v6;u0iHK|rCNl$ZueHRG(7e29sFbAVG|zHaS|>v9|2vzB$itmx{Z8zCFi zAMI{dIH1F?2aUKsJotfWcld#KFbZwm6Dhqo;f;uU4FUD$mb~ z%Fn7yagw%V@PN2N&p>x!JV|cK;&`9*q{N1|m@M!KXL1bcN+}|Y7m&{sak$|xSNskM zhLDTnA8c2wmEMvX50aVqbui^LU^AD2WX`%Jk&jN#Jg(;%3VmXQDZE~93k zFV$!cdkznev{TB-&Opp8oNn75(c*F9Cre$J;gV0^D|K{ul|FRr0KDHnYkuseLqX7m z%Mx6=jz)CfSe*@KpNZ+y#d>2<5P1E1xAy5KVpi?SL@)r-nFmMD)jJxmqo8YN_;SDh zSMqf%-`aR4*xE~6dRTpL#fpfyj?$re|G_kNXhF6iM2P*^7Wg(gTurIXT;1}jO%X|l zY3Ro=oWuD~C02cC6oK4#o>L%ocl!R>-J#VwXCrwyKPm>zsffEaNnPSY%L-+|9n@!> zF%Lnq@c#p+Kv=(xpeTv%30HJ>ZPt=Ib=$qDy`Y!UT^U{d^s4i;-A1)hhfnBiGaw~I=9YOYkyhzCH>T7uuYp}&xWvkKkw^zbi z)5==va^ni_==sTH;+k&;2cHm8!z4h9S4vKgdd5(bCGAltC5I6_BVQBRyxa-jX5yQykn z*w+5xiMHH~OPhU6cDg1sydZ!iD1LK@AR(we-gO%c*nB0aw6}^%$cuz|KT?12I=wWb zr>g-^=xnJXRtD$4^Er3<^#%9h(hl0ZiFc@n^i*`hRVyVa)gQJS-VXCf!=x4AG1!hF z5Y>UndaLg#ird3;m%}c379*%s@<-;o%q7J8f@=+r2=h7Sz<&R8>_})}yLBM;q#^ zvtMa@`JGt6A@b2V+bCO?xTe~WYB*p7y_1>76tt1r@P+YLwme$l7=6!O4!`V`TQk_y zNr;R#QgPvH^YWXPl;S2NA`6wjL;{Y>8RO8phe#N#kQSae?(xb#@v;h!iRsO>zpze+qHzOz?TgDoAO+_;rag=cleh8k~oQ`u@PWW87PR6OFfk_hdi zOt7!G=E0%ob2U|qs6x~<))talU*#tPdYXXlnb@qBeh3_lF?&Q?qesfIH@If%I5^qK zMFuWN*1XJ&eyFSQ$iOoEJ*fuM9TjQ3&1GS)1U?m0*Nu(q%do$oyr?{J^X2(dEcR{@ zqL)JMFM>Zh^8Ea6Tek>@mn~^UP0c*p@Qey{IHLYg(*%H*0c9nCp&p=P9Frx=({(Mv z3Q1+?uKX~Vx8K6HB7M)2@7P^npOlo z$HXTPoG?4HO|q&IyNrb+n6Z-K)19|-3kGUBJ9EPgy+lYuT;y4v$8}(0 zC|Pcoww_d#Hop2e{&^*;u)YP69g}}o^MTbdW#fRuK1C%%NFpIJX4>C{?f-7V=|lpK z#^ko;WL#2S<@m|Z8=!IlQy5@k{jR<}rL8{H?Vzoxvi+^l{D^KG7DTl-WrgXAv&lCX zJxa<+BO}b#@wAKC4^5O^6L!AruBSk(DPZ@@`UKaVCJ#4VNXl%Ye#+J2%TB=Yp%R_e z$A?OvRs!xbhDyZl`0CY!O5;>CG4e#hrS+#(x2S46T}mcJ^$Mnr=^$8%EAe)gm(^c) z?D|`5G+i(~O9l~k1p)eU27yW!V-Qs@+R6Y+*!=ZV!Yd%=KY#^LDe;>1CJ9x!<2J8x z5fo664hMTeTBGcDsJTnSqGFKLsg)fQ!$aMj-Iz8kt_xRR*qdY{-yOZ6wmb zV50*Kl^u^Cx*6a8mI}_9poX4qmBiA(TkDOD7IFx7II`>Z(WCdXiUJGD0V5*-W|f+c z=k_u&_}6<<-M5&pWQ-0+w!0kFc%0FmRNCb4psWB}cg*fBHi{+~(K5@rCm}7+gtcJx zo%u^yr_{QJcilNRE^ghph&E%9l%Wsh?3`SWdefDC*J>T?&>!sFBI$yK^u)!q zgn2jPpGK#9>HGjY`UgArnX8i%Juy+JcaIu}6O#~rwu%Zy>yFzAS?^RCaaH*CGpk$GBU_r!!Xz6~ww9YBMlAUm@Y4xEMBwazLfE>ep!1ai{x zWxP9$tg}mqf-g}6wGZzo$r=23^m-UJYB>^~l9d?Z4uWH8!a4=>h^f~uC`+iy9zQ3f zqe#ac>fZ9E!5N*2b#*TwX_zjQhPcSH9KUc%u4{PPo$Xzh`#H)7mIDy?dO>y93b!4|EZ2lMqkGLnq!uBk6;Xe#xPzWcS@>!CSAw ziwP4ThrlhqS{!V))jfGvNLLeiY!SWv?DQo-rO!A4CxA-NR}CtGV>O-1MWSgYIVX$D3j$hfJ{Oa&>g8zY=_wmKc(b0?`FAiV-GfF9~* zaXHI`N_NKbZPX4$ z-n3bHlbp)N-%3g=eY4GZlYK!(*Te*;i(7094>H-}8owi?r)dD>_Tz{_7U`a6oYR{8 z&_gs@UQf2OgR8;y-y{Q|1HTjox@i|rv4 znsc$~PS+I`L~#3F>g!+Y$!>8CEe=htdtn8;VPW_l5??R^a+iA_gQK7_OKH{0B|I>r}~ zGh6E)-d2#_|Mefg|IN8W=ThVP!9Ly(Ip7!`1mUO>FBci9eZp5Kz+nuRkx-xa&`O;3 zZn=4EuUBH}>+G5kYuF8Q5t^I3;>vEaA{3wMMZ$6hJ%4PpW5q@RQM#L%nznzucI9pu}pcc%woTT zELKN)m*)#?b~ZNQ?k>xl%67qUd@(41jC|Y3e`a9_7%d5vARuTKQrZwvU3}-t7Uk_< z{YF}Zcj(IQyc3F!%Ru@$D9IlB`j0=()7l8gghC+5;laK{Odd~S1{O(q?KF`EJv^H1 z2N{W&eEyx5DS6n@F<$7(?kMoR0O?&L-~%Nrh9;;`e_C?^bAuV3vOrcUHq*=U^p6=sQXM&6C!^KKNT;y4<$GvDM_BYpb)71@)Z|EaV%`ZY@3~LO(sVjf|*fy7F zIFN*RI@jOl$iY!Z`TF65FT#Ryn6TrwkK6g$4-67x(W}`sPPP zDOm+&MMWh=MMVV#B}D~ARVBG=`c_eqt(5kulE%e**bmZ@G06_+MRHR*rlfe&*vvnz z)IS*xXc_`~7Fk8@1M?CkOK>>K<6s_`R`D#gj1NG~6SFbHBhVPTS2{)kPqTou(!k^r zK<5l#Xq{1n?Pm#kwYa8ugC6DdBdJwEIsRwQ3TZ)c*EgG84_gC;l|XsfGZPa4W|jV4 zlr{}Nl(WLFUvF|cXd9bNsl!rI<85_s%J0~_-sI4Ud-tNV(opCMR3EMf8Umh-k59a$ z?I^!v&&<^pOhxM;9*dhKzH7_@j9`GCbykr`>y@y5o9^|x95`VF=!?+Q?0kv7S#0jo z)|i&k&S3I(V@l)!EPdWG^6Xan>^Fu03<4Z*ptJ-qJOk*9>y^N#5P-HRU}BYCv`l%f z%wiRfD#(>$HFRHAy?2_rOKNTYIx;sup1 zyj_bH+3RcGN~^@IRyqS5;ts{~Hiuj?YAuwoIGR&=Gow=oh_@~h!3=>jCOoGQFAXt~ z=Yu@%4CHai1sLkodN=+U+n_N@Ue()Z8JjTP+#yr`6;} z$UaFC=;V8cRxy4~L#4}ml@-2T_s3tUs;GK8mv`n)!Kr@kP;#qSn-uVjjzkZ1@H*U0 zsZR*mVd}BtT4)_1mQWXaZAYl-PLG6!kWP>^_NkUB@o9CT(kM8=TB}w1853|b?#-V8 zO2|d`O>L3KSvFMS2OVR}#Jri`PEtWeGW51*B|5Cnh8}&3#Mu^kfiJ#=|Mq5N`VwB7 z*iaSZscm4j&+OWxgh#o}ITcM!RXHuizHz~~G)~^rwn)rG4-WI91cr!6eQ0#g)!Mg! z3WrmRe68<4I1`kM?C%DdP$op<^(ANXBEwbJVJ%E?s&CIs+EN{HN7nheoPYGdv9|YT1cnoQ)6%p5?^8% zfeh!iB`S8U94al8u=of$C5JgW6+k6(aS_arpR!y+8loc4hkBejFJe!pVyDNrfj7qR z^!slr0F6bUlC}dTwgltUXQk-MV3@=$WpWFwnu< zPIQnN?<*jll1m0FKwMP5XrPWQq ziGfOMwMwF(5|-49{RmJ=bg~R`swEO|&in+Ne_Ba^?y38Fx=|VEfhpUa>;s9R4RNlz z+V@YGCuE37z=>*c=HbMDLoKfWN-}Pp-l(N2>-eyEpmyph#s)`XIC^n6a4#b=h6qXR!lCRA_) zl8Nux#kjG+@qAp~V_RK&6Iei0jnMS|5IC0BmiN&5q?33K4Bn$66EN3>T940KoiUC; z_q65sTC3gGKOLIb(AP5+Iv3Xzg*z109Br?+{uHDIA7L$c8yN(g<`?{yi%ZRjO~NkD z+0#h(;i)sxne}abJVyMjD#IFu0Zg#r&z77t8_!+aDx+*2{tD~DY zUkuC-d|d_@XgC^a21YNEfUD&v;9vnU!kXeI;BuztOO~`o^Tdp)`G$CO`U6unZ*Be9 z^tKWD%+AisOu(%?sl8&0g~oy=RlR+rVrIA5J_wG7FHFGkXN*dV9>Y%E*U^ni7xMIY zH(gzSP1EPa(C8|kSZKKz>4m0>^R-F(hul6f$b@#69sRlH6i;(pbSZ23Z-n!bj&$EsH0MjKse^mUd6t{+Xk$ z<1=09d-liPrXk6A!8{gkXD=FSum|5vXh)Izm?8@Ee=hnGpwefWfWwj~*i}!!i4DQS z%P8xPMpOs2W_ZD5?bK9)BGGigPPX7=FIZxqe_wO+aTi6a@v4TSd)Pz*%tU0lNo@4?b6LOZADL&G|=e@U4v&W%g#sd*i&yUAW%)j7WX9aX4kw3=A%>7ppF zFO~zHJDkv(IotZsDvS|&L#yb3KfWoiXDVy=B%fS4^?ff`Dqd5(rQqSs$#--Q-%($v z1z%m2Q=VJQfZTGRvJ^P;K~QN)&L{3Fp9#$TQb*fo*XRp(%b@ zcODNYz?Nl=sRG3Z!#BWZi}ouN_fY~HG!$u#2f%eq&#RN^OEEJ&p^M7%jA z=X7ANLoDXi0LT!?bGoeC#F+o;lIg7@Yp1Rf6eaBGrkeC}Q++^JWCYU#U{81)a_l@-Ltz&XU~0dddj-|C-N zm52UQ)qi{b8MeM>S}>C|aO6Ec|D}iagX>0NiAZvrh}y58hd$Q?+?>yAP-zZ{UDyvj zTnUS97T}qp9oy8|l}CRc2}-O;VG|Ln%_4fYYXBlgJF+tk4qeOQm{yZ9>8?r zrKF--ocUe{(1V2(SI1V@h1s2Rw=}u;GBLXl)mYJptj>?Gn9q4wFgr4Y8!NV}_EUAz z(>Z0}q4uoSxpEB8n~h-$ZFz9{H7ezOZ9{EkbyayKqOQCi-_$WUz~6m%1u~h>t-idT zfb1G%fMbK*`Q>+Z2b*rTbP2oo5|vy~+fY-5;B6LA+sSHatci%wRdLy|fJ1d-c@w#% zy_M7Rt|`;F>*v( zRiLxm+e$!&Oc+XOB=3AR;)9C-F0rx8a$ z^-ZlHn;gQiK2H49F;B_s2B{NWr1+C5w-mK(9%w%($imh(SHFIzx5ahG5|ZIhsX@5C zhLGp!r1S^XKgu8W$Z@CwJ7>U?Xf`kkA9X-5sn6@O^pzm&cNsPwTD zaO}Sc{yv8U++y2Bawi$BOp#kq?sK)zX-Q#&bdbaCtUw1^1{o>s*`e^Qw7i70?2g^r zv@A}YFgq))>Z)!MlP(mD#I-o{y*hA0KQgDa2TQKV^g43q`0w{^m0TyQC?zAeT^wh2 z57-mZ7<%!HgpsPe`|X^ztl3QV{89W)MnOtmK~YXoPEvNO>bm3Jr#;^1;dztTlPi$P z0&d!##Ra4hCYQu+eNq)Xlmb-*F99^gG zr++)Tst%|u1hh{9C-~d_{p2y;)AlMV=%q1OZOpXWIrc~Q?cDP94|3}_%I(;*?X;zM zP4Q@A5L1}rcF67c@AnF6!Q4h#)l1V5fR_UBMK>oDu?^|soXyTjtKQWxd+{EhT#pX) zIC@ay>mO7$$jK_nD=JECIiR-N=JnOE@@8sYTaCZx;RC0js}fUJHy=2*%O>RV>t;j` zgEfLQw5IQfpQWd7Fp+vXh&F-Q>v2*hEZ=bG~=-EUD@R`&r#xDV9n0l3M%aHb}}zNlJ<5JOr{!I^T!6 zpYpk;|27>%AqciS5HSjgZL)H*@^T8Y;$8g%WHOiAVIH?T#d)2qao+6>eUhBvXhkDFDJKAR#sYay~oqiA{SCYk7E?1w@!%in!}tR7IF`u*qs37PLHzHCm>{3LSdm zxZ5@-jWEdoX_HO<2(g}Cj3A7$=8aCs*B`Dc(;5OKGw0YwG9<$d zr?&qfzvr~--Ml*sp_159i#gC^aS1O<t_77~3!w+)49NQ&5^x_5m6)Fj zRGLbbO&xZ-stLHie{Ko5#gDe-eH3%x;>>}g^A9wwC@-(Bt82#M(4C}K44DAM5h0%9 zTAcl!jO!etkAv*Vu`w!|+FsGbKQ6SQyh1!@4#*v1(Rw=?3GGO7|0r+u#H8p^lvfMR z0j(52f&2k7S;&nxz?u{siOQxEyO2}&R8&HeDXVEfP9K*$GRY)%3v;M1Zy+>xP=^?z zjwbynW5h$f;_}W!<50(_?dYc3a!6H6YHCUttnt28F*?>qK{vHm@D8%&9UKbvxgOcrJ>12F8W;mvlzwz$oA6aSVOVguCRglX z&$Gwn@{bgqrzswv+J^czQg;W1ifQk|cMc7+murnv&#)wQ}6H2tFL!;x)AKiT0uJ3H_ z?5A-UEYUih(b`-YmN!+GRn|1s;|Dr~ck5nc)Z(MoA`)Jd#OrjC!-FTqOG8ZLS(e99 znIq66l}JE0BC9HE1T6VS4p%gGBMHO9?3t(_{sh1dh*6L*z@oP5^-vs$8^uWbYC+J!`Z^AAz`o7%=x@fsf8+ z<0qSLR%Uh3KNlnX5}?v*B;Z6R_LmfXj$!%>?w-0d?JXsZG3^ zp3mhewMJhIjS>*8zDnV?vIaic=hH#tvbHPuBLm z)}YcFtXHtrF)Gs8#wu{%2tf#DhV?*a*6R!8g3rn-Em`@A1F#anf1> zZjIJxjn-(5*62&8RZhUog-U;!f=UxVYpv2h`JewW@i&k;HbTJlx8vxfvA@y&{?Bpf z`@jC3!ycwFhFS-?7{-4M{{_19zbF1Y&R{WH`^M4ze;Ne;hrhMb-$yvCo{r9rwl3Td zNSgcy<1d07+~)(8xEzp8WeFCRmz9^7mDV&MTYH$~k=X^kzc^Y>N@BT-y2<>jlhd-C z1Sg^I$)9(!LohlzG&)L02bIC-6S0ZF8m-Y9eJ=E`Ou~L~3EwXPDlJUF%}d7d=8*n0 z@t^I4hK#4@AGx0od)Gj(`TIZV5LDs}PqaoO@nWXcK*5Dv4j60asM$@+5NdB;Mg>G% zTT`yl8m-aiLjTIr{x5DB!55H#`+ooc0RR8&eFs=m+1B>Vom;=}mMJQtR8g^vjs;N^ z6zN@1QBg-x!GfX!Hl(N^1O-7rq$6EAQWI(dBq5;$NFyNu5|WTc$eD6S=gyt`{O2T~ z1OkGCeskw%dGN`Tb8^n!YwfkpyZ2gqtzQ6@1brij8wIiFHwKsJ#jqRL;Kh6iBcUNL zOz(8!MlWOmNh*U%LJ%sYO@K<2UmsKw@OWLA)+RUtSzl9Mk3_)n&1hOLPb^P$D(y!L zGA`$xHQ2m<)kptW`q^iner05R^#60H^!8#MvnxQ64Ha)OG>^$eVJT8Ju7}-&!%8VJ zs9c!2Y-4uztcoqnjy~jE?RD>rV8$k}vIn&u39P+Nf&ciDB2r$YPvPh)XX7uZn zk5KN+#%e@+b1Q=>l)U#yh*2RhOy%|xT2MVG45N=P7QIQJDi941(pq{*&15E5$Qy$f zqreD5#B6VEZiK@THLufDHa5dM`mlXM{w$-TXJhfE(z}hlcYD^an0^aT>D?S~VhAcR zMYaCL@t5_3%MIg)8s$)lnSv||Gd!QFf1|0GGAe?g(kuQ12OQ;B>459TwtCw-8ZA-T ztgF6iwVvvtW4oqeCpYl)fE@;ZFtZ8Y%wA23ocEHVCvUE1r zOe_g(6IPChsY5+IvQ?#1P%}20sPV)_<#9Y-Mi6mZjP=%lY2M7k-t_YLqOlN zp45{LJ65dPYI-QD1)SQ^e&N(^bO;!(CY9d_+_&38UPwe!T~kw4O?}g%#ixy(Qqzg- zzW2>rJ{4#SZ;85Ss()jfO-f>WKXoi)wE#5aG+bfyUf(-=LNlAlXj!Em=s!jpCH>TX z<4ubesj8`IXlkfwsxDUBv}f_@4e1tfy25Us`P9xXATh zDHA?lW9Pq~`lamD);yS#CCt((C9f4y<|*?Y7QjnyFM=Jgf>}AGup%MdNrL^Y5 zoUCEC4i778o-{Nio;#m-{#{uz-vEIR|s$tbtuY1Ipw;oOV^a1HmerIKr$Em1O7mF&ohWP*+;3eap!>m3) z-+yf+>HBbU|L`=aP%fsQ1${dT&>LAP7gA1a*40$`>qbk*NX(OVfc^_J@r*L1L(gz& zC--Y>EdA(DDqpCo>S$@KP}fk=RsDnUW<8&?S*TQ&kSckfYj)y3fDnxeJ+^Jp&ZSx> z&KKeep|Wj&tfp;Ndr`opb)K%9A`+4GR;Z9lzp(8EDZ%rpyYc4Dx_?qtSJlwe)>PM4 zQ(3xt+aHW>7~CkS>S4|9!3x$4oba$_2YTLR?8&W%+`8^hD`xnHLN&{cLV^9gJ6wrXvwLD4{?1&A_+A$@6@~V;yD#^@&l~ZB? znm`8xWTt>gCIIwAbOFrC>dl?!XH{Q100=TWOQAbkAIz;Fzt6lRqtinr<-D3#l1cyx zN5mWGr4CSN!(51cgMcvW1NHsaMjuk{AD$+zKsco3;)i8RDae89d%jzDT5IfeycvJJ zl2A<+3$@j_W}Qc}19JcvCa7BpVZPHtwqHhNBO*`BfJ7ux7VV z^UgHPs7p&cq<+p^#~fCC0SS;nvmzn_@KcR3&gcA!&p+Wd^#hU-su<~C;b(8Y!e;%t zb8b(vA7h$I8|#tv^~fevQASSa@r%J1Z)X#F&`^NnTWO*-OP*}arlWTod!u7e$N!G7&sDL-Pl~+(osPeLiS1M!=nR&(E+}M!QnBOR064|3QH|-A)@gZbW3gd ziz5Hz3Z0-1gZRNF`Y(L-^aCT=q-xk9JDC09=T+3n1V<<#8G^EtHlQ_Bs@TrWmv zJZ)<&!H`6kWBa~EYmGzfjklW^sw zVdtwjs2CU-;InC6UGz?8#QV4T|08MIMYrkw_#~+pVZgP>)TgC^|p5qv-nV?d4A0VcK7j z&^Hd1_NV3H28Uz<5;HonNK8G7(A6(uL!9JDxFjMP)m#m)LmJv8WBhay9T;ZEy%iRCfwl%CM<=_1;BFvD-D+q+dmm9rRGoKl~Vr8_5jXWrqn_H>&T2T<^rQU@i3dw$L?be z3#5RA-z97bE}PtUR?pPJtJC!hL!u0cxNn{!mI#^5)|PfSRO%I~WGX9xjPD($3&7?| zq#|a2YqR26?0A7>E+jk5^s*ZjO)NVtFMwLpLc)@`;7eEv;!GZiGvzFXIK&c2riy<4 zQl=iyDIvlcv22L7>6>8VN(Ie{vsU_{?0Z9j~?tY%{ zaXVdyy@t0#;e$x%*0NCnfGj5UOrtepkv+hWSjjbHGEY9LQ*p8hfX$J-O3LduI2Ra< zYEL=4@hcBC!|10ieRPl`Tq3(0g`Qy)DHa!>rIfK$A=p&mP&+MccZRpF+3^s2e{3CT zh)u3*xaX93?eLA_o=3e>sOAR~)fa6YHlvG8Y(k@^w;lxn2W3WPSVF0+nH*5!bF!s#rQZTn z62AIY!f7b-Ja>J&k=>rX`liRX?~QczL00rhS+D*hLrbqDuZHQw=1h;sX!pcyGzaZlq0KT$`=!@9qd3WPO1H6iCt zAJQ|}Yp~mF*B;YdyNq@?y6FWV&J+!|_DdkBBp783_A)y%s*9f-y;pM3|9N)GovetX z2dsB$?LM;9quL~m-@^QbK6nPeeb!zadcYbqHyOnpL$ZMG?tV;EoYm%ofB5~Ru2&J^Dh}a_}Vfs1D6QC>!F2i$al`bWh7c2 z57>Prtf039LMSamjUjjK3`~|R(p6ics=j%#ikr!Sj11y%KOhD~WyGmFzuvNsY(UB` zBvi*PG}Cj|JwXf^3YjQ1YDQd2U4w1VFPjJJ zL+-poOAzs1mr^cYPCz5WYb#+6;2g{8Sssono0c^XjNMaZjk<=K#xkvCe_Nuq`V*b= z8&2e8wfA$Tns~X{-4{__mxRJ*cwfu7=ADE^MbuWn_SrzP$m4ZdLo1_j9?8{AO5?!%jtRJj>{2(WhC{8fA6!%6y7$?lje3 z{l(JdYU<0?)z>arYGPn}Cj>FjA`$_tjE+Ls0h>usX)3QKJnlW*;j^kF467rM6kJoB5Hd2 z0mZXM_AasA37x=*xRHar<8Q4fJF79-L0ZD61IPeGh>5Mink9=>)l`?Lt=XYsa>qKb zv7!&;&Y-mUPVJ<#rgyFV5oIKfPyyo1uiQU~rcc;4*@DUN@-22d@gV-jw$L*xH?KUT zXBHEMrniF}Qq&`CJ~U~uKk%f!JqM3|=woE+u*&Y3Z*DNWAH#ZGd>(98O<&y|UrQxg zQ#YnH(_Iq~RLeq1Mx^WvLV?T)bs)W{oi#q>z~PFz!e6VTUJb))R& zPV>p5$Umf-ij+S~CgqyBN%@X-Kz%t?A_6$TNGkUE<+JOY53M`>H0Mb|dEzmLtH<|; zBq3?-vWz3VuCg+B^H5vUGeL#7t9kfgK!HXjA{v!LrLcrTW${CxOp%QGkU(YDY@=&l zPLGf+p>Zm&m)_iIrbJWdO(2j0#KS%L6>r8Kq<1R~tab$=ywWk4bT4>-q2-QM(^!uz zLW!TtA)oE~M=#iIJZG)CW!*9r)wL>1&3D<~4Xx<|c?bATQx!2$OIhik0NjyrYgilMQc*tfs5~#m-4k3A~a&*p^ZJ^>SZREz^J(C(3{hXx#w(E9)o=IH6@`qW-ntb9@nl zLaDtEzhiq~{W`7xS*Epo`Er$2pZ{*hmz%G;7j@?F`bGv4I&)ywhKjo$X5I|}f;?Ud zCij-z=BviLV$(5fCNKyv9-$+T?^|rAuYK*-3vwkm36s+M(FIYa=Wm-`OTiQB0TLY* zToquu{~cN)+tszl6!xM5LI4hSFq;FZg`MCv-9yNr3i&<%qOQ42Q&Uq_d-*J&k`2tx z0YVZLwPO6Yy$n#B5$ zQ|`O<^wc%i=&V?wy;^hehAjpsTq0^Cv0~CBv#8{i37W0XRvMV^3M}->Zp0P_)(6`= zZ&oq6ghFpsTBE zpseKju4}V`O4Ia9?}$S*GL!)HLI{E;VW*-p+`aWS@A;#u?oy3q>dRG^e7533%e&zl_G+n-H zQlok@wR$uh-(v`Y)Hn8gZcz_7vJ06i=ab3B#&dg`^ zV_RKfQ@f~Cpmz`%UlV-FP06TGYD?dRehW}(@2X)dZqfZ`m8*m2!vUuNl~VIsnJmbWgLx{Wi zcTSjWKc#Q%8!st+JPLZx0=?{H_8 zYf>q(k=l`vY`eyD&1$ZcCUgSWj+VR_i|c`gw;$(J z<@S=Ax`{olwUn9%#ZUc?oOpc9tGudYPzPat+7%X(8hGVY(-cq88^0@2m_gjNAN zY^MN~rt(VhiPpN;GR@MPiM#?H}oDIS?_xF_1W55L=6gC-@|W0)#pc<-MC{kMRxH4M_iwtypq|3Nyn5w zytwV;!A%$avx?KG?R9ueWm@)wLmnZg&g3;krBp<@ZaZ~o)5U-{XEPrhbh~%PC$+u1 zVTvU2t^cK*uQGI{%31#uf+Id+Bc#;DS1kxF8B|b-8 zob+{#OqXsyvg7MZ56WVj*lnEG>q#K8>Y!nB<1BpwlxR)BkrPi&+{kYjtncb6jJ3I; zM6==pkGLkddZsitMl@80I)Kv{1qU1hKwWwfV7glOl=`07r!5S~0mtj*Qrieqwd4A8 z!^not&gX-rVLo~%9XDO{o4b^eG6*z3QSaBta$CY|H@%2)VtSseObrCDS09fM$w- z(H~sYD2{>^T!@Gw~IO>=|2&_`AH~Y3Ois`v8N69Ui!)@F``9)pBUE7Y(a z43phq&@@c{6d?^Dg|}pQed+XI^{I+-5b%!>rRAr}9~@ZimAmONmcXgtB5!-^s9Szv za3-cRaR`uYMxg<9ZY4oh>ej*9$CK)6BigHB2W`jpi;V$`$!${v9MEk#DadDG8^8A6 z=pBnJnHr?Wif^;8MwsDhoaB9#I=xe(5S!*I_lP67`VmG zy(JSvm1V0+_RdjN#O%hV!1FrlHnMvru;klxXA#yt>240t0pcWh)mnkHW9T zJiO(kcm1$kat>L3OC2L<@C`J6i6~tCi41)rZJnycMN8{yHQDzSCRobru|8RP+{7PvZ z6~o3qePq4DeW%{})Q(uDVo0pCx4$VW+(OIEVCq@$nKhKE;7g0_?WW4^!P)I!UfHqh zc3y30N%2kFMRwc&>gE-PEELM_AeHo{B(=ueM0bZI=G-v<%y!4bv!mkPq?neNaCk4Z zS+>OS9_2nKO@C+o+6Swug&~SO4z5+iy0_vG%5d@ z^B1;ye|5V$XDL%!drf!4^`S9H zO2@=?xVI2^gBn8R&5IgpRx$}HwPxednM(V({~wMaDwk@?yG77o9LXIYdz18Tdh6kl zfozH;=li(mnC@EX?psW+8v98fP=BG+*I0Y)sih`Qp0qIf-;K|slHQ~QYJ_eOKqWXR!fB%z^)6v0Uq>U1fd(ef_qaLE!+f>J&W+ybB|$j|n6(|u)+wc6wonP8!S?#1 zQ~RTjAB$}4O~A9t0&I; z-gj!rByeL7b8vc4-4KaD1RXR!YdwC?5jjt0n=D`gRN4otu7%aU@I1PC<;ss$m;8^N zp2>ax^rlLtfDZkP3rLE(ay-sxYp(w2kE$x_OV_Shp}S_yD$TVk{<`!ZDj$Dhyma-Q zpn7uaRO2XNN;%K)g^>He`e0^JCtnE8AV>2E#ePQ(LAeA?;l??0oip1uhs88f2mml; zpEYO)QG<=Jdk(wJRn<4FSgEr@U3cl9RF(kVKq0@c{A8W?YS*~(`tYV|n3Ds{{xwvZ zhF6MDbkpDVAhDg=z{KR7b&flJENUj1M!L8^b-Ga2+^P6dp9Gcg94`0*;AcRxkdNe! z8AkGS{nGhlo!9D93FXa+=-LM-Y|c5_+)HSnbtvrS;{bxf&eD6X7H$qo&q@lra;K5q zr>Y7?cO)()7jx&Vwwm6@fB*cCNA!+lrSt;~=;tGVup0m>u$4DFm#c32`0t-8 zoE-*u10o5OHg@t1__f@v=0)z~n!ft|X!>i704EF;0sBnc&CG)1YG|F)?V23zmvXZ3 z8J9;daGa(>yzzqeFdeL1Q+GOPN! z%W9o3Kl zJ-4S!4wONtG?0kN_O;*RXKI_0i<`@yQ)xfI&%zb@9X52coW`12?oZDf+iBP`)Cdcj z)W>DopRBpK!mT)mz?Z((JFBrb$T*G{%d~7Nf5xpxLqt>g-Tiy7T3AG;HZdq;5qRJz zifN8LXQ6gnZ{7WH)F46D2JX~0yq(VyfZGA^b6RpQxoT_8*_O_c-lGHV`w0#>*}~E& z2$lZz9vpBNGFk)Le)N4C}D-7cXkpuslSE$qpLM6)gKM9yv>_d~3 zn zm5OI55|#==C2HDy6*id;j?4n4jFS)mdZ?(h@O6$p>rO`^iOED{@~L%^P8)nOa8aG3 z9Lh670nE`BW^p+0Md!d+@F}R4qTG%Qf0sqB7AmeWdHylAFf#|3-Tu_V32l(F({Kf_ z{g%oAhm3|vc}G;y#$w;oKLU{Q~(tXKwk7P^s?4yr*zXUHhf%41C7fqnp=jfAYDOs+y*bmX@}bmX@Zbww9)rzP9>l zE9ZoGd|$`ZmmCz;A$uZd><*;ow+-d>)uGUdF=D^vDF zQ+3~mO+#foa!sgXp|34$2AS4|9`r9b7}9|!^C$OI0V)k~fgup|qcLgp1_UEJ z!PUY;W8GJa9Q2HC-Ac^M#^MlI2B8-mTHUnn)MsCtozqyi@y*%R8ftAS{Lm1hh!tdb4xGp-?m}ZC!cF8z2-n@c74?I7gwFPtX=)_ z-_(~ZTc)L;1bU*WOUH95Zz+1Zzs>S}S} zU8FoJAG*@PHbHhk@i|-Lwfb7yj@pIS!CIw6$N@J12(s`+cMcnS+gL_tHuuqH50ysx z049_P$sQUT%B&d(dYqC9u{{h?2fZP@`0k;@ll!>-lNDz-AJ5M0;PMpoE-k;6$%gJG zl=NfzXV$VB7-T*6fzr+9`^Elu^`bvk~8RUP&qk z_>#fYj_1&>uD2M_I+&Rw%>LlBA{=9AY^x^|;AXTH!Uv8Nd3WcLeNVhg?Ur+NKn zm$r|ab;*n^pV0w_s17<{>tnR$@O6hxm(JU0UC-H@HqZsl$OffDDcGEwj;2?3?Y;Xj zt*{b@tZA&PtErU{s;;&jgQL@jMaote#$TB>Fe~Sb!ZyzEyS*}{Y=B?cQ+(Ig*z({e zr&EDxfraP-L`zF;L2G$H%7e?sd+%B~!YbhtdNfC<6xK9YCG5e+Iab}@dwT|%BJQ}R z;x8495(cL9bULU6_Wj_q!q+mLNN?%a6|i5{`=G{GR=_OY0V;tnq5kAnPe_~dJv!E5js%rG^vvEcUpz32Aqx_Lh_uc!qHudk`C zMo%aQ;HtB6I@jH0kh#=CvZom2Cn%?%hBYRzoG z>D;5r#$ca1MW71Iw?vV`J@QJXPW|+tNAG zZ@~c<1n-Wb{Y(a4L!}8hxD(i~SC!rsD*ZeIRC;{L^4e6W1l2a-vc>i2^5iQf!KW<} zQjyHo*HGzH)K-tegu3o|hDr*_0@8~+2Dowq2dd5B9H5ebhUtzq$iB7AJ@lN#xloH` zx7>F=&2AkegXTk$+fnG}vdhgvx!dcaY+3$pc4d&o9$!m~gk0Rf;On+gBAZe!TbTJ8 z?N7^9)`ro64qm~Th8U|G@qYUPo>*GMUUJJr(WzuQroin+yrpY$kZVLv;DxgOkQOnE znMW-3J7#p|9b1xm)xzTL-hHqNxNP1t=2UuwK!hAH_OdmJOmAZJj=dgcZ21gOsTu}H z!1f)4*;G>wr!1w!>~I>DgC8qn_{eP%_LL*3#HV1Xn-`you&*bNx7-GoZF6I-z^h! za05#C2K6gp#(v#1-4|Y7A6HlR;JEF1C)=RJhDnYgxdSf9)xzCD$yqia%x*98KfK#x zCfSV+jq6JVxh9^Q5I&r6s5Zj07Rk6SO06#&rvnrUNaRhIm2v)1Y>H>u{r1dlu~A%U1+0BF2g8EUg`pNICTl%&Q%u6QQ$z^uH>dsLw| z1k8=ks!M*iW97=@!9_t))nQg9m-kskX3xo%&XMLDDvb)hkqWe5)3=H}8rqHO9hJ>; z>bfd#UK$S-R2J-+SXUp0&%I%*=C(^WEFL%1E5GL`9jJ6K_T9U3r>B;4MjDQ(BrTYL z26R6rWSnr!e3K6i{>d<{4#|}kS5z`x*9=hvTA5e3b94TrqyD@bMeGf-mI6_Wl#W<>}9*m-EiHb zgf@{Z_^G*nuHn7VrtT-`3~!5V-iBsLxdid}+jIb^y-*flp}Xh$DkoSW999GJXJU`u z9@Z^;qH9G7K?vQA9Mkm{nfhI=AjMDs+N5`ul2|1+z)Qjl%9N97%rOjly*#hT5(=XQ+QMwc{yMalQ_Pbw3WbTH&@!-(%`opq>K1?y03Qz3h0v zU_K3#9a%(u(jX5e6#-mKYkI^z{k<2q8V1G(1S89R3mGYm^06d4`|RO8ypZ=O5DQ7H4ztwcHnXO2ea&U3R$6Do z6NR%&K?x%5>dva53mTS=Q>_^!of|8iU_~I5De*NkxVBq~rkLsOx~%1#mDa}PPt?2h zkqI3m(2mK`R8p>wjlTCT)1;gkt%rQ4VbyC$I!XhoPZ!@YUv3qwc|4)6=03jornSK} zqj%u*He-)nd+$|5`Q?WkQrB@*F}jd27h5_ORC@0YxVu$^M;{I(6E~nv-a>pm(=55Ahg3B5BJAdt zBaUlbuRO_&YDK?j#x$0s);zGi=el#rHV@4M=^)@3QnDO62#gd{O1%Si>RalXZ@*FQ z2A6h$s>7p5iu~Yyu16hRE!=|rYiQ+SKp>^E@zJU8-V(zl`d%v?A!SBX!fYX<8C%+0 zizjA0HM92CwLIyU;#G{ztHYvRq}D#1zU5O3%1%fYATKU!c_6cx3@{}mYU=LHOIju_ zx6N*smNV09NXr7;xRV!MPpNAO?@rArVa^5b+>d44ophrASWps5LR~P26?Qp zv(Y}}8I}H|2h+%FX@o~UnbW56Q5L`}>#PVrckuFlhg%WP@=LMJh-OTEaVlaGf2v@% ziaoMA@T8PlKYg+Q*%MRASZJpo2N*3OzeT1&{s?mEFunN1p$2uEhuuai5M=wH-6lAcAOQwA%lZ+sQ*xbmQ9%<)IqjFQHt zhMKyuWC25MxYF_~8^WDd9=!beY;7HsETFMR(AwM(7iXdCxlXnXd!p>>o8is0)(*Uo z7S~=IeDP}R*B5fqvuZG{NPG{Xsw5%om}}q>=lI;4;l+yP$QJ{uS2a>Vc0 z#fWl3@enXLN%r$4_Yb7$llAK@C6*PEi6lSAFyuS41hjIbuU?3q&Q7P>7XBsqgc-D^ z363ITDfD50Kx>OlvRQpeSDAELAnA*OaJu4ZneXlZ0Q{7JwT<=4-b{PK&Uh!R_VPu;AWRBpdXEV==9jb`cPoO>CBO_4=F(J z*pxsqt(ii~yUXqct(jtQIx`~eLEwT9hS_OvT&MZxWg1JCsjb_v#^k^rBd0^l^e-Da zB;_bKn$QNTMN8G!ZCqn|aQc={Ot@zP zCEP2ofSH&;2l%199A*OE*)k~?$76#t^^pu*_K6F+8lVB#>+}3_4ia57o;>g`%QaOs zG_};V)K%10LkU7n6%vF_qS3+DqC)reo?rfX<+4A2zRg14ztH<#Qcy|;+e!RsLW zKVPj@`9e)gb-DW5sr;#sCDokRBdhq_HrfEt!$v7fLHYWL&?CT?aqTQ*N*k}NK5-Z- zmQY2Hy*`Ipi;Bkj{-@8jTIk&I=oE(ijiYVZU<{nN)xu-uM4v|xM7{v zpCSIMt1n+BPeB>q9@8{PX$WnKusQUV!r8C(*d9wjKSt6B6(KcuZ4Z9AZi?&zEN^7O z`<`PlLDzO{-ShEhx{KA-v^BJ}maN*cYlF+S7%=W;HW@^<8qv*&o{cFZf5ihzk zP`F^99b1iOE#*~uMqhp%dnTj+R}fVD(fyJN(pEzrBCmx^}4&>tPG4K!n1J z&h1;Rz3ihuE&W1Ob%mCu=5jUpPD7XQJ5_`Rfm95P4tKmL_S!aAKFjna{KR8)T`yNK zOyJhvD-WD1@weL2InaBHRr-JqxPVGR_(Z5AV2Wz}i{mfr2bUW@Wj8`$-9JLBN>fu% z3YjBy{>2HhE8~a8SCvwbMPY^)QuS{(6;sBb(pytdo|}+@vaS_bRFD|y>EwI<#HAEe zC|c6Wk}!ry3|v%EiogDeM_-*Uh_9VzsML*Z^|p01TB5R9SAErLJ=I0WcH2COX&oSd zGYr8C68wpe(xM_tNVYqs@fmF20Z-f0VU?YwB7pVA;EpHDw>6kjNMW!~ZghBWlF%<+ zv`lr$(zRQ+T6kP^z8QY#SnRD^rS(0mY0~g_(y$pZ)@S@ouEmrQD@SBY&_WtOh{2}c zwm9|J*sJ_S4{N-#)h}qttw)ECC;DAa!=h80o06`0KK1Y}gA>IfU>NAnAZEB7Fxk9h zk&5aPmAABfN*Rad=9DW}pac9MzwZfI!W3PIX_rXYHpN8!hy&SKWH2!Y0st()OphtC(z7U#z08s-mid zKT~?-;r#;MEf7LYYwB0d|JP6C&t9gJX>u`~0r?i?kvDz+Ok<;|{WpTCALde*aXc*h1|0@ zm?FE|7Ps;%*-hP)XNk@xZYnC9)zz1(sV!4kq`SjrZ%}<;laK`vM|zUd59~dyqO$4D zS!?4UgkL>Cn;`|lJjs2W-kYXR(XWeM#fTX!&$NY)WW{H?XQMUSzSw!g)VrdvMq7yfD4fi5ssr7LepM;FJ|M`3@>< z!kJRo#L(Dur{S(W4u@@TBwl^iTGT$&#gWkY62?Fe1N*Qr(f_L>57wN?ds5RizYaJ7 zkJp81ZGt0^^|kf&NCX_;jG^`N#C)if5KA~+soJR|qywaQbduMO-RIZXK77(dr6|n! zCzN2Et~{q=X+s1YUXMhgI|;ZRD!z?I>>1=JZvzx0A>#GZFdf8}ZaPcAmdf)&i2y#0 z$0VZLX)RqWmS99W4=w4%xAl>rdXG>=&#o?zGc2}13MDrmVG5ZYxRypZ_=C5$d~$J6 z26%uU#y`vNJjmvc$h351M8IHTP=tmiOgF2GH6nRa=oDUGBd1v{8cRn6qJA8s6W&r^ zUDMEl>SlK<+-jn zVu|v;4|FNX);(RnSYQ&Fz!V4iP!> z;GHGQgn4Gzs{$+t>{6=)?92`C?^*2K<$nDVWZh5!oCuARZM8nK<5GqiJxMoakO~dOp%XblEm$F055#ti;iy}`ZHlHcvOnIZuW%IiHM87jxuKRX>(elnI&$NW|fm6Vjr{q9HbkeACPPAHeVbGPXZ14Glp z7mg*ghqp?5xY7ybax<$7BVGK`4+K?bH4!_h%+C1?>z*Hy4z>Y>$4mUoSK8lSaiJ!! zYv`8`vISb81^Ttof~GIf0?nSLgGwC~QrCYARO%Id58@>LKgXfc_b*4p92S?2XGz<* zuQ=cSECEr`t5M8~vuz>yd@3R8_lS2Z@* zU^*Kq{5H0fDS)6-KVQP&@EHA60XAV4jFitiuOrKXFyF z_RBw-17P0Y=F0_IpauHBqt}{lv!LkAkkbaiPFwF?` z;XflU$DMJqV`R!V5O{j|v*I_hD`m&T%SL|q_s9=pcNYKiC-4yc^GDJ5a-8&v|J{%L zZ-3-}^8D> zZk8Qj5mM9^-wiN+VT#iQTA&5`Rnmf{FVF(bp8gM@(vR~Ut27Rk{v!iPKaa@QmS%=Z zGH3-|KM5F3`Ubq)4?l~4__tUAD*a0&TTj@*B1o z#7O{4D1%CTIaCtThWJz#k329$WU#t^tx#!!7HEMMXn_`JfhN*#0V+)kdd8NNlz$#u zSeo&(%=`9lMemyoEqym0tE32(xTD`qfJ)MFs3aMON&+ftnB4ywD)s#e9dHY@Knt`$ z3$#EBG?9KY4!92hmHs^gRN_hh$%R&x7NF7sEztX+nYNUFz&5!(f8kq|M}g>58uCUsPz8;00960?0pGb6Ir%?&-9y_ zo_RCVcDI5cJ1Fi;tGEDyfMMS^w^m$01VIrMao?>{{=zgLw+mMj=-r(1pbCez)&s-o__=bU@)S9MO^^NR)G;NSR7Q zz`u>nP^BMO$^e`aO8h$@cca2z5VN-RTk_3f)vv!?XJWT6sx7Jm6mGWk0pLTBk`hXM za`^WI;6zmEdl6OQ3#rm4kgN1DQzgL!I61^&^x|>7IQ$TUGcoz^c-EZL3Z22>^C2%i z#rU9~aeo=fe5!a~?=Te6!kqFKij?S4y!F|mU@*D-Dada5^jZCRvk~7=D>pSb$8X1` zHOoKya_LuJE!tpeyZ?itO8-77{qj)6x;{pqqr^vu8LCt+q)K?XRH=_S+`nL|^usSf zm6QRvw}U?<7|F($J@otT;dg%7rTC$df5)@t>{d?XmJ(TPkPUK*$;jttKiFs7Uq&*Y zD&E&S3`Mjsr>LqTg`X8~efB7dkz_Up90A$6#L|Z+;Qr*6;+doHYTGr4j*qvrJ+sF4 z$d$yidCghn)hHCYqwhls!2SEA^vgpL>-rddjuIasW&&_?sgiU6u5ZBsI5AaH<|=&v zR0(8&tUN;5lYk>njs)bD5#U;8?;m`+U?i7NE^H;RpsaI@12RE&0wx=sp^lEqs;>ARAmD?1);OC^?WOnhjg7(s z8U}gkgkr$e2C((WD(n6LcW;>hr&zpEI<<%1OJ%Xgm9Og;0s$T5ro9LK&axa>3p|-x zfUGvBrvlh(2iUqKyickmF^V2hO}cTAN2gKg-PAGRe1R6+qOQUl->Dt2*17Tw&BDKz zrr$qiOG!*9KUS)wh;@CGK1Yd<55IZQgRDswrK`26 zr4m_NRaI48heWnCp}V?A##u6IHS@tK_QV*IHo%1L%N2lpPA3m_yCmX})pnmPrWbF< zWEEkt4c!c~REbk?lFJ|Nq?4LZjkT4?Dq;7bwouJzO3%pnsIM*(@x|2>lvcF@!}|Z@EL`LUO?8PD#tEN+hcZHq;wFlgH}o z>gvT|iDXO@0fnrusj6?q5QjPatg*pPG^M$su?AU(!VqY}dkRF_AAx#HR#ex*F0+X| z)UDT&`ejiVlT!;T<2qwG{j{;M32;u)Q>ZJo4Ry#IR6$tW9Y5n;VdfsMvN7LlbZ0D4It@{b(Geh1f&q(D~qm#DHa5`y352 zc&x#TC zsJ&(StgxJL+naL2qx?}y7h0ZPnB%kUv$d*GXd=j}s>W6y1UoxcUY*XtklrZ(AL({($ZdIre$;2{c>h)dum(j zt8<|#p`l5wt+CA&k5BHte%LxL4KqN3TswRR=pTgs_Pm%qSMFLpPH$!4;03FWUVHtr z^>)**RaPuhQ`0b1Sz^B3_VTUjzB*{3l;{GSTTo1@jy)d~cRIYNaj>_0pt$(lj?j&N z)cx|$pZ)IZMPDt_-)6NVtRVC?o5Yy2_)alM?2^!eu$`7hE52E}Oha>-rsgV@r4~Ev zuHLPrHu6Eqx9IZn7(~abUWNq52c1rAZb?L;Q$xZFj~|MRxnTUjU&Z)ajU}p@Dyr(= zT6z1YbZ1foyrIg0`k0HsF+t}`syf+1Q4EfBr6jqUA6>k7qsB5dZEa0hlI9{w*0sGt zH^-j|dKnl}SS8V}s6S_4SCV)<_;B4|?e$9wkX|9FHu3!V{G&2@>*=3NOzQ)gu&PPi zQdzRHzkauBsAXbmidEj*5FLIoa=%wVn8&yNma1#@)Yk3sI8}g+u0f}rvAbomXtU0e zRpuVPNyOA%kPe$3M2xEkYOjYY*jv~(#+ansl925o8~>;S>vs{X-|f~rE>?z@4vmgB za9V&{PCG4B78`1)sA+CmqI%rI{Z%T7(GS1MlW93I=Yt?Elva@;=|SevJOtUZ17y#t zD>bz=wA6Lf|DvL?;&c5I>kj3l;W@%<>V{-U`0A}1dK3KYP}*0Qa8a&2Nf`qm8=M@c zq6{zFuOueVf&VF!wugUOq`zZseeub@1TE}jGqa7FWoM8l-USs%P+dMYB9m^%s%&t;l&t$jv=CB1bC zVHf0D!W*q|XU%#EDfzEEl$qW!Bu#VQKG$Fp28nNgDVhaKpjn`ys5XH6aVMU!F);N(ODtMU33TX}k^=x>O+1lgx|V3_CT0EF7+Rhp{m znyRX5-|q49PorcELHFnLDY+FhdXeKjeumFcLJ7t3DFom?D5~^pL->v0|k{pMHyge0a)w-N@`=Oif``I-m^#N(A5%r z37p^n!Y{7D!j9rQ2Q-h_>0f!-)Y%614K&Br+&3=BOV)k6TZlA`m*=M1Op2WK{zSjT3z4bnmS7(1mU-S6C%Z_R(NW~ipEsja3fA<0s} z(j3@@c$p#5uCPBQ)-N-0y+dFxqcNuH_L1WXN)JxRjcdyYb=Y|l>g@^dxxF=9vmv;^ zuMott;Yz2Rvhy-zwe^7G^Y#b0!BJu&Irp^dE^i|>bJty~Jxms_UGiJq70b5>nvNgOm+F!zumreV~}rdTM7@POTJ~9B|V#%Ok5uwBr}_Cm0h)W z6Wg=4Hl&4R5D74ZNWglZj7;(X1MFo=NmvK!^Ufpt9=Ald^e~CB*qn1tCIPbKxpqTv zk$>?dae~94=9h==I4ic&#f!dCHQcrd5poC-9A;#6$=2n`>z)b>CFZHa7Oy{kq4ve1 z|M-iF>QYtBHQUv!FBv%oo;Uq#xYKg08>NR!Ks?N3q5ngZ>gpjKOUq^7g{R^QAzCqa zymGC5MuYLBoO8}5@+<+;ngGidfD;|u`Wq$J>|G_LG(T0EZ?4@Vf>hWQ_3@cWO{uh;HSe6u(h9($xOzidV?a=gdS@Ht8- zp*TJTs`Nnx;6zl3@rwuG2GI=(Hk$$s^&E~x9eRdID@CDebJ`2f$KLkaW^!e>6HrzO zRF(sFHh_&wd~ru_2dOye@bYU;Yl4%TA9jJg&}u)~19shSN(x?S4;UOyspzj6>?(+{ z54&b^>1k#~7M0RW>LRz-^i)R_zP!Bm(9;7Ur4^lwvDuDz*4H;y&WSm=Av)MI)_%7v z;?SPAy$lyj@+KMG%w8OUfJ1i>u*9y;#u92`KFEjdTwcY9H&}>Xjbz5s`3g(Gd)M+POhlq84$>sHt4P}6%HDK); zUr3^mhRaH>Z;#x+&gaYvzenvEC8*ZgoHl7mS_69=<8vh174`?=9#!01iJ=rm*@sANY(N1-ONWt`8CmpV*QL)8Cs}p@%MRH5R;!>|773G(~kOfch{WGFQV6z$@$Oi zE-aw8BCX8{z=hw8$t&q>X&Y^+Lq3k)vpjh93ddv7hoi6=mF}sR&$2tkm&JWZtUL+FT-)mt z>l6G6)e_y9d&0v&miC46ToqWE(*?=1o7Z-yRAi;QF5QePENd_CXnT|$cNt-R1+t1f zJVbnn^t#*m*AH24J7Q!Wek(4!5QC}cprLCk>9O9~*PQiFC$D+b-829VW&rGd)RY{& z#_5XQfs}02pmf+ruC$@d@QB{aRFsSW|yW zXT9y>&BvlTVi}`p;ZKv~4Wbr-XYUbYZNi2AhR74DF3$gX;G%-kgQyZK6Z`6@x1I{r+kFzfjrCn^ z9HMR?8;>3of4#_OF(qZ?`K8+xt*aB}RYN zIUewOq!v2T{gf1cVy$VImPu6Xn-hM2K5H}(U@AiSU$BS<6(X_s-dAPW!gCRqkBI$WPDeuZ?XujsJuW9$qj^ojU zIrc0}Sa$S!RNf_D!V!Mk)}G$J^Kwo(@VY3_%}|#1Me=lrk8dC0z|o#JR=PK@qEFPN z3`!y3X#*f!Q?fB*WAGrdyDH&p9K3VNd!{8Z2=+cirJPyodVj@{veGU-IN2pAJyIIs zx*{ZXZ6pQNH=eu)@e-b`=5S$Z!6;OwopF|>bj_=swpD1w_n%~tu6}L|< zc65;T8)Cg&g7i{cc)?}4)h+|yONIUAVhTP6GGE}I`E2>KwW@0H`EqJAG!}jir5x~{ z$9w!-pQD5l3PF-8iMF65lL*}(4^?`%0GuMhxQ{3RheeV1TZG%0U5TmbC5pcc5`H#Z zc+cGqaF|V%+*9)iyb(S{Q0h~D-)VVp(&|VmiVg~fh&}h+GtWB*W}vQPm<&b%H8;>t zUuVT+M0|~cu#8}8k7+Owdm6Q14h?CVqI%cYBIZ zB`2T)c@J53)!D+=%Ifxu>OO*4GIVe_4wHGw8gbLi0Vu76g#$^((L1V)TGE0^xU|Oo zg#PjSWu#J2v<%3Wq-?5W3)s0vRy4n;D?NL7x&Erpey_d&=_R*kU2r!&YGHNzxnys{ ztgXV0U6M1fW5N{?HPQgwl91IhgU?r=UE%YhwEkgp=?%BtXAsuUl3G~PZYrc!ZgV&$?s-Nx(UxwCVYRdn@kqpn&xXn-93z0=6D$1q2Qcc9C98x8${Cxy3?kvDBE{SY#=56SbAu}e}0W`qrnf}AwiO5stY7ZABT zXrP=Fd>-Kb#**P{xM_H(bz7k&cT8I{#NdkvW%qqngt)GMmWCb2jbm}o)~AISx+iCu68~4DN~m_4h5IXGk5`3wIJN;lNKMkf;aQ|%dVzO)m5YO(y@tW==jhanoA*E1 z5L{SIW3UEV3j+5>9sTY_Lr*z}KRKL5O7lCoX^rmZU+bu8XzT0f=<4X`Xlv{0XzLj1 zY96)mjE%t#;H4M58Mieu3XXuIudum*n+;Pw{8S3M>_eptQW)nI-L_6rX=8gCU3R*;!q$LSaD+j>NEh$fa zyVXTO-doohEI@h#P%CXvD|UdbXJ)ZMjRyu1xHwfP9 z4HQ=c$jW<8&VapFMiHLHg|sl13K1Gldq@uH;v<|?9CSn|0qA0=w_XeCt+W>0Qy{xy zgG>Pv-qps*4nTP4ye81a)+iVP$FuM$;rqV!u~0vEw~AB?3Rtu6Bce)nfIUP;%Yce< zz|I-4@y>cp7~+cVFiXOY^GjHFyRObjN4MJ&p*-!NxgcSs&?vAF zc@iGU0o)`kS#)GJHHWAnNbYS)j$G{$y87tj#;$~}#)PA*AG@pxO>T}PgS|Y7mm2e0 zyMzX1*=@VXZXE$+xqK<*!Tpp)l@x0o3FkFh+W~vrb6(@$Xi$X7esJg|Iuo!%0G85c z`9mNh6_@I_U#8!X(u;#Kd9rd`0APh^a(qyteYa$2c)Yadwdo}}TCl#Uw^zkQKMm0Xi^ zF~cm_JK=h2)2}#O4EDZSG+0(PSaQ`n{F3wieDqK^$c7v)E;G#4>a_9h8xNB6%Q21B z4Yjq^H9|Di)^wmT{rzL(oH@Bl^4*plVB_&BA48L|wV;3ra>~htH^R-WJvO=?xt4q_ zzcsH4g{sMGFTI)=amswxMJxBDYze17y+}*kd1??GO~n<$w7|*n4ON0@jr*)?;C?2x z&!$uFK$V;UJLz_d?m_K}ma)pxYmPP{X1i}hDlWY@w8KVbZ*2hwmq%p{ubOM_pSQC0 z+~|5#QdYs{yKmUI0OiOxWX?S$A17S+*{~MiHx2G7dEz4}*gU}9P8d>rhu%tS!80`< zrr__FDrFRROcMo#C0yH5TTj8FTgQ4{))xTI@H+`9dFBeqgalQZ8HM6h3C3_oSC<>^7`fzm-^swM%PIw%t0;FC}5<&`z2vy`}ZjQ0unN@NLV> z0K_{`rO5$sD4{(w%tfZ(n9)m&NG~>S$$1zWQ`|!#Rcd^4Ti5vLVwaEvLb8Y|72NZ% zm+w8^&F3hg!~*dP<|_TaPas$6ZB*&k%T=mJb+5O$tFs$;-qb%JJ|-`0l$6^xDBP2V zKSHQ4Id~$@+cUBL?%nzX53d(~2a0M5W0R1(naITDU-jMI5+^R-@VW~6cAaw)w*?0YA`vG~rM^A6UR&0LeRB%TiW z10W-zEhF4%=LJhdYzBrdUX!M1V9(>`q%#P_jor>b84^Iw8!VemBEU@NKa-+QP?`N3MkJyWx3P> zlGPPSS%o>+-m(-Xo+GEzymtl2_Zy>v(PT(3ko;}tgfgLj zmy^qH_^q(9)jf7Oi&8Sio*aHf$hzceGLQFA+~-V!Qw+{9mDJjWZ|kG6$E810P(lfL z_!OwpuaT?t!3E%OHP}Ns`w$w3ZWi`di=Xr6OGXyuNKa7Fq=>ymr;P4g-?#tTO{0JU zPk`LoJ0*O=t09($1!`;BE-^U~)0M_$49gj%}aZvX)S^Y2;H`*ipSMcY) z&s$T^BDS72v5L^~fzY3_SxaX`GRT8IlcP2fu)Cu}_!-)jk6S`2 zb{InAxQXHYSjl!R^^HUI_dU;}WH!Sox4QOAO#I(Ldh)1}Biz-pqHxO{=XOcU>MFv} z?EG?9ARh@-R{+k6P^C9%)b-8R{TI~R*;+^lv4{2hC>A%~4wY^`0=ZO>c&{ni zX1|dZ^gkn;=>7KyO;kym1Y>?{m)4-XPY2#2rQHTQgJjH_x6HLYh9+nA_a2~n;a?7db9Rb=)4HEV+UOlOHVTkF3)b}@&j;yf*~s-9uwF1ZfQv0)w>PJ} zXF3Iu1r9yJ#UEX}@tC^F!zYdOevl;?O~B6YJr>||m>}nMXW^qztMFqEQLoT_9n!gX zN+=-*3k|@{P^Awi0H>r%5NIv!iDqu0D=iOSjt?n7XIHhLs&m_l!s6}*n3`Qj2w&+} z%=*NgA#$KhBCqs#-F2e?>pf<+p~m+b0;_o>_>I#9D4@{VpC#BX4-|ch{Io6YbwhJQ zbzKdzy0#J7L_&2A)47v#-fYkQfZr)G8^d6MJdj^W%DNS}(!oyGGw5mJvyP6+ZYE`z z&ztt3Qw)$@LMV$o;SucSbn)S<+~T&DsupxzVNz9u{Y{^pDq91z-BTc^KQi0t3z#6c ztTX3s$VM;MRX%5)r#)$Jecgg?C`zh^|H?$gD8#@8UUI6+0ehJr_6s)@u(kurE5wo! zZ5BYLmz0KYjBsD!6&!u&QF~g+%xCI5&97RyKPhWUZmN73yk)<;bi0R*Rg8l5_QhrO zH4wUEW2ZlNU#Rq?w?D`h_SPPH;!mh+j5@wI#KY<0BS~3FjoK+G2lM7RI~#C<^^%l5 z_lS&-^A+jsv=(^fgsfWuuztOQo}@e{qZ`<$2IS-C@4i_;bTPFC+m-s#%J!_G_2J8j zA%$%@wQWtWlWHC)CgHjkB&x3+kDa8xMCAdFR)CdEdCIm@@1UrR7uZTv`{P`Avw+J8 zm!z3@6(SkRC_@V|>iVvN^8vblHu|g9*3dk^8jvir&f;6FT>-mQL`w2z&HSw&-l<{~ zsJk~My{ErtxUBNRhDa9!kD#c7kJ?g;8k*~?YsH%djNmZErKi_Lx*B){PoJ%+g*OXm zAamPW>R-G-7zVAD@*85kbWHTcqw~T&cAwZ~9CjxzrvTl8Y;I{R%0NH#dvV5R-Tp|U zE7e84T$ssBR&6cWqCLy6vVsb zDt%Z~Ng03x;ZzwCFXc?G@3xI=mjC8!O%)9tJ)LEGnu}Dm;8e{m!bH*XSsN#co$mw{ zq(-(pKIp9CV5AjtIf+ zbDvqKw}p&JZxz408fh;m@LdzM_6q~`KYXGAJLs-ot^I|%mWsN@YSCZ0?Nc|p zu-gtOs{qR6Z5JuprqTl5rn0mYuz;?yj?Kc*=>jUZ<8Doay~q01I$vmNtIgW{&_ull zG+f{J2TF88L=Vx8GJ5YsXLM%t5~BCsgXki<(TQ%1-n$S)3!?YldymeO@9+QKTWi)` zv(}xtch|GeIeVY|S=|ilem9r(xu*-2VD+j~!ZTR9wPJZ+wpiJ4y%s}Fe=|N!nSdB5 zTB6LGyFnjVqO3%b4B9On?5H|A91*wAjECkPs_XZe*Kz7nX?A|KRYWU$>0aDdI4VkG zJ0yqVBRq5&wt#isd-u40P>!C@d}|pnLX_0NnRfG(i}7H!z2~=LpF!pO1advo7@j)N z*7IRxTsvAxHE0wh*PcKJ6C-I^1eRnDOPcGK{Gx#lujpk8+Tr^x_wT;?Ni9aktsPcd ze_??@C0|@$)UjC0Yqr4l+!Tf;N(FH!l)0U*h*$#B6?$6?i?}M&E%dO6eA4;T*S%EI z?zmHpla5kl$IF{i64j9NZ7;%x;<)6;%)_4%+wRO-vfqbqdK$#+4{4K^$~#O>ss;Ogw@UxVH_=rupo{;-0>O854x_N;A6Na2qoeiO-RKkAa{ej!8bY=U&B0Ykow z>#hy6b{LiD?1%41K^)#*irGbVV@l6nIL@e)$_Z`sM4WOCj9PXQs~NXgTO2F3@vSRN!48KLoOwM5OM8nEfT` zNA3Hs)?-bWm=+MzX|B`M7W!99($n;jZCo#wL&(_q%n&m}DJ7JtO-_Wxw$? zRE&>XV>ybTe!!_%BV0YCFblZ!7tAF|e>%Uuh_^lZI(zi0fX;Wi&AvjSX6tv>i_J{& zCYN*-D!%_KgKv7aDp2fR@N|rg7Fio}pC=p|s;noMalGu7^e5^)yr8taFuy3b+4(FX zf4f3~$?*3V-Om)?S9}>aUH{s(s`=!?!l(76nFC0}nhIWbGF7X+2_XDDT8ZW6jgyrNGTTbUaV zmAl4vruTA9mii)%VMM)&W=^L#t8&^K-n=12LW&p(FDb&p&xqUe5;!}*v>hkm`mp$CXod|@Af2eGY;bV;Xqj8`j0$QXCwly=;1Kt z?LjtsVNH#czuTm!iNd@n#y_)3_HuK~32m;F27YnoFw8I+y=g_8qMxgPHGX^R+`Pz- zV!R^bGal-7A7QrLFCf6#yMb+rYTggphTUp9DU8i|CJWGzUcK^V&TSREGfyB%d_aK? zL4kZMfxn$x;`k%_a!BLiB8chN=*?WUZFi_#FUv!t3(M;;A+cp8j&^%=^`6&_8+QY0 z`XbivM(Sma#N{Kcqu!+!qCsXV&>E}gVr5-y}SZyhZ16e8`ob4Ky?CKGkH zlBB?7S-{aNNCA-FYrVwPu?v>O5WFFH<@9pE;*#>*Kk;SW{*0$t*erf_P^fFSH|($! zu=7(qmBaZ%1nVQVsp6m5-e(CNPGYkdtT69aLYEsV65>`ClH$Nbq(?@n-xXiht`(s- z`0x3!RcY(p);VW!Ud7DH*s&Zai)8roTOAa-)uu>K2A&7cdHxb$?B8cY62a}H2ye1; z`;kc=2fY!;?Bc-rEw1C{9s6a2yghMBFpCy+5f^LHB`>K@v|5Gs!OINl5zFj3p|#%K zS1bX2V%ipZA$r}`tlc$8aH1dxtndKZJITC-B_I@mHeTH|*};(?R}l;xJ=cBnK$%Z` z26dkdw{*7QXK ze`23Sg%;(G_CIVyTpjg<@%hnyndhL|U48g;*cULerSzQH{hdnda$|*_LFe^fbdSwG zoaumAXI(oB_GEdu%*MeOIrhzRO9vCn{Yk#~!)F7Mu#L~pw2leNl;qY2WRAJtG+@$P z?;tDV8qxF(m#&eYR^D`RHYuRiR%Qg1+S@QmP7_lfU^3gBgP1Q$NsSw?Cf&!;MMX=n zlicH%7M9T<`w=x!^VjuzK39N!JyN}i>gOJU@v3i!xB!CiHWnH{5^m5^ludNqVAIzo z{lF;dFFd-f$c)rsE__U9Z`U<*K%X4F-lQxTRPb7-V^no18aP?vy@#uJ6YsAsav$Rvp94f-}R^s=)K-N z``+`?uxcBds9~y*B4F{*J)dfCEAu%y82T7 zxADfnfxlXi^=iHgE4v7XFCwe8+%ql1zV_Pa_+oa?+0~o9pRkDe>{$*6vOfN+v>Pj% ze2IXdm5q*8@XScvZMEyOgKSZ9kwt66oOh+b!|a;LC;y$B(}2gG&=S1hnTr}eZom7k z`@0zJk#OHdoCR~s_U@m;R2<=%vsekKpXy@vTT*i0yviy&!nOUz+L*3)%rRGQ-?vSU zE?2t|>qlF9zK_9qHOU*_F1#$I*NFF$9u9fAoIRZ}sTD^4c(nZ%>8N_D*wMhh>Tl!3 z#aEeI+g@86{f*W85xAbT0G;TFum61e>6!F_{ViuYejvVPq4O`ViT0VmsI=zDu4AN% zYfQ)F%Qji}`nM5Qc&II-!Bp2+Phz>&v#^oxEw|!l9h6(x1(WUPEA751Pb$|)sxGuP zLKg=Mq}66TkM|?FCO-P6=99PWM2N}06-G*oU*&RLZPOLu%pg*Y4I=~L=7o{I^H$q4 z=o0{Zgbv?}>Pq$pKr|#VUUq>cBj4cs&_Fm2SpM{KLHrM8D9c+d(UY|A(ee8GqorkE z?t*UYSelBhd&3kw6JxM&*2n&!#ocxaE?*w~6|I7~^nLF*kf0PD6PQDQOz>ut038{f z4$(yt$A!W1I1U#V{qTPKL*B<+Q53n9h@W2Y|YwZQClFs&~m7i9l! zFw>Q~7@aI(oUGm~g?NQ$emaU5#ZoGB2Ivh_Z8c8VvSQ|WpAVjEGF`U+*jNsn*W%d9WNAs`r$C0chz zYeI{jst`2aZ7r+b`_b_G{(5#QJ~t9B{Wh+#G^-(FF*T+2hoa9wHA>H?%n9Q7*84|0q_{z6YS%a&H=3uhLcrS4t0h3%9I;l>I)Qn90(fES zjKE@Eyuph>(?i~ zzJ1@eTfLLpH?|$fW5P-d#`s6J#e2sP;K1?pidc)c@}G52ZU0sBU3|QK;yMcADhlEz z#2*$%dMiKJ`zrO{r|)_f3jmBEM36Y29_E%=aflx6zB|RNy=I|9w5+~L z#R%p-pY8S8ps5m+00x9?{#ySzAsVZ_RS(RB7sOi$Zo=C_Rx4?yaXW^J%@y6j?}|X= zFTW*$0Xh3GYnn0@SJplIy#125B&=RH(ZBki`rq{{*>x&cz2lc-9xfdfZScM*>K%H} z-UymOuIxjrxW>{6fBE>|l^gCfT2qRNTpUa`fN~Yg1Ku1%;Flx;5ChgjX9%EN)_4q@ zNa(?6JHx3B5sGWL)9GdBY4&=6_t+H8Ja}EtJ3m*hi;4|jQt<==Uz`x3^Y>=78sIT; z`|D)8iMH5+ukZ$9WWO1hw^77CM~nAQHs2p08B8@pcgoG%)iUMJY^6U0i4D$i1hX_K z32154diXsScu-xEpj)WEIh8_>LfI4}tPwxTkq0k0Y|^sU1gO5qX~V9QQ;5)w5316l z7WbLd^QNY+g2t-zKLWmOMO$@V)p3wwjZ!EJ`sY&${e|sVP%tG46&Z~cq zjDjJOBA#O!`=2RrC-F-`zdcN^+x<+g^%WD(lzNV|gvJ~zXr(B+8jG;xXhO(+*~Na3 zvLAM{YU9bb{Mvy}((Z^2+;eRzIqeb?m#W0wkxqS%Q2$mnF(_-@X#%I)wK82?4*2}7 z`xPT^nz&={+lNzO)8kH)e{3OeB&H!^3$P^Gbe8y_Zb`aG4UX6{@Er+dRr5D<7Jt)I zwJxEb03I+w3+QR0rQ*{UqJu$E?uV{WHOSXiPqEi0lnr)E&6>LA%{*G~nxkhY)n@*3 zocw*$C&d@;cq4S|FP9DfZhI`}CK&?NNXI{Dzux~!YwjrvOVF{!Q}D{Ws|#1?YiM^u z-oigbAFLa)?7ͤ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�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 diff --git a/docs/KeePassXC-Connect.png b/docs/KeePassXC-Connect.png deleted file mode 100644 index 55b0f3d4907a0f93340127f5be82a39d9e6b5494..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103252 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 ANB{r; literal 0 HcmV?d00001 diff --git a/docs/KeeShare/DatabaseSettings.png b/docs/KeeShare/DatabaseSettings.png new file mode 100644 index 0000000000000000000000000000000000000000..ec89b51d50c760539ae6df2d5a9e239bc94e305b GIT binary patch literal 27801 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

    pqTd8 zS24!H!Q9Om4<9YM&YwMQjVUSiT+)C$z+z8q!S0lU2cU1ZceVc$wEe#V;r|~$R6hv3 z;-lz4^#8v#{r_*QT>R(Jw~q1MOzb6!M24y%nC%5o9o2Fa0@nh&!22K>w8@qI8?^cV zf?3I4HNxR${g9IG7CF1G0v&=BkJ z#pi(IhMh{TZZAQ1e1&InFSK42V!z>1V&}0Jy2K8D6eQ5*&3NRmV-Wa^5Sn9;YT7hI zmw44Fh#u60h_T++8eo)3oX=K=G*Jtfp#}=1OpGU1%zl;ZjTp;_USXF_@zr3pUPE6UoOO9H1coXP7-3A zlCQkYpy+Z)bo=XZYs$bMKfD%DwG+VmBHfof)Nxfc?POYMnMGfT1;w486XD1Pom?s{ zTfoTv7!GBKM^_YEfF7@KRUyb#q_r0L9s-p(1fRnNtPLl^qGL=YXJ2|a^Xt(xBeT>j za3~t_6KHNxGJGcnnaF{pMPFO|@Pl&LJ~ z?mpkQOZu3?V0Cl}cwfh?-jj!e>Fs5x&#~qW<3}BR9YRdcae_lz)0L0TENO;S_iy5w^dN=5y2?@Bwq5IE?S;0`OByfqJRkf$r0 zfOQZWmULS#`Ru9Hz*Kpg$)eM$b_0|6rs3=TqrN=ry`S1&?qy7AiGl<>TzE;+1Dk4+ zsy-1MGf^^xzp4s%sCt~t#no0hHClg0zq`!TDV zzI4O8xZy7cpz}vJA7@w83RwU8pubGupD*}O7^zVTgcUwCkN2-AxH#@DWK0|%G7w=e z)%GKUE++x-W9&o5;QtSK5mh>E0O@-}v+nfu-||m#4nr=H7=AN5E?&@u=c(Pe|6j)i BQ3?P6 literal 0 HcmV?d00001 diff --git a/docs/KeeShare/GroupSettings_Import.png b/docs/KeeShare/GroupSettings_Import.png new file mode 100644 index 0000000000000000000000000000000000000000..3824c0158bcdd7d0e4d0456aa73c9d007acb060b GIT binary patch literal 30826 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 S{5?2RHeJv)g68R53;Q3=Hj7jM literal 0 HcmV?d00001 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 0f40958bb..3abb64c3a 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -53,13 +53,16 @@ Sharing allows you to share a subset of your credentials with others and vice ve ### Enable Sharing -To use sharing, you need to enable it on a database. +To use sharing, you need to enable for the application. -1. Go to Database → Database Settings -1. Check _Allow import_ if you want to import shared credentials -1. Check _Allow export_ if you want to share credentials +1. Go to Tools → Settings +2. Select the category KeeShare +3. Check _Allow import_ if you want to import shared credentials +4. Check _Allow export_ if you want to share credentials -KeePassXC Databse Sharing Settings +To make sure that your data is valid when im imported by another client, please _generate_ (or _import_) a public/private key pair and enter your _signer_ name. This way your client may verify that the imported data is valid. When Importing, you'll see the known sources with names and fingerprint in the list at the bottom. This is the place to _trust_ or _untrust_ signers. It is only possible to trust someone on application level. + +KeeShare Application Settings ### Sharing Credentials @@ -69,24 +72,28 @@ If you checked _Allow export_ in the Sharing settings you now are good to go to 1. Select the sharing section 1. Choose _Export to path_ as the sharing method 1. Choose a path to store the shared credentials to -1. Generate a password for this share database +1. Generate a password for this share container -The export file will not be generated automatically. Instead, each time the database is saved, the file gets written. If an old file is present, the old file will be overwritten! The file should be written to a location that is accessible by others. An easy setup is a network share or storing the file inside the cloud. +The export file will not be generated automatically. Instead, each time the database is saved, the file gets written (so please deactivate the autosafe feature). If an old file is present, the old file will be overwritten! The file should be written to a location that is accessible by others. An easy setup is a network share or storing the file inside the cloud. -KeePassXC Group Sharing Settings +KeeShare Group Sharing Settings ### Using Shared Credentials -Checking _Allow import_ in the Sharing settings of the database enables you to receive credentials from others. KeePass will watch sharing sources and import any changes immoderately into your database using the synchronization feature. +Checking _Allow import_ in the Sharing settings of the database enables you to receive credentials from others. KeePass will watch sharing sources and import any changes immediately into your database using the synchronization feature. 1. Create a group for import 1. Open the edit sheet on that group 1. Select the sharing section 1. Choose _Import from path_ as the sharing method -1. Choose a database that is shared with you -1. Enter the password for the shared database +1. Choose a share container that is shared with you +1. Enter the password for the shared container -KeePassXC Group Import Settings +KeeShare observes the container for changes and merges them into your database when necessary. Importing merges in time order, so older data is moved to the history, which should have a sufficient size to prevent loss of needed data. + +Please note, that the import currently is not restricted to the configured group. Every entry which was imported and moved outside the import group will be updated regardless of it's location! + +KeeShare Group Import Settings ### Using Synchronized Credentials @@ -98,7 +105,27 @@ Instead of using different groups for sharing and importing you can use a single 1. Choose a database that you want to use a synchronization file 1. Enter the password for the database -KeePassXC Group Synchronization Settings +KeeShare Group Synchronization Settings + +### Disable Sharing for Credentials + +In case you don't want to share (import or export) some credentials, it is possible to you can +* use the application settings and uncheck the options or +* instead of selecting _Import from path_, _Export to path_ or _Synchronize with path_ you'll select _Inactive_ while leaving the path and the password untouched + +### Sharing overview + +There is a simple overview of shared groups to keep track of your data. + +1. Open the Database Settings +1. Select the KeeShare category + +KeeShare Group Sharing Ovewview + +## Technical Details and Limitations of Sharing + +Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC. Since the merge algorithm uses the history of entries to prevent data loss, this history must be enabled and have a sufficient size. Furthermore, the merge algorithm is location independend, therefore it does not matter if entries are moved outside of an import group. These entries will be updated none the less. Moving entries outside of export groups will prevent a further export of the entry, but it will not ensure that the already shared data will be removed from any client. + +KeeShare uses a custom certification mechanism to ensure that the source of the data is the expected one. This ensures that the data was exported by the signer but it is not possible to detect if someone replaced the data with an older version from a valid signer. To prevent this, the container could be placed at a location which is only writeable for valid signers. + -## Technical Details of Sharing -Sharing relies on the combination of file exports and imports as well as the synchronization mechanism provided by KeePassXC diff --git a/share/icons/application/128x128/apps/preferences-system-network-sharing.png b/share/icons/application/128x128/apps/preferences-system-network-sharing.png new file mode 100644 index 0000000000000000000000000000000000000000..5a934582c8875dd8841c156c69ef5e1eaf621ea7 GIT binary patch literal 13033 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 r0`v?JaTnQ1|JSJnK9hO Date: Mon, 1 Oct 2018 10:27:14 -0400 Subject: [PATCH 03/23] Update ci files * Removed libquazip-headers from trusty ci images Trusty does not provide libquazip-headers (they are part of libquazip-dev most likely) * Bump rebuild counter --- Dockerfile | 3 +-- INSTALL.md | 1 + ci/trusty/Dockerfile | 5 ++--- snapcraft.yaml | 2 ++ 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 95c3ed99f..e7b346d1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,8 +53,7 @@ RUN set -x \ zlib1g-dev \ libxi-dev \ libxtst-dev \ - libquazip5-headers \ - libquazip5-dev \ + libquazip-dev \ mesa-common-dev \ libyubikey-dev \ libykpers-1-dev diff --git a/INSTALL.md b/INSTALL.md index c13263913..5ef819899 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -27,6 +27,7 @@ The following libraries are required: * libxi, libxtst, qtx11extras (optional for auto-type on X11) * libsodium (>= 1.0.12, optional for KeePassXC-Browser support) * libargon2 +* libquazip5 Prepare the Building Environment ================================ diff --git a/ci/trusty/Dockerfile b/ci/trusty/Dockerfile index 55c604e75..cc50f8141 100644 --- a/ci/trusty/Dockerfile +++ b/ci/trusty/Dockerfile @@ -18,7 +18,7 @@ FROM ubuntu:14.04 -ENV REBUILD_COUNTER=4 +ENV REBUILD_COUNTER=5 ENV QT5_VERSION=qt53 ENV QT5_PPA_VERSION=${QT5_VERSION}2 @@ -52,8 +52,7 @@ RUN set -x \ zlib1g-dev \ libyubikey-dev \ libykpers-1-dev \ - libquazip5-headers \ - libquazip5-dev \ + libquazip-dev \ libxi-dev \ libxtst-dev \ xvfb diff --git a/snapcraft.yaml b/snapcraft.yaml index d9321d748..ad3dd18d6 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -48,6 +48,8 @@ parts: - libykpers-1-dev - libsodium-dev - libargon2-0-dev + - libquazip5-dev + - libquazip5-headers stage-packages: - dbus - qttranslations5-l10n # common translations From 66a36b3af11b597da3ae2bb867244443a5b71199 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 3 Oct 2018 10:12:19 -0400 Subject: [PATCH 04/23] Add WITH_APP_BUNDLE to INSTALL.md --- INSTALL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 5ef819899..9bdc1c868 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -108,7 +108,8 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct -DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF) -DWITH_DEV_BUILD=[ON|OFF] Enable/Disable deprecated method warnings (default: OFF) -DWITH_ASAN=[ON|OFF] Enable/Disable address sanitizer checks (Linux / macOS only) (default: OFF) - -DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF) + -DWITH_COVERAGE=[ON|OFF] Enable/Disable coverage tests (GCC only) (default: OFF) + -DWITH_APP_BUNDLE=[ON|OFF] Enable Application Bundle for macOS (default: ON) ``` * 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/` From b96b86bfa71514f4be1d22a77ecc6bfd8a76fa3b Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 19 Dec 2018 17:52:12 -0500 Subject: [PATCH 05/23] Cleanup and replumb back to functional state --- src/crypto/ssh/CMakeLists.txt | 6 +- src/gui/ApplicationSettingsWidget.cpp | 17 +- src/gui/DatabaseWidget.cpp | 4 + src/gui/MainWindow.cpp | 4 + src/gui/group/EditGroupWidget.cpp | 7 +- src/gui/group/EditGroupWidget.h | 1 - src/keeshare/KeeShare.cpp | 72 ++----- src/keeshare/KeeShare.h | 14 +- src/keeshare/SettingsWidgetKeeShare.cpp | 4 - src/keeshare/ShareObserver.cpp | 194 ++++++++---------- src/keeshare/ShareObserver.h | 23 +-- .../group/EditGroupWidgetKeeShare.cpp | 22 +- src/sshagent/CMakeLists.txt | 2 +- tests/CMakeLists.txt | 7 +- 14 files changed, 143 insertions(+), 234 deletions(-) diff --git a/src/crypto/ssh/CMakeLists.txt b/src/crypto/ssh/CMakeLists.txt index 709dd2f95..a1a31741d 100644 --- a/src/crypto/ssh/CMakeLists.txt +++ b/src/crypto/ssh/CMakeLists.txt @@ -4,9 +4,9 @@ if(WITH_XC_CRYPTO_SSH) set(crypto_ssh_SOURCES bcrypt_pbkdf.cpp blowfish.c - ASN1Key.cpp - BinaryStream.cpp - OpenSSHKey.cpp + ASN1Key.cpp + BinaryStream.cpp + OpenSSHKey.cpp ) add_library(crypto_ssh STATIC ${crypto_ssh_SOURCES}) diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 2ede0a890..1b4641b21 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -78,16 +78,17 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) connect(this, SIGNAL(apply()), SLOT(saveSettings())); connect(this, SIGNAL(rejected()), SLOT(reject())); - connect(m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableAutoSaveOnExit(bool))); + // clang-format off + connect(m_generalUi->autoSaveAfterEveryChangeCheckBox, SIGNAL(toggled(bool)), SLOT(enableAutoSaveOnExit(bool))); connect(m_generalUi->systrayShowCheckBox, SIGNAL(toggled(bool)), this, SLOT(enableSystray(bool))); - connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool))); - connect(m_secUi->lockDatabaseIdleCheckBox, - SIGNAL(toggled(bool)), - m_secUi->lockDatabaseIdleSpinBox, - SLOT(setEnabled(bool))); - - connect(m_secUi->touchIDResetCheckBox, SIGNAL(toggled(bool)), m_secUi->touchIDResetSpinBox, SLOT(setEnabled(bool))); + connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), + m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool))); + connect(m_secUi->lockDatabaseIdleCheckBox, SIGNAL(toggled(bool)), + m_secUi->lockDatabaseIdleSpinBox, SLOT(setEnabled(bool))); + connect(m_secUi->touchIDResetCheckBox, SIGNAL(toggled(bool)), + m_secUi->touchIDResetSpinBox, SLOT(setEnabled(bool))); + // clang-format on #ifndef WITH_XC_NETWORKING m_secUi->privacy->setVisible(false); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 41c5cf815..f93588a64 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -58,6 +58,7 @@ #include "gui/entry/EntryView.h" #include "gui/group/EditGroupWidget.h" #include "gui/group/GroupView.h" +#include "keeshare/KeeShare.h" #include "touchid/TouchID.h" #include "config-keepassx.h" @@ -372,6 +373,9 @@ void DatabaseWidget::replaceDatabase(QSharedPointer db) connectDatabaseSignals(); m_groupView->changeDatabase(m_db); processAutoOpen(); +#ifdef WITH_XC_KEESHARE + KeeShare::instance()->connectDatabase(m_db, oldDb); +#endif } void DatabaseWidget::cloneEntry() diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index ba0538ed9..70df886c7 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -161,12 +161,16 @@ MainWindow::MainWindow() #ifdef WITH_XC_KEESHARE KeeShare::init(this); m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); + connect(KeeShare::instance(), SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), + SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); #endif setWindowIcon(filePath()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); + // clang-format off connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); + // clang-format on m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 6c35ba923..4d024eabd 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -63,6 +63,9 @@ EditGroupWidget::EditGroupWidget(QWidget* parent) addPage(tr("Group"), FilePath::instance()->icon("actions", "document-edit"), m_editGroupWidgetMain); addPage(tr("Icon"), FilePath::instance()->icon("apps", "preferences-desktop-icons"), m_editGroupWidgetIcons); +#ifdef WITH_XC_KEESHARE + addEditPage(new EditGroupPageKeeShare(this)); +#endif addPage(tr("Properties"), FilePath::instance()->icon("actions", "document-properties"), m_editWidgetProperties); connect(m_mainUi->expireCheck, SIGNAL(toggled(bool)), m_mainUi->expireDatePicker, SLOT(setEnabled(bool))); @@ -82,10 +85,6 @@ EditGroupWidget::EditGroupWidget(QWidget* parent) // clang-format on connect(m_editGroupWidgetIcons, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage())); - -#ifdef WITH_XC_KEESHARE - addEditPage(new EditGroupPageKeeShare(this)); -#endif } EditGroupWidget::~EditGroupWidget() diff --git a/src/gui/group/EditGroupWidget.h b/src/gui/group/EditGroupWidget.h index f8ce8bb40..f013be796 100644 --- a/src/gui/group/EditGroupWidget.h +++ b/src/gui/group/EditGroupWidget.h @@ -82,7 +82,6 @@ private: QPointer m_editWidgetProperties; QScopedPointer m_temporaryGroup; - QPointer m_database; QPointer m_group; QSharedPointer m_db; diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp index b4d415539..576751998 100644 --- a/src/keeshare/KeeShare.cpp +++ b/src/keeshare/KeeShare.cpp @@ -32,10 +32,10 @@ namespace { - static const QString KeeShare_Reference("KeeShare/Reference"); - static const QString KeeShare_Own("KeeShare/Settings.own"); - static const QString KeeShare_Foreign("KeeShare/Settings.foreign"); - static const QString KeeShare_Active("KeeShare/Settings.active"); +static const QString KeeShare_Reference("KeeShare/Reference"); +static const QString KeeShare_Own("KeeShare/Settings.own"); +static const QString KeeShare_Foreign("KeeShare/Settings.foreign"); +static const QString KeeShare_Active("KeeShare/Settings.active"); } KeeShare* KeeShare::m_instance = nullptr; @@ -49,6 +49,12 @@ KeeShare* KeeShare::instance() return m_instance; } +KeeShare::KeeShare(QObject* parent) + : QObject(parent) +{ + connect(config(), SIGNAL(changed(QString)), SLOT(handleSettingsChanged(QString))); +} + void KeeShare::init(QObject* parent) { Q_ASSERT(!m_instance); @@ -162,61 +168,19 @@ QString KeeShare::indicatorSuffix(const Group* group, const QString& text) void KeeShare::connectDatabase(QSharedPointer newDb, QSharedPointer oldDb) { - if (oldDb && m_observersByDatabase.contains(oldDb.data())) { - QPointer observer = m_observersByDatabase.take(oldDb.data()); + if (oldDb && m_observersByDatabase.contains(oldDb->uuid())) { + QPointer observer = m_observersByDatabase.take(oldDb->uuid()); if (observer) { delete observer; } } - if (newDb && !m_observersByDatabase.contains(newDb.data())) { + if (newDb && !m_observersByDatabase.contains(newDb->uuid())) { QPointer observer(new ShareObserver(newDb, this)); - m_observersByDatabase[newDb.data()] = observer; + m_observersByDatabase[newDb->uuid()] = observer; connect(observer.data(), SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), - this, - SLOT(emitSharingMessage(QString, MessageWidget::MessageType))); - } -} - -void KeeShare::handleDatabaseOpened(QSharedPointer db) -{ - QPointer observer = m_observersByDatabase.value(db.data()); - if (observer) { - observer->handleDatabaseOpened(); - } -} - -void KeeShare::handleDatabaseSaved(QSharedPointer db) -{ - QPointer observer = m_observersByDatabase.value(db.data()); - if (observer) { - observer->handleDatabaseSaved(); - } -} - -void KeeShare::emitSharingMessage(const QString& message, KMessageWidget::MessageType type) -{ - QObject* observer = sender(); - auto db = m_databasesByObserver.value(observer); - if (db) { - emit sharingMessage(db, message, type); - } -} - -void KeeShare::handleDatabaseDeleted(QObject* db) -{ - auto observer = m_observersByDatabase.take(db); - if (observer) { - m_databasesByObserver.remove(observer); - } -} - -void KeeShare::handleObserverDeleted(QObject* observer) -{ - auto database = m_databasesByObserver.take(observer); - if (database) { - m_observersByDatabase.remove(database.data()); + SIGNAL(sharingMessage(QString, MessageWidget::MessageType))); } } @@ -226,9 +190,3 @@ void KeeShare::handleSettingsChanged(const QString& key) emit activeChanged(); } } - -KeeShare::KeeShare(QObject* parent) - : QObject(parent) -{ - connect(config(), SIGNAL(changed(QString)), this, SLOT(handleSettingsChanged(QString))); -} diff --git a/src/keeshare/KeeShare.h b/src/keeshare/KeeShare.h index 00a987b60..42c65f1d4 100644 --- a/src/keeshare/KeeShare.h +++ b/src/keeshare/KeeShare.h @@ -19,7 +19,7 @@ #define KEEPASSXC_KEESHARE_H #include -#include +#include #include "gui/MessageWidget.h" #include "keeshare/KeeShareSettings.h" @@ -32,7 +32,7 @@ class QXmlStreamReader; class KeeShare : public QObject { - Q_OBJECT +Q_OBJECT public: static KeeShare* instance(); static void init(QObject* parent); @@ -54,17 +54,12 @@ public: static QString referenceTypeLabel(const KeeShareSettings::Reference& reference); void connectDatabase(QSharedPointer newDb, QSharedPointer oldDb); - void handleDatabaseOpened(QSharedPointer db); - void handleDatabaseSaved(QSharedPointer db); signals: void activeChanged(); - void sharingMessage(QSharedPointer, QString, MessageWidget::MessageType); + void sharingMessage(QString, MessageWidget::MessageType); private slots: - void emitSharingMessage(const QString&, MessageWidget::MessageType); - void handleDatabaseDeleted(QObject*); - void handleObserverDeleted(QObject*); void handleSettingsChanged(const QString&); private: @@ -72,8 +67,7 @@ private: explicit KeeShare(QObject* parent); - QMap> m_observersByDatabase; - QMap> m_databasesByObserver; + QMap> m_observersByDatabase; }; #endif // KEEPASSXC_KEESHARE_H diff --git a/src/keeshare/SettingsWidgetKeeShare.cpp b/src/keeshare/SettingsWidgetKeeShare.cpp index 7da48c4df..86b9e8c87 100644 --- a/src/keeshare/SettingsWidgetKeeShare.cpp +++ b/src/keeshare/SettingsWidgetKeeShare.cpp @@ -95,10 +95,6 @@ void SettingsWidgetKeeShare::saveSettings() // TODO HNH: This depends on the order of saving new data - a better model would be to // store changes to the settings in a temporary object and check on the final values // of this object (similar scheme to Entry) - this way we could validate the settings before save - if (active.in) { - emit settingsMessage(tr("Make sure to have a history size greater than 2 to prevent data loss when importing!"), MessageWidget::Warning); - } - KeeShare::setOwn(m_own); KeeShare::setForeign(m_foreign); KeeShare::setActive(active); diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index b2b20f10b..5545c9d96 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -48,84 +48,96 @@ namespace { - static const QString KeeShare_Signature("container.share.signature"); - static const QString KeeShare_Container("container.share.kdbx"); +static const QString KeeShare_Signature("container.share.signature"); +static const QString KeeShare_Container("container.share.kdbx"); - enum Trust - { - None, - Invalid, - Single, - Lasting, - Known, - Own - }; - - QPair check(QByteArray& data, - const KeeShareSettings::Reference& reference, - const KeeShareSettings::Certificate& ownCertificate, - const QList& knownCertificates, - const KeeShareSettings::Sign& sign) - { - if (sign.signature.isEmpty()) { - QMessageBox warning; - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature")); - warning.setText(ShareObserver::tr("Do you want to import from unsigned container %1").arg(reference.path)); - auto yes = warning.addButton(ShareObserver::tr("Import once"), QMessageBox::ButtonRole::YesRole); - auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); - warning.setDefaultButton(no); - warning.exec(); - const auto trust = warning.clickedButton() == yes ? Single : None; - return qMakePair(trust, KeeShareSettings::Certificate()); - } - auto key = sign.certificate.sshKey(); - key.openKey(QString()); - const Signature signer; - if (!signer.verify(data, sign.signature, key)) { - const QFileInfo info(reference.path); - qCritical("Invalid signature for sharing container %s.", qPrintable(info.absoluteFilePath())); - return qMakePair(Invalid, KeeShareSettings::Certificate()); - } - if (ownCertificate.key == sign.certificate.key) { - return qMakePair(Own, ownCertificate); - } - for (const auto& certificate : knownCertificates) { - if (certificate.key == certificate.key && certificate.trusted) { - return qMakePair(Known, certificate); - } - } +enum Trust +{ + None, + Invalid, + Single, + Lasting, + Known, + Own +}; +QPair check(QByteArray& data, + const KeeShareSettings::Reference& reference, + const KeeShareSettings::Certificate& ownCertificate, + const QList& knownCertificates, + const KeeShareSettings::Sign& sign) +{ + if (sign.signature.isEmpty()) { QMessageBox warning; - warning.setIcon(QMessageBox::Question); - warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container")); - warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2") - .arg(sign.certificate.signer) - .arg(sign.certificate.fingerprint())); - auto yes = warning.addButton(ShareObserver::tr("Import and trust"), QMessageBox::ButtonRole::YesRole); + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature")); + warning.setText(ShareObserver::tr("Do you want to import from unsigned container %1").arg(reference.path)); + auto yes = warning.addButton(ShareObserver::tr("Import once"), QMessageBox::ButtonRole::YesRole); auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); warning.setDefaultButton(no); warning.exec(); - if (warning.clickedButton() != yes) { - qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer)); - return qMakePair(None, sign.certificate); - } - return qMakePair(Lasting, sign.certificate); + const auto trust = warning.clickedButton() == yes ? Single : None; + return qMakePair(trust, KeeShareSettings::Certificate()); } + + auto key = sign.certificate.sshKey(); + key.openKey(QString()); + const Signature signer; + if (!signer.verify(data, sign.signature, key)) { + const QFileInfo info(reference.path); + qCritical("Invalid signature for sharing container %s.", qPrintable(info.absoluteFilePath())); + return qMakePair(Invalid, KeeShareSettings::Certificate()); + } + + if (ownCertificate.key == sign.certificate.key) { + return qMakePair(Own, ownCertificate); + } + + for (const auto& certificate : knownCertificates) { + if (certificate.key == certificate.key && certificate.trusted) { + return qMakePair(Known, certificate); + } + } + + QMessageBox warning; + warning.setIcon(QMessageBox::Question); + warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2") + .arg(sign.certificate.signer) + .arg(sign.certificate.fingerprint())); + auto yes = warning.addButton(ShareObserver::tr("Import and trust"), QMessageBox::ButtonRole::YesRole); + auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); + warning.setDefaultButton(no); + warning.exec(); + if (warning.clickedButton() != yes) { + qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer)); + return qMakePair(None, sign.certificate); + } + return qMakePair(Lasting, sign.certificate); } +} // End Namespace + ShareObserver::ShareObserver(QSharedPointer db, QObject* parent) - : QObject(parent) - , m_db(std::move(db)) - , m_fileWatcher(new BulkFileWatcher(this)) + : QObject(parent) + , m_db(std::move(db)) + , m_fileWatcher(new BulkFileWatcher(this)) { - connect(KeeShare::instance(), SIGNAL(activeChanged()), this, SLOT(handleDatabaseChanged())); + connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); - connect(m_db.data(), SIGNAL(modified()), this, SLOT(handleDatabaseChanged())); + connect(m_db.data(), SIGNAL(modified()), SLOT(handleDatabaseChanged())); + connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved())); - connect(m_fileWatcher, SIGNAL(fileCreated(QString)), this, SLOT(handleFileCreated(QString))); - connect(m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(handleFileChanged(QString))); - connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), this, SLOT(handleFileRemoved(QString))); + connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), SLOT(handleFileUpdated(QString))); + + const auto active = KeeShare::active(); + if (!active.in && !active.out) { + deinitialize(); + } else { + reinitialize(); + } } ShareObserver::~ShareObserver() @@ -147,6 +159,7 @@ void ShareObserver::reinitialize() KeeShareSettings::Reference oldReference; KeeShareSettings::Reference newReference; }; + const auto active = KeeShare::active(); QList updated; QList groups = m_db->rootGroup()->groupsRecursive(true); @@ -155,6 +168,7 @@ void ShareObserver::reinitialize() if (couple.oldReference == couple.newReference) { continue; } + m_groupToReference.remove(couple.group); m_referenceToGroup.remove(couple.oldReference); m_shareToGroup.remove(couple.oldReference.path); @@ -174,6 +188,7 @@ void ShareObserver::reinitialize() if (!update.oldReference.path.isEmpty()) { m_fileWatcher->removePath(update.oldReference.path); } + if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { m_fileWatcher->addPath(update.newReference.path); } @@ -196,6 +211,7 @@ void ShareObserver::reinitialize() } } } + notifyAbout(success, warning, error); } @@ -229,20 +245,8 @@ void ShareObserver::handleDatabaseChanged() } } -void ShareObserver::handleFileUpdated(const QString& path, Change change) +void ShareObserver::handleFileUpdated(const QString& path) { - switch (change) { - case Creation: - qDebug("File created %s", qPrintable(path)); - break; - case Update: - qDebug("File changed %s", qPrintable(path)); - break; - case Deletion: - qDebug("File deleted %s", qPrintable(path)); - break; - } - const Result result = this->importFromReferenceContainer(path); if (!result.isValid()) { return; @@ -262,21 +266,6 @@ void ShareObserver::handleFileUpdated(const QString& path, Change change) notifyAbout(success, warning, error); } -void ShareObserver::handleFileCreated(const QString& path) -{ - handleFileUpdated(path, Creation); -} - -void ShareObserver::handleFileChanged(const QString& path) -{ - handleFileUpdated(path, Update); -} - -void ShareObserver::handleFileRemoved(const QString& path) -{ - handleFileUpdated(path, Deletion); -} - ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { const QFileInfo info(reference.path); @@ -471,30 +460,11 @@ Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& return targetDb; } -const QSharedPointer ShareObserver::database() const -{ - return m_db; -} - QSharedPointer ShareObserver::database() { return m_db; } -void ShareObserver::handleDatabaseOpened() -{ - if (!m_db) { - Q_ASSERT(m_db); - return; - } - const auto active = KeeShare::active(); - if (!active.in && !active.out) { - deinitialize(); - } else { - reinitialize(); - } -} - QList ShareObserver::exportIntoReferenceContainers() { QList results; diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h index 65d7161e4..28b80e093 100644 --- a/src/keeshare/ShareObserver.h +++ b/src/keeshare/ShareObserver.h @@ -35,37 +35,23 @@ class Database; class ShareObserver : public QObject { - Q_OBJECT +Q_OBJECT public: explicit ShareObserver(QSharedPointer db, QObject* parent = nullptr); ~ShareObserver(); - void handleDatabaseSaved(); - void handleDatabaseOpened(); - - const QSharedPointer database() const; QSharedPointer database(); signals: void sharingMessage(QString, MessageWidget::MessageType); -public slots: - void handleDatabaseChanged(); - private slots: - void handleFileCreated(const QString& path); - void handleFileChanged(const QString& path); - void handleFileRemoved(const QString& path); + void handleDatabaseChanged(); + void handleDatabaseSaved(); + void handleFileUpdated(const QString& path); private: - enum Change - { - Creation, - Update, - Deletion - }; - struct Result { enum Type @@ -97,7 +83,6 @@ private: QList exportIntoReferenceContainers(); void deinitialize(); void reinitialize(); - void handleFileUpdated(const QString& path, Change change); void notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error); private: diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index a9299bf7f..44a1ccb0e 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -55,10 +55,10 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); - const auto types = QList() << KeeShareSettings::Inactive - << KeeShareSettings::ImportFrom - << KeeShareSettings::ExportTo - << KeeShareSettings::SynchronizeWith; + const auto types = QList() << KeeShareSettings::Inactive + << KeeShareSettings::ImportFrom + << KeeShareSettings::ExportTo + << KeeShareSettings::SynchronizeWith; for (const auto& type : types) { QString name; switch (type) { @@ -121,7 +121,6 @@ void EditGroupWidgetKeeShare::update() m_ui->pathEdit->clear(); m_ui->passwordGenerator->hide(); m_ui->togglePasswordGeneratorButton->setChecked(false); - } else { const auto reference = KeeShare::referenceOf(m_temporaryGroup); @@ -179,14 +178,9 @@ void EditGroupWidgetKeeShare::selectPath() } switch (reference.type) { case KeeShareSettings::ImportFrom: - filename = fileDialog()->getFileName(this, - tr("Select import source"), - defaultDirPath, - filters, - nullptr, - QFileDialog::DontConfirmOverwrite, - filetype, - filename); + filename = fileDialog()->getFileName( + this, tr("Select import source"), defaultDirPath, filters, nullptr, QFileDialog::DontConfirmOverwrite, + filetype, filename); break; case KeeShareSettings::ExportTo: filename = fileDialog()->getFileName( @@ -203,7 +197,7 @@ void EditGroupWidgetKeeShare::selectPath() return; } - setPath(filename); + m_ui->pathEdit->setText(filename); config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); } diff --git a/src/sshagent/CMakeLists.txt b/src/sshagent/CMakeLists.txt index ff14356ec..d42059a5d 100644 --- a/src/sshagent/CMakeLists.txt +++ b/src/sshagent/CMakeLists.txt @@ -9,5 +9,5 @@ if(WITH_XC_SSHAGENT) ) add_library(sshagent STATIC ${sshagent_SOURCES}) - target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB}) + target_link_libraries(sshagent Qt5::Core Qt5::Widgets Qt5::Network ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB}) endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3ab9e8c8f..f984874d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -99,7 +99,12 @@ set(TEST_LIBRARIES ${GPGERROR_LIBRARIES} ${ZLIB_LIBRARIES}) -set(testsupport_SOURCES TestGlobal.h modeltest.cpp FailDevice.cpp mock/MockClock.cpp util/TemporaryFile.cpp stub/TestRandom.cpp) +set(testsupport_SOURCES + modeltest.cpp + FailDevice.cpp + mock/MockClock.cpp + util/TemporaryFile.cpp + stub/TestRandom.cpp) add_library(testsupport STATIC ${testsupport_SOURCES}) target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test) From 52dcc2855e6347eb732b2559afc9a94105a5987f Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Wed, 2 Jan 2019 10:42:21 +0100 Subject: [PATCH 06/23] Fixed signal slot connections Fixed connection to renamed CustomData::customDataModified() signal Fixed connection to renamed Database::databaseModified() signal Fixed connection to renamed Group::groupModified() signal --- src/gui/EditWidgetProperties.cpp | 2 +- src/keeshare/ShareObserver.cpp | 2 +- src/keeshare/group/EditGroupWidgetKeeShare.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui/EditWidgetProperties.cpp b/src/gui/EditWidgetProperties.cpp index bd4d00b9d..83be745af 100644 --- a/src/gui/EditWidgetProperties.cpp +++ b/src/gui/EditWidgetProperties.cpp @@ -63,7 +63,7 @@ void EditWidgetProperties::setCustomData(CustomData* customData) m_customData = customData; if (m_customData) { - connect(m_customData, SIGNAL(modified()), SLOT(update())); + connect(m_customData, SIGNAL(customDataModified()), SLOT(update())); } update(); diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 5545c9d96..109aebf09 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -125,7 +125,7 @@ ShareObserver::ShareObserver(QSharedPointer db, QObject* parent) { connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); - connect(m_db.data(), SIGNAL(modified()), SLOT(handleDatabaseChanged())); + connect(m_db.data(), SIGNAL(databaseModified()), SLOT(handleDatabaseChanged())); connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved())); connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileUpdated(QString))); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 44a1ccb0e..2d2205c69 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -92,7 +92,7 @@ void EditGroupWidgetKeeShare::setGroup(Group* temporaryGroup) m_temporaryGroup = temporaryGroup; if (m_temporaryGroup) { - connect(m_temporaryGroup, SIGNAL(modified()), SLOT(update())); + connect(m_temporaryGroup, SIGNAL(groupModified()), SLOT(update())); } update(); } From d4c391deb2a714fd9cc733b9c91c8fb698679702 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Thu, 3 Jan 2019 08:46:32 +0100 Subject: [PATCH 07/23] Splitted KeeShare into secure and insecure parts KeeShare is now supported in a secure and insecure flavor (set CMake-Flags accordingly to allow or disallow the corresponding import and exports) --- CMakeLists.txt | 15 +- src/CMakeLists.txt | 3 +- src/config-keepassx.h.cmake | 2 + src/gui/AboutDialog.cpp | 8 +- src/gui/DatabaseWidget.cpp | 2 +- src/gui/EntryPreviewWidget.cpp | 6 +- src/gui/EntryPreviewWidget.h | 2 +- src/gui/MainWindow.cpp | 4 +- src/gui/dbsettings/DatabaseSettingsDialog.cpp | 4 +- src/gui/group/EditGroupWidget.cpp | 4 +- src/gui/group/GroupModel.cpp | 4 +- src/keeshare/CMakeLists.txt | 26 +- src/keeshare/KeeShare.cpp | 12 + src/keeshare/KeeShare.h | 3 + src/keeshare/ShareObserver.cpp | 363 ++++++++++++------ src/keeshare/ShareObserver.h | 8 +- .../group/EditGroupWidgetKeeShare.cpp | 22 +- tests/CMakeLists.txt | 2 +- 18 files changed, 337 insertions(+), 153 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b61d7f78c..3370f1873 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,7 +47,8 @@ option(WITH_XC_NETWORKING "Include networking code (e.g. for downlading website option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF) option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) -option(WITH_XC_KEESHARE "Include sharing support with KeeShare." OFF) +option(WITH_XC_KEESHARE_INSECURE "Sharing integration with KeeShare with insecure sources" OFF) +option(WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources" OFF) if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() @@ -59,12 +60,20 @@ if(WITH_XC_ALL) set(WITH_XC_BROWSER ON) set(WITH_XC_YUBIKEY ON) set(WITH_XC_SSHAGENT ON) - set(WITH_XC_KEESHARE ON) + set(WITH_XC_KEESHARE_INSECURE ON) + set(WITH_XC_KEESHARE_SECURE ON) if(APPLE) set(WITH_XC_TOUCHID ON) endif() endif() +if(WITH_XC_KEESHARE_INSECURE OR WITH_XC_KEESHARE_SECURE) + set(WITH_XC_KEESHARE ON) +else() + set(WITH_XC_KEESHARE OFF) +endif() + + if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE) set(WITH_XC_CRYPTO_SSH ON) else() @@ -358,7 +367,7 @@ endif() include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) # Optional -if(WITH_XC_KEESHARE) +if(WITH_XC_KEESHARE_SECURE) find_package(QuaZip REQUIRED) include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 14fbd5886..446b74447 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,7 +202,8 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") -add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare") +add_feature_info(KeeShare-Insecure WITH_XC_KEESHARE_INSECURE "Sharing integration with KeeShare with insecure sources") +add_feature_info(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") if(APPLE) add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration") diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index 9997ec88a..7d7018861 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -18,6 +18,8 @@ #cmakedefine WITH_XC_YUBIKEY #cmakedefine WITH_XC_SSHAGENT #cmakedefine WITH_XC_KEESHARE +#cmakedefine WITH_XC_KEESHARE_INSECURE +#cmakedefine WITH_XC_KEESHARE_SECURE #cmakedefine WITH_XC_TOUCHID #cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@" diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 9e260efcb..9c9bf88e2 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -88,8 +88,12 @@ AboutDialog::AboutDialog(QWidget* parent) #ifdef WITH_XC_SSHAGENT extensions += "\n- " + tr("SSH Agent"); #endif -#ifdef WITH_XC_KEESHARE - extensions += "\n- " + tr("KeeShare"); +#if defined(WITH_XC_KEESHARE_SECURE) && defined(WITH_XC_KEESHARE_INSECURE) + extensions += "\n- " + tr("KeeShare (secure and insecure sharing)"); +#elif defined(WITH_XC_KEESHARE_SECURE) + extensions += "\n- " + tr("KeeShare (secure sharing)"); +#elif defined(WITH_XC_KEESHARE_INSECURE) + extensions += "\n- " + tr("KeeShare (insecure sharing)"); #endif #ifdef WITH_XC_YUBIKEY extensions += "\n- " + tr("YubiKey"); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index dd56b923a..d1afbfa2f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -373,7 +373,7 @@ void DatabaseWidget::replaceDatabase(QSharedPointer db) connectDatabaseSignals(); m_groupView->changeDatabase(m_db); processAutoOpen(); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) KeeShare::instance()->connectDatabase(m_db, oldDb); #endif } diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index bc6227b6b..19ec53ae9 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -26,7 +26,7 @@ #include "core/FilePath.h" #include "entry/EntryAttachmentsModel.h" #include "gui/Clipboard.h" -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) #include "keeshare/KeeShare.h" #endif @@ -107,7 +107,7 @@ void EntryPreviewWidget::setGroup(Group* selectedGroup) updateGroupGeneralTab(); updateGroupNotesTab(); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) updateGroupSharingTab(); #endif @@ -297,7 +297,7 @@ void EntryPreviewWidget::updateGroupNotesTab() m_ui->groupNotesEdit->setText(notes); } -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) void EntryPreviewWidget::updateGroupSharingTab() { Q_ASSERT(m_currentGroup); diff --git a/src/gui/EntryPreviewWidget.h b/src/gui/EntryPreviewWidget.h index f663925ca..5bfd9dbd6 100644 --- a/src/gui/EntryPreviewWidget.h +++ b/src/gui/EntryPreviewWidget.h @@ -57,7 +57,7 @@ private slots: void updateGroupHeaderLine(); void updateGroupGeneralTab(); void updateGroupNotesTab(); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) void updateGroupSharingTab(); #endif diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c7d31796f..fd9610e55 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -43,7 +43,7 @@ #include "sshagent/AgentSettingsPage.h" #include "sshagent/SSHAgent.h" #endif -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) #include "keeshare/KeeShare.h" #include "keeshare/SettingsPageKeeShare.h" #endif @@ -158,7 +158,7 @@ MainWindow::MainWindow() m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) KeeShare::init(this); m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); connect(KeeShare::instance(), SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index 992351050..5a75127c2 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -25,7 +25,7 @@ #ifdef WITH_XC_BROWSER #include "DatabaseSettingsWidgetBrowser.h" #endif -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) #include "keeshare/DatabaseSettingsPageKeeShare.h" #endif @@ -80,7 +80,7 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) m_securityTabWidget->addTab(m_masterKeyWidget, tr("Master Key")); m_securityTabWidget->addTab(m_encryptionWidget, tr("Encryption Settings")); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) addSettingsPage(new DatabaseSettingsPageKeeShare()); #endif diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 4d024eabd..abfc26412 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -23,7 +23,7 @@ #include "gui/EditWidgetIcons.h" #include "gui/EditWidgetProperties.h" -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) #include "keeshare/group/EditGroupPageKeeShare.h" #endif @@ -63,7 +63,7 @@ EditGroupWidget::EditGroupWidget(QWidget* parent) addPage(tr("Group"), FilePath::instance()->icon("actions", "document-edit"), m_editGroupWidgetMain); addPage(tr("Icon"), FilePath::instance()->icon("apps", "preferences-desktop-icons"), m_editGroupWidgetIcons); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) addEditPage(new EditGroupPageKeeShare(this)); #endif addPage(tr("Properties"), FilePath::instance()->icon("actions", "document-properties"), m_editWidgetProperties); diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index b9345495f..165eaa4a0 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -125,14 +125,14 @@ QVariant GroupModel::data(const QModelIndex& index, int role) const if (role == Qt::DisplayRole) { QString nameTemplate = tr("%1", "Template for name without annotation"); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) nameTemplate = KeeShare::indicatorSuffix(group, nameTemplate); #endif return nameTemplate.arg(group->name()); } else if (role == Qt::DecorationRole) { QPixmap pixmap = group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex) : group->iconScaledPixmap(); -#ifdef WITH_XC_KEESHARE +#if defined(WITH_XC_KEESHARE) pixmap = KeeShare::indicatorBadge(group, pixmap); #endif return pixmap; diff --git a/src/keeshare/CMakeLists.txt b/src/keeshare/CMakeLists.txt index 30a6bc4e1..14aa17b99 100644 --- a/src/keeshare/CMakeLists.txt +++ b/src/keeshare/CMakeLists.txt @@ -2,18 +2,22 @@ if(WITH_XC_KEESHARE) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) set(keeshare_SOURCES - SettingsPageKeeShare.cpp - SettingsWidgetKeeShare.cpp - DatabaseSettingsPageKeeShare.cpp - DatabaseSettingsWidgetKeeShare.cpp - group/EditGroupWidgetKeeShare.cpp - group/EditGroupPageKeeShare.cpp - KeeShare.cpp - KeeShareSettings.cpp - ShareObserver.cpp - Signature.cpp + SettingsPageKeeShare.cpp + SettingsWidgetKeeShare.cpp + DatabaseSettingsPageKeeShare.cpp + DatabaseSettingsWidgetKeeShare.cpp + group/EditGroupWidgetKeeShare.cpp + group/EditGroupPageKeeShare.cpp + KeeShare.cpp + KeeShareSettings.cpp + ShareObserver.cpp + Signature.cpp ) add_library(keeshare STATIC ${keeshare_SOURCES}) - target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${QUAZIP_LIBRARIES} ${crypto_ssh_LIB}) + if(WITH_XC_KEESHARE_SECURE) + target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${QUAZIP_LIBRARIES} ${crypto_ssh_LIB}) + else() + target_link_libraries(keeshare Qt5::Core Qt5::Widgets ${GCRYPT_LIBRARIES} ${crypto_ssh_LIB}) + endif() endif() diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp index 576751998..e552fa9c4 100644 --- a/src/keeshare/KeeShare.cpp +++ b/src/keeshare/KeeShare.cpp @@ -184,6 +184,18 @@ void KeeShare::connectDatabase(QSharedPointer newDb, QSharedPointer newDb, QSharedPointer oldDb); + static const QString& secureContainerFileType(); + static const QString& insecureContainerFileType(); + signals: void activeChanged(); void sharingMessage(QString, MessageWidget::MessageType); diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 109aebf09..d26c68dcf 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -16,6 +16,7 @@ */ #include "ShareObserver.h" +#include "config-keepassx.h" #include "core/Clock.h" #include "core/Config.h" #include "core/CustomData.h" @@ -43,8 +44,10 @@ #include #include +#if defined(WITH_XC_KEESHARE_SECURE) #include #include +#endif namespace { @@ -61,6 +64,11 @@ enum Trust Own }; +bool isOfExportType(const QFileInfo &fileInfo, const QString type) +{ + return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); +} + QPair check(QByteArray& data, const KeeShareSettings::Reference& reference, const KeeShareSettings::Certificate& ownCertificate, @@ -68,52 +76,61 @@ QPair check(QByteArray& data, const KeeShareSettings::Sign& sign) { if (sign.signature.isEmpty()) { - QMessageBox warning; - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature")); - warning.setText(ShareObserver::tr("Do you want to import from unsigned container %1").arg(reference.path)); - auto yes = warning.addButton(ShareObserver::tr("Import once"), QMessageBox::ButtonRole::YesRole); - auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); - warning.setDefaultButton(no); - warning.exec(); - const auto trust = warning.clickedButton() == yes ? Single : None; - return qMakePair(trust, KeeShareSettings::Certificate()); + for (const auto& certificate : knownCertificates) { + if (certificate.key == certificate.key && certificate.trusted) { + return {Known, certificate}; + } + } } + else { + auto key = sign.certificate.sshKey(); + key.openKey(QString()); + const Signature signer; + if (!signer.verify(data, sign.signature, key)) { + qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); + return {Invalid, KeeShareSettings::Certificate()}; + } - auto key = sign.certificate.sshKey(); - key.openKey(QString()); - const Signature signer; - if (!signer.verify(data, sign.signature, key)) { - const QFileInfo info(reference.path); - qCritical("Invalid signature for sharing container %s.", qPrintable(info.absoluteFilePath())); - return qMakePair(Invalid, KeeShareSettings::Certificate()); - } + if (ownCertificate.key == sign.certificate.key) { + return {Own, ownCertificate}; + } - if (ownCertificate.key == sign.certificate.key) { - return qMakePair(Own, ownCertificate); - } - - for (const auto& certificate : knownCertificates) { - if (certificate.key == certificate.key && certificate.trusted) { - return qMakePair(Known, certificate); + for (const auto& certificate : knownCertificates) { + if (certificate.key == certificate.key && certificate.trusted) { + return {Known, certificate}; + } } } QMessageBox warning; - warning.setIcon(QMessageBox::Question); - warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container")); - warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2") - .arg(sign.certificate.signer) - .arg(sign.certificate.fingerprint())); - auto yes = warning.addButton(ShareObserver::tr("Import and trust"), QMessageBox::ButtonRole::YesRole); - auto no = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); - warning.setDefaultButton(no); - warning.exec(); - if (warning.clickedButton() != yes) { - qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer)); - return qMakePair(None, sign.certificate); + KeeShareSettings::Certificate certificate; + if (sign.signature.isEmpty()){ + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature")); + warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import %1?").arg(reference.path)); + certificate = KeeShareSettings::Certificate(); } - return qMakePair(Lasting, sign.certificate); + else { + warning.setIcon(QMessageBox::Question); + warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2") + .arg(sign.certificate.signer) + .arg(sign.certificate.fingerprint())); + certificate = sign.certificate; + } + auto once = warning.addButton(ShareObserver::tr("Only this time"), QMessageBox::ButtonRole::YesRole); + auto always = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); + auto abort = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); + warning.setDefaultButton(abort); + warning.exec(); + if (warning.clickedButton() == once){ + return {Single, certificate}; + } + if (warning.clickedButton() == always){ + return {Lasting, certificate}; + } + qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer)); + return {None, certificate}; } } // End Namespace @@ -266,16 +283,15 @@ void ShareObserver::handleFileUpdated(const QString& path) notifyAbout(success, warning, error); } -ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +ShareObserver::Result ShareObserver::importSecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { - const QFileInfo info(reference.path); - if (!info.exists()) { - qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); - return {reference.path, Result::Warning, tr("File does not exist")}; - } - QuaZip zip(info.absoluteFilePath()); +#if !defined(WITH_XC_KEESHARE_SECURE) + Q_UNUSED(targetGroup); + return { reference.path, Result::Error, tr("Secured share container are not supported") }; +#else + QuaZip zip(reference.path); if (!zip.open(QuaZip::mdUnzip)) { - qCritical("Unable to open file %s.", qPrintable(info.absoluteFilePath())); + qCritical("Unable to open file %s.", qPrintable(reference.path)); return {reference.path, Result::Error, tr("File is not readable")}; } const auto expected = QSet() << KeeShare_Signature << KeeShare_Container; @@ -285,7 +301,7 @@ ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings: actual << file.name; } if (expected != actual) { - qCritical("Invalid sharing container %s.", qPrintable(info.absoluteFilePath())); + qCritical("Invalid sharing container %s.", qPrintable(reference.path)); return {reference.path, Result::Error, tr("Invalid sharing container")}; } @@ -313,6 +329,7 @@ ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings: qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); return {reference.path, Result::Error, reader.errorString()}; } + auto foreign = KeeShare::foreign(); auto own = KeeShare::own(); auto trusted = check(payload, reference, own.certificate, foreign.certificates, sign); @@ -352,14 +369,99 @@ ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings: merger.setForcedMergeMode(Group::Synchronize); const bool changed = merger.merge(); if (changed) { - return {reference.path, Result::Success, tr("Successful import")}; + return {reference.path, Result::Success, tr("Successful secured import")}; } return {}; } default: Q_ASSERT(false); + return {reference.path, Result::Error, tr("Unexpected error")}; + } +#endif +} + +ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +{ +#if !defined(WITH_XC_KEESHARE_INSECURE) + Q_UNUSED(targetGroup); + return {reference.path, Result::Error, tr("Insecured share container are not supported")}; +#else + QFile file(reference.path); + if (!file.open(QIODevice::ReadOnly)){ + qCritical("Unable to open file %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("File is not readable")}; + } + auto payload = file.readAll(); + file.close(); + QBuffer buffer(&payload); + buffer.open(QIODevice::ReadOnly); + + KeePass2Reader reader; + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + auto sourceDb = QSharedPointer::create(); + if (!reader.readDatabase(&buffer, key, sourceDb.data())) { + qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); + return {reference.path, Result::Error, reader.errorString()}; + } + + auto foreign = KeeShare::foreign(); + auto own = KeeShare::own(); + static KeeShareSettings::Sign sign; // invalid sign + auto trusted = check(payload, reference, own.certificate, foreign.certificates, sign); + switch(trusted.first) { + case Known: + case Lasting: { + bool found = false; + for (KeeShareSettings::Certificate& knownCertificate : foreign.certificates) { + if (knownCertificate.key == trusted.second.key) { + knownCertificate.signer = trusted.second.signer; + knownCertificate.trusted = true; + found = true; + } + } + if (!found) { + foreign.certificates << trusted.second; + // we need to update with the new signer + KeeShare::setForeign(foreign); + } + } + [[fallthrough]]; + case Single: { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful unsecured import")}; + } return {}; } + default: + qWarning("Prevent untrusted import"); + return {reference.path, Result::Warning, tr("Untrusted import prevented")}; + } +#endif +} + +ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +{ + const QFileInfo info(reference.path); + if (!info.exists()) { + qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); + return {reference.path, Result::Warning, tr("File does not exist")}; + } + + if (isOfExportType(info, KeeShare::secureContainerFileType())) { + return importSecureContainerInto(reference, targetGroup); + } + if (isOfExportType(info, KeeShare::insecureContainerFileType())) { + return importInsecureContainerInto(reference, targetGroup); + } + return {reference.path, Result::Error, tr("Unknown share container type")}; } ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString& path) @@ -465,10 +567,98 @@ QSharedPointer ShareObserver::database() return m_db; } +ShareObserver::Result ShareObserver::exportIntoReferenceSecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb) +{ +#if !defined(WITH_XC_KEESHARE_SECURE) + Q_UNUSED(targetDb); + return {reference.path, Result::Error, tr("Overwriting secured share container is not supported")}; +#else + QByteArray bytes; + { + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + KeePass2Writer writer; + writer.writeDatabase(&buffer, targetDb); + if (writer.hasError()) { + qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); + return {reference.path, Result::Error, writer.errorString()}; + } + } + const auto own = KeeShare::own(); + QuaZip zip(reference.path); + zip.setFileNameCodec("UTF-8"); + const bool zipOpened = zip.open(QuaZip::mdCreate); + if (!zipOpened) { + ::qWarning("Opening export file failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; + } + { + QuaZipFile file(&zip); + const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); + if (!signatureOpened) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + } + QTextStream stream(&file); + KeeShareSettings::Sign sign; + auto sshKey = own.key.sshKey(); + sshKey.openKey(QString()); + const Signature signer; + sign.signature = signer.create(bytes, sshKey); + sign.certificate = own.certificate; + stream << KeeShareSettings::Sign::serialize(sign); + stream.flush(); + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + } + file.close(); + } + { + QuaZipFile file(&zip); + const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); + if (!dbOpened) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + } + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + } + file.write(bytes); + file.close(); + } + zip.close(); + return {reference.path}; +#endif +} + +ShareObserver::Result ShareObserver::exportIntoReferenceInsecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb) +{ +#if !defined(WITH_XC_KEESHARE_INSECURE) + Q_UNUSED(targetDb); + return {reference.path, Result::Error, tr("Overwriting secured share container is not supported")}; +#else + QFile file(reference.path); + const bool fileOpened = file.open(QIODevice::WriteOnly); + if (!fileOpened) { + ::qWarning("Opening export file failed"); + return {reference.path, Result::Error, tr("Could not write export container")}; + } + KeePass2Writer writer; + writer.writeDatabase(&file, targetDb); + if (writer.hasError()) { + qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data()); + return {reference.path, Result::Error, writer.errorString()}; + } + file.close(); +#endif + return {reference.path}; +} + QList ShareObserver::exportIntoReferenceContainers() { QList results; - const auto own = KeeShare::own(); const auto groups = m_db->rootGroup()->groupsRecursive(true); for (const auto* group : groups) { const auto reference = KeeShare::referenceOf(group); @@ -478,76 +668,19 @@ QList ShareObserver::exportIntoReferenceContainers() m_fileWatcher->ignoreFileChanges(reference.path); QScopedPointer targetDb(exportIntoContainer(reference, group)); - QByteArray bytes; - { - QBuffer buffer(&bytes); - buffer.open(QIODevice::WriteOnly); - KeePass2Writer writer; - writer.writeDatabase(&buffer, targetDb.data()); - if (writer.hasError()) { - qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); - results << Result{reference.path, Result::Error, writer.errorString()}; - m_fileWatcher->observeFileChanges(true); - continue; - } - } - QuaZip zip(reference.path); - zip.setFileNameCodec("UTF-8"); - const bool zipOpened = zip.open(QuaZip::mdCreate); - if (!zipOpened) { - ::qWarning("Opening export file failed: %d", zip.getZipError()); - results << Result{reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; + QFileInfo info(reference.path); + if (isOfExportType(info, KeeShare::secureContainerFileType())) { + results << exportIntoReferenceSecureContainer(reference, targetDb.data()); m_fileWatcher->observeFileChanges(true); continue; } - { - QuaZipFile file(&zip); - const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); - if (!signatureOpened) { - ::qWarning("Embedding signature failed: %d", zip.getZipError()); - results << Result{reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; - m_fileWatcher->observeFileChanges(true); - continue; - } - QTextStream stream(&file); - KeeShareSettings::Sign sign; - auto sshKey = own.key.sshKey(); - sshKey.openKey(QString()); - const Signature signer; - sign.signature = signer.create(bytes, sshKey); - sign.certificate = own.certificate; - stream << KeeShareSettings::Sign::serialize(sign); - stream.flush(); - if (file.getZipError() != ZIP_OK) { - ::qWarning("Embedding signature failed: %d", zip.getZipError()); - results << Result{reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; - m_fileWatcher->observeFileChanges(true); - continue; - } - file.close(); + if (isOfExportType(info, KeeShare::insecureContainerFileType())) { + results << exportIntoReferenceInsecureContainer(reference, targetDb.data()); + m_fileWatcher->observeFileChanges(true); + continue; } - { - QuaZipFile file(&zip); - const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); - if (!dbOpened) { - ::qWarning("Embedding database failed: %d", zip.getZipError()); - results << Result{reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; - m_fileWatcher->observeFileChanges(true); - continue; - } - if (file.getZipError() != ZIP_OK) { - ::qWarning("Embedding database failed: %d", zip.getZipError()); - results << Result{reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; - m_fileWatcher->observeFileChanges(true); - continue; - } - file.write(bytes); - file.close(); - } - zip.close(); - - m_fileWatcher->observeFileChanges(true); - results << Result{reference.path}; + Q_ASSERT(false); + results << Result{reference.path, Result::Error, tr("Unexpected export error occurred")}; } return results; } diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h index 28b80e093..b21934389 100644 --- a/src/keeshare/ShareObserver.h +++ b/src/keeshare/ShareObserver.h @@ -77,10 +77,16 @@ private: static void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb); static Database* exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot); + static Result exportIntoReferenceInsecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb); + static Result exportIntoReferenceSecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb); + static Result importSecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); + static Result importInsecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); static Result importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); + static Result importDatabaseInto(); Result importFromReferenceContainer(const QString& path); - QList exportIntoReferenceContainers(); + QList exportIntoReferenceContainers(); + void deinitialize(); void reinitialize(); void notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 2d2205c69..2881a6630 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -170,26 +170,36 @@ void EditGroupWidgetKeeShare::selectPath() defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); } auto reference = KeeShare::referenceOf(m_temporaryGroup); - const auto filetype = tr("kdbx.share", "Filetype for KeeShare container"); - const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare Container"), tr("All files")); + QString defaultFiletype = ""; + auto knownFilters = QStringList() << QString("%1 (*)").arg("All files"); +#if defined(WITH_XC_KEESHARE_INSECURE) + defaultFiletype = KeeShare::secureContainerFileType(); + knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare insecure container"), KeeShare::insecureContainerFileType())); +#endif +#if defined(WITH_XC_KEESHARE_SECURE) + defaultFiletype = KeeShare::secureContainerFileType(); + knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare secure container"), KeeShare::secureContainerFileType())); +#endif + + const auto filters = knownFilters.join(";;"); auto filename = reference.path; if (filename.isEmpty()) { - filename = tr("%1.%2", "Template for KeeShare container").arg(m_temporaryGroup->name()).arg(filetype); + filename = tr("%1.%2", "Template for KeeShare container").arg(m_temporaryGroup->name()).arg(defaultFiletype); } switch (reference.type) { case KeeShareSettings::ImportFrom: filename = fileDialog()->getFileName( this, tr("Select import source"), defaultDirPath, filters, nullptr, QFileDialog::DontConfirmOverwrite, - filetype, filename); + defaultFiletype, filename); break; case KeeShareSettings::ExportTo: filename = fileDialog()->getFileName( - this, tr("Select export target"), defaultDirPath, filters, nullptr, 0, filetype, filename); + this, tr("Select export target"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); break; case KeeShareSettings::SynchronizeWith: case KeeShareSettings::Inactive: filename = fileDialog()->getFileName( - this, tr("Select import/export file"), defaultDirPath, filters, nullptr, 0, filetype, filename); + this, tr("Select import/export file"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); break; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f984874d8..390dda57b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -141,7 +141,7 @@ add_unit_test(NAME testsymmetriccipher SOURCES TestSymmetricCipher.cpp if(WITH_XC_KEESHARE) add_unit_test(NAME testsignature SOURCES TestSignature.cpp - LIBS ${TEST_LIBRARIES}) + LIBS ${TEST_LIBRARIES}) endif() add_unit_test(NAME testhashedblockstream SOURCES TestHashedBlockStream.cpp From 2e183888256d51796f3e2fef933509f62aad231a Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Thu, 3 Jan 2019 17:50:36 +0100 Subject: [PATCH 08/23] Fixed bug in FileWatcher, improved unsafe sharing BulkFileWatcher didn't recognize added files in observed directory Improved UI for unsecured sharing only (hide certificate ui, added warnings and adjusted indicators) --- src/core/FileWatcher.cpp | 38 ++-- src/gui/FileDialog.cpp | 4 +- .../DatabaseSettingsWidgetKeeShare.cpp | 2 +- src/keeshare/KeeShare.cpp | 24 ++- src/keeshare/KeeShare.h | 1 + src/keeshare/KeeShareSettings.cpp | 46 +++-- src/keeshare/KeeShareSettings.h | 32 ++-- src/keeshare/SettingsWidgetKeeShare.cpp | 37 ++-- src/keeshare/ShareObserver.cpp | 177 +++++++++++------- .../group/EditGroupWidgetKeeShare.cpp | 57 +++++- src/keeshare/group/EditGroupWidgetKeeShare.h | 2 +- tests/TestSharing.cpp | 65 ++++--- 12 files changed, 319 insertions(+), 166 deletions(-) diff --git a/src/core/FileWatcher.cpp b/src/core/FileWatcher.cpp index ac44174bd..4873adbf4 100644 --- a/src/core/FileWatcher.cpp +++ b/src/core/FileWatcher.cpp @@ -182,28 +182,28 @@ void BulkFileWatcher::handleFileChanged(const QString& path) void BulkFileWatcher::handleDirectoryChanged(const QString& path) { qDebug("Directory changed %s", qPrintable(path)); - const QFileInfo directory(path); - const QMap& watchedFiles = m_watchedFilesInDirectory[directory.absolutePath()]; - for (const QString& file : watchedFiles.keys()) { - const QFileInfo info(file); - const bool existed = watchedFiles[info.absoluteFilePath()]; - if (!info.exists() && existed) { - qDebug("Remove watch file %s", qPrintable(info.absoluteFilePath())); - m_fileWatcher.removePath(info.absolutePath()); - emit fileRemoved(info.absoluteFilePath()); + const QFileInfo directoryInfo(path); + const QMap& watchedFiles = m_watchedFilesInDirectory[directoryInfo.absoluteFilePath()]; + for (const QString& filename : watchedFiles.keys()) { + const QFileInfo fileInfo(filename); + const bool existed = watchedFiles[fileInfo.absoluteFilePath()]; + if (!fileInfo.exists() && existed) { + qDebug("Remove watch file %s", qPrintable(fileInfo.absoluteFilePath())); + m_fileWatcher.removePath(fileInfo.absolutePath()); + emit fileRemoved(fileInfo.absoluteFilePath()); } - if (!existed && info.exists()) { - qDebug("Add watch file %s", qPrintable(info.absoluteFilePath())); - m_fileWatcher.addPath(info.absolutePath()); - emit fileCreated(info.absoluteFilePath()); + if (!existed && fileInfo.exists()) { + qDebug("Add watch file %s", qPrintable(fileInfo.absoluteFilePath())); + m_fileWatcher.addPath(fileInfo.absolutePath()); + emit fileCreated(fileInfo.absoluteFilePath()); } - if (existed && info.exists()) { - qDebug("Refresh watch file %s", qPrintable(info.absoluteFilePath())); - m_fileWatcher.removePath(info.absolutePath()); - m_fileWatcher.addPath(info.absolutePath()); - emit fileChanged(info.absoluteFilePath()); + if (existed && fileInfo.exists()) { + qDebug("Refresh watch file %s", qPrintable(fileInfo.absoluteFilePath())); + m_fileWatcher.removePath(fileInfo.absolutePath()); + m_fileWatcher.addPath(fileInfo.absolutePath()); + emit fileChanged(fileInfo.absoluteFilePath()); } - m_watchedFilesInDirectory[info.absolutePath()][info.absoluteFilePath()] = info.exists(); + m_watchedFilesInDirectory[fileInfo.absolutePath()][fileInfo.absoluteFilePath()] = fileInfo.exists(); } } diff --git a/src/gui/FileDialog.cpp b/src/gui/FileDialog.cpp index 77c08e3eb..a39ee9e6b 100644 --- a/src/gui/FileDialog.cpp +++ b/src/gui/FileDialog.cpp @@ -114,7 +114,9 @@ QString FileDialog::getFileName(QWidget* parent, dialog.selectFile(defaultName); } dialog.setOptions(options); - dialog.setDefaultSuffix(defaultExtension); + if (!defaultExtension.isEmpty()) { + dialog.setDefaultSuffix(defaultExtension); + } dialog.setLabelText(QFileDialog::Accept, QFileDialog::tr("Select")); QStringList results; if (dialog.exec()) { diff --git a/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp b/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp index cb24c6ed5..6180c184e 100644 --- a/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp +++ b/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp @@ -57,7 +57,7 @@ void DatabaseSettingsWidgetKeeShare::loadSettings(QSharedPointer db) QStringList hierarchy = group->hierarchy(); hierarchy.removeFirst(); QList row = QList(); - row << new QStandardItem(hierarchy.join(" > ")); + row << new QStandardItem(hierarchy.join(tr(" > ", "Breadcrumb separator"))); row << new QStandardItem(KeeShare::referenceTypeLabel(reference)); row << new QStandardItem(reference.path); m_referencesModel->appendRow(row); diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp index e552fa9c4..3e15d2137 100644 --- a/src/keeshare/KeeShare.cpp +++ b/src/keeshare/KeeShare.cpp @@ -125,16 +125,30 @@ void KeeShare::setReferenceTo(Group* group, const KeeShareSettings::Reference& r customData->set(KeeShare_Reference, encoded); } +bool KeeShare::isEnabled(const Group* group) +{ + const auto reference = KeeShare::referenceOf(group); +#if !defined(WITH_XC_KEESHARE_SECURE) + if (reference.path.endsWith(secureContainerFileType(), Qt::CaseInsensitive)){ + return false; + } +#endif +#if !defined(WITH_XC_KEESHARE_INSECURE) + if (reference.path.endsWith(insecureContainerFileType(), Qt::CaseInsensitive)){ + return false; + } +#endif + const auto active = KeeShare::active(); + return (reference.isImporting() && active.in) || (reference.isExporting() && active.out); +} + QPixmap KeeShare::indicatorBadge(const Group* group, QPixmap pixmap) { if (!isShared(group)) { return pixmap; } - const auto reference = KeeShare::referenceOf(group); - const auto active = KeeShare::active(); - const bool enabled = (reference.isImporting() && active.in) || (reference.isExporting() && active.out); - const QPixmap badge = enabled ? databaseIcons()->iconPixmap(DatabaseIcons::SharedIconIndex) - : databaseIcons()->iconPixmap(DatabaseIcons::UnsharedIconIndex); + const QPixmap badge = isEnabled(group) ? databaseIcons()->iconPixmap(DatabaseIcons::SharedIconIndex) + : databaseIcons()->iconPixmap(DatabaseIcons::UnsharedIconIndex); QImage canvas = pixmap.toImage(); const QRectF target(canvas.width() * 0.4, canvas.height() * 0.4, canvas.width() * 0.6, canvas.height() * 0.6); QPainter painter(&canvas); diff --git a/src/keeshare/KeeShare.h b/src/keeshare/KeeShare.h index a0a7d836e..3692361ec 100644 --- a/src/keeshare/KeeShare.h +++ b/src/keeshare/KeeShare.h @@ -41,6 +41,7 @@ public: static QPixmap indicatorBadge(const Group* group, QPixmap pixmap); static bool isShared(const Group* group); + static bool isEnabled(const Group *group); static KeeShareSettings::Own own(); static KeeShareSettings::Active active(); diff --git a/src/keeshare/KeeShareSettings.cpp b/src/keeshare/KeeShareSettings.cpp index a1fcfac37..47ad76df6 100644 --- a/src/keeshare/KeeShareSettings.cpp +++ b/src/keeshare/KeeShareSettings.cpp @@ -32,10 +32,9 @@ namespace KeeShareSettings { namespace { - Certificate packCertificate(const OpenSSHKey& key, bool verified, const QString& signer) + Certificate packCertificate(const OpenSSHKey& key, const QString& signer) { KeeShareSettings::Certificate extracted; - extracted.trusted = verified; extracted.signer = signer; Q_ASSERT(key.type() == "ssh-rsa"); extracted.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key); @@ -108,9 +107,6 @@ namespace KeeShareSettings writer.writeStartElement("Signer"); writer.writeCharacters(certificate.signer); writer.writeEndElement(); - writer.writeStartElement("Trusted"); - writer.writeCharacters(certificate.trusted ? "True" : "False"); - writer.writeEndElement(); writer.writeStartElement("Key"); writer.writeCharacters(certificate.key.toBase64()); writer.writeEndElement(); @@ -118,7 +114,7 @@ namespace KeeShareSettings bool Certificate::operator==(const Certificate& other) const { - return trusted == other.trusted && key == other.key && signer == other.signer; + return key == other.key && signer == other.signer; } bool Certificate::operator!=(const Certificate& other) const @@ -128,7 +124,7 @@ namespace KeeShareSettings bool Certificate::isNull() const { - return !trusted && key.isEmpty() && signer.isEmpty(); + return key.isEmpty() && signer.isEmpty(); } QString Certificate::fingerprint() const @@ -158,8 +154,6 @@ namespace KeeShareSettings while (!reader.error() && reader.readNextStartElement()) { if (reader.name() == "Signer") { certificate.signer = reader.readElementText(); - } else if (reader.name() == "Trusted") { - certificate.trusted = reader.readElementText() == "True"; } else if (reader.name() == "Key") { certificate.key = QByteArray::fromBase64(reader.readElementText().toLatin1()); } @@ -217,7 +211,7 @@ namespace KeeShareSettings Own own; own.key = packKey(key); const QString name = qgetenv("USER"); // + "@" + QHostInfo::localHostName(); - own.certificate = packCertificate(key, true, name); + own.certificate = packCertificate(key, name); return own; } @@ -301,13 +295,39 @@ namespace KeeShareSettings return own; } + bool ScopedCertificate::operator==(const ScopedCertificate &other) const + { + return trusted == other.trusted && path == other.path && certificate == other.certificate; + } + + bool ScopedCertificate::operator!=(const ScopedCertificate &other) const + { + return !operator==(other); + } + + void ScopedCertificate::serialize(QXmlStreamWriter& writer, const ScopedCertificate& scopedCertificate) + { + writer.writeAttribute("Path", scopedCertificate.path); + writer.writeAttribute("Trusted", scopedCertificate.trusted ? "True" : "False"); + Certificate::serialize(writer, scopedCertificate.certificate); + } + + ScopedCertificate ScopedCertificate::deserialize(QXmlStreamReader &reader) + { + ScopedCertificate scopedCertificate; + scopedCertificate.path = reader.attributes().value("Path").toString(); + scopedCertificate.trusted = reader.attributes().value("Trusted") == "True"; + scopedCertificate.certificate = Certificate::deserialize(reader); + return scopedCertificate; + } + QString Foreign::serialize(const Foreign& foreign) { return xmlSerialize([&](QXmlStreamWriter& writer) { writer.writeStartElement("Foreign"); - for (const Certificate& certificate : foreign.certificates) { + for (const ScopedCertificate& scopedCertificate : foreign.certificates) { writer.writeStartElement("Certificate"); - Certificate::serialize(writer, certificate); + ScopedCertificate::serialize(writer, scopedCertificate); writer.writeEndElement(); } writer.writeEndElement(); @@ -322,7 +342,7 @@ namespace KeeShareSettings if (reader.name() == "Foreign") { while (!reader.error() && reader.readNextStartElement()) { if (reader.name() == "Certificate") { - foreign.certificates << Certificate::deserialize(reader); + foreign.certificates << ScopedCertificate::deserialize(reader); } else { ::qWarning() << "Unknown Cerificates element" << reader.name(); reader.skipCurrentElement(); diff --git a/src/keeshare/KeeShareSettings.h b/src/keeshare/KeeShareSettings.h index 94d67d1ca..a6522a5b4 100644 --- a/src/keeshare/KeeShareSettings.h +++ b/src/keeshare/KeeShareSettings.h @@ -33,16 +33,10 @@ namespace KeeShareSettings { QByteArray key; QString signer; - bool trusted; bool operator==(const Certificate& other) const; bool operator!=(const Certificate& other) const; - Certificate() - : trusted(false) - { - } - bool isNull() const; QString fingerprint() const; QString publicKey() const; @@ -71,11 +65,13 @@ namespace KeeShareSettings { bool in; bool out; + Active() : in(false) , out(false) { } + bool isNull() const { return !in && !out; @@ -92,6 +88,7 @@ namespace KeeShareSettings bool operator==(const Own& other) const; bool operator!=(const Own& other) const; + bool isNull() const { return key.isNull() && certificate.isNull(); @@ -102,14 +99,25 @@ namespace KeeShareSettings static Own generate(); }; + struct ScopedCertificate + { + QString path; + Certificate certificate; + bool trusted; + + bool operator==(const ScopedCertificate& other) const; + bool operator!=(const ScopedCertificate& other) const; + + bool isUnknown() const { return certificate.isNull(); } + bool isKnown() const { return !certificate.isNull(); } + + static void serialize(QXmlStreamWriter& writer, const ScopedCertificate& certificate); + static ScopedCertificate deserialize(QXmlStreamReader& reader); + }; + struct Foreign { - QList certificates; - - bool isNull() const - { - return certificates.isEmpty(); - } + QList certificates; static QString serialize(const Foreign& foreign); static Foreign deserialize(const QString& raw); diff --git a/src/keeshare/SettingsWidgetKeeShare.cpp b/src/keeshare/SettingsWidgetKeeShare.cpp index 86b9e8c87..2b5ba3e3c 100644 --- a/src/keeshare/SettingsWidgetKeeShare.cpp +++ b/src/keeshare/SettingsWidgetKeeShare.cpp @@ -34,6 +34,11 @@ SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent) { m_ui->setupUi(this); +#if !defined(WITH_XC_KEESHARE_SECURE) + // Setting does not help the user of Version without secure export + m_ui->ownCertificateGroupBox->setVisible(false); +#endif + connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString))); connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate())); @@ -65,15 +70,25 @@ void SettingsWidgetKeeShare::loadSettings() void SettingsWidgetKeeShare::updateForeignCertificates() { m_importedCertificateModel.reset(new QStandardItemModel()); - m_importedCertificateModel->setHorizontalHeaderLabels( - QStringList() << tr("Signer") << tr("Status") << tr("Fingerprint") << tr("Certificate")); + m_importedCertificateModel->setHorizontalHeaderLabels(QStringList() << tr("Path") << tr("Status") +#if defined(WITH_XC_KEESHARE_SECURE) + << tr("Signer") << tr("Fingerprint") << tr("Certificate") +#endif + ); - for (const KeeShareSettings::Certificate& certificate : m_foreign.certificates) { - QStandardItem* signer = new QStandardItem(certificate.signer); - QStandardItem* verified = new QStandardItem(certificate.trusted ? tr("trusted") : tr("untrusted")); - QStandardItem* fingerprint = new QStandardItem(certificate.fingerprint()); - QStandardItem* key = new QStandardItem(certificate.publicKey()); - m_importedCertificateModel->appendRow(QList() << signer << verified << fingerprint << key); + for (const auto& scopedCertificate : m_foreign.certificates) { + const auto items = QList() + << new QStandardItem(scopedCertificate.path) + << new QStandardItem(scopedCertificate.trusted ? tr("Trusted") : tr("Untrusted")) +#if defined(WITH_XC_KEESHARE_SECURE) + << new QStandardItem(scopedCertificate.isKnown() + ? scopedCertificate.certificate.signer + : tr("Unknown")) + << new QStandardItem(scopedCertificate.certificate.fingerprint()) + << new QStandardItem(scopedCertificate.certificate.publicKey()) +#endif + ; + m_importedCertificateModel->appendRow(items); } m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data()); @@ -124,7 +139,7 @@ void SettingsWidgetKeeShare::importCertificate() } const auto filetype = tr("key.share", "Filetype for KeeShare key"); const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); - QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, 0); + QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0)); if (filename.isEmpty()) { return; } @@ -161,7 +176,7 @@ void SettingsWidgetKeeShare::exportCertificate() const auto filetype = tr("key.share", "Filetype for KeeShare key"); const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype); - filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, 0, filetype, filename); + filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0), filetype, filename); if (filename.isEmpty()) { return; } @@ -198,7 +213,7 @@ void SettingsWidgetKeeShare::untrustSelectedCertificates() void SettingsWidgetKeeShare::removeSelectedCertificates() { - QList certificates = m_foreign.certificates; + auto certificates = m_foreign.certificates; const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); Q_ASSERT(selectionModel); for (const auto& index : selectionModel->selectedRows()) { diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index d26c68dcf..2f98b1bcd 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -56,12 +56,12 @@ static const QString KeeShare_Container("container.share.kdbx"); enum Trust { - None, Invalid, - Single, - Lasting, - Known, - Own + Own, + UntrustedForever, + UntrustedOnce, + TrustedOnce, + TrustedForever, }; bool isOfExportType(const QFileInfo &fileInfo, const QString type) @@ -72,17 +72,12 @@ bool isOfExportType(const QFileInfo &fileInfo, const QString type) QPair check(QByteArray& data, const KeeShareSettings::Reference& reference, const KeeShareSettings::Certificate& ownCertificate, - const QList& knownCertificates, + const QList& knownCertificates, const KeeShareSettings::Sign& sign) { - if (sign.signature.isEmpty()) { - for (const auto& certificate : knownCertificates) { - if (certificate.key == certificate.key && certificate.trusted) { - return {Known, certificate}; - } - } - } - else { + KeeShareSettings::Certificate certificate; + if (!sign.signature.isEmpty()) { + certificate = sign.certificate; auto key = sign.certificate.sshKey(); key.openKey(QString()); const Signature signer; @@ -92,45 +87,61 @@ QPair check(QByteArray& data, } if (ownCertificate.key == sign.certificate.key) { - return {Own, ownCertificate}; + return {Own, ownCertificate }; } - - for (const auto& certificate : knownCertificates) { - if (certificate.key == certificate.key && certificate.trusted) { - return {Known, certificate}; - } + } + enum Scope { Invalid, Global, Local }; + Scope scope = Invalid; + bool trusted = false; + for (const auto& scopedCertificate : knownCertificates) { + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + // Global scope is overwritten by local scope + scope = Global; + trusted = scopedCertificate.trusted; } + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + scope = Local; + trusted = scopedCertificate.trusted; + break; + } + } + if (scope != Invalid){ + // we introduce now scopes if there is a global + return {trusted ? TrustedForever : UntrustedForever, certificate}; } QMessageBox warning; - KeeShareSettings::Certificate certificate; if (sign.signature.isEmpty()){ warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(ShareObserver::tr("Untrustworthy container without signature")); - warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import %1?").arg(reference.path)); - certificate = KeeShareSettings::Certificate(); + warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); + warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import from %1?") + .arg(reference.path)); } else { warning.setIcon(QMessageBox::Question); - warning.setWindowTitle(ShareObserver::tr("Import from untrustworthy certificate for sharing container")); - warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2") - .arg(sign.certificate.signer) - .arg(sign.certificate.fingerprint())); - certificate = sign.certificate; + warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") + .arg(certificate.signer, certificate.fingerprint(), reference.path)); } - auto once = warning.addButton(ShareObserver::tr("Only this time"), QMessageBox::ButtonRole::YesRole); - auto always = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); - auto abort = warning.addButton(ShareObserver::tr("No"), QMessageBox::ButtonRole::NoRole); - warning.setDefaultButton(abort); + auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); + auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); + auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); + auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); + warning.setDefaultButton(untrustedOnce); warning.exec(); - if (warning.clickedButton() == once){ - return {Single, certificate}; + if (warning.clickedButton() == trustedForever){ + return {TrustedForever, certificate }; } - if (warning.clickedButton() == always){ - return {Lasting, certificate}; + if (warning.clickedButton() == trustedOnce){ + return {TrustedOnce, certificate}; } - qWarning("Prevented import due to untrusted certificate of %s", qPrintable(sign.certificate.signer)); - return {None, certificate}; + if (warning.clickedButton() == untrustedOnce){ + return {UntrustedOnce, certificate }; + } + if (warning.clickedButton() == untrustedForever){ + return {UntrustedForever, certificate }; + } + return {UntrustedOnce, certificate }; } } // End Namespace @@ -287,7 +298,7 @@ ShareObserver::Result ShareObserver::importSecureContainerInto(const KeeShareSet { #if !defined(WITH_XC_KEESHARE_SECURE) Q_UNUSED(targetGroup); - return { reference.path, Result::Error, tr("Secured share container are not supported") }; + return { reference.path, Result::Warning, tr("Secured share container are not supported - import prevented") }; #else QuaZip zip(reference.path); if (!zip.open(QuaZip::mdUnzip)) { @@ -332,34 +343,45 @@ ShareObserver::Result ShareObserver::importSecureContainerInto(const KeeShareSet auto foreign = KeeShare::foreign(); auto own = KeeShare::own(); - auto trusted = check(payload, reference, own.certificate, foreign.certificates, sign); - switch (trusted.first) { - case None: - qWarning("Prevent untrusted import"); - return {reference.path, Result::Warning, tr("Untrusted import prevented")}; - + auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); + switch (trust.first) { case Invalid: - qCritical("Prevent untrusted import"); + qWarning("Prevent untrusted import"); return {reference.path, Result::Error, tr("Untrusted import prevented")}; - case Known: - case Lasting: { + case UntrustedForever: + case TrustedForever: { bool found = false; - for (KeeShareSettings::Certificate& knownCertificate : foreign.certificates) { - if (knownCertificate.key == trusted.second.key) { - knownCertificate.signer = trusted.second.signer; - knownCertificate.trusted = true; + bool trusted = trust.first == TrustedForever; + for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { + if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trusted = trusted; found = true; } } if (!found) { - foreign.certificates << trusted.second; + foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; // we need to update with the new signer KeeShare::setForeign(foreign); } + if (trusted) { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful secured import")}; + } + } + // Silent ignore of untrusted import or unchanging import + return {}; } - [[fallthrough]]; - case Single: + case TrustedOnce: case Own: { qDebug("Synchronize %s %s with %s", qPrintable(reference.path), @@ -384,7 +406,7 @@ ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareS { #if !defined(WITH_XC_KEESHARE_INSECURE) Q_UNUSED(targetGroup); - return {reference.path, Result::Error, tr("Insecured share container are not supported")}; + return {reference.path, Result::Warning, tr("Insecured share container are not supported - import prevented")}; #else QFile file(reference.path); if (!file.open(QIODevice::ReadOnly)){ @@ -408,26 +430,41 @@ ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareS auto foreign = KeeShare::foreign(); auto own = KeeShare::own(); static KeeShareSettings::Sign sign; // invalid sign - auto trusted = check(payload, reference, own.certificate, foreign.certificates, sign); - switch(trusted.first) { - case Known: - case Lasting: { + auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); + switch(trust.first) { + case UntrustedForever: + case TrustedForever: { bool found = false; - for (KeeShareSettings::Certificate& knownCertificate : foreign.certificates) { - if (knownCertificate.key == trusted.second.key) { - knownCertificate.signer = trusted.second.signer; - knownCertificate.trusted = true; + bool trusted = trust.first == TrustedForever; + for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { + if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trusted = trusted; found = true; } } if (!found) { - foreign.certificates << trusted.second; + foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; // we need to update with the new signer KeeShare::setForeign(foreign); } + if (trusted) { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful secured import")}; + } + } + return {}; } - [[fallthrough]]; - case Single: { + + case TrustedOnce: { qDebug("Synchronize %s %s with %s", qPrintable(reference.path), qPrintable(targetGroup->name()), @@ -571,7 +608,7 @@ ShareObserver::Result ShareObserver::exportIntoReferenceSecureContainer(const Ke { #if !defined(WITH_XC_KEESHARE_SECURE) Q_UNUSED(targetDb); - return {reference.path, Result::Error, tr("Overwriting secured share container is not supported")}; + return {reference.path, Result::Warning, tr("Overwriting secured share container is not supported - export prevented")}; #else QByteArray bytes; { @@ -637,7 +674,7 @@ ShareObserver::Result ShareObserver::exportIntoReferenceInsecureContainer(const { #if !defined(WITH_XC_KEESHARE_INSECURE) Q_UNUSED(targetDb); - return {reference.path, Result::Error, tr("Overwriting secured share container is not supported")}; + return {reference.path, Result::Warning, tr("Overwriting secured share container is not supported - export prevented")}; #else QFile file(reference.path); const bool fileOpened = file.open(QIODevice::WriteOnly); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 2881a6630..76ae6b122 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -49,8 +49,8 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); - connect(m_ui->pathEdit, SIGNAL(textChanged(QString)), SLOT(setPath(QString))); - connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(selectPath())); + connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); + connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); @@ -102,15 +102,40 @@ void EditGroupWidgetKeeShare::showSharingState() if (!m_temporaryGroup) { return; } + + auto supportedExtensions = QStringList(); +#if defined(WITH_XC_KEESHARE_INSECURE) + supportedExtensions << KeeShare::insecureContainerFileType(); +#endif +#if defined(WITH_XC_KEESHARE_SECURE) + supportedExtensions << KeeShare::secureContainerFileType(); +#endif + const auto reference = KeeShare::referenceOf(m_temporaryGroup); + if (!reference.path.isEmpty()) { + bool supported = false; + for(const auto &extension : supportedExtensions){ + if (reference.path.endsWith(extension, Qt::CaseInsensitive)){ + supported = true; + break; + } + } + if (!supported) { + m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing your container type. Please use %1.").arg(supportedExtensions.join(", ")), MessageWidget::Warning); + return; + } + } const auto active = KeeShare::active(); if (!active.in && !active.out) { m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information); + return; } if (active.in && !active.out) { m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information); + return; } if (!active.in && active.out) { m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information); + return; } } @@ -149,17 +174,17 @@ void EditGroupWidgetKeeShare::setGeneratedPassword(const QString& password) m_ui->togglePasswordGeneratorButton->setChecked(false); } -void EditGroupWidgetKeeShare::setPath(const QString& path) +void EditGroupWidgetKeeShare::selectPath() { if (!m_temporaryGroup) { return; } auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.path = path; + reference.path = m_ui->pathEdit->text(); KeeShare::setReferenceTo(m_temporaryGroup, reference); } -void EditGroupWidgetKeeShare::selectPath() +void EditGroupWidgetKeeShare::launchPathSelectionDialog() { if (!m_temporaryGroup) { return; @@ -171,20 +196,28 @@ void EditGroupWidgetKeeShare::selectPath() } auto reference = KeeShare::referenceOf(m_temporaryGroup); QString defaultFiletype = ""; + auto supportedExtensions = QStringList(); + auto unsupportedExtensions = QStringList(); auto knownFilters = QStringList() << QString("%1 (*)").arg("All files"); #if defined(WITH_XC_KEESHARE_INSECURE) - defaultFiletype = KeeShare::secureContainerFileType(); + defaultFiletype = KeeShare::insecureContainerFileType(); + supportedExtensions << KeeShare::insecureContainerFileType(); knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare insecure container"), KeeShare::insecureContainerFileType())); +#else + unsupportedExtensions << KeeShare::insecureContainerFileType(); #endif #if defined(WITH_XC_KEESHARE_SECURE) defaultFiletype = KeeShare::secureContainerFileType(); + supportedExtensions << KeeShare::secureContainerFileType(); knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare secure container"), KeeShare::secureContainerFileType())); +#else + unsupportedExtensions << KeeShare::secureContainerFileType(); #endif const auto filters = knownFilters.join(";;"); auto filename = reference.path; if (filename.isEmpty()) { - filename = tr("%1.%2", "Template for KeeShare container").arg(m_temporaryGroup->name()).arg(defaultFiletype); + filename = m_temporaryGroup->name(); } switch (reference.type) { case KeeShareSettings::ImportFrom: @@ -206,6 +239,16 @@ void EditGroupWidgetKeeShare::selectPath() if (filename.isEmpty()) { return; } + bool validFilename = false; + for(const auto& extension : supportedExtensions + unsupportedExtensions){ + if (filename.endsWith(extension, Qt::CaseInsensitive)) { + validFilename = true; + break; + } + } + if (!validFilename){ + filename += (!filename.endsWith(".") ? "." : "") + defaultFiletype; + } m_ui->pathEdit->setText(filename); config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.h b/src/keeshare/group/EditGroupWidgetKeeShare.h index b01bada44..140f13c86 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.h +++ b/src/keeshare/group/EditGroupWidgetKeeShare.h @@ -46,8 +46,8 @@ private slots: void update(); void selectType(); void selectPassword(); + void launchPathSelectionDialog(); void selectPath(); - void setPath(const QString& path); void setGeneratedPassword(const QString& password); void togglePasswordGeneratorButton(bool checked); diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp index f6500ca8b..78d8c1802 100644 --- a/tests/TestSharing.cpp +++ b/tests/TestSharing.cpp @@ -41,7 +41,8 @@ QTEST_GUILESS_MAIN(TestSharing) Q_DECLARE_METATYPE(KeeShareSettings::Type) Q_DECLARE_METATYPE(KeeShareSettings::Key) Q_DECLARE_METATYPE(KeeShareSettings::Certificate) -Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(KeeShareSettings::ScopedCertificate) +Q_DECLARE_METATYPE(QList) void TestSharing::initTestCase() { @@ -127,9 +128,9 @@ void TestSharing::testNullObjects() QVERIFY(xmlActive.isNull()); const KeeShareSettings::Foreign foreign; - QVERIFY(foreign.isNull()); + QVERIFY(foreign.certificates.isEmpty()); const KeeShareSettings::Foreign xmlForeign = KeeShareSettings::Foreign::deserialize(empty); - QVERIFY(xmlForeign.isNull()); + QVERIFY(xmlForeign.certificates.isEmpty()); const KeeShareSettings::Reference reference; QVERIFY(reference.isNull()); @@ -141,28 +142,33 @@ void TestSharing::testCertificateSerialization() { QFETCH(bool, trusted); const OpenSSHKey& key = stubkey(); - KeeShareSettings::Certificate original; - original.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key); - original.signer = "Some &#_\"\" weird string"; + KeeShareSettings::ScopedCertificate original; + original.path = "/path"; + original.certificate = KeeShareSettings::Certificate + { + OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key), + "Some &#_\"\" weird string" + }; original.trusted = trusted; QString buffer; QXmlStreamWriter writer(&buffer); writer.writeStartDocument(); writer.writeStartElement("Certificate"); - KeeShareSettings::Certificate::serialize(writer, original); + KeeShareSettings::ScopedCertificate::serialize(writer, original); writer.writeEndElement(); writer.writeEndDocument(); QXmlStreamReader reader(buffer); reader.readNextStartElement(); QVERIFY(reader.name() == "Certificate"); - KeeShareSettings::Certificate restored = KeeShareSettings::Certificate::deserialize(reader); + KeeShareSettings::ScopedCertificate restored = KeeShareSettings::ScopedCertificate::deserialize(reader); - QCOMPARE(restored.key, original.key); - QCOMPARE(restored.signer, original.signer); + QCOMPARE(restored.certificate.key, original.certificate.key); + QCOMPARE(restored.certificate.signer, original.certificate.signer); QCOMPARE(restored.trusted, original.trusted); + QCOMPARE(restored.path, original.path); - QCOMPARE(restored.sshKey().publicParts(), key.publicParts()); + QCOMPARE(restored.certificate.sshKey().publicParts(), key.publicParts()); } void TestSharing::testCertificateSerialization_data() @@ -234,7 +240,7 @@ void TestSharing::testSettingsSerialization() QFETCH(bool, exporting); QFETCH(KeeShareSettings::Certificate, ownCertificate); QFETCH(KeeShareSettings::Key, ownKey); - QFETCH(QList, foreignCertificates); + QFETCH(QList, foreignCertificates); KeeShareSettings::Own originalOwn; KeeShareSettings::Foreign originalForeign; @@ -257,41 +263,48 @@ void TestSharing::testSettingsSerialization() QCOMPARE(restoredActive.in, importing); QCOMPARE(restoredActive.out, exporting); QCOMPARE(restoredOwn.certificate.key, ownCertificate.key); - QCOMPARE(restoredOwn.certificate.trusted, ownCertificate.trusted); QCOMPARE(restoredOwn.key.key, ownKey.key); QCOMPARE(restoredForeign.certificates.count(), foreignCertificates.count()); for (int i = 0; i < foreignCertificates.count(); ++i) { - QCOMPARE(restoredForeign.certificates[i].key, foreignCertificates[i].key); + QCOMPARE(restoredForeign.certificates[i].certificate.key, foreignCertificates[i].certificate.key); } } void TestSharing::testSettingsSerialization_data() { const OpenSSHKey& sshKey0 = stubkey(0); - KeeShareSettings::Certificate certificate0; - certificate0.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey0); - certificate0.signer = "Some &#_\"\" weird string"; + KeeShareSettings::ScopedCertificate certificate0; + certificate0.path = "/path/0"; + certificate0.certificate = KeeShareSettings::Certificate + { + OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey0), + "Some &#_\"\" weird string" + }; certificate0.trusted = true; KeeShareSettings::Key key0; key0.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Private, sshKey0); const OpenSSHKey& sshKey1 = stubkey(1); - KeeShareSettings::Certificate certificate1; - certificate1.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey1); - certificate1.signer = "Another "; - certificate1.trusted = true; + KeeShareSettings::ScopedCertificate certificate1; + certificate1.path = "/path/1"; + certificate1.certificate = KeeShareSettings::Certificate + { + OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey1), + "Another " + }; + certificate1.trusted = false; QTest::addColumn("importing"); QTest::addColumn("exporting"); QTest::addColumn("ownCertificate"); QTest::addColumn("ownKey"); QTest::addColumn>("foreignCertificates"); - QTest::newRow("1") << false << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); - QTest::newRow("2") << true << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); - QTest::newRow("3") << true << true << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList({ certificate0, certificate1 }); - QTest::newRow("4") << false << true << certificate0 << key0 << QList(); - QTest::newRow("5") << false << false << certificate0 << key0 << QList({ certificate1 }); + QTest::newRow("1") << false << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); + QTest::newRow("2") << true << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); + QTest::newRow("3") << true << true << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList({ certificate0, certificate1 }); + QTest::newRow("4") << false << true << certificate0 << key0 << QList(); + QTest::newRow("5") << false << false << certificate0 << key0 << QList({ certificate1 }); } const OpenSSHKey& TestSharing::stubkey(int index) From 3d02013513429f5fdca35e01f2248911e112df17 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Thu, 3 Jan 2019 18:22:41 +0100 Subject: [PATCH 09/23] Changed KeeShareSettings to explicitly ask KeeShareSettings now allow to trust, untrust or ask on next import from a specific publisher with a specific container --- src/keeshare/KeeShareSettings.cpp | 23 ++++++++++++++++++++--- src/keeshare/KeeShareSettings.h | 7 ++++++- src/keeshare/SettingsWidgetKeeShare.cpp | 24 ++++++++++++++++++------ src/keeshare/SettingsWidgetKeeShare.h | 1 + src/keeshare/SettingsWidgetKeeShare.ui | 9 ++++++++- src/keeshare/ShareObserver.cpp | 22 +++++++++++----------- tests/TestSharing.cpp | 18 ++++++++++-------- 7 files changed, 74 insertions(+), 30 deletions(-) diff --git a/src/keeshare/KeeShareSettings.cpp b/src/keeshare/KeeShareSettings.cpp index 47ad76df6..bfc2f2fd8 100644 --- a/src/keeshare/KeeShareSettings.cpp +++ b/src/keeshare/KeeShareSettings.cpp @@ -297,7 +297,7 @@ namespace KeeShareSettings bool ScopedCertificate::operator==(const ScopedCertificate &other) const { - return trusted == other.trusted && path == other.path && certificate == other.certificate; + return trust == other.trust && path == other.path && certificate == other.certificate; } bool ScopedCertificate::operator!=(const ScopedCertificate &other) const @@ -308,7 +308,15 @@ namespace KeeShareSettings void ScopedCertificate::serialize(QXmlStreamWriter& writer, const ScopedCertificate& scopedCertificate) { writer.writeAttribute("Path", scopedCertificate.path); - writer.writeAttribute("Trusted", scopedCertificate.trusted ? "True" : "False"); + if(scopedCertificate.trust == KeeShareSettings::Trust::Trusted) { + writer.writeAttribute("Trust", "Trusted"); + } + else if(scopedCertificate.trust == KeeShareSettings::Trust::Untrusted){ + writer.writeAttribute("Trust", "Untrusted"); + } + else { + writer.writeAttribute("Trust", "Ask"); + } Certificate::serialize(writer, scopedCertificate.certificate); } @@ -316,7 +324,16 @@ namespace KeeShareSettings { ScopedCertificate scopedCertificate; scopedCertificate.path = reader.attributes().value("Path").toString(); - scopedCertificate.trusted = reader.attributes().value("Trusted") == "True"; + auto trust = reader.attributes().value("Trusted").toString(); + if(trust.compare("Trusted", Qt::CaseInsensitive)) { + scopedCertificate.trust = KeeShareSettings::Trust::Trusted; + } + if(trust.compare("Unrusted", Qt::CaseInsensitive)) { + scopedCertificate.trust = KeeShareSettings::Trust::Untrusted; + } + else { + scopedCertificate.trust = KeeShareSettings::Trust::Ask; + } scopedCertificate.certificate = Certificate::deserialize(reader); return scopedCertificate; } diff --git a/src/keeshare/KeeShareSettings.h b/src/keeshare/KeeShareSettings.h index a6522a5b4..a458fb3db 100644 --- a/src/keeshare/KeeShareSettings.h +++ b/src/keeshare/KeeShareSettings.h @@ -99,11 +99,16 @@ namespace KeeShareSettings static Own generate(); }; + enum class Trust { + Ask, + Untrusted, + Trusted + }; struct ScopedCertificate { QString path; Certificate certificate; - bool trusted; + Trust trust; bool operator==(const ScopedCertificate& other) const; bool operator!=(const ScopedCertificate& other) const; diff --git a/src/keeshare/SettingsWidgetKeeShare.cpp b/src/keeshare/SettingsWidgetKeeShare.cpp index 2b5ba3e3c..be4aee2ef 100644 --- a/src/keeshare/SettingsWidgetKeeShare.cpp +++ b/src/keeshare/SettingsWidgetKeeShare.cpp @@ -46,6 +46,7 @@ SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent) connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate())); connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates())); + connect(m_ui->askImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(askSelectedCertificates())); connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates())); connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates())); } @@ -79,11 +80,11 @@ void SettingsWidgetKeeShare::updateForeignCertificates() for (const auto& scopedCertificate : m_foreign.certificates) { const auto items = QList() << new QStandardItem(scopedCertificate.path) - << new QStandardItem(scopedCertificate.trusted ? tr("Trusted") : tr("Untrusted")) + << new QStandardItem(scopedCertificate.trust == KeeShareSettings::Trust::Ask ? tr("Ask") + : (scopedCertificate.trust == KeeShareSettings::Trust::Trusted ? tr("Trusted") + : tr("Untrusted"))) #if defined(WITH_XC_KEESHARE_SECURE) - << new QStandardItem(scopedCertificate.isKnown() - ? scopedCertificate.certificate.signer - : tr("Unknown")) + << new QStandardItem(scopedCertificate.isKnown() ? scopedCertificate.certificate.signer : tr("Unknown")) << new QStandardItem(scopedCertificate.certificate.fingerprint()) << new QStandardItem(scopedCertificate.certificate.publicKey()) #endif @@ -194,7 +195,18 @@ void SettingsWidgetKeeShare::trustSelectedCertificates() const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); Q_ASSERT(selectionModel); for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trusted = true; + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Trusted; + } + + updateForeignCertificates(); +} + +void SettingsWidgetKeeShare::askSelectedCertificates() +{ + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Ask; } updateForeignCertificates(); @@ -205,7 +217,7 @@ void SettingsWidgetKeeShare::untrustSelectedCertificates() const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); Q_ASSERT(selectionModel); for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trusted = false; + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Untrusted; } updateForeignCertificates(); diff --git a/src/keeshare/SettingsWidgetKeeShare.h b/src/keeshare/SettingsWidgetKeeShare.h index f68b76792..16e089a2e 100644 --- a/src/keeshare/SettingsWidgetKeeShare.h +++ b/src/keeshare/SettingsWidgetKeeShare.h @@ -55,6 +55,7 @@ private slots: void exportCertificate(); void trustSelectedCertificates(); + void askSelectedCertificates(); void untrustSelectedCertificates(); void removeSelectedCertificates(); diff --git a/src/keeshare/SettingsWidgetKeeShare.ui b/src/keeshare/SettingsWidgetKeeShare.ui index c736bdedd..0e7f4c99b 100644 --- a/src/keeshare/SettingsWidgetKeeShare.ui +++ b/src/keeshare/SettingsWidgetKeeShare.ui @@ -7,7 +7,7 @@ 0 0 327 - 423 + 434 @@ -210,6 +210,13 @@ + + + + Ask + + + diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 2f98b1bcd..8b41397e8 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -92,22 +92,22 @@ QPair check(QByteArray& data, } enum Scope { Invalid, Global, Local }; Scope scope = Invalid; - bool trusted = false; + KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; for (const auto& scopedCertificate : knownCertificates) { if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { // Global scope is overwritten by local scope scope = Global; - trusted = scopedCertificate.trusted; + trusted = scopedCertificate.trust; } if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { scope = Local; - trusted = scopedCertificate.trusted; + trusted = scopedCertificate.trust; break; } } - if (scope != Invalid){ + if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask){ // we introduce now scopes if there is a global - return {trusted ? TrustedForever : UntrustedForever, certificate}; + return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; } QMessageBox warning; @@ -352,12 +352,12 @@ ShareObserver::Result ShareObserver::importSecureContainerInto(const KeeShareSet case UntrustedForever: case TrustedForever: { bool found = false; - bool trusted = trust.first == TrustedForever; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { scopedCertificate.certificate.signer = trust.second.signer; scopedCertificate.path = reference.path; - scopedCertificate.trusted = trusted; + scopedCertificate.trust = trusted; found = true; } } @@ -366,7 +366,7 @@ ShareObserver::Result ShareObserver::importSecureContainerInto(const KeeShareSet // we need to update with the new signer KeeShare::setForeign(foreign); } - if (trusted) { + if (trust.first == TrustedForever) { qDebug("Synchronize %s %s with %s", qPrintable(reference.path), qPrintable(targetGroup->name()), @@ -435,12 +435,12 @@ ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareS case UntrustedForever: case TrustedForever: { bool found = false; - bool trusted = trust.first == TrustedForever; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { scopedCertificate.certificate.signer = trust.second.signer; scopedCertificate.path = reference.path; - scopedCertificate.trusted = trusted; + scopedCertificate.trust = trusted; found = true; } } @@ -449,7 +449,7 @@ ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareS // we need to update with the new signer KeeShare::setForeign(foreign); } - if (trusted) { + if (trust.first == TrustedForever) { qDebug("Synchronize %s %s with %s", qPrintable(reference.path), qPrintable(targetGroup->name()), diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp index 78d8c1802..b1e85edb7 100644 --- a/tests/TestSharing.cpp +++ b/tests/TestSharing.cpp @@ -41,6 +41,7 @@ QTEST_GUILESS_MAIN(TestSharing) Q_DECLARE_METATYPE(KeeShareSettings::Type) Q_DECLARE_METATYPE(KeeShareSettings::Key) Q_DECLARE_METATYPE(KeeShareSettings::Certificate) +Q_DECLARE_METATYPE(KeeShareSettings::Trust) Q_DECLARE_METATYPE(KeeShareSettings::ScopedCertificate) Q_DECLARE_METATYPE(QList) @@ -140,7 +141,7 @@ void TestSharing::testNullObjects() void TestSharing::testCertificateSerialization() { - QFETCH(bool, trusted); + QFETCH(KeeShareSettings::Trust, trusted); const OpenSSHKey& key = stubkey(); KeeShareSettings::ScopedCertificate original; original.path = "/path"; @@ -149,7 +150,7 @@ void TestSharing::testCertificateSerialization() OpenSSHKey::serializeToBinary(OpenSSHKey::Public, key), "Some &#_\"\" weird string" }; - original.trusted = trusted; + original.trust = trusted; QString buffer; QXmlStreamWriter writer(&buffer); @@ -165,7 +166,7 @@ void TestSharing::testCertificateSerialization() QCOMPARE(restored.certificate.key, original.certificate.key); QCOMPARE(restored.certificate.signer, original.certificate.signer); - QCOMPARE(restored.trusted, original.trusted); + QCOMPARE(restored.trust, original.trust); QCOMPARE(restored.path, original.path); QCOMPARE(restored.certificate.sshKey().publicParts(), key.publicParts()); @@ -173,9 +174,10 @@ void TestSharing::testCertificateSerialization() void TestSharing::testCertificateSerialization_data() { - QTest::addColumn("trusted"); - QTest::newRow("Trusted") << true; - QTest::newRow("Untrusted") << false; + QTest::addColumn("trusted"); + QTest::newRow("Ask") << KeeShareSettings::Trust::Ask; + QTest::newRow("Trusted") << KeeShareSettings::Trust::Trusted; + QTest::newRow("Untrusted") << KeeShareSettings::Trust::Untrusted; } void TestSharing::testKeySerialization() @@ -280,7 +282,7 @@ void TestSharing::testSettingsSerialization_data() OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey0), "Some &#_\"\" weird string" }; - certificate0.trusted = true; + certificate0.trust = KeeShareSettings::Trust::Trusted; KeeShareSettings::Key key0; key0.key = OpenSSHKey::serializeToBinary(OpenSSHKey::Private, sshKey0); @@ -293,7 +295,7 @@ void TestSharing::testSettingsSerialization_data() OpenSSHKey::serializeToBinary(OpenSSHKey::Public, sshKey1), "Another " }; - certificate1.trusted = false; + certificate1.trust = KeeShareSettings::Trust::Untrusted; QTest::addColumn("importing"); QTest::addColumn("exporting"); From 080b02e9d538658d2ff6254022f2a8161e2b7da8 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Fri, 4 Jan 2019 09:33:00 +0100 Subject: [PATCH 10/23] KeeShare uses only one flag - checks for QuaZip Changed flag for KeeShare to * enable insecure sharing in all cases * enable secure sharing if QuaZip was found --- CMakeLists.txt | 32 +++++++++++++++++--------------- src/CMakeLists.txt | 2 +- src/gui/AboutDialog.cpp | 4 ++-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3370f1873..6e34a82c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,8 +47,7 @@ option(WITH_XC_NETWORKING "Include networking code (e.g. for downlading website option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF) option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) -option(WITH_XC_KEESHARE_INSECURE "Sharing integration with KeeShare with insecure sources" OFF) -option(WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources" OFF) +option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF) if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() @@ -60,20 +59,12 @@ if(WITH_XC_ALL) set(WITH_XC_BROWSER ON) set(WITH_XC_YUBIKEY ON) set(WITH_XC_SSHAGENT ON) - set(WITH_XC_KEESHARE_INSECURE ON) - set(WITH_XC_KEESHARE_SECURE ON) + set(WITH_XC_KEESHARE ON) if(APPLE) set(WITH_XC_TOUCHID ON) endif() endif() -if(WITH_XC_KEESHARE_INSECURE OR WITH_XC_KEESHARE_SECURE) - set(WITH_XC_KEESHARE ON) -else() - set(WITH_XC_KEESHARE OFF) -endif() - - if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE) set(WITH_XC_CRYPTO_SSH ON) else() @@ -367,10 +358,21 @@ endif() include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) # Optional -if(WITH_XC_KEESHARE_SECURE) - find_package(QuaZip REQUIRED) - - include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) +if(WITH_XC_KEESHARE) + find_package(QuaZip QUIET) + if(QuaZip_FOUND) + message(STATUS "Found QuaZip - allow KeeShare with secure container\n") + set(WITH_XC_KEESHARE_INSECURE ON) + set(WITH_XC_KEESHARE_SECURE ON) + include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) + else() + message(STATUS "No QuaZip - restrict KeeShare to insecure container\n") + set(WITH_XC_KEESHARE_INSECURE ON) + set(WITH_XC_KEESHARE_SECURE OFF) + endif() +else() + set(WITH_XC_KEESHARE_INSECURE OFF) + set(WITH_XC_KEESHARE_SECURE OFF) endif() # Optional diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 446b74447..f0313d4e9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,7 +202,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") -add_feature_info(KeeShare-Insecure WITH_XC_KEESHARE_INSECURE "Sharing integration with KeeShare with insecure sources") +add_feature_info(KeeShare-Insecure WITH_XC_KEESHARE_INSECURE "Sharing integration with KeeShare restricted to insecure sources") add_feature_info(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") if(APPLE) diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 9c9bf88e2..cbc0c5cf0 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -91,9 +91,9 @@ AboutDialog::AboutDialog(QWidget* parent) #if defined(WITH_XC_KEESHARE_SECURE) && defined(WITH_XC_KEESHARE_INSECURE) extensions += "\n- " + tr("KeeShare (secure and insecure sharing)"); #elif defined(WITH_XC_KEESHARE_SECURE) - extensions += "\n- " + tr("KeeShare (secure sharing)"); + extensions += "\n- " + tr("KeeShare (only secure sharing)"); #elif defined(WITH_XC_KEESHARE_INSECURE) - extensions += "\n- " + tr("KeeShare (insecure sharing)"); + extensions += "\n- " + tr("KeeShare (only insecure sharing)"); #endif #ifdef WITH_XC_YUBIKEY extensions += "\n- " + tr("YubiKey"); From ba604390d2b4a8f8e6a88cc1175e487e540fa242 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Fri, 4 Jan 2019 11:03:15 +0100 Subject: [PATCH 11/23] Fixed ci builds ubuntu:14.04 does not have quazip5 libraries in the standard repository, therefore only insecure builds are created using the ci server --- CMakeLists.txt | 3 +- Dockerfile | 3 +- ci/trusty/Dockerfile | 3 +- cmake/FindQuaZip.cmake | 62 ++++++++++++++++++++++++++++-------------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e34a82c1..c1471d577 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -359,8 +359,9 @@ include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) # Optional if(WITH_XC_KEESHARE) + # ZLIB is needed and already required find_package(QuaZip QUIET) - if(QuaZip_FOUND) + if(QUAZIP_FOUND) message(STATUS "Found QuaZip - allow KeeShare with secure container\n") set(WITH_XC_KEESHARE_INSECURE ON) set(WITH_XC_KEESHARE_SECURE ON) diff --git a/Dockerfile b/Dockerfile index 79cd3067f..33bdea6a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,7 +54,8 @@ RUN set -x \ zlib1g-dev \ libxi-dev \ libxtst-dev \ - libquazip-dev \ + # ubuntu:14.04 has no quazip (it's optional) + # libquazip5-dev \ mesa-common-dev \ libyubikey-dev \ libykpers-1-dev \ diff --git a/ci/trusty/Dockerfile b/ci/trusty/Dockerfile index 53daf25e3..79e054efe 100644 --- a/ci/trusty/Dockerfile +++ b/ci/trusty/Dockerfile @@ -54,7 +54,8 @@ RUN set -x \ zlib1g-dev \ libyubikey-dev \ libykpers-1-dev \ - libquazip-dev \ + # ubuntu:14.04 has no quazip (it's optional) + # libquazip5-dev \ libxi-dev \ libxtst-dev \ libqrencode-dev \ diff --git a/cmake/FindQuaZip.cmake b/cmake/FindQuaZip.cmake index 58244f4df..23f4918db 100644 --- a/cmake/FindQuaZip.cmake +++ b/cmake/FindQuaZip.cmake @@ -1,23 +1,43 @@ -# Copyright (C) 2018 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 . +# QUAZIP_FOUND - QuaZip library was found +# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir +# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR) +# QUAZIP_LIBRARIES - List of QuaZip libraries +# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers -find_path(QUAZIP_INCLUDE_DIR quazip5/quazip.h) -find_library(QUAZIP_LIBRARIES quazip5) -mark_as_advanced(QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR) - -include(FindPackageHandleStandardArgs) -include_directories(${QUAZIP_INCLUDE_DIR}) -find_package_handle_standard_args(QuaZip DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR) +IF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) + # in cache already + SET(QUAZIP_FOUND TRUE) +ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) + IF (Qt5Core_FOUND) + set(QUAZIP_LIB_VERSION_SUFFIX 5) + ENDIF() + IF (WIN32) + FIND_PATH(QUAZIP_LIBRARY_DIR + WIN32_DEBUG_POSTFIX d + NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll + HINTS "C:/Programme/" "C:/Program Files" + PATH_SUFFIXES QuaZip/lib + ) + FIND_LIBRARY(QUAZIP_LIBRARIES NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll HINTS ${QUAZIP_LIBRARY_DIR}) + FIND_PATH(QUAZIP_INCLUDE_DIR NAMES quazip.h HINTS ${QUAZIP_LIBRARY_DIR}/../ PATH_SUFFIXES include/quazip) + FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR NAMES zlib.h) + ELSE(WIN32) + FIND_PACKAGE(PkgConfig) +# pkg_check_modules(PC_QCA2 QUIET qca2) + pkg_check_modules(PC_QUAZIP quazip) + FIND_LIBRARY(QUAZIP_LIBRARIES + WIN32_DEBUG_POSTFIX d + NAMES quazip${QUAZIP_LIB_VERSION_SUFFIX} + HINTS /usr/lib /usr/lib64 + ) + FIND_PATH(QUAZIP_INCLUDE_DIR quazip.h + HINTS /usr/include /usr/local/include + PATH_SUFFIXES quazip${QUAZIP_LIB_VERSION_SUFFIX} + ) + FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR zlib.h HINTS /usr/include /usr/local/include) + ENDIF (WIN32) + INCLUDE(FindPackageHandleStandardArgs) + SET(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR}) + find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS) +ENDIF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) From 6e25da6a193339e5a75c3c1ae118c2bcddfba4a1 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Fri, 4 Jan 2019 12:04:46 +0100 Subject: [PATCH 12/23] Fixed issues detected by test suite and ci Fixed serialization for KeeShareSettings::ScopedCertificate Fixed tests for KeeShareSettings serialization Fixed tests Cli features - tests translation for recycle bin since the tests are executed with the system locale Fixed initialization issue in ShareObserver --- src/keeshare/KeeShareSettings.cpp | 21 +++++++++------------ src/keeshare/ShareObserver.cpp | 6 +++--- tests/TestCli.cpp | 10 +++++----- tests/TestSharing.cpp | 6 +++--- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/keeshare/KeeShareSettings.cpp b/src/keeshare/KeeShareSettings.cpp index bfc2f2fd8..ff34ed076 100644 --- a/src/keeshare/KeeShareSettings.cpp +++ b/src/keeshare/KeeShareSettings.cpp @@ -308,15 +308,14 @@ namespace KeeShareSettings void ScopedCertificate::serialize(QXmlStreamWriter& writer, const ScopedCertificate& scopedCertificate) { writer.writeAttribute("Path", scopedCertificate.path); + QString trust = "Ask"; if(scopedCertificate.trust == KeeShareSettings::Trust::Trusted) { - writer.writeAttribute("Trust", "Trusted"); + trust = "Trusted"; } - else if(scopedCertificate.trust == KeeShareSettings::Trust::Untrusted){ - writer.writeAttribute("Trust", "Untrusted"); - } - else { - writer.writeAttribute("Trust", "Ask"); + if(scopedCertificate.trust == KeeShareSettings::Trust::Untrusted){ + trust = "Untrusted"; } + writer.writeAttribute("Trust", trust); Certificate::serialize(writer, scopedCertificate.certificate); } @@ -324,16 +323,14 @@ namespace KeeShareSettings { ScopedCertificate scopedCertificate; scopedCertificate.path = reader.attributes().value("Path").toString(); - auto trust = reader.attributes().value("Trusted").toString(); - if(trust.compare("Trusted", Qt::CaseInsensitive)) { + scopedCertificate.trust = KeeShareSettings::Trust::Ask; + auto trust = reader.attributes().value("Trust").toString(); + if(trust.compare("Trusted", Qt::CaseInsensitive) == 0) { scopedCertificate.trust = KeeShareSettings::Trust::Trusted; } - if(trust.compare("Unrusted", Qt::CaseInsensitive)) { + if(trust.compare("Untrusted", Qt::CaseInsensitive) == 0) { scopedCertificate.trust = KeeShareSettings::Trust::Untrusted; } - else { - scopedCertificate.trust = KeeShareSettings::Trust::Ask; - } scopedCertificate.certificate = Certificate::deserialize(reader); return scopedCertificate; } diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 8b41397e8..edebd3062 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -80,7 +80,7 @@ QPair check(QByteArray& data, certificate = sign.certificate; auto key = sign.certificate.sshKey(); key.openKey(QString()); - const Signature signer; + const auto signer = Signature{}; if (!signer.verify(data, sign.signature, key)) { qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); return {Invalid, KeeShareSettings::Certificate()}; @@ -212,7 +212,7 @@ void ShareObserver::reinitialize() QStringList success; QStringList warning; QStringList error; - for (Update update : updated) { + for (const auto& update : updated) { if (!update.oldReference.path.isEmpty()) { m_fileWatcher->removePath(update.oldReference.path); } @@ -222,7 +222,7 @@ void ShareObserver::reinitialize() } if (update.newReference.isImporting()) { - const Result result = this->importFromReferenceContainer(update.newReference.path); + const auto result = this->importFromReferenceContainer(update.newReference.path); if (!result.isValid()) { // tolerable result - blocked import or missing source continue; diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 6a4ed3398..ef633d262 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -919,7 +919,7 @@ void TestCli::testRemove() readBack.close(); QVERIFY(readBackDb); QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); - QVERIFY(readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin")))); pos = m_stdoutFile->pos(); @@ -937,7 +937,7 @@ void TestCli::testRemove() readBack.close(); QVERIFY(readBackDb); QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); - QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin")))); pos = m_stdoutFile->pos(); @@ -977,13 +977,13 @@ void TestCli::testRemoveQuiet() readBack.close(); QVERIFY(readBackDb); QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); - QVERIFY(readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + QVERIFY(readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin")))); pos = m_stdoutFile->pos(); // remove the entry completely Utils::Test::setNextPassword("a"); - removeCmd.execute({"rm", "-q", m_dbFile->fileName(), "/Recycle Bin/Sample Entry"}); + removeCmd.execute({"rm", "-q", m_dbFile->fileName(), QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin"))}); m_stdoutFile->seek(pos); QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); @@ -994,7 +994,7 @@ void TestCli::testRemoveQuiet() readBack.close(); QVERIFY(readBackDb); QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Sample Entry")); - QVERIFY(!readBackDb->rootGroup()->findEntryByPath("/Recycle Bin/Sample Entry")); + QVERIFY(!readBackDb->rootGroup()->findEntryByPath(QString("/%1/Sample Entry").arg(Group::tr("Recycle Bin")))); } void TestCli::testShow() diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp index b1e85edb7..d49e585b1 100644 --- a/tests/TestSharing.cpp +++ b/tests/TestSharing.cpp @@ -301,12 +301,12 @@ void TestSharing::testSettingsSerialization_data() QTest::addColumn("exporting"); QTest::addColumn("ownCertificate"); QTest::addColumn("ownKey"); - QTest::addColumn>("foreignCertificates"); + QTest::addColumn>("foreignCertificates"); QTest::newRow("1") << false << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); QTest::newRow("2") << true << false << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList(); QTest::newRow("3") << true << true << KeeShareSettings::Certificate() << KeeShareSettings::Key() << QList({ certificate0, certificate1 }); - QTest::newRow("4") << false << true << certificate0 << key0 << QList(); - QTest::newRow("5") << false << false << certificate0 << key0 << QList({ certificate1 }); + QTest::newRow("4") << false << true << certificate0.certificate << key0 << QList(); + QTest::newRow("5") << false << false << certificate0.certificate << key0 << QList({ certificate1 }); } const OpenSSHKey& TestSharing::stubkey(int index) From b204451d0644b665ccb1d0af9752b67a79d4ae2d Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Fri, 4 Jan 2019 12:11:20 +0100 Subject: [PATCH 13/23] More ci issues (missing initialize for const) Fixed issues detected in TestSharing --- tests/TestSharing.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp index d49e585b1..ecbb30311 100644 --- a/tests/TestSharing.cpp +++ b/tests/TestSharing.cpp @@ -108,32 +108,32 @@ void TestSharing::testNullObjects() const QString empty; QXmlStreamReader reader(empty); - const KeeShareSettings::Key nullKey; + const KeeShareSettings::Key nullKey = {}; QVERIFY(nullKey.isNull()); const KeeShareSettings::Key xmlKey = KeeShareSettings::Key::deserialize(reader); QVERIFY(xmlKey.isNull()); - const KeeShareSettings::Certificate certificate; + const KeeShareSettings::Certificate certificate = {}; QVERIFY(certificate.isNull()); const KeeShareSettings::Certificate xmlCertificate = KeeShareSettings::Certificate::deserialize(reader); QVERIFY(xmlCertificate.isNull()); - const KeeShareSettings::Own own; + const KeeShareSettings::Own own = {}; QVERIFY(own.isNull()); const KeeShareSettings::Own xmlOwn = KeeShareSettings::Own::deserialize(empty); QVERIFY(xmlOwn.isNull()); - const KeeShareSettings::Active active; + const KeeShareSettings::Active active = {}; QVERIFY(active.isNull()); const KeeShareSettings::Active xmlActive = KeeShareSettings::Active::deserialize(empty); QVERIFY(xmlActive.isNull()); - const KeeShareSettings::Foreign foreign; + const KeeShareSettings::Foreign foreign = {}; QVERIFY(foreign.certificates.isEmpty()); const KeeShareSettings::Foreign xmlForeign = KeeShareSettings::Foreign::deserialize(empty); QVERIFY(xmlForeign.certificates.isEmpty()); - const KeeShareSettings::Reference reference; + const KeeShareSettings::Reference reference = {}; QVERIFY(reference.isNull()); const KeeShareSettings::Reference xmlReference = KeeShareSettings::Reference::deserialize(empty); QVERIFY(xmlReference.isNull()); From 51d63f0e8377026210b9085d176d7dd5d2193381 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Fri, 4 Jan 2019 12:28:36 +0100 Subject: [PATCH 14/23] Fixed GCC issues for const initialization Fixed issues reported by GCC for initialization of const variables --- src/keeshare/ShareObserver.cpp | 6 +++--- tests/TestSharing.cpp | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index edebd3062..e30fd0966 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -80,7 +80,7 @@ QPair check(QByteArray& data, certificate = sign.certificate; auto key = sign.certificate.sshKey(); key.openKey(QString()); - const auto signer = Signature{}; + const auto signer = Signature{}; if (!signer.verify(data, sign.signature, key)) { qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); return {Invalid, KeeShareSettings::Certificate()}; @@ -428,8 +428,8 @@ ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareS } auto foreign = KeeShare::foreign(); - auto own = KeeShare::own(); - static KeeShareSettings::Sign sign; // invalid sign + const auto own = KeeShare::own(); + const auto sign = KeeShareSettings::Sign{}; // invalid sign auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); switch(trust.first) { case UntrustedForever: diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp index ecbb30311..78f37f15f 100644 --- a/tests/TestSharing.cpp +++ b/tests/TestSharing.cpp @@ -108,34 +108,34 @@ void TestSharing::testNullObjects() const QString empty; QXmlStreamReader reader(empty); - const KeeShareSettings::Key nullKey = {}; + const auto nullKey = KeeShareSettings::Key{}; QVERIFY(nullKey.isNull()); - const KeeShareSettings::Key xmlKey = KeeShareSettings::Key::deserialize(reader); + const auto xmlKey = KeeShareSettings::Key::deserialize(reader); QVERIFY(xmlKey.isNull()); - const KeeShareSettings::Certificate certificate = {}; + const auto certificate = KeeShareSettings::Certificate{}; QVERIFY(certificate.isNull()); - const KeeShareSettings::Certificate xmlCertificate = KeeShareSettings::Certificate::deserialize(reader); + const auto xmlCertificate = KeeShareSettings::Certificate::deserialize(reader); QVERIFY(xmlCertificate.isNull()); - const KeeShareSettings::Own own = {}; + const auto own = KeeShareSettings::Own{}; QVERIFY(own.isNull()); - const KeeShareSettings::Own xmlOwn = KeeShareSettings::Own::deserialize(empty); + const auto xmlOwn = KeeShareSettings::Own::deserialize(empty); QVERIFY(xmlOwn.isNull()); - const KeeShareSettings::Active active = {}; + const auto active = KeeShareSettings::Active{}; QVERIFY(active.isNull()); - const KeeShareSettings::Active xmlActive = KeeShareSettings::Active::deserialize(empty); + const auto xmlActive = KeeShareSettings::Active::deserialize(empty); QVERIFY(xmlActive.isNull()); - const KeeShareSettings::Foreign foreign = {}; + const auto foreign = KeeShareSettings::Foreign{}; QVERIFY(foreign.certificates.isEmpty()); - const KeeShareSettings::Foreign xmlForeign = KeeShareSettings::Foreign::deserialize(empty); + const auto xmlForeign = KeeShareSettings::Foreign::deserialize(empty); QVERIFY(xmlForeign.certificates.isEmpty()); - const KeeShareSettings::Reference reference = {}; + const auto reference = KeeShareSettings::Reference{}; QVERIFY(reference.isNull()); - const KeeShareSettings::Reference xmlReference = KeeShareSettings::Reference::deserialize(empty); + const auto xmlReference = KeeShareSettings::Reference::deserialize(empty); QVERIFY(xmlReference.isNull()); } From 2bfe66e840d225ba295bf989defc574b269a7b10 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Fri, 4 Jan 2019 12:38:34 +0100 Subject: [PATCH 15/23] Another attempt to make GCC happy --- src/keeshare/ShareObserver.cpp | 4 ++-- tests/TestSharing.cpp | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index e30fd0966..45f3049c4 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -80,7 +80,7 @@ QPair check(QByteArray& data, certificate = sign.certificate; auto key = sign.certificate.sshKey(); key.openKey(QString()); - const auto signer = Signature{}; + const auto signer = Signature(); if (!signer.verify(data, sign.signature, key)) { qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); return {Invalid, KeeShareSettings::Certificate()}; @@ -429,7 +429,7 @@ ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareS auto foreign = KeeShare::foreign(); const auto own = KeeShare::own(); - const auto sign = KeeShareSettings::Sign{}; // invalid sign + const auto sign = KeeShareSettings::Sign(); // invalid sign auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); switch(trust.first) { case UntrustedForever: diff --git a/tests/TestSharing.cpp b/tests/TestSharing.cpp index 78f37f15f..beff9f15e 100644 --- a/tests/TestSharing.cpp +++ b/tests/TestSharing.cpp @@ -108,32 +108,32 @@ void TestSharing::testNullObjects() const QString empty; QXmlStreamReader reader(empty); - const auto nullKey = KeeShareSettings::Key{}; + const auto nullKey = KeeShareSettings::Key(); QVERIFY(nullKey.isNull()); const auto xmlKey = KeeShareSettings::Key::deserialize(reader); QVERIFY(xmlKey.isNull()); - const auto certificate = KeeShareSettings::Certificate{}; + const auto certificate = KeeShareSettings::Certificate(); QVERIFY(certificate.isNull()); const auto xmlCertificate = KeeShareSettings::Certificate::deserialize(reader); QVERIFY(xmlCertificate.isNull()); - const auto own = KeeShareSettings::Own{}; + const auto own = KeeShareSettings::Own(); QVERIFY(own.isNull()); const auto xmlOwn = KeeShareSettings::Own::deserialize(empty); QVERIFY(xmlOwn.isNull()); - const auto active = KeeShareSettings::Active{}; + const auto active = KeeShareSettings::Active(); QVERIFY(active.isNull()); const auto xmlActive = KeeShareSettings::Active::deserialize(empty); QVERIFY(xmlActive.isNull()); - const auto foreign = KeeShareSettings::Foreign{}; + const auto foreign = KeeShareSettings::Foreign(); QVERIFY(foreign.certificates.isEmpty()); const auto xmlForeign = KeeShareSettings::Foreign::deserialize(empty); QVERIFY(xmlForeign.certificates.isEmpty()); - const auto reference = KeeShareSettings::Reference{}; + const auto reference = KeeShareSettings::Reference(); QVERIFY(reference.isNull()); const auto xmlReference = KeeShareSettings::Reference::deserialize(empty); QVERIFY(xmlReference.isNull()); From f46062cc4a6a8f7c6c5c4d7a50702555dd245d54 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 6 Jan 2019 11:06:29 -0500 Subject: [PATCH 16/23] Minor bug fixes and formatting --- src/keeshare/group/EditGroupWidgetKeeShare.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 76ae6b122..492e05c76 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -94,6 +94,7 @@ void EditGroupWidgetKeeShare::setGroup(Group* temporaryGroup) if (m_temporaryGroup) { connect(m_temporaryGroup, SIGNAL(groupModified()), SLOT(update())); } + update(); } @@ -120,7 +121,10 @@ void EditGroupWidgetKeeShare::showSharingState() } } if (!supported) { - m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing your container type. Please use %1.").arg(supportedExtensions.join(", ")), MessageWidget::Warning); + m_ui->messageWidget->showMessage( + tr("Your KeePassXC version does not support sharing your container type. Please use %1.") + .arg(supportedExtensions.join(", ")), + MessageWidget::Warning); return; } } @@ -144,8 +148,6 @@ void EditGroupWidgetKeeShare::update() if (!m_temporaryGroup) { m_ui->passwordEdit->clear(); m_ui->pathEdit->clear(); - m_ui->passwordGenerator->hide(); - m_ui->togglePasswordGeneratorButton->setChecked(false); } else { const auto reference = KeeShare::referenceOf(m_temporaryGroup); @@ -155,6 +157,10 @@ void EditGroupWidgetKeeShare::update() showSharingState(); } + + m_ui->passwordGenerator->hide(); + m_ui->togglePasswordGeneratorButton->setChecked(false); + m_ui->togglePasswordButton->setChecked(false); } void EditGroupWidgetKeeShare::togglePasswordGeneratorButton(bool checked) @@ -251,6 +257,7 @@ void EditGroupWidgetKeeShare::launchPathSelectionDialog() } m_ui->pathEdit->setText(filename); + selectPath(); config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); } From 383b8b77eb7915dfac50577bd2857b6860776692 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Mon, 7 Jan 2019 10:38:38 +0100 Subject: [PATCH 17/23] Implemented feedback regarding build and ui Changed build options to use only WITH_XC_KEESHARE and WITH_XC_KEESHARE_SECURE - WITH_XC_KEESHARE_INSECURE remains as internal variable to highlight differences (may allow to build schemes later) Message widget in KeeShare settings for groups is not closeable anymore --- CMakeLists.txt | 12 +++++++----- src/CMakeLists.txt | 2 +- src/keeshare/group/EditGroupWidgetKeeShare.cpp | 2 ++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c1471d577..39cb4b904 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF) +option(WITH_XC_KEESHARE_SECURE "Sharing integration with secured KeeShare containers" OFF) if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() @@ -65,6 +66,10 @@ if(WITH_XC_ALL) endif() endif() +if(WITH_XC_KEESHARE_SECURE) + set(WITH_XC_KEESHARE ON) +endif() + if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE) set(WITH_XC_CRYPTO_SSH ON) else() @@ -360,16 +365,13 @@ include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) # Optional if(WITH_XC_KEESHARE) # ZLIB is needed and already required - find_package(QuaZip QUIET) - if(QUAZIP_FOUND) - message(STATUS "Found QuaZip - allow KeeShare with secure container\n") + if(WITH_XC_KEESHARE_SECURE) + find_package(QuaZip REQUIRED) set(WITH_XC_KEESHARE_INSECURE ON) set(WITH_XC_KEESHARE_SECURE ON) include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) else() - message(STATUS "No QuaZip - restrict KeeShare to insecure container\n") set(WITH_XC_KEESHARE_INSECURE ON) - set(WITH_XC_KEESHARE_SECURE OFF) endif() else() set(WITH_XC_KEESHARE_INSECURE OFF) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f0313d4e9..355748a94 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,7 +202,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") -add_feature_info(KeeShare-Insecure WITH_XC_KEESHARE_INSECURE "Sharing integration with KeeShare restricted to insecure sources") +add_feature_info(KeeSharee WITH_XC_KEESHARE "Sharing integration with KeeShare") add_feature_info(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") if(APPLE) diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 492e05c76..63f4182af 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -44,6 +44,8 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) m_ui->passwordGenerator->reset(); m_ui->messageWidget->hide(); + m_ui->messageWidget->setCloseButtonVisible(false); + m_ui->messageWidget->setAutoHideTimeout(-1); connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); From a978880b0b4ae4de8a2a45fdf753cb7c2041f618 Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Mon, 7 Jan 2019 12:02:21 +0100 Subject: [PATCH 18/23] Fixed QuaZip for windows, renaming QuaZip should now usable under windows (fixed include in FindQuaZip) Renamed the representation from secure and unsecure to signed and unsigned --- cmake/FindQuaZip.cmake | 56 +- src/gui/AboutDialog.cpp | 102 +- src/gui/MainWindow.cpp | 1429 +++++++++-------- src/keeshare/KeeShare.cpp | 8 +- src/keeshare/KeeShare.h | 4 +- src/keeshare/SettingsWidgetKeeShare.cpp | 266 +-- src/keeshare/ShareObserver.cpp | 1160 ++++++------- src/keeshare/ShareObserver.h | 8 +- .../group/EditGroupWidgetKeeShare.cpp | 366 ++--- 9 files changed, 1700 insertions(+), 1699 deletions(-) diff --git a/cmake/FindQuaZip.cmake b/cmake/FindQuaZip.cmake index 23f4918db..f70973011 100644 --- a/cmake/FindQuaZip.cmake +++ b/cmake/FindQuaZip.cmake @@ -6,38 +6,38 @@ IF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) - # in cache already - SET(QUAZIP_FOUND TRUE) + # in cache already + SET(QUAZIP_FOUND TRUE) ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) IF (Qt5Core_FOUND) set(QUAZIP_LIB_VERSION_SUFFIX 5) ENDIF() - IF (WIN32) - FIND_PATH(QUAZIP_LIBRARY_DIR - WIN32_DEBUG_POSTFIX d - NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll - HINTS "C:/Programme/" "C:/Program Files" - PATH_SUFFIXES QuaZip/lib - ) - FIND_LIBRARY(QUAZIP_LIBRARIES NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll HINTS ${QUAZIP_LIBRARY_DIR}) - FIND_PATH(QUAZIP_INCLUDE_DIR NAMES quazip.h HINTS ${QUAZIP_LIBRARY_DIR}/../ PATH_SUFFIXES include/quazip) - FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR NAMES zlib.h) - ELSE(WIN32) - FIND_PACKAGE(PkgConfig) + IF (WIN32) + FIND_PATH(QUAZIP_LIBRARY_DIR + WIN32_DEBUG_POSTFIX d + NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll + HINTS "C:/Programme/" "C:/Program Files" + PATH_SUFFIXES QuaZip/lib + ) + FIND_LIBRARY(QUAZIP_LIBRARIES NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll HINTS ${QUAZIP_LIBRARY_DIR}) + FIND_PATH(QUAZIP_INCLUDE_DIR NAMES quazip.h HINTS ${QUAZIP_LIBRARY_DIR}/../ PATH_SUFFIXES include/quazip5) + FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR NAMES zlib.h) + ELSE(WIN32) + FIND_PACKAGE(PkgConfig) # pkg_check_modules(PC_QCA2 QUIET qca2) - pkg_check_modules(PC_QUAZIP quazip) - FIND_LIBRARY(QUAZIP_LIBRARIES - WIN32_DEBUG_POSTFIX d + pkg_check_modules(PC_QUAZIP quazip) + FIND_LIBRARY(QUAZIP_LIBRARIES + WIN32_DEBUG_POSTFIX d NAMES quazip${QUAZIP_LIB_VERSION_SUFFIX} - HINTS /usr/lib /usr/lib64 - ) - FIND_PATH(QUAZIP_INCLUDE_DIR quazip.h - HINTS /usr/include /usr/local/include - PATH_SUFFIXES quazip${QUAZIP_LIB_VERSION_SUFFIX} - ) - FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR zlib.h HINTS /usr/include /usr/local/include) - ENDIF (WIN32) - INCLUDE(FindPackageHandleStandardArgs) - SET(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR}) - find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS) + HINTS /usr/lib /usr/lib64 + ) + FIND_PATH(QUAZIP_INCLUDE_DIR quazip.h + HINTS /usr/include /usr/local/include + PATH_SUFFIXES quazip${QUAZIP_LIB_VERSION_SUFFIX} + ) + FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR zlib.h HINTS /usr/include /usr/local/include) + ENDIF (WIN32) + INCLUDE(FindPackageHandleStandardArgs) + SET(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR}) + find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS) ENDIF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index cbc0c5cf0..8aac63440 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -28,90 +28,90 @@ #include AboutDialog::AboutDialog(QWidget* parent) - : QDialog(parent) - , m_ui(new Ui::AboutDialog()) + : QDialog(parent) + , m_ui(new Ui::AboutDialog()) { - m_ui->setupUi(this); + m_ui->setupUi(this); - resize(minimumSize()); - setWindowFlags(Qt::Sheet); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + resize(minimumSize()); + setWindowFlags(Qt::Sheet); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - m_ui->nameLabel->setText(m_ui->nameLabel->text().replace("${VERSION}", KEEPASSXC_VERSION)); - QFont nameLabelFont = m_ui->nameLabel->font(); - nameLabelFont.setPointSize(nameLabelFont.pointSize() + 4); - m_ui->nameLabel->setFont(nameLabelFont); + m_ui->nameLabel->setText(m_ui->nameLabel->text().replace("${VERSION}", KEEPASSXC_VERSION)); + QFont nameLabelFont = m_ui->nameLabel->font(); + nameLabelFont.setPointSize(nameLabelFont.pointSize() + 4); + m_ui->nameLabel->setFont(nameLabelFont); - m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(48)); + m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(48)); - QString commitHash; - if (!QString(GIT_HEAD).isEmpty()) { - commitHash = GIT_HEAD; - } else if (!QString(DIST_HASH).contains("Format")) { - commitHash = DIST_HASH; - } + QString commitHash; + if (!QString(GIT_HEAD).isEmpty()) { + commitHash = GIT_HEAD; + } else if (!QString(DIST_HASH).contains("Format")) { + commitHash = DIST_HASH; + } - QString debugInfo = "KeePassXC - "; - debugInfo.append(tr("Version %1").arg(KEEPASSXC_VERSION).append("\n")); + QString debugInfo = "KeePassXC - "; + debugInfo.append(tr("Version %1").arg(KEEPASSXC_VERSION).append("\n")); #ifndef KEEPASSXC_BUILD_TYPE_RELEASE - debugInfo.append(tr("Build Type: %1").arg(KEEPASSXC_BUILD_TYPE).append("\n")); + debugInfo.append(tr("Build Type: %1").arg(KEEPASSXC_BUILD_TYPE).append("\n")); #endif - if (!commitHash.isEmpty()) { - debugInfo.append(tr("Revision: %1").arg(commitHash.left(7)).append("\n")); - } + if (!commitHash.isEmpty()) { + debugInfo.append(tr("Revision: %1").arg(commitHash.left(7)).append("\n")); + } #ifdef KEEPASSXC_DIST - debugInfo.append(tr("Distribution: %1").arg(KEEPASSXC_DIST_TYPE).append("\n")); + debugInfo.append(tr("Distribution: %1").arg(KEEPASSXC_DIST_TYPE).append("\n")); #endif - debugInfo.append("\n").append( - QString("%1\n- Qt %2\n- %3\n\n") - .arg(tr("Libraries:"), QString::fromLocal8Bit(qVersion()), Crypto::backendVersion())); + debugInfo.append("\n").append( + QString("%1\n- Qt %2\n- %3\n\n") + .arg(tr("Libraries:"), QString::fromLocal8Bit(qVersion()), Crypto::backendVersion())); #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) - debugInfo.append(tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4") - .arg(QSysInfo::prettyProductName(), - QSysInfo::currentCpuArchitecture(), - QSysInfo::kernelType(), - QSysInfo::kernelVersion())); + debugInfo.append(tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4") + .arg(QSysInfo::prettyProductName(), + QSysInfo::currentCpuArchitecture(), + QSysInfo::kernelType(), + QSysInfo::kernelVersion())); - debugInfo.append("\n\n"); + debugInfo.append("\n\n"); #endif - QString extensions; + QString extensions; #ifdef WITH_XC_AUTOTYPE - extensions += "\n- " + tr("Auto-Type"); + extensions += "\n- " + tr("Auto-Type"); #endif #ifdef WITH_XC_BROWSER - extensions += "\n- " + tr("Browser Integration"); + extensions += "\n- " + tr("Browser Integration"); #endif #ifdef WITH_XC_SSHAGENT - extensions += "\n- " + tr("SSH Agent"); + extensions += "\n- " + tr("SSH Agent"); #endif #if defined(WITH_XC_KEESHARE_SECURE) && defined(WITH_XC_KEESHARE_INSECURE) - extensions += "\n- " + tr("KeeShare (secure and insecure sharing)"); + extensions += "\n- " + tr("KeeShare (signed and unsigned sharing)"); #elif defined(WITH_XC_KEESHARE_SECURE) - extensions += "\n- " + tr("KeeShare (only secure sharing)"); + extensions += "\n- " + tr("KeeShare (only signed sharing)"); #elif defined(WITH_XC_KEESHARE_INSECURE) - extensions += "\n- " + tr("KeeShare (only insecure sharing)"); + extensions += "\n- " + tr("KeeShare (only unsigned sharing)"); #endif #ifdef WITH_XC_YUBIKEY - extensions += "\n- " + tr("YubiKey"); + extensions += "\n- " + tr("YubiKey"); #endif #ifdef WITH_XC_TOUCHID - extensions += "\n- " + tr("TouchID"); + extensions += "\n- " + tr("TouchID"); #endif - if (extensions.isEmpty()) - extensions = " " + tr("None"); + if (extensions.isEmpty()) + extensions = " " + tr("None"); - debugInfo.append(tr("Enabled extensions:").append(extensions)); + debugInfo.append(tr("Enabled extensions:").append(extensions)); - m_ui->debugInfo->setPlainText(debugInfo); + m_ui->debugInfo->setPlainText(debugInfo); - setAttribute(Qt::WA_DeleteOnClose); - connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); - connect(m_ui->copyToClipboard, SIGNAL(clicked()), SLOT(copyToClipboard())); + setAttribute(Qt::WA_DeleteOnClose); + connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); + connect(m_ui->copyToClipboard, SIGNAL(clicked()), SLOT(copyToClipboard())); } AboutDialog::~AboutDialog() @@ -120,6 +120,6 @@ AboutDialog::~AboutDialog() void AboutDialog::copyToClipboard() { - QClipboard* clipboard = QApplication::clipboard(); - clipboard->setText(m_ui->debugInfo->toPlainText()); + QClipboard* clipboard = QApplication::clipboard(); + clipboard->setText(m_ui->debugInfo->toPlainText()); } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index fd9610e55..f3d1d4813 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -20,6 +20,7 @@ #include "ui_MainWindow.h" #include +#include #include #include #include @@ -68,49 +69,49 @@ class BrowserPlugin : public ISettingsPage { public: - BrowserPlugin(DatabaseTabWidget* tabWidget) - { - m_nativeMessagingHost = - QSharedPointer(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); - } + BrowserPlugin(DatabaseTabWidget* tabWidget) + { + m_nativeMessagingHost = + QSharedPointer(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); + } - ~BrowserPlugin() - { - } + ~BrowserPlugin() + { + } - QString name() override - { - return QObject::tr("Browser Integration"); - } + QString name() override + { + return QObject::tr("Browser Integration"); + } - QIcon icon() override - { - return FilePath::instance()->icon("apps", "internet-web-browser"); - } + QIcon icon() override + { + return FilePath::instance()->icon("apps", "internet-web-browser"); + } - QWidget* createWidget() override - { - BrowserOptionDialog* dlg = new BrowserOptionDialog(); - return dlg; - } + QWidget* createWidget() override + { + BrowserOptionDialog* dlg = new BrowserOptionDialog(); + return dlg; + } - void loadSettings(QWidget* widget) override - { - qobject_cast(widget)->loadSettings(); - } + void loadSettings(QWidget* widget) override + { + qobject_cast(widget)->loadSettings(); + } - void saveSettings(QWidget* widget) override - { - qobject_cast(widget)->saveSettings(); - if (browserSettings()->isEnabled()) { - m_nativeMessagingHost->run(); - } else { - m_nativeMessagingHost->stop(); - } - } + void saveSettings(QWidget* widget) override + { + qobject_cast(widget)->saveSettings(); + if (browserSettings()->isEnabled()) { + m_nativeMessagingHost->run(); + } else { + m_nativeMessagingHost->stop(); + } + } private: - QSharedPointer m_nativeMessagingHost; + QSharedPointer m_nativeMessagingHost; }; #endif @@ -120,279 +121,279 @@ MainWindow* g_MainWindow = nullptr; MainWindow* getMainWindow() { return g_MainWindow; } MainWindow::MainWindow() - : m_ui(new Ui::MainWindow()) - , m_trayIcon(nullptr) - , m_appExitCalled(false) - , m_appExiting(false) + : m_ui(new Ui::MainWindow()) + , m_trayIcon(nullptr) + , m_appExitCalled(false) + , m_appExiting(false) { - g_MainWindow = this; + g_MainWindow = this; - m_ui->setupUi(this); + m_ui->setupUi(this); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) - new MainWindowAdaptor(this); - QDBusConnection dbus = QDBusConnection::sessionBus(); - dbus.registerObject("/keepassxc", this); - dbus.registerService("org.keepassxc.KeePassXC.MainWindow"); + new MainWindowAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject("/keepassxc", this); + dbus.registerService("org.keepassxc.KeePassXC.MainWindow"); #endif - setAcceptDrops(true); + setAcceptDrops(true); - // Setup the search widget in the toolbar - auto* search = new SearchWidget(); - search->connectSignals(m_actionMultiplexer); - m_searchWidgetAction = m_ui->toolBar->addWidget(search); - m_searchWidgetAction->setEnabled(false); + // Setup the search widget in the toolbar + auto* search = new SearchWidget(); + search->connectSignals(m_actionMultiplexer); + m_searchWidgetAction = m_ui->toolBar->addWidget(search); + m_searchWidgetAction->setEnabled(false); - m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); + m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); - restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); + restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); #ifdef WITH_XC_BROWSER - m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); + m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); #endif #ifdef WITH_XC_SSHAGENT - SSHAgent::init(this); - connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); - m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); + SSHAgent::init(this); + connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); + m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif #if defined(WITH_XC_KEESHARE) - KeeShare::init(this); - m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); - connect(KeeShare::instance(), SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), - SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); + KeeShare::init(this); + m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); + connect(KeeShare::instance(), SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), + SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); #endif - setWindowIcon(filePath()->applicationIcon()); - m_ui->globalMessageWidget->setHidden(true); - // clang-format off - connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); - connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); - connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); - // clang-format on + setWindowIcon(filePath()->applicationIcon()); + m_ui->globalMessageWidget->setHidden(true); + // clang-format off + connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); + connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); + connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); + // clang-format on - m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); - m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); - connect(m_clearHistoryAction, SIGNAL(triggered()), this, SLOT(clearLastDatabases())); - connect(m_lastDatabasesActions, SIGNAL(triggered(QAction*)), this, SLOT(openRecentDatabase(QAction*))); - connect(m_ui->menuRecentDatabases, SIGNAL(aboutToShow()), this, SLOT(updateLastDatabasesMenu())); + m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); + m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); + connect(m_clearHistoryAction, SIGNAL(triggered()), this, SLOT(clearLastDatabases())); + connect(m_lastDatabasesActions, SIGNAL(triggered(QAction*)), this, SLOT(openRecentDatabase(QAction*))); + connect(m_ui->menuRecentDatabases, SIGNAL(aboutToShow()), this, SLOT(updateLastDatabasesMenu())); - m_copyAdditionalAttributeActions = new QActionGroup(m_ui->menuEntryCopyAttribute); - m_actionMultiplexer.connect( - m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*))); - connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu())); + m_copyAdditionalAttributeActions = new QActionGroup(m_ui->menuEntryCopyAttribute); + m_actionMultiplexer.connect( + m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*))); + connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu())); - Qt::Key globalAutoTypeKey = static_cast(config()->get("GlobalAutoTypeKey").toInt()); - Qt::KeyboardModifiers globalAutoTypeModifiers = - static_cast(config()->get("GlobalAutoTypeModifiers").toInt()); - if (globalAutoTypeKey > 0 && globalAutoTypeModifiers > 0) { - autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); - } + Qt::Key globalAutoTypeKey = static_cast(config()->get("GlobalAutoTypeKey").toInt()); + Qt::KeyboardModifiers globalAutoTypeModifiers = + static_cast(config()->get("GlobalAutoTypeModifiers").toInt()); + if (globalAutoTypeKey > 0 && globalAutoTypeModifiers > 0) { + autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); + } - m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); + m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); - m_inactivityTimer = new InactivityTimer(this); - connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity())); + m_inactivityTimer = new InactivityTimer(this); + connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity())); #ifdef WITH_XC_TOUCHID - m_touchIDinactivityTimer = new InactivityTimer(this); - connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity())); + m_touchIDinactivityTimer = new InactivityTimer(this); + connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity())); #endif - applySettingsChanges(); + applySettingsChanges(); - m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N); - setShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); - setShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); - setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); - setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); - m_ui->actionLockDatabases->setShortcut(Qt::CTRL + Qt::Key_L); - setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); - setShortcut(m_ui->actionEntryNew, QKeySequence::New, Qt::CTRL + Qt::Key_N); - m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E); - m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D); - m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); - m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); - m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); - m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); - m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); - m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); - m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); - m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); - m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); + m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N); + setShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); + setShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); + setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); + setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); + m_ui->actionLockDatabases->setShortcut(Qt::CTRL + Qt::Key_L); + setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); + setShortcut(m_ui->actionEntryNew, QKeySequence::New, Qt::CTRL + Qt::Key_N); + m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E); + m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D); + m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); + m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); + m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); + m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); + m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); + m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); + m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); + m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); + m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus - // Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them - m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryOpenUrl->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyURL->setShortcutVisibleInContextMenu(true); + // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus + // Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them + m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryOpenUrl->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyURL->setShortcutVisibleInContextMenu(true); #endif - // Control window state - new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(showMinimized())); - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_M, this, SLOT(hideWindow())); - // Control database tabs - new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(selectNextDatabaseTab())); - new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(selectNextDatabaseTab())); - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(selectPreviousDatabaseTab())); - new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(selectPreviousDatabaseTab())); - // Toggle password and username visibility in entry view - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden())); - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_B, this, SLOT(toggleUsernamesHidden())); + // Control window state + new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(showMinimized())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_M, this, SLOT(hideWindow())); + // Control database tabs + new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(selectNextDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(selectNextDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(selectPreviousDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(selectPreviousDatabaseTab())); + // Toggle password and username visibility in entry view + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_B, this, SLOT(toggleUsernamesHidden())); - m_ui->actionDatabaseNew->setIcon(filePath()->icon("actions", "document-new")); - m_ui->actionDatabaseOpen->setIcon(filePath()->icon("actions", "document-open")); - m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save")); - m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as")); - m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close")); - m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit")); - m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key")); - m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "document-encrypt")); - m_ui->actionQuit->setIcon(filePath()->icon("actions", "application-exit")); + m_ui->actionDatabaseNew->setIcon(filePath()->icon("actions", "document-new")); + m_ui->actionDatabaseOpen->setIcon(filePath()->icon("actions", "document-open")); + m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save")); + m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as")); + m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close")); + m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit")); + m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key")); + m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "document-encrypt")); + m_ui->actionQuit->setIcon(filePath()->icon("actions", "application-exit")); - m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new")); - m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone")); - m_ui->actionEntryEdit->setIcon(filePath()->icon("actions", "entry-edit")); - m_ui->actionEntryDelete->setIcon(filePath()->icon("actions", "entry-delete")); - m_ui->actionEntryAutoType->setIcon(filePath()->icon("actions", "auto-type")); - m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); - m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); - m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy")); + m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new")); + m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone")); + m_ui->actionEntryEdit->setIcon(filePath()->icon("actions", "entry-edit")); + m_ui->actionEntryDelete->setIcon(filePath()->icon("actions", "entry-delete")); + m_ui->actionEntryAutoType->setIcon(filePath()->icon("actions", "auto-type")); + m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); + m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); + m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy")); - m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new")); - m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); - m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); - m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); + m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new")); + m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); + m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); + m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); - m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); - m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); + m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); + m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); - m_ui->actionAbout->setIcon(filePath()->icon("actions", "help-about")); + m_ui->actionAbout->setIcon(filePath()->icon("actions", "help-about")); - m_actionMultiplexer.connect( - SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); - m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState())); - m_actionMultiplexer.connect(SIGNAL(entrySelectionChanged()), this, SLOT(setMenuActionState())); - m_actionMultiplexer.connect(SIGNAL(groupContextMenuRequested(QPoint)), this, SLOT(showGroupContextMenu(QPoint))); - m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)), this, SLOT(showEntryContextMenu(QPoint))); + m_actionMultiplexer.connect( + SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); + m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState())); + m_actionMultiplexer.connect(SIGNAL(entrySelectionChanged()), this, SLOT(setMenuActionState())); + m_actionMultiplexer.connect(SIGNAL(groupContextMenuRequested(QPoint)), this, SLOT(showGroupContextMenu(QPoint))); + m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)), this, SLOT(showEntryContextMenu(QPoint))); - // Notify search when the active database changes or gets locked - connect(m_ui->tabWidget, - SIGNAL(activateDatabaseChanged(DatabaseWidget*)), - search, - SLOT(databaseChanged(DatabaseWidget*))); - connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), search, SLOT(databaseChanged())); + // Notify search when the active database changes or gets locked + connect(m_ui->tabWidget, + SIGNAL(activateDatabaseChanged(DatabaseWidget*)), + search, + SLOT(databaseChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), search, SLOT(databaseChanged())); - connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int))); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); - connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); - connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); - connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); - connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); - connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(applySettingsChanges())); - connect(m_ui->settingsWidget, SIGNAL(apply()), SLOT(applySettingsChanges())); - connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(switchToDatabases())); - connect(m_ui->settingsWidget, SIGNAL(rejected()), SLOT(switchToDatabases())); + connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int))); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); + connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); + connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); + connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); + connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(applySettingsChanges())); + connect(m_ui->settingsWidget, SIGNAL(apply()), SLOT(applySettingsChanges())); + connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(switchToDatabases())); + connect(m_ui->settingsWidget, SIGNAL(rejected()), SLOT(switchToDatabases())); - connect(m_ui->actionDatabaseNew, SIGNAL(triggered()), m_ui->tabWidget, SLOT(newDatabase())); - connect(m_ui->actionDatabaseOpen, SIGNAL(triggered()), m_ui->tabWidget, SLOT(openDatabase())); - connect(m_ui->actionDatabaseSave, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabase())); - connect(m_ui->actionDatabaseSaveAs, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabaseAs())); - connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab())); - connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase())); - connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); - connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); - connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); - connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); - connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); - connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); - connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit())); + connect(m_ui->actionDatabaseNew, SIGNAL(triggered()), m_ui->tabWidget, SLOT(newDatabase())); + connect(m_ui->actionDatabaseOpen, SIGNAL(triggered()), m_ui->tabWidget, SLOT(openDatabase())); + connect(m_ui->actionDatabaseSave, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabase())); + connect(m_ui->actionDatabaseSaveAs, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabaseAs())); + connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab())); + connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase())); + connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); + connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); + connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); + connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); + connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); + connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); + connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit())); - m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry())); - m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); - m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); - m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); + m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry())); + m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); + m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); + m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); - m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); - m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); + m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); + m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); - m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes())); - m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); - m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); + m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes())); + m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); + m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl())); - m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); - m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); - m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); - m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); + m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); + m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); + m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); + m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); - connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); - connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); - connect(m_ui->passwordGeneratorWidget, SIGNAL(dialogTerminated()), SLOT(closePasswordGen())); + connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); + connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); + connect(m_ui->passwordGeneratorWidget, SIGNAL(dialogTerminated()), SLOT(closePasswordGen())); - connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); - connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); - connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); - connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); - connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); + connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); + connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); + connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); + connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); + connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); - connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); - connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl())); - connect(m_ui->actionBugReport, SIGNAL(triggered()), SLOT(openBugReportUrl())); + connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); + connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl())); + connect(m_ui->actionBugReport, SIGNAL(triggered()), SLOT(openBugReportUrl())); #ifdef Q_OS_MACOS - setUnifiedTitleAndToolBarOnMac(true); + setUnifiedTitleAndToolBarOnMac(true); #endif - // clang-format off - connect(m_ui->tabWidget, - SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), - this, - SLOT(displayGlobalMessage(QString,MessageWidget::MessageType))); - // clang-format on + // clang-format off + connect(m_ui->tabWidget, + SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), + this, + SLOT(displayGlobalMessage(QString,MessageWidget::MessageType))); + // clang-format on - connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage())); + connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage())); - m_screenLockListener = new ScreenLockListener(this); - connect(m_screenLockListener, SIGNAL(screenLocked()), SLOT(handleScreenLock())); + m_screenLockListener = new ScreenLockListener(this); + connect(m_screenLockListener, SIGNAL(screenLocked()), SLOT(handleScreenLock())); - updateTrayIcon(); + updateTrayIcon(); - if (config()->hasAccessError()) { - m_ui->globalMessageWidget->showMessage(tr("Access error for config file %1").arg(config()->getFileName()), - MessageWidget::Error); - } + if (config()->hasAccessError()) { + m_ui->globalMessageWidget->showMessage(tr("Access error for config file %1").arg(config()->getFileName()), + MessageWidget::Error); + } #if !defined(KEEPASSXC_BUILD_TYPE_RELEASE) - m_ui->globalMessageWidget->showMessage( - tr("WARNING: You are using an unstable build of KeePassXC!\n" - "There is a high risk of corruption, maintain a backup of your databases.\n" - "This version is not meant for production use."), - MessageWidget::Warning, - -1); + m_ui->globalMessageWidget->showMessage( + tr("WARNING: You are using an unstable build of KeePassXC!\n" + "There is a high risk of corruption, maintain a backup of your databases.\n" + "This version is not meant for production use."), + MessageWidget::Warning, + -1); #elif (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) && QT_VERSION < QT_VERSION_CHECK(5, 6, 0)) - if (!config()->get("QtErrorMessageShown", false).toBool()) { - m_ui->globalMessageWidget->showMessage( - tr("WARNING: Your Qt version may cause KeePassXC to crash with an On-Screen Keyboard!\n" - "We recommend you use the AppImage available on our downloads page."), - MessageWidget::Warning, - -1); - config()->set("QtErrorMessageShown", true); - } + if (!config()->get("QtErrorMessageShown", false).toBool()) { + m_ui->globalMessageWidget->showMessage( + tr("WARNING: Your Qt version may cause KeePassXC to crash with an On-Screen Keyboard!\n" + "We recommend you use the AppImage available on our downloads page."), + MessageWidget::Warning, + -1); + config()->set("QtErrorMessageShown", true); + } #endif } @@ -402,748 +403,748 @@ MainWindow::~MainWindow() void MainWindow::showErrorMessage(const QString& message) { - m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); + m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); } void MainWindow::appExit() { - m_appExitCalled = true; - close(); + m_appExitCalled = true; + close(); } void MainWindow::updateLastDatabasesMenu() { - m_ui->menuRecentDatabases->clear(); + m_ui->menuRecentDatabases->clear(); - const QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList(); - for (const QString& database : lastDatabases) { - QAction* action = m_ui->menuRecentDatabases->addAction(database); - action->setData(database); - m_lastDatabasesActions->addAction(action); - } - m_ui->menuRecentDatabases->addSeparator(); - m_ui->menuRecentDatabases->addAction(m_clearHistoryAction); + const QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList(); + for (const QString& database : lastDatabases) { + QAction* action = m_ui->menuRecentDatabases->addAction(database); + action->setData(database); + m_lastDatabasesActions->addAction(action); + } + m_ui->menuRecentDatabases->addSeparator(); + m_ui->menuRecentDatabases->addAction(m_clearHistoryAction); } void MainWindow::updateCopyAttributesMenu() { - DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - if (!dbWidget) { - return; - } + DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return; + } - if (dbWidget->numberOfSelectedEntries() != 1) { - return; - } + if (dbWidget->numberOfSelectedEntries() != 1) { + return; + } - QList actions = m_ui->menuEntryCopyAttribute->actions(); - for (int i = m_countDefaultAttributes; i < actions.size(); i++) { - delete actions[i]; - } + QList actions = m_ui->menuEntryCopyAttribute->actions(); + for (int i = m_countDefaultAttributes; i < actions.size(); i++) { + delete actions[i]; + } - const QStringList customEntryAttributes = dbWidget->customEntryAttributes(); - for (const QString& key : customEntryAttributes) { - QAction* action = m_ui->menuEntryCopyAttribute->addAction(key); - action->setData(QVariant(key)); - m_copyAdditionalAttributeActions->addAction(action); - } + const QStringList customEntryAttributes = dbWidget->customEntryAttributes(); + for (const QString& key : customEntryAttributes) { + QAction* action = m_ui->menuEntryCopyAttribute->addAction(key); + action->setData(QVariant(key)); + m_copyAdditionalAttributeActions->addAction(action); + } } void MainWindow::openRecentDatabase(QAction* action) { - openDatabase(action->data().toString()); + openDatabase(action->data().toString()); } void MainWindow::clearLastDatabases() { - config()->set("LastDatabases", QVariant()); - bool inWelcomeWidget = (m_ui->stackedWidget->currentIndex() == 2); + config()->set("LastDatabases", QVariant()); + bool inWelcomeWidget = (m_ui->stackedWidget->currentIndex() == 2); - if (inWelcomeWidget) { - m_ui->welcomeWidget->refreshLastDatabases(); - } + if (inWelcomeWidget) { + m_ui->welcomeWidget->refreshLastDatabases(); + } } void MainWindow::openDatabase(const QString& filePath, const QString& pw, const QString& keyFile) { - if (pw.isEmpty() && keyFile.isEmpty()) { - m_ui->tabWidget->addDatabaseTab(filePath); - return; - } + if (pw.isEmpty() && keyFile.isEmpty()) { + m_ui->tabWidget->addDatabaseTab(filePath); + return; + } - auto db = QSharedPointer::create(); - auto key = QSharedPointer::create(); - if (!pw.isEmpty()) { - key->addKey(QSharedPointer::create(pw)); - } - if (!keyFile.isEmpty()) { - auto fileKey = QSharedPointer::create(); - fileKey->load(keyFile); - key->addKey(fileKey); - } - if (db->open(filePath, key, nullptr, false)) { - auto* dbWidget = new DatabaseWidget(db, this); - m_ui->tabWidget->addDatabaseTab(dbWidget); - dbWidget->switchToMainView(true); - } + auto db = QSharedPointer::create(); + auto key = QSharedPointer::create(); + if (!pw.isEmpty()) { + key->addKey(QSharedPointer::create(pw)); + } + if (!keyFile.isEmpty()) { + auto fileKey = QSharedPointer::create(); + fileKey->load(keyFile); + key->addKey(fileKey); + } + if (db->open(filePath, key, nullptr, false)) { + auto* dbWidget = new DatabaseWidget(db, this); + m_ui->tabWidget->addDatabaseTab(dbWidget); + dbWidget->switchToMainView(true); + } } void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) { - int currentIndex = m_ui->stackedWidget->currentIndex(); + int currentIndex = m_ui->stackedWidget->currentIndex(); - bool inDatabaseTabWidget = (currentIndex == DatabaseTabScreen); - bool inWelcomeWidget = (currentIndex == WelcomeScreen); - bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; + bool inDatabaseTabWidget = (currentIndex == DatabaseTabScreen); + bool inWelcomeWidget = (currentIndex == WelcomeScreen); + bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; - m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); + m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); - m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); + m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); - if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { - DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - Q_ASSERT(dbWidget); + if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { + DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + Q_ASSERT(dbWidget); - if (mode == DatabaseWidget::Mode::None) { - mode = dbWidget->currentMode(); - } + if (mode == DatabaseWidget::Mode::None) { + mode = dbWidget->currentMode(); + } - switch (mode) { - case DatabaseWidget::Mode::ViewMode: { - // bool inSearch = dbWidget->isInSearchMode(); - bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && dbWidget->currentEntryHasFocus(); - bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && dbWidget->currentEntryHasFocus(); - bool groupSelected = dbWidget->isGroupSelected(); - bool recycleBinSelected = dbWidget->isRecycleBinSelected(); + switch (mode) { + case DatabaseWidget::Mode::ViewMode: { + // bool inSearch = dbWidget->isInSearchMode(); + bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && dbWidget->currentEntryHasFocus(); + bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && dbWidget->currentEntryHasFocus(); + bool groupSelected = dbWidget->isGroupSelected(); + bool recycleBinSelected = dbWidget->isRecycleBinSelected(); - m_ui->actionEntryNew->setEnabled(true); - m_ui->actionEntryClone->setEnabled(singleEntrySelected); - m_ui->actionEntryEdit->setEnabled(singleEntrySelected); - m_ui->actionEntryDelete->setEnabled(entriesSelected); - m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); - m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); - m_ui->actionEntryCopyPassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword()); - m_ui->actionEntryCopyURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); - m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes()); - m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected); - m_ui->menuEntryTotp->setEnabled(singleEntrySelected); - m_ui->actionEntryAutoType->setEnabled(singleEntrySelected); - m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); - m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); - m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); - m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); - m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); - m_ui->actionGroupNew->setEnabled(groupSelected); - m_ui->actionGroupEdit->setEnabled(groupSelected); - m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); - m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); - m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); - m_ui->actionChangeMasterKey->setEnabled(true); - m_ui->actionChangeDatabaseSettings->setEnabled(true); - m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); - m_ui->actionDatabaseSaveAs->setEnabled(true); - m_ui->actionExportCsv->setEnabled(true); - m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); + m_ui->actionEntryNew->setEnabled(true); + m_ui->actionEntryClone->setEnabled(singleEntrySelected); + m_ui->actionEntryEdit->setEnabled(singleEntrySelected); + m_ui->actionEntryDelete->setEnabled(entriesSelected); + m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); + m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); + m_ui->actionEntryCopyPassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword()); + m_ui->actionEntryCopyURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); + m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes()); + m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected); + m_ui->menuEntryTotp->setEnabled(singleEntrySelected); + m_ui->actionEntryAutoType->setEnabled(singleEntrySelected); + m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); + m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); + m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionGroupNew->setEnabled(groupSelected); + m_ui->actionGroupEdit->setEnabled(groupSelected); + m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); + m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); + m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); + m_ui->actionChangeMasterKey->setEnabled(true); + m_ui->actionChangeDatabaseSettings->setEnabled(true); + m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); + m_ui->actionDatabaseSaveAs->setEnabled(true); + m_ui->actionExportCsv->setEnabled(true); + m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); - m_searchWidgetAction->setEnabled(true); + m_searchWidgetAction->setEnabled(true); - break; - } - case DatabaseWidget::Mode::EditMode: - case DatabaseWidget::Mode::ImportMode: - case DatabaseWidget::Mode::LockedMode: { - const QList entryActions = m_ui->menuEntries->actions(); - for (QAction* action : entryActions) { - action->setEnabled(false); - } + break; + } + case DatabaseWidget::Mode::EditMode: + case DatabaseWidget::Mode::ImportMode: + case DatabaseWidget::Mode::LockedMode: { + const QList entryActions = m_ui->menuEntries->actions(); + for (QAction* action : entryActions) { + action->setEnabled(false); + } - const QList groupActions = m_ui->menuGroups->actions(); - for (QAction* action : groupActions) { - action->setEnabled(false); - } + const QList groupActions = m_ui->menuGroups->actions(); + for (QAction* action : groupActions) { + action->setEnabled(false); + } - m_ui->actionChangeMasterKey->setEnabled(false); - m_ui->actionChangeDatabaseSettings->setEnabled(false); - m_ui->actionDatabaseSave->setEnabled(false); - m_ui->actionDatabaseSaveAs->setEnabled(false); - m_ui->actionExportCsv->setEnabled(false); - m_ui->actionDatabaseMerge->setEnabled(false); + m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionChangeDatabaseSettings->setEnabled(false); + m_ui->actionDatabaseSave->setEnabled(false); + m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionExportCsv->setEnabled(false); + m_ui->actionDatabaseMerge->setEnabled(false); - m_searchWidgetAction->setEnabled(false); - break; - } - default: - Q_ASSERT(false); - } - m_ui->actionDatabaseClose->setEnabled(true); - } else { - const QList entryActions = m_ui->menuEntries->actions(); - for (QAction* action : entryActions) { - action->setEnabled(false); - } + m_searchWidgetAction->setEnabled(false); + break; + } + default: + Q_ASSERT(false); + } + m_ui->actionDatabaseClose->setEnabled(true); + } else { + const QList entryActions = m_ui->menuEntries->actions(); + for (QAction* action : entryActions) { + action->setEnabled(false); + } - const QList groupActions = m_ui->menuGroups->actions(); - for (QAction* action : groupActions) { - action->setEnabled(false); - } + const QList groupActions = m_ui->menuGroups->actions(); + for (QAction* action : groupActions) { + action->setEnabled(false); + } - m_ui->actionChangeMasterKey->setEnabled(false); - m_ui->actionChangeDatabaseSettings->setEnabled(false); - m_ui->actionDatabaseSave->setEnabled(false); - m_ui->actionDatabaseSaveAs->setEnabled(false); - m_ui->actionDatabaseClose->setEnabled(false); - m_ui->actionExportCsv->setEnabled(false); - m_ui->actionDatabaseMerge->setEnabled(false); + m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionChangeDatabaseSettings->setEnabled(false); + m_ui->actionDatabaseSave->setEnabled(false); + m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionDatabaseClose->setEnabled(false); + m_ui->actionExportCsv->setEnabled(false); + m_ui->actionDatabaseMerge->setEnabled(false); - m_searchWidgetAction->setEnabled(false); - } + m_searchWidgetAction->setEnabled(false); + } - if ((currentIndex == PasswordGeneratorScreen) != m_ui->actionPasswordGenerator->isChecked()) { - bool blocked = m_ui->actionPasswordGenerator->blockSignals(true); - m_ui->actionPasswordGenerator->toggle(); - m_ui->actionPasswordGenerator->blockSignals(blocked); - } else if ((currentIndex == SettingsScreen) != m_ui->actionSettings->isChecked()) { - bool blocked = m_ui->actionSettings->blockSignals(true); - m_ui->actionSettings->toggle(); - m_ui->actionSettings->blockSignals(blocked); - } + if ((currentIndex == PasswordGeneratorScreen) != m_ui->actionPasswordGenerator->isChecked()) { + bool blocked = m_ui->actionPasswordGenerator->blockSignals(true); + m_ui->actionPasswordGenerator->toggle(); + m_ui->actionPasswordGenerator->blockSignals(blocked); + } else if ((currentIndex == SettingsScreen) != m_ui->actionSettings->isChecked()) { + bool blocked = m_ui->actionSettings->blockSignals(true); + m_ui->actionSettings->toggle(); + m_ui->actionSettings->blockSignals(blocked); + } } void MainWindow::updateWindowTitle() { - QString customWindowTitlePart; - int stackedWidgetIndex = m_ui->stackedWidget->currentIndex(); - int tabWidgetIndex = m_ui->tabWidget->currentIndex(); - bool isModified = m_ui->tabWidget->isModified(tabWidgetIndex); + QString customWindowTitlePart; + int stackedWidgetIndex = m_ui->stackedWidget->currentIndex(); + int tabWidgetIndex = m_ui->tabWidget->currentIndex(); + bool isModified = m_ui->tabWidget->isModified(tabWidgetIndex); - if (stackedWidgetIndex == DatabaseTabScreen && tabWidgetIndex != -1) { - customWindowTitlePart = m_ui->tabWidget->tabName(tabWidgetIndex); - if (isModified) { - // remove asterisk '*' from title - customWindowTitlePart.remove(customWindowTitlePart.size() - 1, 1); - } - m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave(tabWidgetIndex)); - } else if (stackedWidgetIndex == 1) { - customWindowTitlePart = tr("Settings"); - } + if (stackedWidgetIndex == DatabaseTabScreen && tabWidgetIndex != -1) { + customWindowTitlePart = m_ui->tabWidget->tabName(tabWidgetIndex); + if (isModified) { + // remove asterisk '*' from title + customWindowTitlePart.remove(customWindowTitlePart.size() - 1, 1); + } + m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave(tabWidgetIndex)); + } else if (stackedWidgetIndex == 1) { + customWindowTitlePart = tr("Settings"); + } - QString windowTitle; - if (customWindowTitlePart.isEmpty()) { - windowTitle = BaseWindowTitle; - } else { - windowTitle = QString("%1[*] - %2").arg(customWindowTitlePart, BaseWindowTitle); - } + QString windowTitle; + if (customWindowTitlePart.isEmpty()) { + windowTitle = BaseWindowTitle; + } else { + windowTitle = QString("%1[*] - %2").arg(customWindowTitlePart, BaseWindowTitle); + } - if (customWindowTitlePart.isEmpty() || stackedWidgetIndex == 1) { - setWindowFilePath(""); - } else { - setWindowFilePath(m_ui->tabWidget->databaseWidgetFromIndex(tabWidgetIndex)->database()->filePath()); - } + if (customWindowTitlePart.isEmpty() || stackedWidgetIndex == 1) { + setWindowFilePath(""); + } else { + setWindowFilePath(m_ui->tabWidget->databaseWidgetFromIndex(tabWidgetIndex)->database()->filePath()); + } - setWindowTitle(windowTitle); - setWindowModified(isModified); + setWindowTitle(windowTitle); + setWindowModified(isModified); } void MainWindow::showAboutDialog() { - auto* aboutDialog = new AboutDialog(this); - aboutDialog->open(); + auto* aboutDialog = new AboutDialog(this); + aboutDialog->open(); } void MainWindow::openDonateUrl() { - QDesktopServices::openUrl(QUrl("https://keepassxc.org/donate")); + QDesktopServices::openUrl(QUrl("https://keepassxc.org/donate")); } void MainWindow::openBugReportUrl() { - QDesktopServices::openUrl(QUrl("https://github.com/keepassxreboot/keepassxc/issues")); + QDesktopServices::openUrl(QUrl("https://github.com/keepassxreboot/keepassxc/issues")); } void MainWindow::switchToDatabases() { - if (m_ui->tabWidget->currentIndex() == -1) { - m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); - } else { - m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); - } + if (m_ui->tabWidget->currentIndex() == -1) { + m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); + } else { + m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); + } } void MainWindow::switchToSettings(bool enabled) { - if (enabled) { - m_ui->settingsWidget->loadSettings(); - m_ui->stackedWidget->setCurrentIndex(SettingsScreen); - } else { - switchToDatabases(); - } + if (enabled) { + m_ui->settingsWidget->loadSettings(); + m_ui->stackedWidget->setCurrentIndex(SettingsScreen); + } else { + switchToDatabases(); + } } void MainWindow::switchToPasswordGen(bool enabled) { - if (enabled) { - m_ui->passwordGeneratorWidget->loadSettings(); - m_ui->passwordGeneratorWidget->regeneratePassword(); - m_ui->passwordGeneratorWidget->setStandaloneMode(true); - m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); - } else { - m_ui->passwordGeneratorWidget->saveSettings(); - switchToDatabases(); - } + if (enabled) { + m_ui->passwordGeneratorWidget->loadSettings(); + m_ui->passwordGeneratorWidget->regeneratePassword(); + m_ui->passwordGeneratorWidget->setStandaloneMode(true); + m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); + } else { + m_ui->passwordGeneratorWidget->saveSettings(); + switchToDatabases(); + } } void MainWindow::closePasswordGen() { - switchToPasswordGen(false); + switchToPasswordGen(false); } void MainWindow::switchToNewDatabase() { - m_ui->tabWidget->newDatabase(); - switchToDatabases(); + m_ui->tabWidget->newDatabase(); + switchToDatabases(); } void MainWindow::switchToOpenDatabase() { - m_ui->tabWidget->openDatabase(); - switchToDatabases(); + m_ui->tabWidget->openDatabase(); + switchToDatabases(); } void MainWindow::switchToDatabaseFile(const QString& file) { - m_ui->tabWidget->addDatabaseTab(file); - switchToDatabases(); + m_ui->tabWidget->addDatabaseTab(file); + switchToDatabases(); } void MainWindow::switchToKeePass1Database() { - m_ui->tabWidget->importKeePass1Database(); - switchToDatabases(); + m_ui->tabWidget->importKeePass1Database(); + switchToDatabases(); } void MainWindow::switchToCsvImport() { - m_ui->tabWidget->importCsv(); - switchToDatabases(); + m_ui->tabWidget->importCsv(); + switchToDatabases(); } void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget) { - Q_UNUSED(dbWidget); - updateTrayIcon(); + Q_UNUSED(dbWidget); + updateTrayIcon(); } void MainWindow::selectNextDatabaseTab() { - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - int index = m_ui->tabWidget->currentIndex() + 1; - if (index >= m_ui->tabWidget->count()) { - m_ui->tabWidget->setCurrentIndex(0); - } else { - m_ui->tabWidget->setCurrentIndex(index); - } - } + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->currentIndex() + 1; + if (index >= m_ui->tabWidget->count()) { + m_ui->tabWidget->setCurrentIndex(0); + } else { + m_ui->tabWidget->setCurrentIndex(index); + } + } } void MainWindow::selectPreviousDatabaseTab() { - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - int index = m_ui->tabWidget->currentIndex() - 1; - if (index < 0) { - m_ui->tabWidget->setCurrentIndex(m_ui->tabWidget->count() - 1); - } else { - m_ui->tabWidget->setCurrentIndex(index); - } - } + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->currentIndex() - 1; + if (index < 0) { + m_ui->tabWidget->setCurrentIndex(m_ui->tabWidget->count() - 1); + } else { + m_ui->tabWidget->setCurrentIndex(index); + } + } } void MainWindow::databaseTabChanged(int tabIndex) { - if (tabIndex != -1 && m_ui->stackedWidget->currentIndex() == WelcomeScreen) { - m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); - } else if (tabIndex == -1 && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); - } + if (tabIndex != -1 && m_ui->stackedWidget->currentIndex() == WelcomeScreen) { + m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); + } else if (tabIndex == -1 && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); + } - m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget()); + m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget()); } void MainWindow::togglePasswordsHidden() { - auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - if (dbWidget) { - dbWidget->setPasswordsHidden(!dbWidget->isPasswordsHidden()); - } + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget) { + dbWidget->setPasswordsHidden(!dbWidget->isPasswordsHidden()); + } } void MainWindow::toggleUsernamesHidden() { - auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - if (dbWidget) { - dbWidget->setUsernamesHidden(!dbWidget->isUsernamesHidden()); - } + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget) { + dbWidget->setUsernamesHidden(!dbWidget->isUsernamesHidden()); + } } void MainWindow::closeEvent(QCloseEvent* event) { - // ignore double close events (happens on macOS when closing from the dock) - if (m_appExiting) { - event->accept(); - return; - } + // ignore double close events (happens on macOS when closing from the dock) + if (m_appExiting) { + event->accept(); + return; + } - if (config()->get("GUI/MinimizeOnClose").toBool() && !m_appExitCalled) { - event->ignore(); - hideWindow(); - return; - } + if (config()->get("GUI/MinimizeOnClose").toBool() && !m_appExitCalled) { + event->ignore(); + hideWindow(); + return; + } - bool accept = saveLastDatabases(); + bool accept = saveLastDatabases(); - if (accept) { - m_appExiting = true; - saveWindowInformation(); + if (accept) { + m_appExiting = true; + saveWindowInformation(); - event->accept(); - QApplication::quit(); - } else { - event->ignore(); - } + event->accept(); + QApplication::quit(); + } else { + event->ignore(); + } } void MainWindow::changeEvent(QEvent* event) { - if ((event->type() == QEvent::WindowStateChange) && isMinimized()) { - if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible() - && config()->get("GUI/MinimizeToTray").toBool()) { - event->ignore(); - QTimer::singleShot(0, this, SLOT(hide())); - } + if ((event->type() == QEvent::WindowStateChange) && isMinimized()) { + if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible() + && config()->get("GUI/MinimizeToTray").toBool()) { + event->ignore(); + QTimer::singleShot(0, this, SLOT(hide())); + } - if (config()->get("security/lockdatabaseminimize").toBool()) { - m_ui->tabWidget->lockDatabases(); - } - } else { - QMainWindow::changeEvent(event); - } + if (config()->get("security/lockdatabaseminimize").toBool()) { + m_ui->tabWidget->lockDatabases(); + } + } else { + QMainWindow::changeEvent(event); + } } void MainWindow::saveWindowInformation() { - if (isVisible()) { - config()->set("GUI/MainWindowGeometry", saveGeometry()); - } + if (isVisible()) { + config()->set("GUI/MainWindowGeometry", saveGeometry()); + } } bool MainWindow::saveLastDatabases() { - bool accept; - m_openDatabases.clear(); - bool openPreviousDatabasesOnStartup = config()->get("OpenPreviousDatabasesOnStartup").toBool(); + bool accept; + m_openDatabases.clear(); + bool openPreviousDatabasesOnStartup = config()->get("OpenPreviousDatabasesOnStartup").toBool(); - if (openPreviousDatabasesOnStartup) { - connect(m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); - } + if (openPreviousDatabasesOnStartup) { + connect(m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); + } - accept = m_ui->tabWidget->closeAllDatabaseTabs(); + accept = m_ui->tabWidget->closeAllDatabaseTabs(); - if (openPreviousDatabasesOnStartup) { - disconnect( - m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); - config()->set("LastOpenedDatabases", m_openDatabases); - } + if (openPreviousDatabasesOnStartup) { + disconnect( + m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); + config()->set("LastOpenedDatabases", m_openDatabases); + } - return accept; + return accept; } void MainWindow::updateTrayIcon() { - if (isTrayIconEnabled()) { - if (!m_trayIcon) { - m_trayIcon = new QSystemTrayIcon(this); - QMenu* menu = new QMenu(this); + if (isTrayIconEnabled()) { + if (!m_trayIcon) { + m_trayIcon = new QSystemTrayIcon(this); + QMenu* menu = new QMenu(this); - QAction* actionToggle = new QAction(tr("Toggle window"), menu); - menu->addAction(actionToggle); + QAction* actionToggle = new QAction(tr("Toggle window"), menu); + menu->addAction(actionToggle); #ifdef Q_OS_MACOS - QAction* actionQuit = new QAction(tr("Quit KeePassXC"), menu); - menu->addAction(actionQuit); + QAction* actionQuit = new QAction(tr("Quit KeePassXC"), menu); + menu->addAction(actionQuit); - connect(actionQuit, SIGNAL(triggered()), SLOT(appExit())); + connect(actionQuit, SIGNAL(triggered()), SLOT(appExit())); #else - menu->addAction(m_ui->actionQuit); + menu->addAction(m_ui->actionQuit); - connect(m_trayIcon, - SIGNAL(activated(QSystemTrayIcon::ActivationReason)), - SLOT(trayIconTriggered(QSystemTrayIcon::ActivationReason))); + connect(m_trayIcon, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + SLOT(trayIconTriggered(QSystemTrayIcon::ActivationReason))); #endif - connect(actionToggle, SIGNAL(triggered()), SLOT(toggleWindow())); + connect(actionToggle, SIGNAL(triggered()), SLOT(toggleWindow())); - m_trayIcon->setContextMenu(menu); + m_trayIcon->setContextMenu(menu); - m_trayIcon->setIcon(filePath()->trayIcon()); - m_trayIcon->show(); - } - if (m_ui->tabWidget->hasLockableDatabases()) { - m_trayIcon->setIcon(filePath()->trayIconUnlocked()); - } else { - m_trayIcon->setIcon(filePath()->trayIconLocked()); - } - } else { - if (m_trayIcon) { - m_trayIcon->hide(); - delete m_trayIcon; - m_trayIcon = nullptr; - } - } + m_trayIcon->setIcon(filePath()->trayIcon()); + m_trayIcon->show(); + } + if (m_ui->tabWidget->hasLockableDatabases()) { + m_trayIcon->setIcon(filePath()->trayIconUnlocked()); + } else { + m_trayIcon->setIcon(filePath()->trayIconLocked()); + } + } else { + if (m_trayIcon) { + m_trayIcon->hide(); + delete m_trayIcon; + m_trayIcon = nullptr; + } + } } void MainWindow::showEntryContextMenu(const QPoint& globalPos) { - m_ui->menuEntries->popup(globalPos); + m_ui->menuEntries->popup(globalPos); } void MainWindow::showGroupContextMenu(const QPoint& globalPos) { - m_ui->menuGroups->popup(globalPos); + m_ui->menuGroups->popup(globalPos); } void MainWindow::setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback) { - if (!QKeySequence::keyBindings(standard).isEmpty()) { - action->setShortcuts(standard); - } else if (fallback != 0) { - action->setShortcut(QKeySequence(fallback)); - } + if (!QKeySequence::keyBindings(standard).isEmpty()) { + action->setShortcuts(standard); + } else if (fallback != 0) { + action->setShortcut(QKeySequence(fallback)); + } } void MainWindow::rememberOpenDatabases(const QString& filePath) { - m_openDatabases.prepend(filePath); + m_openDatabases.prepend(filePath); } void MainWindow::applySettingsChanges() { - int timeout = config()->get("security/lockdatabaseidlesec").toInt() * 1000; - if (timeout <= 0) { - timeout = 60; - } + int timeout = config()->get("security/lockdatabaseidlesec").toInt() * 1000; + if (timeout <= 0) { + timeout = 60; + } - m_inactivityTimer->setInactivityTimeout(timeout); - if (config()->get("security/lockdatabaseidle").toBool()) { - m_inactivityTimer->activate(); - } else { - m_inactivityTimer->deactivate(); - } + m_inactivityTimer->setInactivityTimeout(timeout); + if (config()->get("security/lockdatabaseidle").toBool()) { + m_inactivityTimer->activate(); + } else { + m_inactivityTimer->deactivate(); + } #ifdef WITH_XC_TOUCHID - // forget TouchID (in minutes) - timeout = config()->get("security/resettouchidtimeout").toInt() * 60 * 1000; - if (timeout <= 0) { - timeout = 30 * 60 * 1000; - } + // forget TouchID (in minutes) + timeout = config()->get("security/resettouchidtimeout").toInt() * 60 * 1000; + if (timeout <= 0) { + timeout = 30 * 60 * 1000; + } - m_touchIDinactivityTimer->setInactivityTimeout(timeout); - if (config()->get("security/resettouchid").toBool()) { - m_touchIDinactivityTimer->activate(); - } else { - m_touchIDinactivityTimer->deactivate(); - } + m_touchIDinactivityTimer->setInactivityTimeout(timeout); + if (config()->get("security/resettouchid").toBool()) { + m_touchIDinactivityTimer->activate(); + } else { + m_touchIDinactivityTimer->deactivate(); + } #endif - m_ui->toolBar->setHidden(config()->get("GUI/HideToolbar").toBool()); + m_ui->toolBar->setHidden(config()->get("GUI/HideToolbar").toBool()); - updateTrayIcon(); + updateTrayIcon(); } void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::MiddleClick) { - toggleWindow(); - } + if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::MiddleClick) { + toggleWindow(); + } } void MainWindow::hideWindow() { - saveWindowInformation(); + saveWindowInformation(); #if !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS) - // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at - // the same time (which would happen if both minimize on startup and minimize to tray are set) - // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. - // TODO: Add an explanation for why this is also not done on Mac (or remove the check) - setWindowState(windowState() | Qt::WindowMinimized); + // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at + // the same time (which would happen if both minimize on startup and minimize to tray are set) + // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. + // TODO: Add an explanation for why this is also not done on Mac (or remove the check) + setWindowState(windowState() | Qt::WindowMinimized); #endif - // Only hide if tray icon is active, otherwise window will be gone forever - if (isTrayIconEnabled()) { - hide(); - } else { - showMinimized(); - } + // Only hide if tray icon is active, otherwise window will be gone forever + if (isTrayIconEnabled()) { + hide(); + } else { + showMinimized(); + } - if (config()->get("security/lockdatabaseminimize").toBool()) { - m_ui->tabWidget->lockDatabases(); - } + if (config()->get("security/lockdatabaseminimize").toBool()) { + m_ui->tabWidget->lockDatabases(); + } } void MainWindow::toggleWindow() { - if (isVisible() && !isMinimized()) { - hideWindow(); - } else { - bringToFront(); + if (isVisible() && !isMinimized()) { + hideWindow(); + } else { + bringToFront(); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) - // re-register global D-Bus menu (needed on Ubuntu with Unity) - // see https://github.com/keepassxreboot/keepassxc/issues/271 - // and https://bugreports.qt.io/browse/QTBUG-58723 - // check for !isVisible(), because isNativeMenuBar() does not work with appmenu-qt5 - if (!m_ui->menubar->isVisible()) { - QDBusMessage msg = QDBusMessage::createMethodCall("com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar", - "com.canonical.AppMenu.Registrar", - "RegisterWindow"); - QList args; - args << QVariant::fromValue(static_cast(winId())) - << QVariant::fromValue(QDBusObjectPath("/MenuBar/1")); - msg.setArguments(args); - QDBusConnection::sessionBus().send(msg); - } + // re-register global D-Bus menu (needed on Ubuntu with Unity) + // see https://github.com/keepassxreboot/keepassxc/issues/271 + // and https://bugreports.qt.io/browse/QTBUG-58723 + // check for !isVisible(), because isNativeMenuBar() does not work with appmenu-qt5 + if (!m_ui->menubar->isVisible()) { + QDBusMessage msg = QDBusMessage::createMethodCall("com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar", + "com.canonical.AppMenu.Registrar", + "RegisterWindow"); + QList args; + args << QVariant::fromValue(static_cast(winId())) + << QVariant::fromValue(QDBusObjectPath("/MenuBar/1")); + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); + } #endif - } + } } void MainWindow::lockDatabasesAfterInactivity() { - // ignore event if a modal dialog is open (such as a message box or file dialog) - if (QApplication::activeModalWidget()) { - return; - } + // ignore event if a modal dialog is open (such as a message box or file dialog) + if (QApplication::activeModalWidget()) { + return; + } - m_ui->tabWidget->lockDatabases(); + m_ui->tabWidget->lockDatabases(); } void MainWindow::forgetTouchIDAfterInactivity() { #ifdef WITH_XC_TOUCHID - TouchID::getInstance().reset(); + TouchID::getInstance().reset(); #endif } bool MainWindow::isTrayIconEnabled() const { - return config()->get("GUI/ShowTrayIcon").toBool() && QSystemTrayIcon::isSystemTrayAvailable(); + return config()->get("GUI/ShowTrayIcon").toBool() && QSystemTrayIcon::isSystemTrayAvailable(); } void MainWindow::displayGlobalMessage(const QString& text, - MessageWidget::MessageType type, - bool showClosebutton, - int autoHideTimeout) + MessageWidget::MessageType type, + bool showClosebutton, + int autoHideTimeout) { - m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); - m_ui->globalMessageWidget->showMessage(text, type, autoHideTimeout); + m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); + m_ui->globalMessageWidget->showMessage(text, type, autoHideTimeout); } void MainWindow::displayTabMessage(const QString& text, - MessageWidget::MessageType type, - bool showClosebutton, - int autoHideTimeout) + MessageWidget::MessageType type, + bool showClosebutton, + int autoHideTimeout) { - m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type, showClosebutton, autoHideTimeout); + m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type, showClosebutton, autoHideTimeout); } void MainWindow::hideGlobalMessage() { - m_ui->globalMessageWidget->hideMessage(); + m_ui->globalMessageWidget->hideMessage(); } void MainWindow::showYubiKeyPopup() { - displayGlobalMessage(tr("Please touch the button on your YubiKey!"), - MessageWidget::Information, - false, - MessageWidget::DisableAutoHide); - setEnabled(false); + displayGlobalMessage(tr("Please touch the button on your YubiKey!"), + MessageWidget::Information, + false, + MessageWidget::DisableAutoHide); + setEnabled(false); } void MainWindow::hideYubiKeyPopup() { - hideGlobalMessage(); - setEnabled(true); + hideGlobalMessage(); + setEnabled(true); } void MainWindow::bringToFront() { - ensurePolished(); - setWindowState(windowState() & ~Qt::WindowMinimized); - show(); - raise(); - activateWindow(); + ensurePolished(); + setWindowState(windowState() & ~Qt::WindowMinimized); + show(); + raise(); + activateWindow(); } void MainWindow::handleScreenLock() { - if (config()->get("security/lockdatabasescreenlock").toBool()) { - lockDatabasesAfterInactivity(); - } + if (config()->get("security/lockdatabasescreenlock").toBool()) { + lockDatabasesAfterInactivity(); + } #ifdef WITH_XC_TOUCHID - if (config()->get("security/resettouchidscreenlock").toBool()) { - forgetTouchIDAfterInactivity(); - } + if (config()->get("security/resettouchidscreenlock").toBool()) { + forgetTouchIDAfterInactivity(); + } #endif } QStringList MainWindow::kdbxFilesFromUrls(const QList& urls) { - QStringList kdbxFiles; - for (const QUrl& url : urls) { - const QFileInfo fInfo(url.toLocalFile()); - const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx"; - if (isKdbxFile) { - kdbxFiles.append(fInfo.absoluteFilePath()); - } - } + QStringList kdbxFiles; + for (const QUrl& url : urls) { + const QFileInfo fInfo(url.toLocalFile()); + const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx"; + if (isKdbxFile) { + kdbxFiles.append(fInfo.absoluteFilePath()); + } + } - return kdbxFiles; + return kdbxFiles; } void MainWindow::dragEnterEvent(QDragEnterEvent* event) { - const QMimeData* mimeData = event->mimeData(); - if (mimeData->hasUrls()) { - const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); - if (!kdbxFiles.isEmpty()) { - event->acceptProposedAction(); - } - } + const QMimeData* mimeData = event->mimeData(); + if (mimeData->hasUrls()) { + const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); + if (!kdbxFiles.isEmpty()) { + event->acceptProposedAction(); + } + } } void MainWindow::dropEvent(QDropEvent* event) { - const QMimeData* mimeData = event->mimeData(); - if (mimeData->hasUrls()) { - const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); - if (!kdbxFiles.isEmpty()) { - event->acceptProposedAction(); - } - for (const QString& kdbxFile : kdbxFiles) { - openDatabase(kdbxFile); - } - } + const QMimeData* mimeData = event->mimeData(); + if (mimeData->hasUrls()) { + const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); + if (!kdbxFiles.isEmpty()) { + event->acceptProposedAction(); + } + for (const QString& kdbxFile : kdbxFiles) { + openDatabase(kdbxFile); + } + } } void MainWindow::closeAllDatabases() { - m_ui->tabWidget->closeAllDatabaseTabs(); + m_ui->tabWidget->closeAllDatabaseTabs(); } void MainWindow::lockAllDatabases() { - lockDatabasesAfterInactivity(); + lockDatabasesAfterInactivity(); } diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp index 3e15d2137..ba188a974 100644 --- a/src/keeshare/KeeShare.cpp +++ b/src/keeshare/KeeShare.cpp @@ -129,12 +129,12 @@ bool KeeShare::isEnabled(const Group* group) { const auto reference = KeeShare::referenceOf(group); #if !defined(WITH_XC_KEESHARE_SECURE) - if (reference.path.endsWith(secureContainerFileType(), Qt::CaseInsensitive)){ + if (reference.path.endsWith(signedContainerFileType(), Qt::CaseInsensitive)){ return false; } #endif #if !defined(WITH_XC_KEESHARE_INSECURE) - if (reference.path.endsWith(insecureContainerFileType(), Qt::CaseInsensitive)){ + if (reference.path.endsWith(unsignedContainerFileType(), Qt::CaseInsensitive)){ return false; } #endif @@ -198,13 +198,13 @@ void KeeShare::connectDatabase(QSharedPointer newDb, QSharedPointer newDb, QSharedPointer oldDb); - static const QString& secureContainerFileType(); - static const QString& insecureContainerFileType(); + static const QString& signedContainerFileType(); + static const QString& unsignedContainerFileType(); signals: void activeChanged(); diff --git a/src/keeshare/SettingsWidgetKeeShare.cpp b/src/keeshare/SettingsWidgetKeeShare.cpp index be4aee2ef..03fee3340 100644 --- a/src/keeshare/SettingsWidgetKeeShare.cpp +++ b/src/keeshare/SettingsWidgetKeeShare.cpp @@ -29,26 +29,26 @@ #include SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent) - : QWidget(parent) - , m_ui(new Ui::SettingsWidgetKeeShare()) + : QWidget(parent) + , m_ui(new Ui::SettingsWidgetKeeShare()) { - m_ui->setupUi(this); + m_ui->setupUi(this); #if !defined(WITH_XC_KEESHARE_SECURE) - // Setting does not help the user of Version without secure export - m_ui->ownCertificateGroupBox->setVisible(false); + // Setting does not help the user of Version without signed export + m_ui->ownCertificateGroupBox->setVisible(false); #endif - connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString))); + connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString))); - connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate())); - connect(m_ui->importOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(importCertificate())); - connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate())); + connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate())); + connect(m_ui->importOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(importCertificate())); + connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate())); - connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates())); - connect(m_ui->askImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(askSelectedCertificates())); - connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates())); - connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates())); + connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates())); + connect(m_ui->askImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(askSelectedCertificates())); + connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates())); + connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates())); } SettingsWidgetKeeShare::~SettingsWidgetKeeShare() @@ -57,181 +57,181 @@ SettingsWidgetKeeShare::~SettingsWidgetKeeShare() void SettingsWidgetKeeShare::loadSettings() { - const auto active = KeeShare::active(); - m_ui->enableExportCheckBox->setChecked(active.out); - m_ui->enableImportCheckBox->setChecked(active.in); + const auto active = KeeShare::active(); + m_ui->enableExportCheckBox->setChecked(active.out); + m_ui->enableImportCheckBox->setChecked(active.in); - m_own = KeeShare::own(); - updateOwnCertificate(); + m_own = KeeShare::own(); + updateOwnCertificate(); - m_foreign = KeeShare::foreign(); - updateForeignCertificates(); + m_foreign = KeeShare::foreign(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::updateForeignCertificates() { - m_importedCertificateModel.reset(new QStandardItemModel()); - m_importedCertificateModel->setHorizontalHeaderLabels(QStringList() << tr("Path") << tr("Status") + m_importedCertificateModel.reset(new QStandardItemModel()); + m_importedCertificateModel->setHorizontalHeaderLabels(QStringList() << tr("Path") << tr("Status") #if defined(WITH_XC_KEESHARE_SECURE) - << tr("Signer") << tr("Fingerprint") << tr("Certificate") + << tr("Signer") << tr("Fingerprint") << tr("Certificate") #endif - ); + ); - for (const auto& scopedCertificate : m_foreign.certificates) { - const auto items = QList() - << new QStandardItem(scopedCertificate.path) - << new QStandardItem(scopedCertificate.trust == KeeShareSettings::Trust::Ask ? tr("Ask") - : (scopedCertificate.trust == KeeShareSettings::Trust::Trusted ? tr("Trusted") - : tr("Untrusted"))) + for (const auto& scopedCertificate : m_foreign.certificates) { + const auto items = QList() + << new QStandardItem(scopedCertificate.path) + << new QStandardItem(scopedCertificate.trust == KeeShareSettings::Trust::Ask ? tr("Ask") + : (scopedCertificate.trust == KeeShareSettings::Trust::Trusted ? tr("Trusted") + : tr("Untrusted"))) #if defined(WITH_XC_KEESHARE_SECURE) - << new QStandardItem(scopedCertificate.isKnown() ? scopedCertificate.certificate.signer : tr("Unknown")) - << new QStandardItem(scopedCertificate.certificate.fingerprint()) - << new QStandardItem(scopedCertificate.certificate.publicKey()) + << new QStandardItem(scopedCertificate.isKnown() ? scopedCertificate.certificate.signer : tr("Unknown")) + << new QStandardItem(scopedCertificate.certificate.fingerprint()) + << new QStandardItem(scopedCertificate.certificate.publicKey()) #endif - ; - m_importedCertificateModel->appendRow(items); - } + ; + m_importedCertificateModel->appendRow(items); + } - m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data()); + m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data()); } void SettingsWidgetKeeShare::updateOwnCertificate() { - m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); - m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); - m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); - m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); + m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); + m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); } void SettingsWidgetKeeShare::saveSettings() { - KeeShareSettings::Active active; - active.out = m_ui->enableExportCheckBox->isChecked(); - active.in = m_ui->enableImportCheckBox->isChecked(); - // TODO HNH: This depends on the order of saving new data - a better model would be to - // store changes to the settings in a temporary object and check on the final values - // of this object (similar scheme to Entry) - this way we could validate the settings before save - KeeShare::setOwn(m_own); - KeeShare::setForeign(m_foreign); - KeeShare::setActive(active); + KeeShareSettings::Active active; + active.out = m_ui->enableExportCheckBox->isChecked(); + active.in = m_ui->enableImportCheckBox->isChecked(); + // TODO HNH: This depends on the order of saving new data - a better model would be to + // store changes to the settings in a temporary object and check on the final values + // of this object (similar scheme to Entry) - this way we could validate the settings before save + KeeShare::setOwn(m_own); + KeeShare::setForeign(m_foreign); + KeeShare::setActive(active); } void SettingsWidgetKeeShare::setVerificationExporter(const QString& signer) { - m_own.certificate.signer = signer; - m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_own.certificate.signer = signer; + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); } void SettingsWidgetKeeShare::generateCertificate() { - m_own = KeeShareSettings::Own::generate(); - m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); - m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); - m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); - m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); + m_own = KeeShareSettings::Own::generate(); + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); + m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); + m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); } void SettingsWidgetKeeShare::importCertificate() { - QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); - const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); - if (!dirExists) { - defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); - } - const auto filetype = tr("key.share", "Filetype for KeeShare key"); - const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); - QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0)); - if (filename.isEmpty()) { - return; - } - QFile file(filename); - file.open(QIODevice::ReadOnly); - QTextStream stream(&file); - m_own = KeeShareSettings::Own::deserialize(stream.readAll()); - file.close(); - config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); + QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + const auto filetype = tr("key.share", "Filetype for KeeShare key"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); + QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0)); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + file.open(QIODevice::ReadOnly); + QTextStream stream(&file); + m_own = KeeShareSettings::Own::deserialize(stream.readAll()); + file.close(); + config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); - updateOwnCertificate(); + updateOwnCertificate(); } void SettingsWidgetKeeShare::exportCertificate() { - if (KeeShare::own() != m_own) { - QMessageBox warning; - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(tr("Exporting changed certificate")); - warning.setText(tr("The exported certificate is not the same as the one in use. Do you want to export the current certificate?")); - auto yes = warning.addButton(QMessageBox::StandardButton::Yes); - auto no = warning.addButton(QMessageBox::StandardButton::No); - warning.setDefaultButton(no); - warning.exec(); - if (warning.clickedButton() != yes) { - return; - } - } - QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); - const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); - if (!dirExists) { - defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); - } - const auto filetype = tr("key.share", "Filetype for KeeShare key"); - const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); - QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype); - filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0), filetype, filename); - if (filename.isEmpty()) { - return; - } - QFile file(filename); - file.open(QIODevice::Truncate | QIODevice::WriteOnly); - QTextStream stream(&file); - stream << KeeShareSettings::Own::serialize(m_own); - stream.flush(); - file.close(); - config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); + if (KeeShare::own() != m_own) { + QMessageBox warning; + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(tr("Exporting changed certificate")); + warning.setText(tr("The exported certificate is not the same as the one in use. Do you want to export the current certificate?")); + auto yes = warning.addButton(QMessageBox::StandardButton::Yes); + auto no = warning.addButton(QMessageBox::StandardButton::No); + warning.setDefaultButton(no); + warning.exec(); + if (warning.clickedButton() != yes) { + return; + } + } + QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + const auto filetype = tr("key.share", "Filetype for KeeShare key"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); + QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype); + filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0), filetype, filename); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + file.open(QIODevice::Truncate | QIODevice::WriteOnly); + QTextStream stream(&file); + stream << KeeShareSettings::Own::serialize(m_own); + stream.flush(); + file.close(); + config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); } void SettingsWidgetKeeShare::trustSelectedCertificates() { - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Trusted; - } + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Trusted; + } - updateForeignCertificates(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::askSelectedCertificates() { - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Ask; - } + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Ask; + } - updateForeignCertificates(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::untrustSelectedCertificates() { - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Untrusted; - } + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Untrusted; + } - updateForeignCertificates(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::removeSelectedCertificates() { - auto certificates = m_foreign.certificates; - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - certificates.removeOne(m_foreign.certificates[index.row()]); - } - m_foreign.certificates = certificates; + auto certificates = m_foreign.certificates; + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + certificates.removeOne(m_foreign.certificates[index.row()]); + } + m_foreign.certificates = certificates; - updateForeignCertificates(); + updateForeignCertificates(); } diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 45f3049c4..bb87c1c75 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -56,116 +56,116 @@ static const QString KeeShare_Container("container.share.kdbx"); enum Trust { - Invalid, - Own, - UntrustedForever, - UntrustedOnce, - TrustedOnce, - TrustedForever, + Invalid, + Own, + UntrustedForever, + UntrustedOnce, + TrustedOnce, + TrustedForever, }; bool isOfExportType(const QFileInfo &fileInfo, const QString type) { - return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); + return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); } QPair check(QByteArray& data, - const KeeShareSettings::Reference& reference, - const KeeShareSettings::Certificate& ownCertificate, - const QList& knownCertificates, - const KeeShareSettings::Sign& sign) + const KeeShareSettings::Reference& reference, + const KeeShareSettings::Certificate& ownCertificate, + const QList& knownCertificates, + const KeeShareSettings::Sign& sign) { - KeeShareSettings::Certificate certificate; - if (!sign.signature.isEmpty()) { - certificate = sign.certificate; - auto key = sign.certificate.sshKey(); - key.openKey(QString()); - const auto signer = Signature(); - if (!signer.verify(data, sign.signature, key)) { - qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); - return {Invalid, KeeShareSettings::Certificate()}; - } + KeeShareSettings::Certificate certificate; + if (!sign.signature.isEmpty()) { + certificate = sign.certificate; + auto key = sign.certificate.sshKey(); + key.openKey(QString()); + const auto signer = Signature(); + if (!signer.verify(data, sign.signature, key)) { + qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); + return {Invalid, KeeShareSettings::Certificate()}; + } - if (ownCertificate.key == sign.certificate.key) { - return {Own, ownCertificate }; - } - } - enum Scope { Invalid, Global, Local }; - Scope scope = Invalid; - KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; - for (const auto& scopedCertificate : knownCertificates) { - if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { - // Global scope is overwritten by local scope - scope = Global; - trusted = scopedCertificate.trust; - } - if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { - scope = Local; - trusted = scopedCertificate.trust; - break; - } - } - if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask){ - // we introduce now scopes if there is a global - return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; - } + if (ownCertificate.key == sign.certificate.key) { + return {Own, ownCertificate }; + } + } + enum Scope { Invalid, Global, Local }; + Scope scope = Invalid; + KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; + for (const auto& scopedCertificate : knownCertificates) { + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + // Global scope is overwritten by local scope + scope = Global; + trusted = scopedCertificate.trust; + } + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + scope = Local; + trusted = scopedCertificate.trust; + break; + } + } + if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask){ + // we introduce now scopes if there is a global + return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; + } - QMessageBox warning; - if (sign.signature.isEmpty()){ - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); - warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import from %1?") - .arg(reference.path)); - } - else { - warning.setIcon(QMessageBox::Question); - warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); - warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") - .arg(certificate.signer, certificate.fingerprint(), reference.path)); - } - auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); - auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); - auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); - auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); - warning.setDefaultButton(untrustedOnce); - warning.exec(); - if (warning.clickedButton() == trustedForever){ - return {TrustedForever, certificate }; - } - if (warning.clickedButton() == trustedOnce){ - return {TrustedOnce, certificate}; - } - if (warning.clickedButton() == untrustedOnce){ - return {UntrustedOnce, certificate }; - } - if (warning.clickedButton() == untrustedForever){ - return {UntrustedForever, certificate }; - } - return {UntrustedOnce, certificate }; + QMessageBox warning; + if (sign.signature.isEmpty()){ + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); + warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import from %1?") + .arg(reference.path)); + } + else { + warning.setIcon(QMessageBox::Question); + warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") + .arg(certificate.signer, certificate.fingerprint(), reference.path)); + } + auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); + auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); + auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); + auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); + warning.setDefaultButton(untrustedOnce); + warning.exec(); + if (warning.clickedButton() == trustedForever){ + return {TrustedForever, certificate }; + } + if (warning.clickedButton() == trustedOnce){ + return {TrustedOnce, certificate}; + } + if (warning.clickedButton() == untrustedOnce){ + return {UntrustedOnce, certificate }; + } + if (warning.clickedButton() == untrustedForever){ + return {UntrustedForever, certificate }; + } + return {UntrustedOnce, certificate }; } } // End Namespace ShareObserver::ShareObserver(QSharedPointer db, QObject* parent) - : QObject(parent) - , m_db(std::move(db)) - , m_fileWatcher(new BulkFileWatcher(this)) + : QObject(parent) + , m_db(std::move(db)) + , m_fileWatcher(new BulkFileWatcher(this)) { - connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); + connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); - connect(m_db.data(), SIGNAL(databaseModified()), SLOT(handleDatabaseChanged())); - connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved())); + connect(m_db.data(), SIGNAL(databaseModified()), SLOT(handleDatabaseChanged())); + connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved())); - connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileUpdated(QString))); - connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileUpdated(QString))); - connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), SLOT(handleFileUpdated(QString))); - const auto active = KeeShare::active(); - if (!active.in && !active.out) { - deinitialize(); - } else { - reinitialize(); - } + const auto active = KeeShare::active(); + if (!active.in && !active.out) { + deinitialize(); + } else { + reinitialize(); + } } ShareObserver::~ShareObserver() @@ -174,604 +174,604 @@ ShareObserver::~ShareObserver() void ShareObserver::deinitialize() { - m_fileWatcher->clear(); - m_groupToReference.clear(); - m_referenceToGroup.clear(); + m_fileWatcher->clear(); + m_groupToReference.clear(); + m_referenceToGroup.clear(); } void ShareObserver::reinitialize() { - struct Update - { - Group* group; - KeeShareSettings::Reference oldReference; - KeeShareSettings::Reference newReference; - }; + struct Update + { + Group* group; + KeeShareSettings::Reference oldReference; + KeeShareSettings::Reference newReference; + }; - const auto active = KeeShare::active(); - QList updated; - QList groups = m_db->rootGroup()->groupsRecursive(true); - for (Group* group : groups) { - Update couple{group, m_groupToReference.value(group), KeeShare::referenceOf(group)}; - if (couple.oldReference == couple.newReference) { - continue; - } + const auto active = KeeShare::active(); + QList updated; + QList groups = m_db->rootGroup()->groupsRecursive(true); + for (Group* group : groups) { + Update couple{group, m_groupToReference.value(group), KeeShare::referenceOf(group)}; + if (couple.oldReference == couple.newReference) { + continue; + } - m_groupToReference.remove(couple.group); - m_referenceToGroup.remove(couple.oldReference); - m_shareToGroup.remove(couple.oldReference.path); - if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting()) - || (active.out && couple.newReference.isExporting()))) { - m_groupToReference[couple.group] = couple.newReference; - m_referenceToGroup[couple.newReference] = couple.group; - m_shareToGroup[couple.newReference.path] = couple.group; - } - updated << couple; - } + m_groupToReference.remove(couple.group); + m_referenceToGroup.remove(couple.oldReference); + m_shareToGroup.remove(couple.oldReference.path); + if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting()) + || (active.out && couple.newReference.isExporting()))) { + m_groupToReference[couple.group] = couple.newReference; + m_referenceToGroup[couple.newReference] = couple.group; + m_shareToGroup[couple.newReference.path] = couple.group; + } + updated << couple; + } - QStringList success; - QStringList warning; - QStringList error; - for (const auto& update : updated) { - if (!update.oldReference.path.isEmpty()) { - m_fileWatcher->removePath(update.oldReference.path); - } + QStringList success; + QStringList warning; + QStringList error; + for (const auto& update : updated) { + if (!update.oldReference.path.isEmpty()) { + m_fileWatcher->removePath(update.oldReference.path); + } - if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { - m_fileWatcher->addPath(update.newReference.path); - } + if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { + m_fileWatcher->addPath(update.newReference.path); + } - if (update.newReference.isImporting()) { - const auto result = this->importFromReferenceContainer(update.newReference.path); - if (!result.isValid()) { - // tolerable result - blocked import or missing source - continue; - } + if (update.newReference.isImporting()) { + const auto result = this->importFromReferenceContainer(update.newReference.path); + if (!result.isValid()) { + // tolerable result - blocked import or missing source + continue; + } - if (result.isError()) { - error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Imported from %1").arg(result.path); - } - } - } + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + } + } - notifyAbout(success, warning, error); + notifyAbout(success, warning, error); } void ShareObserver::notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error) { - if (error.isEmpty() && warning.isEmpty() && success.isEmpty()) { - return; - } + if (error.isEmpty() && warning.isEmpty() && success.isEmpty()) { + return; + } - MessageWidget::MessageType type = MessageWidget::Positive; - if (!warning.isEmpty()) { - type = MessageWidget::Warning; - } - if (!error.isEmpty()) { - type = MessageWidget::Error; - } - emit sharingMessage((success + warning + error).join("\n"), type); + MessageWidget::MessageType type = MessageWidget::Positive; + if (!warning.isEmpty()) { + type = MessageWidget::Warning; + } + if (!error.isEmpty()) { + type = MessageWidget::Error; + } + emit sharingMessage((success + warning + error).join("\n"), type); } void ShareObserver::handleDatabaseChanged() { - if (!m_db) { - Q_ASSERT(m_db); - return; - } - const auto active = KeeShare::active(); - if (!active.out && !active.in) { - deinitialize(); - } else { - reinitialize(); - } + if (!m_db) { + Q_ASSERT(m_db); + return; + } + const auto active = KeeShare::active(); + if (!active.out && !active.in) { + deinitialize(); + } else { + reinitialize(); + } } void ShareObserver::handleFileUpdated(const QString& path) { - const Result result = this->importFromReferenceContainer(path); - if (!result.isValid()) { - return; - } - QStringList success; - QStringList warning; - QStringList error; - if (result.isError()) { - error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Imported from %1").arg(result.path); - } - notifyAbout(success, warning, error); + const Result result = this->importFromReferenceContainer(path); + if (!result.isValid()) { + return; + } + QStringList success; + QStringList warning; + QStringList error; + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + notifyAbout(success, warning, error); } -ShareObserver::Result ShareObserver::importSecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { #if !defined(WITH_XC_KEESHARE_SECURE) - Q_UNUSED(targetGroup); - return { reference.path, Result::Warning, tr("Secured share container are not supported - import prevented") }; + Q_UNUSED(targetGroup); + return { reference.path, Result::Warning, tr("Signed share container are not supported - import prevented") }; #else - QuaZip zip(reference.path); - if (!zip.open(QuaZip::mdUnzip)) { - qCritical("Unable to open file %s.", qPrintable(reference.path)); - return {reference.path, Result::Error, tr("File is not readable")}; - } - const auto expected = QSet() << KeeShare_Signature << KeeShare_Container; - const auto files = zip.getFileInfoList(); - QSet actual; - for (const auto& file : files) { - actual << file.name; - } - if (expected != actual) { - qCritical("Invalid sharing container %s.", qPrintable(reference.path)); - return {reference.path, Result::Error, tr("Invalid sharing container")}; - } + QuaZip zip(reference.path); + if (!zip.open(QuaZip::mdUnzip)) { + qCritical("Unable to open file %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("File is not readable")}; + } + const auto expected = QSet() << KeeShare_Signature << KeeShare_Container; + const auto files = zip.getFileInfoList(); + QSet actual; + for (const auto& file : files) { + actual << file.name; + } + if (expected != actual) { + qCritical("Invalid sharing container %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("Invalid sharing container")}; + } - zip.setCurrentFile(KeeShare_Signature); - QuaZipFile signatureFile(&zip); - signatureFile.open(QuaZipFile::ReadOnly); - QTextStream stream(&signatureFile); + zip.setCurrentFile(KeeShare_Signature); + QuaZipFile signatureFile(&zip); + signatureFile.open(QuaZipFile::ReadOnly); + QTextStream stream(&signatureFile); - const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll()); - signatureFile.close(); + const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll()); + signatureFile.close(); - zip.setCurrentFile(KeeShare_Container); - QuaZipFile databaseFile(&zip); - databaseFile.open(QuaZipFile::ReadOnly); - auto payload = databaseFile.readAll(); - databaseFile.close(); - QBuffer buffer(&payload); - buffer.open(QIODevice::ReadOnly); + zip.setCurrentFile(KeeShare_Container); + QuaZipFile databaseFile(&zip); + databaseFile.open(QuaZipFile::ReadOnly); + auto payload = databaseFile.readAll(); + databaseFile.close(); + QBuffer buffer(&payload); + buffer.open(QIODevice::ReadOnly); - KeePass2Reader reader; - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(reference.password)); - auto sourceDb = QSharedPointer::create(); - if (!reader.readDatabase(&buffer, key, sourceDb.data())) { - qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); - return {reference.path, Result::Error, reader.errorString()}; - } + KeePass2Reader reader; + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + auto sourceDb = QSharedPointer::create(); + if (!reader.readDatabase(&buffer, key, sourceDb.data())) { + qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); + return {reference.path, Result::Error, reader.errorString()}; + } - auto foreign = KeeShare::foreign(); - auto own = KeeShare::own(); - auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); - switch (trust.first) { - case Invalid: - qWarning("Prevent untrusted import"); - return {reference.path, Result::Error, tr("Untrusted import prevented")}; + auto foreign = KeeShare::foreign(); + auto own = KeeShare::own(); + auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); + switch (trust.first) { + case Invalid: + qWarning("Prevent untrusted import"); + return {reference.path, Result::Error, tr("Untrusted import prevented")}; - case UntrustedForever: - case TrustedForever: { - bool found = false; - const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; - for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { - if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { - scopedCertificate.certificate.signer = trust.second.signer; - scopedCertificate.path = reference.path; - scopedCertificate.trust = trusted; - found = true; - } - } - if (!found) { - foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; - // we need to update with the new signer - KeeShare::setForeign(foreign); - } - if (trust.first == TrustedForever) { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful secured import")}; - } - } - // Silent ignore of untrusted import or unchanging import - return {}; - } - case TrustedOnce: - case Own: { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful secured import")}; - } - return {}; - } - default: - Q_ASSERT(false); - return {reference.path, Result::Error, tr("Unexpected error")}; - } + case UntrustedForever: + case TrustedForever: { + bool found = false; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { + if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trust = trusted; + found = true; + } + } + if (!found) { + foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; + // we need to update with the new signer + KeeShare::setForeign(foreign); + } + if (trust.first == TrustedForever) { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + } + // Silent ignore of untrusted import or unchanging import + return {}; + } + case TrustedOnce: + case Own: { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + return {}; + } + default: + Q_ASSERT(false); + return {reference.path, Result::Error, tr("Unexpected error")}; + } #endif } -ShareObserver::Result ShareObserver::importInsecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { #if !defined(WITH_XC_KEESHARE_INSECURE) - Q_UNUSED(targetGroup); - return {reference.path, Result::Warning, tr("Insecured share container are not supported - import prevented")}; + Q_UNUSED(targetGroup); + return {reference.path, Result::Warning, tr("Unsigned share container are not supported - import prevented")}; #else - QFile file(reference.path); - if (!file.open(QIODevice::ReadOnly)){ - qCritical("Unable to open file %s.", qPrintable(reference.path)); - return {reference.path, Result::Error, tr("File is not readable")}; - } - auto payload = file.readAll(); - file.close(); - QBuffer buffer(&payload); - buffer.open(QIODevice::ReadOnly); + QFile file(reference.path); + if (!file.open(QIODevice::ReadOnly)){ + qCritical("Unable to open file %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("File is not readable")}; + } + auto payload = file.readAll(); + file.close(); + QBuffer buffer(&payload); + buffer.open(QIODevice::ReadOnly); - KeePass2Reader reader; - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(reference.password)); - auto sourceDb = QSharedPointer::create(); - if (!reader.readDatabase(&buffer, key, sourceDb.data())) { - qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); - return {reference.path, Result::Error, reader.errorString()}; - } + KeePass2Reader reader; + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + auto sourceDb = QSharedPointer::create(); + if (!reader.readDatabase(&buffer, key, sourceDb.data())) { + qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); + return {reference.path, Result::Error, reader.errorString()}; + } - auto foreign = KeeShare::foreign(); - const auto own = KeeShare::own(); - const auto sign = KeeShareSettings::Sign(); // invalid sign - auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); - switch(trust.first) { - case UntrustedForever: - case TrustedForever: { - bool found = false; - const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; - for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { - if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { - scopedCertificate.certificate.signer = trust.second.signer; - scopedCertificate.path = reference.path; - scopedCertificate.trust = trusted; - found = true; - } - } - if (!found) { - foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; - // we need to update with the new signer - KeeShare::setForeign(foreign); - } - if (trust.first == TrustedForever) { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful secured import")}; - } - } - return {}; - } + auto foreign = KeeShare::foreign(); + const auto own = KeeShare::own(); + const auto sign = KeeShareSettings::Sign(); // invalid sign + auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); + switch(trust.first) { + case UntrustedForever: + case TrustedForever: { + bool found = false; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { + if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trust = trusted; + found = true; + } + } + if (!found) { + foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; + // we need to update with the new signer + KeeShare::setForeign(foreign); + } + if (trust.first == TrustedForever) { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + } + return {}; + } - case TrustedOnce: { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful unsecured import")}; - } - return {}; - } - default: - qWarning("Prevent untrusted import"); - return {reference.path, Result::Warning, tr("Untrusted import prevented")}; - } + case TrustedOnce: { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful unsigned import")}; + } + return {}; + } + default: + qWarning("Prevent untrusted import"); + return {reference.path, Result::Warning, tr("Untrusted import prevented")}; + } #endif } ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { - const QFileInfo info(reference.path); - if (!info.exists()) { - qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); - return {reference.path, Result::Warning, tr("File does not exist")}; - } + const QFileInfo info(reference.path); + if (!info.exists()) { + qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); + return {reference.path, Result::Warning, tr("File does not exist")}; + } - if (isOfExportType(info, KeeShare::secureContainerFileType())) { - return importSecureContainerInto(reference, targetGroup); - } - if (isOfExportType(info, KeeShare::insecureContainerFileType())) { - return importInsecureContainerInto(reference, targetGroup); - } - return {reference.path, Result::Error, tr("Unknown share container type")}; + if (isOfExportType(info, KeeShare::signedContainerFileType())) { + return importSingedContainerInto(reference, targetGroup); + } + if (isOfExportType(info, KeeShare::unsignedContainerFileType())) { + return importUnsignedContainerInto(reference, targetGroup); + } + return {reference.path, Result::Error, tr("Unknown share container type")}; } ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString& path) { - if (!KeeShare::active().in) { - return {}; - } - auto shareGroup = m_shareToGroup.value(path); - if (!shareGroup) { - qWarning("Source for %s does not exist", qPrintable(path)); - Q_ASSERT(shareGroup); - return {}; - } - const auto reference = KeeShare::referenceOf(shareGroup); - if (reference.type == KeeShareSettings::Inactive) { - qDebug("Ignore change of inactive reference %s", qPrintable(reference.path)); - return {}; - } - if (reference.type == KeeShareSettings::ExportTo) { - qDebug("Ignore change of export reference %s", qPrintable(reference.path)); - return {}; - } - Q_ASSERT(shareGroup->database() == m_db); - Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid())); - return importContainerInto(reference, shareGroup); + if (!KeeShare::active().in) { + return {}; + } + auto shareGroup = m_shareToGroup.value(path); + if (!shareGroup) { + qWarning("Source for %s does not exist", qPrintable(path)); + Q_ASSERT(shareGroup); + return {}; + } + const auto reference = KeeShare::referenceOf(shareGroup); + if (reference.type == KeeShareSettings::Inactive) { + qDebug("Ignore change of inactive reference %s", qPrintable(reference.path)); + return {}; + } + if (reference.type == KeeShareSettings::ExportTo) { + qDebug("Ignore change of export reference %s", qPrintable(reference.path)); + return {}; + } + Q_ASSERT(shareGroup->database() == m_db); + Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid())); + return importContainerInto(reference, shareGroup); } void ShareObserver::resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb) { - for (const auto& attribute : EntryAttributes::DefaultAttributes) { - const auto standardValue = targetEntry->attributes()->value(attribute); - const auto type = targetEntry->placeholderType(standardValue); - if (type != Entry::PlaceholderType::Reference) { - // No reference to resolve - continue; - } - const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue); - if (referencedTargetEntry) { - // References is within scope, no resolving needed - continue; - } - // We could do more sophisticated **** trying to point the reference to the next in-scope reference - // but those cases with high propability constructed examples and very rare in real usage - const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid()); - const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue); - targetEntry->setUpdateTimeinfo(false); - targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute)); - targetEntry->setUpdateTimeinfo(true); - } + for (const auto& attribute : EntryAttributes::DefaultAttributes) { + const auto standardValue = targetEntry->attributes()->value(attribute); + const auto type = targetEntry->placeholderType(standardValue); + if (type != Entry::PlaceholderType::Reference) { + // No reference to resolve + continue; + } + const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue); + if (referencedTargetEntry) { + // References is within scope, no resolving needed + continue; + } + // We could do more sophisticated **** trying to point the reference to the next in-scope reference + // but those cases with high propability constructed examples and very rare in real usage + const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid()); + const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue); + targetEntry->setUpdateTimeinfo(false); + targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute)); + targetEntry->setUpdateTimeinfo(true); + } } Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot) { - const auto* sourceDb = sourceRoot->database(); - auto* targetDb = new Database(); - targetDb->metadata()->setRecycleBinEnabled(false); - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(reference.password)); + const auto* sourceDb = sourceRoot->database(); + auto* targetDb = new Database(); + targetDb->metadata()->setRecycleBinEnabled(false); + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); - // Copy the source root as the root of the export database, memory manage the old root node - auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags); - const bool updateTimeinfo = targetRoot->canUpdateTimeinfo(); - targetRoot->setUpdateTimeinfo(false); - KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference()); - targetRoot->setUpdateTimeinfo(updateTimeinfo); - const auto sourceEntries = sourceRoot->entriesRecursive(false); - for (const Entry* sourceEntry : sourceEntries) { - auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); - const bool updateTimeinfo = targetEntry->canUpdateTimeinfo(); - targetEntry->setUpdateTimeinfo(false); - targetEntry->setGroup(targetRoot); - targetEntry->setUpdateTimeinfo(updateTimeinfo); - const auto iconUuid = targetEntry->iconUuid(); - if (!iconUuid.isNull()) { - targetDb->metadata()->addCustomIcon(iconUuid, sourceEntry->icon()); - } - } + // Copy the source root as the root of the export database, memory manage the old root node + auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags); + const bool updateTimeinfo = targetRoot->canUpdateTimeinfo(); + targetRoot->setUpdateTimeinfo(false); + KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference()); + targetRoot->setUpdateTimeinfo(updateTimeinfo); + const auto sourceEntries = sourceRoot->entriesRecursive(false); + for (const Entry* sourceEntry : sourceEntries) { + auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); + const bool updateTimeinfo = targetEntry->canUpdateTimeinfo(); + targetEntry->setUpdateTimeinfo(false); + targetEntry->setGroup(targetRoot); + targetEntry->setUpdateTimeinfo(updateTimeinfo); + const auto iconUuid = targetEntry->iconUuid(); + if (!iconUuid.isNull()) { + targetDb->metadata()->addCustomIcon(iconUuid, sourceEntry->icon()); + } + } - targetDb->setKey(key); - auto* obsoleteRoot = targetDb->rootGroup(); - targetDb->setRootGroup(targetRoot); - delete obsoleteRoot; + targetDb->setKey(key); + auto* obsoleteRoot = targetDb->rootGroup(); + targetDb->setRootGroup(targetRoot); + delete obsoleteRoot; - targetDb->metadata()->setName(sourceRoot->name()); + targetDb->metadata()->setName(sourceRoot->name()); - // Push all deletions of the source database to the target - // simple moving out of a share group will not trigger a deletion in the - // target - a more elaborate mechanism may need the use of another custom - // attribute to share unshared entries from the target db - for (const auto& object : sourceDb->deletedObjects()) { - targetDb->addDeletedObject(object); - } - for (auto* targetEntry : targetRoot->entriesRecursive(false)) { - if (targetEntry->hasReferences()) { - resolveReferenceAttributes(targetEntry, sourceDb); - } - } - return targetDb; + // Push all deletions of the source database to the target + // simple moving out of a share group will not trigger a deletion in the + // target - a more elaborate mechanism may need the use of another custom + // attribute to share unshared entries from the target db + for (const auto& object : sourceDb->deletedObjects()) { + targetDb->addDeletedObject(object); + } + for (auto* targetEntry : targetRoot->entriesRecursive(false)) { + if (targetEntry->hasReferences()) { + resolveReferenceAttributes(targetEntry, sourceDb); + } + } + return targetDb; } QSharedPointer ShareObserver::database() { - return m_db; + return m_db; } -ShareObserver::Result ShareObserver::exportIntoReferenceSecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb) +ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb) { #if !defined(WITH_XC_KEESHARE_SECURE) - Q_UNUSED(targetDb); - return {reference.path, Result::Warning, tr("Overwriting secured share container is not supported - export prevented")}; + Q_UNUSED(targetDb); + return {reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")}; #else - QByteArray bytes; - { - QBuffer buffer(&bytes); - buffer.open(QIODevice::WriteOnly); - KeePass2Writer writer; - writer.writeDatabase(&buffer, targetDb); - if (writer.hasError()) { - qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); - return {reference.path, Result::Error, writer.errorString()}; - } - } - const auto own = KeeShare::own(); - QuaZip zip(reference.path); - zip.setFileNameCodec("UTF-8"); - const bool zipOpened = zip.open(QuaZip::mdCreate); - if (!zipOpened) { - ::qWarning("Opening export file failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; - } - { - QuaZipFile file(&zip); - const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); - if (!signatureOpened) { - ::qWarning("Embedding signature failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; - } - QTextStream stream(&file); - KeeShareSettings::Sign sign; - auto sshKey = own.key.sshKey(); - sshKey.openKey(QString()); - const Signature signer; - sign.signature = signer.create(bytes, sshKey); - sign.certificate = own.certificate; - stream << KeeShareSettings::Sign::serialize(sign); - stream.flush(); - if (file.getZipError() != ZIP_OK) { - ::qWarning("Embedding signature failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; - } - file.close(); - } - { - QuaZipFile file(&zip); - const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); - if (!dbOpened) { - ::qWarning("Embedding database failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; - } - if (file.getZipError() != ZIP_OK) { - ::qWarning("Embedding database failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; - } - file.write(bytes); - file.close(); - } - zip.close(); - return {reference.path}; + QByteArray bytes; + { + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + KeePass2Writer writer; + writer.writeDatabase(&buffer, targetDb); + if (writer.hasError()) { + qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); + return {reference.path, Result::Error, writer.errorString()}; + } + } + const auto own = KeeShare::own(); + QuaZip zip(reference.path); + zip.setFileNameCodec("UTF-8"); + const bool zipOpened = zip.open(QuaZip::mdCreate); + if (!zipOpened) { + ::qWarning("Opening export file failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; + } + { + QuaZipFile file(&zip); + const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); + if (!signatureOpened) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + } + QTextStream stream(&file); + KeeShareSettings::Sign sign; + auto sshKey = own.key.sshKey(); + sshKey.openKey(QString()); + const Signature signer; + sign.signature = signer.create(bytes, sshKey); + sign.certificate = own.certificate; + stream << KeeShareSettings::Sign::serialize(sign); + stream.flush(); + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + } + file.close(); + } + { + QuaZipFile file(&zip); + const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); + if (!dbOpened) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + } + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + } + file.write(bytes); + file.close(); + } + zip.close(); + return {reference.path}; #endif } -ShareObserver::Result ShareObserver::exportIntoReferenceInsecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb) +ShareObserver::Result ShareObserver::exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb) { #if !defined(WITH_XC_KEESHARE_INSECURE) - Q_UNUSED(targetDb); - return {reference.path, Result::Warning, tr("Overwriting secured share container is not supported - export prevented")}; + Q_UNUSED(targetDb); + return {reference.path, Result::Warning, tr("Overwriting unsigned share container is not supported - export prevented")}; #else - QFile file(reference.path); - const bool fileOpened = file.open(QIODevice::WriteOnly); - if (!fileOpened) { - ::qWarning("Opening export file failed"); - return {reference.path, Result::Error, tr("Could not write export container")}; - } - KeePass2Writer writer; - writer.writeDatabase(&file, targetDb); - if (writer.hasError()) { - qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data()); - return {reference.path, Result::Error, writer.errorString()}; - } - file.close(); + QFile file(reference.path); + const bool fileOpened = file.open(QIODevice::WriteOnly); + if (!fileOpened) { + ::qWarning("Opening export file failed"); + return {reference.path, Result::Error, tr("Could not write export container")}; + } + KeePass2Writer writer; + writer.writeDatabase(&file, targetDb); + if (writer.hasError()) { + qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data()); + return {reference.path, Result::Error, writer.errorString()}; + } + file.close(); #endif - return {reference.path}; + return {reference.path}; } QList ShareObserver::exportIntoReferenceContainers() { - QList results; - const auto groups = m_db->rootGroup()->groupsRecursive(true); - for (const auto* group : groups) { - const auto reference = KeeShare::referenceOf(group); - if (!reference.isExporting()) { - continue; - } + QList results; + const auto groups = m_db->rootGroup()->groupsRecursive(true); + for (const auto* group : groups) { + const auto reference = KeeShare::referenceOf(group); + if (!reference.isExporting()) { + continue; + } - m_fileWatcher->ignoreFileChanges(reference.path); - QScopedPointer targetDb(exportIntoContainer(reference, group)); - QFileInfo info(reference.path); - if (isOfExportType(info, KeeShare::secureContainerFileType())) { - results << exportIntoReferenceSecureContainer(reference, targetDb.data()); - m_fileWatcher->observeFileChanges(true); - continue; - } - if (isOfExportType(info, KeeShare::insecureContainerFileType())) { - results << exportIntoReferenceInsecureContainer(reference, targetDb.data()); - m_fileWatcher->observeFileChanges(true); - continue; - } - Q_ASSERT(false); - results << Result{reference.path, Result::Error, tr("Unexpected export error occurred")}; - } - return results; + m_fileWatcher->ignoreFileChanges(reference.path); + QScopedPointer targetDb(exportIntoContainer(reference, group)); + QFileInfo info(reference.path); + if (isOfExportType(info, KeeShare::signedContainerFileType())) { + results << exportIntoReferenceSignedContainer(reference, targetDb.data()); + m_fileWatcher->observeFileChanges(true); + continue; + } + if (isOfExportType(info, KeeShare::unsignedContainerFileType())) { + results << exportIntoReferenceUnsignedContainer(reference, targetDb.data()); + m_fileWatcher->observeFileChanges(true); + continue; + } + Q_ASSERT(false); + results << Result{reference.path, Result::Error, tr("Unexpected export error occurred")}; + } + return results; } void ShareObserver::handleDatabaseSaved() { - if (!KeeShare::active().out) { - return; - } - QStringList error; - QStringList warning; - QStringList success; - const auto results = exportIntoReferenceContainers(); - for (const Result& result : results) { - if (!result.isValid()) { - Q_ASSERT(result.isValid()); - continue; - } - if (result.isError()) { - error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Export to %1").arg(result.path); - } - } - notifyAbout(success, warning, error); + if (!KeeShare::active().out) { + return; + } + QStringList error; + QStringList warning; + QStringList success; + const auto results = exportIntoReferenceContainers(); + for (const Result& result : results) { + if (!result.isValid()) { + Q_ASSERT(result.isValid()); + continue; + } + if (result.isError()) { + error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Export to %1").arg(result.path); + } + } + notifyAbout(success, warning, error); } ShareObserver::Result::Result(const QString& path, ShareObserver::Result::Type type, const QString& message) - : path(path) - , type(type) - , message(message) + : path(path) + , type(type) + , message(message) { } bool ShareObserver::Result::isValid() const { - return !path.isEmpty() || !message.isEmpty() || !message.isEmpty() || !message.isEmpty(); + return !path.isEmpty() || !message.isEmpty() || !message.isEmpty() || !message.isEmpty(); } bool ShareObserver::Result::isError() const { - return !message.isEmpty() && type == Error; + return !message.isEmpty() && type == Error; } bool ShareObserver::Result::isInfo() const { - return !message.isEmpty() && type == Info; + return !message.isEmpty() && type == Info; } bool ShareObserver::Result::isWarning() const { - return !message.isEmpty() && type == Warning; + return !message.isEmpty() && type == Warning; } diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h index b21934389..26f010813 100644 --- a/src/keeshare/ShareObserver.h +++ b/src/keeshare/ShareObserver.h @@ -77,10 +77,10 @@ private: static void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb); static Database* exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot); - static Result exportIntoReferenceInsecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb); - static Result exportIntoReferenceSecureContainer(const KeeShareSettings::Reference &reference, Database *targetDb); - static Result importSecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); - static Result importInsecureContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); + static Result exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb); + static Result exportIntoReferenceSignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb); + static Result importSingedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); + static Result importUnsignedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); static Result importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); static Result importDatabaseInto(); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index 63f4182af..eecdaee21 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -31,54 +31,54 @@ #include EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) - : QWidget(parent) - , m_ui(new Ui::EditGroupWidgetKeeShare()) + : QWidget(parent) + , m_ui(new Ui::EditGroupWidgetKeeShare()) { - m_ui->setupUi(this); + m_ui->setupUi(this); - m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); - m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); + m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); + m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); - m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); - m_ui->passwordGenerator->hide(); - m_ui->passwordGenerator->reset(); + m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); + m_ui->passwordGenerator->hide(); + m_ui->passwordGenerator->reset(); - m_ui->messageWidget->hide(); - m_ui->messageWidget->setCloseButtonVisible(false); - m_ui->messageWidget->setAutoHideTimeout(-1); + m_ui->messageWidget->hide(); + m_ui->messageWidget->setCloseButtonVisible(false); + m_ui->messageWidget->setAutoHideTimeout(-1); - connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); - connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); - connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); - connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); - connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); - connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); - connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); + connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); + connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); + connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); + connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); + connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); + connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); + connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); - connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); + connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); - const auto types = QList() << KeeShareSettings::Inactive - << KeeShareSettings::ImportFrom - << KeeShareSettings::ExportTo - << KeeShareSettings::SynchronizeWith; - for (const auto& type : types) { - QString name; - switch (type) { - case KeeShareSettings::Inactive: - name = tr("Inactive"); - break; - case KeeShareSettings::ImportFrom: - name = tr("Import from path"); - break; - case KeeShareSettings::ExportTo: - name = tr("Export to path"); - break; - case KeeShareSettings::SynchronizeWith: - name = tr("Synchronize with path"); - break; - } - m_ui->typeComboBox->insertItem(type, name, static_cast(type)); - } + const auto types = QList() << KeeShareSettings::Inactive + << KeeShareSettings::ImportFrom + << KeeShareSettings::ExportTo + << KeeShareSettings::SynchronizeWith; + for (const auto& type : types) { + QString name; + switch (type) { + case KeeShareSettings::Inactive: + name = tr("Inactive"); + break; + case KeeShareSettings::ImportFrom: + name = tr("Import from path"); + break; + case KeeShareSettings::ExportTo: + name = tr("Export to path"); + break; + case KeeShareSettings::SynchronizeWith: + name = tr("Synchronize with path"); + break; + } + m_ui->typeComboBox->insertItem(type, name, static_cast(type)); + } } EditGroupWidgetKeeShare::~EditGroupWidgetKeeShare() @@ -87,198 +87,198 @@ EditGroupWidgetKeeShare::~EditGroupWidgetKeeShare() void EditGroupWidgetKeeShare::setGroup(Group* temporaryGroup) { - if (m_temporaryGroup) { - m_temporaryGroup->disconnect(this); - } + if (m_temporaryGroup) { + m_temporaryGroup->disconnect(this); + } - m_temporaryGroup = temporaryGroup; + m_temporaryGroup = temporaryGroup; - if (m_temporaryGroup) { - connect(m_temporaryGroup, SIGNAL(groupModified()), SLOT(update())); - } + if (m_temporaryGroup) { + connect(m_temporaryGroup, SIGNAL(groupModified()), SLOT(update())); + } - update(); + update(); } void EditGroupWidgetKeeShare::showSharingState() { - if (!m_temporaryGroup) { - return; - } + if (!m_temporaryGroup) { + return; + } - auto supportedExtensions = QStringList(); + auto supportedExtensions = QStringList(); #if defined(WITH_XC_KEESHARE_INSECURE) - supportedExtensions << KeeShare::insecureContainerFileType(); + supportedExtensions << KeeShare::unsignedContainerFileType(); #endif #if defined(WITH_XC_KEESHARE_SECURE) - supportedExtensions << KeeShare::secureContainerFileType(); + supportedExtensions << KeeShare::signedContainerFileType(); #endif - const auto reference = KeeShare::referenceOf(m_temporaryGroup); - if (!reference.path.isEmpty()) { - bool supported = false; - for(const auto &extension : supportedExtensions){ - if (reference.path.endsWith(extension, Qt::CaseInsensitive)){ - supported = true; - break; - } - } - if (!supported) { - m_ui->messageWidget->showMessage( - tr("Your KeePassXC version does not support sharing your container type. Please use %1.") - .arg(supportedExtensions.join(", ")), - MessageWidget::Warning); - return; - } - } - const auto active = KeeShare::active(); - if (!active.in && !active.out) { - m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information); - return; - } - if (active.in && !active.out) { - m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information); - return; - } - if (!active.in && active.out) { - m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information); - return; - } + const auto reference = KeeShare::referenceOf(m_temporaryGroup); + if (!reference.path.isEmpty()) { + bool supported = false; + for(const auto &extension : supportedExtensions){ + if (reference.path.endsWith(extension, Qt::CaseInsensitive)){ + supported = true; + break; + } + } + if (!supported) { + m_ui->messageWidget->showMessage( + tr("Your KeePassXC version does not support sharing your container type. Please use %1.") + .arg(supportedExtensions.join(", ")), + MessageWidget::Warning); + return; + } + } + const auto active = KeeShare::active(); + if (!active.in && !active.out) { + m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information); + return; + } + if (active.in && !active.out) { + m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information); + return; + } + if (!active.in && active.out) { + m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information); + return; + } } void EditGroupWidgetKeeShare::update() { - if (!m_temporaryGroup) { - m_ui->passwordEdit->clear(); - m_ui->pathEdit->clear(); - } else { - const auto reference = KeeShare::referenceOf(m_temporaryGroup); + if (!m_temporaryGroup) { + m_ui->passwordEdit->clear(); + m_ui->pathEdit->clear(); + } else { + const auto reference = KeeShare::referenceOf(m_temporaryGroup); - m_ui->typeComboBox->setCurrentIndex(reference.type); - m_ui->passwordEdit->setText(reference.password); - m_ui->pathEdit->setText(reference.path); + m_ui->typeComboBox->setCurrentIndex(reference.type); + m_ui->passwordEdit->setText(reference.password); + m_ui->pathEdit->setText(reference.path); - showSharingState(); - } + showSharingState(); + } - m_ui->passwordGenerator->hide(); - m_ui->togglePasswordGeneratorButton->setChecked(false); - m_ui->togglePasswordButton->setChecked(false); + m_ui->passwordGenerator->hide(); + m_ui->togglePasswordGeneratorButton->setChecked(false); + m_ui->togglePasswordButton->setChecked(false); } void EditGroupWidgetKeeShare::togglePasswordGeneratorButton(bool checked) { - m_ui->passwordGenerator->regeneratePassword(); - m_ui->passwordGenerator->setVisible(checked); + m_ui->passwordGenerator->regeneratePassword(); + m_ui->passwordGenerator->setVisible(checked); } void EditGroupWidgetKeeShare::setGeneratedPassword(const QString& password) { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.password = password; - KeeShare::setReferenceTo(m_temporaryGroup, reference); - m_ui->togglePasswordGeneratorButton->setChecked(false); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.password = password; + KeeShare::setReferenceTo(m_temporaryGroup, reference); + m_ui->togglePasswordGeneratorButton->setChecked(false); } void EditGroupWidgetKeeShare::selectPath() { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.path = m_ui->pathEdit->text(); - KeeShare::setReferenceTo(m_temporaryGroup, reference); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.path = m_ui->pathEdit->text(); + KeeShare::setReferenceTo(m_temporaryGroup, reference); } void EditGroupWidgetKeeShare::launchPathSelectionDialog() { - if (!m_temporaryGroup) { - return; - } - QString defaultDirPath = config()->get("KeeShare/LastShareDir").toString(); - const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); - if (!dirExists) { - defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - QString defaultFiletype = ""; - auto supportedExtensions = QStringList(); - auto unsupportedExtensions = QStringList(); - auto knownFilters = QStringList() << QString("%1 (*)").arg("All files"); + if (!m_temporaryGroup) { + return; + } + QString defaultDirPath = config()->get("KeeShare/LastShareDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + QString defaultFiletype = ""; + auto supportedExtensions = QStringList(); + auto unsupportedExtensions = QStringList(); + auto knownFilters = QStringList() << QString("%1 (*)").arg("All files"); #if defined(WITH_XC_KEESHARE_INSECURE) - defaultFiletype = KeeShare::insecureContainerFileType(); - supportedExtensions << KeeShare::insecureContainerFileType(); - knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare insecure container"), KeeShare::insecureContainerFileType())); + defaultFiletype = KeeShare::unsignedContainerFileType(); + supportedExtensions << KeeShare::unsignedContainerFileType(); + knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare unsigned container"), KeeShare::unsignedContainerFileType())); #else - unsupportedExtensions << KeeShare::insecureContainerFileType(); + unsupportedExtensions << KeeShare::unsignedContainerFileType(); #endif #if defined(WITH_XC_KEESHARE_SECURE) - defaultFiletype = KeeShare::secureContainerFileType(); - supportedExtensions << KeeShare::secureContainerFileType(); - knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare secure container"), KeeShare::secureContainerFileType())); + defaultFiletype = KeeShare::signedContainerFileType(); + supportedExtensions << KeeShare::signedContainerFileType(); + knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare signed container"), KeeShare::signedContainerFileType())); #else - unsupportedExtensions << KeeShare::secureContainerFileType(); + unsupportedExtensions << KeeShare::signedContainerFileType(); #endif - const auto filters = knownFilters.join(";;"); - auto filename = reference.path; - if (filename.isEmpty()) { - filename = m_temporaryGroup->name(); - } - switch (reference.type) { - case KeeShareSettings::ImportFrom: - filename = fileDialog()->getFileName( - this, tr("Select import source"), defaultDirPath, filters, nullptr, QFileDialog::DontConfirmOverwrite, - defaultFiletype, filename); - break; - case KeeShareSettings::ExportTo: - filename = fileDialog()->getFileName( - this, tr("Select export target"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); - break; - case KeeShareSettings::SynchronizeWith: - case KeeShareSettings::Inactive: - filename = fileDialog()->getFileName( - this, tr("Select import/export file"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); - break; - } + const auto filters = knownFilters.join(";;"); + auto filename = reference.path; + if (filename.isEmpty()) { + filename = m_temporaryGroup->name(); + } + switch (reference.type) { + case KeeShareSettings::ImportFrom: + filename = fileDialog()->getFileName( + this, tr("Select import source"), defaultDirPath, filters, nullptr, QFileDialog::DontConfirmOverwrite, + defaultFiletype, filename); + break; + case KeeShareSettings::ExportTo: + filename = fileDialog()->getFileName( + this, tr("Select export target"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); + break; + case KeeShareSettings::SynchronizeWith: + case KeeShareSettings::Inactive: + filename = fileDialog()->getFileName( + this, tr("Select import/export file"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); + break; + } - if (filename.isEmpty()) { - return; - } - bool validFilename = false; - for(const auto& extension : supportedExtensions + unsupportedExtensions){ - if (filename.endsWith(extension, Qt::CaseInsensitive)) { - validFilename = true; - break; - } - } - if (!validFilename){ - filename += (!filename.endsWith(".") ? "." : "") + defaultFiletype; - } + if (filename.isEmpty()) { + return; + } + bool validFilename = false; + for(const auto& extension : supportedExtensions + unsupportedExtensions){ + if (filename.endsWith(extension, Qt::CaseInsensitive)) { + validFilename = true; + break; + } + } + if (!validFilename){ + filename += (!filename.endsWith(".") ? "." : "") + defaultFiletype; + } - m_ui->pathEdit->setText(filename); - selectPath(); - config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); + m_ui->pathEdit->setText(filename); + selectPath(); + config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); } void EditGroupWidgetKeeShare::selectPassword() { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.password = m_ui->passwordEdit->text(); - KeeShare::setReferenceTo(m_temporaryGroup, reference); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.password = m_ui->passwordEdit->text(); + KeeShare::setReferenceTo(m_temporaryGroup, reference); } void EditGroupWidgetKeeShare::selectType() { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.type = static_cast(m_ui->typeComboBox->currentData().toInt()); - KeeShare::setReferenceTo(m_temporaryGroup, reference); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.type = static_cast(m_ui->typeComboBox->currentData().toInt()); + KeeShare::setReferenceTo(m_temporaryGroup, reference); } From c954c95da487b5d9b44faa318187ac50e38640bb Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Wed, 9 Jan 2019 16:25:35 +0100 Subject: [PATCH 19/23] Fixed BulkFileWatcher multi signal problem BulkFileWatcher emitted multiple file change signals (like QFileSystemWatcher) for the watched files. Introduced a delay by waiting until the end of the event loop to aggregate signals emitted by QFileSystemWatcher before emitting custom signals. --- src/core/FileWatcher.cpp | 127 +++- src/core/FileWatcher.h | 19 +- src/keeshare/ShareObserver.cpp | 1160 ++++++++++++++++---------------- src/keeshare/ShareObserver.h | 2 + 4 files changed, 694 insertions(+), 614 deletions(-) diff --git a/src/core/FileWatcher.cpp b/src/core/FileWatcher.cpp index 4873adbf4..d57d7de52 100644 --- a/src/core/FileWatcher.cpp +++ b/src/core/FileWatcher.cpp @@ -37,8 +37,7 @@ DelayingFileWatcher::DelayingFileWatcher(QObject* parent) { connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); connect(&m_fileUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges())); - connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), SIGNAL(fileChanged())); - + connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), this, SIGNAL(fileChanged())); m_fileChangeDelayTimer.setSingleShot(true); m_fileUnblockTimer.setSingleShot(true); } @@ -122,6 +121,7 @@ BulkFileWatcher::BulkFileWatcher(QObject* parent) connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileChanged(QString))); connect(&m_fileWatcher, SIGNAL(directoryChanged(QString)), SLOT(handleDirectoryChanged(QString))); connect(&m_fileWatchUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges())); + connect(&m_pendingSignalsTimer, SIGNAL(timeout()), this, SLOT(emitSignals())); m_fileWatchUnblockTimer.setSingleShot(true); } @@ -132,7 +132,7 @@ void BulkFileWatcher::clear() m_fileWatcher.removePath(info.absoluteFilePath()); m_fileWatcher.removePath(info.absolutePath()); } - m_filePaths.clear(); + m_watchedPaths.clear(); m_watchedFilesInDirectory.clear(); m_ignoreFilesChangess.clear(); } @@ -140,70 +140,127 @@ void BulkFileWatcher::clear() void BulkFileWatcher::removePath(const QString& path) { const QFileInfo info(path); - m_fileWatcher.removePath(info.absoluteFilePath()); - m_fileWatcher.removePath(info.absolutePath()); - m_filePaths.remove(info.absoluteFilePath()); - m_filePaths.remove(info.absolutePath()); - m_watchedFilesInDirectory[info.absolutePath()].remove(info.absoluteFilePath()); + const QString filePath = info.absoluteFilePath(); + const QString directoryPath = info.absolutePath(); + m_watchedFilesInDirectory[directoryPath].remove(filePath); + m_fileWatcher.removePath(filePath); + m_watchedPaths.remove(filePath); + if (m_watchedFilesInDirectory[directoryPath].isEmpty()) { + m_fileWatcher.removePath(directoryPath); + m_watchedPaths.remove(directoryPath); + m_watchedFilesInDirectory.remove(directoryPath); + } } void BulkFileWatcher::addPath(const QString& path) { const QFileInfo info(path); - m_fileWatcher.addPath(info.absoluteFilePath()); - m_fileWatcher.addPath(info.absolutePath()); - m_filePaths.insert(info.absoluteFilePath()); - m_filePaths.insert(info.absolutePath()); - m_watchedFilesInDirectory[info.absolutePath()][info.absoluteFilePath()] = info.exists(); -} - -void BulkFileWatcher::restart(const QString& path) -{ - const QFileInfo info(path); - Q_ASSERT(m_filePaths.contains(info.absoluteFilePath())); - Q_ASSERT(m_filePaths.contains(info.absolutePath())); - m_fileWatcher.addPath(info.absoluteFilePath()); - m_fileWatcher.addPath(info.absolutePath()); + const QString filePath = info.absoluteFilePath(); + const QString directoryPath = info.absolutePath(); + if (!m_watchedPaths.value(filePath)) { + const bool fileSuccess = m_fileWatcher.addPath(filePath); + m_watchedPaths[filePath] = fileSuccess; + } + if (!m_watchedPaths.value(directoryPath)) { + const bool directorySuccess = m_fileWatcher.addPath(directoryPath); + m_watchedPaths[directoryPath] = directorySuccess; + } + m_watchedFilesInDirectory[directoryPath][filePath] = info.exists(); } void BulkFileWatcher::handleFileChanged(const QString& path) { + const QFileInfo info(path); + const QString filePath = info.absoluteFilePath(); + const QString directoryPath = info.absolutePath(); + const QMap& watchedFiles = m_watchedFilesInDirectory[directoryPath]; + const bool created = !watchedFiles[filePath] && info.exists(); + const bool deleted = watchedFiles[filePath] && !info.exists(); + const bool changed = !created && !deleted; addPath(path); - const QFileInfo info(path); if (m_ignoreFilesChangess[info.canonicalFilePath()] > Clock::currentDateTimeUtc()) { // changes are blocked return; } - - emit fileChanged(path); + if (created) { + qDebug("File created %s", qPrintable(path)); + scheduleSignal(Created, filePath); + } + if (changed) { + qDebug("File changed %s", qPrintable(path)); + scheduleSignal(Updated, filePath); + } + if (deleted) { + qDebug("File removed %s", qPrintable(path)); + scheduleSignal(Removed, filePath); + } } void BulkFileWatcher::handleDirectoryChanged(const QString& path) { qDebug("Directory changed %s", qPrintable(path)); const QFileInfo directoryInfo(path); - const QMap& watchedFiles = m_watchedFilesInDirectory[directoryInfo.absoluteFilePath()]; + const QString directoryPath = directoryInfo.absoluteFilePath(); + QMap& watchedFiles = m_watchedFilesInDirectory[directoryPath]; for (const QString& filename : watchedFiles.keys()) { const QFileInfo fileInfo(filename); - const bool existed = watchedFiles[fileInfo.absoluteFilePath()]; + const QString filePath = fileInfo.absoluteFilePath(); + const bool existed = watchedFiles[filePath]; if (!fileInfo.exists() && existed) { - qDebug("Remove watch file %s", qPrintable(fileInfo.absoluteFilePath())); - m_fileWatcher.removePath(fileInfo.absolutePath()); - emit fileRemoved(fileInfo.absoluteFilePath()); + qDebug("Remove watch file %s", qPrintable(filePath)); + m_fileWatcher.removePath(filePath); + m_watchedPaths.remove(filePath); + watchedFiles.remove(filePath); + scheduleSignal(Removed, filePath); } if (!existed && fileInfo.exists()) { - qDebug("Add watch file %s", qPrintable(fileInfo.absoluteFilePath())); - m_fileWatcher.addPath(fileInfo.absolutePath()); - emit fileCreated(fileInfo.absoluteFilePath()); + qDebug("Add watch file %s", qPrintable(filePath)); + if (!m_watchedPaths.value(filePath)) { + const bool success = m_fileWatcher.addPath(filePath); + m_watchedPaths[filePath] = success; + watchedFiles[filePath] = fileInfo.exists(); + } + scheduleSignal(Created, filePath); } if (existed && fileInfo.exists()) { + // this case is handled using qDebug("Refresh watch file %s", qPrintable(fileInfo.absoluteFilePath())); m_fileWatcher.removePath(fileInfo.absolutePath()); m_fileWatcher.addPath(fileInfo.absolutePath()); - emit fileChanged(fileInfo.absoluteFilePath()); + scheduleSignal(Updated, filePath); } - m_watchedFilesInDirectory[fileInfo.absolutePath()][fileInfo.absoluteFilePath()] = fileInfo.exists(); + m_watchedFilesInDirectory[directoryPath][filePath] = fileInfo.exists(); + } +} + +void BulkFileWatcher::emitSignals() +{ + QMap> queued; + m_pendingSignals.swap(queued); + for (const auto& path : queued.keys()){ + const auto &signal = queued[path]; + if (signal.last() == Removed) { + emit fileRemoved(path); + continue; + } + if (signal.first() == Created){ + emit fileCreated(path); + continue; + } + emit fileChanged(path); + } +} + +void BulkFileWatcher::scheduleSignal(Signal signal, const QString &path) +{ + // we need to collect signals since the file watcher API may send multiple signals for a "single" change + // therefore we wait until the event loop finished before starting to import any changes + const QString filePath = QFileInfo(path).absoluteFilePath(); + m_pendingSignals[filePath] << signal; + + if (!m_pendingSignalsTimer.isActive()) { + m_pendingSignalsTimer.start(); } } diff --git a/src/core/FileWatcher.h b/src/core/FileWatcher.h index de7dbb1c2..1ef46785c 100644 --- a/src/core/FileWatcher.h +++ b/src/core/FileWatcher.h @@ -57,6 +57,12 @@ private: class BulkFileWatcher : public QObject { Q_OBJECT + + enum Signal { + Created, + Updated, + Removed + }; public: explicit BulkFileWatcher(QObject* parent = nullptr); @@ -65,7 +71,6 @@ public: void removePath(const QString& path); void addPath(const QString& path); - void restart(const QString& path); void ignoreFileChanges(const QString& path); @@ -80,13 +85,21 @@ public slots: private slots: void handleFileChanged(const QString& path); void handleDirectoryChanged(const QString& path); + void emitSignals(); private: - QSet m_filePaths; + void scheduleSignal(Signal event, const QString &path); + +private: + QMap m_watchedPaths; QMap m_ignoreFilesChangess; QFileSystemWatcher m_fileWatcher; QMap> m_watchedFilesInDirectory; - QTimer m_fileWatchUnblockTimer; // needed for Import/Export-References + // needed for Import/Export-References to prevent update after self-write + QTimer m_fileWatchUnblockTimer; + // needed to tolerate multiple signals for same event + QTimer m_pendingSignalsTimer; + QMap> m_pendingSignals; }; #endif // KEEPASSXC_FILEWATCHER_H diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index bb87c1c75..036413e6b 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -56,116 +56,111 @@ static const QString KeeShare_Container("container.share.kdbx"); enum Trust { - Invalid, - Own, - UntrustedForever, - UntrustedOnce, - TrustedOnce, - TrustedForever, + Invalid, + Own, + UntrustedForever, + UntrustedOnce, + TrustedOnce, + TrustedForever, }; bool isOfExportType(const QFileInfo &fileInfo, const QString type) { - return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); + return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); } QPair check(QByteArray& data, - const KeeShareSettings::Reference& reference, - const KeeShareSettings::Certificate& ownCertificate, - const QList& knownCertificates, - const KeeShareSettings::Sign& sign) + const KeeShareSettings::Reference& reference, + const KeeShareSettings::Certificate& ownCertificate, + const QList& knownCertificates, + const KeeShareSettings::Sign& sign) { - KeeShareSettings::Certificate certificate; - if (!sign.signature.isEmpty()) { - certificate = sign.certificate; - auto key = sign.certificate.sshKey(); - key.openKey(QString()); - const auto signer = Signature(); - if (!signer.verify(data, sign.signature, key)) { - qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); - return {Invalid, KeeShareSettings::Certificate()}; - } + KeeShareSettings::Certificate certificate; + if (!sign.signature.isEmpty()) { + certificate = sign.certificate; + auto key = sign.certificate.sshKey(); + key.openKey(QString()); + const auto signer = Signature(); + if (!signer.verify(data, sign.signature, key)) { + qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); + return {Invalid, KeeShareSettings::Certificate()}; + } - if (ownCertificate.key == sign.certificate.key) { - return {Own, ownCertificate }; - } - } - enum Scope { Invalid, Global, Local }; - Scope scope = Invalid; - KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; - for (const auto& scopedCertificate : knownCertificates) { - if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { - // Global scope is overwritten by local scope - scope = Global; - trusted = scopedCertificate.trust; - } - if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { - scope = Local; - trusted = scopedCertificate.trust; - break; - } - } - if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask){ - // we introduce now scopes if there is a global - return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; - } + if (ownCertificate.key == sign.certificate.key) { + return {Own, ownCertificate }; + } + } + enum Scope { Invalid, Global, Local }; + Scope scope = Invalid; + KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; + for (const auto& scopedCertificate : knownCertificates) { + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + // Global scope is overwritten by local scope + scope = Global; + trusted = scopedCertificate.trust; + } + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + scope = Local; + trusted = scopedCertificate.trust; + break; + } + } + if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask){ + // we introduce now scopes if there is a global + return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; + } - QMessageBox warning; - if (sign.signature.isEmpty()){ - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); - warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import from %1?") - .arg(reference.path)); - } - else { - warning.setIcon(QMessageBox::Question); - warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); - warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") - .arg(certificate.signer, certificate.fingerprint(), reference.path)); - } - auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); - auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); - auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); - auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); - warning.setDefaultButton(untrustedOnce); - warning.exec(); - if (warning.clickedButton() == trustedForever){ - return {TrustedForever, certificate }; - } - if (warning.clickedButton() == trustedOnce){ - return {TrustedOnce, certificate}; - } - if (warning.clickedButton() == untrustedOnce){ - return {UntrustedOnce, certificate }; - } - if (warning.clickedButton() == untrustedForever){ - return {UntrustedForever, certificate }; - } - return {UntrustedOnce, certificate }; + QMessageBox warning; + if (sign.signature.isEmpty()){ + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); + warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import from %1?") + .arg(reference.path)); + } + else { + warning.setIcon(QMessageBox::Question); + warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") + .arg(certificate.signer, certificate.fingerprint(), reference.path)); + } + auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); + auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); + auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); + auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); + warning.setDefaultButton(untrustedOnce); + warning.exec(); + if (warning.clickedButton() == trustedForever){ + return {TrustedForever, certificate }; + } + if (warning.clickedButton() == trustedOnce){ + return {TrustedOnce, certificate}; + } + if (warning.clickedButton() == untrustedOnce){ + return {UntrustedOnce, certificate }; + } + if (warning.clickedButton() == untrustedForever){ + return {UntrustedForever, certificate }; + } + return {UntrustedOnce, certificate }; } } // End Namespace ShareObserver::ShareObserver(QSharedPointer db, QObject* parent) - : QObject(parent) - , m_db(std::move(db)) - , m_fileWatcher(new BulkFileWatcher(this)) + : QObject(parent) + , m_db(std::move(db)) + , m_fileWatcher(new BulkFileWatcher(this)) { - connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); + connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); - connect(m_db.data(), SIGNAL(databaseModified()), SLOT(handleDatabaseChanged())); - connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved())); + connect(m_db.data(), SIGNAL(databaseModified()), SLOT(handleDatabaseChanged())); + connect(m_db.data(), SIGNAL(databaseSaved()), SLOT(handleDatabaseSaved())); - connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileUpdated(QString))); - connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileUpdated(QString))); - connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileCreated(QString)), SLOT(handleFileCreated(QString))); + connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(handleFileUpdated(QString))); + connect(m_fileWatcher, SIGNAL(fileRemoved(QString)), SLOT(handleFileDeleted(QString))); - const auto active = KeeShare::active(); - if (!active.in && !active.out) { - deinitialize(); - } else { - reinitialize(); - } + handleDatabaseChanged(); } ShareObserver::~ShareObserver() @@ -174,604 +169,617 @@ ShareObserver::~ShareObserver() void ShareObserver::deinitialize() { - m_fileWatcher->clear(); - m_groupToReference.clear(); - m_referenceToGroup.clear(); + m_fileWatcher->clear(); + m_groupToReference.clear(); + m_referenceToGroup.clear(); } void ShareObserver::reinitialize() { - struct Update - { - Group* group; - KeeShareSettings::Reference oldReference; - KeeShareSettings::Reference newReference; - }; + struct Update + { + Group* group; + KeeShareSettings::Reference oldReference; + KeeShareSettings::Reference newReference; + }; - const auto active = KeeShare::active(); - QList updated; - QList groups = m_db->rootGroup()->groupsRecursive(true); - for (Group* group : groups) { - Update couple{group, m_groupToReference.value(group), KeeShare::referenceOf(group)}; - if (couple.oldReference == couple.newReference) { - continue; - } + const auto active = KeeShare::active(); + QList updated; + QList groups = m_db->rootGroup()->groupsRecursive(true); + for (Group* group : groups) { + Update couple{group, m_groupToReference.value(group), KeeShare::referenceOf(group)}; + if (couple.oldReference == couple.newReference) { + continue; + } - m_groupToReference.remove(couple.group); - m_referenceToGroup.remove(couple.oldReference); - m_shareToGroup.remove(couple.oldReference.path); - if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting()) - || (active.out && couple.newReference.isExporting()))) { - m_groupToReference[couple.group] = couple.newReference; - m_referenceToGroup[couple.newReference] = couple.group; - m_shareToGroup[couple.newReference.path] = couple.group; - } - updated << couple; - } + m_groupToReference.remove(couple.group); + m_referenceToGroup.remove(couple.oldReference); + m_shareToGroup.remove(couple.oldReference.path); + if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting()) + || (active.out && couple.newReference.isExporting()))) { + m_groupToReference[couple.group] = couple.newReference; + m_referenceToGroup[couple.newReference] = couple.group; + m_shareToGroup[couple.newReference.path] = couple.group; + } + updated << couple; + } - QStringList success; - QStringList warning; - QStringList error; - for (const auto& update : updated) { - if (!update.oldReference.path.isEmpty()) { - m_fileWatcher->removePath(update.oldReference.path); - } + QStringList success; + QStringList warning; + QStringList error; + for (const auto& update : updated) { + if (!update.oldReference.path.isEmpty()) { + m_fileWatcher->removePath(update.oldReference.path); + } - if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { - m_fileWatcher->addPath(update.newReference.path); - } + if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { + m_fileWatcher->addPath(update.newReference.path); + } - if (update.newReference.isImporting()) { - const auto result = this->importFromReferenceContainer(update.newReference.path); - if (!result.isValid()) { - // tolerable result - blocked import or missing source - continue; - } + if (update.newReference.isImporting()) { + const auto result = this->importFromReferenceContainer(update.newReference.path); + if (!result.isValid()) { + // tolerable result - blocked import or missing source + continue; + } - if (result.isError()) { - error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Imported from %1").arg(result.path); - } - } - } + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + } + } - notifyAbout(success, warning, error); + notifyAbout(success, warning, error); } void ShareObserver::notifyAbout(const QStringList& success, const QStringList& warning, const QStringList& error) { - if (error.isEmpty() && warning.isEmpty() && success.isEmpty()) { - return; - } + if (error.isEmpty() && warning.isEmpty() && success.isEmpty()) { + return; + } - MessageWidget::MessageType type = MessageWidget::Positive; - if (!warning.isEmpty()) { - type = MessageWidget::Warning; - } - if (!error.isEmpty()) { - type = MessageWidget::Error; - } - emit sharingMessage((success + warning + error).join("\n"), type); + MessageWidget::MessageType type = MessageWidget::Positive; + if (!warning.isEmpty()) { + type = MessageWidget::Warning; + } + if (!error.isEmpty()) { + type = MessageWidget::Error; + } + emit sharingMessage((success + warning + error).join("\n"), type); } void ShareObserver::handleDatabaseChanged() { - if (!m_db) { - Q_ASSERT(m_db); - return; - } - const auto active = KeeShare::active(); - if (!active.out && !active.in) { - deinitialize(); - } else { - reinitialize(); - } + if (!m_db) { + Q_ASSERT(m_db); + return; + } + const auto active = KeeShare::active(); + if (!active.out && !active.in) { + deinitialize(); + } else { + reinitialize(); + } +} + +void ShareObserver::handleFileCreated(const QString& path) +{ + // there is currently no difference in handling an added share or updating from one + this->handleFileUpdated(path); +} + +void ShareObserver::handleFileDeleted(const QString& path) +{ + Q_UNUSED(path); + // There is nothing we can or should do for now, ignore deletion } void ShareObserver::handleFileUpdated(const QString& path) { - const Result result = this->importFromReferenceContainer(path); - if (!result.isValid()) { - return; - } - QStringList success; - QStringList warning; - QStringList error; - if (result.isError()) { - error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Imported from %1").arg(result.path); - } - notifyAbout(success, warning, error); + const Result result = this->importFromReferenceContainer(path); + if (!result.isValid()) { + return; + } + QStringList success; + QStringList warning; + QStringList error; + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path, result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path, result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path, result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + notifyAbout(success, warning, error); } ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { #if !defined(WITH_XC_KEESHARE_SECURE) - Q_UNUSED(targetGroup); - return { reference.path, Result::Warning, tr("Signed share container are not supported - import prevented") }; + Q_UNUSED(targetGroup); + return { reference.path, Result::Warning, tr("Signed share container are not supported - import prevented") }; #else - QuaZip zip(reference.path); - if (!zip.open(QuaZip::mdUnzip)) { - qCritical("Unable to open file %s.", qPrintable(reference.path)); - return {reference.path, Result::Error, tr("File is not readable")}; - } - const auto expected = QSet() << KeeShare_Signature << KeeShare_Container; - const auto files = zip.getFileInfoList(); - QSet actual; - for (const auto& file : files) { - actual << file.name; - } - if (expected != actual) { - qCritical("Invalid sharing container %s.", qPrintable(reference.path)); - return {reference.path, Result::Error, tr("Invalid sharing container")}; - } + QuaZip zip(reference.path); + if (!zip.open(QuaZip::mdUnzip)) { + qCritical("Unable to open file %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("File is not readable")}; + } + const auto expected = QSet() << KeeShare_Signature << KeeShare_Container; + const auto files = zip.getFileInfoList(); + QSet actual; + for (const auto& file : files) { + actual << file.name; + } + if (expected != actual) { + qCritical("Invalid sharing container %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("Invalid sharing container")}; + } - zip.setCurrentFile(KeeShare_Signature); - QuaZipFile signatureFile(&zip); - signatureFile.open(QuaZipFile::ReadOnly); - QTextStream stream(&signatureFile); + zip.setCurrentFile(KeeShare_Signature); + QuaZipFile signatureFile(&zip); + signatureFile.open(QuaZipFile::ReadOnly); + QTextStream stream(&signatureFile); - const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll()); - signatureFile.close(); + const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll()); + signatureFile.close(); - zip.setCurrentFile(KeeShare_Container); - QuaZipFile databaseFile(&zip); - databaseFile.open(QuaZipFile::ReadOnly); - auto payload = databaseFile.readAll(); - databaseFile.close(); - QBuffer buffer(&payload); - buffer.open(QIODevice::ReadOnly); + zip.setCurrentFile(KeeShare_Container); + QuaZipFile databaseFile(&zip); + databaseFile.open(QuaZipFile::ReadOnly); + auto payload = databaseFile.readAll(); + databaseFile.close(); + QBuffer buffer(&payload); + buffer.open(QIODevice::ReadOnly); - KeePass2Reader reader; - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(reference.password)); - auto sourceDb = QSharedPointer::create(); - if (!reader.readDatabase(&buffer, key, sourceDb.data())) { - qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); - return {reference.path, Result::Error, reader.errorString()}; - } + KeePass2Reader reader; + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + auto sourceDb = QSharedPointer::create(); + if (!reader.readDatabase(&buffer, key, sourceDb.data())) { + qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); + return {reference.path, Result::Error, reader.errorString()}; + } - auto foreign = KeeShare::foreign(); - auto own = KeeShare::own(); - auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); - switch (trust.first) { - case Invalid: - qWarning("Prevent untrusted import"); - return {reference.path, Result::Error, tr("Untrusted import prevented")}; + auto foreign = KeeShare::foreign(); + auto own = KeeShare::own(); + auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); + switch (trust.first) { + case Invalid: + qWarning("Prevent untrusted import"); + return {reference.path, Result::Error, tr("Untrusted import prevented")}; - case UntrustedForever: - case TrustedForever: { - bool found = false; - const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; - for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { - if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { - scopedCertificate.certificate.signer = trust.second.signer; - scopedCertificate.path = reference.path; - scopedCertificate.trust = trusted; - found = true; - } - } - if (!found) { - foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; - // we need to update with the new signer - KeeShare::setForeign(foreign); - } - if (trust.first == TrustedForever) { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful signed import")}; - } - } - // Silent ignore of untrusted import or unchanging import - return {}; - } - case TrustedOnce: - case Own: { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful signed import")}; - } - return {}; - } - default: - Q_ASSERT(false); - return {reference.path, Result::Error, tr("Unexpected error")}; - } + case UntrustedForever: + case TrustedForever: { + bool found = false; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { + if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trust = trusted; + found = true; + } + } + if (!found) { + foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; + // we need to update with the new signer + KeeShare::setForeign(foreign); + } + if (trust.first == TrustedForever) { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + } + // Silent ignore of untrusted import or unchanging import + return {}; + } + case TrustedOnce: + case Own: { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + return {}; + } + default: + Q_ASSERT(false); + return {reference.path, Result::Error, tr("Unexpected error")}; + } #endif } ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { #if !defined(WITH_XC_KEESHARE_INSECURE) - Q_UNUSED(targetGroup); - return {reference.path, Result::Warning, tr("Unsigned share container are not supported - import prevented")}; + Q_UNUSED(targetGroup); + return {reference.path, Result::Warning, tr("Unsigned share container are not supported - import prevented")}; #else - QFile file(reference.path); - if (!file.open(QIODevice::ReadOnly)){ - qCritical("Unable to open file %s.", qPrintable(reference.path)); - return {reference.path, Result::Error, tr("File is not readable")}; - } - auto payload = file.readAll(); - file.close(); - QBuffer buffer(&payload); - buffer.open(QIODevice::ReadOnly); + QFile file(reference.path); + if (!file.open(QIODevice::ReadOnly)){ + qCritical("Unable to open file %s.", qPrintable(reference.path)); + return {reference.path, Result::Error, tr("File is not readable")}; + } + auto payload = file.readAll(); + file.close(); + QBuffer buffer(&payload); + buffer.open(QIODevice::ReadOnly); - KeePass2Reader reader; - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(reference.password)); - auto sourceDb = QSharedPointer::create(); - if (!reader.readDatabase(&buffer, key, sourceDb.data())) { - qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); - return {reference.path, Result::Error, reader.errorString()}; - } + KeePass2Reader reader; + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); + auto sourceDb = QSharedPointer::create(); + if (!reader.readDatabase(&buffer, key, sourceDb.data())) { + qCritical("Error while parsing the database: %s", qPrintable(reader.errorString())); + return {reference.path, Result::Error, reader.errorString()}; + } - auto foreign = KeeShare::foreign(); - const auto own = KeeShare::own(); - const auto sign = KeeShareSettings::Sign(); // invalid sign - auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); - switch(trust.first) { - case UntrustedForever: - case TrustedForever: { - bool found = false; - const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; - for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { - if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { - scopedCertificate.certificate.signer = trust.second.signer; - scopedCertificate.path = reference.path; - scopedCertificate.trust = trusted; - found = true; - } - } - if (!found) { - foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; - // we need to update with the new signer - KeeShare::setForeign(foreign); - } - if (trust.first == TrustedForever) { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful signed import")}; - } - } - return {}; - } + auto foreign = KeeShare::foreign(); + const auto own = KeeShare::own(); + const auto sign = KeeShareSettings::Sign(); // invalid sign + auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); + switch(trust.first) { + case UntrustedForever: + case TrustedForever: { + bool found = false; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { + if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trust = trusted; + found = true; + } + } + if (!found) { + foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; + // we need to update with the new signer + KeeShare::setForeign(foreign); + } + if (trust.first == TrustedForever) { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + } + return {}; + } - case TrustedOnce: { - qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful unsigned import")}; - } - return {}; - } - default: - qWarning("Prevent untrusted import"); - return {reference.path, Result::Warning, tr("Untrusted import prevented")}; - } + case TrustedOnce: { + qDebug("Synchronize %s %s with %s", + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful unsigned import")}; + } + return {}; + } + default: + qWarning("Prevent untrusted import"); + return {reference.path, Result::Warning, tr("Untrusted import prevented")}; + } #endif } ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) { - const QFileInfo info(reference.path); - if (!info.exists()) { - qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); - return {reference.path, Result::Warning, tr("File does not exist")}; - } + const QFileInfo info(reference.path); + if (!info.exists()) { + qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath())); + return {reference.path, Result::Warning, tr("File does not exist")}; + } - if (isOfExportType(info, KeeShare::signedContainerFileType())) { - return importSingedContainerInto(reference, targetGroup); - } - if (isOfExportType(info, KeeShare::unsignedContainerFileType())) { - return importUnsignedContainerInto(reference, targetGroup); - } - return {reference.path, Result::Error, tr("Unknown share container type")}; + if (isOfExportType(info, KeeShare::signedContainerFileType())) { + return importSingedContainerInto(reference, targetGroup); + } + if (isOfExportType(info, KeeShare::unsignedContainerFileType())) { + return importUnsignedContainerInto(reference, targetGroup); + } + return {reference.path, Result::Error, tr("Unknown share container type")}; } ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString& path) { - if (!KeeShare::active().in) { - return {}; - } - auto shareGroup = m_shareToGroup.value(path); - if (!shareGroup) { - qWarning("Source for %s does not exist", qPrintable(path)); - Q_ASSERT(shareGroup); - return {}; - } - const auto reference = KeeShare::referenceOf(shareGroup); - if (reference.type == KeeShareSettings::Inactive) { - qDebug("Ignore change of inactive reference %s", qPrintable(reference.path)); - return {}; - } - if (reference.type == KeeShareSettings::ExportTo) { - qDebug("Ignore change of export reference %s", qPrintable(reference.path)); - return {}; - } - Q_ASSERT(shareGroup->database() == m_db); - Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid())); - return importContainerInto(reference, shareGroup); + if (!KeeShare::active().in) { + return {}; + } + auto shareGroup = m_shareToGroup.value(path); + if (!shareGroup) { + qWarning("Source for %s does not exist", qPrintable(path)); + Q_ASSERT(shareGroup); + return {}; + } + const auto reference = KeeShare::referenceOf(shareGroup); + if (reference.type == KeeShareSettings::Inactive) { + // changes of inactive references are ignored + return {}; + } + if (reference.type == KeeShareSettings::ExportTo) { + // changes of export only references are ignored + return {}; + } + + Q_ASSERT(shareGroup->database() == m_db); + Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid())); + return importContainerInto(reference, shareGroup); } void ShareObserver::resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb) { - for (const auto& attribute : EntryAttributes::DefaultAttributes) { - const auto standardValue = targetEntry->attributes()->value(attribute); - const auto type = targetEntry->placeholderType(standardValue); - if (type != Entry::PlaceholderType::Reference) { - // No reference to resolve - continue; - } - const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue); - if (referencedTargetEntry) { - // References is within scope, no resolving needed - continue; - } - // We could do more sophisticated **** trying to point the reference to the next in-scope reference - // but those cases with high propability constructed examples and very rare in real usage - const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid()); - const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue); - targetEntry->setUpdateTimeinfo(false); - targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute)); - targetEntry->setUpdateTimeinfo(true); - } + for (const auto& attribute : EntryAttributes::DefaultAttributes) { + const auto standardValue = targetEntry->attributes()->value(attribute); + const auto type = targetEntry->placeholderType(standardValue); + if (type != Entry::PlaceholderType::Reference) { + // No reference to resolve + continue; + } + const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue); + if (referencedTargetEntry) { + // References is within scope, no resolving needed + continue; + } + // We could do more sophisticated **** trying to point the reference to the next in-scope reference + // but those cases with high propability constructed examples and very rare in real usage + const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid()); + const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue); + targetEntry->setUpdateTimeinfo(false); + targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute)); + targetEntry->setUpdateTimeinfo(true); + } } Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot) { - const auto* sourceDb = sourceRoot->database(); - auto* targetDb = new Database(); - targetDb->metadata()->setRecycleBinEnabled(false); - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(reference.password)); + const auto* sourceDb = sourceRoot->database(); + auto* targetDb = new Database(); + targetDb->metadata()->setRecycleBinEnabled(false); + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(reference.password)); - // Copy the source root as the root of the export database, memory manage the old root node - auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags); - const bool updateTimeinfo = targetRoot->canUpdateTimeinfo(); - targetRoot->setUpdateTimeinfo(false); - KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference()); - targetRoot->setUpdateTimeinfo(updateTimeinfo); - const auto sourceEntries = sourceRoot->entriesRecursive(false); - for (const Entry* sourceEntry : sourceEntries) { - auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); - const bool updateTimeinfo = targetEntry->canUpdateTimeinfo(); - targetEntry->setUpdateTimeinfo(false); - targetEntry->setGroup(targetRoot); - targetEntry->setUpdateTimeinfo(updateTimeinfo); - const auto iconUuid = targetEntry->iconUuid(); - if (!iconUuid.isNull()) { - targetDb->metadata()->addCustomIcon(iconUuid, sourceEntry->icon()); - } - } + // Copy the source root as the root of the export database, memory manage the old root node + auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags); + const bool updateTimeinfo = targetRoot->canUpdateTimeinfo(); + targetRoot->setUpdateTimeinfo(false); + KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference()); + targetRoot->setUpdateTimeinfo(updateTimeinfo); + const auto sourceEntries = sourceRoot->entriesRecursive(false); + for (const Entry* sourceEntry : sourceEntries) { + auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory); + const bool updateTimeinfo = targetEntry->canUpdateTimeinfo(); + targetEntry->setUpdateTimeinfo(false); + targetEntry->setGroup(targetRoot); + targetEntry->setUpdateTimeinfo(updateTimeinfo); + const auto iconUuid = targetEntry->iconUuid(); + if (!iconUuid.isNull()) { + targetDb->metadata()->addCustomIcon(iconUuid, sourceEntry->icon()); + } + } - targetDb->setKey(key); - auto* obsoleteRoot = targetDb->rootGroup(); - targetDb->setRootGroup(targetRoot); - delete obsoleteRoot; + targetDb->setKey(key); + auto* obsoleteRoot = targetDb->rootGroup(); + targetDb->setRootGroup(targetRoot); + delete obsoleteRoot; - targetDb->metadata()->setName(sourceRoot->name()); + targetDb->metadata()->setName(sourceRoot->name()); - // Push all deletions of the source database to the target - // simple moving out of a share group will not trigger a deletion in the - // target - a more elaborate mechanism may need the use of another custom - // attribute to share unshared entries from the target db - for (const auto& object : sourceDb->deletedObjects()) { - targetDb->addDeletedObject(object); - } - for (auto* targetEntry : targetRoot->entriesRecursive(false)) { - if (targetEntry->hasReferences()) { - resolveReferenceAttributes(targetEntry, sourceDb); - } - } - return targetDb; + // Push all deletions of the source database to the target + // simple moving out of a share group will not trigger a deletion in the + // target - a more elaborate mechanism may need the use of another custom + // attribute to share unshared entries from the target db + for (const auto& object : sourceDb->deletedObjects()) { + targetDb->addDeletedObject(object); + } + for (auto* targetEntry : targetRoot->entriesRecursive(false)) { + if (targetEntry->hasReferences()) { + resolveReferenceAttributes(targetEntry, sourceDb); + } + } + return targetDb; } QSharedPointer ShareObserver::database() { - return m_db; + return m_db; } ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb) { #if !defined(WITH_XC_KEESHARE_SECURE) - Q_UNUSED(targetDb); - return {reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")}; + Q_UNUSED(targetDb); + return {reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")}; #else - QByteArray bytes; - { - QBuffer buffer(&bytes); - buffer.open(QIODevice::WriteOnly); - KeePass2Writer writer; - writer.writeDatabase(&buffer, targetDb); - if (writer.hasError()) { - qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); - return {reference.path, Result::Error, writer.errorString()}; - } - } - const auto own = KeeShare::own(); - QuaZip zip(reference.path); - zip.setFileNameCodec("UTF-8"); - const bool zipOpened = zip.open(QuaZip::mdCreate); - if (!zipOpened) { - ::qWarning("Opening export file failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; - } - { - QuaZipFile file(&zip); - const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); - if (!signatureOpened) { - ::qWarning("Embedding signature failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; - } - QTextStream stream(&file); - KeeShareSettings::Sign sign; - auto sshKey = own.key.sshKey(); - sshKey.openKey(QString()); - const Signature signer; - sign.signature = signer.create(bytes, sshKey); - sign.certificate = own.certificate; - stream << KeeShareSettings::Sign::serialize(sign); - stream.flush(); - if (file.getZipError() != ZIP_OK) { - ::qWarning("Embedding signature failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; - } - file.close(); - } - { - QuaZipFile file(&zip); - const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); - if (!dbOpened) { - ::qWarning("Embedding database failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; - } - if (file.getZipError() != ZIP_OK) { - ::qWarning("Embedding database failed: %d", zip.getZipError()); - return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; - } - file.write(bytes); - file.close(); - } - zip.close(); - return {reference.path}; + QByteArray bytes; + { + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + KeePass2Writer writer; + writer.writeDatabase(&buffer, targetDb); + if (writer.hasError()) { + qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data()); + return {reference.path, Result::Error, writer.errorString()}; + } + } + const auto own = KeeShare::own(); + QuaZip zip(reference.path); + zip.setFileNameCodec("UTF-8"); + const bool zipOpened = zip.open(QuaZip::mdCreate); + if (!zipOpened) { + ::qWarning("Opening export file failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())}; + } + { + QuaZipFile file(&zip); + const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature)); + if (!signatureOpened) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + } + QTextStream stream(&file); + KeeShareSettings::Sign sign; + auto sshKey = own.key.sshKey(); + sshKey.openKey(QString()); + const Signature signer; + sign.signature = signer.create(bytes, sshKey); + sign.certificate = own.certificate; + stream << KeeShareSettings::Sign::serialize(sign); + stream.flush(); + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding signature failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; + } + file.close(); + } + { + QuaZipFile file(&zip); + const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container)); + if (!dbOpened) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + } + if (file.getZipError() != ZIP_OK) { + ::qWarning("Embedding database failed: %d", zip.getZipError()); + return {reference.path, Result::Error, tr("Could not embed database (%1)").arg(file.getZipError())}; + } + file.write(bytes); + file.close(); + } + zip.close(); + return {reference.path}; #endif } ShareObserver::Result ShareObserver::exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb) { #if !defined(WITH_XC_KEESHARE_INSECURE) - Q_UNUSED(targetDb); - return {reference.path, Result::Warning, tr("Overwriting unsigned share container is not supported - export prevented")}; + Q_UNUSED(targetDb); + return {reference.path, Result::Warning, tr("Overwriting unsigned share container is not supported - export prevented")}; #else - QFile file(reference.path); - const bool fileOpened = file.open(QIODevice::WriteOnly); - if (!fileOpened) { - ::qWarning("Opening export file failed"); - return {reference.path, Result::Error, tr("Could not write export container")}; - } - KeePass2Writer writer; - writer.writeDatabase(&file, targetDb); - if (writer.hasError()) { - qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data()); - return {reference.path, Result::Error, writer.errorString()}; - } - file.close(); + QFile file(reference.path); + const bool fileOpened = file.open(QIODevice::WriteOnly); + if (!fileOpened) { + ::qWarning("Opening export file failed"); + return {reference.path, Result::Error, tr("Could not write export container")}; + } + KeePass2Writer writer; + writer.writeDatabase(&file, targetDb); + if (writer.hasError()) { + qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data()); + return {reference.path, Result::Error, writer.errorString()}; + } + file.close(); #endif - return {reference.path}; + return {reference.path}; } QList ShareObserver::exportIntoReferenceContainers() { - QList results; - const auto groups = m_db->rootGroup()->groupsRecursive(true); - for (const auto* group : groups) { - const auto reference = KeeShare::referenceOf(group); - if (!reference.isExporting()) { - continue; - } + QList results; + const auto groups = m_db->rootGroup()->groupsRecursive(true); + for (const auto* group : groups) { + const auto reference = KeeShare::referenceOf(group); + if (!reference.isExporting()) { + continue; + } - m_fileWatcher->ignoreFileChanges(reference.path); - QScopedPointer targetDb(exportIntoContainer(reference, group)); - QFileInfo info(reference.path); - if (isOfExportType(info, KeeShare::signedContainerFileType())) { - results << exportIntoReferenceSignedContainer(reference, targetDb.data()); - m_fileWatcher->observeFileChanges(true); - continue; - } - if (isOfExportType(info, KeeShare::unsignedContainerFileType())) { - results << exportIntoReferenceUnsignedContainer(reference, targetDb.data()); - m_fileWatcher->observeFileChanges(true); - continue; - } - Q_ASSERT(false); - results << Result{reference.path, Result::Error, tr("Unexpected export error occurred")}; - } - return results; + m_fileWatcher->ignoreFileChanges(reference.path); + QScopedPointer targetDb(exportIntoContainer(reference, group)); + QFileInfo info(reference.path); + if (isOfExportType(info, KeeShare::signedContainerFileType())) { + results << exportIntoReferenceSignedContainer(reference, targetDb.data()); + m_fileWatcher->observeFileChanges(true); + continue; + } + if (isOfExportType(info, KeeShare::unsignedContainerFileType())) { + results << exportIntoReferenceUnsignedContainer(reference, targetDb.data()); + m_fileWatcher->observeFileChanges(true); + continue; + } + Q_ASSERT(false); + results << Result{reference.path, Result::Error, tr("Unexpected export error occurred")}; + } + return results; } void ShareObserver::handleDatabaseSaved() { - if (!KeeShare::active().out) { - return; - } - QStringList error; - QStringList warning; - QStringList success; - const auto results = exportIntoReferenceContainers(); - for (const Result& result : results) { - if (!result.isValid()) { - Q_ASSERT(result.isValid()); - continue; - } - if (result.isError()) { - error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Export to %1").arg(result.path); - } - } - notifyAbout(success, warning, error); + if (!KeeShare::active().out) { + return; + } + QStringList error; + QStringList warning; + QStringList success; + const auto results = exportIntoReferenceContainers(); + for (const Result& result : results) { + if (!result.isValid()) { + Q_ASSERT(result.isValid()); + continue; + } + if (result.isError()) { + error << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Export to %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Export to %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Export to %1").arg(result.path); + } + } + notifyAbout(success, warning, error); } ShareObserver::Result::Result(const QString& path, ShareObserver::Result::Type type, const QString& message) - : path(path) - , type(type) - , message(message) + : path(path) + , type(type) + , message(message) { } bool ShareObserver::Result::isValid() const { - return !path.isEmpty() || !message.isEmpty() || !message.isEmpty() || !message.isEmpty(); + return !path.isEmpty() || !message.isEmpty() || !message.isEmpty() || !message.isEmpty(); } bool ShareObserver::Result::isError() const { - return !message.isEmpty() && type == Error; + return !message.isEmpty() && type == Error; } bool ShareObserver::Result::isInfo() const { - return !message.isEmpty() && type == Info; + return !message.isEmpty() && type == Info; } bool ShareObserver::Result::isWarning() const { - return !message.isEmpty() && type == Warning; + return !message.isEmpty() && type == Warning; } diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h index 26f010813..d67a1f7ef 100644 --- a/src/keeshare/ShareObserver.h +++ b/src/keeshare/ShareObserver.h @@ -49,7 +49,9 @@ signals: private slots: void handleDatabaseChanged(); void handleDatabaseSaved(); + void handleFileCreated(const QString& path); void handleFileUpdated(const QString& path); + void handleFileDeleted(const QString& path); private: struct Result From d11bb247b34b9002b9b0949644c8d30f4388f205 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Thu, 10 Jan 2019 07:57:13 -0700 Subject: [PATCH 20/23] Simplify CMakeLists --- CMakeLists.txt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 39cb4b904..7b2ea7d12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -364,14 +364,11 @@ include_directories(SYSTEM ${ARGON2_INCLUDE_DIR}) # Optional if(WITH_XC_KEESHARE) - # ZLIB is needed and already required + set(WITH_XC_KEESHARE_INSECURE ON) if(WITH_XC_KEESHARE_SECURE) + # ZLIB is needed and already required find_package(QuaZip REQUIRED) - set(WITH_XC_KEESHARE_INSECURE ON) - set(WITH_XC_KEESHARE_SECURE ON) include_directories(SYSTEM ${QUAZIP_INCLUDE_DIR}) - else() - set(WITH_XC_KEESHARE_INSECURE ON) endif() else() set(WITH_XC_KEESHARE_INSECURE OFF) From a8e266129a6cd35bc8db77de88c3bbbc9697be5e Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 16 Jan 2019 22:53:29 -0500 Subject: [PATCH 21/23] Corrected formatting and cleanup --- INSTALL.md | 5 +- README.md | 6 +- cmake/FindQuaZip.cmake | 16 +- snapcraft.yaml | 2 - src/core/Database.cpp | 2 +- src/crypto/CryptoHash.h | 1 - src/crypto/ssh/CMakeLists.txt | 6 +- src/gui/AboutDialog.cpp | 102 +-- src/gui/EntryPreviewWidget.cpp | 17 +- src/gui/MainWindow.cpp | 1429 ++++++++++++++++---------------- 10 files changed, 791 insertions(+), 795 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 9bdc1c868..5267cd85a 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -27,7 +27,6 @@ The following libraries are required: * libxi, libxtst, qtx11extras (optional for auto-type on X11) * libsodium (>= 1.0.12, optional for KeePassXC-Browser support) * libargon2 -* libquazip5 Prepare the Building Environment ================================ @@ -100,10 +99,12 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct -DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF) -DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (favicon download) (default: OFF) -DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF) - -DWITH_XC_SHARING=[ON|OFF] Enable/Disable Sharing extension (default: OFF) + -DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group syncronization extension (default: OFF) -DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF) -DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF) + -DWITH_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare secure containers, requires libquazip5 (default: OFF) + -DWITH_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON) -DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF) -DWITH_DEV_BUILD=[ON|OFF] Enable/Disable deprecated method warnings (default: OFF) diff --git a/README.md b/README.md index d4ae0f840..6d4c29a2c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # KeePassXC [![TeamCity Build Status](https://ci.keepassxc.org/app/rest/builds/buildType:\(project:KeepassXC\)/statusIcon)](https://ci.keepassxc.org/?guest=1) [![codecov](https://codecov.io/gh/keepassxreboot/keepassxc/branch/develop/graph/badge.svg)](https://codecov.io/gh/keepassxreboot/keepassxc) - -## Note -This is a feature fork to introduce the concept of Sharing to KeepPassXC. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details. - ## About KeePassXC [KeePassXC](https://keepassxc.org) is a cross-platform community fork of [KeePassX](https://www.keepassx.org/). @@ -38,7 +34,7 @@ so please check out your distribution's package list to see if KeePassXC is avai [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepasshttp-connector/dafgdjggglmmknipkhngniifhplpcldb), and [passafari](https://github.com/mmichaa/passafari.safariextension/) in Safari. [[See note about KeePassHTTP]](#note-about-keepasshttp) - Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk) -- Sharing of passwords using KeeShare. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details. +- Synchronize passwords using KeeShare. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details. - Many bug fixes For a full list of features and changes, read the [CHANGELOG](CHANGELOG) document. diff --git a/cmake/FindQuaZip.cmake b/cmake/FindQuaZip.cmake index f70973011..8d3091810 100644 --- a/cmake/FindQuaZip.cmake +++ b/cmake/FindQuaZip.cmake @@ -4,15 +4,14 @@ # QUAZIP_LIBRARIES - List of QuaZip libraries # QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers - -IF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) +IF(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) # in cache already SET(QUAZIP_FOUND TRUE) -ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) - IF (Qt5Core_FOUND) +ELSE(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) + IF(Qt5Core_FOUND) set(QUAZIP_LIB_VERSION_SUFFIX 5) ENDIF() - IF (WIN32) + IF(WIN32) FIND_PATH(QUAZIP_LIBRARY_DIR WIN32_DEBUG_POSTFIX d NAMES libquazip${QUAZIP_LIB_VERSION_SUFFIX}.dll @@ -24,11 +23,10 @@ ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR NAMES zlib.h) ELSE(WIN32) FIND_PACKAGE(PkgConfig) -# pkg_check_modules(PC_QCA2 QUIET qca2) pkg_check_modules(PC_QUAZIP quazip) FIND_LIBRARY(QUAZIP_LIBRARIES WIN32_DEBUG_POSTFIX d - NAMES quazip${QUAZIP_LIB_VERSION_SUFFIX} + NAMES quazip${QUAZIP_LIB_VERSION_SUFFIX} HINTS /usr/lib /usr/lib64 ) FIND_PATH(QUAZIP_INCLUDE_DIR quazip.h @@ -36,8 +34,8 @@ ELSE (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) PATH_SUFFIXES quazip${QUAZIP_LIB_VERSION_SUFFIX} ) FIND_PATH(QUAZIP_ZLIB_INCLUDE_DIR zlib.h HINTS /usr/include /usr/local/include) - ENDIF (WIN32) + ENDIF(WIN32) INCLUDE(FindPackageHandleStandardArgs) SET(QUAZIP_INCLUDE_DIRS ${QUAZIP_INCLUDE_DIR} ${QUAZIP_ZLIB_INCLUDE_DIR}) find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_LIBRARIES QUAZIP_INCLUDE_DIR QUAZIP_ZLIB_INCLUDE_DIR QUAZIP_INCLUDE_DIRS) -ENDIF (QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) +ENDIF(QUAZIP_INCLUDE_DIRS AND QUAZIP_LIBRARIES) diff --git a/snapcraft.yaml b/snapcraft.yaml index ae5f42bc8..f7bc1434b 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -50,8 +50,6 @@ parts: - libsodium-dev - libargon2-0-dev - libqrencode-dev - - libquazip5-dev - - libquazip5-headers stage-packages: - dbus - qttranslations5-l10n # common translations diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 0545107d0..261f2757a 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -476,7 +476,7 @@ bool Database::challengeMasterSeed(const QByteArray& masterSeed) m_data.masterSeed = masterSeed; return m_data.key->challenge(masterSeed, m_data.challengeResponseKey); } - return true; + return false; } void Database::setCipher(const QUuid& cipher) diff --git a/src/crypto/CryptoHash.h b/src/crypto/CryptoHash.h index bd312121a..02f90eb4d 100644 --- a/src/crypto/CryptoHash.h +++ b/src/crypto/CryptoHash.h @@ -34,7 +34,6 @@ public: explicit CryptoHash(Algorithm algo, bool hmac = false); ~CryptoHash(); void addData(const QByteArray& data); - void reset(); QByteArray result() const; void setKey(const QByteArray& data); diff --git a/src/crypto/ssh/CMakeLists.txt b/src/crypto/ssh/CMakeLists.txt index a1a31741d..fdaf68e41 100644 --- a/src/crypto/ssh/CMakeLists.txt +++ b/src/crypto/ssh/CMakeLists.txt @@ -4,9 +4,9 @@ if(WITH_XC_CRYPTO_SSH) set(crypto_ssh_SOURCES bcrypt_pbkdf.cpp blowfish.c - ASN1Key.cpp - BinaryStream.cpp - OpenSSHKey.cpp + ASN1Key.cpp + BinaryStream.cpp + OpenSSHKey.cpp ) add_library(crypto_ssh STATIC ${crypto_ssh_SOURCES}) diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index 8aac63440..7139a0224 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -28,90 +28,90 @@ #include AboutDialog::AboutDialog(QWidget* parent) - : QDialog(parent) - , m_ui(new Ui::AboutDialog()) + : QDialog(parent) + , m_ui(new Ui::AboutDialog()) { - m_ui->setupUi(this); + m_ui->setupUi(this); - resize(minimumSize()); - setWindowFlags(Qt::Sheet); - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + resize(minimumSize()); + setWindowFlags(Qt::Sheet); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - m_ui->nameLabel->setText(m_ui->nameLabel->text().replace("${VERSION}", KEEPASSXC_VERSION)); - QFont nameLabelFont = m_ui->nameLabel->font(); - nameLabelFont.setPointSize(nameLabelFont.pointSize() + 4); - m_ui->nameLabel->setFont(nameLabelFont); + m_ui->nameLabel->setText(m_ui->nameLabel->text().replace("${VERSION}", KEEPASSXC_VERSION)); + QFont nameLabelFont = m_ui->nameLabel->font(); + nameLabelFont.setPointSize(nameLabelFont.pointSize() + 4); + m_ui->nameLabel->setFont(nameLabelFont); - m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(48)); + m_ui->iconLabel->setPixmap(filePath()->applicationIcon().pixmap(48)); - QString commitHash; - if (!QString(GIT_HEAD).isEmpty()) { - commitHash = GIT_HEAD; - } else if (!QString(DIST_HASH).contains("Format")) { - commitHash = DIST_HASH; - } + QString commitHash; + if (!QString(GIT_HEAD).isEmpty()) { + commitHash = GIT_HEAD; + } else if (!QString(DIST_HASH).contains("Format")) { + commitHash = DIST_HASH; + } - QString debugInfo = "KeePassXC - "; - debugInfo.append(tr("Version %1").arg(KEEPASSXC_VERSION).append("\n")); + QString debugInfo = "KeePassXC - "; + debugInfo.append(tr("Version %1").arg(KEEPASSXC_VERSION).append("\n")); #ifndef KEEPASSXC_BUILD_TYPE_RELEASE - debugInfo.append(tr("Build Type: %1").arg(KEEPASSXC_BUILD_TYPE).append("\n")); + debugInfo.append(tr("Build Type: %1").arg(KEEPASSXC_BUILD_TYPE).append("\n")); #endif - if (!commitHash.isEmpty()) { - debugInfo.append(tr("Revision: %1").arg(commitHash.left(7)).append("\n")); - } + if (!commitHash.isEmpty()) { + debugInfo.append(tr("Revision: %1").arg(commitHash.left(7)).append("\n")); + } #ifdef KEEPASSXC_DIST - debugInfo.append(tr("Distribution: %1").arg(KEEPASSXC_DIST_TYPE).append("\n")); + debugInfo.append(tr("Distribution: %1").arg(KEEPASSXC_DIST_TYPE).append("\n")); #endif - debugInfo.append("\n").append( - QString("%1\n- Qt %2\n- %3\n\n") - .arg(tr("Libraries:"), QString::fromLocal8Bit(qVersion()), Crypto::backendVersion())); + debugInfo.append("\n").append( + QString("%1\n- Qt %2\n- %3\n\n") + .arg(tr("Libraries:"), QString::fromLocal8Bit(qVersion()), Crypto::backendVersion())); #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) - debugInfo.append(tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4") - .arg(QSysInfo::prettyProductName(), - QSysInfo::currentCpuArchitecture(), - QSysInfo::kernelType(), - QSysInfo::kernelVersion())); + debugInfo.append(tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4") + .arg(QSysInfo::prettyProductName(), + QSysInfo::currentCpuArchitecture(), + QSysInfo::kernelType(), + QSysInfo::kernelVersion())); - debugInfo.append("\n\n"); + debugInfo.append("\n\n"); #endif - QString extensions; + QString extensions; #ifdef WITH_XC_AUTOTYPE - extensions += "\n- " + tr("Auto-Type"); + extensions += "\n- " + tr("Auto-Type"); #endif #ifdef WITH_XC_BROWSER - extensions += "\n- " + tr("Browser Integration"); + extensions += "\n- " + tr("Browser Integration"); #endif #ifdef WITH_XC_SSHAGENT - extensions += "\n- " + tr("SSH Agent"); + extensions += "\n- " + tr("SSH Agent"); #endif #if defined(WITH_XC_KEESHARE_SECURE) && defined(WITH_XC_KEESHARE_INSECURE) - extensions += "\n- " + tr("KeeShare (signed and unsigned sharing)"); + extensions += "\n- " + tr("KeeShare (signed and unsigned sharing)"); #elif defined(WITH_XC_KEESHARE_SECURE) - extensions += "\n- " + tr("KeeShare (only signed sharing)"); + extensions += "\n- " + tr("KeeShare (only signed sharing)"); #elif defined(WITH_XC_KEESHARE_INSECURE) - extensions += "\n- " + tr("KeeShare (only unsigned sharing)"); + extensions += "\n- " + tr("KeeShare (only unsigned sharing)"); #endif #ifdef WITH_XC_YUBIKEY - extensions += "\n- " + tr("YubiKey"); + extensions += "\n- " + tr("YubiKey"); #endif #ifdef WITH_XC_TOUCHID - extensions += "\n- " + tr("TouchID"); + extensions += "\n- " + tr("TouchID"); #endif - if (extensions.isEmpty()) - extensions = " " + tr("None"); + if (extensions.isEmpty()) + extensions = " " + tr("None"); - debugInfo.append(tr("Enabled extensions:").append(extensions)); + debugInfo.append(tr("Enabled extensions:").append(extensions)); - m_ui->debugInfo->setPlainText(debugInfo); + m_ui->debugInfo->setPlainText(debugInfo); - setAttribute(Qt::WA_DeleteOnClose); - connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); - connect(m_ui->copyToClipboard, SIGNAL(clicked()), SLOT(copyToClipboard())); + setAttribute(Qt::WA_DeleteOnClose); + connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); + connect(m_ui->copyToClipboard, SIGNAL(clicked()), SLOT(copyToClipboard())); } AboutDialog::~AboutDialog() @@ -120,6 +120,6 @@ AboutDialog::~AboutDialog() void AboutDialog::copyToClipboard() { - QClipboard* clipboard = QApplication::clipboard(); - clipboard->setText(m_ui->debugInfo->toPlainText()); + QClipboard* clipboard = QApplication::clipboard(); + clipboard->setText(m_ui->debugInfo->toPlainText()); } diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 19ec53ae9..06280a2a5 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -30,19 +30,18 @@ #include "keeshare/KeeShare.h" #endif -namespace -{ +namespace { constexpr int GeneralTabIndex = 0; } EntryPreviewWidget::EntryPreviewWidget(QWidget* parent) - : QWidget(parent) - , m_ui(new Ui::EntryPreviewWidget()) - , m_locked(false) - , m_currentEntry(nullptr) - , m_currentGroup(nullptr) - , m_selectedTabEntry(0) - , m_selectedTabGroup(0) + : QWidget(parent) + , m_ui(new Ui::EntryPreviewWidget()) + , m_locked(false) + , m_currentEntry(nullptr) + , m_currentGroup(nullptr) + , m_selectedTabEntry(0) + , m_selectedTabGroup(0) { m_ui->setupUi(this); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f3d1d4813..2ecaa31ce 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -20,8 +20,8 @@ #include "ui_MainWindow.h" #include -#include #include +#include #include #include #include @@ -33,12 +33,12 @@ #include "core/FilePath.h" #include "core/InactivityTimer.h" #include "core/Metadata.h" -#include "keys/CompositeKey.h" -#include "keys/PasswordKey.h" -#include "keys/FileKey.h" #include "gui/AboutDialog.h" #include "gui/DatabaseWidget.h" #include "gui/SearchWidget.h" +#include "keys/CompositeKey.h" +#include "keys/FileKey.h" +#include "keys/PasswordKey.h" #ifdef WITH_XC_SSHAGENT #include "sshagent/AgentSettingsPage.h" @@ -69,331 +69,335 @@ class BrowserPlugin : public ISettingsPage { public: - BrowserPlugin(DatabaseTabWidget* tabWidget) - { - m_nativeMessagingHost = - QSharedPointer(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); - } + BrowserPlugin(DatabaseTabWidget* tabWidget) + { + m_nativeMessagingHost = + QSharedPointer(new NativeMessagingHost(tabWidget, browserSettings()->isEnabled())); + } - ~BrowserPlugin() - { - } + ~BrowserPlugin() + { + } - QString name() override - { - return QObject::tr("Browser Integration"); - } + QString name() override + { + return QObject::tr("Browser Integration"); + } - QIcon icon() override - { - return FilePath::instance()->icon("apps", "internet-web-browser"); - } + QIcon icon() override + { + return FilePath::instance()->icon("apps", "internet-web-browser"); + } - QWidget* createWidget() override - { - BrowserOptionDialog* dlg = new BrowserOptionDialog(); - return dlg; - } + QWidget* createWidget() override + { + BrowserOptionDialog* dlg = new BrowserOptionDialog(); + return dlg; + } - void loadSettings(QWidget* widget) override - { - qobject_cast(widget)->loadSettings(); - } + void loadSettings(QWidget* widget) override + { + qobject_cast(widget)->loadSettings(); + } - void saveSettings(QWidget* widget) override - { - qobject_cast(widget)->saveSettings(); - if (browserSettings()->isEnabled()) { - m_nativeMessagingHost->run(); - } else { - m_nativeMessagingHost->stop(); - } - } + void saveSettings(QWidget* widget) override + { + qobject_cast(widget)->saveSettings(); + if (browserSettings()->isEnabled()) { + m_nativeMessagingHost->run(); + } else { + m_nativeMessagingHost->stop(); + } + } private: - QSharedPointer m_nativeMessagingHost; + QSharedPointer m_nativeMessagingHost; }; #endif const QString MainWindow::BaseWindowTitle = "KeePassXC"; MainWindow* g_MainWindow = nullptr; -MainWindow* getMainWindow() { return g_MainWindow; } +MainWindow* getMainWindow() +{ + return g_MainWindow; +} MainWindow::MainWindow() - : m_ui(new Ui::MainWindow()) - , m_trayIcon(nullptr) - , m_appExitCalled(false) - , m_appExiting(false) + : m_ui(new Ui::MainWindow()) + , m_trayIcon(nullptr) + , m_appExitCalled(false) + , m_appExiting(false) { - g_MainWindow = this; + g_MainWindow = this; - m_ui->setupUi(this); + m_ui->setupUi(this); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) - new MainWindowAdaptor(this); - QDBusConnection dbus = QDBusConnection::sessionBus(); - dbus.registerObject("/keepassxc", this); - dbus.registerService("org.keepassxc.KeePassXC.MainWindow"); + new MainWindowAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject("/keepassxc", this); + dbus.registerService("org.keepassxc.KeePassXC.MainWindow"); #endif - setAcceptDrops(true); + setAcceptDrops(true); - // Setup the search widget in the toolbar - auto* search = new SearchWidget(); - search->connectSignals(m_actionMultiplexer); - m_searchWidgetAction = m_ui->toolBar->addWidget(search); - m_searchWidgetAction->setEnabled(false); + // Setup the search widget in the toolbar + auto* search = new SearchWidget(); + search->connectSignals(m_actionMultiplexer); + m_searchWidgetAction = m_ui->toolBar->addWidget(search); + m_searchWidgetAction->setEnabled(false); - m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); + m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); - restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); + restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray()); #ifdef WITH_XC_BROWSER - m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); + m_ui->settingsWidget->addSettingsPage(new BrowserPlugin(m_ui->tabWidget)); #endif #ifdef WITH_XC_SSHAGENT - SSHAgent::init(this); - connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); - m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); + SSHAgent::init(this); + connect(SSHAgent::instance(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); + m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage(m_ui->tabWidget)); #endif #if defined(WITH_XC_KEESHARE) - KeeShare::init(this); - m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); - connect(KeeShare::instance(), SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), - SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); + KeeShare::init(this); + m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); + connect(KeeShare::instance(), + SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), + SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); #endif - setWindowIcon(filePath()->applicationIcon()); - m_ui->globalMessageWidget->setHidden(true); - // clang-format off + setWindowIcon(filePath()->applicationIcon()); + m_ui->globalMessageWidget->setHidden(true); + // clang-format off connect(m_ui->globalMessageWidget, &MessageWidget::linkActivated, &MessageWidget::openHttpUrl); connect(m_ui->globalMessageWidget, SIGNAL(showAnimationStarted()), m_ui->globalMessageWidgetContainer, SLOT(show())); connect(m_ui->globalMessageWidget, SIGNAL(hideAnimationFinished()), m_ui->globalMessageWidgetContainer, SLOT(hide())); - // clang-format on + // clang-format on - m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); - m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); - connect(m_clearHistoryAction, SIGNAL(triggered()), this, SLOT(clearLastDatabases())); - connect(m_lastDatabasesActions, SIGNAL(triggered(QAction*)), this, SLOT(openRecentDatabase(QAction*))); - connect(m_ui->menuRecentDatabases, SIGNAL(aboutToShow()), this, SLOT(updateLastDatabasesMenu())); + m_clearHistoryAction = new QAction(tr("Clear history"), m_ui->menuFile); + m_lastDatabasesActions = new QActionGroup(m_ui->menuRecentDatabases); + connect(m_clearHistoryAction, SIGNAL(triggered()), this, SLOT(clearLastDatabases())); + connect(m_lastDatabasesActions, SIGNAL(triggered(QAction*)), this, SLOT(openRecentDatabase(QAction*))); + connect(m_ui->menuRecentDatabases, SIGNAL(aboutToShow()), this, SLOT(updateLastDatabasesMenu())); - m_copyAdditionalAttributeActions = new QActionGroup(m_ui->menuEntryCopyAttribute); - m_actionMultiplexer.connect( - m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*))); - connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu())); + m_copyAdditionalAttributeActions = new QActionGroup(m_ui->menuEntryCopyAttribute); + m_actionMultiplexer.connect( + m_copyAdditionalAttributeActions, SIGNAL(triggered(QAction*)), SLOT(copyAttribute(QAction*))); + connect(m_ui->menuEntryCopyAttribute, SIGNAL(aboutToShow()), this, SLOT(updateCopyAttributesMenu())); - Qt::Key globalAutoTypeKey = static_cast(config()->get("GlobalAutoTypeKey").toInt()); - Qt::KeyboardModifiers globalAutoTypeModifiers = - static_cast(config()->get("GlobalAutoTypeModifiers").toInt()); - if (globalAutoTypeKey > 0 && globalAutoTypeModifiers > 0) { - autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); - } + Qt::Key globalAutoTypeKey = static_cast(config()->get("GlobalAutoTypeKey").toInt()); + Qt::KeyboardModifiers globalAutoTypeModifiers = + static_cast(config()->get("GlobalAutoTypeModifiers").toInt()); + if (globalAutoTypeKey > 0 && globalAutoTypeModifiers > 0) { + autoType()->registerGlobalShortcut(globalAutoTypeKey, globalAutoTypeModifiers); + } - m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); + m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable()); - m_inactivityTimer = new InactivityTimer(this); - connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity())); + m_inactivityTimer = new InactivityTimer(this); + connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity())); #ifdef WITH_XC_TOUCHID - m_touchIDinactivityTimer = new InactivityTimer(this); - connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity())); + m_touchIDinactivityTimer = new InactivityTimer(this); + connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity())); #endif - applySettingsChanges(); + applySettingsChanges(); - m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N); - setShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); - setShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); - setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); - setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); - m_ui->actionLockDatabases->setShortcut(Qt::CTRL + Qt::Key_L); - setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); - setShortcut(m_ui->actionEntryNew, QKeySequence::New, Qt::CTRL + Qt::Key_N); - m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E); - m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D); - m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); - m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); - m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); - m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); - m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); - m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); - m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); - m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); - m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); + m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N); + setShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); + setShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); + setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); + setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); + m_ui->actionLockDatabases->setShortcut(Qt::CTRL + Qt::Key_L); + setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); + setShortcut(m_ui->actionEntryNew, QKeySequence::New, Qt::CTRL + Qt::Key_N); + m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E); + m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D); + m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); + m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); + m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); + m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); + m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); + m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); + m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); + m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); + m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus - // Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them - m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryOpenUrl->setShortcutVisibleInContextMenu(true); - m_ui->actionEntryCopyURL->setShortcutVisibleInContextMenu(true); + // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus + // Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them + m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryOpenUrl->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryCopyURL->setShortcutVisibleInContextMenu(true); #endif - // Control window state - new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(showMinimized())); - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_M, this, SLOT(hideWindow())); - // Control database tabs - new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(selectNextDatabaseTab())); - new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(selectNextDatabaseTab())); - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(selectPreviousDatabaseTab())); - new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(selectPreviousDatabaseTab())); - // Toggle password and username visibility in entry view - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden())); - new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_B, this, SLOT(toggleUsernamesHidden())); + // Control window state + new QShortcut(Qt::CTRL + Qt::Key_M, this, SLOT(showMinimized())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_M, this, SLOT(hideWindow())); + // Control database tabs + new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(selectNextDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(selectNextDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(selectPreviousDatabaseTab())); + new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(selectPreviousDatabaseTab())); + // Toggle password and username visibility in entry view + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden())); + new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_B, this, SLOT(toggleUsernamesHidden())); - m_ui->actionDatabaseNew->setIcon(filePath()->icon("actions", "document-new")); - m_ui->actionDatabaseOpen->setIcon(filePath()->icon("actions", "document-open")); - m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save")); - m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as")); - m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close")); - m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit")); - m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key")); - m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "document-encrypt")); - m_ui->actionQuit->setIcon(filePath()->icon("actions", "application-exit")); + m_ui->actionDatabaseNew->setIcon(filePath()->icon("actions", "document-new")); + m_ui->actionDatabaseOpen->setIcon(filePath()->icon("actions", "document-open")); + m_ui->actionDatabaseSave->setIcon(filePath()->icon("actions", "document-save")); + m_ui->actionDatabaseSaveAs->setIcon(filePath()->icon("actions", "document-save-as")); + m_ui->actionDatabaseClose->setIcon(filePath()->icon("actions", "document-close")); + m_ui->actionChangeDatabaseSettings->setIcon(filePath()->icon("actions", "document-edit")); + m_ui->actionChangeMasterKey->setIcon(filePath()->icon("actions", "database-change-key")); + m_ui->actionLockDatabases->setIcon(filePath()->icon("actions", "document-encrypt")); + m_ui->actionQuit->setIcon(filePath()->icon("actions", "application-exit")); - m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new")); - m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone")); - m_ui->actionEntryEdit->setIcon(filePath()->icon("actions", "entry-edit")); - m_ui->actionEntryDelete->setIcon(filePath()->icon("actions", "entry-delete")); - m_ui->actionEntryAutoType->setIcon(filePath()->icon("actions", "auto-type")); - m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); - m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); - m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy")); + m_ui->actionEntryNew->setIcon(filePath()->icon("actions", "entry-new")); + m_ui->actionEntryClone->setIcon(filePath()->icon("actions", "entry-clone")); + m_ui->actionEntryEdit->setIcon(filePath()->icon("actions", "entry-edit")); + m_ui->actionEntryDelete->setIcon(filePath()->icon("actions", "entry-delete")); + m_ui->actionEntryAutoType->setIcon(filePath()->icon("actions", "auto-type")); + m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); + m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); + m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy")); - m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new")); - m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); - m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); - m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); + m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new")); + m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); + m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); + m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); - m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); - m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); + m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); + m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); - m_ui->actionAbout->setIcon(filePath()->icon("actions", "help-about")); + m_ui->actionAbout->setIcon(filePath()->icon("actions", "help-about")); - m_actionMultiplexer.connect( - SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); - m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState())); - m_actionMultiplexer.connect(SIGNAL(entrySelectionChanged()), this, SLOT(setMenuActionState())); - m_actionMultiplexer.connect(SIGNAL(groupContextMenuRequested(QPoint)), this, SLOT(showGroupContextMenu(QPoint))); - m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)), this, SLOT(showEntryContextMenu(QPoint))); + m_actionMultiplexer.connect( + SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode))); + m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState())); + m_actionMultiplexer.connect(SIGNAL(entrySelectionChanged()), this, SLOT(setMenuActionState())); + m_actionMultiplexer.connect(SIGNAL(groupContextMenuRequested(QPoint)), this, SLOT(showGroupContextMenu(QPoint))); + m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)), this, SLOT(showEntryContextMenu(QPoint))); - // Notify search when the active database changes or gets locked - connect(m_ui->tabWidget, - SIGNAL(activateDatabaseChanged(DatabaseWidget*)), - search, - SLOT(databaseChanged(DatabaseWidget*))); - connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), search, SLOT(databaseChanged())); + // Notify search when the active database changes or gets locked + connect(m_ui->tabWidget, + SIGNAL(activateDatabaseChanged(DatabaseWidget*)), + search, + SLOT(databaseChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), search, SLOT(databaseChanged())); - connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int))); - connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); - connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); - connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); - connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); - connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); - connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(applySettingsChanges())); - connect(m_ui->settingsWidget, SIGNAL(apply()), SLOT(applySettingsChanges())); - connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(switchToDatabases())); - connect(m_ui->settingsWidget, SIGNAL(rejected()), SLOT(switchToDatabases())); + connect(m_ui->tabWidget, SIGNAL(tabNameChanged()), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(databaseTabChanged(int))); + connect(m_ui->tabWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); + connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); + connect(m_ui->tabWidget, SIGNAL(databaseUnlocked(DatabaseWidget*)), SLOT(databaseStatusChanged(DatabaseWidget*))); + connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(setMenuActionState())); + connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(updateWindowTitle())); + connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(applySettingsChanges())); + connect(m_ui->settingsWidget, SIGNAL(apply()), SLOT(applySettingsChanges())); + connect(m_ui->settingsWidget, SIGNAL(accepted()), SLOT(switchToDatabases())); + connect(m_ui->settingsWidget, SIGNAL(rejected()), SLOT(switchToDatabases())); - connect(m_ui->actionDatabaseNew, SIGNAL(triggered()), m_ui->tabWidget, SLOT(newDatabase())); - connect(m_ui->actionDatabaseOpen, SIGNAL(triggered()), m_ui->tabWidget, SLOT(openDatabase())); - connect(m_ui->actionDatabaseSave, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabase())); - connect(m_ui->actionDatabaseSaveAs, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabaseAs())); - connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab())); - connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase())); - connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); - connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); - connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); - connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); - connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); - connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); - connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit())); + connect(m_ui->actionDatabaseNew, SIGNAL(triggered()), m_ui->tabWidget, SLOT(newDatabase())); + connect(m_ui->actionDatabaseOpen, SIGNAL(triggered()), m_ui->tabWidget, SLOT(openDatabase())); + connect(m_ui->actionDatabaseSave, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabase())); + connect(m_ui->actionDatabaseSaveAs, SIGNAL(triggered()), m_ui->tabWidget, SLOT(saveDatabaseAs())); + connect(m_ui->actionDatabaseClose, SIGNAL(triggered()), m_ui->tabWidget, SLOT(closeCurrentDatabaseTab())); + connect(m_ui->actionDatabaseMerge, SIGNAL(triggered()), m_ui->tabWidget, SLOT(mergeDatabase())); + connect(m_ui->actionChangeMasterKey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeMasterKey())); + connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); + connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); + connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); + connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); + connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); + connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit())); - m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry())); - m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); - m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); - m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); + m_actionMultiplexer.connect(m_ui->actionEntryNew, SIGNAL(triggered()), SLOT(createEntry())); + m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); + m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); + m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); - m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); - m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); + m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); + m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); - m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); - m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes())); - m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); - m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); + m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); + m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes())); + m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); + m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl())); - m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); - m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); - m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); - m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); + m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); + m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); + m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); + m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); - connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); - connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); - connect(m_ui->passwordGeneratorWidget, SIGNAL(dialogTerminated()), SLOT(closePasswordGen())); + connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); + connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); + connect(m_ui->passwordGeneratorWidget, SIGNAL(dialogTerminated()), SLOT(closePasswordGen())); - connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); - connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); - connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); - connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); - connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); + connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); + connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); + connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); + connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); + connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); - connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); - connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl())); - connect(m_ui->actionBugReport, SIGNAL(triggered()), SLOT(openBugReportUrl())); + connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); + connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl())); + connect(m_ui->actionBugReport, SIGNAL(triggered()), SLOT(openBugReportUrl())); #ifdef Q_OS_MACOS - setUnifiedTitleAndToolBarOnMac(true); + setUnifiedTitleAndToolBarOnMac(true); #endif - // clang-format off + // clang-format off connect(m_ui->tabWidget, SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), this, SLOT(displayGlobalMessage(QString,MessageWidget::MessageType))); - // clang-format on + // clang-format on - connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage())); + connect(m_ui->tabWidget, SIGNAL(messageDismissGlobal()), this, SLOT(hideGlobalMessage())); - m_screenLockListener = new ScreenLockListener(this); - connect(m_screenLockListener, SIGNAL(screenLocked()), SLOT(handleScreenLock())); + m_screenLockListener = new ScreenLockListener(this); + connect(m_screenLockListener, SIGNAL(screenLocked()), SLOT(handleScreenLock())); - updateTrayIcon(); + updateTrayIcon(); - if (config()->hasAccessError()) { - m_ui->globalMessageWidget->showMessage(tr("Access error for config file %1").arg(config()->getFileName()), - MessageWidget::Error); - } + if (config()->hasAccessError()) { + m_ui->globalMessageWidget->showMessage(tr("Access error for config file %1").arg(config()->getFileName()), + MessageWidget::Error); + } #if !defined(KEEPASSXC_BUILD_TYPE_RELEASE) - m_ui->globalMessageWidget->showMessage( - tr("WARNING: You are using an unstable build of KeePassXC!\n" - "There is a high risk of corruption, maintain a backup of your databases.\n" - "This version is not meant for production use."), - MessageWidget::Warning, - -1); + m_ui->globalMessageWidget->showMessage( + tr("WARNING: You are using an unstable build of KeePassXC!\n" + "There is a high risk of corruption, maintain a backup of your databases.\n" + "This version is not meant for production use."), + MessageWidget::Warning, + -1); #elif (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) && QT_VERSION < QT_VERSION_CHECK(5, 6, 0)) - if (!config()->get("QtErrorMessageShown", false).toBool()) { - m_ui->globalMessageWidget->showMessage( - tr("WARNING: Your Qt version may cause KeePassXC to crash with an On-Screen Keyboard!\n" - "We recommend you use the AppImage available on our downloads page."), - MessageWidget::Warning, - -1); - config()->set("QtErrorMessageShown", true); - } + if (!config()->get("QtErrorMessageShown", false).toBool()) { + m_ui->globalMessageWidget->showMessage( + tr("WARNING: Your Qt version may cause KeePassXC to crash with an On-Screen Keyboard!\n" + "We recommend you use the AppImage available on our downloads page."), + MessageWidget::Warning, + -1); + config()->set("QtErrorMessageShown", true); + } #endif } @@ -403,748 +407,749 @@ MainWindow::~MainWindow() void MainWindow::showErrorMessage(const QString& message) { - m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); + m_ui->globalMessageWidget->showMessage(message, MessageWidget::Error); } void MainWindow::appExit() { - m_appExitCalled = true; - close(); + m_appExitCalled = true; + close(); } void MainWindow::updateLastDatabasesMenu() { - m_ui->menuRecentDatabases->clear(); + m_ui->menuRecentDatabases->clear(); - const QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList(); - for (const QString& database : lastDatabases) { - QAction* action = m_ui->menuRecentDatabases->addAction(database); - action->setData(database); - m_lastDatabasesActions->addAction(action); - } - m_ui->menuRecentDatabases->addSeparator(); - m_ui->menuRecentDatabases->addAction(m_clearHistoryAction); + const QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList(); + for (const QString& database : lastDatabases) { + QAction* action = m_ui->menuRecentDatabases->addAction(database); + action->setData(database); + m_lastDatabasesActions->addAction(action); + } + m_ui->menuRecentDatabases->addSeparator(); + m_ui->menuRecentDatabases->addAction(m_clearHistoryAction); } void MainWindow::updateCopyAttributesMenu() { - DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - if (!dbWidget) { - return; - } + DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (!dbWidget) { + return; + } - if (dbWidget->numberOfSelectedEntries() != 1) { - return; - } + if (dbWidget->numberOfSelectedEntries() != 1) { + return; + } - QList actions = m_ui->menuEntryCopyAttribute->actions(); - for (int i = m_countDefaultAttributes; i < actions.size(); i++) { - delete actions[i]; - } + QList actions = m_ui->menuEntryCopyAttribute->actions(); + for (int i = m_countDefaultAttributes; i < actions.size(); i++) { + delete actions[i]; + } - const QStringList customEntryAttributes = dbWidget->customEntryAttributes(); - for (const QString& key : customEntryAttributes) { - QAction* action = m_ui->menuEntryCopyAttribute->addAction(key); - action->setData(QVariant(key)); - m_copyAdditionalAttributeActions->addAction(action); - } + const QStringList customEntryAttributes = dbWidget->customEntryAttributes(); + for (const QString& key : customEntryAttributes) { + QAction* action = m_ui->menuEntryCopyAttribute->addAction(key); + action->setData(QVariant(key)); + m_copyAdditionalAttributeActions->addAction(action); + } } void MainWindow::openRecentDatabase(QAction* action) { - openDatabase(action->data().toString()); + openDatabase(action->data().toString()); } void MainWindow::clearLastDatabases() { - config()->set("LastDatabases", QVariant()); - bool inWelcomeWidget = (m_ui->stackedWidget->currentIndex() == 2); + config()->set("LastDatabases", QVariant()); + bool inWelcomeWidget = (m_ui->stackedWidget->currentIndex() == 2); - if (inWelcomeWidget) { - m_ui->welcomeWidget->refreshLastDatabases(); - } + if (inWelcomeWidget) { + m_ui->welcomeWidget->refreshLastDatabases(); + } } void MainWindow::openDatabase(const QString& filePath, const QString& pw, const QString& keyFile) { - if (pw.isEmpty() && keyFile.isEmpty()) { - m_ui->tabWidget->addDatabaseTab(filePath); - return; - } + if (pw.isEmpty() && keyFile.isEmpty()) { + m_ui->tabWidget->addDatabaseTab(filePath); + return; + } - auto db = QSharedPointer::create(); - auto key = QSharedPointer::create(); - if (!pw.isEmpty()) { - key->addKey(QSharedPointer::create(pw)); - } - if (!keyFile.isEmpty()) { - auto fileKey = QSharedPointer::create(); - fileKey->load(keyFile); - key->addKey(fileKey); - } - if (db->open(filePath, key, nullptr, false)) { - auto* dbWidget = new DatabaseWidget(db, this); - m_ui->tabWidget->addDatabaseTab(dbWidget); - dbWidget->switchToMainView(true); - } + auto db = QSharedPointer::create(); + auto key = QSharedPointer::create(); + if (!pw.isEmpty()) { + key->addKey(QSharedPointer::create(pw)); + } + if (!keyFile.isEmpty()) { + auto fileKey = QSharedPointer::create(); + fileKey->load(keyFile); + key->addKey(fileKey); + } + if (db->open(filePath, key, nullptr, false)) { + auto* dbWidget = new DatabaseWidget(db, this); + m_ui->tabWidget->addDatabaseTab(dbWidget); + dbWidget->switchToMainView(true); + } } void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) { - int currentIndex = m_ui->stackedWidget->currentIndex(); + int currentIndex = m_ui->stackedWidget->currentIndex(); - bool inDatabaseTabWidget = (currentIndex == DatabaseTabScreen); - bool inWelcomeWidget = (currentIndex == WelcomeScreen); - bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; + bool inDatabaseTabWidget = (currentIndex == DatabaseTabScreen); + bool inWelcomeWidget = (currentIndex == WelcomeScreen); + bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; - m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); + m_ui->actionDatabaseMerge->setEnabled(inDatabaseTabWidget); - m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); + m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); - if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { - DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - Q_ASSERT(dbWidget); + if (inDatabaseTabWidget && m_ui->tabWidget->currentIndex() != -1) { + DatabaseWidget* dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + Q_ASSERT(dbWidget); - if (mode == DatabaseWidget::Mode::None) { - mode = dbWidget->currentMode(); - } + if (mode == DatabaseWidget::Mode::None) { + mode = dbWidget->currentMode(); + } - switch (mode) { - case DatabaseWidget::Mode::ViewMode: { - // bool inSearch = dbWidget->isInSearchMode(); - bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && dbWidget->currentEntryHasFocus(); - bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && dbWidget->currentEntryHasFocus(); - bool groupSelected = dbWidget->isGroupSelected(); - bool recycleBinSelected = dbWidget->isRecycleBinSelected(); + switch (mode) { + case DatabaseWidget::Mode::ViewMode: { + // bool inSearch = dbWidget->isInSearchMode(); + bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && dbWidget->currentEntryHasFocus(); + bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && dbWidget->currentEntryHasFocus(); + bool groupSelected = dbWidget->isGroupSelected(); + bool recycleBinSelected = dbWidget->isRecycleBinSelected(); - m_ui->actionEntryNew->setEnabled(true); - m_ui->actionEntryClone->setEnabled(singleEntrySelected); - m_ui->actionEntryEdit->setEnabled(singleEntrySelected); - m_ui->actionEntryDelete->setEnabled(entriesSelected); - m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); - m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); - m_ui->actionEntryCopyPassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword()); - m_ui->actionEntryCopyURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); - m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes()); - m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected); - m_ui->menuEntryTotp->setEnabled(singleEntrySelected); - m_ui->actionEntryAutoType->setEnabled(singleEntrySelected); - m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); - m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); - m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); - m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); - m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); - m_ui->actionGroupNew->setEnabled(groupSelected); - m_ui->actionGroupEdit->setEnabled(groupSelected); - m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); - m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); - m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); - m_ui->actionChangeMasterKey->setEnabled(true); - m_ui->actionChangeDatabaseSettings->setEnabled(true); - m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); - m_ui->actionDatabaseSaveAs->setEnabled(true); - m_ui->actionExportCsv->setEnabled(true); - m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); + m_ui->actionEntryNew->setEnabled(true); + m_ui->actionEntryClone->setEnabled(singleEntrySelected); + m_ui->actionEntryEdit->setEnabled(singleEntrySelected); + m_ui->actionEntryDelete->setEnabled(entriesSelected); + m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); + m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); + m_ui->actionEntryCopyPassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword()); + m_ui->actionEntryCopyURL->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); + m_ui->actionEntryCopyNotes->setEnabled(singleEntrySelected && dbWidget->currentEntryHasNotes()); + m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected); + m_ui->menuEntryTotp->setEnabled(singleEntrySelected); + m_ui->actionEntryAutoType->setEnabled(singleEntrySelected); + m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl()); + m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); + m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionGroupNew->setEnabled(groupSelected); + m_ui->actionGroupEdit->setEnabled(groupSelected); + m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); + m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); + m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); + m_ui->actionChangeMasterKey->setEnabled(true); + m_ui->actionChangeDatabaseSettings->setEnabled(true); + m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); + m_ui->actionDatabaseSaveAs->setEnabled(true); + m_ui->actionExportCsv->setEnabled(true); + m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1); - m_searchWidgetAction->setEnabled(true); + m_searchWidgetAction->setEnabled(true); - break; - } - case DatabaseWidget::Mode::EditMode: - case DatabaseWidget::Mode::ImportMode: - case DatabaseWidget::Mode::LockedMode: { - const QList entryActions = m_ui->menuEntries->actions(); - for (QAction* action : entryActions) { - action->setEnabled(false); - } + break; + } + case DatabaseWidget::Mode::EditMode: + case DatabaseWidget::Mode::ImportMode: + case DatabaseWidget::Mode::LockedMode: { + const QList entryActions = m_ui->menuEntries->actions(); + for (QAction* action : entryActions) { + action->setEnabled(false); + } - const QList groupActions = m_ui->menuGroups->actions(); - for (QAction* action : groupActions) { - action->setEnabled(false); - } + const QList groupActions = m_ui->menuGroups->actions(); + for (QAction* action : groupActions) { + action->setEnabled(false); + } - m_ui->actionChangeMasterKey->setEnabled(false); - m_ui->actionChangeDatabaseSettings->setEnabled(false); - m_ui->actionDatabaseSave->setEnabled(false); - m_ui->actionDatabaseSaveAs->setEnabled(false); - m_ui->actionExportCsv->setEnabled(false); - m_ui->actionDatabaseMerge->setEnabled(false); + m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionChangeDatabaseSettings->setEnabled(false); + m_ui->actionDatabaseSave->setEnabled(false); + m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionExportCsv->setEnabled(false); + m_ui->actionDatabaseMerge->setEnabled(false); - m_searchWidgetAction->setEnabled(false); - break; - } - default: - Q_ASSERT(false); - } - m_ui->actionDatabaseClose->setEnabled(true); - } else { - const QList entryActions = m_ui->menuEntries->actions(); - for (QAction* action : entryActions) { - action->setEnabled(false); - } + m_searchWidgetAction->setEnabled(false); + break; + } + default: + Q_ASSERT(false); + } + m_ui->actionDatabaseClose->setEnabled(true); + } else { + const QList entryActions = m_ui->menuEntries->actions(); + for (QAction* action : entryActions) { + action->setEnabled(false); + } - const QList groupActions = m_ui->menuGroups->actions(); - for (QAction* action : groupActions) { - action->setEnabled(false); - } + const QList groupActions = m_ui->menuGroups->actions(); + for (QAction* action : groupActions) { + action->setEnabled(false); + } - m_ui->actionChangeMasterKey->setEnabled(false); - m_ui->actionChangeDatabaseSettings->setEnabled(false); - m_ui->actionDatabaseSave->setEnabled(false); - m_ui->actionDatabaseSaveAs->setEnabled(false); - m_ui->actionDatabaseClose->setEnabled(false); - m_ui->actionExportCsv->setEnabled(false); - m_ui->actionDatabaseMerge->setEnabled(false); + m_ui->actionChangeMasterKey->setEnabled(false); + m_ui->actionChangeDatabaseSettings->setEnabled(false); + m_ui->actionDatabaseSave->setEnabled(false); + m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionDatabaseClose->setEnabled(false); + m_ui->actionExportCsv->setEnabled(false); + m_ui->actionDatabaseMerge->setEnabled(false); - m_searchWidgetAction->setEnabled(false); - } + m_searchWidgetAction->setEnabled(false); + } - if ((currentIndex == PasswordGeneratorScreen) != m_ui->actionPasswordGenerator->isChecked()) { - bool blocked = m_ui->actionPasswordGenerator->blockSignals(true); - m_ui->actionPasswordGenerator->toggle(); - m_ui->actionPasswordGenerator->blockSignals(blocked); - } else if ((currentIndex == SettingsScreen) != m_ui->actionSettings->isChecked()) { - bool blocked = m_ui->actionSettings->blockSignals(true); - m_ui->actionSettings->toggle(); - m_ui->actionSettings->blockSignals(blocked); - } + if ((currentIndex == PasswordGeneratorScreen) != m_ui->actionPasswordGenerator->isChecked()) { + bool blocked = m_ui->actionPasswordGenerator->blockSignals(true); + m_ui->actionPasswordGenerator->toggle(); + m_ui->actionPasswordGenerator->blockSignals(blocked); + } else if ((currentIndex == SettingsScreen) != m_ui->actionSettings->isChecked()) { + bool blocked = m_ui->actionSettings->blockSignals(true); + m_ui->actionSettings->toggle(); + m_ui->actionSettings->blockSignals(blocked); + } } void MainWindow::updateWindowTitle() { - QString customWindowTitlePart; - int stackedWidgetIndex = m_ui->stackedWidget->currentIndex(); - int tabWidgetIndex = m_ui->tabWidget->currentIndex(); - bool isModified = m_ui->tabWidget->isModified(tabWidgetIndex); + QString customWindowTitlePart; + int stackedWidgetIndex = m_ui->stackedWidget->currentIndex(); + int tabWidgetIndex = m_ui->tabWidget->currentIndex(); + bool isModified = m_ui->tabWidget->isModified(tabWidgetIndex); - if (stackedWidgetIndex == DatabaseTabScreen && tabWidgetIndex != -1) { - customWindowTitlePart = m_ui->tabWidget->tabName(tabWidgetIndex); - if (isModified) { - // remove asterisk '*' from title - customWindowTitlePart.remove(customWindowTitlePart.size() - 1, 1); - } - m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave(tabWidgetIndex)); - } else if (stackedWidgetIndex == 1) { - customWindowTitlePart = tr("Settings"); - } + if (stackedWidgetIndex == DatabaseTabScreen && tabWidgetIndex != -1) { + customWindowTitlePart = m_ui->tabWidget->tabName(tabWidgetIndex); + if (isModified) { + // remove asterisk '*' from title + customWindowTitlePart.remove(customWindowTitlePart.size() - 1, 1); + } + m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave(tabWidgetIndex)); + } else if (stackedWidgetIndex == 1) { + customWindowTitlePart = tr("Settings"); + } - QString windowTitle; - if (customWindowTitlePart.isEmpty()) { - windowTitle = BaseWindowTitle; - } else { - windowTitle = QString("%1[*] - %2").arg(customWindowTitlePart, BaseWindowTitle); - } + QString windowTitle; + if (customWindowTitlePart.isEmpty()) { + windowTitle = BaseWindowTitle; + } else { + windowTitle = QString("%1[*] - %2").arg(customWindowTitlePart, BaseWindowTitle); + } - if (customWindowTitlePart.isEmpty() || stackedWidgetIndex == 1) { - setWindowFilePath(""); - } else { - setWindowFilePath(m_ui->tabWidget->databaseWidgetFromIndex(tabWidgetIndex)->database()->filePath()); - } + if (customWindowTitlePart.isEmpty() || stackedWidgetIndex == 1) { + setWindowFilePath(""); + } else { + setWindowFilePath(m_ui->tabWidget->databaseWidgetFromIndex(tabWidgetIndex)->database()->filePath()); + } - setWindowTitle(windowTitle); - setWindowModified(isModified); + setWindowTitle(windowTitle); + setWindowModified(isModified); } void MainWindow::showAboutDialog() { - auto* aboutDialog = new AboutDialog(this); - aboutDialog->open(); + auto* aboutDialog = new AboutDialog(this); + aboutDialog->open(); } void MainWindow::openDonateUrl() { - QDesktopServices::openUrl(QUrl("https://keepassxc.org/donate")); + QDesktopServices::openUrl(QUrl("https://keepassxc.org/donate")); } void MainWindow::openBugReportUrl() { - QDesktopServices::openUrl(QUrl("https://github.com/keepassxreboot/keepassxc/issues")); + QDesktopServices::openUrl(QUrl("https://github.com/keepassxreboot/keepassxc/issues")); } void MainWindow::switchToDatabases() { - if (m_ui->tabWidget->currentIndex() == -1) { - m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); - } else { - m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); - } + if (m_ui->tabWidget->currentIndex() == -1) { + m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); + } else { + m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); + } } void MainWindow::switchToSettings(bool enabled) { - if (enabled) { - m_ui->settingsWidget->loadSettings(); - m_ui->stackedWidget->setCurrentIndex(SettingsScreen); - } else { - switchToDatabases(); - } + if (enabled) { + m_ui->settingsWidget->loadSettings(); + m_ui->stackedWidget->setCurrentIndex(SettingsScreen); + } else { + switchToDatabases(); + } } void MainWindow::switchToPasswordGen(bool enabled) { - if (enabled) { - m_ui->passwordGeneratorWidget->loadSettings(); - m_ui->passwordGeneratorWidget->regeneratePassword(); - m_ui->passwordGeneratorWidget->setStandaloneMode(true); - m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); - } else { - m_ui->passwordGeneratorWidget->saveSettings(); - switchToDatabases(); - } + if (enabled) { + m_ui->passwordGeneratorWidget->loadSettings(); + m_ui->passwordGeneratorWidget->regeneratePassword(); + m_ui->passwordGeneratorWidget->setStandaloneMode(true); + m_ui->stackedWidget->setCurrentIndex(PasswordGeneratorScreen); + } else { + m_ui->passwordGeneratorWidget->saveSettings(); + switchToDatabases(); + } } void MainWindow::closePasswordGen() { - switchToPasswordGen(false); + switchToPasswordGen(false); } void MainWindow::switchToNewDatabase() { - m_ui->tabWidget->newDatabase(); - switchToDatabases(); + m_ui->tabWidget->newDatabase(); + switchToDatabases(); } void MainWindow::switchToOpenDatabase() { - m_ui->tabWidget->openDatabase(); - switchToDatabases(); + m_ui->tabWidget->openDatabase(); + switchToDatabases(); } void MainWindow::switchToDatabaseFile(const QString& file) { - m_ui->tabWidget->addDatabaseTab(file); - switchToDatabases(); + m_ui->tabWidget->addDatabaseTab(file); + switchToDatabases(); } void MainWindow::switchToKeePass1Database() { - m_ui->tabWidget->importKeePass1Database(); - switchToDatabases(); + m_ui->tabWidget->importKeePass1Database(); + switchToDatabases(); } void MainWindow::switchToCsvImport() { - m_ui->tabWidget->importCsv(); - switchToDatabases(); + m_ui->tabWidget->importCsv(); + switchToDatabases(); } void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget) { - Q_UNUSED(dbWidget); - updateTrayIcon(); + Q_UNUSED(dbWidget); + updateTrayIcon(); } void MainWindow::selectNextDatabaseTab() { - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - int index = m_ui->tabWidget->currentIndex() + 1; - if (index >= m_ui->tabWidget->count()) { - m_ui->tabWidget->setCurrentIndex(0); - } else { - m_ui->tabWidget->setCurrentIndex(index); - } - } + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->currentIndex() + 1; + if (index >= m_ui->tabWidget->count()) { + m_ui->tabWidget->setCurrentIndex(0); + } else { + m_ui->tabWidget->setCurrentIndex(index); + } + } } void MainWindow::selectPreviousDatabaseTab() { - if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - int index = m_ui->tabWidget->currentIndex() - 1; - if (index < 0) { - m_ui->tabWidget->setCurrentIndex(m_ui->tabWidget->count() - 1); - } else { - m_ui->tabWidget->setCurrentIndex(index); - } - } + if (m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + int index = m_ui->tabWidget->currentIndex() - 1; + if (index < 0) { + m_ui->tabWidget->setCurrentIndex(m_ui->tabWidget->count() - 1); + } else { + m_ui->tabWidget->setCurrentIndex(index); + } + } } void MainWindow::databaseTabChanged(int tabIndex) { - if (tabIndex != -1 && m_ui->stackedWidget->currentIndex() == WelcomeScreen) { - m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); - } else if (tabIndex == -1 && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { - m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); - } + if (tabIndex != -1 && m_ui->stackedWidget->currentIndex() == WelcomeScreen) { + m_ui->stackedWidget->setCurrentIndex(DatabaseTabScreen); + } else if (tabIndex == -1 && m_ui->stackedWidget->currentIndex() == DatabaseTabScreen) { + m_ui->stackedWidget->setCurrentIndex(WelcomeScreen); + } - m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget()); + m_actionMultiplexer.setCurrentObject(m_ui->tabWidget->currentDatabaseWidget()); } void MainWindow::togglePasswordsHidden() { - auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - if (dbWidget) { - dbWidget->setPasswordsHidden(!dbWidget->isPasswordsHidden()); - } + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget) { + dbWidget->setPasswordsHidden(!dbWidget->isPasswordsHidden()); + } } void MainWindow::toggleUsernamesHidden() { - auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); - if (dbWidget) { - dbWidget->setUsernamesHidden(!dbWidget->isUsernamesHidden()); - } + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget) { + dbWidget->setUsernamesHidden(!dbWidget->isUsernamesHidden()); + } } void MainWindow::closeEvent(QCloseEvent* event) { - // ignore double close events (happens on macOS when closing from the dock) - if (m_appExiting) { - event->accept(); - return; - } + // ignore double close events (happens on macOS when closing from the dock) + if (m_appExiting) { + event->accept(); + return; + } - if (config()->get("GUI/MinimizeOnClose").toBool() && !m_appExitCalled) { - event->ignore(); - hideWindow(); - return; - } + if (config()->get("GUI/MinimizeOnClose").toBool() && !m_appExitCalled) { + event->ignore(); + hideWindow(); + return; + } - bool accept = saveLastDatabases(); + bool accept = saveLastDatabases(); - if (accept) { - m_appExiting = true; - saveWindowInformation(); + if (accept) { + m_appExiting = true; + saveWindowInformation(); - event->accept(); - QApplication::quit(); - } else { - event->ignore(); - } + event->accept(); + QApplication::quit(); + } else { + event->ignore(); + } } void MainWindow::changeEvent(QEvent* event) { - if ((event->type() == QEvent::WindowStateChange) && isMinimized()) { - if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible() - && config()->get("GUI/MinimizeToTray").toBool()) { - event->ignore(); - QTimer::singleShot(0, this, SLOT(hide())); - } + if ((event->type() == QEvent::WindowStateChange) && isMinimized()) { + if (isTrayIconEnabled() && m_trayIcon && m_trayIcon->isVisible() + && config()->get("GUI/MinimizeToTray").toBool()) { + event->ignore(); + QTimer::singleShot(0, this, SLOT(hide())); + } - if (config()->get("security/lockdatabaseminimize").toBool()) { - m_ui->tabWidget->lockDatabases(); - } - } else { - QMainWindow::changeEvent(event); - } + if (config()->get("security/lockdatabaseminimize").toBool()) { + m_ui->tabWidget->lockDatabases(); + } + } else { + QMainWindow::changeEvent(event); + } } void MainWindow::saveWindowInformation() { - if (isVisible()) { - config()->set("GUI/MainWindowGeometry", saveGeometry()); - } + if (isVisible()) { + config()->set("GUI/MainWindowGeometry", saveGeometry()); + } } bool MainWindow::saveLastDatabases() { - bool accept; - m_openDatabases.clear(); - bool openPreviousDatabasesOnStartup = config()->get("OpenPreviousDatabasesOnStartup").toBool(); + bool accept; + m_openDatabases.clear(); + bool openPreviousDatabasesOnStartup = config()->get("OpenPreviousDatabasesOnStartup").toBool(); - if (openPreviousDatabasesOnStartup) { - connect(m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); - } + if (openPreviousDatabasesOnStartup) { + connect( + m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); + } - accept = m_ui->tabWidget->closeAllDatabaseTabs(); + accept = m_ui->tabWidget->closeAllDatabaseTabs(); - if (openPreviousDatabasesOnStartup) { - disconnect( - m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); - config()->set("LastOpenedDatabases", m_openDatabases); - } + if (openPreviousDatabasesOnStartup) { + disconnect( + m_ui->tabWidget, SIGNAL(databaseClosed(const QString&)), this, SLOT(rememberOpenDatabases(const QString&))); + config()->set("LastOpenedDatabases", m_openDatabases); + } - return accept; + return accept; } void MainWindow::updateTrayIcon() { - if (isTrayIconEnabled()) { - if (!m_trayIcon) { - m_trayIcon = new QSystemTrayIcon(this); - QMenu* menu = new QMenu(this); + if (isTrayIconEnabled()) { + if (!m_trayIcon) { + m_trayIcon = new QSystemTrayIcon(this); + QMenu* menu = new QMenu(this); - QAction* actionToggle = new QAction(tr("Toggle window"), menu); - menu->addAction(actionToggle); + QAction* actionToggle = new QAction(tr("Toggle window"), menu); + menu->addAction(actionToggle); #ifdef Q_OS_MACOS - QAction* actionQuit = new QAction(tr("Quit KeePassXC"), menu); - menu->addAction(actionQuit); + QAction* actionQuit = new QAction(tr("Quit KeePassXC"), menu); + menu->addAction(actionQuit); - connect(actionQuit, SIGNAL(triggered()), SLOT(appExit())); + connect(actionQuit, SIGNAL(triggered()), SLOT(appExit())); #else - menu->addAction(m_ui->actionQuit); + menu->addAction(m_ui->actionQuit); - connect(m_trayIcon, - SIGNAL(activated(QSystemTrayIcon::ActivationReason)), - SLOT(trayIconTriggered(QSystemTrayIcon::ActivationReason))); + connect(m_trayIcon, + SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + SLOT(trayIconTriggered(QSystemTrayIcon::ActivationReason))); #endif - connect(actionToggle, SIGNAL(triggered()), SLOT(toggleWindow())); + connect(actionToggle, SIGNAL(triggered()), SLOT(toggleWindow())); - m_trayIcon->setContextMenu(menu); + m_trayIcon->setContextMenu(menu); - m_trayIcon->setIcon(filePath()->trayIcon()); - m_trayIcon->show(); - } - if (m_ui->tabWidget->hasLockableDatabases()) { - m_trayIcon->setIcon(filePath()->trayIconUnlocked()); - } else { - m_trayIcon->setIcon(filePath()->trayIconLocked()); - } - } else { - if (m_trayIcon) { - m_trayIcon->hide(); - delete m_trayIcon; - m_trayIcon = nullptr; - } - } + m_trayIcon->setIcon(filePath()->trayIcon()); + m_trayIcon->show(); + } + if (m_ui->tabWidget->hasLockableDatabases()) { + m_trayIcon->setIcon(filePath()->trayIconUnlocked()); + } else { + m_trayIcon->setIcon(filePath()->trayIconLocked()); + } + } else { + if (m_trayIcon) { + m_trayIcon->hide(); + delete m_trayIcon; + m_trayIcon = nullptr; + } + } } void MainWindow::showEntryContextMenu(const QPoint& globalPos) { - m_ui->menuEntries->popup(globalPos); + m_ui->menuEntries->popup(globalPos); } void MainWindow::showGroupContextMenu(const QPoint& globalPos) { - m_ui->menuGroups->popup(globalPos); + m_ui->menuGroups->popup(globalPos); } void MainWindow::setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback) { - if (!QKeySequence::keyBindings(standard).isEmpty()) { - action->setShortcuts(standard); - } else if (fallback != 0) { - action->setShortcut(QKeySequence(fallback)); - } + if (!QKeySequence::keyBindings(standard).isEmpty()) { + action->setShortcuts(standard); + } else if (fallback != 0) { + action->setShortcut(QKeySequence(fallback)); + } } void MainWindow::rememberOpenDatabases(const QString& filePath) { - m_openDatabases.prepend(filePath); + m_openDatabases.prepend(filePath); } void MainWindow::applySettingsChanges() { - int timeout = config()->get("security/lockdatabaseidlesec").toInt() * 1000; - if (timeout <= 0) { - timeout = 60; - } + int timeout = config()->get("security/lockdatabaseidlesec").toInt() * 1000; + if (timeout <= 0) { + timeout = 60; + } - m_inactivityTimer->setInactivityTimeout(timeout); - if (config()->get("security/lockdatabaseidle").toBool()) { - m_inactivityTimer->activate(); - } else { - m_inactivityTimer->deactivate(); - } + m_inactivityTimer->setInactivityTimeout(timeout); + if (config()->get("security/lockdatabaseidle").toBool()) { + m_inactivityTimer->activate(); + } else { + m_inactivityTimer->deactivate(); + } #ifdef WITH_XC_TOUCHID - // forget TouchID (in minutes) - timeout = config()->get("security/resettouchidtimeout").toInt() * 60 * 1000; - if (timeout <= 0) { - timeout = 30 * 60 * 1000; - } + // forget TouchID (in minutes) + timeout = config()->get("security/resettouchidtimeout").toInt() * 60 * 1000; + if (timeout <= 0) { + timeout = 30 * 60 * 1000; + } - m_touchIDinactivityTimer->setInactivityTimeout(timeout); - if (config()->get("security/resettouchid").toBool()) { - m_touchIDinactivityTimer->activate(); - } else { - m_touchIDinactivityTimer->deactivate(); - } + m_touchIDinactivityTimer->setInactivityTimeout(timeout); + if (config()->get("security/resettouchid").toBool()) { + m_touchIDinactivityTimer->activate(); + } else { + m_touchIDinactivityTimer->deactivate(); + } #endif - m_ui->toolBar->setHidden(config()->get("GUI/HideToolbar").toBool()); + m_ui->toolBar->setHidden(config()->get("GUI/HideToolbar").toBool()); - updateTrayIcon(); + updateTrayIcon(); } void MainWindow::trayIconTriggered(QSystemTrayIcon::ActivationReason reason) { - if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::MiddleClick) { - toggleWindow(); - } + if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::MiddleClick) { + toggleWindow(); + } } void MainWindow::hideWindow() { - saveWindowInformation(); + saveWindowInformation(); #if !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS) - // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at - // the same time (which would happen if both minimize on startup and minimize to tray are set) - // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. - // TODO: Add an explanation for why this is also not done on Mac (or remove the check) - setWindowState(windowState() | Qt::WindowMinimized); + // On some Linux systems, the window should NOT be minimized and hidden (i.e. not shown), at + // the same time (which would happen if both minimize on startup and minimize to tray are set) + // since otherwise it causes problems on restore as seen on issue #1595. Hiding it is enough. + // TODO: Add an explanation for why this is also not done on Mac (or remove the check) + setWindowState(windowState() | Qt::WindowMinimized); #endif - // Only hide if tray icon is active, otherwise window will be gone forever - if (isTrayIconEnabled()) { - hide(); - } else { - showMinimized(); - } + // Only hide if tray icon is active, otherwise window will be gone forever + if (isTrayIconEnabled()) { + hide(); + } else { + showMinimized(); + } - if (config()->get("security/lockdatabaseminimize").toBool()) { - m_ui->tabWidget->lockDatabases(); - } + if (config()->get("security/lockdatabaseminimize").toBool()) { + m_ui->tabWidget->lockDatabases(); + } } void MainWindow::toggleWindow() { - if (isVisible() && !isMinimized()) { - hideWindow(); - } else { - bringToFront(); + if (isVisible() && !isMinimized()) { + hideWindow(); + } else { + bringToFront(); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) && (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) - // re-register global D-Bus menu (needed on Ubuntu with Unity) - // see https://github.com/keepassxreboot/keepassxc/issues/271 - // and https://bugreports.qt.io/browse/QTBUG-58723 - // check for !isVisible(), because isNativeMenuBar() does not work with appmenu-qt5 - if (!m_ui->menubar->isVisible()) { - QDBusMessage msg = QDBusMessage::createMethodCall("com.canonical.AppMenu.Registrar", - "/com/canonical/AppMenu/Registrar", - "com.canonical.AppMenu.Registrar", - "RegisterWindow"); - QList args; - args << QVariant::fromValue(static_cast(winId())) - << QVariant::fromValue(QDBusObjectPath("/MenuBar/1")); - msg.setArguments(args); - QDBusConnection::sessionBus().send(msg); - } + // re-register global D-Bus menu (needed on Ubuntu with Unity) + // see https://github.com/keepassxreboot/keepassxc/issues/271 + // and https://bugreports.qt.io/browse/QTBUG-58723 + // check for !isVisible(), because isNativeMenuBar() does not work with appmenu-qt5 + if (!m_ui->menubar->isVisible()) { + QDBusMessage msg = QDBusMessage::createMethodCall("com.canonical.AppMenu.Registrar", + "/com/canonical/AppMenu/Registrar", + "com.canonical.AppMenu.Registrar", + "RegisterWindow"); + QList args; + args << QVariant::fromValue(static_cast(winId())) + << QVariant::fromValue(QDBusObjectPath("/MenuBar/1")); + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); + } #endif - } + } } void MainWindow::lockDatabasesAfterInactivity() { - // ignore event if a modal dialog is open (such as a message box or file dialog) - if (QApplication::activeModalWidget()) { - return; - } + // ignore event if a modal dialog is open (such as a message box or file dialog) + if (QApplication::activeModalWidget()) { + return; + } - m_ui->tabWidget->lockDatabases(); + m_ui->tabWidget->lockDatabases(); } void MainWindow::forgetTouchIDAfterInactivity() { #ifdef WITH_XC_TOUCHID - TouchID::getInstance().reset(); + TouchID::getInstance().reset(); #endif } bool MainWindow::isTrayIconEnabled() const { - return config()->get("GUI/ShowTrayIcon").toBool() && QSystemTrayIcon::isSystemTrayAvailable(); + return config()->get("GUI/ShowTrayIcon").toBool() && QSystemTrayIcon::isSystemTrayAvailable(); } void MainWindow::displayGlobalMessage(const QString& text, - MessageWidget::MessageType type, - bool showClosebutton, - int autoHideTimeout) + MessageWidget::MessageType type, + bool showClosebutton, + int autoHideTimeout) { - m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); - m_ui->globalMessageWidget->showMessage(text, type, autoHideTimeout); + m_ui->globalMessageWidget->setCloseButtonVisible(showClosebutton); + m_ui->globalMessageWidget->showMessage(text, type, autoHideTimeout); } void MainWindow::displayTabMessage(const QString& text, - MessageWidget::MessageType type, - bool showClosebutton, - int autoHideTimeout) + MessageWidget::MessageType type, + bool showClosebutton, + int autoHideTimeout) { - m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type, showClosebutton, autoHideTimeout); + m_ui->tabWidget->currentDatabaseWidget()->showMessage(text, type, showClosebutton, autoHideTimeout); } void MainWindow::hideGlobalMessage() { - m_ui->globalMessageWidget->hideMessage(); + m_ui->globalMessageWidget->hideMessage(); } void MainWindow::showYubiKeyPopup() { - displayGlobalMessage(tr("Please touch the button on your YubiKey!"), - MessageWidget::Information, - false, - MessageWidget::DisableAutoHide); - setEnabled(false); + displayGlobalMessage(tr("Please touch the button on your YubiKey!"), + MessageWidget::Information, + false, + MessageWidget::DisableAutoHide); + setEnabled(false); } void MainWindow::hideYubiKeyPopup() { - hideGlobalMessage(); - setEnabled(true); + hideGlobalMessage(); + setEnabled(true); } void MainWindow::bringToFront() { - ensurePolished(); - setWindowState(windowState() & ~Qt::WindowMinimized); - show(); - raise(); - activateWindow(); + ensurePolished(); + setWindowState(windowState() & ~Qt::WindowMinimized); + show(); + raise(); + activateWindow(); } void MainWindow::handleScreenLock() { - if (config()->get("security/lockdatabasescreenlock").toBool()) { - lockDatabasesAfterInactivity(); - } + if (config()->get("security/lockdatabasescreenlock").toBool()) { + lockDatabasesAfterInactivity(); + } #ifdef WITH_XC_TOUCHID - if (config()->get("security/resettouchidscreenlock").toBool()) { - forgetTouchIDAfterInactivity(); - } + if (config()->get("security/resettouchidscreenlock").toBool()) { + forgetTouchIDAfterInactivity(); + } #endif } QStringList MainWindow::kdbxFilesFromUrls(const QList& urls) { - QStringList kdbxFiles; - for (const QUrl& url : urls) { - const QFileInfo fInfo(url.toLocalFile()); - const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx"; - if (isKdbxFile) { - kdbxFiles.append(fInfo.absoluteFilePath()); - } - } + QStringList kdbxFiles; + for (const QUrl& url : urls) { + const QFileInfo fInfo(url.toLocalFile()); + const bool isKdbxFile = fInfo.isFile() && fInfo.suffix().toLower() == "kdbx"; + if (isKdbxFile) { + kdbxFiles.append(fInfo.absoluteFilePath()); + } + } - return kdbxFiles; + return kdbxFiles; } void MainWindow::dragEnterEvent(QDragEnterEvent* event) { - const QMimeData* mimeData = event->mimeData(); - if (mimeData->hasUrls()) { - const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); - if (!kdbxFiles.isEmpty()) { - event->acceptProposedAction(); - } - } + const QMimeData* mimeData = event->mimeData(); + if (mimeData->hasUrls()) { + const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); + if (!kdbxFiles.isEmpty()) { + event->acceptProposedAction(); + } + } } void MainWindow::dropEvent(QDropEvent* event) { - const QMimeData* mimeData = event->mimeData(); - if (mimeData->hasUrls()) { - const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); - if (!kdbxFiles.isEmpty()) { - event->acceptProposedAction(); - } - for (const QString& kdbxFile : kdbxFiles) { - openDatabase(kdbxFile); - } - } + const QMimeData* mimeData = event->mimeData(); + if (mimeData->hasUrls()) { + const QStringList kdbxFiles = kdbxFilesFromUrls(mimeData->urls()); + if (!kdbxFiles.isEmpty()) { + event->acceptProposedAction(); + } + for (const QString& kdbxFile : kdbxFiles) { + openDatabase(kdbxFile); + } + } } void MainWindow::closeAllDatabases() { - m_ui->tabWidget->closeAllDatabaseTabs(); + m_ui->tabWidget->closeAllDatabaseTabs(); } void MainWindow::lockAllDatabases() { - lockDatabasesAfterInactivity(); + lockDatabasesAfterInactivity(); } From 8c5da624e511ad01bcbbd5b10678ea66bfe1cab4 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Fri, 18 Jan 2019 21:53:02 -0500 Subject: [PATCH 22/23] KeeShare code formatting and spelling correction --- src/CMakeLists.txt | 2 +- .../DatabaseSettingsWidgetKeeShare.cpp | 4 +- src/keeshare/KeeShare.cpp | 22 +- src/keeshare/KeeShare.h | 4 +- src/keeshare/KeeShareSettings.cpp | 18 +- src/keeshare/KeeShareSettings.h | 15 +- src/keeshare/SettingsWidgetKeeShare.cpp | 274 +++++++------ src/keeshare/ShareObserver.cpp | 295 +++++++------- src/keeshare/ShareObserver.h | 7 +- .../group/EditGroupWidgetKeeShare.cpp | 384 +++++++++--------- 10 files changed, 538 insertions(+), 487 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 355748a94..854ce877f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,7 +202,7 @@ add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing") add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)") add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser") add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent") -add_feature_info(KeeSharee WITH_XC_KEESHARE "Sharing integration with KeeShare") +add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare") add_feature_info(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") if(APPLE) diff --git a/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp b/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp index 6180c184e..85315c332 100644 --- a/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp +++ b/src/keeshare/DatabaseSettingsWidgetKeeShare.cpp @@ -45,8 +45,8 @@ void DatabaseSettingsWidgetKeeShare::loadSettings(QSharedPointer db) m_referencesModel.reset(new QStandardItemModel()); - m_referencesModel->setHorizontalHeaderLabels( - QStringList() << tr("Breadcrumb") << tr("Type") << tr("Path") << tr("Last Signer") << tr("Certificates")); + m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Breadcrumb") << tr("Type") << tr("Path") + << tr("Last Signer") << tr("Certificates")); const QList groups = db->rootGroup()->groupsRecursive(true); for (const Group* group : groups) { if (!KeeShare::isShared(group)) { diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp index ba188a974..751429558 100644 --- a/src/keeshare/KeeShare.cpp +++ b/src/keeshare/KeeShare.cpp @@ -32,11 +32,11 @@ namespace { -static const QString KeeShare_Reference("KeeShare/Reference"); -static const QString KeeShare_Own("KeeShare/Settings.own"); -static const QString KeeShare_Foreign("KeeShare/Settings.foreign"); -static const QString KeeShare_Active("KeeShare/Settings.active"); -} + static const QString KeeShare_Reference("KeeShare/Reference"); + static const QString KeeShare_Own("KeeShare/Settings.own"); + static const QString KeeShare_Foreign("KeeShare/Settings.foreign"); + static const QString KeeShare_Active("KeeShare/Settings.active"); +} // namespace KeeShare* KeeShare::m_instance = nullptr; @@ -129,13 +129,13 @@ bool KeeShare::isEnabled(const Group* group) { const auto reference = KeeShare::referenceOf(group); #if !defined(WITH_XC_KEESHARE_SECURE) - if (reference.path.endsWith(signedContainerFileType(), Qt::CaseInsensitive)){ - return false; + if (reference.path.endsWith(signedContainerFileType(), Qt::CaseInsensitive)) { + return false; } #endif #if !defined(WITH_XC_KEESHARE_INSECURE) - if (reference.path.endsWith(unsignedContainerFileType(), Qt::CaseInsensitive)){ - return false; + if (reference.path.endsWith(unsignedContainerFileType(), Qt::CaseInsensitive)) { + return false; } #endif const auto active = KeeShare::active(); @@ -198,13 +198,13 @@ void KeeShare::connectDatabase(QSharedPointer newDb, QSharedPointer SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent) - : QWidget(parent) - , m_ui(new Ui::SettingsWidgetKeeShare()) + : QWidget(parent) + , m_ui(new Ui::SettingsWidgetKeeShare()) { - m_ui->setupUi(this); + m_ui->setupUi(this); #if !defined(WITH_XC_KEESHARE_SECURE) - // Setting does not help the user of Version without signed export - m_ui->ownCertificateGroupBox->setVisible(false); + // Setting does not help the user of Version without signed export + m_ui->ownCertificateGroupBox->setVisible(false); #endif - connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString))); + connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString))); - connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate())); - connect(m_ui->importOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(importCertificate())); - connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate())); + connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate())); + connect(m_ui->importOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(importCertificate())); + connect(m_ui->exportOwnCertificateButton, SIGNAL(clicked(bool)), SLOT(exportCertificate())); - connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates())); - connect(m_ui->askImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(askSelectedCertificates())); - connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates())); - connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates())); + connect(m_ui->trustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(trustSelectedCertificates())); + connect(m_ui->askImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(askSelectedCertificates())); + connect(m_ui->untrustImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(untrustSelectedCertificates())); + connect(m_ui->removeImportedCertificateButton, SIGNAL(clicked(bool)), SLOT(removeSelectedCertificates())); } SettingsWidgetKeeShare::~SettingsWidgetKeeShare() @@ -57,181 +57,189 @@ SettingsWidgetKeeShare::~SettingsWidgetKeeShare() void SettingsWidgetKeeShare::loadSettings() { - const auto active = KeeShare::active(); - m_ui->enableExportCheckBox->setChecked(active.out); - m_ui->enableImportCheckBox->setChecked(active.in); + const auto active = KeeShare::active(); + m_ui->enableExportCheckBox->setChecked(active.out); + m_ui->enableImportCheckBox->setChecked(active.in); - m_own = KeeShare::own(); - updateOwnCertificate(); + m_own = KeeShare::own(); + updateOwnCertificate(); - m_foreign = KeeShare::foreign(); - updateForeignCertificates(); + m_foreign = KeeShare::foreign(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::updateForeignCertificates() { - m_importedCertificateModel.reset(new QStandardItemModel()); - m_importedCertificateModel->setHorizontalHeaderLabels(QStringList() << tr("Path") << tr("Status") + auto headers = QStringList() << tr("Path") << tr("Status"); #if defined(WITH_XC_KEESHARE_SECURE) - << tr("Signer") << tr("Fingerprint") << tr("Certificate") + headers << tr("Signer") << tr("Fingerprint") << tr("Certificate"); #endif - ); - for (const auto& scopedCertificate : m_foreign.certificates) { - const auto items = QList() - << new QStandardItem(scopedCertificate.path) - << new QStandardItem(scopedCertificate.trust == KeeShareSettings::Trust::Ask ? tr("Ask") - : (scopedCertificate.trust == KeeShareSettings::Trust::Trusted ? tr("Trusted") - : tr("Untrusted"))) + m_importedCertificateModel.reset(new QStandardItemModel()); + m_importedCertificateModel->setHorizontalHeaderLabels(headers); + + for (const auto& scopedCertificate : m_foreign.certificates) { + const auto items = QList() + << new QStandardItem(scopedCertificate.path) + << new QStandardItem(scopedCertificate.trust == KeeShareSettings::Trust::Ask + ? tr("Ask") + : (scopedCertificate.trust == KeeShareSettings::Trust::Trusted + ? tr("Trusted") + : tr("Untrusted"))) #if defined(WITH_XC_KEESHARE_SECURE) - << new QStandardItem(scopedCertificate.isKnown() ? scopedCertificate.certificate.signer : tr("Unknown")) - << new QStandardItem(scopedCertificate.certificate.fingerprint()) - << new QStandardItem(scopedCertificate.certificate.publicKey()) + << new QStandardItem(scopedCertificate.isKnown() ? scopedCertificate.certificate.signer + : tr("Unknown")) + << new QStandardItem(scopedCertificate.certificate.fingerprint()) + << new QStandardItem(scopedCertificate.certificate.publicKey()) #endif - ; - m_importedCertificateModel->appendRow(items); - } + ; + m_importedCertificateModel->appendRow(items); + } - m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data()); + m_ui->importedCertificateTableView->setModel(m_importedCertificateModel.data()); + m_ui->importedCertificateTableView->resizeColumnsToContents(); } void SettingsWidgetKeeShare::updateOwnCertificate() { - m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); - m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); - m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); - m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); + m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); + m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); } void SettingsWidgetKeeShare::saveSettings() { - KeeShareSettings::Active active; - active.out = m_ui->enableExportCheckBox->isChecked(); - active.in = m_ui->enableImportCheckBox->isChecked(); - // TODO HNH: This depends on the order of saving new data - a better model would be to - // store changes to the settings in a temporary object and check on the final values - // of this object (similar scheme to Entry) - this way we could validate the settings before save - KeeShare::setOwn(m_own); - KeeShare::setForeign(m_foreign); - KeeShare::setActive(active); + KeeShareSettings::Active active; + active.out = m_ui->enableExportCheckBox->isChecked(); + active.in = m_ui->enableImportCheckBox->isChecked(); + // TODO HNH: This depends on the order of saving new data - a better model would be to + // store changes to the settings in a temporary object and check on the final values + // of this object (similar scheme to Entry) - this way we could validate the settings before save + KeeShare::setOwn(m_own); + KeeShare::setForeign(m_foreign); + KeeShare::setActive(active); } void SettingsWidgetKeeShare::setVerificationExporter(const QString& signer) { - m_own.certificate.signer = signer; - m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_own.certificate.signer = signer; + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); } void SettingsWidgetKeeShare::generateCertificate() { - m_own = KeeShareSettings::Own::generate(); - m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); - m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); - m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); - m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); + m_own = KeeShareSettings::Own::generate(); + m_ui->ownCertificateSignerEdit->setText(m_own.certificate.signer); + m_ui->ownCertificatePublicKeyEdit->setText(m_own.certificate.publicKey()); + m_ui->ownCertificatePrivateKeyEdit->setText(m_own.key.privateKey()); + m_ui->ownCertificateFingerprintEdit->setText(m_own.certificate.fingerprint()); } void SettingsWidgetKeeShare::importCertificate() { - QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); - const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); - if (!dirExists) { - defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); - } - const auto filetype = tr("key.share", "Filetype for KeeShare key"); - const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); - QString filename = fileDialog()->getOpenFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0)); - if (filename.isEmpty()) { - return; - } - QFile file(filename); - file.open(QIODevice::ReadOnly); - QTextStream stream(&file); - m_own = KeeShareSettings::Own::deserialize(stream.readAll()); - file.close(); - config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); + QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + const auto filetype = tr("key.share", "Filetype for KeeShare key"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); + QString filename = fileDialog()->getOpenFileName( + this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0)); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + file.open(QIODevice::ReadOnly); + QTextStream stream(&file); + m_own = KeeShareSettings::Own::deserialize(stream.readAll()); + file.close(); + config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); - updateOwnCertificate(); + updateOwnCertificate(); } void SettingsWidgetKeeShare::exportCertificate() { - if (KeeShare::own() != m_own) { - QMessageBox warning; - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(tr("Exporting changed certificate")); - warning.setText(tr("The exported certificate is not the same as the one in use. Do you want to export the current certificate?")); - auto yes = warning.addButton(QMessageBox::StandardButton::Yes); - auto no = warning.addButton(QMessageBox::StandardButton::No); - warning.setDefaultButton(no); - warning.exec(); - if (warning.clickedButton() != yes) { - return; - } - } - QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); - const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); - if (!dirExists) { - defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); - } - const auto filetype = tr("key.share", "Filetype for KeeShare key"); - const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); - QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype); - filename = fileDialog()->getSaveFileName(this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0), filetype, filename); - if (filename.isEmpty()) { - return; - } - QFile file(filename); - file.open(QIODevice::Truncate | QIODevice::WriteOnly); - QTextStream stream(&file); - stream << KeeShareSettings::Own::serialize(m_own); - stream.flush(); - file.close(); - config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); + if (KeeShare::own() != m_own) { + QMessageBox warning; + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(tr("Exporting changed certificate")); + warning.setText(tr("The exported certificate is not the same as the one in use. Do you want to export the " + "current certificate?")); + auto yes = warning.addButton(QMessageBox::StandardButton::Yes); + auto no = warning.addButton(QMessageBox::StandardButton::No); + warning.setDefaultButton(no); + warning.exec(); + if (warning.clickedButton() != yes) { + return; + } + } + QString defaultDirPath = config()->get("KeeShare/LastKeyDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + const auto filetype = tr("key.share", "Filetype for KeeShare key"); + const auto filters = QString("%1 (*." + filetype + ");;%2 (*)").arg(tr("KeeShare key file"), tr("All files")); + QString filename = tr("%1.%2", "Template for KeeShare key file").arg(m_own.certificate.signer).arg(filetype); + filename = fileDialog()->getSaveFileName( + this, tr("Select path"), defaultDirPath, filters, nullptr, QFileDialog::Options(0), filetype, filename); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + file.open(QIODevice::Truncate | QIODevice::WriteOnly); + QTextStream stream(&file); + stream << KeeShareSettings::Own::serialize(m_own); + stream.flush(); + file.close(); + config()->set("KeeShare/LastKeyDir", QFileInfo(filename).absolutePath()); } void SettingsWidgetKeeShare::trustSelectedCertificates() { - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Trusted; - } + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Trusted; + } - updateForeignCertificates(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::askSelectedCertificates() { - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Ask; - } + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Ask; + } - updateForeignCertificates(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::untrustSelectedCertificates() { - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Untrusted; - } + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + m_foreign.certificates[index.row()].trust = KeeShareSettings::Trust::Untrusted; + } - updateForeignCertificates(); + updateForeignCertificates(); } void SettingsWidgetKeeShare::removeSelectedCertificates() { - auto certificates = m_foreign.certificates; - const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); - Q_ASSERT(selectionModel); - for (const auto& index : selectionModel->selectedRows()) { - certificates.removeOne(m_foreign.certificates[index.row()]); - } - m_foreign.certificates = certificates; + auto certificates = m_foreign.certificates; + const auto* selectionModel = m_ui->importedCertificateTableView->selectionModel(); + Q_ASSERT(selectionModel); + for (const auto& index : selectionModel->selectedRows()) { + certificates.removeOne(m_foreign.certificates[index.row()]); + } + m_foreign.certificates = certificates; - updateForeignCertificates(); + updateForeignCertificates(); } diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index 036413e6b..fdd5ccf39 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -51,105 +51,111 @@ namespace { -static const QString KeeShare_Signature("container.share.signature"); -static const QString KeeShare_Container("container.share.kdbx"); + static const QString KeeShare_Signature("container.share.signature"); + static const QString KeeShare_Container("container.share.kdbx"); -enum Trust -{ - Invalid, - Own, - UntrustedForever, - UntrustedOnce, - TrustedOnce, - TrustedForever, -}; + enum Trust + { + Invalid, + Own, + UntrustedForever, + UntrustedOnce, + TrustedOnce, + TrustedForever, + }; -bool isOfExportType(const QFileInfo &fileInfo, const QString type) -{ - return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); -} + bool isOfExportType(const QFileInfo& fileInfo, const QString type) + { + return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive); + } -QPair check(QByteArray& data, - const KeeShareSettings::Reference& reference, - const KeeShareSettings::Certificate& ownCertificate, - const QList& knownCertificates, - const KeeShareSettings::Sign& sign) -{ - KeeShareSettings::Certificate certificate; - if (!sign.signature.isEmpty()) { - certificate = sign.certificate; - auto key = sign.certificate.sshKey(); - key.openKey(QString()); - const auto signer = Signature(); - if (!signer.verify(data, sign.signature, key)) { + QPair + check(QByteArray& data, + const KeeShareSettings::Reference& reference, + const KeeShareSettings::Certificate& ownCertificate, + const QList& knownCertificates, + const KeeShareSettings::Sign& sign) + { + KeeShareSettings::Certificate certificate; + if (!sign.signature.isEmpty()) { + certificate = sign.certificate; + auto key = sign.certificate.sshKey(); + key.openKey(QString()); + const auto signer = Signature(); + if (!signer.verify(data, sign.signature, key)) { qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path)); return {Invalid, KeeShareSettings::Certificate()}; - } + } - if (ownCertificate.key == sign.certificate.key) { - return {Own, ownCertificate }; - } - } - enum Scope { Invalid, Global, Local }; - Scope scope = Invalid; - KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; - for (const auto& scopedCertificate : knownCertificates) { - if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + if (ownCertificate.key == sign.certificate.key) { + return {Own, ownCertificate}; + } + } + enum Scope + { + Invalid, + Global, + Local + }; + Scope scope = Invalid; + KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask; + for (const auto& scopedCertificate : knownCertificates) { + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { // Global scope is overwritten by local scope scope = Global; trusted = scopedCertificate.trust; - } - if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { + } + if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) { scope = Local; trusted = scopedCertificate.trust; break; - } - } - if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask){ - // we introduce now scopes if there is a global - return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; - } + } + } + if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask) { + // we introduce now scopes if there is a global + return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate}; + } - QMessageBox warning; - if (sign.signature.isEmpty()){ - warning.setIcon(QMessageBox::Warning); - warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); - warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not signed. Do you really want to import from %1?") + QMessageBox warning; + if (sign.signature.isEmpty()) { + warning.setIcon(QMessageBox::Warning); + warning.setWindowTitle(ShareObserver::tr("Import from container without signature")); + warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not " + "signed. Do you really want to import from %1?") .arg(reference.path)); - } - else { - warning.setIcon(QMessageBox::Question); - warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); - warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") + } else { + warning.setIcon(QMessageBox::Question); + warning.setWindowTitle(ShareObserver::tr("Import from container with certificate")); + warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3") .arg(certificate.signer, certificate.fingerprint(), reference.path)); - } - auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); - auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); - auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); - auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); - warning.setDefaultButton(untrustedOnce); - warning.exec(); - if (warning.clickedButton() == trustedForever){ - return {TrustedForever, certificate }; - } - if (warning.clickedButton() == trustedOnce){ - return {TrustedOnce, certificate}; - } - if (warning.clickedButton() == untrustedOnce){ - return {UntrustedOnce, certificate }; - } - if (warning.clickedButton() == untrustedForever){ - return {UntrustedForever, certificate }; - } - return {UntrustedOnce, certificate }; -} + } + auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole); + auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole); + auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole); + auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole); + warning.setDefaultButton(untrustedOnce); + warning.exec(); + if (warning.clickedButton() == trustedForever) { + return {TrustedForever, certificate}; + } + if (warning.clickedButton() == trustedOnce) { + return {TrustedOnce, certificate}; + } + if (warning.clickedButton() == untrustedOnce) { + return {UntrustedOnce, certificate}; + } + if (warning.clickedButton() == untrustedForever) { + return {UntrustedForever, certificate}; + } + return {UntrustedOnce, certificate}; + } } // End Namespace ShareObserver::ShareObserver(QSharedPointer db, QObject* parent) - : QObject(parent) - , m_db(std::move(db)) - , m_fileWatcher(new BulkFileWatcher(this)) + : QObject(parent) + , m_db(std::move(db)) + , m_fileWatcher(new BulkFileWatcher(this)) { connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(handleDatabaseChanged())); @@ -195,8 +201,9 @@ void ShareObserver::reinitialize() m_groupToReference.remove(couple.group); m_referenceToGroup.remove(couple.oldReference); m_shareToGroup.remove(couple.oldReference.path); - if (couple.newReference.isValid() && ((active.in && couple.newReference.isImporting()) - || (active.out && couple.newReference.isExporting()))) { + if (couple.newReference.isValid() + && ((active.in && couple.newReference.isImporting()) + || (active.out && couple.newReference.isExporting()))) { m_groupToReference[couple.group] = couple.newReference; m_referenceToGroup[couple.newReference] = couple.group; m_shareToGroup[couple.newReference.path] = couple.group; @@ -213,27 +220,27 @@ void ShareObserver::reinitialize() } if (!update.newReference.path.isEmpty() && update.newReference.type != KeeShareSettings::Inactive) { - m_fileWatcher->addPath(update.newReference.path); + m_fileWatcher->addPath(update.newReference.path); } if (update.newReference.isImporting()) { - const auto result = this->importFromReferenceContainer(update.newReference.path); + const auto result = this->importFromReferenceContainer(update.newReference.path); if (!result.isValid()) { - // tolerable result - blocked import or missing source - continue; - } + // tolerable result - blocked import or missing source + continue; + } - if (result.isError()) { - error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isWarning()) { - warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); - } else if (result.isInfo()) { - success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); - } else { - success << tr("Imported from %1").arg(result.path); - } - } - } + if (result.isError()) { + error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isWarning()) { + warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message); + } else if (result.isInfo()) { + success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message); + } else { + success << tr("Imported from %1").arg(result.path); + } + } + } notifyAbout(success, warning, error); } @@ -301,11 +308,12 @@ void ShareObserver::handleFileUpdated(const QString& path) notifyAbout(success, warning, error); } -ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSettings::Reference& reference, + Group* targetGroup) { #if !defined(WITH_XC_KEESHARE_SECURE) Q_UNUSED(targetGroup); - return { reference.path, Result::Warning, tr("Signed share container are not supported - import prevented") }; + return {reference.path, Result::Warning, tr("Signed share container are not supported - import prevented")}; #else QuaZip zip(reference.path); if (!zip.open(QuaZip::mdUnzip)) { @@ -359,25 +367,26 @@ ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSet case UntrustedForever: case TrustedForever: { bool found = false; - const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + const auto trusted = + trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { - scopedCertificate.certificate.signer = trust.second.signer; - scopedCertificate.path = reference.path; - scopedCertificate.trust = trusted; - found = true; + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trust = trusted; + found = true; } } if (!found) { - foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; + foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted}; // we need to update with the new signer KeeShare::setForeign(foreign); } if (trust.first == TrustedForever) { qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); Merger merger(sourceDb->rootGroup(), targetGroup); merger.setForcedMergeMode(Group::Synchronize); const bool changed = merger.merge(); @@ -391,16 +400,16 @@ ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSet case TrustedOnce: case Own: { qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); - Merger merger(sourceDb->rootGroup(), targetGroup); - merger.setForcedMergeMode(Group::Synchronize); - const bool changed = merger.merge(); - if (changed) { - return {reference.path, Result::Success, tr("Successful signed import")}; - } - return {}; + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); + Merger merger(sourceDb->rootGroup(), targetGroup); + merger.setForcedMergeMode(Group::Synchronize); + const bool changed = merger.merge(); + if (changed) { + return {reference.path, Result::Success, tr("Successful signed import")}; + } + return {}; } default: Q_ASSERT(false); @@ -409,14 +418,15 @@ ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSet #endif } -ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareSettings::Reference& reference, + Group* targetGroup) { #if !defined(WITH_XC_KEESHARE_INSECURE) Q_UNUSED(targetGroup); return {reference.path, Result::Warning, tr("Unsigned share container are not supported - import prevented")}; #else QFile file(reference.path); - if (!file.open(QIODevice::ReadOnly)){ + if (!file.open(QIODevice::ReadOnly)) { qCritical("Unable to open file %s.", qPrintable(reference.path)); return {reference.path, Result::Error, tr("File is not readable")}; } @@ -438,34 +448,35 @@ ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareS const auto own = KeeShare::own(); const auto sign = KeeShareSettings::Sign(); // invalid sign auto trust = check(payload, reference, own.certificate, foreign.certificates, sign); - switch(trust.first) { + switch (trust.first) { case UntrustedForever: case TrustedForever: { bool found = false; - const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + const auto trusted = + trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { - scopedCertificate.certificate.signer = trust.second.signer; - scopedCertificate.path = reference.path; - scopedCertificate.trust = trusted; - found = true; + scopedCertificate.certificate.signer = trust.second.signer; + scopedCertificate.path = reference.path; + scopedCertificate.trust = trusted; + found = true; } } if (!found) { - foreign.certificates << KeeShareSettings::ScopedCertificate{ reference.path, trust.second, trusted}; + foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted}; // we need to update with the new signer KeeShare::setForeign(foreign); } if (trust.first == TrustedForever) { qDebug("Synchronize %s %s with %s", - qPrintable(reference.path), - qPrintable(targetGroup->name()), - qPrintable(sourceDb->rootGroup()->name())); + qPrintable(reference.path), + qPrintable(targetGroup->name()), + qPrintable(sourceDb->rootGroup()->name())); Merger merger(sourceDb->rootGroup(), targetGroup); merger.setForcedMergeMode(Group::Synchronize); const bool changed = merger.merge(); if (changed) { - return {reference.path, Result::Success, tr("Successful signed import")}; + return {reference.path, Result::Success, tr("Successful signed import")}; } } return {}; @@ -491,7 +502,8 @@ ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareS #endif } -ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup) +ShareObserver::Result ShareObserver::importContainerInto(const KeeShareSettings::Reference& reference, + Group* targetGroup) { const QFileInfo info(reference.path); if (!info.exists()) { @@ -601,7 +613,7 @@ Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& } for (auto* targetEntry : targetRoot->entriesRecursive(false)) { if (targetEntry->hasReferences()) { - resolveReferenceAttributes(targetEntry, sourceDb); + resolveReferenceAttributes(targetEntry, sourceDb); } } return targetDb; @@ -612,11 +624,13 @@ QSharedPointer ShareObserver::database() return m_db; } -ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb) +ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const KeeShareSettings::Reference& reference, + Database* targetDb) { #if !defined(WITH_XC_KEESHARE_SECURE) Q_UNUSED(targetDb); - return {reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")}; + return { + reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")}; #else QByteArray bytes; { @@ -657,7 +671,7 @@ ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const Ke ::qWarning("Embedding signature failed: %d", zip.getZipError()); return {reference.path, Result::Error, tr("Could not embed signature (%1)").arg(file.getZipError())}; } - file.close(); + file.close(); } { QuaZipFile file(&zip); @@ -678,11 +692,14 @@ ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const Ke #endif } -ShareObserver::Result ShareObserver::exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb) +ShareObserver::Result ShareObserver::exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference& reference, + Database* targetDb) { #if !defined(WITH_XC_KEESHARE_INSECURE) Q_UNUSED(targetDb); - return {reference.path, Result::Warning, tr("Overwriting unsigned share container is not supported - export prevented")}; + return {reference.path, + Result::Warning, + tr("Overwriting unsigned share container is not supported - export prevented")}; #else QFile file(reference.path); const bool fileOpened = file.open(QIODevice::WriteOnly); diff --git a/src/keeshare/ShareObserver.h b/src/keeshare/ShareObserver.h index d67a1f7ef..95c088006 100644 --- a/src/keeshare/ShareObserver.h +++ b/src/keeshare/ShareObserver.h @@ -35,7 +35,7 @@ class Database; class ShareObserver : public QObject { -Q_OBJECT + Q_OBJECT public: explicit ShareObserver(QSharedPointer db, QObject* parent = nullptr); @@ -79,8 +79,9 @@ private: static void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb); static Database* exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot); - static Result exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb); - static Result exportIntoReferenceSignedContainer(const KeeShareSettings::Reference &reference, Database *targetDb); + static Result exportIntoReferenceUnsignedContainer(const KeeShareSettings::Reference& reference, + Database* targetDb); + static Result exportIntoReferenceSignedContainer(const KeeShareSettings::Reference& reference, Database* targetDb); static Result importSingedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); static Result importUnsignedContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); static Result importContainerInto(const KeeShareSettings::Reference& reference, Group* targetGroup); diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index eecdaee21..bb2cc19ae 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -31,54 +31,53 @@ #include EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent) - : QWidget(parent) - , m_ui(new Ui::EditGroupWidgetKeeShare()) + : QWidget(parent) + , m_ui(new Ui::EditGroupWidgetKeeShare()) { - m_ui->setupUi(this); + m_ui->setupUi(this); - m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); - m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); + m_ui->togglePasswordButton->setIcon(filePath()->onOffIcon("actions", "password-show")); + m_ui->togglePasswordGeneratorButton->setIcon(filePath()->icon("actions", "password-generator", false)); - m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); - m_ui->passwordGenerator->hide(); - m_ui->passwordGenerator->reset(); + m_ui->passwordGenerator->layout()->setContentsMargins(0, 0, 0, 0); + m_ui->passwordGenerator->hide(); + m_ui->passwordGenerator->reset(); - m_ui->messageWidget->hide(); - m_ui->messageWidget->setCloseButtonVisible(false); - m_ui->messageWidget->setAutoHideTimeout(-1); + m_ui->messageWidget->hide(); + m_ui->messageWidget->setCloseButtonVisible(false); + m_ui->messageWidget->setAutoHideTimeout(-1); - connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); - connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); - connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); - connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); - connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); - connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); - connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); + connect(m_ui->togglePasswordButton, SIGNAL(toggled(bool)), m_ui->passwordEdit, SLOT(setShowPassword(bool))); + connect(m_ui->togglePasswordGeneratorButton, SIGNAL(toggled(bool)), SLOT(togglePasswordGeneratorButton(bool))); + connect(m_ui->passwordEdit, SIGNAL(textChanged(QString)), SLOT(selectPassword())); + connect(m_ui->passwordGenerator, SIGNAL(appliedPassword(QString)), SLOT(setGeneratedPassword(QString))); + connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath())); + connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog())); + connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType())); - connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); + connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(showSharingState())); - const auto types = QList() << KeeShareSettings::Inactive - << KeeShareSettings::ImportFrom - << KeeShareSettings::ExportTo - << KeeShareSettings::SynchronizeWith; - for (const auto& type : types) { - QString name; - switch (type) { - case KeeShareSettings::Inactive: - name = tr("Inactive"); - break; - case KeeShareSettings::ImportFrom: - name = tr("Import from path"); - break; - case KeeShareSettings::ExportTo: - name = tr("Export to path"); - break; - case KeeShareSettings::SynchronizeWith: - name = tr("Synchronize with path"); - break; - } - m_ui->typeComboBox->insertItem(type, name, static_cast(type)); - } + const auto types = QList() + << KeeShareSettings::Inactive << KeeShareSettings::ImportFrom << KeeShareSettings::ExportTo + << KeeShareSettings::SynchronizeWith; + for (const auto& type : types) { + QString name; + switch (type) { + case KeeShareSettings::Inactive: + name = tr("Inactive"); + break; + case KeeShareSettings::ImportFrom: + name = tr("Import from path"); + break; + case KeeShareSettings::ExportTo: + name = tr("Export to path"); + break; + case KeeShareSettings::SynchronizeWith: + name = tr("Synchronize with path"); + break; + } + m_ui->typeComboBox->insertItem(type, name, static_cast(type)); + } } EditGroupWidgetKeeShare::~EditGroupWidgetKeeShare() @@ -87,198 +86,217 @@ EditGroupWidgetKeeShare::~EditGroupWidgetKeeShare() void EditGroupWidgetKeeShare::setGroup(Group* temporaryGroup) { - if (m_temporaryGroup) { - m_temporaryGroup->disconnect(this); - } + if (m_temporaryGroup) { + m_temporaryGroup->disconnect(this); + } - m_temporaryGroup = temporaryGroup; + m_temporaryGroup = temporaryGroup; - if (m_temporaryGroup) { - connect(m_temporaryGroup, SIGNAL(groupModified()), SLOT(update())); - } + if (m_temporaryGroup) { + connect(m_temporaryGroup, SIGNAL(groupModified()), SLOT(update())); + } - update(); + update(); } void EditGroupWidgetKeeShare::showSharingState() { - if (!m_temporaryGroup) { - return; - } + if (!m_temporaryGroup) { + return; + } - auto supportedExtensions = QStringList(); + auto supportedExtensions = QStringList(); #if defined(WITH_XC_KEESHARE_INSECURE) - supportedExtensions << KeeShare::unsignedContainerFileType(); + supportedExtensions << KeeShare::unsignedContainerFileType(); #endif #if defined(WITH_XC_KEESHARE_SECURE) - supportedExtensions << KeeShare::signedContainerFileType(); + supportedExtensions << KeeShare::signedContainerFileType(); #endif - const auto reference = KeeShare::referenceOf(m_temporaryGroup); - if (!reference.path.isEmpty()) { - bool supported = false; - for(const auto &extension : supportedExtensions){ - if (reference.path.endsWith(extension, Qt::CaseInsensitive)){ - supported = true; - break; - } - } - if (!supported) { - m_ui->messageWidget->showMessage( - tr("Your KeePassXC version does not support sharing your container type. Please use %1.") - .arg(supportedExtensions.join(", ")), - MessageWidget::Warning); - return; - } - } - const auto active = KeeShare::active(); - if (!active.in && !active.out) { - m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information); - return; - } - if (active.in && !active.out) { - m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information); - return; - } - if (!active.in && active.out) { - m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information); - return; - } + const auto reference = KeeShare::referenceOf(m_temporaryGroup); + if (!reference.path.isEmpty()) { + bool supported = false; + for (const auto& extension : supportedExtensions) { + if (reference.path.endsWith(extension, Qt::CaseInsensitive)) { + supported = true; + break; + } + } + if (!supported) { + m_ui->messageWidget->showMessage( + tr("Your KeePassXC version does not support sharing your container type. Please use %1.") + .arg(supportedExtensions.join(", ")), + MessageWidget::Warning); + return; + } + } + const auto active = KeeShare::active(); + if (!active.in && !active.out) { + m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information); + return; + } + if (active.in && !active.out) { + m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information); + return; + } + if (!active.in && active.out) { + m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information); + return; + } } void EditGroupWidgetKeeShare::update() { - if (!m_temporaryGroup) { - m_ui->passwordEdit->clear(); - m_ui->pathEdit->clear(); - } else { - const auto reference = KeeShare::referenceOf(m_temporaryGroup); + if (!m_temporaryGroup) { + m_ui->passwordEdit->clear(); + m_ui->pathEdit->clear(); + } else { + const auto reference = KeeShare::referenceOf(m_temporaryGroup); - m_ui->typeComboBox->setCurrentIndex(reference.type); - m_ui->passwordEdit->setText(reference.password); - m_ui->pathEdit->setText(reference.path); + m_ui->typeComboBox->setCurrentIndex(reference.type); + m_ui->passwordEdit->setText(reference.password); + m_ui->pathEdit->setText(reference.path); - showSharingState(); - } + showSharingState(); + } - m_ui->passwordGenerator->hide(); - m_ui->togglePasswordGeneratorButton->setChecked(false); - m_ui->togglePasswordButton->setChecked(false); + m_ui->passwordGenerator->hide(); + m_ui->togglePasswordGeneratorButton->setChecked(false); + m_ui->togglePasswordButton->setChecked(false); } void EditGroupWidgetKeeShare::togglePasswordGeneratorButton(bool checked) { - m_ui->passwordGenerator->regeneratePassword(); - m_ui->passwordGenerator->setVisible(checked); + m_ui->passwordGenerator->regeneratePassword(); + m_ui->passwordGenerator->setVisible(checked); } void EditGroupWidgetKeeShare::setGeneratedPassword(const QString& password) { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.password = password; - KeeShare::setReferenceTo(m_temporaryGroup, reference); - m_ui->togglePasswordGeneratorButton->setChecked(false); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.password = password; + KeeShare::setReferenceTo(m_temporaryGroup, reference); + m_ui->togglePasswordGeneratorButton->setChecked(false); } void EditGroupWidgetKeeShare::selectPath() { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.path = m_ui->pathEdit->text(); - KeeShare::setReferenceTo(m_temporaryGroup, reference); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.path = m_ui->pathEdit->text(); + KeeShare::setReferenceTo(m_temporaryGroup, reference); } void EditGroupWidgetKeeShare::launchPathSelectionDialog() { - if (!m_temporaryGroup) { - return; - } - QString defaultDirPath = config()->get("KeeShare/LastShareDir").toString(); - const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); - if (!dirExists) { - defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - QString defaultFiletype = ""; - auto supportedExtensions = QStringList(); - auto unsupportedExtensions = QStringList(); - auto knownFilters = QStringList() << QString("%1 (*)").arg("All files"); + if (!m_temporaryGroup) { + return; + } + QString defaultDirPath = config()->get("KeeShare/LastShareDir").toString(); + const bool dirExists = !defaultDirPath.isEmpty() && QDir(defaultDirPath).exists(); + if (!dirExists) { + defaultDirPath = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).first(); + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + QString defaultFiletype = ""; + auto supportedExtensions = QStringList(); + auto unsupportedExtensions = QStringList(); + auto knownFilters = QStringList() << QString("%1 (*)").arg("All files"); #if defined(WITH_XC_KEESHARE_INSECURE) - defaultFiletype = KeeShare::unsignedContainerFileType(); - supportedExtensions << KeeShare::unsignedContainerFileType(); - knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare unsigned container"), KeeShare::unsignedContainerFileType())); + defaultFiletype = KeeShare::unsignedContainerFileType(); + supportedExtensions << KeeShare::unsignedContainerFileType(); + knownFilters.prepend( + QString("%1 (*.%2)").arg(tr("KeeShare unsigned container"), KeeShare::unsignedContainerFileType())); #else - unsupportedExtensions << KeeShare::unsignedContainerFileType(); + unsupportedExtensions << KeeShare::unsignedContainerFileType(); #endif #if defined(WITH_XC_KEESHARE_SECURE) - defaultFiletype = KeeShare::signedContainerFileType(); - supportedExtensions << KeeShare::signedContainerFileType(); - knownFilters.prepend(QString("%1 (*.%2)").arg(tr("KeeShare signed container"), KeeShare::signedContainerFileType())); + defaultFiletype = KeeShare::signedContainerFileType(); + supportedExtensions << KeeShare::signedContainerFileType(); + knownFilters.prepend( + QString("%1 (*.%2)").arg(tr("KeeShare signed container"), KeeShare::signedContainerFileType())); #else - unsupportedExtensions << KeeShare::signedContainerFileType(); + unsupportedExtensions << KeeShare::signedContainerFileType(); #endif - const auto filters = knownFilters.join(";;"); - auto filename = reference.path; - if (filename.isEmpty()) { - filename = m_temporaryGroup->name(); - } - switch (reference.type) { - case KeeShareSettings::ImportFrom: - filename = fileDialog()->getFileName( - this, tr("Select import source"), defaultDirPath, filters, nullptr, QFileDialog::DontConfirmOverwrite, - defaultFiletype, filename); - break; - case KeeShareSettings::ExportTo: - filename = fileDialog()->getFileName( - this, tr("Select export target"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); - break; - case KeeShareSettings::SynchronizeWith: - case KeeShareSettings::Inactive: - filename = fileDialog()->getFileName( - this, tr("Select import/export file"), defaultDirPath, filters, nullptr, QFileDialog::Option(0), defaultFiletype, filename); - break; - } + const auto filters = knownFilters.join(";;"); + auto filename = reference.path; + if (filename.isEmpty()) { + filename = m_temporaryGroup->name(); + } + switch (reference.type) { + case KeeShareSettings::ImportFrom: + filename = fileDialog()->getFileName(this, + tr("Select import source"), + defaultDirPath, + filters, + nullptr, + QFileDialog::DontConfirmOverwrite, + defaultFiletype, + filename); + break; + case KeeShareSettings::ExportTo: + filename = fileDialog()->getFileName(this, + tr("Select export target"), + defaultDirPath, + filters, + nullptr, + QFileDialog::Option(0), + defaultFiletype, + filename); + break; + case KeeShareSettings::SynchronizeWith: + case KeeShareSettings::Inactive: + filename = fileDialog()->getFileName(this, + tr("Select import/export file"), + defaultDirPath, + filters, + nullptr, + QFileDialog::Option(0), + defaultFiletype, + filename); + break; + } - if (filename.isEmpty()) { - return; - } - bool validFilename = false; - for(const auto& extension : supportedExtensions + unsupportedExtensions){ - if (filename.endsWith(extension, Qt::CaseInsensitive)) { - validFilename = true; - break; - } - } - if (!validFilename){ - filename += (!filename.endsWith(".") ? "." : "") + defaultFiletype; - } + if (filename.isEmpty()) { + return; + } + bool validFilename = false; + for (const auto& extension : supportedExtensions + unsupportedExtensions) { + if (filename.endsWith(extension, Qt::CaseInsensitive)) { + validFilename = true; + break; + } + } + if (!validFilename) { + filename += (!filename.endsWith(".") ? "." : "") + defaultFiletype; + } - m_ui->pathEdit->setText(filename); - selectPath(); - config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); + m_ui->pathEdit->setText(filename); + selectPath(); + config()->set("KeeShare/LastShareDir", QFileInfo(filename).absolutePath()); } void EditGroupWidgetKeeShare::selectPassword() { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.password = m_ui->passwordEdit->text(); - KeeShare::setReferenceTo(m_temporaryGroup, reference); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.password = m_ui->passwordEdit->text(); + KeeShare::setReferenceTo(m_temporaryGroup, reference); } void EditGroupWidgetKeeShare::selectType() { - if (!m_temporaryGroup) { - return; - } - auto reference = KeeShare::referenceOf(m_temporaryGroup); - reference.type = static_cast(m_ui->typeComboBox->currentData().toInt()); - KeeShare::setReferenceTo(m_temporaryGroup, reference); + if (!m_temporaryGroup) { + return; + } + auto reference = KeeShare::referenceOf(m_temporaryGroup); + reference.type = static_cast(m_ui->typeComboBox->currentData().toInt()); + KeeShare::setReferenceTo(m_temporaryGroup, reference); } From 18e234aefc8f0801746d0cc62596bd35a40e3fc5 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 19 Jan 2019 09:31:27 -0500 Subject: [PATCH 23/23] Fixed storing trust of share and persistent message --- src/keeshare/ShareObserver.cpp | 20 +++++++++++-------- .../group/EditGroupWidgetKeeShare.cpp | 2 ++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp index fdd5ccf39..8f2e2c3ab 100644 --- a/src/keeshare/ShareObserver.cpp +++ b/src/keeshare/ShareObserver.cpp @@ -367,21 +367,23 @@ ShareObserver::Result ShareObserver::importSingedContainerInto(const KeeShareSet case UntrustedForever: case TrustedForever: { bool found = false; - const auto trusted = - trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted + : KeeShareSettings::Trust::Untrusted; for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { scopedCertificate.certificate.signer = trust.second.signer; scopedCertificate.path = reference.path; scopedCertificate.trust = trusted; found = true; + break; } } if (!found) { foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted}; - // we need to update with the new signer - KeeShare::setForeign(foreign); } + // update foreign certificates with new settings + KeeShare::setForeign(foreign); + if (trust.first == TrustedForever) { qDebug("Synchronize %s %s with %s", qPrintable(reference.path), @@ -452,21 +454,23 @@ ShareObserver::Result ShareObserver::importUnsignedContainerInto(const KeeShareS case UntrustedForever: case TrustedForever: { bool found = false; - const auto trusted = - trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted; + const auto trusted = trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted + : KeeShareSettings::Trust::Untrusted; for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) { if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) { scopedCertificate.certificate.signer = trust.second.signer; scopedCertificate.path = reference.path; scopedCertificate.trust = trusted; found = true; + break; } } if (!found) { foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted}; - // we need to update with the new signer - KeeShare::setForeign(foreign); } + // update foreign certificates with new settings + KeeShare::setForeign(foreign); + if (trust.first == TrustedForever) { qDebug("Synchronize %s %s with %s", qPrintable(reference.path), diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp index bb2cc19ae..253bb9819 100644 --- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp +++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp @@ -127,6 +127,8 @@ void EditGroupWidgetKeeShare::showSharingState() .arg(supportedExtensions.join(", ")), MessageWidget::Warning); return; + } else { + m_ui->messageWidget->hide(); } } const auto active = KeeShare::active();