diff --git a/CMakeLists.txt b/CMakeLists.txt index abac73c94a..0b6af4abfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,9 @@ option(WITH_XC_YUBIKEY "Include YubiKey support." OFF) option(WITH_XC_SSHAGENT "Include SSH agent support." OFF) option(WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)" OFF) option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON) +if(UNIX AND NOT APPLE) + option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF) +endif() if(APPLE) option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF) endif() @@ -65,6 +68,9 @@ if(WITH_XC_ALL) if(APPLE) set(WITH_XC_TOUCHID ON) endif() + if(UNIX AND NOT APPLE) + set(WITH_XC_FDOSECRETS ON) + endif() endif() if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE) diff --git a/COPYING b/COPYING index b3eddd7669..ed2f73a4f0 100644 --- a/COPYING +++ b/COPYING @@ -241,3 +241,7 @@ License: LGPL-2.1 Files: share/macosx/dmg-background.tiff Copyright: 2008-2014, Andrey Tarantsov License: MIT + +Files: share/icons/application/scalable/apps/freedesktop.svg +Copyright: GPL-2+ +Comment: from Freedesktop.org website diff --git a/INSTALL.md b/INSTALL.md index c0c3a7a086..1ea2268550 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -102,6 +102,7 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct -DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF) -DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF) -DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF) + -DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF) -DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group synchronization extension (default: OFF) -DWITH_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare signed containers, requires libquazip5 (default: OFF) -DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF) diff --git a/share/icons/application/scalable/apps/freedesktop.svg b/share/icons/application/scalable/apps/freedesktop.svg new file mode 100644 index 0000000000..455a0b3a52 --- /dev/null +++ b/share/icons/application/scalable/apps/freedesktop.svg @@ -0,0 +1,92 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5ddcb76eab..0407008f42 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,6 +194,9 @@ add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible wit add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)") add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response") add_feature_info(UpdateCheck WITH_XC_UPDATECHECK "Automatic update checking") +if(UNIX AND NOT APPLE) + add_feature_info(FdoSecrets WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API.") +endif() if(APPLE) add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration") endif() @@ -226,6 +229,11 @@ if(WITH_XC_SSHAGENT) set(sshagent_LIB sshagent) endif() +add_subdirectory(fdosecrets) +if(WITH_XC_FDOSECRETS) + set(fdosecrets_LIB fdosecrets) +endif() + set(autotype_SOURCES core/Tools.cpp autotype/AutoType.cpp @@ -270,6 +278,7 @@ target_link_libraries(keepassx_core autotype ${keepassxcbrowser_LIB} ${qrcode_LIB} + ${fdosecrets_LIB} Qt5::Core Qt5::Concurrent Qt5::Network diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index 2acff44666..6aceaa2a0a 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -22,6 +22,7 @@ #cmakedefine WITH_XC_KEESHARE_SECURE #cmakedefine WITH_XC_UPDATECHECK #cmakedefine WITH_XC_TOUCHID +#cmakedefine WITH_XC_FDOSECRETS #cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@" #cmakedefine KEEPASSXC_BUILD_TYPE_RELEASE diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index f6c67ad502..cf6a4b5f3f 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -140,12 +140,20 @@ bool EntrySearcher::searchEntryImpl(Entry* entry) case Field::Notes: found = term->regex.match(entry->notes()).hasMatch(); break; - case Field::Attribute: + case Field::AttributeKey: found = !attributes.filter(term->regex).empty(); break; case Field::Attachment: found = !attachments.filter(term->regex).empty(); break; + case Field::AttributeValue: + // skip protected attributes + if (entry->attributes()->isProtected(term->word)) { + continue; + } + found = entry->attributes()->contains(term->word) + && term->regex.match(entry->attributes()->value(term->word)).hasMatch(); + break; default: // Terms without a specific field try to match title, username, url, and notes found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() @@ -207,12 +215,18 @@ void EntrySearcher::parseSearchTerms(const QString& searchString) } else if (field.compare("notes", cs) == 0) { term->field = Field::Notes; } else if (field.startsWith("attr", cs)) { - term->field = Field::Attribute; + term->field = Field::AttributeKey; } else if (field.startsWith("attach", cs)) { term->field = Field::Attachment; - } else { - term->field = Field::Undefined; + } else if (field.startsWith("_", cs)) { + term->field = Field::AttributeValue; + // searching a custom attribute + // in this case term->word is the attribute key (removing the leading "_") + // and term->regex is used to match attribute value + term->word = field.mid(1); } + } else { + term->field = Field::Undefined; } m_searchTerms.append(term); diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h index 153a0612ee..4a33949244 100644 --- a/src/core/EntrySearcher.h +++ b/src/core/EntrySearcher.h @@ -48,8 +48,9 @@ class EntrySearcher Password, Url, Notes, - Attribute, - Attachment + AttributeKey, + Attachment, + AttributeValue }; struct SearchTerm diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 87741ee0cd..814ac2f3b5 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -1057,6 +1057,23 @@ Entry* Group::addEntryWithPath(const QString& entryPath) return entry; } +void Group::applyGroupIconTo(Entry* entry) +{ + if (!config()->get("UseGroupIconOnEntryCreation").toBool()) { + return; + } + + if (iconNumber() == Group::DefaultIconNumber && iconUuid().isNull()) { + return; + } + + if (iconUuid().isNull()) { + entry->setIcon(iconNumber()); + } else { + entry->setIcon(iconUuid()); + } +} + bool Group::GroupData::operator==(const Group::GroupData& other) const { return equals(other, CompareItemDefault); diff --git a/src/core/Group.h b/src/core/Group.h index 4b85692074..59e455ac07 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -167,6 +167,8 @@ class Group : public QObject void addEntry(Entry* entry); void removeEntry(Entry* entry); + void applyGroupIconTo(Entry* entry); + signals: void groupDataChanged(Group* group); void groupAboutToAdd(Group* group, int index); diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index cdb0d1f562..ee4295eec5 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -126,6 +126,8 @@ int SymmetricCipher::algorithmIvSize(Algorithm algo) switch (algo) { case ChaCha20: return 12; + case Aes128: + return 16; case Aes256: return 16; case Twofish: diff --git a/src/fdosecrets/CMakeLists.txt b/src/fdosecrets/CMakeLists.txt new file mode 100644 index 0000000000..9d3fcb6a92 --- /dev/null +++ b/src/fdosecrets/CMakeLists.txt @@ -0,0 +1,36 @@ +if(WITH_XC_FDOSECRETS) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + + add_library(fdosecrets STATIC + # app settings page + FdoSecretsPlugin.cpp + widgets/SettingsWidgetFdoSecrets.cpp + + # per database settings page + DatabaseSettingsPageFdoSecrets.cpp + widgets/DatabaseSettingsWidgetFdoSecrets.cpp + + # setting storage + FdoSecretsSettings.cpp + + # gcrypt MPI wrapper + GcryptMPI.cpp + + # dbus objects + objects/DBusObject.cpp + objects/Service.cpp + objects/Session.cpp + objects/SessionCipher.cpp + objects/Collection.cpp + objects/Item.cpp + objects/Prompt.cpp + objects/adaptors/ServiceAdaptor.cpp + objects/adaptors/SessionAdaptor.cpp + objects/adaptors/CollectionAdaptor.cpp + objects/adaptors/ItemAdaptor.cpp + objects/adaptors/PromptAdaptor.cpp + objects/DBusReturn.cpp + objects/DBusTypes.cpp + ) + target_link_libraries(fdosecrets Qt5::Core Qt5::Widgets Qt5::DBus ${GCRYPT_LIBRARIES}) +endif() diff --git a/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp new file mode 100644 index 0000000000..afd888ed27 --- /dev/null +++ b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DatabaseSettingsPageFdoSecrets.h" + +#include "fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h" + +#include "core/FilePath.h" + +QString DatabaseSettingsPageFdoSecrets::name() +{ + return QObject::tr("Secret Service Integration"); +} + +QIcon DatabaseSettingsPageFdoSecrets::icon() +{ + return filePath()->icon(QStringLiteral("apps"), QStringLiteral("freedesktop")); +} + +QWidget* DatabaseSettingsPageFdoSecrets::createWidget() +{ + return new DatabaseSettingsWidgetFdoSecrets; +} + +void DatabaseSettingsPageFdoSecrets::loadSettings(QWidget* widget, QSharedPointer db) +{ + auto settingsWidget = qobject_cast(widget); + settingsWidget->loadSettings(db); +} + +void DatabaseSettingsPageFdoSecrets::saveSettings(QWidget* widget) +{ + auto settingsWidget = qobject_cast(widget); + settingsWidget->saveSettings(); +} diff --git a/src/fdosecrets/DatabaseSettingsPageFdoSecrets.h b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.h new file mode 100644 index 0000000000..c54f5a2761 --- /dev/null +++ b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H +#define KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H + +#include "gui/dbsettings/DatabaseSettingsDialog.h" + +class DatabaseSettingsPageFdoSecrets : public IDatabaseSettingsPage +{ + Q_DISABLE_COPY(DatabaseSettingsPageFdoSecrets) +public: + DatabaseSettingsPageFdoSecrets() = default; + + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget, QSharedPointer db) override; + void saveSettings(QWidget* widget) override; +}; + +#endif // KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp new file mode 100644 index 0000000000..668b5fb046 --- /dev/null +++ b/src/fdosecrets/FdoSecretsPlugin.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FdoSecretsPlugin.h" + +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/DBusTypes.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/widgets/SettingsWidgetFdoSecrets.h" + +#include "gui/DatabaseTabWidget.h" + +#include + +using FdoSecrets::Service; + +FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget) + : m_dbTabs(tabWidget) +{ + FdoSecrets::registerDBusTypes(); +} + +QWidget* FdoSecretsPlugin::createWidget() +{ + return new SettingsWidgetFdoSecrets(this); +} + +void FdoSecretsPlugin::loadSettings(QWidget* widget) +{ + qobject_cast(widget)->loadSettings(); +} + +void FdoSecretsPlugin::saveSettings(QWidget* widget) +{ + qobject_cast(widget)->saveSettings(); + updateServiceState(); +} + +void FdoSecretsPlugin::updateServiceState() +{ + if (FdoSecrets::settings()->isEnabled()) { + if (!m_secretService && m_dbTabs) { + m_secretService.reset(new Service(this, m_dbTabs)); + connect(m_secretService.data(), &Service::error, this, [this](const QString& msg) { + emit error(tr("Fdo Secret Service: %1").arg(msg)); + }); + if (!m_secretService->initialize()) { + m_secretService.reset(); + } + } + } else { + if (m_secretService) { + m_secretService.reset(); + } + } +} + +Service* FdoSecretsPlugin::serviceInstance() const +{ + return m_secretService.data(); +} + +void FdoSecretsPlugin::emitRequestSwitchToDatabases() +{ + emit requestSwitchToDatabases(); +} + +void FdoSecretsPlugin::emitRequestShowNotification(const QString& msg, const QString& title) +{ + if (!FdoSecrets::settings()->showNotification()) { + return; + } + emit requestShowNotification(msg, title, 10000); +} diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h new file mode 100644 index 0000000000..2a57ea0db3 --- /dev/null +++ b/src/fdosecrets/FdoSecretsPlugin.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETSPLUGIN_H +#define KEEPASSXC_FDOSECRETSPLUGIN_H + +#include "core/FilePath.h" +#include "gui/ApplicationSettingsWidget.h" + +#include +#include + +class DatabaseTabWidget; + +namespace FdoSecrets +{ + class Service; +} // namespace FdoSecrets + +class FdoSecretsPlugin : public QObject, public ISettingsPage +{ + Q_OBJECT +public: + explicit FdoSecretsPlugin(DatabaseTabWidget* tabWidget); + ~FdoSecretsPlugin() override = default; + + QString name() override + { + return QObject::tr("Secret Service Integration"); + } + + QIcon icon() override + { + return FilePath::instance()->icon("apps", "freedesktop"); + } + + QWidget* createWidget() override; + void loadSettings(QWidget* widget) override; + void saveSettings(QWidget* widget) override; + + void updateServiceState(); + + /** + * @return The service instance, can be nullptr if the service is disabled. + */ + FdoSecrets::Service* serviceInstance() const; + +public slots: + void emitRequestSwitchToDatabases(); + void emitRequestShowNotification(const QString& msg, const QString& title = {}); + +signals: + void error(const QString& msg); + void requestSwitchToDatabases(); + void requestShowNotification(const QString& msg, const QString& title, int msTimeoutHint); + +private: + QPointer m_dbTabs; + QScopedPointer m_secretService; +}; + +#endif // KEEPASSXC_FDOSECRETSPLUGIN_H diff --git a/src/fdosecrets/FdoSecretsSettings.cpp b/src/fdosecrets/FdoSecretsSettings.cpp new file mode 100644 index 0000000000..36c41705f3 --- /dev/null +++ b/src/fdosecrets/FdoSecretsSettings.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FdoSecretsSettings.h" + +#include "core/Config.h" +#include "core/CustomData.h" +#include "core/Database.h" +#include "core/Metadata.h" + +namespace Keys +{ + + constexpr auto FdoSecretsEnabled = "FdoSecrets/Enabled"; + constexpr auto FdoSecretsShowNotification = "FdoSecrets/ShowNotification"; + constexpr auto FdoSecretsNoConfirmDeleteItem = "FdoSecrets/NoConfirmDeleteItem"; + + namespace Db + { + constexpr auto FdoSecretsExposedGroup = "FDO_SECRETS_EXPOSED_GROUP"; + } // namespace Db + +} // namespace Keys + +namespace FdoSecrets +{ + + FdoSecretsSettings* FdoSecretsSettings::m_instance = nullptr; + + FdoSecretsSettings* FdoSecretsSettings::instance() + { + if (!m_instance) { + m_instance = new FdoSecretsSettings; + } + return m_instance; + } + + bool FdoSecretsSettings::isEnabled() const + { + return config()->get(Keys::FdoSecretsEnabled, false).toBool(); + } + + void FdoSecretsSettings::setEnabled(bool enabled) + { + config()->set(Keys::FdoSecretsEnabled, enabled); + } + + bool FdoSecretsSettings::showNotification() const + { + return config()->get(Keys::FdoSecretsShowNotification, true).toBool(); + } + + void FdoSecretsSettings::setShowNotification(bool show) + { + config()->set(Keys::FdoSecretsShowNotification, show); + } + + bool FdoSecretsSettings::noConfirmDeleteItem() const + { + return config()->get(Keys::FdoSecretsNoConfirmDeleteItem, false).toBool(); + } + + void FdoSecretsSettings::setNoConfirmDeleteItem(bool noConfirm) + { + config()->set(Keys::FdoSecretsNoConfirmDeleteItem, noConfirm); + } + + QUuid FdoSecretsSettings::exposedGroup(const QSharedPointer& db) const + { + return exposedGroup(db.data()); + } + + void FdoSecretsSettings::setExposedGroup(const QSharedPointer& db, + const QUuid& group) // clazy:exclude=function-args-by-value + { + setExposedGroup(db.data(), group); + } + + QUuid FdoSecretsSettings::exposedGroup(Database* db) const + { + return {db->metadata()->customData()->value(Keys::Db::FdoSecretsExposedGroup)}; + } + + void FdoSecretsSettings::setExposedGroup(Database* db, const QUuid& group) // clazy:exclude=function-args-by-value + { + db->metadata()->customData()->set(Keys::Db::FdoSecretsExposedGroup, group.toString()); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/FdoSecretsSettings.h b/src/fdosecrets/FdoSecretsSettings.h new file mode 100644 index 0000000000..5a90288763 --- /dev/null +++ b/src/fdosecrets/FdoSecretsSettings.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETSSETTINGS_H +#define KEEPASSXC_FDOSECRETSSETTINGS_H + +#include +#include + +class Database; + +namespace FdoSecrets +{ + + class FdoSecretsSettings + { + public: + FdoSecretsSettings() = default; + static FdoSecretsSettings* instance(); + + bool isEnabled() const; + void setEnabled(bool enabled); + + bool showNotification() const; + void setShowNotification(bool show); + + bool noConfirmDeleteItem() const; + void setNoConfirmDeleteItem(bool noConfirm); + + // Per db settings + + QUuid exposedGroup(const QSharedPointer& db) const; + void setExposedGroup(const QSharedPointer& db, const QUuid& group); + QUuid exposedGroup(Database* db) const; + void setExposedGroup(Database* db, const QUuid& group); + + private: + static FdoSecretsSettings* m_instance; + }; + + inline FdoSecretsSettings* settings() + { + return FdoSecretsSettings::instance(); + } + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETSSETTINGS_H diff --git a/src/fdosecrets/GcryptMPI.cpp b/src/fdosecrets/GcryptMPI.cpp new file mode 100644 index 0000000000..4fc792deba --- /dev/null +++ b/src/fdosecrets/GcryptMPI.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "GcryptMPI.h" + +GcryptMPI MpiFromBytes(const QByteArray& bytes, bool secure, gcry_mpi_format format) +{ + auto bufLen = static_cast(bytes.size()); + + const char* buf = nullptr; + + // gcry_mpi_scan uses secure memory only if input buffer is secure memory, so we have to make a copy + GcryptMemPtr secureBuf; + if (secure) { + secureBuf.reset(static_cast(gcry_malloc_secure(bufLen))); + memcpy(secureBuf.get(), bytes.data(), bufLen); + buf = secureBuf.get(); + } else { + buf = bytes.data(); + } + + gcry_mpi_t rawMpi; + auto err = gcry_mpi_scan(&rawMpi, format, buf, format == GCRYMPI_FMT_HEX ? 0 : bufLen, nullptr); + GcryptMPI mpi(rawMpi); + + if (gcry_err_code(err) != GPG_ERR_NO_ERROR) { + mpi.reset(); + } + + return mpi; +} + +GcryptMPI MpiFromHex(const char* hex, bool secure) +{ + return MpiFromBytes(QByteArray::fromRawData(hex, static_cast(strlen(hex) + 1)), secure, GCRYMPI_FMT_HEX); +} + +QByteArray MpiToBytes(const GcryptMPI& mpi) +{ + unsigned char* buf = nullptr; + size_t buflen = 0; + gcry_mpi_aprint(GCRYMPI_FMT_USG, &buf, &buflen, mpi.get()); + + QByteArray bytes(reinterpret_cast(buf), static_cast(buflen)); + + gcry_free(buf); + + return bytes; +} diff --git a/src/fdosecrets/GcryptMPI.h b/src/fdosecrets/GcryptMPI.h new file mode 100644 index 0000000000..bbc3d04397 --- /dev/null +++ b/src/fdosecrets/GcryptMPI.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_GCRYPTMPI_H +#define KEEPASSXC_GCRYPTMPI_H + +#include + +#include + +#include +#include + +template using deleter_from_fn = std::integral_constant; + +/** + * A simple RAII wrapper for gcry_mpi_t + */ +using GcryptMPI = std::unique_ptr>; + +/** + * A simple RAII wrapper for libgcrypt allocated memory + */ +using GcryptMemPtr = std::unique_ptr>; + +/** + * Parse a QByteArray as MPI + * @param bytes + * @param secure + * @param format + * @return + */ +GcryptMPI MpiFromBytes(const QByteArray& bytes, bool secure = true, gcry_mpi_format format = GCRYMPI_FMT_USG); + +/** + * Parse MPI from hex data + * @param hex + * @param secure + * @return + */ +GcryptMPI MpiFromHex(const char* hex, bool secure = true); + +/** + * Dump MPI to bytes in USG format + * @param mpi + * @return + */ +QByteArray MpiToBytes(const GcryptMPI& mpi); + +#endif // KEEPASSXC_GCRYPTMPI_H diff --git a/src/fdosecrets/README.md b/src/fdosecrets/README.md new file mode 100644 index 0000000000..22278860c8 --- /dev/null +++ b/src/fdosecrets/README.md @@ -0,0 +1,42 @@ +# Freedesktop.org Secret Storage Spec Server Side API + +This plugin implements the [Secret Storage specification][secrets] version 0.2. While running KeePassXC, it acts as a +Secret Service server, registered on DBus, so clients like seahorse, python-secretstorage, or other implementations +can connect and access the exposed database in KeePassXC. + +[secrets]: (https://www.freedesktop.org/wiki/Specifications/secret-storage-spec/) + +## Configurable settings + +* The user can specify if a database is exposed on DBus, and which group is exposed. +* Whether to show desktop notification is shown when an entry is retrived. +* Whether to skip confirmation for entries deleted from DBus + +## Implemented Attributes on Item Object + +The following attributes are exposed: + +|Key|Value| +|:---:|:---:| +|Title|The entry title| +|UserName|The entry user name| +|URL|The entry URL| +|Notes|The entry notes| + +In addition, all non-protected custom attributes are also exposed. + +## Architecture + +* `FdoSecrets::Service` is the top level DBus service +* There is one and only one `FdoSecrets::Collection` per opened database tab +* Each entry under the exposed database group has a corresponding `FdoSecrets::Item` DBus object. + +### Signal connections + +- Collections are created when a corresponding database tab opened +- If the database is locked, a collection is created +- When the database is unlocked, collection populates its children +- If the unlocked database's exposed group is none, collection deletes itself +- If the database's exposed group changes, collection repopulates +- If the database's exposed group changes to none, collection deletes itself +- If the database's exposed group changes from none, the service recreates a collection diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp new file mode 100644 index 0000000000..119ec4ad26 --- /dev/null +++ b/src/fdosecrets/objects/Collection.cpp @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Collection.h" + +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Service.h" + +#include "core/Config.h" +#include "core/Database.h" +#include "core/EntrySearcher.h" +#include "gui/DatabaseTabWidget.h" +#include "gui/DatabaseWidget.h" + +#include + +namespace FdoSecrets +{ + + Collection::Collection(Service* parent, DatabaseWidget* backend) + : DBusObject(parent) + , m_backend(backend) + , m_exposedGroup(nullptr) + , m_registered(false) + { + // whenever the file path or the database object itself change, we do a full reload. + connect(backend, &DatabaseWidget::databaseFilePathChanged, this, &Collection::reloadBackend); + connect(backend, &DatabaseWidget::databaseReplaced, this, &Collection::reloadBackend); + + // also remember to clear/populate the database when lock state changes. + connect(backend, &DatabaseWidget::databaseUnlocked, this, &Collection::onDatabaseLockChanged); + connect(backend, &DatabaseWidget::databaseLocked, this, &Collection::onDatabaseLockChanged); + + reloadBackend(); + } + + void Collection::reloadBackend() + { + if (m_registered) { + // delete all items + // this has to be done because the backend is actually still there, just we don't expose them + // NOTE: Do NOT use a for loop, because Item::doDelete will remove itself from m_items. + while (!m_items.isEmpty()) { + m_items.first()->doDelete(); + } + cleanupConnections(); + + unregisterCurrentPath(); + m_registered = false; + } + + // make sure we have updated copy of the filepath, which is used to identify the database. + m_backendPath = m_backend->database()->filePath(); + + // the database may not have a name (derived from filePath) yet, which may happen if it's newly created. + // defer the registration to next time a file path change happens. + if (!name().isEmpty()) { + registerWithPath( + QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), encodePath(name())), + new CollectionAdaptor(this)); + m_registered = true; + } + + // populate contents after expose on dbus, because items rely on parent's dbus object path + if (!backendLocked()) { + populateContents(); + } else { + cleanupConnections(); + } + } + + DBusReturn Collection::ensureBackend() const + { + if (!m_backend) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + } + return {}; + } + + DBusReturn Collection::ensureUnlocked() const + { + if (backendLocked()) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED)); + } + return {}; + } + + DBusReturn> Collection::items() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + return m_items; + } + + DBusReturn Collection::label() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + + if (backendLocked()) { + return name(); + } + return m_backend->database()->metadata()->name(); + } + + DBusReturn Collection::setLabel(const QString& label) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + m_backend->database()->metadata()->setName(label); + return {}; + } + + DBusReturn Collection::locked() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + return backendLocked(); + } + + DBusReturn Collection::created() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + return static_cast( + m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + } + + DBusReturn Collection::modified() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + // FIXME: there seems not to have a global modified time. + // Use a more accurate time, considering all metadata, group, entry. + return static_cast( + m_backend->database()->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + } + + DBusReturn Collection::deleteCollection() + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + + // Delete means close database + auto prompt = new DeleteCollectionPrompt(service(), this); + if (backendLocked()) { + // this won't raise a dialog, immediate execute + auto pret = prompt->prompt({}); + if (pret.isError()) { + return pret; + } + prompt = nullptr; + } + // defer the close to the prompt + return prompt; + } + + DBusReturn> Collection::searchItems(const StringStringMap& attributes) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + // searchItems should work, whether `this` is locked or not. + // however, we can't search items the same way as in gnome-keying, + // because there's no database at all when locked. + return QList{}; + } + + // shortcut logic for Uuid/Path attributes, as they can uniquely identify an item. + if (attributes.contains(ItemAttributes::UuidKey)) { + auto uuid = QUuid::fromRfc4122(QByteArray::fromHex(attributes.value(ItemAttributes::UuidKey).toLatin1())); + auto entry = m_exposedGroup->findEntryByUuid(uuid); + if (entry) { + return QList{m_entryToItem.value(entry)}; + } else { + return QList{}; + } + } + + if (attributes.contains(ItemAttributes::PathKey)) { + auto path = attributes.value(ItemAttributes::PathKey); + auto entry = m_exposedGroup->findEntryByPath(path); + if (entry) { + return QList{m_entryToItem.value(entry)}; + } else { + return QList{}; + } + } + + static QMap attrKeyToField{ + {EntryAttributes::TitleKey, QStringLiteral("title")}, + {EntryAttributes::UserNameKey, QStringLiteral("user")}, + {EntryAttributes::URLKey, QStringLiteral("url")}, + {EntryAttributes::NotesKey, QStringLiteral("notes")}, + }; + + QStringList terms; + for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) { + if (it.key() == EntryAttributes::PasswordKey) { + continue; + } + auto field = attrKeyToField.value(it.key(), QStringLiteral("_") + Item::encodeAttributeKey(it.key())); + terms << QStringLiteral(R"raw(+%1:"%2")raw").arg(field, it.value()); + } + + QList items; + const auto foundEntries = EntrySearcher().search(terms.join(' '), m_exposedGroup); + items.reserve(foundEntries.size()); + for (const auto& entry : foundEntries) { + items << m_entryToItem.value(entry); + } + return items; + } + + DBusReturn + Collection::createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + prompt = nullptr; + + Item* item = nullptr; + QString itemPath; + StringStringMap attributes; + + // check existing item using attributes + auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")); + if (iterAttr != properties.end()) { + attributes = qdbus_cast(iterAttr.value().value()); + + itemPath = attributes.value(ItemAttributes::PathKey); + + auto existings = searchItems(attributes); + if (existings.isError()) { + return existings; + } + if (!existings.value().isEmpty() && replace) { + item = existings.value().front(); + } + } + + if (!item) { + // normalize itemPath + itemPath = itemPath.startsWith('/') ? QString{} : QStringLiteral("/") + itemPath; + + // split itemPath to groupPath and itemName + auto components = itemPath.split('/'); + Q_ASSERT(components.size() >= 2); + + auto itemName = components.takeLast(); + Group* group = findCreateGroupByPath(components.join('/')); + + // create new Entry in backend + auto* entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setTitle(itemName); + entry->setUsername(m_backend->database()->metadata()->defaultUserName()); + group->applyGroupIconTo(entry); + + entry->setGroup(group); + + // when creation finishes in backend, we will already have item + item = m_entryToItem.value(entry, nullptr); + Q_ASSERT(item); + } + + ret = item->setProperties(properties); + if (ret.isError()) { + return ret; + } + ret = item->setSecret(secret); + if (ret.isError()) { + return ret; + } + + return item; + } + + DBusReturn Collection::setProperties(const QVariantMap& properties) + { + auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION ".Label")).toString(); + + auto ret = setLabel(label); + if (ret.isError()) { + return ret; + } + + return {}; + } + + const QSet Collection::aliases() const + { + return m_aliases; + } + + DBusReturn Collection::addAlias(QString alias) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + + alias = encodePath(alias); + + if (m_aliases.contains(alias)) { + return {}; + } + + emit aliasAboutToAdd(alias); + + bool ok = QDBusConnection::sessionBus().registerObject( + QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias), this); + if (ok) { + m_aliases.insert(alias); + emit aliasAdded(alias); + } + + return {}; + } + + DBusReturn Collection::removeAlias(QString alias) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + + alias = encodePath(alias); + + if (!m_aliases.contains(alias)) { + return {}; + } + + QDBusConnection::sessionBus().unregisterObject( + QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias)); + + m_aliases.remove(alias); + emit aliasRemoved(alias); + + return {}; + } + + QString Collection::name() const + { + return QFileInfo(m_backendPath).baseName(); + } + + DatabaseWidget* Collection::backend() const + { + return m_backend; + } + + void Collection::onDatabaseLockChanged() + { + auto locked = backendLocked(); + if (!locked) { + populateContents(); + } else { + cleanupConnections(); + } + emit collectionLockChanged(locked); + emit collectionChanged(); + } + + void Collection::populateContents() + { + if (!m_registered) { + return; + } + + // we have an unlocked db + + auto newUuid = FdoSecrets::settings()->exposedGroup(m_backend->database()); + auto newGroup = m_backend->database()->rootGroup()->findGroupByUuid(newUuid); + if (!newGroup) { + // no exposed group, delete self + doDelete(); + return; + } + + // clean up old group connections + cleanupConnections(); + + m_exposedGroup = newGroup; + + // Attach signal to update exposed group settings if the group was removed. + // The lifetime of the connection is bound to the database object, because + // in Database::~Database, groups are also deleted, but we don't want to + // trigger this. + // This rely on the fact that QObject disconnects signals BEFORE deleting + // children. + QPointer db = m_backend->database().data(); + connect(m_exposedGroup.data(), &Group::groupAboutToRemove, db, [db](Group* toBeRemoved) { + if (!db) { + return; + } + auto uuid = FdoSecrets::settings()->exposedGroup(db); + auto exposedGroup = db->rootGroup()->findGroupByUuid(uuid); + if (toBeRemoved == exposedGroup) { + // reset the exposed group to none + FdoSecrets::settings()->setExposedGroup(db, {}); + } + }); + + // Monitor exposed group settings + connect(m_backend->database()->metadata()->customData(), &CustomData::customDataModified, this, [this]() { + if (!m_exposedGroup || !m_backend) { + return; + } + if (m_exposedGroup->uuid() == FdoSecrets::settings()->exposedGroup(m_backend->database())) { + // no change + return; + } + onDatabaseExposedGroupChanged(); + }); + + // Add items for existing entry + const auto entries = m_exposedGroup->entriesRecursive(false); + for (const auto& entry : entries) { + onEntryAdded(entry, false); + } + + connectGroupSignalRecursive(m_exposedGroup); + } + + void Collection::onDatabaseExposedGroupChanged() + { + // delete all items + // this has to be done because the backend is actually still there + // just we don't expose them + for (const auto& item : asConst(m_items)) { + item->doDelete(); + } + + // repopulate + Q_ASSERT(!backendLocked()); + populateContents(); + } + + void Collection::onEntryAdded(Entry* entry, bool emitSignal) + { + if (inRecycleBin(entry)) { + return; + } + + auto item = new Item(this, entry); + m_items << item; + m_entryToItem[entry] = item; + + // forward delete signals + connect(entry->group(), &Group::entryAboutToRemove, item, [item](Entry* toBeRemoved) { + if (item->backend() == toBeRemoved) { + item->doDelete(); + } + }); + + // relay signals + connect(item, &Item::itemChanged, this, [this, item]() { emit itemChanged(item); }); + connect(item, &Item::itemAboutToDelete, this, [this, item]() { + m_items.removeAll(item); + m_entryToItem.remove(item->backend()); + emit itemDeleted(item); + }); + + if (emitSignal) { + emit itemCreated(item); + } + } + + void Collection::connectGroupSignalRecursive(Group* group) + { + if (inRecycleBin(group)) { + return; + } + + connect(group, &Group::groupModified, this, &Collection::collectionChanged); + connect(group, &Group::entryAdded, this, [this](Entry* entry) { onEntryAdded(entry, true); }); + + const auto children = group->children(); + for (const auto& cg : children) { + connectGroupSignalRecursive(cg); + } + } + + Service* Collection::service() const + { + return qobject_cast(parent()); + } + + void Collection::doLock() + { + Q_ASSERT(m_backend); + + m_backend->lock(); + } + + void Collection::doUnlock() + { + Q_ASSERT(m_backend); + + service()->doUnlockDatabaseInDialog(m_backend); + } + + void Collection::doDelete() + { + emit collectionAboutToDelete(); + + unregisterCurrentPath(); + + // remove alias manually to trigger signal + for (const auto& a : aliases()) { + removeAlias(a).okOrDie(); + } + + cleanupConnections(); + + m_exposedGroup = nullptr; + + // reset backend and delete self + m_backend = nullptr; + deleteLater(); + } + + void Collection::cleanupConnections() + { + if (m_exposedGroup) { + m_exposedGroup->disconnect(this); + } + m_items.clear(); + } + + QString Collection::backendFilePath() const + { + return m_backendPath; + } + + Group* Collection::exposedRootGroup() const + { + return m_exposedGroup; + } + + bool Collection::backendLocked() const + { + return !m_backend || !m_backend->database()->isInitialized() || m_backend->isLocked(); + } + + void Collection::doDeleteEntries(QList entries) + { + m_backend->deleteEntries(std::move(entries)); + } + + Group* Collection::findCreateGroupByPath(const QString& groupPath) + { + auto group = m_exposedGroup->findGroupByPath(groupPath); + if (group) { + return group; + } + + // groupPath can't be empty here, because otherwise it will match m_exposedGroup and was returned above + Q_ASSERT(!groupPath.isEmpty()); + + auto groups = groupPath.split('/', QString::SkipEmptyParts); + auto groupName = groups.takeLast(); + + // create parent group + auto parentGroup = findCreateGroupByPath(groups.join('/')); + + // create this group + group = new Group(); + group->setUuid(QUuid::createUuid()); + group->setName(groupName); + group->setIcon(Group::DefaultIconNumber); + group->setParent(parentGroup); + + return group; + } + + bool Collection::inRecycleBin(Group* group) const + { + Q_ASSERT(m_backend); + + if (!m_backend->database()->metadata()->recycleBin()) { + return false; + } + + while (group) { + if (group->uuid() == m_backend->database()->metadata()->recycleBin()->uuid()) { + return true; + } + group = group->parentGroup(); + } + return false; + } + + bool Collection::inRecycleBin(Entry* entry) const + { + Q_ASSERT(entry); + return inRecycleBin(entry->group()); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h new file mode 100644 index 0000000000..f11669b7de --- /dev/null +++ b/src/fdosecrets/objects/Collection.h @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_COLLECTION_H +#define KEEPASSXC_FDOSECRETS_COLLECTION_H + +#include "DBusObject.h" + +#include "adaptors/CollectionAdaptor.h" + +#include +#include + +class Database; +class DatabaseWidget; +class Entry; +class Group; + +namespace FdoSecrets +{ + class Item; + class PromptBase; + class Service; + class Collection : public DBusObject + { + Q_OBJECT + public: + explicit Collection(Service* parent, DatabaseWidget* backend); + + DBusReturn> items() const; + + DBusReturn label() const; + DBusReturn setLabel(const QString& label); + + DBusReturn locked() const; + + DBusReturn created() const; + + DBusReturn modified() const; + + DBusReturn deleteCollection(); + DBusReturn> searchItems(const StringStringMap& attributes); + DBusReturn + createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt); + + signals: + void itemCreated(const Item* item); + void itemDeleted(const Item* item); + void itemChanged(const Item* item); + + void collectionChanged(); + void collectionAboutToDelete(); + void collectionLockChanged(bool newLocked); + + void aliasAboutToAdd(const QString& alias); + void aliasAdded(const QString& alias); + void aliasRemoved(const QString& alias); + + public: + DBusReturn setProperties(const QVariantMap& properties); + + DBusReturn removeAlias(QString alias); + DBusReturn addAlias(QString alias); + const QSet aliases() const; + + /** + * A human readable name of the collection, available even if the db is locked + * @return + */ + QString name() const; + + Group* exposedRootGroup() const; + DatabaseWidget* backend() const; + QString backendFilePath() const; + Service* service() const; + bool inRecycleBin(Group* group) const; + bool inRecycleBin(Entry* entry) const; + + public slots: + // expose some methods for Prmopt to use + void doLock(); + void doUnlock(); + // will remove self + void doDelete(); + + // delete the Entry in backend from this collection + void doDeleteEntries(QList entries); + + private slots: + void onDatabaseLockChanged(); + void onDatabaseExposedGroupChanged(); + void reloadBackend(); + + private: + friend class DeleteCollectionPrompt; + friend class CreateCollectionPrompt; + + void onEntryAdded(Entry* entry, bool emitSignal); + void populateContents(); + void connectGroupSignalRecursive(Group* group); + void cleanupConnections(); + + bool backendLocked() const; + + /** + * Check if the backend is a valid object, send error reply if not. + * @return true if the backend is valid. + */ + DBusReturn ensureBackend() const; + + /** + * Ensure the database is unlocked, send error reply if locked. + * @return true if the database is locked + */ + DBusReturn ensureUnlocked() const; + + /** + * Like mkdir -p, find or create the group by path, under m_exposedGroup + * @param groupPath + * @return created or found group + */ + Group* findCreateGroupByPath(const QString& groupPath); + + private: + QPointer m_backend; + QString m_backendPath; + QPointer m_exposedGroup; + + QSet m_aliases; + QList m_items; + QMap m_entryToItem; + + bool m_registered; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_COLLECTION_H diff --git a/src/fdosecrets/objects/DBusObject.cpp b/src/fdosecrets/objects/DBusObject.cpp new file mode 100644 index 0000000000..8bf1ae4e59 --- /dev/null +++ b/src/fdosecrets/objects/DBusObject.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusObject.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace FdoSecrets +{ + + DBusObject::DBusObject(DBusObject* parent) + : QObject(parent) + { + } + + void DBusObject::registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor) + { + m_objectPath.setPath(path); + m_dbusAdaptor = adaptor; + adaptor->setParent(this); + auto ok = QDBusConnection::sessionBus().registerObject(m_objectPath.path(), this); + Q_UNUSED(ok); + Q_ASSERT(ok); + } + + QString DBusObject::callingPeerName() const + { + auto pid = callingPeerPid(); + QFile proc(QStringLiteral("/proc/%1/comm").arg(pid)); + if (!proc.open(QFile::ReadOnly)) { + return callingPeer(); + } + QTextStream stream(&proc); + return stream.readAll().trimmed(); + } + + QString encodePath(const QString& value) + { + // force "-.~_" to be encoded + return QUrl::toPercentEncoding(value, "", "-.~_").replace('%', '_'); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/DBusObject.h b/src/fdosecrets/objects/DBusObject.h new file mode 100644 index 0000000000..539a2dfd7f --- /dev/null +++ b/src/fdosecrets/objects/DBusObject.h @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSOBJECT_H +#define KEEPASSXC_FDOSECRETS_DBUSOBJECT_H + +#include "fdosecrets/objects/DBusReturn.h" +#include "fdosecrets/objects/DBusTypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QDBusAbstractAdaptor; + +namespace FdoSecrets +{ + class Service; + + /** + * @brief A common base class for all dbus-exposed objects + */ + class DBusObject : public QObject, public QDBusContext + { + Q_OBJECT + public: + explicit DBusObject(DBusObject* parent = nullptr); + + const QDBusObjectPath& objectPath() const + { + return m_objectPath; + } + + protected: + void registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor); + + void unregisterCurrentPath() + { + QDBusConnection::sessionBus().unregisterObject(m_objectPath.path()); + m_dbusAdaptor = nullptr; + m_objectPath.setPath(QStringLiteral("/")); + } + + QString callingPeer() const + { + Q_ASSERT(calledFromDBus()); + return message().service(); + } + + uint callingPeerPid() const + { + return connection().interface()->servicePid(callingPeer()); + } + + QString callingPeerName() const; + + template Adaptor& dbusAdaptor() const + { + return *static_cast(m_dbusAdaptor); + } + + DBusObject* p() const + { + return qobject_cast(parent()); + } + + private: + /** + * Derived class should not directly use sendErrorReply. + * Instead, use raiseError + */ + using QDBusContext::sendErrorReply; + + template friend class DBusReturn; + + private: + QDBusAbstractAdaptor* m_dbusAdaptor; + QDBusObjectPath m_objectPath; + }; + + /** + * Return the object path of the pointed DBusObject, or "/" if the pointer is null + * @tparam T + * @param object + * @return + */ + template QDBusObjectPath objectPathSafe(T* object) + { + if (object) { + return object->objectPath(); + } + return QDBusObjectPath(QStringLiteral("/")); + } + + /** + * Convert a list of DBusObjects to object path + * @tparam T + * @param objects + * @return + */ + template QList objectsToPath(QList objects) + { + QList res; + res.reserve(objects.size()); + for (auto object : objects) { + res.append(objectPathSafe(object)); + } + return res; + } + + /** + * Convert an object path to a pointer of the object + * @tparam T + * @param path + * @return the pointer of the object, or nullptr if path is "/" + */ + template T* pathToObject(const QDBusObjectPath& path) + { + if (path.path() == QStringLiteral("/")) { + return nullptr; + } + return qobject_cast(QDBusConnection::sessionBus().objectRegisteredAt(path.path())); + } + + /** + * Convert a list of object paths to a list of objects. + * "/" paths (i.e. nullptrs) will be skipped in the resulting list + * @tparam T + * @param paths + * @return + */ + template QList pathsToObject(const QList& paths) + { + QList res; + res.reserve(paths.size()); + for (const auto& path : paths) { + auto object = pathToObject(path); + if (object) { + res.append(object); + } + } + return res; + } + + /** + * Encode the string value to a DBus object path safe representation, + * using a schema similar to URI encoding, but with percentage(%) replaced with + * underscore(_). All characters except [A-Za-z0-9] are encoded. For non-ascii + * characters, UTF-8 encoding is first applied and each of the resulting byte + * value is encoded. + * @param value + * @return encoded string + */ + QString encodePath(const QString& value); + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_DBUSOBJECT_H diff --git a/src/fdosecrets/objects/DBusReturn.cpp b/src/fdosecrets/objects/DBusReturn.cpp new file mode 100644 index 0000000000..ffd10add91 --- /dev/null +++ b/src/fdosecrets/objects/DBusReturn.cpp @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusReturn.h" diff --git a/src/fdosecrets/objects/DBusReturn.h b/src/fdosecrets/objects/DBusReturn.h new file mode 100644 index 0000000000..6c94eab183 --- /dev/null +++ b/src/fdosecrets/objects/DBusReturn.h @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSRETURN_H +#define KEEPASSXC_FDOSECRETS_DBUSRETURN_H + +#include +#include +#include + +#include + +namespace FdoSecrets +{ + + namespace details + { + class DBusReturnImpl + { + public: + /** + * Check if this object contains an error + * @return true if it contains an error, false otherwise. + */ + bool isError() const + { + return !m_errorName.isEmpty(); + } + + /** + * Get the error name + * @return + */ + QString errorName() const + { + return m_errorName; + } + + void okOrDie() const + { + Q_ASSERT(!isError()); + } + + protected: + struct WithErrorTag + { + }; + + /** + * Construct from an error + * @param errorName + * @param value + */ + DBusReturnImpl(QString errorName, WithErrorTag) + : m_errorName(std::move(errorName)) + { + } + + DBusReturnImpl() = default; + + protected: + QString m_errorName; + }; + } // namespace details + + /** + * Either a return value or a DBus error + * @tparam T + */ + template class DBusReturn : public details::DBusReturnImpl + { + protected: + using DBusReturnImpl::DBusReturnImpl; + + public: + using value_type = T; + + DBusReturn() = default; + + /** + * Implicitly construct from a value + * @param value + */ + DBusReturn(T&& value) // NOLINT(google-explicit-constructor) + : m_value(std::move(value)) + { + } + + DBusReturn(const T& value) // NOLINT(google-explicit-constructor) + : m_value(std::move(value)) + { + } + + /** + * Implicitly convert from another error of different value type. + * + * @tparam U must not be the same as T + * @param other + */ + template ::value>::type> + DBusReturn(const DBusReturn& other) // NOLINT(google-explicit-constructor) + : DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{}) + { + Q_ASSERT(other.isError()); + } + + /** + * Construct from error + * @param errorType + * @return a DBusReturn object containing the error + */ + static DBusReturn Error(QDBusError::ErrorType errorType) + { + return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}}; + } + + /** + * Overloaded version + * @param errorName + * @return a DBusReturnImpl object containing the error + */ + static DBusReturn Error(QString errorName) + { + return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}}; + } + + /** + * Get a reference to the enclosed value + * @return + */ + const T& value() const& + { + okOrDie(); + return m_value; + } + + /** + * Get a rvalue reference to the enclosed value if this object is rvalue + * @return a rvalue reference to the enclosed value + */ + T value() && + { + okOrDie(); + return std::move(m_value); + } + + template T valueOrHandle(P* p) const& + { + if (isError()) { + if (p->calledFromDBus()) { + p->sendErrorReply(errorName()); + } + return {}; + } + return m_value; + } + + template T&& valueOrHandle(P* p) && + { + if (isError()) { + if (p->calledFromDBus()) { + p->sendErrorReply(errorName()); + } + } + return std::move(m_value); + } + + private: + T m_value{}; + }; + + template <> class DBusReturn : public details::DBusReturnImpl + { + protected: + using DBusReturnImpl::DBusReturnImpl; + + public: + using value_type = void; + + DBusReturn() = default; + + /** + * Implicitly convert from another error of different value type. + * + * @tparam U must not be the same as T + * @param other + */ + template ::value>::type> + DBusReturn(const DBusReturn& other) // NOLINT(google-explicit-constructor) + : DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{}) + { + Q_ASSERT(other.isError()); + } + + /** + * Construct from error + * @param errorType + * @return a DBusReturn object containing the error + */ + static DBusReturn Error(QDBusError::ErrorType errorType) + { + return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}}; + } + + /** + * Overloaded version + * @param errorName + * @return a DBusReturnImpl object containing the error + */ + static DBusReturn Error(QString errorName) + { + return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}}; + } + + /** + * If this is return contains an error, handle it if we were called from DBus + * @tparam P + * @param p + */ + template void handle(P* p) const + { + if (isError()) { + if (p->calledFromDBus()) { + p->sendErrorReply(errorName()); + } + } + } + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_DBUSRETURN_H diff --git a/src/fdosecrets/objects/DBusTypes.cpp b/src/fdosecrets/objects/DBusTypes.cpp new file mode 100644 index 0000000000..fff25124e6 --- /dev/null +++ b/src/fdosecrets/objects/DBusTypes.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019 Aetf + * Copyright 2010, Michael Leupold + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DBusTypes.h" + +#include + +namespace FdoSecrets +{ + + void registerDBusTypes() + { + // register meta-types needed for this adaptor + qRegisterMetaType(); + qDBusRegisterMetaType(); + + qRegisterMetaType(); + qDBusRegisterMetaType(); + + qRegisterMetaType(); + qDBusRegisterMetaType(); + + // NOTE: this is already registered by Qt in qtextratypes.h + // qRegisterMetaType >(); + // qDBusRegisterMetaType >(); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/DBusTypes.h b/src/fdosecrets/objects/DBusTypes.h new file mode 100644 index 0000000000..384a1e6b20 --- /dev/null +++ b/src/fdosecrets/objects/DBusTypes.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2019 Aetf + * Copyright 2010, Michael Leupold + * Copyright 2010-2011, Valentin Rusu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSTYPES_H +#define KEEPASSXC_FDOSECRETS_DBUSTYPES_H + +#include +#include +#include +#include + +#define DBUS_SERVICE_SECRET "org.freedesktop.secrets" + +#define DBUS_INTERFACE_SECRET_SERVICE "org.freedesktop.Secret.Service" +#define DBUS_INTERFACE_SECRET_SESSION "org.freedesktop.Secret.Session" +#define DBUS_INTERFACE_SECRET_COLLECTION "org.freedesktop.Secret.Collection" +#define DBUS_INTERFACE_SECRET_ITEM "org.freedesktop.Secret.Item" +#define DBUS_INTERFACE_SECRET_PROMPT "org.freedesktop.Secret.Prompt" + +#define DBUS_ERROR_SECRET_NO_SESSION "org.freedesktop.Secret.Error.NoSession" +#define DBUS_ERROR_SECRET_NO_SUCH_OBJECT "org.freedesktop.Secret.Error.NoSuchObject" +#define DBUS_ERROR_SECRET_IS_LOCKED "org.freedesktop.Secret.Error.IsLocked" + +#define DBUS_PATH_SECRETS "/org/freedesktop/secrets" + +#define DBUS_PATH_TEMPLATE_ALIAS "%1/aliases/%2" +#define DBUS_PATH_TEMPLATE_SESSION "%1/session/%2" +#define DBUS_PATH_TEMPLATE_COLLECTION "%1/collection/%2" +#define DBUS_PATH_TEMPLATE_ITEM "%1/%2" + +namespace FdoSecrets +{ + /** + * This is the basic Secret structure exchanged via the dbus API + * See the spec for more details + */ + struct SecretStruct + { + QDBusObjectPath session{}; + QByteArray parameters{}; + QByteArray value{}; + QString contentType{}; + }; + + inline QDBusArgument& operator<<(QDBusArgument& argument, const SecretStruct& secret) + { + argument.beginStructure(); + argument << secret.session << secret.parameters << secret.value << secret.contentType; + argument.endStructure(); + return argument; + } + + inline const QDBusArgument& operator>>(const QDBusArgument& argument, SecretStruct& secret) + { + argument.beginStructure(); + argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType; + argument.endStructure(); + return argument; + } + + /** + * Register the types needed for the fd.o Secrets D-Bus interface. + */ + void registerDBusTypes(); + +} // namespace FdoSecrets + +typedef QMap StringStringMap; +typedef QMap ObjectPathSecretMap; + +Q_DECLARE_METATYPE(FdoSecrets::SecretStruct) +Q_DECLARE_METATYPE(StringStringMap); +Q_DECLARE_METATYPE(ObjectPathSecretMap); + +#endif // KEEPASSXC_FDOSECRETS_DBUSTYPES_H diff --git a/src/fdosecrets/objects/Item.cpp b/src/fdosecrets/objects/Item.cpp new file mode 100644 index 0000000000..fa37564a26 --- /dev/null +++ b/src/fdosecrets/objects/Item.cpp @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Item.h" + +#include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" + +#include "core/Entry.h" +#include "core/EntryAttributes.h" +#include "core/Group.h" +#include "core/Tools.h" + +#include +#include +#include +#include + +namespace FdoSecrets +{ + + const QSet Item::ReadOnlyAttributes(QSet() << ItemAttributes::UuidKey << ItemAttributes::PathKey); + + static void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType); + static SecretStruct getEntrySecret(Entry* entry); + + namespace + { + constexpr auto FDO_SECRETS_DATA = "FDO_SECRETS_DATA"; + constexpr auto FDO_SECRETS_CONTENT_TYPE = "FDO_SECRETS_CONTENT_TYPE"; + } // namespace + + Item::Item(Collection* parent, Entry* backend) + : DBusObject(parent) + , m_backend(backend) + { + Q_ASSERT(!p()->objectPath().path().isEmpty()); + + registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_ITEM).arg(p()->objectPath().path(), m_backend->uuidToHex()), + new ItemAdaptor(this)); + + connect(m_backend.data(), &Entry::entryModified, this, &Item::itemChanged); + } + + DBusReturn Item::locked() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + return collection()->locked(); + } + + DBusReturn Item::attributes() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + StringStringMap attrs; + + // add default attributes except password + auto entryAttrs = m_backend->attributes(); + for (const auto& attr : EntryAttributes::DefaultAttributes) { + if (entryAttrs->isProtected(attr) || attr == EntryAttributes::PasswordKey) { + continue; + } + + auto value = entryAttrs->value(attr); + if (entryAttrs->isReference(attr)) { + value = m_backend->maskPasswordPlaceholders(value); + value = m_backend->resolveMultiplePlaceholders(value); + } + attrs[attr] = value; + } + + // add custom attributes + const auto customKeys = entryAttrs->customKeys(); + for (const auto& attr : customKeys) { + // decode attr key + auto decoded = decodeAttributeKey(attr); + + attrs[decoded] = entryAttrs->value(attr); + } + + // add some informative and readonly attributes + attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex(); + attrs[ItemAttributes::PathKey] = path(); + return attrs; + } + + DBusReturn Item::setAttributes(const StringStringMap& attrs) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + m_backend->beginUpdate(); + + auto entryAttrs = m_backend->attributes(); + for (auto it = attrs.constBegin(); it != attrs.constEnd(); ++it) { + if (entryAttrs->isProtected(it.key()) || it.key() == EntryAttributes::PasswordKey) { + continue; + } + + if (ReadOnlyAttributes.contains(it.key())) { + continue; + } + + auto encoded = encodeAttributeKey(it.key()); + entryAttrs->set(encoded, it.value()); + } + + m_backend->endUpdate(); + + return {}; + } + + DBusReturn Item::label() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + return m_backend->title(); + } + + DBusReturn Item::setLabel(const QString& label) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + m_backend->beginUpdate(); + m_backend->setTitle(label); + m_backend->endUpdate(); + + return {}; + } + + DBusReturn Item::created() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + return static_cast(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + } + + DBusReturn Item::modified() const + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + return static_cast(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + } + + DBusReturn Item::deleteItem() + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + auto prompt = new DeleteItemPrompt(service(), this); + return prompt; + } + + DBusReturn Item::getSecret(Session* session) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + if (!session) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + } + + auto secret = getEntrySecret(m_backend); + + // encode using session + secret = session->encode(secret); + + // show notification is this was directly called from DBus + if (calledFromDBus()) { + service()->plugin()->emitRequestShowNotification( + tr(R"(Entry "%1" from database "%2" was used by %3)") + .arg(m_backend->title(), collection()->name(), callingPeerName())); + } + return secret; + } + + DBusReturn Item::setSecret(const SecretStruct& secret) + { + auto ret = ensureBackend(); + if (ret.isError()) { + return ret; + } + ret = ensureUnlocked(); + if (ret.isError()) { + return ret; + } + + auto session = pathToObject(secret.session); + if (!session) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + } + + // decode using session + auto decoded = session->decode(secret); + + // set in backend + m_backend->beginUpdate(); + setEntrySecret(m_backend, decoded.value, decoded.contentType); + m_backend->endUpdate(); + + return {}; + } + + DBusReturn Item::setProperties(const QVariantMap& properties) + { + auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Label")).toString(); + + auto ret = setLabel(label); + if (ret.isError()) { + return ret; + } + + auto attributes = qdbus_cast( + properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value()); + ret = setAttributes(attributes); + if (ret.isError()) { + return ret; + } + + return {}; + } + + Collection* Item::collection() const + { + return qobject_cast(p()); + } + + DBusReturn Item::ensureBackend() const + { + if (!m_backend) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + } + return {}; + } + + DBusReturn Item::ensureUnlocked() const + { + auto locked = collection()->locked(); + if (locked.isError()) { + return locked; + } + if (locked.value()) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED)); + } + return {}; + } + + Entry* Item::backend() const + { + return m_backend; + } + + void Item::doDelete() + { + emit itemAboutToDelete(); + + // Unregister current path early, do not rely on deleteLater's call to destructor + // as in case of Entry moving between groups, new Item will be created at the same DBus path + // before the current Item is deleted in the event loop. + unregisterCurrentPath(); + + m_backend = nullptr; + deleteLater(); + } + + Service* Item::service() const + { + return collection()->service(); + } + + QString Item::path() const + { + QStringList pathComponents{m_backend->title()}; + + Group* group = m_backend->group(); + while (group && group != collection()->exposedRootGroup()) { + pathComponents.prepend(group->name()); + group = group->parentGroup(); + } + // we should always reach the exposed root group + Q_ASSERT(group); + + // root group is represented by a single slash, thus adding an empty component. + pathComponents.prepend(QLatin1Literal("")); + + return pathComponents.join('/'); + } + + QString Item::encodeAttributeKey(const QString &key) + { + return QUrl::toPercentEncoding(key, "", "_:").replace('%', '_'); + } + + QString Item::decodeAttributeKey(const QString &key) + { + return QString::fromUtf8(QByteArray::fromPercentEncoding(key.toLatin1(), '_')); + } + + void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType) + { + auto mimeName = contentType.split(';').takeFirst().trimmed(); + + // find the mime type + QMimeDatabase db; + auto mimeType = db.mimeTypeForName(mimeName); + + // find a suitable codec + QTextCodec* codec = nullptr; + static const QRegularExpression charsetPattern(QStringLiteral(R"re(charset=(?.+)$)re")); + auto match = charsetPattern.match(contentType); + if (match.hasMatch()) { + codec = QTextCodec::codecForName(match.captured(QStringLiteral("encode")).toLatin1()); + } else { + codec = QTextCodec::codecForName(QByteArrayLiteral("utf-8")); + } + + if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) { + // we can't handle this content type, save the data as attachment + entry->attachments()->set(FDO_SECRETS_DATA, data); + entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType); + return; + } + + // save the data to password field + if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) { + entry->attachments()->remove(FDO_SECRETS_DATA); + } + if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) { + entry->attributes()->remove(FDO_SECRETS_CONTENT_TYPE); + } + + Q_ASSERT(codec); + entry->setPassword(codec->toUnicode(data)); + } + + SecretStruct getEntrySecret(Entry* entry) + { + SecretStruct ss; + + if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) { + ss.value = entry->attachments()->value(FDO_SECRETS_DATA); + Q_ASSERT(entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)); + ss.contentType = entry->attributes()->value(FDO_SECRETS_CONTENT_TYPE); + return ss; + } + + ss.value = entry->resolveMultiplePlaceholders(entry->password()).toUtf8(); + ss.contentType = QStringLiteral("text/plain"); + return ss; + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Item.h b/src/fdosecrets/objects/Item.h new file mode 100644 index 0000000000..cfec77fe58 --- /dev/null +++ b/src/fdosecrets/objects/Item.h @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_ITEM_H +#define KEEPASSXC_FDOSECRETS_ITEM_H + +#include "fdosecrets/objects/DBusObject.h" +#include "fdosecrets/objects/adaptors/ItemAdaptor.h" + +#include + +class Entry; + +namespace FdoSecrets +{ + + namespace ItemAttributes + { + constexpr const auto UuidKey = "Uuid"; + constexpr const auto PathKey = "Path"; + } // namespace ItemAttributes + + class Session; + class Collection; + class PromptBase; + + class Item : public DBusObject + { + Q_OBJECT + public: + explicit Item(Collection* parent, Entry* backend); + + DBusReturn locked() const; + + DBusReturn attributes() const; + DBusReturn setAttributes(const StringStringMap& attrs); + + DBusReturn label() const; + DBusReturn setLabel(const QString& label); + + DBusReturn created() const; + + DBusReturn modified() const; + + DBusReturn deleteItem(); + DBusReturn getSecret(Session* session); + DBusReturn setSecret(const SecretStruct& secret); + + signals: + void itemChanged(); + void itemAboutToDelete(); + + public: + static const QSet ReadOnlyAttributes; + + /** + * Due to the limitation in EntrySearcher, custom attr key cannot contain ':', + * Thus we encode the key when saving and decode it when returning. + * @param key + * @return + */ + static QString encodeAttributeKey(const QString& key); + static QString decodeAttributeKey(const QString& key); + + DBusReturn setProperties(const QVariantMap& properties); + + Entry* backend() const; + Collection* collection() const; + Service* service() const; + + /** + * Compute the entry path relative to the exposed group + * @return the entry path + */ + QString path() const; + + public slots: + void doDelete(); + + private: + /** + * Check if the backend is a valid object, send error reply if not. + * @return No error if the backend is valid. + */ + DBusReturn ensureBackend() const; + + /** + * Ensure the database is unlocked, send error reply if locked. + * @return true if the database is locked + */ + DBusReturn ensureUnlocked() const; + + private: + QPointer m_backend; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_ITEM_H diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp new file mode 100644 index 0000000000..14bc66b687 --- /dev/null +++ b/src/fdosecrets/objects/Prompt.cpp @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Prompt.h" + +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Service.h" + +#include "core/Tools.h" +#include "gui/DatabaseWidget.h" +#include "gui/MessageBox.h" + +#include +#include + +namespace FdoSecrets +{ + + PromptBase::PromptBase(Service* parent) + : DBusObject(parent) + { + registerWithPath( + QStringLiteral("%1/prompt/%2").arg(p()->objectPath().path(), Tools::uuidToHex(QUuid::createUuid())), + new PromptAdaptor(this)); + + connect(this, &PromptBase::completed, this, &PromptBase::deleteLater); + } + + QWindow* PromptBase::findWindow(const QString& windowId) + { + // find parent window, or nullptr if not found + bool ok = false; + WId wid = windowId.toULongLong(&ok, 0); + QWindow* parent = nullptr; + if (ok) { + parent = QWindow::fromWinId(wid); + } + if (parent) { + // parent is not the child of any object, so make sure it gets deleted at some point + QObject::connect(this, &QObject::destroyed, parent, &QObject::deleteLater); + } + return parent; + } + + Service* PromptBase::service() const + { + return qobject_cast(parent()); + } + + DBusReturn PromptBase::dismiss() + { + emit completed(true, {}); + return {}; + } + + DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll) + : PromptBase(parent) + , m_collection(coll) + { + } + + DBusReturn DeleteCollectionPrompt::prompt(const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusReturn ret; + QMetaObject::invokeMethod(this, + "prompt", + Qt::BlockingQueuedConnection, + Q_ARG(QString, windowId), + Q_RETURN_ARG(DBusReturn, ret)); + return ret; + } + + if (!m_collection) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + } + + MessageBox::OverrideParent override(findWindow(windowId)); + // only need to delete in backend, collection will react itself. + service()->doCloseDatabase(m_collection->backend()); + + emit completed(false, {}); + + return {}; + } + + CreateCollectionPrompt::CreateCollectionPrompt(Service* parent) + : PromptBase(parent) + { + } + + DBusReturn CreateCollectionPrompt::prompt(const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusReturn ret; + QMetaObject::invokeMethod(this, + "prompt", + Qt::BlockingQueuedConnection, + Q_ARG(QString, windowId), + Q_RETURN_ARG(DBusReturn, ret)); + return ret; + } + + MessageBox::OverrideParent override(findWindow(windowId)); + + auto coll = service()->doNewDatabase(); + if (!coll) { + return dismiss(); + } + + emit collectionCreated(coll); + emit completed(false, coll->objectPath().path()); + + return {}; + } + + LockCollectionsPrompt::LockCollectionsPrompt(Service* parent, const QList& colls) + : PromptBase(parent) + { + m_collections.reserve(colls.size()); + for (const auto& c : asConst(colls)) { + m_collections << c; + } + } + + DBusReturn LockCollectionsPrompt::prompt(const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusReturn ret; + QMetaObject::invokeMethod(this, + "prompt", + Qt::BlockingQueuedConnection, + Q_ARG(QString, windowId), + Q_RETURN_ARG(DBusReturn, ret)); + return ret; + } + + MessageBox::OverrideParent override(findWindow(windowId)); + + QList locked; + for (const auto& c : asConst(m_collections)) { + if (c) { + c->doLock(); + locked << c->objectPath(); + } + } + + emit completed(false, QVariant::fromValue(locked)); + + return {}; + } + + UnlockCollectionsPrompt::UnlockCollectionsPrompt(Service* parent, const QList& colls) + : PromptBase(parent) + { + m_collections.reserve(colls.size()); + for (const auto& c : asConst(colls)) { + m_collections << c; + } + } + + DBusReturn UnlockCollectionsPrompt::prompt(const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusReturn ret; + QMetaObject::invokeMethod(this, + "prompt", + Qt::BlockingQueuedConnection, + Q_ARG(QString, windowId), + Q_RETURN_ARG(DBusReturn, ret)); + return ret; + } + + MessageBox::OverrideParent override(findWindow(windowId)); + + QList unlocked; + for (const auto& c : asConst(m_collections)) { + if (c) { + c->doUnlock(); + unlocked << c->objectPath(); + } + } + + emit completed(false, QVariant::fromValue(unlocked)); + + return {}; + } + + DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item) + : PromptBase(parent) + , m_item(item) + { + } + + DBusReturn DeleteItemPrompt::prompt(const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusReturn ret; + QMetaObject::invokeMethod(this, + "prompt", + Qt::BlockingQueuedConnection, + Q_ARG(QString, windowId), + Q_RETURN_ARG(DBusReturn, ret)); + return ret; + } + + MessageBox::OverrideParent override(findWindow(windowId)); + + // delete item's backend. Item will be notified after the backend is deleted. + if (m_item) { + if (FdoSecrets::settings()->noConfirmDeleteItem()) { + MessageBox::setNextAnswer(MessageBox::Move); + } + m_item->collection()->doDeleteEntries({m_item->backend()}); + } + + emit completed(false, {}); + + return {}; + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Prompt.h b/src/fdosecrets/objects/Prompt.h new file mode 100644 index 0000000000..2d13084f6e --- /dev/null +++ b/src/fdosecrets/objects/Prompt.h @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_PROMPT_H +#define KEEPASSXC_FDOSECRETS_PROMPT_H + +#include "fdosecrets/objects/DBusObject.h" +#include "fdosecrets/objects/adaptors/PromptAdaptor.h" + +#include + +class QWindow; + +class DatabaseWidget; + +namespace FdoSecrets +{ + + class Service; + + class PromptBase : public DBusObject + { + Q_OBJECT + public: + explicit PromptBase(Service* parent); + + virtual DBusReturn prompt(const QString& windowId) = 0; + + virtual DBusReturn dismiss(); + + signals: + void completed(bool dismissed, const QVariant& result); + + protected: + QWindow* findWindow(const QString& windowId); + Service* service() const; + }; + + class Collection; + + class DeleteCollectionPrompt : public PromptBase + { + Q_OBJECT + + public: + explicit DeleteCollectionPrompt(Service* parent, Collection* coll); + + DBusReturn prompt(const QString& windowId) override; + + private: + QPointer m_collection; + }; + + class CreateCollectionPrompt : public PromptBase + { + Q_OBJECT + + public: + explicit CreateCollectionPrompt(Service* parent); + + DBusReturn prompt(const QString& windowId) override; + + signals: + void collectionCreated(Collection* coll); + }; + + class LockCollectionsPrompt : public PromptBase + { + Q_OBJECT + public: + explicit LockCollectionsPrompt(Service* parent, const QList& colls); + + DBusReturn prompt(const QString& windowId) override; + + private: + QList> m_collections; + }; + + class UnlockCollectionsPrompt : public PromptBase + { + Q_OBJECT + public: + explicit UnlockCollectionsPrompt(Service* parent, const QList& coll); + + DBusReturn prompt(const QString& windowId) override; + + private: + QList> m_collections; + }; + + class Item; + class DeleteItemPrompt : public PromptBase + { + Q_OBJECT + + public: + explicit DeleteItemPrompt(Service* parent, Item* item); + + DBusReturn prompt(const QString& windowId) override; + + private: + QPointer m_item; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_PROMPT_H diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp new file mode 100644 index 0000000000..c15d4052c9 --- /dev/null +++ b/src/fdosecrets/objects/Service.cpp @@ -0,0 +1,477 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Service.h" + +#include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Session.h" + +#include "gui/DatabaseTabWidget.h" +#include "gui/DatabaseWidget.h" + +#include +#include +#include + +namespace +{ + constexpr auto DEFAULT_ALIAS = "default"; +} + +namespace FdoSecrets +{ + + Service::Service(FdoSecretsPlugin* plugin, + QPointer dbTabs) // clazy: exclude=ctor-missing-parent-argument + : DBusObject(nullptr) + , m_plugin(plugin) + , m_databases(std::move(dbTabs)) + , m_insdieEnsureDefaultAlias(false) + , m_serviceWatcher(nullptr) + { + registerWithPath(QStringLiteral(DBUS_PATH_SECRETS), new ServiceAdaptor(this)); + } + + Service::~Service() + { + QDBusConnection::sessionBus().unregisterService(QStringLiteral(DBUS_SERVICE_SECRET)); + } + + bool Service::initialize() + { + if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) { + qDebug() << "Another secret service is running"; + emit error(tr("Failed to register DBus service at %1: another secret service is running.") + .arg(QLatin1Literal(DBUS_SERVICE_SECRET))); + return false; + } + + // Connect to service unregistered signal + m_serviceWatcher.reset(new QDBusServiceWatcher()); + connect( + m_serviceWatcher.data(), &QDBusServiceWatcher::serviceUnregistered, this, &Service::dbusServiceUnregistered); + + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + + // Add existing database tabs + for (int idx = 0; idx != m_databases->count(); ++idx) { + auto dbWidget = m_databases->databaseWidgetFromIndex(idx); + onDatabaseTabOpened(dbWidget, false); + } + + // Connect to new database signal + // No need to connect to close signal, as collection will remove itself when backend delete/close database tab. + connect(m_databases.data(), &DatabaseTabWidget::databaseOpened, this, [this](DatabaseWidget* dbWidget) { + onDatabaseTabOpened(dbWidget, true); + }); + + // make default alias track current activated database + connect(m_databases.data(), &DatabaseTabWidget::activateDatabaseChanged, this, &Service::ensureDefaultAlias); + + return true; + } + + void Service::onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal) + { + auto coll = new Collection(this, dbWidget); + + m_collections << coll; + m_dbToCollection[dbWidget] = coll; + + // handle alias + connect(coll, &Collection::aliasAboutToAdd, this, &Service::onCollectionAliasAboutToAdd); + connect(coll, &Collection::aliasAdded, this, &Service::onCollectionAliasAdded); + connect(coll, &Collection::aliasRemoved, this, &Service::onCollectionAliasRemoved); + + ensureDefaultAlias(); + + // Forward delete signal, we have to rely on filepath to identify the database being closed, + // but we can not access m_backend safely because during the databaseClosed signal, + // m_backend may already be reset to nullptr + // We want to remove the collection object from dbus as early as possible, to avoid + // race conditions when deleteLater was called on the m_backend, but not delivered yet, + // and new method calls from dbus occurred. Therefore we can't rely on the destroyed + // signal on m_backend. + // bind to coll lifespan + connect(m_databases.data(), &DatabaseTabWidget::databaseClosed, coll, [coll](const QString& filePath) { + if (filePath == coll->backendFilePath()) { + coll->doDelete(); + } + }); + + // relay signals + connect(coll, &Collection::collectionChanged, this, [this, coll]() { emit collectionChanged(coll); }); + connect(coll, &Collection::collectionAboutToDelete, this, [this, coll]() { + m_collections.removeAll(coll); + m_dbToCollection.remove(coll->backend()); + emit collectionDeleted(coll); + }); + + // a special case: the database changed from no expose to expose something. + // in this case, there is no collection out there monitoring it, so create a new collection + if (!dbWidget->isLocked()) { + monitorDatabaseExposedGroup(dbWidget); + } + connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [this, dbWidget]() { + monitorDatabaseExposedGroup(dbWidget); + }); + + if (emitSignal) { + emit collectionCreated(coll); + } + } + + void Service::monitorDatabaseExposedGroup(DatabaseWidget* dbWidget) + { + Q_ASSERT(dbWidget); + connect( + dbWidget->database()->metadata()->customData(), &CustomData::customDataModified, this, [this, dbWidget]() { + if (!FdoSecrets::settings()->exposedGroup(dbWidget->database()).isNull() && !findCollection(dbWidget)) { + onDatabaseTabOpened(dbWidget, true); + } + }); + } + + void Service::ensureDefaultAlias() + { + if (m_insdieEnsureDefaultAlias) { + return; + } + + m_insdieEnsureDefaultAlias = true; + + auto coll = findCollection(m_databases->currentDatabaseWidget()); + if (coll) { + // adding alias will automatically remove the association with previous collection. + coll->addAlias(DEFAULT_ALIAS).okOrDie(); + } + + m_insdieEnsureDefaultAlias = false; + } + + void Service::dbusServiceUnregistered(const QString& service) + { + Q_ASSERT(m_serviceWatcher); + + auto removed = m_serviceWatcher->removeWatchedService(service); + Q_UNUSED(removed); + Q_ASSERT(removed); + + Session::CleanupNegotiation(service); + auto sess = m_peerToSession.value(service, nullptr); + if (sess) { + sess->close().okOrDie(); + } + } + + DBusReturn> Service::collections() const + { + return m_collections; + } + + DBusReturn Service::openSession(const QString& algorithm, const QVariant& input, Session*& result) + { + QVariant output; + bool incomplete = false; + auto peer = callingPeer(); + + // watch for service unregister to cleanup + Q_ASSERT(m_serviceWatcher); + m_serviceWatcher->addWatchedService(peer); + + // negotiate cipher + auto ciphers = Session::CreateCiphers(peer, algorithm, input, output, incomplete); + if (incomplete) { + result = nullptr; + return output; + } + if (!ciphers) { + return DBusReturn<>::Error(QDBusError::NotSupported); + } + result = new Session(std::move(ciphers), callingPeerName(), this); + + m_sessions.append(result); + m_peerToSession[peer] = result; + connect(result, &Session::aboutToClose, this, [this, peer, result]() { + emit sessionClosed(result); + m_sessions.removeAll(result); + m_peerToSession.remove(peer); + }); + emit sessionOpened(result); + + return output; + } + + DBusReturn + Service::createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt) + { + prompt = nullptr; + + // return existing collection if alias is non-empty and exists. + auto collection = findCollection(alias); + if (!collection) { + auto cp = new CreateCollectionPrompt(this); + prompt = cp; + + // collection will be created when the prompt complets. + // once it's done, we set additional properties on the collection + connect(cp, &CreateCollectionPrompt::collectionCreated, cp, [alias, properties](Collection* coll) { + coll->setProperties(properties).okOrDie(); + if (!alias.isEmpty()) { + coll->addAlias(alias).okOrDie(); + } + }); + } + return collection; + } + + DBusReturn> Service::searchItems(const StringStringMap& attributes, QList& locked) + { + auto ret = collections(); + if (ret.isError()) { + return ret; + } + + QList unlocked; + for (const auto& coll : ret.value()) { + auto items = coll->searchItems(attributes); + if (items.isError()) { + return items; + } + auto l = coll->locked(); + if (l.isError()) { + return l; + } + if (l.value()) { + locked.append(items.value()); + } else { + unlocked.append(items.value()); + } + } + return unlocked; + } + + DBusReturn> Service::unlock(const QList& objects, PromptBase*& prompt) + { + QSet needUnlock; + needUnlock.reserve(objects.size()); + for (const auto& obj : asConst(objects)) { + auto coll = qobject_cast(obj); + if (coll) { + needUnlock << coll; + } else { + auto item = qobject_cast(obj); + if (!item) { + continue; + } + // we lock the whole collection for item + needUnlock << item->collection(); + } + } + + // return anything already unlocked + QList unlocked; + QList toUnlock; + for (const auto& coll : asConst(needUnlock)) { + auto l = coll->locked(); + if (l.isError()) { + return l; + } + if (!l.value()) { + unlocked << coll; + } else { + toUnlock << coll; + } + } + prompt = new UnlockCollectionsPrompt(this, toUnlock); + return unlocked; + } + + DBusReturn> Service::lock(const QList& objects, PromptBase*& prompt) + { + QSet needLock; + needLock.reserve(objects.size()); + for (const auto& obj : asConst(objects)) { + auto coll = qobject_cast(obj); + if (coll) { + needLock << coll; + } else { + auto item = qobject_cast(obj); + if (!item) { + continue; + } + // we lock the whole collection for item + needLock << item->collection(); + } + } + + // return anything already locked + QList locked; + QList toLock; + for (const auto& coll : asConst(needLock)) { + auto l = coll->locked(); + if (l.isError()) { + return l; + } + if (l.value()) { + locked << coll; + } else { + toLock << coll; + } + } + prompt = new LockCollectionsPrompt(this, toLock); + return locked; + } + + DBusReturn> Service::getSecrets(const QList& items, Session* session) + { + if (!session) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + } + + QHash res; + + for (const auto& item : asConst(items)) { + auto ret = item->getSecret(session); + if (ret.isError()) { + return ret; + } + res[item] = std::move(ret).value(); + } + if (calledFromDBus()) { + plugin()->emitRequestShowNotification( + tr(R"(%n Entry(s) was used by %1)", "%1 is the name of an application", res.size()) + .arg(callingPeerName())); + } + return res; + } + + DBusReturn Service::readAlias(const QString& name) + { + return findCollection(name); + } + + DBusReturn Service::setAlias(const QString& name, Collection* collection) + { + if (!collection) { + // remove alias name from its collection + collection = findCollection(name); + if (!collection) { + return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + } + return collection->removeAlias(name); + } + return collection->addAlias(name); + } + + Collection* Service::findCollection(const QString& alias) const + { + if (alias.isEmpty()) { + return nullptr; + } + + auto it = m_aliases.find(alias); + if (it != m_aliases.end()) { + return it.value(); + } + return nullptr; + } + + void Service::onCollectionAliasAboutToAdd(const QString& alias) + { + auto coll = qobject_cast(sender()); + + auto it = m_aliases.constFind(alias); + if (it != m_aliases.constEnd() && it.value() != coll) { + // another collection holds the alias + // remove it first + it.value()->removeAlias(alias).okOrDie(); + + // onCollectionAliasRemoved called through signal + // `it` becomes invalidated now + } + } + + void Service::onCollectionAliasAdded(const QString& alias) + { + auto coll = qobject_cast(sender()); + m_aliases[alias] = coll; + } + + void Service::onCollectionAliasRemoved(const QString& alias) + { + m_aliases.remove(alias); + ensureDefaultAlias(); + } + + Collection* Service::findCollection(const DatabaseWidget* db) const + { + return m_dbToCollection.value(db, nullptr); + } + + const QList Service::sessions() const + { + return m_sessions; + } + + void Service::doCloseDatabase(DatabaseWidget* dbWidget) + { + m_databases->closeDatabaseTab(dbWidget); + } + + Collection* Service::doNewDatabase() + { + auto dbWidget = m_databases->newDatabase(); + if (!dbWidget) { + return nullptr; + } + + // database created through dbus will be exposed to dbus by default + auto db = dbWidget->database(); + FdoSecrets::settings()->setExposedGroup(db, db->rootGroup()->uuid()); + + auto collection = findCollection(dbWidget); + + Q_ASSERT(collection); + + return collection; + } + + void Service::doSwitchToChangeDatabaseSettings(DatabaseWidget* dbWidget) + { + // switch selected to current + // unlock if needed + if (dbWidget->isLocked()) { + m_databases->unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::None); + } + m_databases->setCurrentWidget(dbWidget); + m_databases->changeDatabaseSettings(); + + // open settings (switch from app settings to m_dbTabs) + m_plugin->emitRequestSwitchToDatabases(); + } + + void Service::doUnlockDatabaseInDialog(DatabaseWidget* dbWidget) + { + m_databases->unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::None); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Service.h b/src/fdosecrets/objects/Service.h new file mode 100644 index 0000000000..bfa8ea843a --- /dev/null +++ b/src/fdosecrets/objects/Service.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_SERVICE_H +#define KEEPASSXC_FDOSECRETS_SERVICE_H + +#include "fdosecrets/objects/DBusObject.h" +#include "fdosecrets/objects/adaptors/ServiceAdaptor.h" + +#include +#include +#include +#include +#include + +class QDBusServiceWatcher; + +class DatabaseTabWidget; +class DatabaseWidget; +class Group; + +class FdoSecretsPlugin; + +namespace FdoSecrets +{ + + class Collection; + class Item; + class PromptBase; + class ServiceAdaptor; + class Session; + + class Service : public DBusObject // clazy: exclude=ctor-missing-parent-argument + { + Q_OBJECT + public: + explicit Service(FdoSecretsPlugin* plugin, QPointer dbTabs); + ~Service() override; + + bool initialize(); + + DBusReturn openSession(const QString& algorithm, const QVariant& input, Session*& result); + DBusReturn + createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt); + DBusReturn> searchItems(const StringStringMap& attributes, QList& locked); + + DBusReturn> unlock(const QList& objects, PromptBase*& prompt); + + DBusReturn> lock(const QList& objects, PromptBase*& prompt); + + DBusReturn> getSecrets(const QList& items, Session* session); + + DBusReturn readAlias(const QString& name); + + DBusReturn setAlias(const QString& name, Collection* collection); + + /** + * List of collections + * @return + */ + DBusReturn> collections() const; + + signals: + void collectionCreated(Collection* collection); + void collectionDeleted(Collection* collection); + void collectionChanged(Collection* collection); + + void sessionOpened(Session* sess); + void sessionClosed(Session* sess); + + /** + * Report error message to the GUI + * @param msg + */ + void error(const QString& msg); + + public: + /** + * List of sessions + * @return + */ + const QList sessions() const; + + FdoSecretsPlugin* plugin() const + { + return m_plugin; + } + + public slots: + void doCloseDatabase(DatabaseWidget* dbWidget); + Collection* doNewDatabase(); + void doSwitchToChangeDatabaseSettings(DatabaseWidget* dbWidget); + void doUnlockDatabaseInDialog(DatabaseWidget* dbWidget); + + private slots: + void dbusServiceUnregistered(const QString& service); + void ensureDefaultAlias(); + + void onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal); + void monitorDatabaseExposedGroup(DatabaseWidget* dbWidget); + + void onCollectionAliasAboutToAdd(const QString& alias); + void onCollectionAliasAdded(const QString& alias); + + void onCollectionAliasRemoved(const QString& alias); + + private: + /** + * Find collection by alias name + * @param alias + * @return the collection under alias + */ + Collection* findCollection(const QString& alias) const; + + /** + * Find collection by dbWidget + * @param db + * @return the collection corresponding to the db + */ + Collection* findCollection(const DatabaseWidget* db) const; + + private: + FdoSecretsPlugin* m_plugin; + QPointer m_databases; + + QHash m_aliases; + QList m_collections; + QHash m_dbToCollection; + + QList m_sessions; + QHash m_peerToSession; + + bool m_insdieEnsureDefaultAlias; + + QScopedPointer m_serviceWatcher; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_SERVICE_H diff --git a/src/fdosecrets/objects/Session.cpp b/src/fdosecrets/objects/Session.cpp new file mode 100644 index 0000000000..ad9ac40754 --- /dev/null +++ b/src/fdosecrets/objects/Session.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "Session.h" + +#include "fdosecrets/objects/SessionCipher.h" + +#include "core/Tools.h" + +namespace FdoSecrets +{ + + QHash Session::negoniationState; + + Session::Session(std::unique_ptr&& cipher, const QString& peer, Service* parent) + : DBusObject(parent) + , m_cipher(std::move(cipher)) + , m_peer(peer) + , m_id(QUuid::createUuid()) + { + registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_SESSION).arg(p()->objectPath().path(), id()), + new SessionAdaptor(this)); + } + + void Session::CleanupNegotiation(const QString& peer) + { + negoniationState.remove(peer); + } + + DBusReturn Session::close() + { + emit aboutToClose(); + deleteLater(); + + return {}; + } + + QString Session::peer() const + { + return m_peer; + } + + QString Session::id() const + { + return Tools::uuidToHex(m_id); + } + + std::unique_ptr Session::CreateCiphers(const QString& peer, + const QString& algorithm, + const QVariant& input, + QVariant& output, + bool& incomplete) + { + Q_UNUSED(peer); + incomplete = false; + + std::unique_ptr cipher{}; + if (algorithm == QLatin1Literal("plain")) { + cipher.reset(new PlainCipher); + } else if (algorithm == QLatin1Literal("dh-ietf1024-sha256-aes128-cbc-pkcs7")) { + QByteArray clientPublicKey = input.toByteArray(); + cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(clientPublicKey)); + } else { + // error notSupported + } + + if (!cipher) { + return {}; + } + + if (!cipher->isValid()) { + qWarning() << "FdoSecrets: Error creating cipher"; + return {}; + } + + output = cipher->negotiationOutput(); + return cipher; + } + + SecretStruct Session::encode(const SecretStruct& input) const + { + auto output = m_cipher->encrypt(input); + output.session = objectPath(); + return output; + } + + SecretStruct Session::decode(const SecretStruct& input) const + { + return m_cipher->decrypt(input); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Session.h b/src/fdosecrets/objects/Session.h new file mode 100644 index 0000000000..472cc6e5ae --- /dev/null +++ b/src/fdosecrets/objects/Session.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_SESSION_H +#define KEEPASSXC_FDOSECRETS_SESSION_H + +#include "fdosecrets/objects/DBusObject.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/SessionCipher.h" +#include "fdosecrets/objects/adaptors/SessionAdaptor.h" + +#include +#include +#include +#include + +#include + +namespace FdoSecrets +{ + + class CipherPair; + class Session : public DBusObject + { + Q_OBJECT + public: + static std::unique_ptr CreateCiphers(const QString& peer, + const QString& algorithm, + const QVariant& intpu, + QVariant& output, + bool& incomplete); + static void CleanupNegotiation(const QString& peer); + + explicit Session(std::unique_ptr&& cipher, const QString& peer, Service* parent); + + DBusReturn close(); + + /** + * Encode the secret struct. Note only the value field is encoded. + * @param input + * @return + */ + SecretStruct encode(const SecretStruct& input) const; + + /** + * Decode the secret struct. + * @param input + * @return + */ + SecretStruct decode(const SecretStruct& input) const; + + /** + * The peer application that opened this session + * @return + */ + QString peer() const; + + QString id() const; + + signals: + /** + * The session is going to be closed + * @param sess + */ + void aboutToClose(); + + private: + std::unique_ptr m_cipher; + QString m_peer; + QUuid m_id; + + static QHash negoniationState; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_SESSION_H diff --git a/src/fdosecrets/objects/SessionCipher.cpp b/src/fdosecrets/objects/SessionCipher.cpp new file mode 100644 index 0000000000..374580eed5 --- /dev/null +++ b/src/fdosecrets/objects/SessionCipher.cpp @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SessionCipher.h" + +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "crypto/SymmetricCipher.h" + +#include + +#include + +namespace +{ + constexpr const auto IETF1024_SECOND_OAKLEY_GROUP_P_HEX = "FFFFFFFFFFFFFFFFC90FDAA22168C234" + "C4C6628B80DC1CD129024E088A67CC74" + "020BBEA63B139B22514A08798E3404DD" + "EF9519B3CD3A431B302B0A6DF25F1437" + "4FE1356D6D51C245E485B576625E7EC6" + "F44C42E9A637ED6B0BFF5CB6F406B7ED" + "EE386BFB5A899FA5AE9F24117C4B1FE6" + "49286651ECE65381FFFFFFFFFFFFFFFF"; + constexpr const size_t KEY_SIZE_BYTES = 128; + constexpr const int AES_KEY_LEN = 16; // 128 bits + + const auto IETF1024_SECOND_OAKLEY_GROUP_P = MpiFromHex(IETF1024_SECOND_OAKLEY_GROUP_P_HEX, false); +} // namespace + +namespace FdoSecrets +{ + + QVariant CipherPair::negotiationOutput() const + { + return {}; + } + + DhIetf1024Sha256Aes128CbcPkcs7::DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes) + : m_valid(false) + { + // read client public key + auto clientPub = MpiFromBytes(clientPublicKeyBytes, false); + + // generate server side private, 128 bytes + GcryptMPI serverPrivate(gcry_mpi_snew(KEY_SIZE_BYTES * 8)); + gcry_mpi_randomize(serverPrivate.get(), KEY_SIZE_BYTES * 8, GCRY_STRONG_RANDOM); + + // generate server side public key + GcryptMPI serverPublic(gcry_mpi_snew(KEY_SIZE_BYTES * 8)); + // the generator of Second Oakley Group is 2 + gcry_mpi_powm(serverPublic.get(), GCRYMPI_CONST_TWO, serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get()); + + initialize(std::move(clientPub), std::move(serverPublic), std::move(serverPrivate)); + } + + bool + DhIetf1024Sha256Aes128CbcPkcs7::initialize(GcryptMPI clientPublic, GcryptMPI serverPublic, GcryptMPI serverPrivate) + { + QByteArray commonSecretBytes; + if (!diffieHullman(clientPublic, serverPrivate, commonSecretBytes)) { + return false; + } + + m_privateKey = MpiToBytes(serverPrivate); + m_publicKey = MpiToBytes(serverPublic); + + m_aesKey = hkdf(commonSecretBytes); + + m_valid = true; + return true; + } + + bool DhIetf1024Sha256Aes128CbcPkcs7::diffieHullman(const GcryptMPI& clientPub, + const GcryptMPI& serverPrivate, + QByteArray& commonSecretBytes) + { + if (!clientPub || !serverPrivate) { + return false; + } + + // calc common secret + GcryptMPI commonSecret(gcry_mpi_snew(KEY_SIZE_BYTES * 8)); + gcry_mpi_powm(commonSecret.get(), clientPub.get(), serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get()); + commonSecretBytes = MpiToBytes(commonSecret); + + return true; + } + + QByteArray DhIetf1024Sha256Aes128CbcPkcs7::hkdf(const QByteArray& IKM) + { + // HKDF-Extract(salt, IKM) -> PRK + // PRK = HMAC-Hash(salt, IKM) + + // we use NULL salt as per spec + auto PRK = CryptoHash::hmac(IKM, + QByteArrayLiteral("\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0" + "\0\0\0\0\0\0\0\0"), + CryptoHash::Sha256); + + // HKDF-Expand(PRK, info, L) -> OKM + // N = ceil(L/HashLen) + // T = T(1) | T(2) | T(3) | ... | T(N) + // OKM = first L octets of T + // where: + // T(0) = empty string (zero length) + // T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) + // T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) + // T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) + // ... + // + // (where the constant concatenated to the end of each T(n) is a + // single octet.) + + // we use empty info as per spec + // HashLen = 32 (sha256) + // L = 16 (16 * 8 = 128 bits) + // N = ceil(16/32) = 1 + + auto T1 = CryptoHash::hmac(QByteArrayLiteral("\x01"), PRK, CryptoHash::Sha256); + + // resulting AES key is first 128 bits + Q_ASSERT(T1.size() >= AES_KEY_LEN); + auto OKM = T1.left(AES_KEY_LEN); + return OKM; + } + + SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::encrypt(const SecretStruct& input) + { + SecretStruct output = input; + output.value.clear(); + output.parameters.clear(); + + SymmetricCipher encrypter(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Encrypt); + + auto IV = randomGen()->randomArray(SymmetricCipher::algorithmIvSize(SymmetricCipher::Aes128)); + if (!encrypter.init(m_aesKey, IV)) { + qWarning() << "Error encrypt: " << encrypter.errorString(); + return output; + } + + output.parameters = IV; + + bool ok; + output.value = input.value; + output.value = encrypter.process(padPkcs7(output.value, encrypter.blockSize()), &ok); + if (!ok) { + qWarning() << "Error encrypt: " << encrypter.errorString(); + return output; + } + + return output; + } + + QByteArray& DhIetf1024Sha256Aes128CbcPkcs7::padPkcs7(QByteArray& input, int blockSize) + { + // blockSize must be a power of 2. + Q_ASSERT_X(blockSize > 0 && !(blockSize & (blockSize - 1)), "padPkcs7", "blockSize must be a power of 2"); + + int padLen = blockSize - (input.size() & (blockSize - 1)); + + input.append(QByteArray(padLen, static_cast(padLen))); + return input; + } + + SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::decrypt(const SecretStruct& input) + { + auto IV = input.parameters; + SymmetricCipher decrypter(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); + if (!decrypter.init(m_aesKey, IV)) { + qWarning() << "Error decoding: " << decrypter.errorString(); + return input; + } + bool ok; + SecretStruct output = input; + output.parameters.clear(); + output.value = decrypter.process(input.value, &ok); + + if (!ok) { + qWarning() << "Error decoding: " << decrypter.errorString(); + return input; + } + + unpadPkcs7(output.value); + return output; + } + + QByteArray& DhIetf1024Sha256Aes128CbcPkcs7::unpadPkcs7(QByteArray& input) + { + if (input.isEmpty()) { + return input; + } + + int padLen = input[input.size() - 1]; + input.chop(padLen); + return input; + } + + bool DhIetf1024Sha256Aes128CbcPkcs7::isValid() const + { + return m_valid; + } + + QVariant DhIetf1024Sha256Aes128CbcPkcs7::negotiationOutput() const + { + return m_publicKey; + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/SessionCipher.h b/src/fdosecrets/objects/SessionCipher.h new file mode 100644 index 0000000000..7d1d32d428 --- /dev/null +++ b/src/fdosecrets/objects/SessionCipher.h @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_SESSIONCIPHER_H +#define KEEPASSXC_FDOSECRETS_SESSIONCIPHER_H + +#include "fdosecrets/GcryptMPI.h" +#include "fdosecrets/objects/Session.h" + +class TestFdoSecrets; + +namespace FdoSecrets +{ + + class CipherPair + { + Q_DISABLE_COPY(CipherPair) + public: + CipherPair() = default; + virtual ~CipherPair() = default; + virtual SecretStruct encrypt(const SecretStruct& input) = 0; + virtual SecretStruct decrypt(const SecretStruct& input) = 0; + virtual bool isValid() const = 0; + virtual QVariant negotiationOutput() const; + }; + + class PlainCipher : public CipherPair + { + Q_DISABLE_COPY(PlainCipher) + public: + PlainCipher() = default; + SecretStruct encrypt(const SecretStruct& input) override + { + return input; + } + + SecretStruct decrypt(const SecretStruct& input) override + { + return input; + } + + bool isValid() const override + { + return true; + } + }; + + class DhIetf1024Sha256Aes128CbcPkcs7 : public CipherPair + { + bool m_valid; + QByteArray m_privateKey; + QByteArray m_publicKey; + QByteArray m_aesKey; + + /** + * Diffie Hullman Key Exchange + * Given client public key, generate server private/public key pair and common secret. + * This also sets m_publicKey to server's public key + * @param clientPublicKey client public key + * @param serverPrivate server private key + * @param commonSecretBytes output common secret + * @return true on success. + */ + bool + diffieHullman(const GcryptMPI& clientPublicKey, const GcryptMPI& serverPrivate, QByteArray& commonSecretBytes); + + /** + * Perform HKDF defined in RFC5869, using sha256 as hash function + * @param IKM input keying material + * @return derived 128-bit key suitable for AES + */ + QByteArray hkdf(const QByteArray& IKM); + + /** + * Add PKCS#7 style padding to input inplace + * @param input + * @param blockSize the block size to use, must be 2's power + * @return reference to input for chaining + */ + QByteArray& padPkcs7(QByteArray& input, int blockSize); + + /** + * Remove PKCS#7 style padding from input inplace + * @param input + * @return reference to input for chaining + */ + QByteArray& unpadPkcs7(QByteArray& input); + + bool initialize(GcryptMPI clientPublic, GcryptMPI serverPublic, GcryptMPI serverPrivate); + + DhIetf1024Sha256Aes128CbcPkcs7() + : m_valid(false) + { + } + + public: + explicit DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes); + + SecretStruct encrypt(const SecretStruct& input) override; + + SecretStruct decrypt(const SecretStruct& input) override; + + bool isValid() const override; + + QVariant negotiationOutput() const override; + + private: + Q_DISABLE_COPY(DhIetf1024Sha256Aes128CbcPkcs7); + friend class ::TestFdoSecrets; + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_SESSIONCIPHER_H diff --git a/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp b/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp new file mode 100644 index 0000000000..77eb3b413f --- /dev/null +++ b/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CollectionAdaptor.h" + +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" + +namespace FdoSecrets +{ + + CollectionAdaptor::CollectionAdaptor(Collection* parent) + : DBusAdaptor(parent) + { + connect( + p(), &Collection::itemCreated, this, [this](const Item* item) { emit ItemCreated(objectPathSafe(item)); }); + connect( + p(), &Collection::itemDeleted, this, [this](const Item* item) { emit ItemDeleted(objectPathSafe(item)); }); + connect( + p(), &Collection::itemChanged, this, [this](const Item* item) { emit ItemChanged(objectPathSafe(item)); }); + } + + const QList CollectionAdaptor::items() const + { + return objectsToPath(p()->items().valueOrHandle(p())); + } + + QString CollectionAdaptor::label() const + { + return p()->label().valueOrHandle(p()); + } + + void CollectionAdaptor::setLabel(const QString& label) + { + p()->setLabel(label).handle(p()); + } + + bool CollectionAdaptor::locked() const + { + return p()->locked().valueOrHandle(p()); + } + + qulonglong CollectionAdaptor::created() const + { + return p()->created().valueOrHandle(p()); + } + + qulonglong CollectionAdaptor::modified() const + { + return p()->modified().valueOrHandle(p()); + } + + QDBusObjectPath CollectionAdaptor::Delete() + { + return objectPathSafe(p()->deleteCollection().valueOrHandle(p())); + } + + QList CollectionAdaptor::SearchItems(const StringStringMap& attributes) + { + return objectsToPath(p()->searchItems(attributes).valueOrHandle(p())); + } + + QDBusObjectPath CollectionAdaptor::CreateItem(const QVariantMap& properties, + const SecretStruct& secret, + bool replace, + QDBusObjectPath& prompt) + { + PromptBase* pp = nullptr; + auto item = p()->createItem(properties, secret, replace, pp).valueOrHandle(p()); + prompt = objectPathSafe(pp); + return objectPathSafe(item); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/CollectionAdaptor.h b/src/fdosecrets/objects/adaptors/CollectionAdaptor.h new file mode 100644 index 0000000000..f5220108e1 --- /dev/null +++ b/src/fdosecrets/objects/adaptors/CollectionAdaptor.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H +#define KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H + +#include "fdosecrets/objects/adaptors/DBusAdaptor.h" + +#include + +namespace FdoSecrets +{ + + class Collection; + class CollectionAdaptor : public DBusAdaptor + { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_COLLECTION) + + Q_PROPERTY(QList Items READ items) + Q_PROPERTY(QString Label READ label WRITE setLabel) + Q_PROPERTY(bool Locked READ locked) + Q_PROPERTY(qulonglong Created READ created) + Q_PROPERTY(qulonglong Modified READ modified) + + public: + explicit CollectionAdaptor(Collection* parent); + ~CollectionAdaptor() override = default; + + const QList items() const; + + QString label() const; + void setLabel(const QString& label); + + bool locked() const; + + qulonglong created() const; + + qulonglong modified() const; + + public slots: + QDBusObjectPath Delete(); + QList SearchItems(const StringStringMap& attributes); + QDBusObjectPath CreateItem(const QVariantMap& properties, + const FdoSecrets::SecretStruct& secret, + bool replace, + QDBusObjectPath& prompt); + + signals: + void ItemCreated(const QDBusObjectPath& item); + void ItemDeleted(const QDBusObjectPath& item); + void ItemChanged(const QDBusObjectPath& item); + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_COLLECTIONADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/DBusAdaptor.h b/src/fdosecrets/objects/adaptors/DBusAdaptor.h new file mode 100644 index 0000000000..bd70ab60a3 --- /dev/null +++ b/src/fdosecrets/objects/adaptors/DBusAdaptor.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H +#define KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H + +#include "fdosecrets/objects/DBusReturn.h" +#include "fdosecrets/objects/DBusTypes.h" + +#include + +namespace FdoSecrets +{ + + /** + * @brief A common adapter class + */ + template class DBusAdaptor : public QDBusAbstractAdaptor + { + public: + explicit DBusAdaptor(QObject* parent = nullptr) + : QDBusAbstractAdaptor(parent) + { + } + + ~DBusAdaptor() override = default; + + protected: + Parent* p() const + { + return qobject_cast(parent()); + } + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_DBUSADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp b/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp new file mode 100644 index 0000000000..7116041b84 --- /dev/null +++ b/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ItemAdaptor.h" + +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Session.h" + +namespace FdoSecrets +{ + + ItemAdaptor::ItemAdaptor(Item* parent) + : DBusAdaptor(parent) + { + } + + bool ItemAdaptor::locked() const + { + return p()->locked().valueOrHandle(p()); + } + + const StringStringMap ItemAdaptor::attributes() const + { + return p()->attributes().valueOrHandle(p()); + } + + void ItemAdaptor::setAttributes(const StringStringMap& attrs) + { + p()->setAttributes(attrs).handle(p()); + } + + QString ItemAdaptor::label() const + { + return p()->label().valueOrHandle(p()); + } + + void ItemAdaptor::setLabel(const QString& label) + { + p()->setLabel(label).handle(p()); + } + + qulonglong ItemAdaptor::created() const + { + return p()->created().valueOrHandle(p()); + } + + qulonglong ItemAdaptor::modified() const + { + return p()->modified().valueOrHandle(p()); + } + + QDBusObjectPath ItemAdaptor::Delete() + { + auto prompt = p()->deleteItem().valueOrHandle(p()); + return objectPathSafe(prompt); + } + + SecretStruct ItemAdaptor::GetSecret(const QDBusObjectPath& session) + { + return p()->getSecret(pathToObject(session)).valueOrHandle(p()); + } + + void ItemAdaptor::SetSecret(const SecretStruct& secret) + { + p()->setSecret(secret).handle(p()); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/ItemAdaptor.h b/src/fdosecrets/objects/adaptors/ItemAdaptor.h new file mode 100644 index 0000000000..4a6da4bf9a --- /dev/null +++ b/src/fdosecrets/objects/adaptors/ItemAdaptor.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H +#define KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H + +#include "fdosecrets/objects/adaptors/DBusAdaptor.h" + +namespace FdoSecrets +{ + + class Item; + class ItemAdaptor : public DBusAdaptor + { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_ITEM) + + Q_PROPERTY(bool Locked READ locked) + Q_PROPERTY(StringStringMap Attributes READ attributes WRITE setAttributes) + Q_PROPERTY(QString Label READ label WRITE setLabel) + Q_PROPERTY(qulonglong Created READ created) + Q_PROPERTY(qulonglong Modified READ modified) + + public: + explicit ItemAdaptor(Item* parent); + ~ItemAdaptor() override = default; + + bool locked() const; + + const StringStringMap attributes() const; + void setAttributes(const StringStringMap& attrs); + + QString label() const; + void setLabel(const QString& label); + + qulonglong created() const; + + qulonglong modified() const; + + public slots: + QDBusObjectPath Delete(); + FdoSecrets::SecretStruct GetSecret(const QDBusObjectPath& session); + void SetSecret(const FdoSecrets::SecretStruct& secret); + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_ITEMADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp b/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp new file mode 100644 index 0000000000..bcc1e271dd --- /dev/null +++ b/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "PromptAdaptor.h" + +#include "fdosecrets/objects/Prompt.h" + +namespace FdoSecrets +{ + + PromptAdaptor::PromptAdaptor(PromptBase* parent) + : DBusAdaptor(parent) + { + connect(p(), &PromptBase::completed, this, [this](bool dismissed, QVariant result) { + // make sure the result contains a valid value, otherwise QDBusVariant refuses to marshall it. + if (!result.isValid()) { + result = QString{}; + } + emit Completed(dismissed, QDBusVariant(std::move(result))); + }); + } + + void PromptAdaptor::Prompt(const QString& windowId) + { + p()->prompt(windowId).handle(p()); + } + + void PromptAdaptor::Dismiss() + { + p()->dismiss().handle(p()); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/PromptAdaptor.h b/src/fdosecrets/objects/adaptors/PromptAdaptor.h new file mode 100644 index 0000000000..9f43908196 --- /dev/null +++ b/src/fdosecrets/objects/adaptors/PromptAdaptor.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H +#define KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H + +#include "fdosecrets/objects/adaptors/DBusAdaptor.h" + +namespace FdoSecrets +{ + + class PromptBase; + class PromptAdaptor : public DBusAdaptor + { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_PROMPT) + + public: + explicit PromptAdaptor(PromptBase* parent); + ~PromptAdaptor() override = default; + + public slots: + void Prompt(const QString& windowId); + void Dismiss(); + + signals: + void Completed(bool dismissed, const QDBusVariant& result); + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_PROMPTADAPTOR_H diff --git a/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp b/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp new file mode 100644 index 0000000000..a260c4920d --- /dev/null +++ b/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ServiceAdaptor.h" + +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" + +namespace FdoSecrets +{ + + ServiceAdaptor::ServiceAdaptor(Service* parent) + : DBusAdaptor(parent) + { + connect(p(), &Service::collectionCreated, this, [this](Collection* coll) { + emit CollectionCreated(objectPathSafe(coll)); + }); + connect(p(), &Service::collectionDeleted, this, [this](Collection* coll) { + emit CollectionDeleted(objectPathSafe(coll)); + }); + connect(p(), &Service::collectionChanged, this, [this](Collection* coll) { + emit CollectionChanged(objectPathSafe(coll)); + }); + } + + const QList ServiceAdaptor::collections() const + { + auto colls = p()->collections().valueOrHandle(p()); + return objectsToPath(std::move(colls)); + } + + QDBusVariant + ServiceAdaptor::OpenSession(const QString& algorithm, const QDBusVariant& input, QDBusObjectPath& result) + { + Session* session = nullptr; + auto output = p()->openSession(algorithm, input.variant(), session).valueOrHandle(p()); + result = objectPathSafe(session); + return QDBusVariant(std::move(output)); + } + + QDBusObjectPath + ServiceAdaptor::CreateCollection(const QVariantMap& properties, const QString& alias, QDBusObjectPath& prompt) + { + PromptBase* pp; + auto coll = p()->createCollection(properties, alias, pp).valueOrHandle(p()); + prompt = objectPathSafe(pp); + return objectPathSafe(coll); + } + + const QList ServiceAdaptor::SearchItems(const StringStringMap& attributes, + QList& locked) + { + QList lockedItems, unlockedItems; + unlockedItems = p()->searchItems(attributes, lockedItems).valueOrHandle(p()); + locked = objectsToPath(lockedItems); + return objectsToPath(unlockedItems); + } + + const QList ServiceAdaptor::Unlock(const QList& paths, QDBusObjectPath& prompt) + { + auto objects = pathsToObject(paths); + if (!paths.isEmpty() && objects.isEmpty()) { + DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p()); + return {}; + } + + PromptBase* pp = nullptr; + auto unlocked = p()->unlock(objects, pp).valueOrHandle(p()); + + prompt = objectPathSafe(pp); + return objectsToPath(unlocked); + } + + const QList ServiceAdaptor::Lock(const QList& paths, QDBusObjectPath& prompt) + { + auto objects = pathsToObject(paths); + if (!paths.isEmpty() && objects.isEmpty()) { + DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p()); + return {}; + } + + PromptBase* pp = nullptr; + auto locked = p()->lock(objects, pp).valueOrHandle(p()); + + prompt = objectPathSafe(pp); + return objectsToPath(locked); + } + + const ObjectPathSecretMap ServiceAdaptor::GetSecrets(const QList& items, + const QDBusObjectPath& session) + { + auto itemObjects = pathsToObject(items); + if (!items.isEmpty() && itemObjects.isEmpty()) { + DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)).handle(p()); + return {}; + } + + auto secrets = p()->getSecrets(pathsToObject(items), pathToObject(session)).valueOrHandle(p()); + + ObjectPathSecretMap res; + auto iter = secrets.begin(); + while (iter != secrets.end()) { + res[objectPathSafe(iter.key())] = std::move(iter.value()); + ++iter; + } + return res; + } + + QDBusObjectPath ServiceAdaptor::ReadAlias(const QString& name) + { + auto coll = p()->readAlias(name).valueOrHandle(p()); + return objectPathSafe(coll); + } + + void ServiceAdaptor::SetAlias(const QString& name, const QDBusObjectPath& collection) + { + p()->setAlias(name, pathToObject(collection)).handle(p()); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/ServiceAdaptor.h b/src/fdosecrets/objects/adaptors/ServiceAdaptor.h new file mode 100644 index 0000000000..b369c12738 --- /dev/null +++ b/src/fdosecrets/objects/adaptors/ServiceAdaptor.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H +#define KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H + +#include "DBusAdaptor.h" + +#include + +namespace FdoSecrets +{ + /** + * @brief Adapter class for interface org.freedesktop.Secret.Service + */ + class Service; + class ServiceAdaptor : public DBusAdaptor + { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SERVICE) + + Q_PROPERTY(QList Collections READ collections) + + public: + explicit ServiceAdaptor(Service* parent); + ~ServiceAdaptor() override = default; + + const QList collections() const; + + public slots: + QDBusVariant OpenSession(const QString& algorithm, const QDBusVariant& input, QDBusObjectPath& result); + + QDBusObjectPath CreateCollection(const QVariantMap& properties, const QString& alias, QDBusObjectPath& prompt); + + const QList SearchItems(const StringStringMap& attributes, QList& locked); + + const QList Unlock(const QList& paths, QDBusObjectPath& prompt); + + const QList Lock(const QList& paths, QDBusObjectPath& prompt); + + const ObjectPathSecretMap GetSecrets(const QList& items, const QDBusObjectPath& session); + + QDBusObjectPath ReadAlias(const QString& name); + + void SetAlias(const QString& name, const QDBusObjectPath& collection); + + signals: + void CollectionCreated(const QDBusObjectPath& collection); + + void CollectionDeleted(const QDBusObjectPath& collection); + + void CollectionChanged(const QDBusObjectPath& collection); + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_SECRETSERVICEDBUS_H diff --git a/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp b/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp new file mode 100644 index 0000000000..6597bfffeb --- /dev/null +++ b/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SessionAdaptor.h" + +#include "fdosecrets/objects/Session.h" + +namespace FdoSecrets +{ + + SessionAdaptor::SessionAdaptor(Session* parent) + : DBusAdaptor(parent) + { + } + + void SessionAdaptor::Close() + { + p()->close().handle(p()); + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/objects/adaptors/SessionAdaptor.h b/src/fdosecrets/objects/adaptors/SessionAdaptor.h new file mode 100644 index 0000000000..4080617014 --- /dev/null +++ b/src/fdosecrets/objects/adaptors/SessionAdaptor.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H +#define KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H + +#include "fdosecrets/objects/adaptors/DBusAdaptor.h" + +namespace FdoSecrets +{ + + class Session; + class SessionAdaptor : public DBusAdaptor + { + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SESSION) + + public: + explicit SessionAdaptor(Session* parent); + ~SessionAdaptor() override = default; + + public slots: + void Close(); + }; + +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_SESSIONADAPTOR_H diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp new file mode 100644 index 0000000000..fadd015422 --- /dev/null +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DatabaseSettingsWidgetFdoSecrets.h" +#include "ui_DatabaseSettingsWidgetFdoSecrets.h" + +#include "fdosecrets/FdoSecretsSettings.h" + +#include "core/Database.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "gui/group/GroupModel.h" + +#include + +namespace +{ + enum class ExposedGroup + { + None, + Expose + }; +} // namespace + +class DatabaseSettingsWidgetFdoSecrets::GroupModelNoRecycle : public QSortFilterProxyModel +{ + Q_OBJECT + + Database* m_db; + +public: + explicit GroupModelNoRecycle(Database* db) + : m_db(db) + { + Q_ASSERT(db); + setSourceModel(new GroupModel(m_db, this)); + } + + Group* groupFromIndex(const QModelIndex& index) const + { + return groupFromSourceIndex(mapToSource(index)); + } + + Group* groupFromSourceIndex(const QModelIndex& index) const + { + auto groupModel = qobject_cast(sourceModel()); + Q_ASSERT(groupModel); + return groupModel->groupFromIndex(index); + } + + QModelIndex indexFromGroup(Group* group) const + { + auto groupModel = qobject_cast(sourceModel()); + Q_ASSERT(groupModel); + return mapFromSource(groupModel->index(group)); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override + { + auto source_idx = sourceModel()->index(source_row, 0, source_parent); + if (!source_idx.isValid()) { + return false; + } + + auto recycleBin = m_db->metadata()->recycleBin(); + if (!recycleBin) { + return true; + } + + // can not call mapFromSource, which internally calls filterAcceptsRow + auto group = groupFromSourceIndex(source_idx); + + return group->uuid() != recycleBin->uuid(); + } +}; + +DatabaseSettingsWidgetFdoSecrets::DatabaseSettingsWidgetFdoSecrets(QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::DatabaseSettingsWidgetFdoSecrets) +{ + m_ui->setupUi(this); + m_ui->buttonGroup->setId(m_ui->radioDonotExpose, static_cast(ExposedGroup::None)); + m_ui->buttonGroup->setId(m_ui->radioExpose, static_cast(ExposedGroup::Expose)); + + // make sure there is at least a selection + connect(m_ui->radioExpose, &QRadioButton::toggled, this, [this](bool checked) { + if (checked && !m_ui->selectGroup->selectionModel()->hasSelection()) { + auto model = m_ui->selectGroup->model(); + if (model) { + auto idx = model->index(0, 0); + m_ui->selectGroup->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent); + } + } + }); +} + +DatabaseSettingsWidgetFdoSecrets::~DatabaseSettingsWidgetFdoSecrets() = default; + +void DatabaseSettingsWidgetFdoSecrets::loadSettings(QSharedPointer db) +{ + m_db = std::move(db); + + m_model.reset(new GroupModelNoRecycle(m_db.data())); + m_ui->selectGroup->setModel(m_model.data()); + + auto group = m_db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(m_db)); + if (!group) { + m_ui->radioDonotExpose->setChecked(true); + } else { + auto idx = m_model->indexFromGroup(group); + m_ui->selectGroup->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent); + // expand all its parents + idx = idx.parent(); + while (idx.isValid()) { + m_ui->selectGroup->expand(idx); + idx = idx.parent(); + } + m_ui->radioExpose->setChecked(true); + } + + settingsWarning(); +} + +void DatabaseSettingsWidgetFdoSecrets::saveSettings() +{ + Q_ASSERT(m_db); + Q_ASSERT(m_model); + + QUuid exposedGroup; + switch (static_cast(m_ui->buttonGroup->checkedId())) { + case ExposedGroup::None: + break; + case ExposedGroup::Expose: { + auto idx = m_ui->selectGroup->selectionModel()->selectedIndexes().takeFirst(); + Q_ASSERT(idx.isValid()); + exposedGroup = m_model->groupFromIndex(idx)->uuid(); + break; + } + } + + FdoSecrets::settings()->setExposedGroup(m_db, exposedGroup); +} + +void DatabaseSettingsWidgetFdoSecrets::settingsWarning() +{ + if (FdoSecrets::settings()->isEnabled()) { + m_ui->groupBox->setEnabled(true); + m_ui->warningWidget->hideMessage(); + } else { + m_ui->groupBox->setEnabled(false); + m_ui->warningWidget->showMessage(tr("Enable fd.o Secret Service to access these settings."), + MessageWidget::Warning); + m_ui->warningWidget->setCloseButtonVisible(false); + m_ui->warningWidget->setAutoHideTimeout(-1); + } +} + +#include "DatabaseSettingsWidgetFdoSecrets.moc" diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h new file mode 100644 index 0000000000..829a2fae83 --- /dev/null +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_DATABASESETTINGSWIDGETFDOSECRETS_H +#define KEEPASSXC_DATABASESETTINGSWIDGETFDOSECRETS_H + +#include +#include +#include + +namespace Ui +{ + class DatabaseSettingsWidgetFdoSecrets; +} + +class Database; +class DatabaseSettingsWidgetFdoSecrets : public QWidget +{ + Q_OBJECT +public: + explicit DatabaseSettingsWidgetFdoSecrets(QWidget* parent = nullptr); + ~DatabaseSettingsWidgetFdoSecrets() override; + + void loadSettings(QSharedPointer db); + void saveSettings(); + +private: + void settingsWarning(); + +private: + QScopedPointer m_ui; + + QSharedPointer m_db; + + class GroupModelNoRecycle; + QScopedPointer m_model; +}; + +#endif // KEEPASSXC_DATABASESETTINGSWIDGETFDOSECRETS_H diff --git a/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui new file mode 100644 index 0000000000..6bacb32b62 --- /dev/null +++ b/src/fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.ui @@ -0,0 +1,110 @@ + + + DatabaseSettingsWidgetFdoSecrets + + + + 0 + 0 + 400 + 300 + + + + + + + + + + Exposed Entries + + + + + + Don't e&xpose this database + + + true + + + buttonGroup + + + + + + + Expose entries &under this group: + + + buttonGroup + + + + + + + false + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + MessageWidget + QWidget +
gui/MessageWidget.h
+ 1 +
+
+ + + + radioExpose + toggled(bool) + selectGroup + setEnabled(bool) + + + 199 + 92 + + + 199 + 189 + + + + + + + +
diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp new file mode 100644 index 0000000000..920b603d99 --- /dev/null +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SettingsWidgetFdoSecrets.h" +#include "ui_SettingsWidgetFdoSecrets.h" + +#include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/objects/Collection.h" +#include "fdosecrets/objects/Prompt.h" +#include "fdosecrets/objects/Session.h" + +#include "core/DatabaseIcons.h" +#include "core/FilePath.h" +#include "gui/DatabaseWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using FdoSecrets::Collection; +using FdoSecrets::Service; +using FdoSecrets::Session; + +SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent) + : QWidget(parent) + , m_ui(new Ui::SettingsWidgetFdoSecrets()) + , m_plugin(plugin) +{ + m_ui->setupUi(this); + + auto sessHeader = m_ui->tableSessions->horizontalHeader(); + sessHeader->setSelectionMode(QAbstractItemView::NoSelection); + sessHeader->setSectionsClickable(false); + sessHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application + sessHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button + + auto dbHeader = m_ui->tableDatabases->horizontalHeader(); + dbHeader->setSelectionMode(QAbstractItemView::NoSelection); + dbHeader->setSectionsClickable(false); + dbHeader->setSectionResizeMode(0, QHeaderView::Stretch); // file name + dbHeader->setSectionResizeMode(1, QHeaderView::Stretch); // group + dbHeader->setSectionResizeMode(2, QHeaderView::ResizeToContents); // manage button + + m_ui->tabWidget->setEnabled(m_ui->enableFdoSecretService->isChecked()); + connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, m_ui->tabWidget, &QTabWidget::setEnabled); +} + +SettingsWidgetFdoSecrets::~SettingsWidgetFdoSecrets() = default; + +void SettingsWidgetFdoSecrets::populateSessions(bool enabled) +{ + m_ui->tableSessions->setRowCount(0); + + auto service = m_plugin->serviceInstance(); + if (!service || !enabled) { + return; + } + + for (const auto& sess : service->sessions()) { + addSessionRow(sess); + } +} + +void SettingsWidgetFdoSecrets::addSessionRow(Session* sess) +{ + auto row = m_ui->tableSessions->rowCount(); + m_ui->tableSessions->insertRow(row); + + // column 0: application name + auto item = new QTableWidgetItem(sess->peer()); + item->setData(Qt::UserRole, QVariant::fromValue(sess)); + m_ui->tableSessions->setItem(row, 0, item); + + // column 1: disconnect button + auto btn = new QPushButton(tr("Disconnect")); + connect(btn, &QPushButton::clicked, sess, &Session::close); + m_ui->tableSessions->setCellWidget(row, 1, btn); + + // column 2: hidden uuid + m_ui->tableSessions->setItem(row, 2, new QTableWidgetItem(sess->id())); +} + +void SettingsWidgetFdoSecrets::removeSessionRow(Session* sess) +{ + int row = 0; + while (row != m_ui->tableSessions->rowCount()) { + auto item = m_ui->tableSessions->item(row, 0); + const auto itemSess = item->data(Qt::UserRole).value(); + if (itemSess == sess) { + break; + } + ++row; + } + if (row == m_ui->tableSessions->rowCount()) { + qWarning() << "Unknown Fdo Secret Service session" << sess->id() << "while removing collection from table"; + return; + } + + m_ui->tableSessions->removeRow(row); +} + +void SettingsWidgetFdoSecrets::populateDatabases(bool enabled) +{ + m_ui->tableDatabases->setRowCount(0); + + auto service = m_plugin->serviceInstance(); + if (!service || !enabled) { + return; + } + + auto ret = service->collections(); + if (ret.isError()) { + return; + } + for (const auto& coll : ret.value()) { + addDatabaseRow(coll); + } +} + +void SettingsWidgetFdoSecrets::addDatabaseRow(Collection* coll) +{ + auto row = m_ui->tableDatabases->rowCount(); + m_ui->tableDatabases->insertRow(row); + + // column 0: File name + QFileInfo fi(coll->backend()->database()->filePath()); + auto item = new QTableWidgetItem(fi.fileName()); + item->setData(Qt::UserRole, QVariant::fromValue(coll)); + m_ui->tableDatabases->setItem(row, 0, item); + + // column 2: manage button: hboxlayout: unlock/lock settings + // create this first so we have a widget to bind connection to, + // which can then be auto deleted when the row is deleted. + auto widget = createManageButtons(coll); + m_ui->tableDatabases->setCellWidget(row, 2, widget); + + // column 1: Group name + auto itemGroupName = new QTableWidgetItem(); + updateExposedGroupItem(itemGroupName, coll); + + connect(coll, &Collection::collectionLockChanged, widget, [this, itemGroupName, coll](bool) { + updateExposedGroupItem(itemGroupName, coll); + }); + + m_ui->tableDatabases->setItem(row, 1, itemGroupName); +} + +QWidget* SettingsWidgetFdoSecrets::createManageButtons(Collection* coll) +{ + auto toolbar = new QToolBar; + toolbar->setFloatable(false); + toolbar->setMovable(false); + + // db settings + auto dbSettingsAct = new QAction(tr("Database settings"), toolbar); + dbSettingsAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("document-edit"))); + dbSettingsAct->setToolTip(tr("Edit database settings")); + dbSettingsAct->setEnabled(!coll->locked().value()); + connect(dbSettingsAct, &QAction::triggered, this, [this, coll]() { + auto db = coll->backend(); + m_plugin->serviceInstance()->doSwitchToChangeDatabaseSettings(db); + }); + toolbar->addAction(dbSettingsAct); + + // unlock/lock + auto lockAct = new QAction(tr("Unlock database"), toolbar); + lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), true)); + lockAct->setToolTip(tr("Unlock database to show more information")); + connect(coll, &Collection::collectionLockChanged, lockAct, [lockAct, dbSettingsAct](bool locked) { + if (locked) { + lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-locked"), true)); + lockAct->setToolTip(tr("Unlock database to show more information")); + } else { + lockAct->setIcon(filePath()->icon(QStringLiteral("actions"), QStringLiteral("object-unlocked"), true)); + lockAct->setToolTip(tr("Lock database")); + } + dbSettingsAct->setEnabled(!locked); + }); + connect(lockAct, &QAction::triggered, this, [coll]() { + if (coll->locked().value()) { + coll->doUnlock(); + } else { + coll->doLock(); + } + }); + toolbar->addAction(lockAct); + + return toolbar; +} + +void SettingsWidgetFdoSecrets::updateExposedGroupItem(QTableWidgetItem* item, Collection* coll) +{ + if (coll->locked().value()) { + item->setText(tr("Unlock to show")); + item->setIcon(filePath()->icon(QStringLiteral("apps"), QStringLiteral("object-locked"), true)); + QFont font; + font.setItalic(true); + item->setFont(font); + return; + } + + auto db = coll->backend()->database(); + auto group = db->rootGroup()->findGroupByUuid(FdoSecrets::settings()->exposedGroup(db)); + if (group) { + item->setText(group->name()); + item->setIcon(group->isExpired() ? databaseIcons()->iconPixmap(DatabaseIcons::ExpiredIconIndex) + : group->iconScaledPixmap()); + if (group->isExpired()) { + QFont font; + font.setStrikeOut(true); + item->setFont(font); + } + } else { + item->setText(tr("None")); + item->setIcon(filePath()->icon(QStringLiteral("apps"), QStringLiteral("paint-none"), true)); + } +} + +void SettingsWidgetFdoSecrets::removeDatabaseRow(Collection* coll) +{ + int row = 0; + while (row != m_ui->tableDatabases->rowCount()) { + auto item = m_ui->tableDatabases->item(row, 0); + const auto itemColl = item->data(Qt::UserRole).value(); + if (itemColl == coll) { + break; + } + ++row; + } + if (row == m_ui->tableDatabases->rowCount()) { + qWarning() << "Unknown Fdo Secret Service collection" << coll->name() << "while removing collection from table"; + return; + } + + m_ui->tableDatabases->removeRow(row); +} + +void SettingsWidgetFdoSecrets::loadSettings() +{ + m_ui->enableFdoSecretService->setChecked(FdoSecrets::settings()->isEnabled()); + m_ui->showNotification->setChecked(FdoSecrets::settings()->showNotification()); + m_ui->noConfirmDeleteItem->setChecked(FdoSecrets::settings()->noConfirmDeleteItem()); +} + +void SettingsWidgetFdoSecrets::saveSettings() +{ + FdoSecrets::settings()->setEnabled(m_ui->enableFdoSecretService->isChecked()); + FdoSecrets::settings()->setShowNotification(m_ui->showNotification->isChecked()); + FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked()); +} + +void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + + QMetaObject::invokeMethod(this, "updateTables", Qt::QueuedConnection, Q_ARG(bool, true)); +} + +void SettingsWidgetFdoSecrets::hideEvent(QHideEvent* event) +{ + QWidget::hideEvent(event); + + QMetaObject::invokeMethod(this, "updateTables", Qt::QueuedConnection, Q_ARG(bool, false)); +} + +void SettingsWidgetFdoSecrets::updateTables(bool enabled) +{ + if (enabled) { + // update the table + populateDatabases(m_ui->enableFdoSecretService->isChecked()); + populateSessions(m_ui->enableFdoSecretService->isChecked()); + + // re-layout the widget to adjust the table cell size + adjustSize(); + + connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateSessions); + connect(m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateDatabases); + + auto service = m_plugin->serviceInstance(); + if (service) { + connect(service, &Service::sessionOpened, this, &SettingsWidgetFdoSecrets::addSessionRow); + connect(service, &Service::sessionClosed, this, &SettingsWidgetFdoSecrets::removeSessionRow); + connect(service, &Service::collectionCreated, this, &SettingsWidgetFdoSecrets::addDatabaseRow); + connect(service, &Service::collectionDeleted, this, &SettingsWidgetFdoSecrets::removeDatabaseRow); + } + } else { + disconnect( + m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateSessions); + disconnect( + m_ui->enableFdoSecretService, &QCheckBox::toggled, this, &SettingsWidgetFdoSecrets::populateDatabases); + + auto service = m_plugin->serviceInstance(); + if (service) { + disconnect(service, &Service::sessionOpened, this, &SettingsWidgetFdoSecrets::addSessionRow); + disconnect(service, &Service::sessionClosed, this, &SettingsWidgetFdoSecrets::removeSessionRow); + disconnect(service, &Service::collectionCreated, this, &SettingsWidgetFdoSecrets::addDatabaseRow); + disconnect(service, &Service::collectionDeleted, this, &SettingsWidgetFdoSecrets::removeDatabaseRow); + } + } +} diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h new file mode 100644 index 0000000000..eac1f1e3c8 --- /dev/null +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2018 Aetf + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H +#define KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H + +#include +#include + +class QTableWidgetItem; + +namespace FdoSecrets +{ + + class Session; + class Collection; + +} // namespace FdoSecrets + +class FdoSecretsPlugin; + +namespace Ui +{ + class SettingsWidgetFdoSecrets; +} +class SettingsWidgetFdoSecrets : public QWidget +{ + Q_OBJECT +public: + explicit SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent = nullptr); + ~SettingsWidgetFdoSecrets() override; + +public slots: + void loadSettings(); + void saveSettings(); + +private slots: + void populateSessions(bool enabled); + void populateDatabases(bool enabled); + void addSessionRow(FdoSecrets::Session* sess); + void removeSessionRow(FdoSecrets::Session* sess); + void addDatabaseRow(FdoSecrets::Collection* coll); + void removeDatabaseRow(FdoSecrets::Collection* coll); + + void updateTables(bool enabled); + +protected: + void showEvent(QShowEvent* event) override; + + void hideEvent(QHideEvent* event) override; + +private: + QWidget* createManageButtons(FdoSecrets::Collection* coll); + + void updateExposedGroupItem(QTableWidgetItem* item, FdoSecrets::Collection* coll); + +private: + QScopedPointer m_ui; + FdoSecretsPlugin* m_plugin; +}; + +#endif // KEEPASSXC_SETTINGSWIDGETFDOSECRETS_H diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui new file mode 100644 index 0000000000..b77e086c9a --- /dev/null +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -0,0 +1,162 @@ + + + SettingsWidgetFdoSecrets + + + + 0 + 0 + 525 + 457 + + + + Options + + + + + + + + + Enable KeepassXC Freedesktop.org Secret Service integration + + + + + + + 0 + + + + General + + + + + + Show notification when credentials are requested + + + + + + + <html><head/><body><p>If recycle bin is enabled for the database, entries will be moved to recycle bin directly. Otherwise, they will be deleted without confirmation.</p><p>You will still be prompted if any entries are referenced by others.</p></body></html> + + + Don't confirm when entries are deleted by clients. + + + + + + + Qt::Vertical + + + QSizePolicy::Preferred + + + + 20 + 40 + + + + + + + + Exposed database groups: + + + + + + + Qt::NoFocus + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + false + + + + File Name + + + + + Group + + + + + Manage + + + + + + + + + Authorization + + + + + + These applications are currently connected: + + + + + + + Qt::NoFocus + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + false + + + + Application + + + + + Manage + + + + + + + + + + + + + diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index fb234795cc..61051e3f0b 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -116,15 +116,17 @@ QSharedPointer DatabaseTabWidget::execNewDatabaseWizard() return db; } -void DatabaseTabWidget::newDatabase() +DatabaseWidget* DatabaseTabWidget::newDatabase() { auto db = execNewDatabaseWizard(); if (!db) { - return; + return nullptr; } - addDatabaseTab(new DatabaseWidget(db, this)); + auto dbWidget = new DatabaseWidget(db, this); + addDatabaseTab(dbWidget); db->markAsModified(); + return dbWidget; } void DatabaseTabWidget::openDatabase() @@ -187,10 +189,12 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou { Q_ASSERT(dbWidget->database()); + // emit before index change + emit databaseOpened(dbWidget); + int index = addTab(dbWidget, ""); updateTabName(index); toggleTabbar(); - if (!inBackground) { setCurrentIndex(index); } diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index bafbfa37af..af84c0a1e1 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -60,7 +60,7 @@ public slots: bool closeDatabaseTabFromSender(); void updateTabName(int index = -1); - void newDatabase(); + DatabaseWidget* newDatabase(); void openDatabase(); void mergeDatabase(); void importCsv(); @@ -80,6 +80,7 @@ public slots: void performGlobalAutoType(); signals: + void databaseOpened(DatabaseWidget* dbWidget); void databaseClosed(const QString& filePath); void databaseUnlocked(DatabaseWidget* dbWidget); void databaseLocked(DatabaseWidget* dbWidget); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index e4f175bf26..178af80051 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -362,27 +362,10 @@ void DatabaseWidget::createEntry() m_newEntry->setUuid(QUuid::createUuid()); m_newEntry->setUsername(m_db->metadata()->defaultUserName()); m_newParent = m_groupView->currentGroup(); - setIconFromParent(); + m_newParent->applyGroupIconTo(m_newEntry.data()); switchToEntryEdit(m_newEntry.data(), true); } -void DatabaseWidget::setIconFromParent() -{ - if (!config()->get("UseGroupIconOnEntryCreation").toBool()) { - return; - } - - if (m_newParent->iconNumber() == Group::DefaultIconNumber && m_newParent->iconUuid().isNull()) { - return; - } - - if (m_newParent->iconUuid().isNull()) { - m_newEntry->setIcon(m_newParent->iconNumber()); - } else { - m_newEntry->setIcon(m_newParent->iconUuid()); - } -} - void DatabaseWidget::replaceDatabase(QSharedPointer db) { // TODO: instead of increasing the ref count temporarily, there should be a clean @@ -393,6 +376,9 @@ void DatabaseWidget::replaceDatabase(QSharedPointer db) connectDatabaseSignals(); m_groupView->changeDatabase(m_db); processAutoOpen(); + + emit databaseReplaced(oldDb, m_db); + #if defined(WITH_XC_KEESHARE) KeeShare::instance()->connectDatabase(m_db, oldDb); #else @@ -461,6 +447,11 @@ void DatabaseWidget::deleteSelectedEntries() selectedEntries.append(m_entryView->entryFromIndex(index)); } + deleteEntries(std::move(selectedEntries)); +} + +void DatabaseWidget::deleteEntries(QList selectedEntries) +{ // Confirm entry removal before moving forward auto* recycleBin = m_db->metadata()->recycleBin(); bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid())) diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index fb9cf817e4..7e012d2c35 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -125,6 +125,9 @@ class DatabaseWidget : public QStackedWidget void databaseUnlocked(); void databaseLocked(); + // Emitted in replaceDatabase, may be caused by lock, reload, unlock, load. + void databaseReplaced(const QSharedPointer& oldDb, const QSharedPointer& newDb); + void closeRequest(); void currentModeChanged(DatabaseWidget::Mode mode); void groupChanged(); @@ -151,6 +154,7 @@ public slots: void createEntry(); void cloneEntry(); void deleteSelectedEntries(); + void deleteEntries(QList entries); void setFocus(); void copyTitle(); void copyUsername(); @@ -223,7 +227,6 @@ private slots: private: int addChildWidget(QWidget* w); void setClipboardTextAndMinimize(const QString& text); - void setIconFromParent(); void processAutoOpen(); bool confirmDeleteEntries(QList entries, bool permanent); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e5f5ea6132..5f5f198785 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -59,6 +59,11 @@ #include "keeshare/KeeShare.h" #include "keeshare/SettingsPageKeeShare.h" #endif + +#ifdef WITH_XC_FDOSECRETS +#include "fdosecrets/FdoSecretsPlugin.h" +#endif + #ifdef WITH_XC_BROWSER #include "browser/BrowserOptionDialog.h" #include "browser/BrowserSettings.h" @@ -181,6 +186,16 @@ MainWindow::MainWindow() SIGNAL(sharingMessage(QString, MessageWidget::MessageType)), SLOT(displayGlobalMessage(QString, MessageWidget::MessageType))); #endif + +#ifdef WITH_XC_FDOSECRETS + auto fdoSS = new FdoSecretsPlugin(m_ui->tabWidget); + connect(fdoSS, &FdoSecretsPlugin::error, this, &MainWindow::showErrorMessage); + connect(fdoSS, &FdoSecretsPlugin::requestSwitchToDatabases, this, &MainWindow::switchToDatabases); + connect(fdoSS, &FdoSecretsPlugin::requestShowNotification, this, &MainWindow::displayDesktopNotification); + fdoSS->updateServiceState(); + m_ui->settingsWidget->addSettingsPage(fdoSS); +#endif + setWindowIcon(filePath()->applicationIcon()); m_ui->globalMessageWidget->setHidden(true); // clang-format off @@ -1261,3 +1276,20 @@ void MainWindow::lockAllDatabases() { lockDatabasesAfterInactivity(); } + +void MainWindow::displayDesktopNotification(const QString& msg, QString title, int msTimeoutHint) +{ + if (!m_trayIcon || !QSystemTrayIcon::supportsMessages()) { + return; + } + + if (title.isEmpty()) { + title = BaseWindowTitle; + } + +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + m_trayIcon->showMessage(title, msg, filePath()->applicationIcon(), msTimeoutHint); +#else + m_trayIcon->showMessage(title, msg, QSystemTrayIcon::Information, msTimeoutHint); +#endif +} diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index f1e543468f..7c10727f6b 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -74,6 +74,7 @@ public slots: void bringToFront(); void closeAllDatabases(); void lockAllDatabases(); + void displayDesktopNotification(const QString& msg, QString title = "", int msTimeoutHint = 10000); protected: void closeEvent(QCloseEvent* event) override; diff --git a/src/gui/MessageBox.cpp b/src/gui/MessageBox.cpp index 582baa5ccb..9382287318 100644 --- a/src/gui/MessageBox.cpp +++ b/src/gui/MessageBox.cpp @@ -18,6 +18,10 @@ #include "MessageBox.h" +#include + +QWindow* MessageBox::m_overrideParent(nullptr); + MessageBox::Button MessageBox::m_nextAnswer(MessageBox::NoButton); QHash MessageBox::m_addedButtonLookup = @@ -81,6 +85,14 @@ MessageBox::Button MessageBox::messageBox(QWidget* parent, msgBox.setWindowTitle(title); msgBox.setText(text); + if (m_overrideParent) { + // Force the creation of the QWindow, without this windowHandle() will return nullptr + msgBox.winId(); + auto msgBoxWindow = msgBox.windowHandle(); + Q_ASSERT(msgBoxWindow); + msgBoxWindow->setTransientParent(m_overrideParent); + } + for (uint64_t b = First; b <= Last; b <<= 1) { if (b & buttons) { QString text = m_buttonDefs[static_cast