From eca9c658f4d0a8e956d49ce2e9eea81704e1de9b Mon Sep 17 00:00:00 2001 From: Christian Kieschnick Date: Mon, 1 Oct 2018 10:26:24 -0400 Subject: [PATCH] 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