diff options
author | Christian Kieschnick <christian.kieschnick@hicknhack-software.com> | 2018-10-01 17:26:24 +0300 |
---|---|---|
committer | Jonathan White <support@dmapps.us> | 2018-10-01 17:39:37 +0300 |
commit | eca9c658f4d0a8e956d49ce2e9eea81704e1de9b (patch) | |
tree | f49da9147abee9a96a0acce17548233a988b1f34 /src | |
parent | c1e9f45df9f21b7697241037643770a2862bb7ef (diff) |
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
Diffstat (limited to 'src')
87 files changed, 4923 insertions, 397 deletions
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> Clock::m_instance = QSharedPointer<Clock>(); +QSharedPointer<Clock> 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<Entry*> 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 <QHash> #include <QObject> +#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<DeletedObject> 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 <debfx@fobos.de> + * Copyright (C) 2017 KeePassXC Team <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "FileWatcher.h" + +#include "core/Clock.h" +#include <QFileInfo> + +#ifdef Q_OS_LINUX +#include <sys/vfs.h> +#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<QString, bool>& 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<int>(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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_FILEWATCHER_H +#define KEEPASSXC_FILEWATCHER_H + +#include <QFileSystemWatcher> +#include <QSet> +#include <QTimer> +#include <QVariant> + +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<QString> m_filePaths; + QMap<QString, QDateTime> m_ignoreFilesChangess; + QFileSystemWatcher m_fileWatcher; + QMap<QString, QMap<QString, bool>> 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<char*>(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 <typename Key, typename Value, void deleter(Value)> struct Map + { + QMap<Key, Value> 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<size_t>(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> 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 <QByteArray> #include <QScopedPointer> +#include <QSharedPointer> 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<Random> m_instance; + explicit Random(RandomBackend* backend); QScopedPointer<RandomBackend> 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 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 <gcrypt.h> 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 (!parsePublicHeader(stream)) { + return false; + } + + QByteArray n, e; + readInt(stream, n); + readInt(stream, e); + + QList<QByteArray> publicData; + publicData.append(e); + publicData.append(n); + + QList<QByteArray> 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 (!parseHeader(stream, KEY_ZERO)) { + if (!parsePrivateHeader(stream, KEY_ZERO)) { return false; } diff --git a/src/sshagent/ASN1Key.h b/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 <http://www.gnu.org/licenses/>. */ -#ifndef ASN1KEY_H -#define ASN1KEY_H +#ifndef KEEPASSXC_ASN1KEY_H +#define KEEPASSXC_ASN1KEY_H #include "OpenSSHKey.h" #include <QtCore> @@ -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 index 2aa8ac1c7..2aa8ac1c7 100644 --- a/src/sshagent/BinaryStream.cpp +++ b/src/crypto/ssh/BinaryStream.cpp diff --git a/src/sshagent/BinaryStream.h b/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 <http://www.gnu.org/licenses/>. */ -#ifndef BINARYSTREAM_H -#define BINARYSTREAM_H +#ifndef KEEPASSXC_BINARYSTREAM_H +#define KEEPASSXC_BINARYSTREAM_H #include <QBuffer> #include <QIODevice> @@ -65,4 +65,4 @@ private: QScopedPointer<QBuffer> 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 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 <QCryptographicHash> #include <QRegularExpression> #include <QStringList> -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 <gcrypt.h> + +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<QString, QList<QByteArray>> 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<QByteArray> data; + while (stream.readString(temp)) { + data << temp; + } + return ::qMakePair(type, data); + } + + QByteArray binarySerialize(const QString& type, const QList<QByteArray>& 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<Index, gcry_mpi_t, &gcry_mpi_release> mpi; + Tools::Map<Index, gcry_sexp_t, &gcry_sexp_release> 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<QByteArray> publicParts; + QList<QByteArray> 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<QByteArray>()) - , m_privateData(QList<QByteArray>()) - , m_privateType(QString()) + , m_rawType(QString()) + , m_rawData(QByteArray()) + , m_rawPublicData(QList<QByteArray>()) + , m_rawPrivateData(QList<QByteArray>()) , 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<QByteArray>& data) { - m_publicData = data; + m_rawPublicData = data; } void OpenSSHKey::setPrivateData(const QList<QByteArray>& 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<SymmetricCipher> 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<QByteArray> OpenSSHKey::publicParts() const +{ + return m_rawPublicData; +} + +QList<QByteArray> 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 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 <http://www.gnu.org/licenses/>. */ -#ifndef OPENSSHKEY_H -#define OPENSSHKEY_H +#ifndef KEEPASSXC_OPENSSHKEY_H +#define KEEPASSXC_OPENSSHKEY_H -#include "BinaryStream.h" #include <QtCore> -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<QByteArray> publicParts() const; + QList<QByteArray> privateParts() const; + const QString& privateType() const; + + static const QString TYPE_DSA_PRIVATE; + static const QString TYPE_RSA_PRIVATE; + static const QString TYPE_RSA_PUBLIC; + static const QString TYPE_OPENSSH_PRIVATE; - bool parsePEM(const QByteArray& in, QByteArray& out); + 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<QByteArray> m_publicData; - QList<QByteArray> m_privateData; - QString m_privateType; + + QString m_rawType; + QByteArray m_rawData; + QList<QByteArray> m_rawPublicData; + QList<QByteArray> 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 index fed4cdb29..fed4cdb29 100644 --- a/src/sshagent/bcrypt_pbkdf.cpp +++ b/src/crypto/ssh/bcrypt_pbkdf.cpp diff --git a/src/sshagent/blf.h b/src/crypto/ssh/blf.h index f1ac5a5c2..f1ac5a5c2 100644 --- a/src/sshagent/blf.h +++ b/src/crypto/ssh/blf.h diff --git a/src/sshagent/blowfish.c b/src/crypto/ssh/blowfish.c index e10f7e7d9..e10f7e7d9 100644 --- a/src/sshagent/blowfish.c +++ b/src/crypto/ssh/blowfish.c diff --git a/src/sshagent/includes.h b/src/crypto/ssh/includes.h index 23b4aeeb6..23b4aeeb6 100644 --- a/src/sshagent/includes.h +++ b/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<Database> m_db; - QPointer<Metadata> m_meta; + QPointer<const Database> m_db; + QPointer<const Metadata> m_meta; KeePass2RandomStream* m_randomStream = nullptr; QHash<QByteArray, int> 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<Database*>(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,28 +1180,10 @@ 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); - } -} - -void DatabaseWidget::unblockAutoReload() -{ - m_ignoreAutoReload = false; - updateFilePath(m_filePath); -} - -void DatabaseWidget::onWatchedFileChanged() -{ - if (m_ignoreAutoReload) { - return; + m_fileWatcher->observeFileChanges(true); } - if (m_fileWatchTimer.isActive()) - return; - - m_fileWatchTimer.start(500); } void DatabaseWidget::reloadDatabaseFile() @@ -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<DelayingFileWatcher> 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 <QWidget> @@ -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 @@ <ui version="4.0"> <class>DetailsWidget</class> <widget class="QWidget" name="DetailsWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>652</width> + <height>274</height> + </rect> + </property> <layout class="QVBoxLayout" name="verticalLayout_7"> <property name="spacing"> <number>0</number> @@ -38,7 +46,7 @@ <number>0</number> </property> <item> - <layout class="QHBoxLayout" name="entryHorizontalLayout" stretch="0,0,0,0,0"> + <layout class="QHBoxLayout" name="entryHorizontalLayout" stretch="0,0,0,0,0,0"> <property name="sizeConstraint"> <enum>QLayout::SetDefaultConstraint</enum> </property> @@ -77,6 +85,19 @@ </widget> </item> <item> + <spacer name="entryHorizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> <widget class="QWidget" name="entryTotpWidget" native="true"> <layout class="QGridLayout" name="gridLayout_3"> <property name="leftMargin"> @@ -167,47 +188,6 @@ <property name="bottomMargin"> <number>0</number> </property> - <item row="1" column="2"> - <widget class="ElidedLabel" name="entryPasswordLabel"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>100</width> - <height>0</height> - </size> - </property> - </widget> - </item> - <item row="2" column="2"> - <widget class="ElidedLabel" name="entryUrlLabel"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>100</width> - <height>0</height> - </size> - </property> - <property name="cursor"> - <cursorShape>PointingHandCursor</cursorShape> - </property> - <property name="text"> - <string/> - </property> - <property name="textInteractionFlags"> - <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set> - </property> - </widget> - </item> <item row="0" column="2"> <widget class="QLabel" name="entryUsernameLabel"> <property name="sizePolicy"> @@ -252,35 +232,13 @@ </property> </widget> </item> - <item row="3" column="1"> - <widget class="QLabel" name="entryExpirationTitleLabel"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="font"> - <font> - <weight>75</weight> - <bold>true</bold> - </font> - </property> - <property name="text"> - <string>Expiration</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - <item row="3" column="2"> - <widget class="QLabel" name="entryExpirationLabel"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <item row="1" column="2"> + <widget class="ElidedLabel" name="entryPasswordLabel"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> </property> </widget> </item> @@ -306,24 +264,24 @@ </property> </widget> </item> - <item row="0" column="0"> - <spacer name="entryLeftHorizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> + <item row="2" column="2"> + <widget class="ElidedLabel" name="entryUrlLabel"> + <property name="minimumSize"> <size> - <width>20</width> - <height>20</height> + <width>100</width> + <height>0</height> </size> </property> - </spacer> + <property name="cursor"> + <cursorShape>PointingHandCursor</cursorShape> + </property> + <property name="text"> + <string/> + </property> + </widget> </item> - <item row="1" column="1"> - <widget class="QLabel" name="entryPasswordTitleLabel"> + <item row="3" column="1"> + <widget class="QLabel" name="entryExpirationTitleLabel"> <property name="sizePolicy"> <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> <horstretch>0</horstretch> @@ -337,22 +295,32 @@ </font> </property> <property name="text"> - <string>Password</string> + <string>Expiration</string> </property> <property name="alignment"> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> </property> </widget> </item> + <item row="3" column="2"> + <widget class="QLabel" name="entryExpirationLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> <item row="4" column="2"> - <spacer name="verticalSpacer"> + <spacer name="entryBottomVerticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> <property name="sizeHint" stdset="0"> <size> - <width>0</width> - <height>0</height> + <width>20</width> + <height>10</height> </size> </property> </spacer> @@ -473,7 +441,7 @@ <number>0</number> </property> <item> - <layout class="QHBoxLayout" name="groupHorizontalLayout" stretch="0,0,0"> + <layout class="QHBoxLayout" name="groupHorizontalLayout" stretch="0,0,0,0"> <property name="sizeConstraint"> <enum>QLayout::SetDefaultConstraint</enum> </property> @@ -512,6 +480,19 @@ </widget> </item> <item> + <spacer name="groupHorizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> <widget class="QToolButton" name="groupCloseButton"> <property name="toolTip"> <string>Close</string> @@ -673,10 +654,16 @@ </widget> </item> <item row="3" column="2"> - <spacer name="verticalSpacer_2"> + <spacer name="groupBottomVerticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>10</height> + </size> + </property> </spacer> </item> </layout> @@ -701,6 +688,89 @@ </item> </layout> </widget> + <widget class="QWidget" name="groupShareTab"> + <attribute name="title"> + <string>Share</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <item> + <widget class="QWidget" name="groupShareWidget" native="true"> + <layout class="QGridLayout" name="gridLayout_4"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="1" column="2"> + <widget class="QLabel" name="groupSharePathLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string notr="true"><path></string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="groupShareTypeLabel"> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string notr="true"><type></string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="2"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>147</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + </layout> + </widget> </widget> </item> </layout> 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<QStandardItem*>() << 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<QStandardItem*>() << 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 <QStandardItemModel> #include <QWidget> -#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<Ui::EditWidgetProperties> m_ui; QPointer<CustomData> m_customData; QPointer<QStandardItemModel> 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,70 @@ QStringList FileDialog::getOpenFileNames(QWidget* parent, } } +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; + 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); +#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& defaultExtension, + const QString& defaultName) { if (!m_nextFileName.isEmpty()) { QString result = m_nextFileName; @@ -98,6 +155,7 @@ 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); @@ -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,9 +608,20 @@ <string>Report a &bug</string> </property> </action> + <action name="actionShare_entry"> + <property name="text"> + <string>Share entry</string> + </property> + </action> </widget> <customwidgets> <customwidget> + <class>PasswordGeneratorWidget</class> + <extends>QWidget</extends> + <header>gui/PasswordGeneratorWidget.h</header> + <container>1</container> + </customwidget> + <customwidget> <class>MessageWidget</class> <extends>QWidget</extends> <header>gui/MessageWidget.h</header> @@ -634,12 +645,6 @@ <header>gui/WelcomeWidget.h</header> <container>1</container> </customwidget> - <customwidget> - <class>PasswordGeneratorWidget</class> - <extends>QWidget</extends> - <header>gui/PasswordGeneratorWidget.h</header> - <container>1</container> - </customwidget> </customwidgets> <resources/> <connections/> 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<IDatabaseSettingsPage> 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<QTabWidget> m_securityTabWidget; QPointer<DatabaseSettingsWidgetMasterKey> m_masterKeyWidget; QPointer<DatabaseSettingsWidgetEncryption> m_encryptionWidget; + + class ExtraPage; + QList<ExtraPage> 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<Ui::EditEntryWidgetAutoType> m_autoTypeUi; const QScopedPointer<Ui::EditEntryWidgetSSHAgent> m_sshAgentUi; const QScopedPointer<Ui::EditEntryWidgetHistory> m_historyUi; + const QScopedPointer<CustomData> 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<IEditGroupPage> 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); - - m_editWidgetProperties->setFields(group->timeInfo(), group->uuid()); - m_editWidgetProperties->setCustomData(group->customData()); + 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()); + + 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_group->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex())); - m_group->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex())); + 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->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<Ui::EditGroupWidgetMain> m_mainUi; + QPointer<QWidget> m_editGroupWidgetMain; QPointer<EditWidgetIcons> m_editGroupWidgetIcons; QPointer<EditWidgetProperties> m_editWidgetProperties; - QPointer<Group> m_group; + QScopedPointer<Group> m_temporaryGroup; QPointer<Database> m_database; + QPointer<Group> m_group; + + class ExtraPage; + QList<ExtraPage> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "DatabaseSettingsPageKeeShare.h" + +#include "core/Database.h" +#include "core/FilePath.h" +#include "core/Group.h" +#include "keeshare/DatabaseSettingsWidgetKeeShare.h" +#include "keeshare/KeeShare.h" + +#include <QApplication> + +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<DatabaseSettingsWidgetKeeShare*>(widget); + settingsWidget->loadSettings(db); +} + +void DatabaseSettingsPageKeeShare::saveSettings(QWidget* widget) +{ + DatabaseSettingsWidgetKeeShare* settingsWidget = reinterpret_cast<DatabaseSettingsWidgetKeeShare*>(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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_DATABASESETTINGSPAGEKEESHARE_H +#define KEEPASSXC_DATABASESETTINGSPAGEKEESHARE_H + +#include <QObject> +#include <QPointer> +#include <QWidget> + +#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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QMessageBox> +#include <QStandardItemModel> + +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<Group*> 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<QStandardItem*> row = QList<QStandardItem*>(); + 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_DATABASESETTINGSWIDGETKEESHARE_H +#define KEEPASSXC_DATABASESETTINGSWIDGETKEESHARE_H + +#include <QPointer> +#include <QScopedPointer> +#include <QWidget> + +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<Ui::DatabaseSettingsWidgetKeeShare> m_ui; + + QScopedPointer<QStandardItemModel> m_referencesModel; + QPointer<Database> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DatabaseSettingsWidgetKeeShare</class> + <widget class="QWidget" name="DatabaseSettingsWidgetKeeShare"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>327</width> + <height>379</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="enableGroupBox"> + <property name="title"> + <string>Sharing</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" rowspan="2"> + <widget class="QTableView" name="sharedGroupsView"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="textElideMode"> + <enum>Qt::ElideMiddle</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QMessageBox> +#include <QPainter> +#include <QPushButton> + +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<ShareObserver> observer = m_observersByDatabase.take(oldDb); + if (observer) { + delete observer; + } + } + + if (newDb && !m_observersByDatabase.contains(newDb)) { + QPointer<ShareObserver> 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<ShareObserver> observer = m_observersByDatabase.value(db); + if (observer) { + observer->handleDatabaseOpened(); + } +} + +void KeeShare::handleDatabaseSaved(Database* db) +{ + QPointer<ShareObserver> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_KEESHARE_H +#define KEEPASSXC_KEESHARE_H + +#include <QMap> +#include <QObject> + +#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<QObject*, QPointer<ShareObserver>> m_observersByDatabase; + QMap<QObject*, QPointer<Database>> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QMessageBox> +#include <QPainter> +#include <QPushButton> + +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<void(QXmlStreamWriter& writer)> 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<void(QXmlStreamReader& reader)> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_KEESHARESETTINGS_H +#define KEEPASSXC_KEESHARESETTINGS_H + +#include <QMap> +#include <QObject> + +#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<Certificate> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QApplication> +#include <QObject> + +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<SettingsWidgetKeeShare*>(widget); + settingsWidget->loadSettings(); +} + +void SettingsPageKeeShare::saveSettings(QWidget* widget) +{ + Q_UNUSED(widget); + SettingsWidgetKeeShare* settingsWidget = reinterpret_cast<SettingsWidgetKeeShare*>(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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_SETTINGSPAGEKEESHARE_H +#define KEEPASSXC_SETTINGSPAGEKEESHARE_H + +#include <QObject> +#include <QPointer> +#include <QWidget> + +#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<DatabaseTabWidget> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QMessageBox> +#include <QStandardItemModel> + +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<QStandardItem*>() << 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<KeeShareSettings::Certificate> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_SETTINGSWIDGETKEESHARE_H +#define KEEPASSXC_SETTINGSWIDGETKEESHARE_H + +#include <QPointer> +#include <QScopedPointer> +#include <QWidget> + +#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<Ui::SettingsWidgetKeeShare> m_ui; + + KeeShareSettings::Own m_own; + KeeShareSettings::Foreign m_foreign; + QScopedPointer<QStandardItemModel> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SettingsWidgetKeeShare</class> + <widget class="QWidget" name="SettingsWidgetKeeShare"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>327</width> + <height>423</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QGroupBox" name="activeGroupBox"> + <property name="title"> + <string>Active</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="3" column="0"> + <widget class="QCheckBox" name="enableExportCheckBox"> + <property name="text"> + <string>Allow export</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QCheckBox" name="enableImportCheckBox"> + <property name="text"> + <string>Allow import</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="ownCertificateGroupBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Own certificate</string> + </property> + <layout class="QGridLayout" name="gridLayout_2" columnstretch="0,1,1"> + <item row="5" column="0"> + <widget class="QLabel" name="ownCertificateFingerprintLabel"> + <property name="text"> + <string>Fingerprint:</string> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QLineEdit" name="ownCertificatePrivateKeyEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="ownCertificatePublicKeyLabel"> + <property name="text"> + <string>Certificate:</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="ownCertificateSignerLabel"> + <property name="text"> + <string>Signer</string> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="ownCertificatePrivateKeyLabel"> + <property name="text"> + <string>Key:</string> + </property> + </widget> + </item> + <item row="4" column="1" colspan="2"> + <widget class="QLineEdit" name="ownCertificatePublicKeyEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLineEdit" name="ownCertificateSignerEdit"/> + </item> + <item row="5" column="1" colspan="2"> + <widget class="QLineEdit" name="ownCertificateFingerprintEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="0" colspan="3"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="generateOwnCerticateButton"> + <property name="text"> + <string>Generate</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="importOwnCertificateButton"> + <property name="text"> + <string>Import</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="exportOwnCertificateButton"> + <property name="text"> + <string>Export</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="importedCertificatesGroupBox"> + <property name="title"> + <string>Imported certificates</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="1" column="0" rowspan="2"> + <widget class="QTableView" name="importedCertificateTableView"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="showDropIndicator" stdset="0"> + <bool>false</bool> + </property> + <property name="dragDropOverwriteMode"> + <bool>false</bool> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::MultiSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <attribute name="horizontalHeaderVisible"> + <bool>true</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="certificatesActionLayout"> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="trustImportedCertificateButton"> + <property name="text"> + <string>Trust</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="untrustImportedCertificateButton"> + <property name="text"> + <string>Untrust</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeImportedCertificateButton"> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QBuffer> +#include <QDebug> +#include <QFileInfo> +#include <QIcon> +#include <QMessageBox> +#include <QPainter> +#include <QPushButton> +#include <QStringBuilder> + +#include <quazip5/quazip.h> +#include <quazip5/quazipfile.h> + +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<Trust, KeeShareSettings::Certificate> check(QByteArray& data, + const KeeShareSettings::Reference& reference, + const KeeShareSettings::Certificate& ownCertificate, + const QList<KeeShareSettings::Certificate>& 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<Update> updated; + QList<Group*> 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<QString>() << KeeShare_Signature << KeeShare_Container; + const auto files = zip.getFileInfoList(); + QSet<QString> 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<CompositeKey>::create(); + key->addKey(QSharedPointer<PasswordKey>::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<CompositeKey>::create(); + key->addKey(QSharedPointer<PasswordKey>::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::Result> ShareObserver::exportIntoReferenceContainers() +{ + QList<Result> 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<Database> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_SHAREOBSERVER_H +#define KEEPASSXC_SHAREOBSERVER_H + +#include <QMap> +#include <QObject> +#include <QSet> +#include <QStringList> +#include <QTimer> + +#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<ShareObserver::Result> 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<KeeShareSettings::Reference, QPointer<Group>> m_referenceToGroup; + QMap<QPointer<Group>, KeeShareSettings::Reference> m_groupToReference; + QMap<QString, QPointer<Group>> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "Signature.h" +#include "core/Tools.h" +#include "crypto/Crypto.h" +#include "crypto/CryptoHash.h" +#include "crypto/ssh/OpenSSHKey.h" + +#include <QByteArray> +#include <gcrypt.h> + +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<QByteArray> parts = key.privateParts(); + if (parts.count() != 6) { + raiseError("Unsupported signing key"); + return QString(); + } + + const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256); + + Tools::Map<Index, gcry_mpi_t, &gcry_mpi_release> mpi; + Tools::Map<Index, gcry_sexp_t, &gcry_sexp_release> 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<QByteArray> parts = key.publicParts(); + if (parts.count() != 2) { + raiseError("Unsupported verification key"); + return false; + } + + const QByteArray block = CryptoHash::hash(data, CryptoHash::Sha256); + + Tools::Map<MPI, gcry_mpi_t, &gcry_mpi_release> mpi; + Tools::Map<SEXP, gcry_sexp_t, &gcry_sexp_release> 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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_SIGNATURE_H +#define KEEPASSXC_SIGNATURE_H + +#include <QString> +#include <gcrypt.h> + +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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#include "EditGroupPageKeeShare.h" + +#include "core/FilePath.h" +#include "keeshare/group/EditGroupWidgetKeeShare.h" + +#include <QApplication> + +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<EditGroupWidgetKeeShare*>(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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#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 <QDir> +#include <QStandardPaths> + +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::Type>() << 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<int>(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<KeeShareSettings::Type>(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 <team@keepassxc.org> + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef KEEPASSXC_EDITGROUPWIDGETKEESHARE_H +#define KEEPASSXC_EDITGROUPWIDGETKEESHARE_H + +#include <QPointer> +#include <QStandardItemModel> +#include <QWidget> + +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<Ui::EditGroupWidgetKeeShare> m_ui; + QPointer<Group> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EditGroupWidgetKeeShare</class> + <widget class="QWidget" name="EditGroupWidgetKeeShare"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>342</width> + <height>378</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="MessageWidget" name="messageWidget" native="true"/> + </item> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="2" column="0"> + <widget class="QLabel" name="typeLabel"> + <property name="text"> + <string>Type:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QComboBox" name="typeComboBox"/> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="pathLabel"> + <property name="text"> + <string>Path:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="pathLayout"> + <item> + <widget class="QLineEdit" name="pathEdit"/> + </item> + <item> + <widget class="QToolButton" name="pathSelectionButton"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="passwordLabel"> + <property name="text"> + <string>Password:</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <layout class="QHBoxLayout" name="passwordLayout"> + <item> + <widget class="PasswordEdit" name="passwordEdit"> + <property name="echoMode"> + <enum>QLineEdit::Password</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="togglePasswordButton"> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="togglePasswordGeneratorButton"> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item row="5" column="1"> + <widget class="PasswordGeneratorWidget" name="passwordGenerator" native="true"/> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>PasswordGeneratorWidget</class> + <extends>QWidget</extends> + <header>gui/PasswordGeneratorWidget.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>PasswordEdit</class> + <extends>QLineEdit</extends> + <header>gui/PasswordEdit.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>MessageWidget</class> + <extends>QWidget</extends> + <header>gui/MessageWidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> 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 <QtNetwork> @@ -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 <http://www.gnu.org/licenses/>. */ -#ifndef AGENTCLIENT_H -#define AGENTCLIENT_H +#ifndef KEEPASSXC_SSHAGENT_H +#define KEEPASSXC_SSHAGENT_H -#include "OpenSSHKey.h" #include <QList> #include <QtCore> #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 |