From 9a8a5a00063c91f0df556066ad85944bcc369382 Mon Sep 17 00:00:00 2001 From: Aetf Date: Fri, 5 Feb 2021 15:07:59 -0500 Subject: [PATCH] FdoSecrets: Major Refactor and Code Consolidation (#5747) * Fixes #3837 * Change objects to use DBusMgr rather than separate adaptors - Update all DBus invokable methods to new parameter order - Change all usage of DBusReturn to simpler DBusResult - Use DBusMgr to handle path and service registration - Remove adaptor/* - Set path in DBusObject - Unregister service when service is destroyed - Restore handling of invalid QVariant in prompt complete signal - Clean up meta type registration - Move dbus related file together - Convert to QSharedPointer as much as possible - Fix mapping of the Delete method - Handle dbus property get all * Add per-client states - Move cipher negotiation to DBusClient - Show list of clients instead of sessions in the settings page - Add settings for confirmation of accessing items - Fix infinite recursion when client disconnected - Use optional explicit DBusClient parameter instead. This makes accessing the client info in an async context explicit, and thus prevent accidental assertions in prompts. * Improve User Interface - Add per-item access confirmation (if enabled) - Remove the "disable for site" button for the access control dialog - Improve the text on the settings page to be more consistent - Fix disconnect buttons in settings page not working - Make the unlock prompt method nonblocking * Fix and cleanup unit tests - Use QTRY_COMPARE when checking signal spies, as dbus signals are threaded - Fixes in meta type registration and type conversion - Remove QStringLiteral in COMPARE macros, making diff output readable - Add testing for remembering auth decision --- src/core/Config.cpp | 3 +- src/core/Config.h | 3 +- src/core/Global.h | 9 + src/fdosecrets/CMakeLists.txt | 17 +- src/fdosecrets/FdoSecretsPlugin.cpp | 45 +- src/fdosecrets/FdoSecretsPlugin.h | 8 +- src/fdosecrets/FdoSecretsSettings.cpp | 18 +- src/fdosecrets/FdoSecretsSettings.h | 7 +- src/fdosecrets/dbus/DBusClient.cpp | 144 ++ src/fdosecrets/dbus/DBusClient.h | 146 ++ src/fdosecrets/dbus/DBusConstants.h | 49 + src/fdosecrets/dbus/DBusDispatch.cpp | 395 +++++ src/fdosecrets/dbus/DBusMgr.cpp | 623 ++++++++ src/fdosecrets/dbus/DBusMgr.h | 335 ++++ .../{objects => dbus}/DBusObject.cpp | 29 +- src/fdosecrets/dbus/DBusObject.h | 130 ++ src/fdosecrets/dbus/DBusTypes.cpp | 219 +++ src/fdosecrets/dbus/DBusTypes.h | 107 ++ src/fdosecrets/objects/Collection.cpp | 283 ++-- src/fdosecrets/objects/Collection.h | 38 +- src/fdosecrets/objects/DBusObject.h | 202 --- src/fdosecrets/objects/DBusReturn.cpp | 18 - src/fdosecrets/objects/DBusReturn.h | 258 --- src/fdosecrets/objects/DBusTypes.cpp | 53 - src/fdosecrets/objects/DBusTypes.h | 92 -- src/fdosecrets/objects/Item.cpp | 182 +-- src/fdosecrets/objects/Item.h | 49 +- src/fdosecrets/objects/Prompt.cpp | 391 +++-- src/fdosecrets/objects/Prompt.h | 113 +- src/fdosecrets/objects/Service.cpp | 323 ++-- src/fdosecrets/objects/Service.h | 61 +- src/fdosecrets/objects/Session.cpp | 71 +- src/fdosecrets/objects/Session.h | 37 +- src/fdosecrets/objects/SessionCipher.cpp | 8 +- src/fdosecrets/objects/SessionCipher.h | 12 +- .../objects/adaptors/CollectionAdaptor.cpp | 93 -- .../objects/adaptors/CollectionAdaptor.h | 71 - src/fdosecrets/objects/adaptors/DBusAdaptor.h | 51 - .../objects/adaptors/ItemAdaptor.cpp | 83 - src/fdosecrets/objects/adaptors/ItemAdaptor.h | 62 - .../objects/adaptors/PromptAdaptor.cpp | 48 - .../objects/adaptors/PromptAdaptor.h | 46 - .../objects/adaptors/ServiceAdaptor.cpp | 138 -- .../objects/adaptors/ServiceAdaptor.h | 71 - .../objects/adaptors/SessionAdaptor.cpp | 35 - .../objects/adaptors/SessionAdaptor.h | 42 - .../widgets/AccessControlDialog.cpp | 241 +++ src/fdosecrets/widgets/AccessControlDialog.h | 125 ++ src/fdosecrets/widgets/AccessControlDialog.ui | 133 ++ src/fdosecrets/widgets/RowButtonHelper.cpp | 68 + src/fdosecrets/widgets/RowButtonHelper.h | 42 + src/fdosecrets/widgets/SettingsModels.cpp | 85 +- src/fdosecrets/widgets/SettingsModels.h | 24 +- .../widgets/SettingsWidgetFdoSecrets.cpp | 124 +- .../widgets/SettingsWidgetFdoSecrets.h | 14 - .../widgets/SettingsWidgetFdoSecrets.ui | 29 +- src/gui/DatabaseWidget.cpp | 6 +- src/gui/DatabaseWidget.h | 2 +- tests/TestFdoSecrets.cpp | 40 +- tests/TestFdoSecrets.h | 1 + tests/data/NewDatabase.kdbx | Bin 20334 -> 20590 bytes .../org.freedesktop.Secret.Collection.xml | 33 - .../org.freedesktop.Secret.Item.xml | 21 - .../org.freedesktop.Secret.Prompt.xml | 11 - .../org.freedesktop.Secret.Service.xml | 55 - .../org.freedesktop.Secret.Session.xml | 4 - tests/gui/CMakeLists.txt | 2 +- tests/gui/TestGuiFdoSecrets.cpp | 1390 ++++++++++------- tests/gui/TestGuiFdoSecrets.h | 57 +- tests/util/FdoSecretsProxy.cpp | 34 + tests/util/FdoSecretsProxy.h | 402 +++++ 71 files changed, 5086 insertions(+), 3075 deletions(-) create mode 100644 src/fdosecrets/dbus/DBusClient.cpp create mode 100644 src/fdosecrets/dbus/DBusClient.h create mode 100644 src/fdosecrets/dbus/DBusConstants.h create mode 100644 src/fdosecrets/dbus/DBusDispatch.cpp create mode 100644 src/fdosecrets/dbus/DBusMgr.cpp create mode 100644 src/fdosecrets/dbus/DBusMgr.h rename src/fdosecrets/{objects => dbus}/DBusObject.cpp (69%) create mode 100644 src/fdosecrets/dbus/DBusObject.h create mode 100644 src/fdosecrets/dbus/DBusTypes.cpp create mode 100644 src/fdosecrets/dbus/DBusTypes.h delete mode 100644 src/fdosecrets/objects/DBusObject.h delete mode 100644 src/fdosecrets/objects/DBusReturn.cpp delete mode 100644 src/fdosecrets/objects/DBusReturn.h delete mode 100644 src/fdosecrets/objects/DBusTypes.cpp delete mode 100644 src/fdosecrets/objects/DBusTypes.h delete mode 100644 src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp delete mode 100644 src/fdosecrets/objects/adaptors/CollectionAdaptor.h delete mode 100644 src/fdosecrets/objects/adaptors/DBusAdaptor.h delete mode 100644 src/fdosecrets/objects/adaptors/ItemAdaptor.cpp delete mode 100644 src/fdosecrets/objects/adaptors/ItemAdaptor.h delete mode 100644 src/fdosecrets/objects/adaptors/PromptAdaptor.cpp delete mode 100644 src/fdosecrets/objects/adaptors/PromptAdaptor.h delete mode 100644 src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp delete mode 100644 src/fdosecrets/objects/adaptors/ServiceAdaptor.h delete mode 100644 src/fdosecrets/objects/adaptors/SessionAdaptor.cpp delete mode 100644 src/fdosecrets/objects/adaptors/SessionAdaptor.h create mode 100644 src/fdosecrets/widgets/AccessControlDialog.cpp create mode 100644 src/fdosecrets/widgets/AccessControlDialog.h create mode 100644 src/fdosecrets/widgets/AccessControlDialog.ui create mode 100644 src/fdosecrets/widgets/RowButtonHelper.cpp create mode 100644 src/fdosecrets/widgets/RowButtonHelper.h delete mode 100644 tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml delete mode 100644 tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml delete mode 100644 tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml delete mode 100644 tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml delete mode 100644 tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml create mode 100644 tests/util/FdoSecretsProxy.cpp create mode 100644 tests/util/FdoSecretsProxy.h diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 8b2b16cf1..3b3adbfb7 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -173,7 +173,8 @@ static const QHash configStrings = { // FdoSecrets {Config::FdoSecrets_Enabled, {QS("FdoSecrets/Enabled"), Roaming, false}}, {Config::FdoSecrets_ShowNotification, {QS("FdoSecrets/ShowNotification"), Roaming, true}}, - {Config::FdoSecrets_NoConfirmDeleteItem, {QS("FdoSecrets/NoConfirmDeleteItem"), Roaming, false}}, + {Config::FdoSecrets_ConfirmDeleteItem, {QS("FdoSecrets/ConfirmDeleteItem"), Roaming, true}}, + {Config::FdoSecrets_ConfirmAccessItem, {QS("FdoSecrets/ConfirmAccessItem"), Roaming, true}}, // KeeShare {Config::KeeShare_QuietSuccess, {QS("KeeShare/QuietSuccess"), Roaming, false}}, diff --git a/src/core/Config.h b/src/core/Config.h index 8b9a02a5f..fc8af88e0 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -151,7 +151,8 @@ public: FdoSecrets_Enabled, FdoSecrets_ShowNotification, - FdoSecrets_NoConfirmDeleteItem, + FdoSecrets_ConfirmDeleteItem, + FdoSecrets_ConfirmAccessItem, KeeShare_QuietSuccess, KeeShare_Own, diff --git a/src/core/Global.h b/src/core/Global.h index aebdb4559..5e7375148 100644 --- a/src/core/Global.h +++ b/src/core/Global.h @@ -53,6 +53,15 @@ enum IconSize Large }; +enum class AuthDecision +{ + Undecided, + Allowed, + AllowedOnce, + Denied, + DeniedOnce, +}; + template struct AddConst { typedef const T Type; diff --git a/src/fdosecrets/CMakeLists.txt b/src/fdosecrets/CMakeLists.txt index a9750bc2d..35728a179 100644 --- a/src/fdosecrets/CMakeLists.txt +++ b/src/fdosecrets/CMakeLists.txt @@ -6,11 +6,15 @@ if(WITH_XC_FDOSECRETS) FdoSecretsPlugin.cpp widgets/SettingsModels.cpp widgets/SettingsWidgetFdoSecrets.cpp + widgets/RowButtonHelper.cpp # per database settings page DatabaseSettingsPageFdoSecrets.cpp widgets/DatabaseSettingsWidgetFdoSecrets.cpp + # prompt dialog + widgets/AccessControlDialog.cpp + # setting storage FdoSecretsSettings.cpp @@ -18,20 +22,17 @@ if(WITH_XC_FDOSECRETS) GcryptMPI.cpp # dbus objects - objects/DBusObject.cpp + dbus/DBusClient.cpp + dbus/DBusMgr.cpp + dbus/DBusDispatch.cpp + dbus/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 + dbus/DBusTypes.cpp ) target_link_libraries(fdosecrets Qt5::Core Qt5::Widgets Qt5::DBus ${GCRYPT_LIBRARIES}) endif() diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp index 8004de246..20247a571 100644 --- a/src/fdosecrets/FdoSecretsPlugin.cpp +++ b/src/fdosecrets/FdoSecretsPlugin.cpp @@ -18,14 +18,14 @@ #include "FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" -#include "fdosecrets/objects/DBusTypes.h" +#include "fdosecrets/dbus/DBusMgr.h" +#include "fdosecrets/dbus/DBusTypes.h" #include "fdosecrets/objects/Service.h" #include "fdosecrets/widgets/SettingsWidgetFdoSecrets.h" #include "gui/DatabaseTabWidget.h" -#include - +using FdoSecrets::DBusMgr; using FdoSecrets::Service; // TODO: Only used for testing. Need to split service functions away from settings page. @@ -33,9 +33,13 @@ QPointer g_fdoSecretsPlugin; FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget) : m_dbTabs(tabWidget) + , m_dbus(new DBusMgr()) { + registerDBusTypes(m_dbus); + m_dbus->populateMethodCache(); + + connect(m_dbus.data(), &DBusMgr::error, this, &FdoSecretsPlugin::emitError); g_fdoSecretsPlugin = this; - FdoSecrets::registerDBusTypes(); } FdoSecretsPlugin* FdoSecretsPlugin::getPlugin() @@ -63,7 +67,7 @@ void FdoSecretsPlugin::updateServiceState() { if (FdoSecrets::settings()->isEnabled()) { if (!m_secretService && m_dbTabs) { - m_secretService = Service::Create(this, m_dbTabs); + m_secretService = Service::Create(this, m_dbTabs, m_dbus); if (!m_secretService) { FdoSecrets::settings()->setEnabled(false); return; @@ -88,6 +92,11 @@ DatabaseTabWidget* FdoSecretsPlugin::dbTabs() const return m_dbTabs; } +const QSharedPointer& FdoSecretsPlugin::dbus() const +{ + return m_dbus; +} + void FdoSecretsPlugin::emitRequestSwitchToDatabases() { emit requestSwitchToDatabases(); @@ -106,29 +115,3 @@ void FdoSecretsPlugin::emitError(const QString& msg) emit error(tr("Fdo Secret Service: %1").arg(msg)); qDebug() << msg; } - -QString FdoSecretsPlugin::reportExistingService() const -{ - auto pidStr = tr("Unknown", "Unknown PID"); - auto exeStr = tr("Unknown", "Unknown executable path"); - - // try get pid - auto pid = QDBusConnection::sessionBus().interface()->servicePid(DBUS_SERVICE_SECRET); - if (pid.isValid()) { - pidStr = QString::number(pid.value()); - - // try get the first part of the cmdline, which usually is the executable name/path - QFile proc(QStringLiteral("/proc/%1/cmdline").arg(pid.value())); - if (proc.open(QFile::ReadOnly)) { - auto parts = proc.readAll().split('\0'); - if (parts.length() >= 1) { - exeStr = QString::fromLocal8Bit(parts[0]).trimmed(); - } - } - } - auto otherService = tr("PID: %1, Executable: %2", "PID: 1234, Executable: /path/to/exe") - .arg(pidStr, exeStr.toHtmlEscaped()); - return tr("Another secret service is running (%1).
" - "Please stop/remove it before re-enabling the Secret Service Integration.") - .arg(otherService); -} diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h index 282334600..13f8669f8 100644 --- a/src/fdosecrets/FdoSecretsPlugin.h +++ b/src/fdosecrets/FdoSecretsPlugin.h @@ -30,6 +30,7 @@ class DatabaseTabWidget; namespace FdoSecrets { class Service; + class DBusMgr; } // namespace FdoSecrets class FdoSecretsPlugin : public QObject, public ISettingsPage @@ -66,10 +67,10 @@ public: DatabaseTabWidget* dbTabs() const; /** - * Check the running secret service and returns info about it - * @return html string suitable to be shown in the UI + * @brief The dbus manager instance + * @return */ - QString reportExistingService() const; + const QSharedPointer& dbus() const; // TODO: Only used for testing. Need to split service functions away from settings page. static FdoSecretsPlugin* getPlugin(); @@ -93,6 +94,7 @@ signals: private: QPointer m_dbTabs; + QSharedPointer m_dbus; QSharedPointer m_secretService; }; diff --git a/src/fdosecrets/FdoSecretsSettings.cpp b/src/fdosecrets/FdoSecretsSettings.cpp index 20eff4a08..c2ebf9d43 100644 --- a/src/fdosecrets/FdoSecretsSettings.cpp +++ b/src/fdosecrets/FdoSecretsSettings.cpp @@ -64,14 +64,24 @@ namespace FdoSecrets config()->set(Config::FdoSecrets_ShowNotification, show); } - bool FdoSecretsSettings::noConfirmDeleteItem() const + bool FdoSecretsSettings::confirmDeleteItem() const { - return config()->get(Config::FdoSecrets_NoConfirmDeleteItem).toBool(); + return config()->get(Config::FdoSecrets_ConfirmDeleteItem).toBool(); } - void FdoSecretsSettings::setNoConfirmDeleteItem(bool noConfirm) + void FdoSecretsSettings::setConfirmDeleteItem(bool confirm) { - config()->set(Config::FdoSecrets_NoConfirmDeleteItem, noConfirm); + config()->set(Config::FdoSecrets_ConfirmDeleteItem, confirm); + } + + bool FdoSecretsSettings::confirmAccessItem() const + { + return config()->get(Config::FdoSecrets_ConfirmAccessItem).toBool(); + } + + void FdoSecretsSettings::setConfirmAccessItem(bool confirmAccessItem) + { + config()->set(Config::FdoSecrets_ConfirmAccessItem, confirmAccessItem); } QUuid FdoSecretsSettings::exposedGroup(const QSharedPointer& db) const diff --git a/src/fdosecrets/FdoSecretsSettings.h b/src/fdosecrets/FdoSecretsSettings.h index 5a9028876..24a37a8d2 100644 --- a/src/fdosecrets/FdoSecretsSettings.h +++ b/src/fdosecrets/FdoSecretsSettings.h @@ -38,8 +38,11 @@ namespace FdoSecrets bool showNotification() const; void setShowNotification(bool show); - bool noConfirmDeleteItem() const; - void setNoConfirmDeleteItem(bool noConfirm); + bool confirmDeleteItem() const; + void setConfirmDeleteItem(bool confirm); + + bool confirmAccessItem() const; + void setConfirmAccessItem(bool confirmAccessItem); // Per db settings diff --git a/src/fdosecrets/dbus/DBusClient.cpp b/src/fdosecrets/dbus/DBusClient.cpp new file mode 100644 index 000000000..4fa47465f --- /dev/null +++ b/src/fdosecrets/dbus/DBusClient.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 Aetf + * Copyright (C) 2020 Jan Klötzke + * + * 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 "DBusClient.h" + +#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" +#include "fdosecrets/objects/SessionCipher.h" + +namespace FdoSecrets +{ + DBusClient::DBusClient(DBusMgr* dbus, const QString& address, uint pid, const QString& name) + : m_dbus(dbus) + , m_address(address) + , m_pid(pid) + , m_name(name) + { + } + + bool DBusClient::itemKnown(const QUuid& uuid) const + { + return m_authorizedAll || m_allowed.contains(uuid) || m_allowedOnce.contains(uuid) || m_denied.contains(uuid) + || m_deniedOnce.contains(uuid); + } + + bool DBusClient::itemAuthorized(const QUuid& uuid) const + { + if (!FdoSecrets::settings()->confirmAccessItem()) { + // everyone is authorized if this is not enabled + return true; + } + if (m_authorizedAll) { + // this client is trusted + return true; + } + if (m_deniedOnce.contains(uuid) || m_denied.contains(uuid)) { + // explicitly denied + return false; + } + if (m_allowedOnce.contains(uuid) || m_allowed.contains(uuid)) { + // explicitly allowed + return true; + } + // haven't asked, not authorized by default + return false; + } + + bool DBusClient::itemAuthorizedResetOnce(const QUuid& uuid) + { + auto auth = itemAuthorized(uuid); + m_deniedOnce.remove(uuid); + m_allowedOnce.remove(uuid); + return auth; + } + + void DBusClient::setItemAuthorized(const QUuid& uuid, AuthDecision auth) + { + // uuid should only be in exactly one set at any time + m_allowed.remove(uuid); + m_allowedOnce.remove(uuid); + m_denied.remove(uuid); + m_deniedOnce.remove(uuid); + switch (auth) { + case AuthDecision::Allowed: + m_allowed.insert(uuid); + break; + case AuthDecision::AllowedOnce: + m_allowedOnce.insert(uuid); + break; + case AuthDecision::Denied: + m_denied.insert(uuid); + break; + case AuthDecision::DeniedOnce: + m_deniedOnce.insert(uuid); + break; + default: + break; + } + } + + void DBusClient::setAllAuthorized(bool authorized) + { + m_authorizedAll = authorized; + } + + void DBusClient::clearAuthorization() + { + m_authorizedAll = false; + m_allowed.clear(); + m_allowedOnce.clear(); + m_denied.clear(); + m_deniedOnce.clear(); + } + + void DBusClient::disconnectDBus() + { + clearAuthorization(); + // notify DBusMgr about the removal + m_dbus->removeClient(this); + } + + QSharedPointer + DBusClient::negotiateCipher(const QString& algorithm, const QVariant& input, QVariant& output, bool& incomplete) + { + incomplete = false; + + QSharedPointer cipher{}; + if (algorithm == PlainCipher::Algorithm) { + cipher.reset(new PlainCipher); + } else if (algorithm == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { + 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; + } +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusClient.h b/src/fdosecrets/dbus/DBusClient.h new file mode 100644 index 000000000..994a9d4f6 --- /dev/null +++ b/src/fdosecrets/dbus/DBusClient.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2020 Aetf + * Copyright (C) 2020 Jan Klötzke + * + * 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_DBUSCLIENT_H +#define KEEPASSXC_FDOSECRETS_DBUSCLIENT_H + +#include +#include +#include +#include +#include + +#include "core/Global.h" + +namespace FdoSecrets +{ + class DBusMgr; + class CipherPair; + + /** + * Represent a client that has made requests to our service. A client is identified by its + * DBus address, which is guaranteed to be unique by the DBus protocol. + * + * An object of this class is created on the first request and destroyed + * when the client address vanishes from the bus. DBus guarantees that the + * client address is not reused. + * + * One client may have multiple `Session`s with our service, and this class + * manages the negotiation state (if any) of ciphers and per-client authorization + * status. + */ + class DBusClient + { + public: + /** + * @brief Given peer's service address, construct a client object + * @param address obtained from `QDBusMessage::service()` + * @param pid the process PID + * @param name the process name + */ + explicit DBusClient(DBusMgr* dbus, const QString& address, uint pid, const QString& name); + + DBusMgr* dbus() const + { + return m_dbus; + } + + /** + * @return The human readable client name, usually the process name + */ + QString name() const + { + return m_name; + } + + /** + * @return The unique DBus address of the client + */ + QString address() const + { + return m_address; + } + + /** + * @return The process id of the client + */ + uint pid() const + { + return m_pid; + } + + QSharedPointer + negotiateCipher(const QString& algorithm, const QVariant& input, QVariant& output, bool& incomplete); + + /** + * Check if the item is known in this client's auth list + */ + bool itemKnown(const QUuid& uuid) const; + + /** + * Check if client may access item identified by @a uuid. + */ + bool itemAuthorized(const QUuid& uuid) const; + + /** + * Check if client may access item identified by @a uuid, and also reset any once auth. + */ + bool itemAuthorizedResetOnce(const QUuid& uuid); + + /** + * Authorize client to access item identified by @a uuid. + */ + void setItemAuthorized(const QUuid& uuid, AuthDecision auth); + + /** + * Authorize client to access all items. + */ + void setAllAuthorized(bool authorized = true); + + /** + * Forget all previous authorization. + */ + void clearAuthorization(); + + /** + * Forcefully disconnect the client. + * Force close any remaining session, and cleanup negotiation states + */ + void disconnectDBus(); + + private: + QPointer m_dbus; + QString m_address; + + uint m_pid{0}; + QString m_name{}; + + bool m_authorizedAll{false}; + + QSet m_allowed{}; + QSet m_denied{}; + + QSet m_allowedOnce{}; + QSet m_deniedOnce{}; + }; + + using DBusClientPtr = QSharedPointer; +} // namespace FdoSecrets +Q_DECLARE_METATYPE(FdoSecrets::DBusClientPtr); + +#endif // KEEPASSXC_FDOSECRETS_DBUSCLIENT_H diff --git a/src/fdosecrets/dbus/DBusConstants.h b/src/fdosecrets/dbus/DBusConstants.h new file mode 100644 index 000000000..74c13d721 --- /dev/null +++ b/src/fdosecrets/dbus/DBusConstants.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 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_DBUSCONSTANTS_H +#define KEEPASSXC_FDOSECRETS_DBUSCONSTANTS_H + +#include + +static const auto DBUS_SERVICE_SECRET = QStringLiteral("org.freedesktop.secrets"); + +#define DBUS_INTERFACE_SECRET_SERVICE_LITERAL "org.freedesktop.Secret.Service" +#define DBUS_INTERFACE_SECRET_SESSION_LITERAL "org.freedesktop.Secret.Session" +#define DBUS_INTERFACE_SECRET_COLLECTION_LITERAL "org.freedesktop.Secret.Collection" +#define DBUS_INTERFACE_SECRET_ITEM_LITERAL "org.freedesktop.Secret.Item" +#define DBUS_INTERFACE_SECRET_PROMPT_LITERAL "org.freedesktop.Secret.Prompt" + +static const auto DBUS_INTERFACE_SECRET_SERVICE = QStringLiteral(DBUS_INTERFACE_SECRET_SERVICE_LITERAL); +static const auto DBUS_INTERFACE_SECRET_SESSION = QStringLiteral(DBUS_INTERFACE_SECRET_SESSION_LITERAL); +static const auto DBUS_INTERFACE_SECRET_COLLECTION = QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION_LITERAL); +static const auto DBUS_INTERFACE_SECRET_ITEM = QStringLiteral(DBUS_INTERFACE_SECRET_ITEM_LITERAL); +static const auto DBUS_INTERFACE_SECRET_PROMPT = QStringLiteral(DBUS_INTERFACE_SECRET_PROMPT_LITERAL); + +static const auto DBUS_ERROR_SECRET_NO_SESSION = QStringLiteral("org.freedesktop.Secret.Error.NoSession"); +static const auto DBUS_ERROR_SECRET_NO_SUCH_OBJECT = QStringLiteral("org.freedesktop.Secret.Error.NoSuchObject"); +static const auto DBUS_ERROR_SECRET_IS_LOCKED = QStringLiteral("org.freedesktop.Secret.Error.IsLocked"); + +static const auto DBUS_PATH_SECRETS = QStringLiteral("/org/freedesktop/secrets"); + +static const auto DBUS_PATH_TEMPLATE_ALIAS = QStringLiteral("%1/aliases/%2"); +static const auto DBUS_PATH_TEMPLATE_SESSION = QStringLiteral("%1/session/%2"); +static const auto DBUS_PATH_TEMPLATE_COLLECTION = QStringLiteral("%1/collection/%2"); +static const auto DBUS_PATH_TEMPLATE_ITEM = QStringLiteral("%1/%2"); +static const auto DBUS_PATH_TEMPLATE_PROMPT = QStringLiteral("%1/prompt/%2"); + +#endif // KEEPASSXC_FDOSECRETS_DBUSCONSTANTS_H diff --git a/src/fdosecrets/dbus/DBusDispatch.cpp b/src/fdosecrets/dbus/DBusDispatch.cpp new file mode 100644 index 000000000..eecce574c --- /dev/null +++ b/src/fdosecrets/dbus/DBusDispatch.cpp @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2020 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 "DBusMgr.h" + +#include "fdosecrets/dbus/DBusObject.h" +#include "fdosecrets/dbus/DBusTypes.h" +#include "fdosecrets/objects/Item.h" +#include "fdosecrets/objects/Service.h" + +#include "core/Global.h" + +#include +#include + +namespace FdoSecrets +{ + QString camelToPascal(const QString& camel) + { + if (camel.isEmpty()) { + return camel; + } + return camel.at(0).toUpper() + camel.mid(1); + } + + bool prepareInputParams(const QVector& inputTypes, + const QVariantList& args, + QVarLengthArray& params, + QVariantList& auxParams) + { + // prepare params + for (int count = 0; count != inputTypes.size(); ++count) { + const auto& id = inputTypes.at(count); + const auto& arg = args.at(count); + + if (arg.userType() == id) { + // shortcut for no conversion + params.append(const_cast(arg.constData())); + continue; + } + + // we need at least one conversion, allocate a slot in auxParams + auxParams.append(QVariant(id, nullptr)); + auto& out = auxParams.last(); + // first handle QDBusArgument to wire types + if (arg.userType() == qMetaTypeId()) { + auto wireId = typeToWireType(id).dbusTypeId; + out = QVariant(wireId, nullptr); + + const auto& in = arg.value(); + if (!QDBusMetaType::demarshall(in, wireId, out.data())) { + qDebug() << "Internal error: failed QDBusArgument conversion from" << arg << "to type" + << QMetaType::typeName(wireId) << wireId; + return false; + } + } else { + // make a copy to store the converted value + out = arg; + } + // other conversions are handled here + if (!out.convert(id)) { + qDebug() << "Internal error: failed conversion from" << arg << "to type" << QMetaType::typeName(id) + << id; + return false; + } + // good to go + params.append(const_cast(out.constData())); + } + return true; + } + + void DBusMgr::populateMethodCache(const QMetaObject& mo) + { + for (int i = mo.methodOffset(); i != mo.methodCount(); ++i) { + auto mm = mo.method(i); + + // only register public Q_INVOKABLE methods + if (mm.access() != QMetaMethod::Public || mm.methodType() != QMetaMethod::Method) { + continue; + } + if (mm.returnType() != qMetaTypeId()) { + continue; + } + + auto iface = mo.classInfo(mo.indexOfClassInfo("D-Bus Interface")).value(); + if (!iface) { + continue; + } + + // map from function name to dbus name + auto member = camelToPascal(mm.name()); + // also "remove" => "Delete" due to c++ keyword restriction + if (member == "Remove") { + member = QStringLiteral("Delete"); + } + auto cacheKey = QStringLiteral("%1.%2").arg(iface, member); + + // skip if we already have it + auto it = m_cachedMethods.find(cacheKey); + if (it != m_cachedMethods.end()) { + continue; + } + + MethodData md; + md.isProperty = mm.tag() && mm.tag() == QStringLiteral("DBUS_PROPERTY"); + md.slotIdx = mm.methodIndex(); + + bool valid = true; + // assumes output params (reference parameter) all follows input params + bool outputBegin = false; + for (const auto& paramType : mm.parameterTypes()) { + auto id = QMetaType::type(paramType); + + // handle the first optional calling client param + if (id == qMetaTypeId()) { + md.needsCallingClient = true; + continue; + } + + // handle output types + if (paramType.endsWith('&')) { + outputBegin = true; + id = QMetaType::type(paramType.left(paramType.length() - 1)); + md.outputTypes.append(id); + auto paramData = typeToWireType(id); + if (paramData.signature.isEmpty()) { + qDebug() << "Internal error: unhandled new output type for dbus signature" << paramType; + valid = false; + break; + } + md.outputTargetTypes.append(paramData.dbusTypeId); + continue; + } + + // handle input types + if (outputBegin) { + qDebug() << "Internal error: invalid method parameter order, no input parameter after output ones" + << mm.name(); + valid = false; + break; + } + auto sig = typeToWireType(id).signature; + if (sig.isEmpty()) { + qDebug() << "Internal error: unhandled new parameter type for dbus signature" << paramType; + valid = false; + break; + } + md.inputTypes.append(id); + md.signature += sig; + } + if (valid) { + m_cachedMethods.insert(cacheKey, md); + } + } + } + + bool DBusMgr::handleMessage(const QDBusMessage& message, const QDBusConnection&) + { + // save a mutable copy of the message, as we may modify it to unify property access + // and method call + RequestedMethod req{ + message.interface(), + message.member(), + message.signature(), + message.arguments(), + RequestType::Method, + }; + + if (req.interface == "org.freedesktop.DBus.Introspectable") { + // introspection can be handled by Qt, just return false + return false; + } else if (req.interface == "org.freedesktop.DBus.Properties") { + // but we need to handle properties ourselves like regular functions + if (!rewriteRequestForProperty(req)) { + // invalid message + qDebug() << "Invalid message" << message; + return false; + } + } + + // who's calling? + const auto& client = findClient(message.service()); + if (!client) { + // the client already died + return false; + } + + // activate the target object + return activateObject(client, message.path(), req, message); + } + + bool DBusMgr::rewriteRequestForProperty(RequestedMethod& req) + { + if (req.member == "Set" && req.signature == "ssv") { + // convert to normal method call: SetName + req.interface = req.args.at(0).toString(); + req.member = req.member + req.args.at(1).toString(); + // unwrap the QDBusVariant and expose the inner signature + auto arg = req.args.last().value().variant(); + req.args = {arg}; + if (arg.userType() == qMetaTypeId()) { + req.signature = arg.value().currentSignature(); + } else if (arg.userType() == QMetaType::QString) { + req.signature = "s"; + } else { + qDebug() << "Unhandled SetProperty value type" << QMetaType::typeName(arg.userType()) << arg.userType(); + return false; + } + } else if (req.member == "Get" && req.signature == "ss") { + // convert to normal method call: Name + req.interface = req.args.at(0).toString(); + req.member = req.args.at(1).toString(); + req.signature = ""; + req.args = {}; + req.type = RequestType::PropertyGet; + } else if (req.member == "GetAll" && req.signature == "s") { + // special handled in activateObject + req.interface = req.args.at(0).toString(); + req.member = ""; + req.signature = ""; + req.args = {}; + req.type = RequestType::PropertyGetAll; + } else { + return false; + } + return true; + } + + bool DBusMgr::activateObject(const DBusClientPtr& client, + const QString& path, + const RequestedMethod& req, + const QDBusMessage& msg) + { + auto obj = m_objects.value(path, nullptr); + if (!obj) { + qDebug() << "DBusMgr::handleMessage with unknown path" << msg; + return false; + } + Q_ASSERT_X(QThread::currentThread() == obj->thread(), + "QDBusConnection: internal threading error", + "function called for an object that is in another thread!!"); + + auto mo = obj->metaObject(); + // either interface matches, or interface is empty if req is property get all + QString interface = mo->classInfo(mo->indexOfClassInfo("D-Bus Interface")).value(); + if (req.interface != interface && !(req.type == RequestType::PropertyGetAll && req.interface.isEmpty())) { + qDebug() << "DBusMgr::handleMessage with mismatch interface" << msg; + return false; + } + + // special handle of property getall + if (req.type == RequestType::PropertyGetAll) { + return objectPropertyGetAll(client, obj, interface, msg); + } + + // find the slot to call + auto cacheKey = QStringLiteral("%1.%2").arg(req.interface, req.member); + auto it = m_cachedMethods.find(cacheKey); + if (it == m_cachedMethods.end()) { + qDebug() << "DBusMgr::handleMessage with nonexisting method" << cacheKey; + return false; + } + + // requested signature is verified by Qt to match the content of arguments, + // but this list of arguments itself is untrusted + if (it->signature != req.signature || it->inputTypes.size() != req.args.size()) { + qDebug() << "Message signature does not match, expected" << it->signature << it->inputTypes.size() << "got" + << req.signature << req.args.size(); + return false; + } + + DBusResult ret; + QVariantList outputArgs; + if (!deliverMethod(client, obj, *it, req.args, ret, outputArgs)) { + qDebug() << "Failed to deliver method" << msg; + return sendDBus(msg.createErrorReply(QDBusError::InternalError, tr("Failed to deliver message"))); + } + + if (!ret.ok()) { + return sendDBus(msg.createErrorReply(ret, "")); + } + if (req.type == RequestType::PropertyGet) { + // property get need the reply wrapped in QDBusVariant + outputArgs[0] = QVariant::fromValue(QDBusVariant(outputArgs.first())); + } + return sendDBus(msg.createReply(outputArgs)); + } + + bool DBusMgr::objectPropertyGetAll(const DBusClientPtr& client, + DBusObject* obj, + const QString& interface, + const QDBusMessage& msg) + { + QVariantMap result; + + // prefix match the cacheKey + auto prefix = interface + "."; + for (auto it = m_cachedMethods.constBegin(); it != m_cachedMethods.constEnd(); ++it) { + if (!it.key().startsWith(prefix)) { + continue; + } + if (!it.value().isProperty) { + continue; + } + auto name = it.key().mid(prefix.size()); + + DBusResult ret; + QVariantList outputArgs; + if (!deliverMethod(client, obj, it.value(), {}, ret, outputArgs)) { + // ignore any error per spec + continue; + } + if (ret.err()) { + // ignore any error per spec + continue; + } + Q_ASSERT(outputArgs.size() == 1); + + result.insert(name, outputArgs.first()); + } + + return sendDBus(msg.createReply(QVariantList{result})); + } + + bool DBusMgr::deliverMethod(const DBusClientPtr& client, + DBusObject* obj, + const MethodData& method, + const QVariantList& args, + DBusResult& ret, + QVariantList& outputArgs) + { + QVarLengthArray params; + QVariantList auxParams; + + // the first one is for return type + params.append(&ret); + + if (method.needsCallingClient) { + auxParams.append(QVariant::fromValue(client)); + params.append(const_cast(auxParams.last().constData())); + } + + // prepare input + if (!prepareInputParams(method.inputTypes, args, params, auxParams)) { + qDebug() << "Failed to prepare input params"; + return false; + } + + // prepare output args + outputArgs.reserve(outputArgs.size() + method.outputTypes.size()); + for (const auto& outputType : asConst(method.outputTypes)) { + outputArgs.append(QVariant(outputType, nullptr)); + params.append(const_cast(outputArgs.last().constData())); + } + + // call it + bool fail = obj->qt_metacall(QMetaObject::InvokeMetaMethod, method.slotIdx, params.data()) >= 0; + if (fail) { + // generate internal error + qWarning() << "Internal error: Failed to deliver message"; + return false; + } + + if (!ret.ok()) { + // error reply + return true; + } + + // output args need to be converted before they can be directly sent out: + for (int i = 0; i != outputArgs.size(); ++i) { + auto& outputArg = outputArgs[i]; + if (!outputArg.convert(method.outputTargetTypes.at(i))) { + qWarning() << "Internal error: Failed to convert message output to type" + << method.outputTargetTypes.at(i); + return false; + } + } + + return true; + } +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusMgr.cpp b/src/fdosecrets/dbus/DBusMgr.cpp new file mode 100644 index 000000000..cd44ce2f9 --- /dev/null +++ b/src/fdosecrets/dbus/DBusMgr.cpp @@ -0,0 +1,623 @@ +/* + * Copyright (C) 2020 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 "DBusMgr.h" + +#include "fdosecrets/dbus/DBusConstants.h" +#include "fdosecrets/dbus/DBusTypes.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" + +#include "core/Entry.h" +#include "core/Tools.h" + +#include +#include +#include + +namespace FdoSecrets +{ + static const auto IntrospectionService = R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)xml"; + + static const auto IntrospectionCollection = R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)xml"; + + static const auto IntrospectionItem = R"xml( + + + + + + + + + + + + + + + + + + + + + +)xml"; + + static const auto IntrospectionSession = R"xml( + + + + +)xml"; + + static const auto IntrospectionPrompt = R"xml( + + + + + + + + + + + +)xml"; + + DBusMgr::DBusMgr() + : m_conn(QDBusConnection::sessionBus()) + { + // remove client when it disappears on the bus + m_watcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(&m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, &DBusMgr::dbusServiceUnregistered); + m_watcher.setConnection(m_conn); + } + + void DBusMgr::populateMethodCache() + { + // these are the methods we expose on DBus + populateMethodCache(Service::staticMetaObject); + populateMethodCache(Collection::staticMetaObject); + populateMethodCache(Item::staticMetaObject); + populateMethodCache(PromptBase::staticMetaObject); + populateMethodCache(Session::staticMetaObject); + } + + DBusMgr::~DBusMgr() = default; + + void DBusMgr::overrideClient(const DBusClientPtr& fake) + { + m_overrideClient = fake; + } + + QList DBusMgr::clients() const + { + return m_clients.values(); + } + + bool DBusMgr::serviceInfo(const QString& addr, ProcessInfo& info) const + { + auto pid = m_conn.interface()->servicePid(addr); + if (!pid.isValid()) { + return false; + } + info.pid = pid.value(); + // The /proc/pid/exe link is more reliable than /proc/pid/cmdline + // It's still weak and if the application does a prctl(PR_SET_DUMPABLE, 0) this link cannot be accessed. + QFileInfo proc(QStringLiteral("/proc/%1/exe").arg(pid.value())); + info.exePath = proc.canonicalFilePath(); + + return true; + } + + bool DBusMgr::sendDBusSignal(const QString& path, + const QString& interface, + const QString& name, + const QVariantList& arguments) + { + auto msg = QDBusMessage::createSignal(path, interface, name); + msg.setArguments(arguments); + return sendDBus(msg); + } + + bool DBusMgr::sendDBus(const QDBusMessage& reply) + { + bool ok = m_conn.send(reply); + if (!ok) { + qDebug() << "Failed to send on DBus:" << reply; + emit error(tr("Failed to send reply on DBus")); + } + return ok; + } + + // `this` object is registered at multiple paths: + // /org/freedesktop/secrets + // /org/freedesktop/secrets/collection/xxx + // /org/freedesktop/secrets/collection/xxx/yyy + // /org/freedesktop/secrets/aliases/xxx + // /org/freedesktop/secrets/session/xxx + // /org/freedesktop/secrets/prompt/xxx + // + // The path validation is left to Qt, this method only do the minimum + // required to differentiate the paths. + DBusMgr::ParsedPath DBusMgr::parsePath(const QString& path) + { + Q_ASSERT(path.startsWith('/')); + Q_ASSERT(path == "/" || !path.endsWith('/')); + + static const QString DBusPathSecrets = DBUS_PATH_SECRETS; + + if (!path.startsWith(DBusPathSecrets)) { + return ParsedPath{}; + } + auto parts = path.mid(DBusPathSecrets.size()).split('/'); + // the first part is always empty + if (parts.isEmpty() || parts.first() != "") { + return ParsedPath{}; + } + parts.takeFirst(); + + if (parts.isEmpty()) { + return ParsedPath{PathType::Service}; + } else if (parts.size() == 2) { + if (parts.at(0) == "collection") { + return ParsedPath{PathType::Collection, parts.at(1)}; + } else if (parts.at(0) == "aliases") { + return ParsedPath{PathType::Aliases, parts.at(1)}; + } else if (parts.at(0) == "prompt") { + return ParsedPath{PathType::Prompt, parts.at(1)}; + } else if (parts.at(0) == "session") { + return ParsedPath{PathType::Session, parts.at(1)}; + } + } else if (parts.size() == 3) { + if (parts.at(0) == "collection") { + return ParsedPath{PathType::Item, parts.at(2), parts.at(1)}; + } + } + return ParsedPath{}; + } + + QString DBusMgr::introspect(const QString& path) const + { + auto parsed = parsePath(path); + switch (parsed.type) { + case PathType::Service: + return IntrospectionService; + case PathType::Collection: + case PathType::Aliases: + return IntrospectionCollection; + case PathType::Prompt: + return IntrospectionPrompt; + case PathType::Session: + return IntrospectionSession; + case PathType::Item: + return IntrospectionItem; + case PathType::Unknown: + default: + return ""; + } + } + + bool DBusMgr::serviceOccupied() const + { + auto reply = m_conn.interface()->isServiceRegistered(DBUS_SERVICE_SECRET); + if (!reply.isValid()) { + return false; + } + if (reply.value()) { + auto pid = m_conn.interface()->servicePid(DBUS_SERVICE_SECRET); + if (pid.isValid() && pid.value() != qApp->applicationPid()) { + return true; + } + } + return false; + } + + QString DBusMgr::reportExistingService() const + { + auto pidStr = tr("Unknown", "Unknown PID"); + auto exeStr = tr("Unknown", "Unknown executable path"); + + ProcessInfo info{}; + if (serviceInfo(DBUS_SERVICE_SECRET, info)) { + pidStr = QString::number(info.pid); + if (!info.exePath.isEmpty()) { + exeStr = info.exePath; + } + } + + auto otherService = tr("PID: %1, Executable: %2", "PID: 1234, Executable: /path/to/exe") + .arg(pidStr, exeStr.toHtmlEscaped()); + return tr("Another secret service is running (%1).
" + "Please stop/remove it before re-enabling the Secret Service Integration.") + .arg(otherService); + } + + bool DBusMgr::registerObject(const QString& path, DBusObject* obj, bool primary) + { + if (!m_conn.registerVirtualObject(path, this)) { + qDebug() << "failed to register" << obj << "at" << path; + return false; + } + connect(obj, &DBusObject::destroyed, this, &DBusMgr::unregisterObject); + m_objects.insert(path, obj); + if (primary) { + obj->setObjectPath(path); + } + return true; + } + + bool DBusMgr::registerObject(Service* service) + { + if (!m_conn.registerService(DBUS_SERVICE_SECRET)) { + const auto existing = reportExistingService(); + qDebug() << "Failed to register DBus service at " << DBUS_SERVICE_SECRET; + qDebug() << existing; + emit error(tr("Failed to register DBus service at %1.
").arg(DBUS_SERVICE_SECRET) + existing); + return false; + } + connect(service, &DBusObject::destroyed, this, [this]() { m_conn.unregisterService(DBUS_SERVICE_SECRET); }); + + if (!registerObject(DBUS_PATH_SECRETS, service)) { + qDebug() << "Failed to register service on DBus at path" << DBUS_PATH_SECRETS; + emit error(tr("Failed to register service on DBus at path '%1'").arg(DBUS_PATH_SECRETS)); + return false; + } + + connect(service, &Service::collectionCreated, this, &DBusMgr::emitCollectionCreated); + connect(service, &Service::collectionChanged, this, &DBusMgr::emitCollectionChanged); + connect(service, &Service::collectionDeleted, this, &DBusMgr::emitCollectionDeleted); + + return true; + } + + bool DBusMgr::registerObject(Collection* coll) + { + auto name = encodePath(coll->name()); + auto path = DBUS_PATH_TEMPLATE_COLLECTION.arg(DBUS_PATH_SECRETS, name); + if (!registerObject(path, coll)) { + // try again with a suffix + name.append(QString("_%1").arg(Tools::uuidToHex(QUuid::createUuid()).left(4))); + path = DBUS_PATH_TEMPLATE_COLLECTION.arg(DBUS_PATH_SECRETS, name); + + if (!registerObject(path, coll)) { + qDebug() << "Failed to register database on DBus under name" << name; + emit error(tr("Failed to register database on DBus under the name '%1'").arg(name)); + return false; + } + } + + connect(coll, &Collection::itemCreated, this, &DBusMgr::emitItemCreated); + connect(coll, &Collection::itemChanged, this, &DBusMgr::emitItemChanged); + connect(coll, &Collection::itemDeleted, this, &DBusMgr::emitItemDeleted); + + return true; + } + + bool DBusMgr::registerObject(Session* sess) + { + auto path = DBUS_PATH_TEMPLATE_SESSION.arg(DBUS_PATH_SECRETS, sess->id()); + if (!registerObject(path, sess)) { + emit error(tr("Failed to register session on DBus at path '%1'").arg(path)); + return false; + } + return true; + } + + bool DBusMgr::registerObject(Item* item) + { + auto path = DBUS_PATH_TEMPLATE_ITEM.arg(item->collection()->objectPath().path(), item->backend()->uuidToHex()); + if (!registerObject(path, item)) { + emit error(tr("Failed to register item on DBus at path '%1'").arg(path)); + return false; + } + return true; + } + + bool DBusMgr::registerObject(PromptBase* prompt) + { + auto path = DBUS_PATH_TEMPLATE_PROMPT.arg(DBUS_PATH_SECRETS, Tools::uuidToHex(QUuid::createUuid())); + if (!registerObject(path, prompt)) { + emit error(tr("Failed to register prompt object on DBus at path '%1'").arg(path)); + return false; + } + + connect(prompt, &PromptBase::completed, this, &DBusMgr::emitPromptCompleted); + + return true; + } + + void DBusMgr::unregisterObject(DBusObject* obj) + { + auto count = m_objects.remove(obj->objectPath().path()); + if (count > 0) { + m_conn.unregisterObject(obj->objectPath().path()); + obj->setObjectPath("/"); + } + } + + bool DBusMgr::registerAlias(Collection* coll, const QString& alias) + { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + if (!registerObject(path, coll, false)) { + qDebug() << "Failed to register database on DBus under alias" << alias; + // usually this is reported back directly on dbus, so no need to show in UI + return false; + } + // alias signals are handled together with collections' primary path in emitCollection* + // but we need to handle object destroy here + connect(coll, &DBusObject::destroyed, this, [this, alias]() { unregisterAlias(alias); }); + return true; + } + + void DBusMgr::unregisterAlias(const QString& alias) + { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + // DBusMgr::unregisterObject only handles primary path + m_objects.remove(path); + m_conn.unregisterObject(path); + } + + void DBusMgr::emitCollectionCreated(Collection* coll) + { + QVariantList args; + args += QVariant::fromValue(coll->objectPath()); + sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, QStringLiteral("CollectionCreated"), args); + } + + void DBusMgr::emitCollectionChanged(Collection* coll) + { + QVariantList args; + args += QVariant::fromValue(coll->objectPath()); + sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, "CollectionChanged", args); + } + + void DBusMgr::emitCollectionDeleted(Collection* coll) + { + QVariantList args; + args += QVariant::fromValue(coll->objectPath()); + sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, QStringLiteral("CollectionDeleted"), args); + } + + void DBusMgr::emitItemCreated(Item* item) + { + auto coll = item->collection(); + QVariantList args; + args += QVariant::fromValue(item->objectPath()); + // send on primary path + sendDBusSignal( + coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemCreated"), args); + // also send on all alias path + for (const auto& alias : coll->aliases()) { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemCreated"), args); + } + } + + void DBusMgr::emitItemChanged(Item* item) + { + auto coll = item->collection(); + QVariantList args; + args += QVariant::fromValue(item->objectPath()); + // send on primary path + sendDBusSignal( + coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemChanged"), args); + // also send on all alias path + for (const auto& alias : coll->aliases()) { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemChanged"), args); + } + } + + void DBusMgr::emitItemDeleted(Item* item) + { + auto coll = item->collection(); + QVariantList args; + args += QVariant::fromValue(item->objectPath()); + // send on primary path + sendDBusSignal( + coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemDeleted"), args); + // also send on all alias path + for (const auto& alias : coll->aliases()) { + auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias); + sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemDeleted"), args); + } + } + + void DBusMgr::emitPromptCompleted(bool dismissed, QVariant result) + { + auto prompt = qobject_cast(sender()); + if (!prompt) { + qDebug() << "Wrong sender in emitPromptCompleted"; + return; + } + + // make sure the result contains a valid value, otherwise QDBusVariant refuses to marshall it. + if (!result.isValid()) { + result = QString{}; + } + + QVariantList args; + args += QVariant::fromValue(dismissed); + args += QVariant::fromValue(QDBusVariant(result)); + sendDBusSignal(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT, QStringLiteral("Completed"), args); + } + + DBusClientPtr DBusMgr::findClient(const QString& addr) + { + if (m_overrideClient) { + return m_overrideClient; + } + + auto it = m_clients.find(addr); + if (it == m_clients.end()) { + auto client = createClient(addr); + if (!client) { + return {}; + } + it = m_clients.insert(addr, client); + } + // double check the client + ProcessInfo info{}; + if (!serviceInfo(addr, info) || info.pid != it.value()->pid()) { + dbusServiceUnregistered(addr); + return {}; + } + return it.value(); + } + + DBusClientPtr DBusMgr::createClient(const QString& addr) + { + ProcessInfo info{}; + if (!serviceInfo(addr, info)) { + return {}; + } + + auto client = DBusClientPtr(new DBusClient(this, addr, info.pid, info.exePath.isEmpty() ? addr : info.exePath)); + + emit clientConnected(client); + m_watcher.addWatchedService(addr); + + return client; + } + + void DBusMgr::removeClient(DBusClient* client) + { + if (!client) { + return; + } + + auto it = m_clients.find(client->address()); + if (it == m_clients.end()) { + return; + } + + emit clientDisconnected(*it); + m_clients.erase(it); + } + + void DBusMgr::dbusServiceUnregistered(const QString& service) + { + auto removed = m_watcher.removeWatchedService(service); + if (!removed) { + qDebug("FdoSecrets: Failed to remove service watcher"); + } + + auto it = m_clients.find(service); + if (it == m_clients.end()) { + return; + } + auto client = it.value(); + + client->disconnectDBus(); + } +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusMgr.h b/src/fdosecrets/dbus/DBusMgr.h new file mode 100644 index 000000000..ce4a88fc5 --- /dev/null +++ b/src/fdosecrets/dbus/DBusMgr.h @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2020 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_DBUSMGR_H +#define KEEPASSXC_FDOSECRETS_DBUSMGR_H + +#include "fdosecrets/dbus/DBusClient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class TestFdoSecrets; + +namespace FdoSecrets +{ + class Collection; + class Service; + class PromptBase; + class Session; + class Item; + class DBusObject; + class DBusResult; + + /** + * DBusMgr takes care of the interaction between dbus and business logic objects (DBusObject). It handles the + * following + * - Registering/unregistering service name + * - Registering/unregistering paths + * - Relay signals from DBusObject to dbus + * - Manage per-client states, mapping from dbus caller address to Client + * - Deliver method calls from dbus to DBusObject + * + * Special note in implementation of method delivery: + * There are two sets of vocabulary classes in use for method delivery. + * The Qt DBus system uses QDBusVariant/QDBusObjectPath and other primitive types in QDBusMessage::arguments(), + * i.e. the on-the-wire types. + * The DBusObject invokable methods uses QVariant/DBusObject* and other primitive types in parameters (parameter + * types). FdoSecrets::typeToWireType establishes the mapping from parameter types to on-the-wire types. The + * conversion between types is done with the help of QMetaType convert. + * + * The method delivery sequence: + * - DBusMgr::handleMessage unifies method call and property access into the same form + * - DBusMgr::activateObject finds the target object and calls the method by doing the following + * * check the object exists and the interface matches + * * find the cached method information MethodData + * * DBusMgr::prepareInputParams check and convert input arguments in QDBusMessage::arguments() to types expected + * by DBusObject + * * prepare output argument storage + * * call the method + * * convert types to what Qt DBus expects + * + * The MethodData is pre-computed using Qt meta object system by finding methods with signature matching a certain + * pattern: + * Q_INVOKABLE DBusResult methodName(const DBusClientPtr& client, + * const X& input1, + * const Y& input2, + * Z& output1, + * ZZ& output2) + * Note that the first parameter of client is optional. + */ + class DBusMgr : public QDBusVirtualObject + { + Q_OBJECT + public: + explicit DBusMgr(); + + /** + * @brief Must be called after all dbus types are registered + */ + void populateMethodCache(); + + ~DBusMgr() override; + + QString introspect(const QString& path) const override; + bool handleMessage(const QDBusMessage& message, const QDBusConnection& connection) override; + + /** + * @return current connected clients + */ + QList clients() const; + + /** + * @return whether the org.freedesktop.secrets service is owned by others + */ + bool serviceOccupied() const; + + /** + * Check the running secret service and return info about it + * @return html string suitable to be shown in the UI + */ + QString reportExistingService() const; + + // expose on dbus and handle signals + bool registerObject(Service* service); + bool registerObject(Collection* coll); + bool registerObject(Session* sess); + bool registerObject(Item* item); + bool registerObject(PromptBase* prompt); + + void unregisterObject(DBusObject* obj); + + // and the signals are handled together with collection's primary path + bool registerAlias(Collection* coll, const QString& alias); + void unregisterAlias(const QString& alias); + + /** + * Return the object path of the pointed DBusObject, or "/" if the pointer is null + * @tparam T + * @param object + * @return + */ + template static QDBusObjectPath objectPathSafe(T* object) + { + if (object) { + return object->objectPath(); + } + return QDBusObjectPath(QStringLiteral("/")); + } + template static QDBusObjectPath objectPathSafe(QPointer object) + { + return objectPathSafe(object.data()); + } + static QDBusObjectPath objectPathSafe(std::nullptr_t) + { + return QDBusObjectPath(QStringLiteral("/")); + } + + /** + * Convert a list of DBusObjects to object path + * @tparam T + * @param objects + * @return + */ + template static 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) const + { + if (path.path() == QStringLiteral("/")) { + return nullptr; + } + auto obj = qobject_cast(m_objects.value(path.path(), nullptr)); + if (!obj) { + qDebug() << "object not found at path" << path.path(); + qDebug() << m_objects; + } + return obj; + } + + /** + * 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) const + { + QList res; + res.reserve(paths.size()); + for (const auto& path : paths) { + auto object = pathToObject(path); + if (object) { + res.append(object); + } + } + return res; + } + + // Force client to be a specific object, used for testing + void overrideClient(const DBusClientPtr& fake); + + signals: + void clientConnected(const DBusClientPtr& client); + void clientDisconnected(const DBusClientPtr& client); + void error(const QString& msg); + + private slots: + void emitCollectionCreated(Collection* coll); + void emitCollectionChanged(Collection* coll); + void emitCollectionDeleted(Collection* coll); + void emitItemCreated(Item* item); + void emitItemChanged(Item* item); + void emitItemDeleted(Item* item); + void emitPromptCompleted(bool dismissed, QVariant result); + + void dbusServiceUnregistered(const QString& service); + + private: + QDBusConnection m_conn; + + struct ProcessInfo + { + uint pid; + QString exePath; + }; + bool serviceInfo(const QString& addr, ProcessInfo& info) const; + + bool sendDBusSignal(const QString& path, + const QString& interface, + const QString& name, + const QVariantList& arguments); + bool sendDBus(const QDBusMessage& reply); + + // object path registration + QHash> m_objects{}; + enum class PathType + { + Service, + Collection, + Aliases, + Prompt, + Session, + Item, + Unknown, + }; + struct ParsedPath + { + PathType type; + QString id; + // only used when type == Item + QString parentId; + explicit ParsedPath(PathType type = PathType::Unknown, QString id = "", QString parentId = "") + : type(type) + , id(std::move(id)) + , parentId(std::move(parentId)) + { + } + }; + static ParsedPath parsePath(const QString& path); + bool registerObject(const QString& path, DBusObject* obj, bool primary = true); + + // method dispatching + struct MethodData + { + int slotIdx{-1}; + QByteArray signature{}; + QVector inputTypes{}; + QVector outputTypes{}; + QVector outputTargetTypes{}; + bool isProperty{false}; + bool needsCallingClient{false}; + }; + QHash m_cachedMethods{}; + void populateMethodCache(const QMetaObject& mo); + + enum class RequestType + { + Method, + PropertyGet, + PropertyGetAll, + }; + struct RequestedMethod + { + QString interface; + QString member; + QString signature; + QVariantList args; + RequestType type; + }; + static bool rewriteRequestForProperty(RequestedMethod& req); + bool activateObject(const DBusClientPtr& client, + const QString& path, + const RequestedMethod& req, + const QDBusMessage& msg); + bool objectPropertyGetAll(const DBusClientPtr& client, + DBusObject* obj, + const QString& interface, + const QDBusMessage& msg); + static bool deliverMethod(const DBusClientPtr& client, + DBusObject* obj, + const MethodData& method, + const QVariantList& args, + DBusResult& ret, + QVariantList& outputArgs); + + // client management + friend class DBusClient; + + DBusClientPtr findClient(const QString& addr); + DBusClientPtr createClient(const QString& addr); + + /** + * @brief This gets called from DBusClient::disconnectDBus + * @param client + */ + void removeClient(DBusClient* client); + + QDBusServiceWatcher m_watcher{}; + // mapping from the unique dbus peer address to client object + QHash m_clients{}; + + DBusClientPtr m_overrideClient; + + friend class ::TestFdoSecrets; + }; +} // namespace FdoSecrets + +#endif // KEEPASSXC_FDOSECRETS_DBUSMGR_H diff --git a/src/fdosecrets/objects/DBusObject.cpp b/src/fdosecrets/dbus/DBusObject.cpp similarity index 69% rename from src/fdosecrets/objects/DBusObject.cpp rename to src/fdosecrets/dbus/DBusObject.cpp index fb7533c1c..63a8df604 100644 --- a/src/fdosecrets/objects/DBusObject.cpp +++ b/src/fdosecrets/dbus/DBusObject.cpp @@ -19,37 +19,32 @@ #include #include -#include #include -#include namespace FdoSecrets { DBusObject::DBusObject(DBusObject* parent) : QObject(parent) - , m_dbusAdaptor(nullptr) + , m_dbus(parent->dbus()) { } - bool DBusObject::registerWithPath(const QString& path, bool primary) + DBusObject::DBusObject(QSharedPointer dbus) + : QObject(nullptr) + , m_objectPath("/") + , m_dbus(std::move(dbus)) { - if (primary) { - m_objectPath.setPath(path); - } - - return QDBusConnection::sessionBus().registerObject(path, this); } - QString DBusObject::callingPeerName() const + DBusObject::~DBusObject() { - 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(); + emit destroyed(this); + } + + void DBusObject::setObjectPath(const QString& path) + { + m_objectPath.setPath(path); } QString encodePath(const QString& value) diff --git a/src/fdosecrets/dbus/DBusObject.h b/src/fdosecrets/dbus/DBusObject.h new file mode 100644 index 000000000..d11778907 --- /dev/null +++ b/src/fdosecrets/dbus/DBusObject.h @@ -0,0 +1,130 @@ +/* + * 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 "DBusConstants.h" +#include "DBusMgr.h" +#include "DBusTypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef Q_MOC_RUN +// define the tag text as empty, so the compiler doesn't see it +#define DBUS_PROPERTY +#endif // #ifndef Q_MOC_RUN + +namespace FdoSecrets +{ + class Service; + + /** + * @brief A common base class for all dbus-exposed objects. + */ + class DBusObject : public QObject + { + Q_OBJECT + public: + ~DBusObject() override; + + const QDBusObjectPath& objectPath() const + { + return m_objectPath; + } + + const QSharedPointer& dbus() const + { + return m_dbus; + } + + signals: + /** + * @brief Necessary because by the time QObject::destroyed is emitted, + * we already lost any info in DBusObject + */ + void destroyed(DBusObject* self); + + protected: + explicit DBusObject(DBusObject* parent); + explicit DBusObject(QSharedPointer dbus); + + private: + friend class DBusMgr; + void setObjectPath(const QString& path); + + QDBusObjectPath m_objectPath; + QSharedPointer m_dbus; + }; + + /** + * @brief A dbus error or not + */ + class DBusResult : public QString + { + public: + DBusResult() = default; + explicit DBusResult(QString error) + : QString(std::move(error)) + { + } + + // Implicitly convert from QDBusError + DBusResult(QDBusError::ErrorType error) // NOLINT(google-explicit-constructor) + : QString(QDBusError::errorString(error)) + { + } + + bool ok() const + { + return isEmpty(); + } + bool err() const + { + return !isEmpty(); + } + void okOrDie() const + { + Q_ASSERT(ok()); + } + }; + + /** + * 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 + +Q_DECLARE_METATYPE(FdoSecrets::DBusResult); + +#endif // KEEPASSXC_FDOSECRETS_DBUSOBJECT_H diff --git a/src/fdosecrets/dbus/DBusTypes.cpp b/src/fdosecrets/dbus/DBusTypes.cpp new file mode 100644 index 000000000..715c95237 --- /dev/null +++ b/src/fdosecrets/dbus/DBusTypes.cpp @@ -0,0 +1,219 @@ +/* + * 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 "fdosecrets/dbus/DBusMgr.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" + +#include + +namespace FdoSecrets +{ + bool inherits(const QMetaObject* derived, const QMetaObject* base) + { + for (auto super = derived; super; super = super->superClass()) { + if (super == base) { + return true; + } + } + return false; + } + + template void registerConverter(const QWeakPointer& weak) + { + // from parameter type to on-the-wire type + QMetaType::registerConverter([](const T* obj) { return DBusMgr::objectPathSafe(obj); }); + QMetaType::registerConverter, QList>( + [](const QList objs) { return DBusMgr::objectsToPath(objs); }); + + // the opposite + QMetaType::registerConverter([weak](const QDBusObjectPath& path) -> T* { + if (auto dbus = weak.lock()) { + return dbus->pathToObject(path); + } + qDebug() << "No DBusMgr when looking up path" << path.path(); + return nullptr; + }); + QMetaType::registerConverter, QList>([weak](const QList& paths) { + if (auto dbus = weak.lock()) { + return dbus->pathsToObject(paths); + } + qDebug() << "No DBusMgr when looking up paths"; + return QList{}; + }); + } + + void registerDBusTypes(const QSharedPointer& dbus) + { + // On the wire types: + // - various primary types + // - QDBusVariant + // - wire::Secret + // - wire::ObjectPathSecretMap + // - QDBusObjectPath + // - QList + + // Parameter types: + // - various primary types + // - QVariant + // - Secret + // - ObjectSecretMap + // - DBusObject* (and derived classes) + // - QList + + // NOTE: when registering, in additional to the class' fully qualified name, + // the partial-namespace/non-namespace name should also be registered as alias + // otherwise all those usages in Q_INVOKABLE methods without FQN won't be included + // in the meta type system. +#define REG_METATYPE(type) \ + qRegisterMetaType(); \ + qRegisterMetaType(#type) + + // register on-the-wire types + // Qt container types for builtin types don't need registration + REG_METATYPE(wire::Secret); + REG_METATYPE(wire::StringStringMap); + REG_METATYPE(wire::ObjectPathSecretMap); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + // register parameter types + REG_METATYPE(Secret); + REG_METATYPE(StringStringMap); + REG_METATYPE(ItemSecretMap); + REG_METATYPE(DBusResult); + REG_METATYPE(DBusClientPtr); + +#define REG_DBUS_OBJ(name) \ + REG_METATYPE(name*); \ + REG_METATYPE(QList) + REG_DBUS_OBJ(DBusObject); + REG_DBUS_OBJ(Service); + REG_DBUS_OBJ(Collection); + REG_DBUS_OBJ(Item); + REG_DBUS_OBJ(Session); + REG_DBUS_OBJ(PromptBase); +#undef REG_DBUS_OBJ + +#undef REG_METATYPE + + QWeakPointer weak = dbus; + // register converter between on-the-wire types and parameter types + // some pairs are missing because that particular direction isn't used + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + registerConverter(weak); + + QMetaType::registerConverter( + [weak](const wire::Secret& from) { return from.unmarshal(weak); }); + QMetaType::registerConverter(&Secret::marshal); + + QMetaType::registerConverter([](const ItemSecretMap& map) { + wire::ObjectPathSecretMap ret; + for (auto it = map.constBegin(); it != map.constEnd(); ++it) { + ret.insert(it.key()->objectPath(), it.value().marshal()); + } + return ret; + }); + + QMetaType::registerConverter([](const QDBusVariant& obj) { return obj.variant(); }); + QMetaType::registerConverter([](const QVariant& obj) { return QDBusVariant(obj); }); + + // structural types are received as QDBusArgument, + // top level QDBusArgument in method parameters are directly handled + // in prepareInputParams. + // But in Collection::createItem, we need to convert a inner QDBusArgument to StringStringMap + QMetaType::registerConverter([](const QDBusArgument& arg) { + if (arg.currentSignature() != "a{ss}") { + return StringStringMap{}; + } + // QDBusArgument is COW and qdbus_cast modifies it by detaching even it is const. + // we don't want to modify the instance (arg) stored in the qvariant so we create a copy + const auto copy = arg; // NOLINT(performance-unnecessary-copy-initialization) + return qdbus_cast(copy); + }); + } + + ParamData typeToWireType(int id) + { + switch (id) { + case QMetaType::QString: + return {QByteArrayLiteral("s"), QMetaType::QString}; + case QMetaType::QVariant: + return {QByteArrayLiteral("v"), qMetaTypeId()}; + case QMetaType::QVariantMap: + return {QByteArrayLiteral("a{sv}"), QMetaType::QVariantMap}; + case QMetaType::Bool: + return {QByteArrayLiteral("b"), QMetaType::Bool}; + case QMetaType::ULongLong: + return {QByteArrayLiteral("t"), QMetaType::ULongLong}; + default: + break; + } + if (id == qMetaTypeId()) { + return {QByteArrayLiteral("a{ss}"), qMetaTypeId()}; + } else if (id == qMetaTypeId()) { + return {QByteArrayLiteral("a{o(oayays)}"), qMetaTypeId()}; + } else if (id == qMetaTypeId()) { + return {QByteArrayLiteral("(oayays)"), qMetaTypeId()}; + } else if (id == qMetaTypeId()) { + return {QByteArrayLiteral("o"), qMetaTypeId()}; + } else if (id == qMetaTypeId>()) { + return {QByteArrayLiteral("ao"), qMetaTypeId>()}; + } + + QMetaType mt(id); + if (!mt.isValid()) { + return {}; + } + if (QByteArray(QMetaType::typeName(id)).startsWith("QList")) { + // QList + return {QByteArrayLiteral("ao"), qMetaTypeId>()}; + } + if (!inherits(mt.metaObject(), &DBusObject::staticMetaObject)) { + return {}; + } + // DBusObjects + return {QByteArrayLiteral("o"), qMetaTypeId()}; + } + + ::FdoSecrets::Secret wire::Secret::unmarshal(const QWeakPointer& weak) const + { + if (auto dbus = weak.lock()) { + return {dbus->pathToObject(session), parameters, value, contentType}; + } + qDebug() << "No DBusMgr when converting wire::Secret"; + return {nullptr, parameters, value, contentType}; + } + + wire::Secret Secret::marshal() const + { + return {DBusMgr::objectPathSafe(session), parameters, value, contentType}; + } + +} // namespace FdoSecrets diff --git a/src/fdosecrets/dbus/DBusTypes.h b/src/fdosecrets/dbus/DBusTypes.h new file mode 100644 index 000000000..01171e530 --- /dev/null +++ b/src/fdosecrets/dbus/DBusTypes.h @@ -0,0 +1,107 @@ +/* + * 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 + +namespace FdoSecrets +{ + struct Secret; + class DBusMgr; + + // types used directly in Qt DBus system + namespace wire + { + struct Secret + { + QDBusObjectPath session; + QByteArray parameters; + QByteArray value; + QString contentType; + + ::FdoSecrets::Secret unmarshal(const QWeakPointer& weak) const; + }; + + inline QDBusArgument& operator<<(QDBusArgument& argument, const Secret& secret) + { + argument.beginStructure(); + argument << secret.session << secret.parameters << secret.value << secret.contentType; + argument.endStructure(); + return argument; + } + + inline const QDBusArgument& operator>>(const QDBusArgument& argument, Secret& secret) + { + argument.beginStructure(); + argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType; + argument.endStructure(); + return argument; + } + + using StringStringMap = QMap; + using ObjectPathSecretMap = QMap; + } // namespace wire + + // types used in method parameters + class Session; + class Item; + struct Secret + { + const Session* session; + QByteArray parameters; + QByteArray value; + QString contentType; + + wire::Secret marshal() const; + }; + using wire::StringStringMap; + using ItemSecretMap = QHash; + + /** + * Register the types needed for the fd.o Secrets D-Bus interface. + */ + void registerDBusTypes(const QSharedPointer& dbus); + + struct ParamData + { + QByteArray signature; + int dbusTypeId; + }; + + /** + * @brief Convert parameter type to on-the-wire type and associated dbus signature. + * This is NOT a generic version, and only handles types used in org.freedesktop.secrets + * @param id + * @return ParamData + */ + ParamData typeToWireType(int id); +} // namespace FdoSecrets + +Q_DECLARE_METATYPE(FdoSecrets::wire::Secret) +Q_DECLARE_METATYPE(FdoSecrets::wire::StringStringMap); +Q_DECLARE_METATYPE(FdoSecrets::wire::ObjectPathSecretMap); + +Q_DECLARE_METATYPE(FdoSecrets::Secret) + +#endif // KEEPASSXC_FDOSECRETS_DBUSTYPES_H diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp index 0f856d87f..f4341ef81 100644 --- a/src/fdosecrets/objects/Collection.cpp +++ b/src/fdosecrets/objects/Collection.cpp @@ -19,6 +19,7 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Prompt.h" #include "fdosecrets/objects/Service.h" @@ -40,7 +41,7 @@ namespace FdoSecrets } Collection::Collection(Service* parent, DatabaseWidget* backend) - : DBusObjectHelper(parent) + : DBusObject(parent) , m_backend(backend) , m_exposedGroup(nullptr) { @@ -72,23 +73,14 @@ namespace FdoSecrets m_items.first()->doDelete(); } cleanupConnections(); - unregisterPrimaryPath(); + dbus()->unregisterObject(this); // make sure we have updated copy of the filepath, which is used to identify the database. m_backendPath = m_backend->database()->canonicalFilePath(); // register the object, handling potentially duplicated name - auto name = encodePath(this->name()); - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), name); - if (!registerWithPath(path)) { - // try again with a suffix - name += QStringLiteral("_%1").arg(Tools::uuidToHex(QUuid::createUuid()).left(4)); - path = QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), name); - - if (!registerWithPath(path)) { - service()->plugin()->emitError(tr("Failed to register database on DBus under the name '%1'").arg(name)); - return false; - } + if (!dbus()->registerObject(this)) { + return false; } // populate contents after expose on dbus, because items rely on parent's dbus object path @@ -98,6 +90,7 @@ namespace FdoSecrets cleanupConnections(); } + emit collectionChanged(); return true; } @@ -108,52 +101,55 @@ namespace FdoSecrets } } - DBusReturn Collection::ensureBackend() const + DBusResult Collection::ensureBackend() const { if (!m_backend) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } return {}; } - DBusReturn Collection::ensureUnlocked() const + DBusResult Collection::ensureUnlocked() const { if (backendLocked()) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED)); + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); } return {}; } - DBusReturn> Collection::items() const + DBusResult Collection::items(QList& items) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return m_items; + items = m_items; + return {}; } - DBusReturn Collection::label() const + DBusResult Collection::label(QString& label) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } if (backendLocked()) { - return name(); + label = name(); + } else { + label = m_backend->database()->metadata()->name(); } - return m_backend->database()->metadata()->name(); + return {}; } - DBusReturn Collection::setLabel(const QString& label) + DBusResult Collection::setLabel(const QString& label) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -161,82 +157,87 @@ namespace FdoSecrets return {}; } - DBusReturn Collection::locked() const + DBusResult Collection::locked(bool& locked) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return backendLocked(); + locked = backendLocked(); + return {}; } - DBusReturn Collection::created() const + DBusResult Collection::created(qulonglong& created) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return static_cast(m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() - / 1000); + created = static_cast( + m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + + return {}; } - DBusReturn Collection::modified() const + DBusResult Collection::modified(qulonglong& modified) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { 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( + modified = static_cast( m_backend->database()->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + return {}; } - DBusReturn Collection::deleteCollection() + DBusResult Collection::remove(const DBusClientPtr& client, PromptBase*& prompt) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } // Delete means close database - auto dpret = DeleteCollectionPrompt::Create(service(), this); - if (dpret.isError()) { - return dpret; + prompt = PromptBase::Create(service(), this); + if (!prompt) { + return QDBusError::InternalError; } - auto prompt = dpret.value(); if (backendLocked()) { // this won't raise a dialog, immediate execute - auto pret = prompt->prompt({}); - if (pret.isError()) { - return pret; + ret = prompt->prompt(client, {}); + if (ret.err()) { + return ret; } prompt = nullptr; } // defer the close to the prompt - return prompt; + return {}; } - DBusReturn> Collection::searchItems(const StringStringMap& attributes) + DBusResult Collection::searchItems(const StringStringMap& attributes, QList& items) { + items.clear(); + auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { // 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{}; + return {}; } // shortcut logic for Uuid/Path attributes, as they can uniquely identify an item. @@ -244,20 +245,18 @@ namespace FdoSecrets 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{}; + items += m_entryToItem.value(entry); } + return {}; } 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{}; + items += m_entryToItem.value(entry); } + return {}; } QList terms; @@ -265,13 +264,12 @@ namespace FdoSecrets terms << attributeToTerm(it.key(), it.value()); } - QList items; const auto foundEntries = EntrySearcher(false, true).search(terms, m_exposedGroup); items.reserve(foundEntries.size()); for (const auto& entry : foundEntries) { items << m_entryToItem.value(entry); } - return items; + return {}; } EntrySearcher::SearchTerm Collection::attributeToTerm(const QString& key, const QString& value) @@ -296,99 +294,58 @@ namespace FdoSecrets return term; } - DBusReturn - Collection::createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt) + DBusResult Collection::createItem(const QVariantMap& properties, + const Secret& secret, + bool replace, + Item*& item, + PromptBase*& prompt) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - if (!pathToObject(secret.session)) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); - } - - prompt = nullptr; - - bool newlyCreated = true; - Item* item = nullptr; + item = nullptr; QString itemPath; - StringStringMap attributes; - auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")); + auto iterAttr = properties.find(DBUS_INTERFACE_SECRET_ITEM + ".Attributes"); if (iterAttr != properties.end()) { - attributes = iterAttr.value().value(); + // the actual value in iterAttr.value() is QDBusArgument, which represents a structure + // and qt has no idea what this corresponds to. + // we thus force a conversion to StringStringMap here. The conversion is registered in + // DBusTypes.cpp + auto attributes = iterAttr.value().value(); itemPath = attributes.value(ItemAttributes::PathKey); // check existing item using attributes - auto existing = searchItems(attributes); - if (existing.isError()) { - return existing; + QList existing; + ret = searchItems(attributes, existing); + if (ret.err()) { + return ret; } - if (!existing.value().isEmpty() && replace) { - item = existing.value().front(); - newlyCreated = false; + if (!existing.isEmpty() && replace) { + item = existing.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->applyGroupIconOnCreateTo(entry); - - entry->setGroup(group); - - // when creation finishes in backend, we will already have item - item = m_entryToItem.value(entry, nullptr); - - if (!item) { - // may happen if entry somehow ends up in recycle bin - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); - } + prompt = PromptBase::Create(service(), this, properties, secret, itemPath, item); + if (!prompt) { + return QDBusError::InternalError; } - - ret = item->setProperties(properties); - if (ret.isError()) { - if (newlyCreated) { - item->doDelete(); - } - return ret; - } - ret = item->setSecret(secret); - if (ret.isError()) { - if (newlyCreated) { - item->doDelete(); - } - return ret; - } - - return item; + return {}; } - DBusReturn Collection::setProperties(const QVariantMap& properties) + DBusResult Collection::setProperties(const QVariantMap& properties) { - auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION ".Label")).toString(); + auto label = properties.value(DBUS_INTERFACE_SECRET_COLLECTION + ".Label").toString(); auto ret = setLabel(label); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -400,10 +357,10 @@ namespace FdoSecrets return m_aliases; } - DBusReturn Collection::addAlias(QString alias) + DBusResult Collection::addAlias(QString alias) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -415,22 +372,20 @@ namespace FdoSecrets emit aliasAboutToAdd(alias); - bool ok = - registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias), false); - if (ok) { + if (dbus()->registerAlias(this, alias)) { m_aliases.insert(alias); emit aliasAdded(alias); } else { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); + return QDBusError::InvalidObjectPath; } return {}; } - DBusReturn Collection::removeAlias(QString alias) + DBusResult Collection::removeAlias(QString alias) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -440,9 +395,7 @@ namespace FdoSecrets return {}; } - QDBusConnection::sessionBus().unregisterObject( - QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias)); - + dbus()->unregisterAlias(alias); m_aliases.remove(alias); emit aliasRemoved(alias); @@ -470,14 +423,11 @@ namespace FdoSecrets void Collection::onDatabaseLockChanged() { - auto locked = backendLocked(); - if (!locked) { - populateContents(); - } else { - cleanupConnections(); + if (!reloadBackend()) { + doDelete(); + return; } - emit collectionLockChanged(locked); - emit collectionChanged(); + emit collectionLockChanged(backendLocked()); } void Collection::populateContents() @@ -550,6 +500,8 @@ namespace FdoSecrets onEntryAdded(entry, false); } + // Do not connect to databaseModified signal because we only want signals for the subset under m_exposedGroup + connect(m_backend->database()->metadata(), &Metadata::metadataModified, this, &Collection::collectionChanged); connectGroupSignalRecursive(m_exposedGroup); } @@ -641,7 +593,8 @@ namespace FdoSecrets emit collectionAboutToDelete(); - unregisterPrimaryPath(); + // remove from dbus early + dbus()->unregisterObject(this); // remove alias manually to trigger signal for (const auto& a : aliases()) { @@ -692,7 +645,7 @@ namespace FdoSecrets void Collection::doDeleteEntries(QList entries) { - m_backend->deleteEntries(std::move(entries)); + m_backend->deleteEntries(std::move(entries), FdoSecrets::settings()->confirmDeleteItem()); } Group* Collection::findCreateGroupByPath(const QString& groupPath) @@ -748,4 +701,36 @@ namespace FdoSecrets return inRecycleBin(entry->group()); } + Item* Collection::doNewItem(const DBusClientPtr& client, QString itemPath) + { + Q_ASSERT(m_backend); + + // 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->applyGroupIconOnCreateTo(entry); + + entry->setGroup(group); + + // the item was just created so there is no point in having it not authorized + client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed); + + // when creation finishes in backend, we will already have item + auto created = m_entryToItem.value(entry, nullptr); + + return created; + } + } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h index 80940d5a7..d80fc0e3b 100644 --- a/src/fdosecrets/objects/Collection.h +++ b/src/fdosecrets/objects/Collection.h @@ -18,9 +18,9 @@ #ifndef KEEPASSXC_FDOSECRETS_COLLECTION_H #define KEEPASSXC_FDOSECRETS_COLLECTION_H -#include "DBusObject.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" -#include "adaptors/CollectionAdaptor.h" #include "core/EntrySearcher.h" #include @@ -36,9 +36,10 @@ namespace FdoSecrets class Item; class PromptBase; class Service; - class Collection : public DBusObjectHelper + class Collection : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_COLLECTION_LITERAL) explicit Collection(Service* parent, DatabaseWidget* backend); @@ -54,21 +55,21 @@ namespace FdoSecrets */ static Collection* Create(Service* parent, DatabaseWidget* backend); - DBusReturn> items() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult items(QList& items) const; - DBusReturn label() const; - DBusReturn setLabel(const QString& label); + Q_INVOKABLE DBUS_PROPERTY DBusResult label(QString& label) const; + Q_INVOKABLE DBusResult setLabel(const QString& label); - DBusReturn locked() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult locked(bool& locked) const; - DBusReturn created() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult created(qulonglong& created) const; - DBusReturn modified() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult modified(qulonglong& modified) const; - DBusReturn deleteCollection(); - DBusReturn> searchItems(const StringStringMap& attributes); - DBusReturn - createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt); + Q_INVOKABLE DBusResult remove(const DBusClientPtr& client, PromptBase*& prompt); + Q_INVOKABLE DBusResult searchItems(const StringStringMap& attributes, QList& items); + Q_INVOKABLE DBusResult + createItem(const QVariantMap& properties, const Secret& secret, bool replace, Item*& item, PromptBase*& prompt); signals: void itemCreated(Item* item); @@ -86,15 +87,15 @@ namespace FdoSecrets void doneUnlockCollection(bool accepted); public: - DBusReturn setProperties(const QVariantMap& properties); + DBusResult setProperties(const QVariantMap& properties); bool isValid() const { return backend(); } - DBusReturn removeAlias(QString alias); - DBusReturn addAlias(QString alias); + DBusResult removeAlias(QString alias); + DBusResult addAlias(QString alias); const QSet aliases() const; /** @@ -116,6 +117,7 @@ namespace FdoSecrets // expose some methods for Prompt to use bool doLock(); void doUnlock(); + Item* doNewItem(const DBusClientPtr& client, QString itemPath); // will remove self void doDelete(); @@ -147,13 +149,13 @@ namespace FdoSecrets * Check if the backend is a valid object, send error reply if not. * @return true if the backend is valid. */ - DBusReturn ensureBackend() const; + DBusResult ensureBackend() const; /** * Ensure the database is unlocked, send error reply if locked. * @return true if the database is locked */ - DBusReturn ensureUnlocked() const; + DBusResult ensureUnlocked() const; /** * Like mkdir -p, find or create the group by path, under m_exposedGroup diff --git a/src/fdosecrets/objects/DBusObject.h b/src/fdosecrets/objects/DBusObject.h deleted file mode 100644 index d51642a86..000000000 --- a/src/fdosecrets/objects/DBusObject.h +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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 -#include - -namespace FdoSecrets -{ - class Service; - - /** - * @brief A common base class for all dbus-exposed objects. - * However, derived class should inherit from `DBusObjectHelper`, which is - * the only way to set DBus adaptor and enforces correct adaptor creation. - */ - class DBusObject : public QObject, public QDBusContext - { - Q_OBJECT - public: - const QDBusObjectPath& objectPath() const - { - return m_objectPath; - } - - QDBusAbstractAdaptor& dbusAdaptor() const - { - return *m_dbusAdaptor; - } - - protected: - /** - * @brief Register this object at given DBus path - * @param path DBus path to register at - * @param primary whether this path to be considered primary. The primary path is the one to be returned by - * `DBusObject::objectPath`. - * @return true on success - */ - bool registerWithPath(const QString& path, bool primary = true); - - void unregisterPrimaryPath() - { - if (m_objectPath.path() == QStringLiteral("/")) { - return; - } - QDBusConnection::sessionBus().unregisterObject(m_objectPath.path()); - m_objectPath.setPath(QStringLiteral("/")); - } - - QString callingPeer() const - { - Q_ASSERT(calledFromDBus()); - return message().service(); - } - - uint callingPeerPid() const - { - return connection().interface()->servicePid(callingPeer()); - } - - QString callingPeerName() const; - - DBusObject* p() const - { - return qobject_cast(parent()); - } - - private: - explicit DBusObject(DBusObject* parent); - - /** - * Derived class should not directly use sendErrorReply. - * Instead, use raiseError - */ - using QDBusContext::sendErrorReply; - - template friend class DBusReturn; - template friend class DBusObjectHelper; - - QDBusAbstractAdaptor* m_dbusAdaptor; - QDBusObjectPath m_objectPath; - }; - - template class DBusObjectHelper : public DBusObject - { - protected: - explicit DBusObjectHelper(DBusObject* parent) - : DBusObject(parent) - { - // creating new Adaptor has to be delayed into constructor's body, - // and can't be simply moved to initializer list, because at that - // point the base QObject class hasn't been initialized and will sigfault. - m_dbusAdaptor = new Adaptor(static_cast(this)); - m_dbusAdaptor->setParent(this); - } - }; - - /** - * 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 deleted file mode 100644 index ffd10add9..000000000 --- a/src/fdosecrets/objects/DBusReturn.cpp +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 deleted file mode 100644 index 889b8e11c..000000000 --- a/src/fdosecrets/objects/DBusReturn.h +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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); - } - - /** - * Get value or handle the error by the passed in dbus object - * @tparam P - * @param p - * @return - */ - template T valueOrHandle(P* p) const& - { - if (isError()) { - if (p->calledFromDBus()) { - p->sendErrorReply(errorName()); - } - return {}; - } - return m_value; - } - - /** - * Get value or handle the error by the passed in dbus object - * @tparam P - * @param p - * @return - */ - 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 deleted file mode 100644 index c249eaeed..000000000 --- a/src/fdosecrets/objects/DBusTypes.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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(); - - QMetaType::registerConverter([](const QDBusArgument& arg) { - if (arg.currentSignature() != "a{ss}") { - return StringStringMap{}; - } - // QDBusArgument is COW and qdbus_cast modifies it by detaching even it is const. - // we don't want to modify the instance (arg) stored in the qvariant so we create a copy - const auto copy = arg; // NOLINT(performance-unnecessary-copy-initialization) - return qdbus_cast(copy); - }); - - // 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 deleted file mode 100644 index ef1e2276a..000000000 --- a/src/fdosecrets/objects/DBusTypes.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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" -#define DBUS_PATH_TEMPLATE_PROMPT "%1/prompt/%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 index adf4f3d4c..b79649379 100644 --- a/src/fdosecrets/objects/Item.cpp +++ b/src/fdosecrets/objects/Item.cpp @@ -18,6 +18,7 @@ #include "Item.h" #include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Prompt.h" #include "fdosecrets/objects/Service.h" @@ -40,7 +41,7 @@ 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); + static Secret getEntrySecret(Entry* entry); namespace { @@ -51,8 +52,7 @@ namespace FdoSecrets Item* Item::Create(Collection* parent, Entry* backend) { QScopedPointer res{new Item(parent, backend)}; - - if (!res->registerSelf()) { + if (!res->dbus()->registerObject(res.data())) { return nullptr; } @@ -60,46 +60,37 @@ namespace FdoSecrets } Item::Item(Collection* parent, Entry* backend) - : DBusObjectHelper(parent) + : DBusObject(parent) , m_backend(backend) { - Q_ASSERT(!p()->objectPath().path().isEmpty()); - connect(m_backend.data(), &Entry::entryModified, this, &Item::itemChanged); } - bool Item::registerSelf() - { - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_ITEM).arg(p()->objectPath().path(), m_backend->uuidToHex()); - bool ok = registerWithPath(path); - if (!ok) { - service()->plugin()->emitError(tr("Failed to register item on DBus at path '%1'").arg(path)); - } - return ok; - } - - DBusReturn Item::locked() const + DBusResult Item::locked(const DBusClientPtr& client, bool& locked) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return collection()->locked(); + ret = collection()->locked(locked); + if (ret.err()) { + return ret; + } + locked = locked || !client->itemAuthorized(m_backend->uuid()); + return {}; } - DBusReturn Item::attributes() const + DBusResult Item::attributes(StringStringMap& attrs) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - StringStringMap attrs; - // add default attributes except password auto entryAttrs = m_backend->attributes(); for (const auto& attr : EntryAttributes::DefaultAttributes) { @@ -124,17 +115,17 @@ namespace FdoSecrets // add some informative and readonly attributes attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex(); attrs[ItemAttributes::PathKey] = path(); - return attrs; + return {}; } - DBusReturn Item::setAttributes(const StringStringMap& attrs) + DBusResult Item::setAttributes(const StringStringMap& attrs) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -158,28 +149,29 @@ namespace FdoSecrets return {}; } - DBusReturn Item::label() const + DBusResult Item::label(QString& label) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return m_backend->title(); + label = m_backend->title(); + return {}; } - DBusReturn Item::setLabel(const QString& label) + DBusResult Item::setLabel(const QString& label) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -190,91 +182,106 @@ namespace FdoSecrets return {}; } - DBusReturn Item::created() const + DBusResult Item::created(qulonglong& created) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return static_cast(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + created = static_cast(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + return {}; } - DBusReturn Item::modified() const + DBusResult Item::modified(qulonglong& modified) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } - return static_cast(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + modified = static_cast(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000); + return {}; } - DBusReturn Item::deleteItem() + DBusResult Item::remove(PromptBase*& prompt) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } - auto prompt = DeleteItemPrompt::Create(service(), this); - return prompt.value(); + prompt = PromptBase::Create(service(), this); + if (!prompt) { + return QDBusError::InternalError; + } + return {}; } - DBusReturn Item::getSecret(Session* session) + DBusResult Item::getSecret(const DBusClientPtr& client, Session* session, Secret& secret) + { + auto ret = getSecretNoNotification(client, session, secret); + if (ret.ok()) { + service()->plugin()->emitRequestShowNotification( + tr(R"(Entry "%1" from database "%2" was used by %3)") + .arg(m_backend->title(), collection()->name(), client->name())); + } + return ret; + } + + DBusResult Item::getSecretNoNotification(const DBusClientPtr& client, Session* session, Secret& secret) const { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } + if (!client->itemAuthorizedResetOnce(backend()->uuid())) { + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); + } if (!session) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); } - auto secret = getEntrySecret(m_backend); + 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; + return {}; } - DBusReturn Item::setSecret(const SecretStruct& secret) + DBusResult Item::setSecret(const DBusClientPtr& client, const Secret& secret) { auto ret = ensureBackend(); - if (ret.isError()) { + if (ret.err()) { return ret; } ret = ensureUnlocked(); - if (ret.isError()) { + if (ret.err()) { return ret; } + if (!client->itemAuthorizedResetOnce(backend()->uuid())) { + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); + } - auto session = pathToObject(secret.session); - if (!session) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + if (!secret.session) { + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); } // decode using session - auto decoded = session->decode(secret); + auto decoded = secret.session->decode(secret); // set in backend m_backend->beginUpdate(); @@ -284,19 +291,18 @@ namespace FdoSecrets return {}; } - DBusReturn Item::setProperties(const QVariantMap& properties) + DBusResult Item::setProperties(const QVariantMap& properties) { - auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Label")).toString(); + auto label = properties.value(DBUS_INTERFACE_SECRET_ITEM + ".Label").toString(); auto ret = setLabel(label); - if (ret.isError()) { + if (ret.err()) { return ret; } - auto attributes = - properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value(); + auto attributes = properties.value(DBUS_INTERFACE_SECRET_ITEM + ".Attributes").value(); ret = setAttributes(attributes); - if (ret.isError()) { + if (ret.err()) { return ret; } @@ -305,25 +311,26 @@ namespace FdoSecrets Collection* Item::collection() const { - return qobject_cast(p()); + return qobject_cast(parent()); } - DBusReturn Item::ensureBackend() const + DBusResult Item::ensureBackend() const { if (!m_backend) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } return {}; } - DBusReturn Item::ensureUnlocked() const + DBusResult Item::ensureUnlocked() const { - auto locked = collection()->locked(); - if (locked.isError()) { - return locked; + bool l; + auto ret = collection()->locked(l); + if (ret.err()) { + return ret; } - if (locked.value()) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED)); + if (l) { + return DBusResult(DBUS_ERROR_SECRET_IS_LOCKED); } return {}; } @@ -340,7 +347,7 @@ namespace FdoSecrets // 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. - unregisterPrimaryPath(); + dbus()->unregisterObject(this); m_backend = nullptr; deleteLater(); @@ -369,13 +376,6 @@ namespace FdoSecrets return pathComponents.join('/'); } - bool Item::isDeletePermanent() const - { - auto recycleBin = backend()->database()->metadata()->recycleBin(); - return (recycleBin && recycleBin->findEntryByUuid(backend()->uuid())) - || !backend()->database()->metadata()->recycleBinEnabled(); - } - void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType) { auto mimeName = contentType.split(';').takeFirst().trimmed(); @@ -414,9 +414,9 @@ namespace FdoSecrets entry->setPassword(codec->toUnicode(data)); } - SecretStruct getEntrySecret(Entry* entry) + Secret getEntrySecret(Entry* entry) { - SecretStruct ss; + Secret ss{}; if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) { ss.value = entry->attachments()->value(FDO_SECRETS_DATA); diff --git a/src/fdosecrets/objects/Item.h b/src/fdosecrets/objects/Item.h index 8c753a3d5..f246a31e1 100644 --- a/src/fdosecrets/objects/Item.h +++ b/src/fdosecrets/objects/Item.h @@ -18,8 +18,8 @@ #ifndef KEEPASSXC_FDOSECRETS_ITEM_H #define KEEPASSXC_FDOSECRETS_ITEM_H -#include "fdosecrets/objects/DBusObject.h" -#include "fdosecrets/objects/adaptors/ItemAdaptor.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" #include @@ -38,9 +38,10 @@ namespace FdoSecrets class Collection; class PromptBase; - class Item : public DBusObjectHelper + class Item : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_ITEM_LITERAL) explicit Item(Collection* parent, Entry* backend); @@ -55,21 +56,21 @@ namespace FdoSecrets */ static Item* Create(Collection* parent, Entry* backend); - DBusReturn locked() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult locked(const DBusClientPtr& client, bool& locked) const; - DBusReturn attributes() const; - DBusReturn setAttributes(const StringStringMap& attrs); + Q_INVOKABLE DBUS_PROPERTY DBusResult attributes(StringStringMap& attrs) const; + Q_INVOKABLE DBusResult setAttributes(const StringStringMap& attrs); - DBusReturn label() const; - DBusReturn setLabel(const QString& label); + Q_INVOKABLE DBUS_PROPERTY DBusResult label(QString& label) const; + Q_INVOKABLE DBusResult setLabel(const QString& label); - DBusReturn created() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult created(qulonglong& created) const; - DBusReturn modified() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult modified(qulonglong& modified) const; - DBusReturn deleteItem(); - DBusReturn getSecret(Session* session); - DBusReturn setSecret(const SecretStruct& secret); + Q_INVOKABLE DBusResult remove(PromptBase*& prompt); + Q_INVOKABLE DBusResult getSecret(const DBusClientPtr& client, Session* session, Secret& secret); + Q_INVOKABLE DBusResult setSecret(const DBusClientPtr& client, const Secret& secret); signals: void itemChanged(); @@ -78,7 +79,8 @@ namespace FdoSecrets public: static const QSet ReadOnlyAttributes; - DBusReturn setProperties(const QVariantMap& properties); + DBusResult getSecretNoNotification(const DBusClientPtr& client, Session* session, Secret& secret) const; + DBusResult setProperties(const QVariantMap& properties); Entry* backend() const; Collection* collection() const; @@ -90,39 +92,26 @@ namespace FdoSecrets */ QString path() const; - /** - * If the containing db does not have recycle bin enabled, - * or the entry is already in the recycle bin (not possible for item, though), - * the delete is permanent - * @return true if delete is permanent - */ - bool isDeletePermanent() const; - public slots: void doDelete(); - /** - * @brief Register self on DBus - * @return - */ - bool registerSelf(); - /** * Check if the backend is a valid object, send error reply if not. * @return No error if the backend is valid. */ - DBusReturn ensureBackend() const; + DBusResult ensureBackend() const; /** * Ensure the database is unlocked, send error reply if locked. * @return true if the database is locked */ - DBusReturn ensureUnlocked() const; + DBusResult ensureUnlocked() const; private: QPointer m_backend; }; } // namespace FdoSecrets +Q_DECLARE_METATYPE(FdoSecrets::ItemSecretMap); #endif // KEEPASSXC_FDOSECRETS_ITEM_H diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp index efed63179..78cc3efa6 100644 --- a/src/fdosecrets/objects/Prompt.cpp +++ b/src/fdosecrets/objects/Prompt.cpp @@ -18,10 +18,12 @@ #include "Prompt.h" #include "fdosecrets/FdoSecretsPlugin.h" -#include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Service.h" +#include "fdosecrets/objects/Session.h" +#include "fdosecrets/widgets/AccessControlDialog.h" #include "core/Tools.h" #include "gui/DatabaseWidget.h" @@ -29,27 +31,17 @@ #include #include +#include namespace FdoSecrets { PromptBase::PromptBase(Service* parent) - : DBusObjectHelper(parent) + : DBusObject(parent) { connect(this, &PromptBase::completed, this, &PromptBase::deleteLater); } - bool PromptBase::registerSelf() - { - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_PROMPT) - .arg(p()->objectPath().path(), Tools::uuidToHex(QUuid::createUuid())); - bool ok = registerWithPath(path); - if (!ok) { - service()->plugin()->emitError(tr("Failed to register item on DBus at path '%1'").arg(path)); - } - return ok; - } - QWindow* PromptBase::findWindow(const QString& windowId) { // find parent window, or nullptr if not found @@ -71,41 +63,29 @@ namespace FdoSecrets return qobject_cast(parent()); } - DBusReturn PromptBase::dismiss() + DBusResult PromptBase::dismiss() { emit completed(true, ""); return {}; } - DBusReturn DeleteCollectionPrompt::Create(Service* parent, Collection* coll) - { - QScopedPointer res{new DeleteCollectionPrompt(parent, coll)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll) : PromptBase(parent) , m_collection(coll) { } - DBusReturn DeleteCollectionPrompt::prompt(const QString& windowId) + DBusResult DeleteCollectionPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } if (!m_collection) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT)); + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } MessageBox::OverrideParent override(findWindow(windowId)); @@ -117,29 +97,19 @@ namespace FdoSecrets return {}; } - DBusReturn CreateCollectionPrompt::Create(Service* parent) - { - QScopedPointer res{new CreateCollectionPrompt(parent)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - - CreateCollectionPrompt::CreateCollectionPrompt(Service* parent) + CreateCollectionPrompt::CreateCollectionPrompt(Service* parent, QVariantMap properties, QString alias) : PromptBase(parent) + , m_properties(std::move(properties)) + , m_alias(std::move(alias)) { } - DBusReturn CreateCollectionPrompt::prompt(const QString& windowId) + DBusResult CreateCollectionPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } @@ -150,27 +120,30 @@ namespace FdoSecrets return dismiss(); } - emit collectionCreated(coll); + auto ret = coll->setProperties(m_properties); + if (ret.err()) { + coll->doDelete(); + return dismiss(); + } + if (!m_alias.isEmpty()) { + ret = coll->addAlias(m_alias); + if (ret.err()) { + coll->doDelete(); + return dismiss(); + } + } + emit completed(false, QVariant::fromValue(coll->objectPath())); return {}; } - DBusReturn CreateCollectionPrompt::dismiss() + DBusResult CreateCollectionPrompt::dismiss() { - emit completed(true, QVariant::fromValue(QDBusObjectPath{"/"})); + emit completed(true, QVariant::fromValue(DBusMgr::objectPathSafe(nullptr))); return {}; } - DBusReturn LockCollectionsPrompt::Create(Service* parent, const QList& colls) - { - QScopedPointer res{new LockCollectionsPrompt(parent, colls)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - LockCollectionsPrompt::LockCollectionsPrompt(Service* parent, const QList& colls) : PromptBase(parent) { @@ -180,15 +153,12 @@ namespace FdoSecrets } } - DBusReturn LockCollectionsPrompt::prompt(const QString& windowId) + DBusResult LockCollectionsPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } @@ -208,113 +178,177 @@ namespace FdoSecrets return {}; } - DBusReturn LockCollectionsPrompt::dismiss() + DBusResult LockCollectionsPrompt::dismiss() { emit completed(true, QVariant::fromValue(m_locked)); return {}; } - DBusReturn UnlockCollectionsPrompt::Create(Service* parent, - const QList& coll) - { - QScopedPointer res{new UnlockCollectionsPrompt(parent, coll)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - - UnlockCollectionsPrompt::UnlockCollectionsPrompt(Service* parent, const QList& colls) + UnlockPrompt::UnlockPrompt(Service* parent, const QSet& colls, const QSet& items) : PromptBase(parent) { m_collections.reserve(colls.size()); - for (const auto& c : asConst(colls)) { - m_collections << c; + for (const auto& coll : asConst(colls)) { + m_collections << coll; + connect(coll, &Collection::doneUnlockCollection, this, &UnlockPrompt::collectionUnlockFinished); + } + for (const auto& item : asConst(items)) { + m_items[item->collection()] << item; } } - DBusReturn UnlockCollectionsPrompt::prompt(const QString& windowId) + DBusResult UnlockPrompt::prompt(const DBusClientPtr& client, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } MessageBox::OverrideParent override(findWindow(windowId)); + // for use in unlockItems + m_windowId = windowId; + m_client = client; + + // first unlock any collections + bool waitingForCollections = false; for (const auto& c : asConst(m_collections)) { if (c) { - // doUnlock is nonblocking - connect(c, &Collection::doneUnlockCollection, this, &UnlockCollectionsPrompt::collectionUnlockFinished); + // doUnlock is nonblocking, execution will continue in collectionUnlockFinished c->doUnlock(); + waitingForCollections = true; } } + // unlock items directly if no collection unlocking pending + // o.w. do it in collectionUnlockFinished + if (!waitingForCollections) { + // do not block the current method + QTimer::singleShot(0, this, &UnlockPrompt::unlockItems); + } + return {}; } - void UnlockCollectionsPrompt::collectionUnlockFinished(bool accepted) + void UnlockPrompt::collectionUnlockFinished(bool accepted) { auto coll = qobject_cast(sender()); if (!coll) { return; } - if (!m_collections.contains(coll)) { - // should not happen - coll->disconnect(this); - return; - } - // one shot coll->disconnect(this); + if (!m_collections.contains(coll)) { + // should not happen + return; + } + if (accepted) { m_unlocked << coll->objectPath(); } else { m_numRejected += 1; + // no longer need to unlock the item if its containing collection + // didn't unlock. + m_items.remove(coll); } - // if we've get all + // if we got response for all collections if (m_numRejected + m_unlocked.size() == m_collections.size()) { - emit completed(m_unlocked.isEmpty(), QVariant::fromValue(m_unlocked)); + // next step is to unlock items + unlockItems(); } } - DBusReturn UnlockCollectionsPrompt::dismiss() + + void UnlockPrompt::unlockItems() + { + auto client = m_client.lock(); + if (!client) { + // client already gone + return; + } + + // flatten to list of entries + QList entries; + for (const auto& itemsPerColl : m_items.values()) { + for (const auto& item : itemsPerColl) { + if (!item) { + m_numRejected += 1; + continue; + } + auto entry = item->backend(); + if (client->itemKnown(entry->uuid())) { + if (!client->itemAuthorized(entry->uuid())) { + m_numRejected += 1; + } + continue; + } + // attach a temporary property so later we can get the item + // back from the dialog's result + entry->setProperty(FdoSecretsBackend, QVariant::fromValue(item.data())); + entries << entry; + } + } + if (!entries.isEmpty()) { + QString app = tr("%1 (PID: %2)").arg(client->name()).arg(client->pid()); + auto ac = new AccessControlDialog(findWindow(m_windowId), entries, app, AuthOption::Remember); + connect(ac, &AccessControlDialog::finished, this, &UnlockPrompt::itemUnlockFinished); + connect(ac, &AccessControlDialog::finished, ac, &AccessControlDialog::deleteLater); + ac->open(); + } else { + itemUnlockFinished({}); + } + } + + void UnlockPrompt::itemUnlockFinished(const QHash& decisions) + { + auto client = m_client.lock(); + if (!client) { + // client already gone + return; + } + for (auto it = decisions.constBegin(); it != decisions.constEnd(); ++it) { + auto entry = it.key(); + // get back the corresponding item + auto item = entry->property(FdoSecretsBackend).value(); + entry->setProperty(FdoSecretsBackend, {}); + Q_ASSERT(item); + + // set auth + client->setItemAuthorized(entry->uuid(), it.value()); + + if (client->itemAuthorized(entry->uuid())) { + m_unlocked += item->objectPath(); + } else { + m_numRejected += 1; + } + } + // if anything is not unlocked, treat the whole prompt as dismissed + // so the client has a chance to handle the error + emit completed(m_numRejected > 0, QVariant::fromValue(m_unlocked)); + } + + DBusResult UnlockPrompt::dismiss() { emit completed(true, QVariant::fromValue(m_unlocked)); return {}; } - DBusReturn DeleteItemPrompt::Create(Service* parent, Item* item) - { - QScopedPointer res{new DeleteItemPrompt(parent, item)}; - if (!res->registerSelf()) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); - } - return res.take(); - } - DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item) : PromptBase(parent) , m_item(item) { } - DBusReturn DeleteItemPrompt::prompt(const QString& windowId) + DBusResult DeleteItemPrompt::prompt(const DBusClientPtr&, const QString& windowId) { if (thread() != QThread::currentThread()) { - DBusReturn ret; - QMetaObject::invokeMethod(this, - "prompt", - Qt::BlockingQueuedConnection, - Q_ARG(QString, windowId), - Q_RETURN_ARG(DBusReturn, ret)); + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); return ret; } @@ -322,14 +356,6 @@ namespace FdoSecrets // delete item's backend. Item will be notified after the backend is deleted. if (m_item) { - if (FdoSecrets::settings()->noConfirmDeleteItem()) { - // based on permanent or not, different button is used - if (m_item->isDeletePermanent()) { - MessageBox::setNextAnswer(MessageBox::Delete); - } else { - MessageBox::setNextAnswer(MessageBox::Move); - } - } m_item->collection()->doDeleteEntries({m_item->backend()}); } @@ -337,4 +363,121 @@ namespace FdoSecrets return {}; } + + CreateItemPrompt::CreateItemPrompt(Service* parent, + Collection* coll, + QVariantMap properties, + Secret secret, + QString itemPath, + Item* existing) + : PromptBase(parent) + , m_coll(coll) + , m_properties(std::move(properties)) + , m_secret(std::move(secret)) + , m_itemPath(std::move(itemPath)) + , m_item(existing) + // session aliveness also need to be tracked, for potential use later in updateItem + , m_sess(m_secret.session) + { + } + + DBusResult CreateItemPrompt::prompt(const DBusClientPtr& client, const QString& windowId) + { + if (thread() != QThread::currentThread()) { + DBusResult ret; + QMetaObject::invokeMethod( + this, "prompt", Qt::BlockingQueuedConnection, Q_ARG(QString, windowId), Q_RETURN_ARG(DBusResult, ret)); + return ret; + } + + MessageBox::OverrideParent override(findWindow(windowId)); + + if (!m_coll) { + return dismiss(); + } + + // save a weak reference to the client which may be used asynchronously later + m_client = client; + + // the item doesn't exists yet, create it + if (!m_item) { + m_item = m_coll->doNewItem(client, m_itemPath); + if (!m_item) { + // may happen if entry somehow ends up in recycle bin + return DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); + } + + auto ret = updateItem(); + if (ret.err()) { + m_item->doDelete(); + return ret; + } + emit completed(false, QVariant::fromValue(m_item->objectPath())); + } else { + bool locked = false; + auto ret = m_item->locked(client, locked); + if (ret.err()) { + return ret; + } + if (locked) { + // give the user a chance to unlock the item + auto prompt = PromptBase::Create(service(), QSet{}, QSet{m_item}); + if (!prompt) { + return QDBusError::InternalError; + } + // postpone anything after the confirmation + connect(prompt, &PromptBase::completed, this, &CreateItemPrompt::itemUnlocked); + return prompt->prompt(client, windowId); + } else { + ret = updateItem(); + if (ret.err()) { + return ret; + } + emit completed(false, QVariant::fromValue(m_item->objectPath())); + } + } + return {}; + } + + DBusResult CreateItemPrompt::dismiss() + { + emit completed(true, QVariant::fromValue(DBusMgr::objectPathSafe(nullptr))); + return {}; + } + + void CreateItemPrompt::itemUnlocked(bool dismissed, const QVariant& result) + { + auto unlocked = result.value>(); + if (!unlocked.isEmpty()) { + // in theory we should check if the object path matches m_item, but a mismatch should not happen, + // because we control the unlock prompt ourselves + updateItem(); + } + emit completed(dismissed, QVariant::fromValue(DBusMgr::objectPathSafe(m_item))); + } + + DBusResult CreateItemPrompt::updateItem() + { + auto client = m_client.lock(); + if (!client) { + // client already gone + return {}; + } + + if (!m_sess || m_sess != m_secret.session) { + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); + } + if (!m_item) { + return {}; + } + auto ret = m_item->setProperties(m_properties); + if (ret.err()) { + return ret; + } + ret = m_item->setSecret(client, m_secret); + if (ret.err()) { + return ret; + } + return {}; + } } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Prompt.h b/src/fdosecrets/objects/Prompt.h index 9a9725675..ea5afc5d6 100644 --- a/src/fdosecrets/objects/Prompt.h +++ b/src/fdosecrets/objects/Prompt.h @@ -18,27 +18,41 @@ #ifndef KEEPASSXC_FDOSECRETS_PROMPT_H #define KEEPASSXC_FDOSECRETS_PROMPT_H -#include "fdosecrets/objects/DBusObject.h" -#include "fdosecrets/objects/adaptors/PromptAdaptor.h" +#include "core/Global.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" +#include #include class QWindow; class DatabaseWidget; +class Entry; namespace FdoSecrets { class Service; - class PromptBase : public DBusObjectHelper + class PromptBase : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_PROMPT_LITERAL) public: - virtual DBusReturn prompt(const QString& windowId) = 0; + Q_INVOKABLE virtual DBusResult prompt(const DBusClientPtr& client, const QString& windowId) = 0; - virtual DBusReturn dismiss(); + Q_INVOKABLE virtual DBusResult dismiss(); + + template static PromptBase* Create(Service* parent, ARGS&&... args) + { + QScopedPointer res{new PROMPT(parent, std::forward(args)...)}; + if (!res->dbus()->registerObject(res.data())) { + // internal error; + return nullptr; + } + return res.take(); + } signals: void completed(bool dismissed, const QVariant& result); @@ -46,7 +60,6 @@ namespace FdoSecrets protected: explicit PromptBase(Service* parent); - bool registerSelf(); QWindow* findWindow(const QString& windowId); Service* service() const; }; @@ -60,11 +73,11 @@ namespace FdoSecrets explicit DeleteCollectionPrompt(Service* parent, Collection* coll); public: - static DBusReturn Create(Service* parent, Collection* coll); - - DBusReturn prompt(const QString& windowId) override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; private: + friend class PromptBase; + QPointer m_collection; }; @@ -72,16 +85,17 @@ namespace FdoSecrets { Q_OBJECT - explicit CreateCollectionPrompt(Service* parent); + explicit CreateCollectionPrompt(Service* parent, QVariantMap properties, QString alias); public: - static DBusReturn Create(Service* parent); + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; - DBusReturn prompt(const QString& windowId) override; - DBusReturn dismiss() override; + private: + friend class PromptBase; - signals: - void collectionCreated(Collection* coll); + QVariantMap m_properties; + QString m_alias; }; class LockCollectionsPrompt : public PromptBase @@ -91,35 +105,46 @@ namespace FdoSecrets explicit LockCollectionsPrompt(Service* parent, const QList& colls); public: - static DBusReturn Create(Service* parent, const QList& colls); - - DBusReturn prompt(const QString& windowId) override; - DBusReturn dismiss() override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; private: + friend class PromptBase; + QList> m_collections; QList m_locked; }; - class UnlockCollectionsPrompt : public PromptBase + class DBusClient; + class UnlockPrompt : public PromptBase { Q_OBJECT - explicit UnlockCollectionsPrompt(Service* parent, const QList& coll); + explicit UnlockPrompt(Service* parent, const QSet& colls, const QSet& items); public: - static DBusReturn Create(Service* parent, const QList& coll); - - DBusReturn prompt(const QString& windowId) override; - DBusReturn dismiss() override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; private slots: void collectionUnlockFinished(bool accepted); + void itemUnlockFinished(const QHash& results); private: + void unlockItems(); + + friend class PromptBase; + + static constexpr auto FdoSecretsBackend = "FdoSecretsBackend"; + QList> m_collections; + QHash>> m_items; QList m_unlocked; int m_numRejected = 0; + + // info about calling client + QWeakPointer m_client; + QString m_windowId; }; class Item; @@ -130,14 +155,46 @@ namespace FdoSecrets explicit DeleteItemPrompt(Service* parent, Item* item); public: - static DBusReturn Create(Service* parent, Item* item); - - DBusReturn prompt(const QString& windowId) override; + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; private: + friend class PromptBase; + QPointer m_item; }; + class CreateItemPrompt : public PromptBase + { + Q_OBJECT + + explicit CreateItemPrompt(Service* parent, + Collection* coll, + QVariantMap properties, + Secret secret, + QString itemPath, + Item* existing); + + public: + DBusResult prompt(const DBusClientPtr& client, const QString& windowId) override; + DBusResult dismiss() override; + private slots: + void itemUnlocked(bool dismissed, const QVariant& result); + + private: + DBusResult updateItem(); + + friend class PromptBase; + + QPointer m_coll; + QVariantMap m_properties; + Secret m_secret; + QString m_itemPath; + QPointer m_item; + + QPointer m_sess; + QWeakPointer m_client; + }; + } // namespace FdoSecrets #endif // KEEPASSXC_FDOSECRETS_PROMPT_H diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index 957203d8b..38dc0aff6 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -19,6 +19,7 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/Prompt.h" @@ -28,7 +29,6 @@ #include "gui/DatabaseWidget.h" #include -#include #include #include @@ -39,9 +39,10 @@ namespace namespace FdoSecrets { - QSharedPointer Service::Create(FdoSecretsPlugin* plugin, QPointer dbTabs) + QSharedPointer + Service::Create(FdoSecretsPlugin* plugin, QPointer dbTabs, QSharedPointer dbus) { - QSharedPointer res{new Service(plugin, std::move(dbTabs))}; + QSharedPointer res{new Service(plugin, std::move(dbTabs), std::move(dbus))}; if (!res->initialize()) { return {}; } @@ -49,43 +50,25 @@ namespace FdoSecrets } Service::Service(FdoSecretsPlugin* plugin, - QPointer dbTabs) // clazy: exclude=ctor-missing-parent-argument - : DBusObjectHelper(nullptr) + QPointer dbTabs, + QSharedPointer dbus) // clazy: exclude=ctor-missing-parent-argument + : DBusObject(std::move(dbus)) , m_plugin(plugin) , m_databases(std::move(dbTabs)) , m_insideEnsureDefaultAlias(false) - , m_serviceWatcher(nullptr) { connect( m_databases, &DatabaseTabWidget::databaseUnlockDialogFinished, this, &Service::doneUnlockDatabaseInDialog); } - Service::~Service() - { - QDBusConnection::sessionBus().unregisterService(QStringLiteral(DBUS_SERVICE_SECRET)); - } + Service::~Service() = default; bool Service::initialize() { - if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) { - plugin()->emitError( - tr("Failed to register DBus service at %1.
").arg(QLatin1String(DBUS_SERVICE_SECRET)) - + m_plugin->reportExistingService()); + if (!dbus()->registerObject(this)) { return false; } - if (!registerWithPath(QStringLiteral(DBUS_PATH_SECRETS))) { - plugin()->emitError(tr("Failed to register DBus path %1.
").arg(QStringLiteral(DBUS_PATH_SECRETS))); - return false; - } - - // Connect to service unregistered signal - m_serviceWatcher.reset(new QDBusServiceWatcher()); - connect( - m_serviceWatcher.get(), &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); @@ -199,161 +182,157 @@ namespace FdoSecrets m_insideEnsureDefaultAlias = false; } - void Service::dbusServiceUnregistered(const QString& service) + DBusResult Service::collections(QList& collections) const { - Q_ASSERT(m_serviceWatcher); - - auto removed = m_serviceWatcher->removeWatchedService(service); - if (!removed) { - qDebug("FdoSecrets: Failed to remove service watcher"); - } - - Session::CleanupNegotiation(service); - auto sess = m_peerToSession.value(service, nullptr); - if (sess) { - sess->close().okOrDie(); - } + collections = m_collections; + return {}; } - DBusReturn> Service::collections() const + DBusResult Service::openSession(const DBusClientPtr& client, + const QString& algorithm, + const QVariant& input, + QVariant& output, + Session*& result) { - 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); + bool incomplete = false; + auto ciphers = client->negotiateCipher(algorithm, input, output, incomplete); if (incomplete) { result = nullptr; - return output; + return {}; } if (!ciphers) { - return DBusReturn<>::Error(QDBusError::NotSupported); + return QDBusError::NotSupported; } - result = Session::Create(std::move(ciphers), callingPeerName(), this); + + // create session using the negotiated cipher + result = Session::Create(std::move(ciphers), client->name(), this); if (!result) { - return DBusReturn<>::Error(QDBusError::InvalidObjectPath); + return QDBusError::InternalError; } - 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); + // remove session when the client disconnects + connect(dbus().data(), &DBusMgr::clientDisconnected, result, [result, client](const DBusClientPtr& toRemove) { + if (toRemove == client) { + result->close().okOrDie(); + } }); - emit sessionOpened(result); - return output; + // keep a list of sessions + m_sessions.append(result); + connect(result, &Session::aboutToClose, this, [this, result]() { m_sessions.removeAll(result); }); + + return {}; } - DBusReturn - Service::createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt) + DBusResult Service::createCollection(const QVariantMap& properties, + const QString& alias, + Collection*& collection, + PromptBase*& prompt) { prompt = nullptr; // return existing collection if alias is non-empty and exists. - auto collection = findCollection(alias); + collection = findCollection(alias); if (!collection) { - auto cp = CreateCollectionPrompt::Create(this); - if (cp.isError()) { - return cp; + prompt = PromptBase::Create(this, properties, alias); + if (!prompt) { + return QDBusError::InternalError; } - prompt = cp.value(); - - // collection will be created when the prompt completes. - // once it's done, we set additional properties on the collection - connect(cp.value(), - &CreateCollectionPrompt::collectionCreated, - cp.value(), - [alias, properties](Collection* coll) { - coll->setProperties(properties).okOrDie(); - if (!alias.isEmpty()) { - coll->addAlias(alias).okOrDie(); - } - }); } - return collection; + return {}; } - DBusReturn> Service::searchItems(const StringStringMap& attributes, QList& locked) + DBusResult Service::searchItems(const DBusClientPtr& client, + const StringStringMap& attributes, + QList& unlocked, + QList& locked) const { - auto ret = collections(); - if (ret.isError()) { + QList colls; + auto ret = collections(colls); + if (ret.err()) { return ret; } - QList unlocked; - for (const auto& coll : ret.value()) { - auto items = coll->searchItems(attributes); - if (items.isError()) { - return items; + for (const auto& coll : asConst(colls)) { + QList items; + ret = coll->searchItems(attributes, items); + if (ret.err()) { + return ret; } - 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; + // item locked state already covers its collection's locked state + for (const auto& item : asConst(items)) { + bool l; + ret = item->locked(client, l); + if (ret.err()) { + return ret; + } + if (l) { + locked.append(item); + } else { + unlocked.append(item); } - // 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; - } - } - if (!toUnlock.isEmpty()) { - auto up = UnlockCollectionsPrompt::Create(this, toUnlock); - if (up.isError()) { - return up; - } - prompt = up.value(); - } - return unlocked; + return {}; } - DBusReturn> Service::lock(const QList& objects, PromptBase*& prompt) + DBusResult Service::unlock(const DBusClientPtr& client, + const QList& objects, + QList& unlocked, + PromptBase*& prompt) + { + QSet collectionsToUnlock; + QSet itemsToUnlock; + collectionsToUnlock.reserve(objects.size()); + itemsToUnlock.reserve(objects.size()); + + for (const auto& obj : asConst(objects)) { + // the object is either an item or an collection + auto item = qobject_cast(obj); + auto coll = item ? item->collection() : qobject_cast(obj); + // either way there should be a collection + if (!coll) { + continue; + } + + bool collLocked{false}, itemLocked{false}; + // if the collection needs unlock + auto ret = coll->locked(collLocked); + if (ret.err()) { + return ret; + } + if (collLocked) { + collectionsToUnlock << coll; + } + + if (item) { + // item may also need unlock + ret = item->locked(client, itemLocked); + if (ret.err()) { + return ret; + } + if (itemLocked) { + itemsToUnlock << item; + } + } + + // both collection and item are not locked + if (!collLocked && !itemLocked) { + unlocked << obj; + } + } + + if (!collectionsToUnlock.isEmpty() || !itemsToUnlock.isEmpty()) { + prompt = PromptBase::Create(this, collectionsToUnlock, itemsToUnlock); + if (!prompt) { + return QDBusError::InternalError; + } + } + return {}; + } + + DBusResult Service::lock(const QList& objects, QList& locked, PromptBase*& prompt) { QSet needLock; needLock.reserve(objects.size()); @@ -372,64 +351,62 @@ namespace FdoSecrets } // return anything already locked - QList locked; QList toLock; for (const auto& coll : asConst(needLock)) { - auto l = coll->locked(); - if (l.isError()) { - return l; + bool l; + auto ret = coll->locked(l); + if (ret.err()) { + return ret; } - if (l.value()) { + if (l) { locked << coll; } else { toLock << coll; } } if (!toLock.isEmpty()) { - auto lp = LockCollectionsPrompt::Create(this, toLock); - if (lp.isError()) { - return lp; + prompt = PromptBase::Create(this, toLock); + if (!prompt) { + return QDBusError::InternalError; } - prompt = lp.value(); } - return locked; + return {}; } - DBusReturn> Service::getSecrets(const QList& items, Session* session) + DBusResult Service::getSecrets(const DBusClientPtr& client, + const QList& items, + Session* session, + ItemSecretMap& secrets) const { if (!session) { - return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION)); + return DBusResult(DBUS_ERROR_SECRET_NO_SESSION); } - QHash res; - for (const auto& item : asConst(items)) { - auto ret = item->getSecret(session); - if (ret.isError()) { + auto ret = item->getSecretNoNotification(client, session, secrets[item]); + if (ret.err()) { 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; + plugin()->emitRequestShowNotification( + tr(R"(%n Entry(s) was used by %1)", "%1 is the name of an application", secrets.size()) + .arg(client->name())); + return {}; } - DBusReturn Service::readAlias(const QString& name) + DBusResult Service::readAlias(const QString& name, Collection*& collection) const { - return findCollection(name); + collection = findCollection(name); + return {}; } - DBusReturn Service::setAlias(const QString& name, Collection* collection) + DBusResult 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 DBusResult(DBUS_ERROR_SECRET_NO_SUCH_OBJECT); } return collection->removeAlias(name); } @@ -481,7 +458,7 @@ namespace FdoSecrets return m_dbToCollection.value(db, nullptr); } - const QList Service::sessions() const + QList Service::sessions() const { return m_sessions; } diff --git a/src/fdosecrets/objects/Service.h b/src/fdosecrets/objects/Service.h index 5b1ff5acd..674e5c225 100644 --- a/src/fdosecrets/objects/Service.h +++ b/src/fdosecrets/objects/Service.h @@ -18,18 +18,14 @@ #ifndef KEEPASSXC_FDOSECRETS_SERVICE_H #define KEEPASSXC_FDOSECRETS_SERVICE_H -#include "fdosecrets/objects/DBusObject.h" -#include "fdosecrets/objects/adaptors/ServiceAdaptor.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusObject.h" #include #include #include #include -#include - -class QDBusServiceWatcher; - class DatabaseTabWidget; class DatabaseWidget; class Group; @@ -42,14 +38,14 @@ namespace FdoSecrets class Collection; class Item; class PromptBase; - class ServiceAdaptor; class Session; - class Service : public DBusObjectHelper // clazy: exclude=ctor-missing-parent-argument + class Service : public DBusObject // clazy: exclude=ctor-missing-parent-argument { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SERVICE_LITERAL) - explicit Service(FdoSecretsPlugin* plugin, QPointer dbTabs); + explicit Service(FdoSecretsPlugin* plugin, QPointer dbTabs, QSharedPointer dbus); public: /** @@ -58,38 +54,51 @@ namespace FdoSecrets * This may be caused by * - failed initialization */ - static QSharedPointer Create(FdoSecretsPlugin* plugin, QPointer dbTabs); + static QSharedPointer + Create(FdoSecretsPlugin* plugin, QPointer dbTabs, QSharedPointer dbus); ~Service() override; - 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); + Q_INVOKABLE DBusResult openSession(const DBusClientPtr& client, + const QString& algorithm, + const QVariant& input, + QVariant& output, + Session*& result); + Q_INVOKABLE DBusResult createCollection(const QVariantMap& properties, + const QString& alias, + Collection*& collection, + PromptBase*& prompt); + Q_INVOKABLE DBusResult searchItems(const DBusClientPtr& client, + const StringStringMap& attributes, + QList& unlocked, + QList& locked) const; - DBusReturn> unlock(const QList& objects, PromptBase*& prompt); + Q_INVOKABLE DBusResult unlock(const DBusClientPtr& client, + const QList& objects, + QList& unlocked, + PromptBase*& prompt); - DBusReturn> lock(const QList& objects, PromptBase*& prompt); + Q_INVOKABLE DBusResult lock(const QList& objects, QList& locked, PromptBase*& prompt); - DBusReturn> getSecrets(const QList& items, Session* session); + Q_INVOKABLE DBusResult getSecrets(const DBusClientPtr& client, + const QList& items, + Session* session, + ItemSecretMap& secrets) const; - DBusReturn readAlias(const QString& name); + Q_INVOKABLE DBusResult readAlias(const QString& name, Collection*& collection) const; - DBusReturn setAlias(const QString& name, Collection* collection); + Q_INVOKABLE DBusResult setAlias(const QString& name, Collection* collection); /** * List of collections * @return */ - DBusReturn> collections() const; + Q_INVOKABLE DBUS_PROPERTY DBusResult collections(QList& collections) const; signals: void collectionCreated(Collection* collection); void collectionDeleted(Collection* collection); void collectionChanged(Collection* collection); - void sessionOpened(Session* sess); - void sessionClosed(Session* sess); - /** * Finish signal for async action doUnlockDatabaseInDialog * @param accepted If false, the action is canceled by the user @@ -102,7 +111,7 @@ namespace FdoSecrets * List of sessions * @return */ - const QList sessions() const; + QList sessions() const; FdoSecretsPlugin* plugin() const { @@ -121,7 +130,6 @@ namespace FdoSecrets void doUnlockDatabaseInDialog(DatabaseWidget* dbWidget); private slots: - void dbusServiceUnregistered(const QString& service); void ensureDefaultAlias(); void onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal); @@ -158,11 +166,8 @@ namespace FdoSecrets QHash m_dbToCollection; QList m_sessions; - QHash m_peerToSession; bool m_insideEnsureDefaultAlias; - - std::unique_ptr m_serviceWatcher; }; } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Session.cpp b/src/fdosecrets/objects/Session.cpp index 0c643f2f1..04c9f6076 100644 --- a/src/fdosecrets/objects/Session.cpp +++ b/src/fdosecrets/objects/Session.cpp @@ -14,53 +14,36 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include "Session.h" #include "fdosecrets/FdoSecretsPlugin.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/SessionCipher.h" #include "core/Tools.h" namespace FdoSecrets { - - QHash Session::negotiationState; - - Session* Session::Create(std::unique_ptr&& cipher, const QString& peer, Service* parent) + Session* Session::Create(QSharedPointer cipher, const QString& peer, Service* parent) { QScopedPointer res{new Session(std::move(cipher), peer, parent)}; - - if (!res->registerSelf()) { + if (!res->dbus()->registerObject(res.data())) { return nullptr; } return res.take(); } - Session::Session(std::unique_ptr&& cipher, const QString& peer, Service* parent) - : DBusObjectHelper(parent) + Session::Session(QSharedPointer cipher, const QString& peer, Service* parent) + : DBusObject(parent) , m_cipher(std::move(cipher)) , m_peer(peer) , m_id(QUuid::createUuid()) { } - bool Session::registerSelf() - { - auto path = QStringLiteral(DBUS_PATH_TEMPLATE_SESSION).arg(p()->objectPath().path(), id()); - bool ok = registerWithPath(path); - if (!ok) { - service()->plugin()->emitError(tr("Failed to register session on DBus at path '%1'").arg(path)); - } - return ok; - } - - void Session::CleanupNegotiation(const QString& peer) - { - negotiationState.remove(peer); - } - - DBusReturn Session::close() + DBusResult Session::close() { emit aboutToClose(); deleteLater(); @@ -83,48 +66,16 @@ namespace FdoSecrets return qobject_cast(parent()); } - 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 == QLatin1String(PlainCipher::Algorithm)) { - cipher.reset(new PlainCipher); - } else if (algorithm == QLatin1String(DhIetf1024Sha256Aes128CbcPkcs7::Algorithm)) { - 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 + Secret Session::encode(const Secret& input) const { auto output = m_cipher->encrypt(input); - output.session = objectPath(); + output.session = this; return output; } - SecretStruct Session::decode(const SecretStruct& input) const + Secret Session::decode(const Secret& input) const { + Q_ASSERT(input.session == this); return m_cipher->decrypt(input); } - } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/Session.h b/src/fdosecrets/objects/Session.h index 3bb6ea257..f3366d680 100644 --- a/src/fdosecrets/objects/Session.h +++ b/src/fdosecrets/objects/Session.h @@ -18,36 +18,24 @@ #ifndef KEEPASSXC_FDOSECRETS_SESSION_H #define KEEPASSXC_FDOSECRETS_SESSION_H -#include "fdosecrets/objects/DBusObject.h" +#include "fdosecrets/dbus/DBusObject.h" #include "fdosecrets/objects/Service.h" -#include "fdosecrets/objects/SessionCipher.h" -#include "fdosecrets/objects/adaptors/SessionAdaptor.h" -#include -#include +#include #include #include -#include - namespace FdoSecrets { - class CipherPair; - class Session : public DBusObjectHelper + class Session : public DBusObject { Q_OBJECT + Q_CLASSINFO("D-Bus Interface", DBUS_INTERFACE_SECRET_SESSION_LITERAL) - explicit Session(std::unique_ptr&& cipher, const QString& peer, Service* parent); + explicit Session(QSharedPointer cipher, const QString& peer, Service* parent); public: - static std::unique_ptr CreateCiphers(const QString& peer, - const QString& algorithm, - const QVariant& input, - QVariant& output, - bool& incomplete); - static void CleanupNegotiation(const QString& peer); - /** * @brief Create a new instance of `Session`. * @param cipher the negotiated cipher @@ -57,23 +45,23 @@ namespace FdoSecrets * This may be caused by * - DBus path registration error */ - static Session* Create(std::unique_ptr&& cipher, const QString& peer, Service* parent); + static Session* Create(QSharedPointer cipher, const QString& peer, Service* parent); - DBusReturn close(); + Q_INVOKABLE DBusResult close(); /** * Encode the secret struct. Note only the value field is encoded. * @param input * @return */ - SecretStruct encode(const SecretStruct& input) const; + Secret encode(const Secret& input) const; /** * Decode the secret struct. * @param input * @return */ - SecretStruct decode(const SecretStruct& input) const; + Secret decode(const Secret& input) const; /** * The peer application that opened this session @@ -93,14 +81,9 @@ namespace FdoSecrets void aboutToClose(); private: - bool registerSelf(); - - private: - std::unique_ptr m_cipher; + QSharedPointer m_cipher; QString m_peer; QUuid m_id; - - static QHash negotiationState; }; } // namespace FdoSecrets diff --git a/src/fdosecrets/objects/SessionCipher.cpp b/src/fdosecrets/objects/SessionCipher.cpp index 26f080c3b..efb6da1fe 100644 --- a/src/fdosecrets/objects/SessionCipher.cpp +++ b/src/fdosecrets/objects/SessionCipher.cpp @@ -149,9 +149,9 @@ namespace FdoSecrets return OKM; } - SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::encrypt(const SecretStruct& input) + Secret DhIetf1024Sha256Aes128CbcPkcs7::encrypt(const Secret& input) { - SecretStruct output = input; + Secret output = input; output.value.clear(); output.parameters.clear(); @@ -187,7 +187,7 @@ namespace FdoSecrets return input; } - SecretStruct DhIetf1024Sha256Aes128CbcPkcs7::decrypt(const SecretStruct& input) + Secret DhIetf1024Sha256Aes128CbcPkcs7::decrypt(const Secret& input) { auto IV = input.parameters; SymmetricCipher decrypter(SymmetricCipher::Aes128, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); @@ -196,7 +196,7 @@ namespace FdoSecrets return input; } bool ok; - SecretStruct output = input; + Secret output = input; output.parameters.clear(); output.value = decrypter.process(input.value, &ok); diff --git a/src/fdosecrets/objects/SessionCipher.h b/src/fdosecrets/objects/SessionCipher.h index 4d656c0af..e1450784b 100644 --- a/src/fdosecrets/objects/SessionCipher.h +++ b/src/fdosecrets/objects/SessionCipher.h @@ -33,8 +33,8 @@ namespace FdoSecrets public: CipherPair() = default; virtual ~CipherPair() = default; - virtual SecretStruct encrypt(const SecretStruct& input) = 0; - virtual SecretStruct decrypt(const SecretStruct& input) = 0; + virtual Secret encrypt(const Secret& input) = 0; + virtual Secret decrypt(const Secret& input) = 0; virtual bool isValid() const = 0; virtual QVariant negotiationOutput() const = 0; }; @@ -46,12 +46,12 @@ namespace FdoSecrets static constexpr const char Algorithm[] = "plain"; PlainCipher() = default; - SecretStruct encrypt(const SecretStruct& input) override + Secret encrypt(const Secret& input) override { return input; } - SecretStruct decrypt(const SecretStruct& input) override + Secret decrypt(const Secret& input) override { return input; } @@ -120,9 +120,9 @@ namespace FdoSecrets explicit DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes); - SecretStruct encrypt(const SecretStruct& input) override; + Secret encrypt(const Secret& input) override; - SecretStruct decrypt(const SecretStruct& input) override; + Secret decrypt(const Secret& input) override; bool isValid() const override; diff --git a/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp b/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp deleted file mode 100644 index 275145b44..000000000 --- a/src/fdosecrets/objects/adaptors/CollectionAdaptor.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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) - { - // p() isn't ready yet as this is called in Parent's constructor - connect(parent, &Collection::itemCreated, this, [this](const Item* item) { - emit ItemCreated(objectPathSafe(item)); - }); - connect(parent, &Collection::itemDeleted, this, [this](const Item* item) { - emit ItemDeleted(objectPathSafe(item)); - }); - connect(parent, &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 deleted file mode 100644 index f5220108e..000000000 --- a/src/fdosecrets/objects/adaptors/CollectionAdaptor.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 deleted file mode 100644 index 93bbc72f0..000000000 --- a/src/fdosecrets/objects/adaptors/DBusAdaptor.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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(Parent* 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 deleted file mode 100644 index 7116041b8..000000000 --- a/src/fdosecrets/objects/adaptors/ItemAdaptor.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 deleted file mode 100644 index 4a6da4bf9..000000000 --- a/src/fdosecrets/objects/adaptors/ItemAdaptor.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 deleted file mode 100644 index ff8a945cd..000000000 --- a/src/fdosecrets/objects/adaptors/PromptAdaptor.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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) - { - // p() isn't ready yet as this is called in Parent's constructor - connect(parent, &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 deleted file mode 100644 index 9f4390819..000000000 --- a/src/fdosecrets/objects/adaptors/PromptAdaptor.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 deleted file mode 100644 index cacf9a994..000000000 --- a/src/fdosecrets/objects/adaptors/ServiceAdaptor.cpp +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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) - { - // p() isn't ready yet as this is called in Parent's constructor - connect(parent, &Service::collectionCreated, this, [this](Collection* coll) { - emit CollectionCreated(objectPathSafe(coll)); - }); - connect(parent, &Service::collectionDeleted, this, [this](Collection* coll) { - emit CollectionDeleted(objectPathSafe(coll)); - }); - connect(parent, &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 deleted file mode 100644 index b369c1273..000000000 --- a/src/fdosecrets/objects/adaptors/ServiceAdaptor.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6597bfffe..000000000 --- a/src/fdosecrets/objects/adaptors/SessionAdaptor.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 deleted file mode 100644 index 408061701..000000000 --- a/src/fdosecrets/objects/adaptors/SessionAdaptor.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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/AccessControlDialog.cpp b/src/fdosecrets/widgets/AccessControlDialog.cpp new file mode 100644 index 000000000..1cc3a983d --- /dev/null +++ b/src/fdosecrets/widgets/AccessControlDialog.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2013 Francois Ferrand + * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 3 of the License, or + * (at your option) any later version. + * + * 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 "AccessControlDialog.h" +#include "ui_AccessControlDialog.h" + +#include "fdosecrets/widgets/RowButtonHelper.h" + +#include "core/Entry.h" + +#include + +#include + +AccessControlDialog::AccessControlDialog(QWindow* parent, + const QList& entries, + const QString& app, + AuthOptions authOptions) + : m_ui(new Ui::AccessControlDialog()) + , m_model(new EntryModel(entries)) +{ + if (parent) { + // Force the creation of the QWindow, without this windowHandle() will return nullptr + winId(); + auto window = windowHandle(); + Q_ASSERT(window); + window->setTransientParent(parent); + } + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + + m_ui->setupUi(this); + + connect(m_ui->cancelButton, &QPushButton::clicked, this, [this]() { done(DenyAll); }); + connect(m_ui->allowButton, &QPushButton::clicked, this, [this]() { done(AllowSelected); }); + connect(m_ui->itemsTable, &QTableView::clicked, m_model.data(), &EntryModel::toggleCheckState); + connect(m_ui->rememberCheck, &QCheckBox::clicked, this, &AccessControlDialog::rememberChecked); + connect(this, &QDialog::finished, this, &AccessControlDialog::dialogFinished); + + m_ui->rememberMsg->setCloseButtonVisible(false); + m_ui->rememberMsg->setMessageType(MessageWidget::Information); + + m_ui->appLabel->setText(m_ui->appLabel->text().arg(app)); + + m_ui->itemsTable->setModel(m_model.data()); + installWidgetItemDelegate(m_ui->itemsTable, 2, [this](QWidget* p, const QModelIndex& idx) { + auto btn = new DenyButton(p, idx); + connect(btn, &DenyButton::clicked, this, &AccessControlDialog::denyEntryClicked); + return btn; + }); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + m_ui->itemsTable->resizeColumnsToContents(); + + if (!authOptions.testFlag(AuthOption::Remember)) { + m_ui->rememberCheck->setHidden(true); + m_ui->rememberCheck->setChecked(false); + } + if (!authOptions.testFlag(AuthOption::PerEntryDeny)) { + m_ui->itemsTable->horizontalHeader()->setSectionHidden(2, true); + } + + m_ui->allowButton->setFocus(); +} + +AccessControlDialog::~AccessControlDialog() = default; + +void AccessControlDialog::rememberChecked(bool checked) +{ + if (checked) { + m_ui->rememberMsg->animatedShow(); + } else { + m_ui->rememberMsg->animatedHide(); + } +} + +void AccessControlDialog::denyEntryClicked(Entry* entry, const QModelIndex& index) +{ + m_decisions.insert(entry, AuthDecision::Denied); + m_model->removeRow(index.row()); + if (m_model->rowCount({}) == 0) { + reject(); + } +} + +void AccessControlDialog::dialogFinished(int result) +{ + auto allow = m_ui->rememberCheck->isChecked() ? AuthDecision::Allowed : AuthDecision::AllowedOnce; + auto deny = m_ui->rememberCheck->isChecked() ? AuthDecision::Denied : AuthDecision::DeniedOnce; + + for (int row = 0; row != m_model->rowCount({}); ++row) { + auto entry = m_model->data(m_model->index(row, 2), Qt::EditRole).value(); + auto selected = m_model->data(m_model->index(row, 0), Qt::CheckStateRole).value(); + Q_ASSERT(entry); + switch (result) { + case AllowSelected: + if (selected) { + m_decisions.insert(entry, allow); + } else { + m_decisions.insert(entry, AuthDecision::Undecided); + } + break; + case DenyAll: + m_decisions.insert(entry, deny); + break; + case Rejected: + default: + m_decisions.insert(entry, AuthDecision::Undecided); + break; + } + } + + emit finished(m_decisions); +} + +QHash AccessControlDialog::decisions() const +{ + return m_decisions; +} + +AccessControlDialog::EntryModel::EntryModel(QList entries, QObject* parent) + : QAbstractTableModel(parent) + , m_entries(std::move(entries)) + , m_selected(QSet::fromList(m_entries)) +{ +} + +int AccessControlDialog::EntryModel::rowCount(const QModelIndex& parent) const +{ + return isValid(parent) ? 0 : m_entries.count(); +} + +int AccessControlDialog::EntryModel::columnCount(const QModelIndex& parent) const +{ + return isValid(parent) ? 0 : 3; +} + +bool AccessControlDialog::EntryModel::isValid(const QModelIndex& index) const +{ + return index.isValid() && index.row() < rowCount({}) && index.column() < columnCount({}); +} + +void AccessControlDialog::EntryModel::toggleCheckState(const QModelIndex& index) +{ + if (!isValid(index)) { + return; + } + auto entry = m_entries.at(index.row()); + // click anywhere in the row to check/uncheck the item + auto it = m_selected.find(entry); + if (it == m_selected.end()) { + m_selected.insert(entry); + } else { + m_selected.erase(it); + } + auto rowIdx = index.sibling(index.row(), 0); + emit dataChanged(rowIdx, rowIdx, {Qt::CheckStateRole}); +} + +QVariant AccessControlDialog::EntryModel::data(const QModelIndex& index, int role) const +{ + if (!isValid(index)) { + return {}; + } + auto entry = m_entries.at(index.row()); + + switch (index.column()) { + case 0: + switch (role) { + case Qt::DisplayRole: + return entry->title(); + case Qt::DecorationRole: + return entry->icon(); + case Qt::CheckStateRole: + return QVariant::fromValue(m_selected.contains(entry) ? Qt::Checked : Qt::Unchecked); + default: + return {}; + } + case 1: + switch (role) { + case Qt::DisplayRole: + return entry->username(); + default: + return {}; + } + case 2: + switch (role) { + case Qt::EditRole: + return QVariant::fromValue(entry); + default: + return {}; + } + default: + return {}; + } +} + +bool AccessControlDialog::EntryModel::removeRows(int row, int count, const QModelIndex& parent) +{ + beginRemoveRows(parent, row, row + count - 1); + while (count--) { + m_entries.removeAt(row); + } + endRemoveRows(); + return true; +} + +AccessControlDialog::DenyButton::DenyButton(QWidget* p, const QModelIndex& idx) + : QPushButton(p) + , m_index(idx) + , m_entry() +{ + setText(tr("Deny for this program")); + connect(this, &QPushButton::clicked, [this]() { emit clicked(entry(), m_index); }); +} + +void AccessControlDialog::DenyButton::setEntry(Entry* e) +{ + m_entry = e; +} + +Entry* AccessControlDialog::DenyButton::entry() const +{ + return m_entry; +} diff --git a/src/fdosecrets/widgets/AccessControlDialog.h b/src/fdosecrets/widgets/AccessControlDialog.h new file mode 100644 index 000000000..0c394b123 --- /dev/null +++ b/src/fdosecrets/widgets/AccessControlDialog.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2013 Francois Ferrand + * Copyright (C) 2017 KeePassXC Team + * Copyright (C) 2020 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 3 of the License, or + * (at your option) any later version. + * + * 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_ACCESSCONTROLDIALOG_H +#define KEEPASSXC_FDOSECRETS_ACCESSCONTROLDIALOG_H + +#include +#include +#include +#include +#include +#include + +#include "core/Global.h" + +class Entry; + +namespace Ui +{ + class AccessControlDialog; +} + +enum class AuthOption +{ + None = 0, + Remember = 1 << 1, + PerEntryDeny = 1 << 2, +}; +Q_DECLARE_FLAGS(AuthOptions, AuthOption); +Q_DECLARE_OPERATORS_FOR_FLAGS(AuthOptions); + +class AccessControlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AccessControlDialog(QWindow* parent, + const QList& entries, + const QString& app, + AuthOptions authOptions = AuthOption::Remember | AuthOption::PerEntryDeny); + ~AccessControlDialog() override; + + enum DialogCode + { + Rejected, + AllowSelected, + DenyAll, + }; + + QHash decisions() const; + +signals: + void finished(const QHash& results); + +private slots: + void rememberChecked(bool checked); + void denyEntryClicked(Entry* entry, const QModelIndex& index); + void dialogFinished(int result); + +private: + class EntryModel; + class DenyButton; + + QScopedPointer m_ui; + QScopedPointer m_model; + QHash m_decisions; +}; + +class AccessControlDialog::EntryModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit EntryModel(QList entries, QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool removeRows(int row, int count, const QModelIndex& parent) override; + +public slots: + void toggleCheckState(const QModelIndex& index); + +private: + bool isValid(const QModelIndex& index) const; + + QList m_entries; + QSet m_selected; +}; + +class AccessControlDialog::DenyButton : public QPushButton +{ + Q_OBJECT + + Q_PROPERTY(Entry* entry READ entry WRITE setEntry USER true) + + QPersistentModelIndex m_index; + QPointer m_entry; + +public: + explicit DenyButton(QWidget* p, const QModelIndex& idx); + + void setEntry(Entry* e); + Entry* entry() const; + +signals: + void clicked(Entry*, const QModelIndex& idx); +}; + +#endif // KEEPASSXC_FDOSECRETS_ACCESSCONTROLDIALOG_H diff --git a/src/fdosecrets/widgets/AccessControlDialog.ui b/src/fdosecrets/widgets/AccessControlDialog.ui new file mode 100644 index 000000000..2de17d5b8 --- /dev/null +++ b/src/fdosecrets/widgets/AccessControlDialog.ui @@ -0,0 +1,133 @@ + + + AccessControlDialog + + + + 0 + 0 + 405 + 252 + + + + KeePassXC - Access Request + + + + + + + 50 + false + + + + <html><head/><body><p><span style=" font-weight:600;">%1 </span>is requesting access to the following entries:</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + Your decision for above entries will be remembered for the duration the requesting client is running. + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remember + + + true + + + + + + + Allow access to entries + + + Allow Selected + + + true + + + true + + + + + + + Deny All + + + true + + + + + + + + + + MessageWidget + QWidget +
gui/MessageWidget.h
+ 1 +
+
+ + +
diff --git a/src/fdosecrets/widgets/RowButtonHelper.cpp b/src/fdosecrets/widgets/RowButtonHelper.cpp new file mode 100644 index 000000000..b2f816802 --- /dev/null +++ b/src/fdosecrets/widgets/RowButtonHelper.cpp @@ -0,0 +1,68 @@ +/* + * 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 "RowButtonHelper.h" + +#include +#include +#include + +#include + +namespace +{ + class WidgetItemDelegate : public QStyledItemDelegate + { + std::function m_create; + + public: + explicit WidgetItemDelegate(QObject* parent, std::function&& create) + : QStyledItemDelegate(parent) + , m_create(std::move(create)) + { + } + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem&, const QModelIndex& index) const override + { + if (!index.isValid()) + return nullptr; + return m_create(parent, index); + } + }; +} // namespace + +void installWidgetItemDelegate(QAbstractItemView* view, + int column, + std::function&& create) +{ + auto delegate = new WidgetItemDelegate(view, std::move(create)); + // doesn't take ownership + view->setItemDelegateForColumn(column, delegate); + + for (int row = 0; row != view->model()->rowCount({}); ++row) { + view->openPersistentEditor(view->model()->index(row, column)); + } + QObject::connect(view->model(), + &QAbstractItemModel::rowsInserted, + delegate, + [view, column](const QModelIndex&, int first, int last) { + for (int i = first; i <= last; ++i) { + auto idx = view->model()->index(i, column); + view->openPersistentEditor(idx); + } + }); +} diff --git a/src/fdosecrets/widgets/RowButtonHelper.h b/src/fdosecrets/widgets/RowButtonHelper.h new file mode 100644 index 000000000..476787138 --- /dev/null +++ b/src/fdosecrets/widgets/RowButtonHelper.h @@ -0,0 +1,42 @@ +/* + * 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_ROWBUTTONHELPER_H +#define KEEPASSXC_FDOSECRETS_ROWBUTTONHELPER_H + +#include + +class QAbstractItemView; +class QWidget; +class QModelIndex; + +void installWidgetItemDelegate(QAbstractItemView* view, + int column, + std::function&& create); + +/** + * @brief Open an editor on the cell, the editor's user property will be edited. + */ +template +void installWidgetItemDelegate(QAbstractItemView* view, + int column, + std::function&& create) +{ + installWidgetItemDelegate(view, column, [create](QWidget* p, const QModelIndex& idx) { return create(p, idx); }); +} + +#endif // KEEPASSXC_FDOSECRETS_ROWBUTTONHELPER_H diff --git a/src/fdosecrets/widgets/SettingsModels.cpp b/src/fdosecrets/widgets/SettingsModels.cpp index 70372a2a2..aa2b33ada 100644 --- a/src/fdosecrets/widgets/SettingsModels.cpp +++ b/src/fdosecrets/widgets/SettingsModels.cpp @@ -19,11 +19,10 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Service.h" #include "fdosecrets/objects/Session.h" -#include "core/Database.h" -#include "core/DatabaseIcons.h" #include "gui/DatabaseTabWidget.h" #include "gui/DatabaseWidget.h" #include "gui/Icons.h" @@ -244,35 +243,22 @@ namespace FdoSecrets } } - SettingsSessionModel::SettingsSessionModel(FdoSecretsPlugin* plugin, QObject* parent) + SettingsClientModel::SettingsClientModel(DBusMgr& dbus, QObject* parent) : QAbstractTableModel(parent) - , m_service(nullptr) + , m_dbus(dbus) { - setService(plugin->serviceInstance()); - connect(plugin, &FdoSecretsPlugin::secretServiceStarted, this, [plugin, this]() { - setService(plugin->serviceInstance()); - }); - connect(plugin, &FdoSecretsPlugin::secretServiceStopped, this, [this]() { setService(nullptr); }); + populateModel(); } - void SettingsSessionModel::setService(Service* service) - { - auto old = m_service; - m_service = service; - if (old != m_service) { - populateModel(); - } - } - - int SettingsSessionModel::rowCount(const QModelIndex& parent) const + int SettingsClientModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; } - return m_sessions.size(); + return m_clients.size(); } - int SettingsSessionModel::columnCount(const QModelIndex& parent) const + int SettingsClientModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) { return 0; @@ -280,7 +266,7 @@ namespace FdoSecrets return 2; } - QVariant SettingsSessionModel::headerData(int section, Qt::Orientation orientation, int role) const + QVariant SettingsClientModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal) { return {}; @@ -300,89 +286,88 @@ namespace FdoSecrets } } - QVariant SettingsSessionModel::data(const QModelIndex& index, int role) const + QVariant SettingsClientModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return {}; } - const auto& sess = m_sessions[index.row()]; - if (!sess) { + const auto& client = m_clients[index.row()]; + if (!client) { return {}; } switch (index.column()) { case 0: - return dataForApplication(sess, role); + return dataForApplication(client, role); case 1: - return dataForManage(sess, role); + return dataForManage(client, role); default: return {}; } } - QVariant SettingsSessionModel::dataForApplication(Session* sess, int role) const + QVariant SettingsClientModel::dataForApplication(const DBusClientPtr& client, int role) const { switch (role) { case Qt::DisplayRole: - return sess->peer(); + return client->name(); default: return {}; } } - QVariant SettingsSessionModel::dataForManage(Session* sess, int role) const + QVariant SettingsClientModel::dataForManage(const DBusClientPtr& client, int role) const { switch (role) { case Qt::EditRole: { - return QVariant::fromValue(sess); + return QVariant::fromValue(client); } default: return {}; } } - void SettingsSessionModel::populateModel() + void SettingsClientModel::populateModel() { beginResetModel(); - m_sessions.clear(); + m_clients.clear(); - if (m_service) { - // Add existing database tabs - for (const auto& sess : m_service->sessions()) { - sessionAdded(sess, false); - } - - // connect signals - connect(m_service, &Service::sessionOpened, this, [this](Session* sess) { sessionAdded(sess, true); }); - connect(m_service, &Service::sessionClosed, this, &SettingsSessionModel::sessionRemoved); + // Add existing database tabs + for (const auto& client : m_dbus.clients()) { + clientConnected(client, false); } + // connect signals + connect(&m_dbus, &DBusMgr::clientConnected, this, [this](const DBusClientPtr& client) { + clientConnected(client, true); + }); + connect(&m_dbus, &DBusMgr::clientDisconnected, this, &SettingsClientModel::clientDisconnected); + endResetModel(); } - void SettingsSessionModel::sessionAdded(Session* sess, bool emitSignals) + void SettingsClientModel::clientConnected(const DBusClientPtr& client, bool emitSignals) { - int row = m_sessions.size(); + int row = m_clients.size(); if (emitSignals) { beginInsertRows({}, row, row); } - m_sessions.append(sess); + m_clients.append(client); if (emitSignals) { endInsertRows(); } } - void SettingsSessionModel::sessionRemoved(Session* sess) + void SettingsClientModel::clientDisconnected(const DBusClientPtr& client) { - for (int i = 0; i != m_sessions.size(); i++) { - if (m_sessions[i] == sess) { + for (int i = 0; i != m_clients.size(); i++) { + if (m_clients[i] == client) { beginRemoveRows({}, i, i); - m_sessions[i]->disconnect(this); - m_sessions.removeAt(i); + m_clients.removeAt(i); endRemoveRows(); break; diff --git a/src/fdosecrets/widgets/SettingsModels.h b/src/fdosecrets/widgets/SettingsModels.h index b07bb1637..e933f5cfa 100644 --- a/src/fdosecrets/widgets/SettingsModels.h +++ b/src/fdosecrets/widgets/SettingsModels.h @@ -18,12 +18,13 @@ #ifndef KEEPASSXC_FDOSECRETS_SETTINGSMODELS_H #define KEEPASSXC_FDOSECRETS_SETTINGSMODELS_H +#include "fdosecrets/dbus/DBusClient.h" + #include #include class DatabaseTabWidget; class DatabaseWidget; -class FdoSecretsPlugin; namespace FdoSecrets { @@ -58,14 +59,13 @@ namespace FdoSecrets QList> m_dbs; }; - class Service; - class Session; + class DBusMgr; - class SettingsSessionModel : public QAbstractTableModel + class SettingsClientModel : public QAbstractTableModel { Q_OBJECT public: - explicit SettingsSessionModel(FdoSecretsPlugin* plugin, QObject* parent = nullptr); + explicit SettingsClientModel(DBusMgr& dbus, QObject* parent = nullptr); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; @@ -73,22 +73,20 @@ namespace FdoSecrets QVariant headerData(int section, Qt::Orientation orientation, int role) const override; private: - void setService(Service* service); - - QVariant dataForApplication(Session* sess, int role) const; - QVariant dataForManage(Session* sess, int role) const; + QVariant dataForApplication(const DBusClientPtr& client, int role) const; + QVariant dataForManage(const DBusClientPtr& client, int role) const; private slots: void populateModel(); - void sessionAdded(Session* sess, bool emitSignals); - void sessionRemoved(Session* sess); + void clientConnected(const DBusClientPtr& client, bool emitSignals); + void clientDisconnected(const DBusClientPtr& client); private: // source - QPointer m_service; + DBusMgr& m_dbus; // internal copy, so we can emit with changed index - QList m_sessions; + QList m_clients; }; } // namespace FdoSecrets diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp index 731695932..5b7b8054b 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.cpp @@ -20,24 +20,21 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Session.h" +#include "fdosecrets/widgets/RowButtonHelper.h" #include "fdosecrets/widgets/SettingsModels.h" #include "gui/DatabaseWidget.h" #include "gui/Icons.h" #include -#include -#include #include -#include -#include #include -#include -using FdoSecrets::Session; +using FdoSecrets::DBusClientPtr; +using FdoSecrets::SettingsClientModel; using FdoSecrets::SettingsDatabaseModel; -using FdoSecrets::SettingsSessionModel; namespace { @@ -158,10 +155,10 @@ namespace { Q_OBJECT - Q_PROPERTY(Session* session READ session WRITE setSession USER true) + Q_PROPERTY(const DBusClientPtr& client READ client WRITE setClient USER true) public: - explicit ManageSession(FdoSecretsPlugin*, QWidget* parent = nullptr) + explicit ManageSession(QWidget* parent = nullptr) : QToolBar(parent) { setFloatable(false); @@ -173,15 +170,15 @@ namespace spacer->setVisible(true); addWidget(spacer); - m_disconnectAct = new QAction(tr("Disconnect"), this); - m_disconnectAct->setIcon(icons()->icon(QStringLiteral("dialog-close"))); - m_disconnectAct->setToolTip(tr("Disconnect this application")); - connect(m_disconnectAct, &QAction::triggered, this, [this]() { - if (m_session) { - m_session->close(); + auto disconnectAct = new QAction(tr("Disconnect"), this); + disconnectAct->setIcon(icons()->icon(QStringLiteral("dialog-close"))); + disconnectAct->setToolTip(tr("Disconnect this application")); + connect(disconnectAct, &QAction::triggered, this, [this]() { + if (m_client) { + m_client->disconnectDBus(); } }); - addAction(m_disconnectAct); + addAction(disconnectAct); // use a dummy widget to center the buttons spacer = new QWidget(this); @@ -190,71 +187,46 @@ namespace addWidget(spacer); } - Session* session() + const DBusClientPtr& client() const { - return m_session; + return m_client; } - void setSession(Session* sess) + void setClient(DBusClientPtr client) { - m_session = sess; + m_client = std::move(client); } private: - Session* m_session = nullptr; - QAction* m_disconnectAct = nullptr; - }; - - template class Creator : public QItemEditorCreatorBase - { - public: - inline explicit Creator(FdoSecretsPlugin* plugin) - : QItemEditorCreatorBase() - , m_plugin(plugin) - , m_propertyName(T::staticMetaObject.userProperty().name()) - { - } - - inline QWidget* createWidget(QWidget* parent) const override - { - return new T(m_plugin, parent); - } - - inline QByteArray valuePropertyName() const override - { - return m_propertyName; - } - - private: - FdoSecretsPlugin* m_plugin; - QByteArray m_propertyName; + DBusClientPtr m_client{}; }; } // namespace SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWidget* parent) : QWidget(parent) , m_ui(new Ui::SettingsWidgetFdoSecrets()) - , m_factory(new QItemEditorFactory) , m_plugin(plugin) { m_ui->setupUi(this); m_ui->warningMsg->setHidden(true); m_ui->warningMsg->setCloseButtonVisible(false); - auto sessModel = new SettingsSessionModel(plugin, this); - m_ui->tableSessions->setModel(sessModel); - setupView(m_ui->tableSessions, 1, qMetaTypeId(), new Creator(m_plugin)); + auto clientModel = new SettingsClientModel(*plugin->dbus(), this); + m_ui->tableClients->setModel(clientModel); + installWidgetItemDelegate( + m_ui->tableClients, 1, [](QWidget* p, const QModelIndex&) { return new ManageSession(p); }); // config header after setting model, otherwise the header doesn't have enough sections - auto sessViewHeader = m_ui->tableSessions->horizontalHeader(); - sessViewHeader->setSelectionMode(QAbstractItemView::NoSelection); - sessViewHeader->setSectionsClickable(false); - sessViewHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application - sessViewHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button + auto clientViewHeader = m_ui->tableClients->horizontalHeader(); + clientViewHeader->setSelectionMode(QAbstractItemView::NoSelection); + clientViewHeader->setSectionsClickable(false); + clientViewHeader->setSectionResizeMode(0, QHeaderView::Stretch); // application + clientViewHeader->setSectionResizeMode(1, QHeaderView::ResizeToContents); // disconnect button auto dbModel = new SettingsDatabaseModel(plugin->dbTabs(), this); m_ui->tableDatabases->setModel(dbModel); - setupView(m_ui->tableDatabases, 2, qMetaTypeId(), new Creator(m_plugin)); + installWidgetItemDelegate( + m_ui->tableDatabases, 2, [plugin](QWidget* p, const QModelIndex&) { return new ManageDatabase(plugin, p); }); // config header after setting model, otherwise the header doesn't have enough sections auto dbViewHeader = m_ui->tableDatabases->horizontalHeader(); @@ -277,40 +249,22 @@ SettingsWidgetFdoSecrets::SettingsWidgetFdoSecrets(FdoSecretsPlugin* plugin, QWi connect(m_plugin, SIGNAL(secretServiceStopped()), &m_checkTimer, SLOT(start())); } -void SettingsWidgetFdoSecrets::setupView(QAbstractItemView* view, - int manageColumn, - int editorTypeId, - QItemEditorCreatorBase* creator) -{ - auto manageButtonDelegate = new QStyledItemDelegate(this); - m_factory->registerEditor(editorTypeId, creator); - manageButtonDelegate->setItemEditorFactory(m_factory.data()); - view->setItemDelegateForColumn(manageColumn, manageButtonDelegate); - connect(view->model(), - &QAbstractItemModel::rowsInserted, - this, - [view, manageColumn](const QModelIndex&, int first, int last) { - for (int i = first; i <= last; ++i) { - auto idx = view->model()->index(i, manageColumn); - view->openPersistentEditor(idx); - } - }); -} - SettingsWidgetFdoSecrets::~SettingsWidgetFdoSecrets() = default; void SettingsWidgetFdoSecrets::loadSettings() { m_ui->enableFdoSecretService->setChecked(FdoSecrets::settings()->isEnabled()); m_ui->showNotification->setChecked(FdoSecrets::settings()->showNotification()); - m_ui->noConfirmDeleteItem->setChecked(FdoSecrets::settings()->noConfirmDeleteItem()); + m_ui->confirmDeleteItem->setChecked(FdoSecrets::settings()->confirmDeleteItem()); + m_ui->confirmAccessItem->setChecked(FdoSecrets::settings()->confirmAccessItem()); } void SettingsWidgetFdoSecrets::saveSettings() { FdoSecrets::settings()->setEnabled(m_ui->enableFdoSecretService->isChecked()); FdoSecrets::settings()->setShowNotification(m_ui->showNotification->isChecked()); - FdoSecrets::settings()->setNoConfirmDeleteItem(m_ui->noConfirmDeleteItem->isChecked()); + FdoSecrets::settings()->setConfirmDeleteItem(m_ui->confirmDeleteItem->isChecked()); + FdoSecrets::settings()->setConfirmAccessItem(m_ui->confirmAccessItem->isChecked()); } void SettingsWidgetFdoSecrets::showEvent(QShowEvent* event) @@ -333,17 +287,9 @@ void SettingsWidgetFdoSecrets::checkDBusName() return; } - auto reply = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral(DBUS_SERVICE_SECRET)); - if (!reply.isValid()) { + if (m_plugin->dbus()->serviceOccupied()) { m_ui->warningMsg->showMessage( - tr("Error: Failed to connect to DBus. Please check your DBus setup."), MessageWidget::Error, -1); - m_ui->enableFdoSecretService->setChecked(false); - m_ui->enableFdoSecretService->setEnabled(false); - return; - } - if (reply.value()) { - m_ui->warningMsg->showMessage( - tr("Warning: ") + m_plugin->reportExistingService(), MessageWidget::Warning, -1); + tr("Warning: ") + m_plugin->dbus()->reportExistingService(), MessageWidget::Warning, -1); m_ui->enableFdoSecretService->setChecked(false); m_ui->enableFdoSecretService->setEnabled(false); return; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h index c323b3900..c4a58a5eb 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.h @@ -25,16 +25,6 @@ #include class QAbstractItemView; -class QItemEditorCreatorBase; -class QItemEditorFactory; - -namespace FdoSecrets -{ - - class Session; - class Collection; - -} // namespace FdoSecrets class FdoSecretsPlugin; @@ -61,12 +51,8 @@ protected: void showEvent(QShowEvent* event) override; void hideEvent(QHideEvent* event) override; -private: - void setupView(QAbstractItemView* view, int manageColumn, int editorTypeId, QItemEditorCreatorBase* creator); - private: QScopedPointer m_ui; - QScopedPointer m_factory; FdoSecretsPlugin* m_plugin; QTimer m_checkTimer; }; diff --git a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui index abc15d56e..7034d7bd6 100644 --- a/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui +++ b/src/fdosecrets/widgets/SettingsWidgetFdoSecrets.ui @@ -49,17 +49,36 @@ - Show notification when credentials are requested + Show notification when passwords are retrieved by clients + + + true - + - <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> + <html><head/><body><p>If enabled, any attempt to read a password must be confirmed. Otherwise, clients can read passwords without confirmation when the database is unlocked.</p><p>This option only covers the access to the password of an entry. Clients can always enumerate the items of exposed databases and query their attributes.</p></body></html> - Don't confirm when entries are deleted by clients + Confirm when passwords are retrieved by clients + + + true + + + + + + + <html><head/><body><p><span style=" font-family:'-apple-system','BlinkMacSystemFont','Segoe UI','Helvetica','Arial','sans-serif','Apple Color Emoji','Segoe UI Emoji'; font-size:14px; color:#24292e; background-color:#ffffff;">This setting does not override disabling recycle bin prompts</span></p></body></html> + + + Confirm when clients request entry deletion + + + true @@ -120,7 +139,7 @@
- + Qt::NoFocus diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 2d640aeb5..41c1cc6a9 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -474,20 +474,20 @@ void DatabaseWidget::deleteSelectedEntries() deleteEntries(std::move(selectedEntries)); } -void DatabaseWidget::deleteEntries(QList selectedEntries) +void DatabaseWidget::deleteEntries(QList selectedEntries, bool confirm) { // Confirm entry removal before moving forward auto* recycleBin = m_db->metadata()->recycleBin(); bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid())) || !m_db->metadata()->recycleBinEnabled(); - if (!confirmDeleteEntries(selectedEntries, permanent)) { + if (confirm && !confirmDeleteEntries(selectedEntries, permanent)) { return; } // Find references to selected entries and prompt for direction if necessary auto it = selectedEntries.begin(); - while (it != selectedEntries.end()) { + while (confirm && it != selectedEntries.end()) { auto references = m_db->rootGroup()->referencesRecursive(*it); if (!references.isEmpty()) { // Ignore references that are selected for deletion diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index efe602121..fc8d9ee73 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -162,7 +162,7 @@ public slots: void createEntry(); void cloneEntry(); void deleteSelectedEntries(); - void deleteEntries(QList entries); + void deleteEntries(QList entries, bool confirm = true); void focusOnEntries(bool editIfFocused = false); void focusOnGroups(bool editIfFocused = false); void moveEntryUp(); diff --git a/tests/TestFdoSecrets.cpp b/tests/TestFdoSecrets.cpp index 299c3f7bf..eba97f67c 100644 --- a/tests/TestFdoSecrets.cpp +++ b/tests/TestFdoSecrets.cpp @@ -20,13 +20,13 @@ #include "TestGlobal.h" #include "core/EntrySearcher.h" +#include "crypto/Crypto.h" #include "fdosecrets/GcryptMPI.h" +#include "fdosecrets/dbus/DBusMgr.h" #include "fdosecrets/objects/Collection.h" #include "fdosecrets/objects/Item.h" #include "fdosecrets/objects/SessionCipher.h" -#include "crypto/Crypto.h" - QTEST_GUILESS_MAIN(TestFdoSecrets) void TestFdoSecrets::initTestCase() @@ -144,3 +144,39 @@ void TestFdoSecrets::testSpecialCharsInAttributeValue() QCOMPARE(res[0]->title(), QStringLiteral("titleB")); } } + +void TestFdoSecrets::testDBusPathParse() +{ + using FdoSecrets::DBusMgr; + using PathType = FdoSecrets::DBusMgr::PathType; + + auto parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets")); + QCOMPARE(parsed.type, PathType::Service); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/collection/xxx")); + QCOMPARE(parsed.type, PathType::Collection); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/collection/xxx/yyy")); + QCOMPARE(parsed.type, PathType::Item); + QCOMPARE(parsed.id, QStringLiteral("yyy")); + QCOMPARE(parsed.parentId, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/aliases/xxx")); + QCOMPARE(parsed.type, PathType::Aliases); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/session/xxx")); + QCOMPARE(parsed.type, PathType::Session); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/secrets/prompt/xxx")); + QCOMPARE(parsed.type, PathType::Prompt); + QCOMPARE(parsed.id, QStringLiteral("xxx")); + + parsed = DBusMgr::parsePath(QStringLiteral("/org/freedesktop/other/prompt/xxx")); + QCOMPARE(parsed.type, PathType::Unknown); + + parsed = DBusMgr::parsePath(QStringLiteral("/org")); + QCOMPARE(parsed.type, PathType::Unknown); +} diff --git a/tests/TestFdoSecrets.h b/tests/TestFdoSecrets.h index 1cecbbeac..c41a6578f 100644 --- a/tests/TestFdoSecrets.h +++ b/tests/TestFdoSecrets.h @@ -32,6 +32,7 @@ private slots: void testDhIetf1024Sha256Aes128CbcPkcs7(); void testCrazyAttributeKey(); void testSpecialCharsInAttributeValue(); + void testDBusPathParse(); }; #endif // KEEPASSXC_TESTFDOSECRETS_H diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index a6d6adb1707f66cae69a44f51c092070d50a7e71..dfffcc1e4e037bc90059f4b7fb1f81e5d2ed1d61 100644 GIT binary patch literal 20590 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 literal 20334 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 Re=GOn%~jn6yKMhW0sxLNVyFNB diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml deleted file mode 100644 index 3b5dd64fd..000000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Collection.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml deleted file mode 100644 index d9c39a2e9..000000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Item.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml deleted file mode 100644 index 92aa8df84..000000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Prompt.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml deleted file mode 100644 index 40240bb43..000000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Service.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml b/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml deleted file mode 100644 index 7d358df7b..000000000 --- a/tests/data/dbus/interfaces/org.freedesktop.Secret.Session.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/tests/gui/CMakeLists.txt b/tests/gui/CMakeLists.txt index 1d5822d20..3264da515 100644 --- a/tests/gui/CMakeLists.txt +++ b/tests/gui/CMakeLists.txt @@ -24,7 +24,7 @@ endif() if(WITH_XC_FDOSECRETS) add_unit_test(NAME testguifdosecrets - SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp + SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp ../util/FdoSecretsProxy.cpp LIBS ${TEST_LIBRARIES} # The following doesn't work because dbus-run-session expects execname to be in PATH # dbus-run-session -- execname diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp index eb8192d5f..555021274 100644 --- a/tests/gui/TestGuiFdoSecrets.cpp +++ b/tests/gui/TestGuiFdoSecrets.cpp @@ -19,12 +19,12 @@ #include "fdosecrets/FdoSecretsPlugin.h" #include "fdosecrets/FdoSecretsSettings.h" +#include "fdosecrets/dbus/DBusClient.h" +#include "fdosecrets/dbus/DBusMgr.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" #include "fdosecrets/objects/SessionCipher.h" +#include "fdosecrets/widgets/AccessControlDialog.h" #include "TestGlobal.h" #include "config-keepassx-tests.h" @@ -39,17 +39,13 @@ #include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "gui/wizard/NewDatabaseWizard.h" +#include "util/FdoSecretsProxy.h" #include "util/TemporaryFile.h" -#include -#include -#include -#include +#include #include -#include #include #include -#include #include #include @@ -74,111 +70,77 @@ int main(int argc, char* argv[]) #define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default" -#define VERIFY(statement) \ +// assert macros compatible with function having return values +#define VERIFY2_RET(statement, msg) \ do { \ - if (!QTest::qVerify(static_cast(statement), #statement, "", __FILE__, __LINE__)) \ + if (!QTest::qVerify(static_cast(statement), #statement, (msg), __FILE__, __LINE__)) \ return {}; \ } while (false) -#define COMPARE(actual, expected) \ +#define COMPARE_RET(actual, expected) \ do { \ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ return {}; \ } while (false) -#define FAIL(message) \ +// by default use these with Qt macros +#define VERIFY QVERIFY +#define COMPARE QCOMPARE +#define VERIFY2 QVERIFY2 + +#define DBUS_COMPARE(actual, expected) \ do { \ - QTest::qFail(static_cast(message), __FILE__, __LINE__); \ - return {}; \ + auto reply = (actual); \ + VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \ + COMPARE(reply.value(), (expected)); \ } while (false) -#define COMPARE_DBUS_LOCAL_CALL(actual, expected) \ +#define DBUS_VERIFY(stmt) \ do { \ - const auto a = (actual); \ - QVERIFY(!a.isError()); \ - QCOMPARE(a.value(), (expected)); \ + auto reply = (stmt); \ + VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \ } while (false) -#define CHECKED_DBUS_LOCAL_CALL(name, stmt) \ - std::remove_cv::type name; \ +#define DBUS_GET(var, stmt) \ + std::remove_cv())>::type var; \ do { \ - const auto rep = stmt; \ - QVERIFY(!rep.isError()); \ - name = rep.value(); \ + const auto rep = (stmt); \ + VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \ + var = rep.argumentAt<0>(); \ } while (false) -namespace -{ - std::unique_ptr interfaceOf(const QDBusObjectPath& objPath, const QString& interface) - { - std::unique_ptr iface(new QDBusInterface(DBUS_SERVICE_SECRET, objPath.path(), interface)); - iface->setTimeout(5); - VERIFY(iface->isValid()); - return iface; - } - - std::unique_ptr interfaceOf(FdoSecrets::DBusObject* obj) - { - VERIFY(obj); - auto metaAdaptor = obj->dbusAdaptor().metaObject(); - auto ifaceName = metaAdaptor->classInfo(metaAdaptor->indexOfClassInfo("D-Bus Interface")).value(); - - return interfaceOf(obj->objectPath(), ifaceName); - } - - template QString extractElement(const QString& doc, T cond) - { - QXmlStreamReader reader(doc); - while (!reader.atEnd()) { - int st = reader.characterOffset(); - - if (reader.readNext() != QXmlStreamReader::StartElement || !cond(reader)) { - continue; - } - - reader.skipCurrentElement(); - if (reader.hasError()) { - break; - } - - // remove whitespaces between elements to be a little bit flexible - int ed = reader.characterOffset(); - return doc.mid(st - 1, ed - st + 1).replace(QRegularExpression(R"(>[\s\n]+<)"), "><"); - } - VERIFY(!reader.hasError()); - return {}; - } - - bool checkDBusSpec(const QString& path, const QString& interface) - { - QFile f(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/dbus/interfaces/%1.xml").arg(interface)); - VERIFY(f.open(QFile::ReadOnly | QFile::Text)); - QTextStream in(&f); - auto spec = in.readAll().replace(QRegularExpression(R"(>[\s\n]+<)"), "><").trimmed(); - - auto bus = QDBusConnection::sessionBus(); - auto msg = QDBusMessage::createMethodCall( - DBUS_SERVICE_SECRET, path, "org.freedesktop.DBus.Introspectable", "Introspect"); - - // BlockWithGui enters event loop - auto reply = QDBusPendingReply(bus.call(msg, QDBus::BlockWithGui, 5)); - VERIFY(reply.isValid()); - auto actual = extractElement(reply.argumentAt<0>(), [&](const QXmlStreamReader& reader) { - return reader.name() == "interface" && reader.attributes().value("name") == interface; - }); - - COMPARE(actual, spec); - return true; - } -} // namespace +#define DBUS_GET2(name1, name2, stmt) \ + std::remove_cv())>::type name1; \ + std::remove_cv())>::type name2; \ + do { \ + const auto rep = (stmt); \ + VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \ + name1 = rep.argumentAt<0>(); \ + name2 = rep.argumentAt<1>(); \ + } while (false) using namespace FdoSecrets; +class FakeClient : public DBusClient +{ +public: + explicit FakeClient(DBusMgr* dbus) + : DBusClient(dbus, QStringLiteral("local"), 0, "fake-client") + { + } +}; + +// pretty print QDBusObjectPath in QCOMPARE +char* toString(const QDBusObjectPath& path) +{ + return QTest::toString("ObjectPath(" + path.path() + ")"); +} + TestGuiFdoSecrets::~TestGuiFdoSecrets() = default; void TestGuiFdoSecrets::initTestCase() { - QVERIFY(Crypto::init()); + VERIFY(Crypto::init()); Config::createTempFileInstance(); config()->set(Config::AutoSaveAfterEveryChange, false); config()->set(Config::AutoSaveOnExit, false); @@ -193,15 +155,15 @@ void TestGuiFdoSecrets::initTestCase() m_mainWindow.reset(new MainWindow()); m_tabWidget = m_mainWindow->findChild("tabWidget"); - QVERIFY(m_tabWidget); + VERIFY(m_tabWidget); m_plugin = FdoSecretsPlugin::getPlugin(); - QVERIFY(m_plugin); + VERIFY(m_plugin); m_mainWindow->show(); // Load the NewDatabase.kdbx file into temporary storage QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx")); - QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); - QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); + VERIFY(sourceDbFile.open(QIODevice::ReadOnly)); + VERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData)); sourceDbFile.close(); // set keys for session encryption @@ -223,23 +185,27 @@ void TestGuiFdoSecrets::initTestCase() "ab5c26b0ea3480c9aba8154cf"); // use the same cipher to do the client side encryption, but exchange the position of client/server keys m_cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7); - QVERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)), - MpiFromHex("30d18c6b328bac970c05bda6af2e708b9" - "d6bbbb6dc136c1a2d96e870fabc86ad74" - "1846a26a4197f32f65ea2e7580ad2afe3" - "dd5d6c1224b8368b0df2cd75d520a9ff9" - "7fe894cc7da71b7bd285b4633359c16c8" - "d341f822fa4f0fdf59b5d3448658c46a2" - "a86dbb14ff85823873f8a259ccc52bbb8" - "2b5a4c2a75447982553b42221"), - MpiFromHex("84aafe9c9f356f7762307f4d791acb59e" - "8e3fd562abdbb481d0587f8400ad6c51d" - "af561a1beb9a22c8cd4d2807367c5787b" - "2e06d631ccbb5194b6bb32211583ce688" - "f9c2cebc22a9e4d494d12ebdd570c61a1" - "62a94e88561d25ccd0415339d1f59e1b0" - "6bc6b6b5fde46e23b2410eb034be390d3" - "2407ec7ae90f0831f24afd5ac"))); + VERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)), + MpiFromHex("30d18c6b328bac970c05bda6af2e708b9" + "d6bbbb6dc136c1a2d96e870fabc86ad74" + "1846a26a4197f32f65ea2e7580ad2afe3" + "dd5d6c1224b8368b0df2cd75d520a9ff9" + "7fe894cc7da71b7bd285b4633359c16c8" + "d341f822fa4f0fdf59b5d3448658c46a2" + "a86dbb14ff85823873f8a259ccc52bbb8" + "2b5a4c2a75447982553b42221"), + MpiFromHex("84aafe9c9f356f7762307f4d791acb59e" + "8e3fd562abdbb481d0587f8400ad6c51d" + "af561a1beb9a22c8cd4d2807367c5787b" + "2e06d631ccbb5194b6bb32211583ce688" + "f9c2cebc22a9e4d494d12ebdd570c61a1" + "62a94e88561d25ccd0415339d1f59e1b0" + "6bc6b6b5fde46e23b2410eb034be390d3" + "2407ec7ae90f0831f24afd5ac"))); + + // set a fake dbus client all the time so we can freely access DBusMgr anywhere + m_client.reset(new FakeClient(m_plugin->dbus().data())); + m_plugin->dbus()->overrideClient(m_client); } // Every test starts with opening the temp database @@ -247,8 +213,8 @@ void TestGuiFdoSecrets::init() { m_dbFile.reset(new TemporaryFile()); // Write the temp storage to a temp database file for use in our tests - QVERIFY(m_dbFile->open()); - QCOMPARE(m_dbFile->write(m_dbData), static_cast((m_dbData.size()))); + VERIFY(m_dbFile->open()); + COMPARE(m_dbFile->write(m_dbData), static_cast((m_dbData.size()))); m_dbFile->close(); // make sure window is activated or focus tests may fail @@ -262,7 +228,7 @@ void TestGuiFdoSecrets::init() // by default expose the root group FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); - QVERIFY(m_dbWidget->save()); + VERIFY(m_dbWidget->save()); } // Every test ends with closing the temp database without saving @@ -270,6 +236,7 @@ void TestGuiFdoSecrets::cleanup() { // restore to default settings FdoSecrets::settings()->setShowNotification(false); + FdoSecrets::settings()->setConfirmAccessItem(false); FdoSecrets::settings()->setEnabled(false); if (m_plugin) { m_plugin->updateServiceState(); @@ -279,93 +246,57 @@ void TestGuiFdoSecrets::cleanup() for (int i = 0; i != m_tabWidget->count(); ++i) { m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean(); } - QVERIFY(m_tabWidget->closeAllDatabaseTabs()); + VERIFY(m_tabWidget->closeAllDatabaseTabs()); QApplication::processEvents(); if (m_dbFile) { m_dbFile->remove(); } + + m_client->clearAuthorization(); } void TestGuiFdoSecrets::cleanupTestCase() { + m_plugin->dbus()->overrideClient({}); if (m_dbFile) { m_dbFile->remove(); } } -void TestGuiFdoSecrets::testDBusSpec() -{ - auto service = enableService(); - QVERIFY(service); - - // service - QCOMPARE(service->objectPath().path(), QStringLiteral(DBUS_PATH_SECRETS)); - QVERIFY(checkDBusSpec(service->objectPath().path(), DBUS_INTERFACE_SECRET_SERVICE)); - - // default alias - QVERIFY(checkDBusSpec(DBUS_PATH_DEFAULT_ALIAS, DBUS_INTERFACE_SECRET_COLLECTION)); - - // collection - auto coll = getDefaultCollection(service); - QVERIFY(coll); - QVERIFY(checkDBusSpec(coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION)); - - // item - auto item = getFirstItem(coll); - QVERIFY(item); - QVERIFY(checkDBusSpec(item->objectPath().path(), DBUS_INTERFACE_SECRET_ITEM)); - - // session - auto sess = openSession(service, PlainCipher::Algorithm); - QVERIFY(sess); - QVERIFY(checkDBusSpec(sess->objectPath().path(), DBUS_INTERFACE_SECRET_SESSION)); - - // prompt - FdoSecrets::settings()->setNoConfirmDeleteItem(true); - PromptBase* prompt = nullptr; - { - auto rep = item->deleteItem(); - QVERIFY(!rep.isError()); - prompt = rep.value(); - } - QVERIFY(prompt); - QVERIFY(checkDBusSpec(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT)); -} - void TestGuiFdoSecrets::testServiceEnable() { QSignalSpy sigError(m_plugin, SIGNAL(error(QString))); - QVERIFY(sigError.isValid()); + VERIFY(sigError.isValid()); QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted())); - QVERIFY(sigStarted.isValid()); + VERIFY(sigStarted.isValid()); // make sure no one else is holding the service - QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); + VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); // enable the service auto service = enableService(); - QVERIFY(service); + VERIFY(service); // service started without error - QVERIFY(sigError.isEmpty()); - QCOMPARE(sigStarted.size(), 1); + VERIFY(sigError.isEmpty()); + COMPARE(sigStarted.size(), 1); QApplication::processEvents(); - QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); + VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET)); // there will be one default collection auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); - COMPARE_DBUS_LOCAL_CALL(coll->locked(), false); - COMPARE_DBUS_LOCAL_CALL(coll->label(), m_db->metadata()->name()); - COMPARE_DBUS_LOCAL_CALL( - coll->created(), - static_cast(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000)); - COMPARE_DBUS_LOCAL_CALL( + DBUS_COMPARE(coll->locked(), false); + DBUS_COMPARE(coll->label(), m_db->metadata()->name()); + + DBUS_COMPARE(coll->created(), + static_cast(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000)); + DBUS_COMPARE( coll->modified(), static_cast(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000)); } @@ -375,69 +306,66 @@ void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase() // reset the exposed group and then enable the service FdoSecrets::settings()->setExposedGroup(m_db, {}); auto service = enableService(); - QVERIFY(service); + VERIFY(service); // no collections - COMPARE_DBUS_LOCAL_CALL(service->collections(), QList{}); + DBUS_COMPARE(service->collections(), QList{}); } void TestGuiFdoSecrets::testServiceSearch() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto item = getFirstItem(coll); - QVERIFY(item); + VERIFY(item); - item->backend()->attributes()->set("fdosecrets-test", "1"); - item->backend()->attributes()->set("fdosecrets-test-protected", "2", true); + auto entries = m_db->rootGroup()->entriesRecursive(false); + VERIFY(!entries.isEmpty()); + const auto& entry = entries.first(); + entry->attributes()->set("fdosecrets-test", "1"); + entry->attributes()->set("fdosecrets-test-protected", "2", true); const QString crazyKey = "_a:bc&-+'-e%12df_d"; const QString crazyValue = "[v]al@-ue"; - item->backend()->attributes()->set(crazyKey, crazyValue); + entry->attributes()->set(crazyKey, crazyValue); // search by title { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Title", item->backend()->title()}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked, {item}); + DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } // search by attribute { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test", "1"}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked, {item}); + DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test", "1"}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{crazyKey, crazyValue}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked, {item}); + DBUS_GET2(unlocked, locked, service->SearchItems({{crazyKey, crazyValue}})); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } // searching using empty terms returns nothing { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked.size(), 0); + DBUS_GET2(unlocked, locked, service->SearchItems({})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); } // searching using protected attributes or password returns nothing { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Password", item->backend()->password()}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked.size(), 0); + DBUS_GET2(unlocked, locked, service->SearchItems({{"Password", entry->password()}})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); } { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test-protected", "2"}}, locked)); - QCOMPARE(locked.size(), 0); - QCOMPARE(unlocked.size(), 0); + DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test-protected", "2"}})); + COMPARE(locked, {}); + COMPARE(unlocked, {}); } } @@ -446,47 +374,44 @@ void TestGuiFdoSecrets::testServiceUnlock() lockDatabaseInBackend(); auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); - QSignalSpy spyCollectionCreated(&service->dbusAdaptor(), SIGNAL(CollectionCreated(QDBusObjectPath))); - QVERIFY(spyCollectionCreated.isValid()); - QSignalSpy spyCollectionDeleted(&service->dbusAdaptor(), SIGNAL(CollectionDeleted(QDBusObjectPath))); - QVERIFY(spyCollectionDeleted.isValid()); - QSignalSpy spyCollectionChanged(&service->dbusAdaptor(), SIGNAL(CollectionChanged(QDBusObjectPath))); - QVERIFY(spyCollectionChanged.isValid()); + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath))); + VERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath))); + VERIFY(spyCollectionChanged.isValid()); - PromptBase* prompt = nullptr; - { - CHECKED_DBUS_LOCAL_CALL(unlocked, service->unlock({coll.data()}, prompt)); - // nothing is unlocked immediately without user's action - QVERIFY(unlocked.isEmpty()); - } - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())})); + // nothing is unlocked immediately without user's action + COMPARE(unlocked, {}); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // nothing is unlocked yet - QCOMPARE(spyPromptCompleted.count(), 0); - QVERIFY(coll); - QVERIFY(coll->backend()->isLocked()); + QTRY_COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(coll->locked(), true); // drive the prompt - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); // still not unlocked before user action - QCOMPARE(spyPromptCompleted.count(), 0); - QVERIFY(coll); - QVERIFY(coll->backend()->isLocked()); + QTRY_COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(coll->locked(), true); // interact with the dialog QApplication::processEvents(); { auto dbOpenDlg = m_tabWidget->findChild(); - QVERIFY(dbOpenDlg); + VERIFY(dbOpenDlg); auto editPassword = dbOpenDlg->findChild("editPassword"); - QVERIFY(editPassword); + VERIFY(editPassword); editPassword->setFocus(); QTest::keyClicks(editPassword, "a"); QTest::keyClick(editPassword, Qt::Key_Enter); @@ -494,203 +419,286 @@ void TestGuiFdoSecrets::testServiceUnlock() QApplication::processEvents(); // unlocked - QVERIFY(coll); - QVERIFY(!coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), false); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); { auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.size(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).value().variant().value>(), {coll->objectPath()}); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); } - QCOMPARE(spyCollectionCreated.count(), 0); - QCOMPARE(spyCollectionChanged.count(), 1); + QTRY_COMPARE(spyCollectionCreated.count(), 0); + QTRY_VERIFY(!spyCollectionChanged.isEmpty()); + for (const auto& args : spyCollectionChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); + } + QTRY_COMPARE(spyCollectionDeleted.count(), 0); +} + +void TestGuiFdoSecrets::testServiceUnlockItems() +{ + FdoSecrets::settings()->setConfirmAccessItem(true); + + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto item = getFirstItem(coll); + VERIFY(item); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + VERIFY(sess); + + DBUS_COMPARE(item->locked(), true); + { - auto args = spyCollectionChanged.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), coll->objectPath()); + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())})); + // nothing is unlocked immediately without user's action + COMPARE(unlocked, {}); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // nothing is unlocked yet + COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(item->locked(), true); + + // drive the prompt + DBUS_VERIFY(prompt->Prompt("")); + // only allow once + VERIFY(driveAccessControlDialog(false)); + + // unlocked + DBUS_COMPARE(item->locked(), false); + + VERIFY(spyPromptCompleted.wait()); + COMPARE(spyPromptCompleted.count(), 1); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(item->path())}); + } } - QCOMPARE(spyCollectionDeleted.count(), 0); + + // access the secret should reset the locking state + { + DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path()))); + } + DBUS_COMPARE(item->locked(), true); + + // unlock again with remember + { + DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())})); + // nothing is unlocked immediately without user's action + COMPARE(unlocked, {}); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // nothing is unlocked yet + COMPARE(spyPromptCompleted.count(), 0); + DBUS_COMPARE(item->locked(), true); + + // drive the prompt + DBUS_VERIFY(prompt->Prompt("")); + // only allow and remember + VERIFY(driveAccessControlDialog(true)); + + // unlocked + DBUS_COMPARE(item->locked(), false); + + VERIFY(spyPromptCompleted.wait()); + COMPARE(spyPromptCompleted.count(), 1); + { + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(item->path())}); + } + } + + // access the secret does not reset the locking state + { + DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path()))); + } + DBUS_COMPARE(item->locked(), false); } void TestGuiFdoSecrets::testServiceLock() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); - QSignalSpy spyCollectionCreated(&service->dbusAdaptor(), SIGNAL(CollectionCreated(QDBusObjectPath))); - QVERIFY(spyCollectionCreated.isValid()); - QSignalSpy spyCollectionDeleted(&service->dbusAdaptor(), SIGNAL(CollectionDeleted(QDBusObjectPath))); - QVERIFY(spyCollectionDeleted.isValid()); - QSignalSpy spyCollectionChanged(&service->dbusAdaptor(), SIGNAL(CollectionChanged(QDBusObjectPath))); - QVERIFY(spyCollectionChanged.isValid()); + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath))); + VERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath))); + VERIFY(spyCollectionChanged.isValid()); // if the db is modified, prompt user m_db->markAsModified(); { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt)); - QCOMPARE(locked.size(), 0); - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())})); + COMPARE(locked, {}); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click cancel MessageBox::setNextAnswer(MessageBox::Cancel); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QVERIFY(!coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), false); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), true); - QCOMPARE(args.at(1).value>(), {}); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), true); + COMPARE(getSignalVariantArgument>(args.at(1)), {}); } { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt)); - QCOMPARE(locked.size(), 0); - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())})); + COMPARE(locked, {}); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click save MessageBox::setNextAnswer(MessageBox::Save); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QVERIFY(coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), true); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).value().variant().value>(), {coll->objectPath()}); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(getSignalVariantArgument>(args.at(1)), {QDBusObjectPath(coll->path())}); } - QCOMPARE(spyCollectionCreated.count(), 0); - QCOMPARE(spyCollectionChanged.count(), 1); - { - auto args = spyCollectionChanged.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), coll->objectPath()); + QTRY_COMPARE(spyCollectionCreated.count(), 0); + QTRY_VERIFY(!spyCollectionChanged.isEmpty()); + for (const auto& args : spyCollectionChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); } - QCOMPARE(spyCollectionDeleted.count(), 0); + QTRY_COMPARE(spyCollectionDeleted.count(), 0); // locking item locks the whole db unlockDatabaseInBackend(); { auto item = getFirstItem(coll); - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL(locked, service->lock({item}, prompt)); - QCOMPARE(locked.size(), 0); - QVERIFY(prompt); + DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(item->path())})); + COMPARE(locked, {}); + auto prompt = getProxy(promptPath); + VERIFY(prompt); MessageBox::setNextAnswer(MessageBox::Save); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QVERIFY(coll->backend()->isLocked()); + DBUS_COMPARE(coll->locked(), true); } } void TestGuiFdoSecrets::testSessionOpen() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto sess = openSession(service, PlainCipher::Algorithm); - QVERIFY(sess); - QCOMPARE(service->sessions().size(), 1); + VERIFY(sess); sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); - QCOMPARE(service->sessions().size(), 2); + VERIFY(sess); } void TestGuiFdoSecrets::testSessionClose() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto sess = openSession(service, PlainCipher::Algorithm); - QVERIFY(sess); + VERIFY(sess); - QCOMPARE(service->sessions().size(), 1); - - auto rep = sess->close(); - QVERIFY(!rep.isError()); - - QCOMPARE(service->sessions().size(), 0); + DBUS_VERIFY(sess->Close()); } void TestGuiFdoSecrets::testCollectionCreate() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); - QSignalSpy spyCollectionCreated(&service->dbusAdaptor(), SIGNAL(CollectionCreated(QDBusObjectPath))); - QVERIFY(spyCollectionCreated.isValid()); + QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath))); + VERIFY(spyCollectionCreated.isValid()); // returns existing if alias is nonempty and exists { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL( - coll, service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "NewDB"}}, "default", prompt)); - QVERIFY(!prompt); - QCOMPARE(coll, getDefaultCollection(service).data()); + auto existing = getDefaultCollection(service); + DBUS_GET2(collPath, + promptPath, + service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "NewDB"}}, "default")); + COMPARE(promptPath, QDBusObjectPath("/")); + COMPARE(collPath.path(), existing->path()); } - QCOMPARE(spyCollectionCreated.count(), 0); + QTRY_COMPARE(spyCollectionCreated.count(), 0); // create new one and set properties { - PromptBase* prompt = nullptr; - CHECKED_DBUS_LOCAL_CALL( - created, - service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "Test NewDB"}}, "mydatadb", prompt)); - QVERIFY(!created); - QVERIFY(prompt); + DBUS_GET2(collPath, + promptPath, + service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "Test NewDB"}}, "mydatadb")); + COMPARE(collPath, QDBusObjectPath("/")); + auto prompt = getProxy(promptPath); + VERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); - QTimer::singleShot(50, this, SLOT(createDatabaseCallback())); - QVERIFY(!prompt->prompt("").isError()); + QTimer::singleShot(50, this, &TestGuiFdoSecrets::driveNewDatabaseWizard); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.size(), 2); - QCOMPARE(args.at(0).toBool(), false); - auto coll = - FdoSecrets::pathToObject(args.at(1).value().variant().value()); - QVERIFY(coll); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + auto coll = getProxy(getSignalVariantArgument(args.at(1))); + VERIFY(coll); - QCOMPARE(coll->backend()->database()->metadata()->name(), QStringLiteral("Test NewDB")); + DBUS_COMPARE(coll->label(), QStringLiteral("Test NewDB")); - QCOMPARE(spyCollectionCreated.count(), 1); + QTRY_COMPARE(spyCollectionCreated.count(), 1); { args = spyCollectionCreated.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), coll->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); } } } -void TestGuiFdoSecrets::createDatabaseCallback() +void TestGuiFdoSecrets::driveNewDatabaseWizard() { auto wizard = m_tabWidget->findChild(); - QVERIFY(wizard); + VERIFY(wizard); - QCOMPARE(wizard->currentId(), 0); + COMPARE(wizard->currentId(), 0); wizard->next(); wizard->next(); - QCOMPARE(wizard->currentId(), 2); + COMPARE(wizard->currentId(), 2); // enter password auto* passwordEdit = wizard->findChild("enterPasswordEdit"); @@ -701,7 +709,7 @@ void TestGuiFdoSecrets::createDatabaseCallback() // save database to temporary file TemporaryFile tmpFile; - QVERIFY(tmpFile.open()); + VERIFY(tmpFile.open()); tmpFile.close(); fileDialog()->setNextFileName(tmpFile.fileName()); @@ -713,43 +721,69 @@ void TestGuiFdoSecrets::createDatabaseCallback() void TestGuiFdoSecrets::testCollectionDelete() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); // save the path which will be gone after the deletion. - auto collPath = coll->objectPath(); + auto collPath = coll->path(); - QSignalSpy spyCollectionDeleted(&service->dbusAdaptor(), SIGNAL(CollectionDeleted(QDBusObjectPath))); - QVERIFY(spyCollectionDeleted.isValid()); + QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath))); + VERIFY(spyCollectionDeleted.isValid()); m_db->markAsModified(); - CHECKED_DBUS_LOCAL_CALL(prompt, coll->deleteCollection()); - QVERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + DBUS_GET(promptPath, coll->Delete()); + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click save MessageBox::setNextAnswer(MessageBox::Save); - QVERIFY(!prompt->prompt("").isError()); + DBUS_VERIFY(prompt->Prompt("")); - QApplication::processEvents(); - - // closing the tab should have deleted coll if not in testing + // closing the tab should have deleted the database if not in testing // but deleteLater is not processed in QApplication::processEvent // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents - // QVERIFY(!coll); + QApplication::processEvents(); - QCOMPARE(spyPromptCompleted.count(), 1); + // however, the object should already be taken down from dbus + { + auto reply = coll->locked(); + VERIFY(reply.isFinished() && reply.isError()); + COMPARE(reply.error().type(), QDBusError::UnknownObject); + } + + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).value().variant().toString(), QStringLiteral("")); - QCOMPARE(spyCollectionDeleted.count(), 1); + QTRY_COMPARE(spyCollectionDeleted.count(), 1); { args = spyCollectionDeleted.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), collPath); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), collPath); + } +} + +void TestGuiFdoSecrets::testCollectionChange() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + + QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath))); + VERIFY(spyCollectionChanged.isValid()); + + DBUS_VERIFY(coll->setLabel("anotherLabel")); + COMPARE(m_db->metadata()->name(), QStringLiteral("anotherLabel")); + QTRY_COMPARE(spyCollectionChanged.size(), 1); + { + auto args = spyCollectionChanged.takeFirst(); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), coll->path()); } } @@ -757,57 +791,58 @@ void TestGuiFdoSecrets::testHiddenFilename() { // when file name contains leading dot, all parts excepting the last should be used // for collection name, and the registration should success - QVERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx")); + VERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx")); // reset is necessary to not hold database longer and cause connections // not cleaned up when the database tab is closed. m_db.reset(); - QVERIFY(m_tabWidget->closeAllDatabaseTabs()); + VERIFY(m_tabWidget->closeAllDatabaseTabs()); m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a"); m_dbWidget = m_tabWidget->currentDatabaseWidget(); m_db = m_dbWidget->database(); // enable the service auto service = enableService(); - QVERIFY(service); + VERIFY(service); // collection is properly registered auto coll = getDefaultCollection(service); - QVERIFY(coll->objectPath().path() != "/"); - QCOMPARE(coll->name(), QStringLiteral(".Name")); + auto collObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(coll->path())); + VERIFY(collObj); + COMPARE(collObj->name(), QStringLiteral(".Name")); } void TestGuiFdoSecrets::testDuplicateName() { QTemporaryDir dir; - QVERIFY(dir.isValid()); + VERIFY(dir.isValid()); // create another file under different path but with the same filename QString anotherFile = dir.path() + "/" + QFileInfo(*m_dbFile).fileName(); m_dbFile->copy(anotherFile); m_tabWidget->addDatabaseTab(anotherFile, false, "a"); auto service = enableService(); - QVERIFY(service); + VERIFY(service); // when two databases have the same name, one of it will have part of its uuid suffixed - const auto pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC"); - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 2); - QCOMPARE(colls[0]->objectPath().path(), pathNoSuffix); - QVERIFY(colls[1]->objectPath().path() != pathNoSuffix); + const QString pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC"); + DBUS_GET(colls, service->collections()); + COMPARE(colls.size(), 2); + COMPARE(colls[0].path(), pathNoSuffix); + VERIFY(colls[1].path() != pathNoSuffix); } void TestGuiFdoSecrets::testItemCreate() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); + VERIFY(sess); - QSignalSpy spyItemCreated(&coll->dbusAdaptor(), SIGNAL(ItemCreated(QDBusObjectPath))); - QVERIFY(spyItemCreated.isValid()); + QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath))); + VERIFY(spyItemCreated.isValid()); // create item StringStringMap attributes{ @@ -816,56 +851,90 @@ void TestGuiFdoSecrets::testItemCreate() }; auto item = createItem(sess, coll, "abc", "Password", attributes, false); - QVERIFY(item); + VERIFY(item); // signals { - QCOMPARE(spyItemCreated.count(), 1); + QTRY_COMPARE(spyItemCreated.count(), 1); auto args = spyItemCreated.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item->path()); } // attributes { - CHECKED_DBUS_LOCAL_CALL(actual, item->attributes()); + DBUS_GET(actual, item->attributes()); for (const auto& key : attributes.keys()) { - QVERIFY(actual.contains(key)); - QCOMPARE(actual[key], attributes[key]); + COMPARE(actual[key], attributes[key]); } } // label - COMPARE_DBUS_LOCAL_CALL(item->label(), QStringLiteral("abc")); + DBUS_COMPARE(item->label(), QStringLiteral("abc")); // secrets { - CHECKED_DBUS_LOCAL_CALL(ss, item->getSecret(sess)); - ss = m_cipher->decrypt(ss); - QCOMPARE(ss.value, QByteArray("Password")); + DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path()))); + auto decrypted = m_cipher->decrypt(ss.unmarshal(m_plugin->dbus())); + COMPARE(decrypted.value, QByteArrayLiteral("Password")); } // searchable { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems(attributes, locked)); - QCOMPARE(locked, QList{}); - QCOMPARE(unlocked, QList{item}); + DBUS_GET2(unlocked, locked, service->SearchItems(attributes)); + COMPARE(locked, {}); + COMPARE(unlocked, {QDBusObjectPath(item->path())}); } { - CHECKED_DBUS_LOCAL_CALL(unlocked, coll->searchItems(attributes)); - QVERIFY(unlocked.contains(item)); + DBUS_GET(unlocked, coll->SearchItems(attributes)); + VERIFY(unlocked.contains(QDBusObjectPath(item->path()))); + } +} + +void TestGuiFdoSecrets::testItemChange() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto item = getFirstItem(coll); + VERIFY(item); + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + + QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath))); + VERIFY(spyItemChanged.isValid()); + + DBUS_VERIFY(item->setLabel("anotherLabel")); + COMPARE(entry->title(), QStringLiteral("anotherLabel")); + QTRY_VERIFY(!spyItemChanged.isEmpty()); + for (const auto& args : spyItemChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item->path()); + } + + spyItemChanged.clear(); + DBUS_VERIFY(item->setAttributes({ + {"abc", "def"}, + })); + COMPARE(entry->attributes()->value("abc"), QStringLiteral("def")); + QTRY_VERIFY(!spyItemChanged.isEmpty()); + for (const auto& args : spyItemChanged) { + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item->path()); } } void TestGuiFdoSecrets::testItemReplace() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); + VERIFY(sess); // create item StringStringMap attr1{ @@ -880,38 +949,38 @@ void TestGuiFdoSecrets::testItemReplace() }; auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false); - QVERIFY(item1); + VERIFY(item1); auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false); - QVERIFY(item2); + VERIFY(item2); { - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); - QCOMPARE(unlocked.size(), 2); + DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}})); + QSet expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())}; + COMPARE(QSet::fromList(unlocked), expected); } - QSignalSpy spyItemCreated(&coll->dbusAdaptor(), SIGNAL(ItemCreated(QDBusObjectPath))); - QVERIFY(spyItemCreated.isValid()); - QSignalSpy spyItemChanged(&coll->dbusAdaptor(), SIGNAL(ItemChanged(QDBusObjectPath))); - QVERIFY(spyItemChanged.isValid()); + QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath))); + VERIFY(spyItemCreated.isValid()); + QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath))); + VERIFY(spyItemChanged.isValid()); { // when replace, existing item with matching attr is updated auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true); - QVERIFY(item3); - QCOMPARE(item2, item3); - COMPARE_DBUS_LOCAL_CALL(item3->label(), QStringLiteral("abc3")); + VERIFY(item3); + COMPARE(item2->path(), item3->path()); + DBUS_COMPARE(item3->label(), QStringLiteral("abc3")); // there are still 2 entries - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); - QCOMPARE(unlocked.size(), 2); + DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}})); + QSet expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())}; + COMPARE(QSet::fromList(unlocked), expected); - QCOMPARE(spyItemCreated.count(), 0); + QTRY_COMPARE(spyItemCreated.count(), 0); // there may be multiple changed signals, due to each item attribute is set separately - QVERIFY(!spyItemChanged.isEmpty()); + QTRY_VERIFY(!spyItemChanged.isEmpty()); for (const auto& args : spyItemChanged) { - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item3->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item3->path()); } } @@ -920,221 +989,321 @@ void TestGuiFdoSecrets::testItemReplace() { // when NOT replace, another entry is created auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false); - QVERIFY(item4); - COMPARE_DBUS_LOCAL_CALL(item2->label(), QStringLiteral("abc3")); - COMPARE_DBUS_LOCAL_CALL(item4->label(), QStringLiteral("abc4")); + VERIFY(item4); + DBUS_COMPARE(item2->label(), QStringLiteral("abc3")); + DBUS_COMPARE(item4->label(), QStringLiteral("abc4")); // there are 3 entries - QList locked; - CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked)); - QCOMPARE(unlocked.size(), 3); + DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}})); + QSet expected{ + QDBusObjectPath(item1->path()), + QDBusObjectPath(item2->path()), + QDBusObjectPath(item4->path()), + }; + COMPARE(QSet::fromList(unlocked), expected); - QCOMPARE(spyItemCreated.count(), 1); + QTRY_COMPARE(spyItemCreated.count(), 1); { auto args = spyItemCreated.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item4->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item4->path()); } // there may be multiple changed signals, due to each item attribute is set separately - QVERIFY(!spyItemChanged.isEmpty()); + VERIFY(!spyItemChanged.isEmpty()); for (const auto& args : spyItemChanged) { - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), item4->objectPath()); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), item4->path()); } } } +void TestGuiFdoSecrets::testItemReplaceExistingLocked() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + VERIFY(sess); + + // create item + StringStringMap attr1{ + {"application", "fdosecrets-test"}, + {"attr-i[bute]", "![some] -value*"}, + {"fdosecrets-attr", "1"}, + }; + + auto item = createItem(sess, coll, "abc1", "Password", attr1, false); + VERIFY(item); + + // make sure the item is locked + { + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + FdoSecrets::settings()->setConfirmAccessItem(true); + m_client->setItemAuthorized(entry->uuid(), AuthDecision::Undecided); + DBUS_COMPARE(item->locked(), true); + } + + // when replace with a locked item, there will be an prompt + auto item2 = createItem(sess, coll, "abc2", "PasswordUpdated", attr1, true, true); + VERIFY(item2); + COMPARE(item2->path(), item->path()); + DBUS_COMPARE(item2->label(), QStringLiteral("abc2")); +} + void TestGuiFdoSecrets::testItemSecret() { const QString TEXT_PLAIN = "text/plain"; const QString APPLICATION_OCTET_STREAM = "application/octet-stream"; auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto item = getFirstItem(coll); - QVERIFY(item); + VERIFY(item); auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); - QVERIFY(sess); + VERIFY(sess); + + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); // plain text secret { - CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess)); - auto ss = m_cipher->decrypt(encrypted); - QCOMPARE(ss.contentType, TEXT_PLAIN); - QCOMPARE(ss.value, item->backend()->password().toUtf8()); + DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); + auto ss = m_cipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); + COMPARE(ss.contentType, TEXT_PLAIN); + COMPARE(ss.value, entry->password().toUtf8()); } - // get secret with notification (only works when called from DBUS) + // get secret with notification FdoSecrets::settings()->setShowNotification(true); { QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int))); - QVERIFY(spyShowNotification.isValid()); + VERIFY(spyShowNotification.isValid()); - auto iitem = interfaceOf(item); - QVERIFY(static_cast(iitem)); + DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); + auto ss = m_cipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); + COMPARE(ss.contentType, TEXT_PLAIN); + COMPARE(ss.value, entry->password().toUtf8()); - auto replyMsg = iitem->call(QDBus::BlockWithGui, "GetSecret", QVariant::fromValue(sess->objectPath())); - auto reply = QDBusPendingReply(replyMsg); - QVERIFY(reply.isValid()); - auto ss = m_cipher->decrypt(reply.argumentAt<0>()); + COMPARE(ss.contentType, TEXT_PLAIN); + COMPARE(ss.value, entry->password().toUtf8()); - QCOMPARE(ss.contentType, TEXT_PLAIN); - QCOMPARE(ss.value, item->backend()->password().toUtf8()); - - QCOMPARE(spyShowNotification.count(), 1); + QTRY_COMPARE(spyShowNotification.count(), 1); } FdoSecrets::settings()->setShowNotification(false); // set secret with plain text { - SecretStruct ss; + // first create Secret in wire format, + // then convert to internal format and encrypt + // finally convert encrypted internal format back to wire format to pass to SetSecret + wire::Secret ss; ss.contentType = TEXT_PLAIN; ss.value = "NewPassword"; - ss.session = sess->objectPath(); - QVERIFY(!item->setSecret(m_cipher->encrypt(ss)).isError()); + ss.session = QDBusObjectPath(sess->path()); + auto encrypted = m_cipher->encrypt(ss.unmarshal(m_plugin->dbus())); + DBUS_VERIFY(item->SetSecret(encrypted.marshal())); - QCOMPARE(item->backend()->password().toUtf8(), ss.value); + COMPARE(entry->password().toUtf8(), ss.value); } // set secret with something else is saved as attachment { - SecretStruct expected; + wire::Secret expected; expected.contentType = APPLICATION_OCTET_STREAM; - expected.value = "NewPasswordBinary"; - expected.session = sess->objectPath(); - QVERIFY(!item->setSecret(m_cipher->encrypt(expected)).isError()); + expected.value = QByteArrayLiteral("NewPasswordBinary"); + expected.session = QDBusObjectPath(sess->path()); + DBUS_VERIFY(item->SetSecret(m_cipher->encrypt(expected.unmarshal(m_plugin->dbus())).marshal())); - QCOMPARE(item->backend()->password(), QStringLiteral("")); + COMPARE(entry->password(), QStringLiteral("")); - CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess)); - auto ss = m_cipher->decrypt(encrypted); - QCOMPARE(ss.contentType, expected.contentType); - QCOMPARE(ss.value, expected.value); + DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); + auto ss = m_cipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); + COMPARE(ss.contentType, expected.contentType); + COMPARE(ss.value, expected.value); } } void TestGuiFdoSecrets::testItemDelete() { - FdoSecrets::settings()->setNoConfirmDeleteItem(false); + FdoSecrets::settings()->setConfirmDeleteItem(true); auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); auto item = getFirstItem(coll); - QVERIFY(item); + VERIFY(item); // save the path which will be gone after the deletion. - auto itemPath = item->objectPath(); + auto itemPath = item->path(); - QSignalSpy spyItemDeleted(&coll->dbusAdaptor(), SIGNAL(ItemDeleted(QDBusObjectPath))); - QVERIFY(spyItemDeleted.isValid()); + QSignalSpy spyItemDeleted(coll.data(), SIGNAL(ItemDeleted(QDBusObjectPath))); + VERIFY(spyItemDeleted.isValid()); - CHECKED_DBUS_LOCAL_CALL(prompt, item->deleteItem()); - QVERIFY(prompt); + DBUS_GET(promptPath, item->Delete()); + auto prompt = getProxy(promptPath); + VERIFY(prompt); - QSignalSpy spyPromptCompleted(&prompt->dbusAdaptor(), SIGNAL(Completed(bool, QDBusVariant))); - QVERIFY(spyPromptCompleted.isValid()); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); // prompt and click save - if (item->isDeletePermanent()) { - MessageBox::setNextAnswer(MessageBox::Delete); - } else { - MessageBox::setNextAnswer(MessageBox::Move); - } - QVERIFY(!prompt->prompt("").isError()); - + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + MessageBox::setNextAnswer(MessageBox::Delete); + DBUS_VERIFY(prompt->Prompt("")); QApplication::processEvents(); - QCOMPARE(spyPromptCompleted.count(), 1); + QTRY_COMPARE(spyPromptCompleted.count(), 1); auto args = spyPromptCompleted.takeFirst(); - QCOMPARE(args.count(), 2); - QCOMPARE(args.at(0).toBool(), false); - QCOMPARE(args.at(1).toString(), QStringLiteral("")); + COMPARE(args.count(), 2); + COMPARE(args.at(0).toBool(), false); + COMPARE(args.at(1).toString(), QStringLiteral("")); - QCOMPARE(spyItemDeleted.count(), 1); + QTRY_COMPARE(spyItemDeleted.count(), 1); + args = spyItemDeleted.takeFirst(); + COMPARE(args.size(), 1); + COMPARE(args.at(0).value().path(), itemPath); +} + +void TestGuiFdoSecrets::testItemLockState() +{ + auto service = enableService(); + VERIFY(service); + auto coll = getDefaultCollection(service); + VERIFY(coll); + auto item = getFirstItem(coll); + VERIFY(item); + auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm); + VERIFY(sess); + auto itemObj = m_plugin->dbus()->pathToObject(QDBusObjectPath(item->path())); + VERIFY(itemObj); + auto entry = itemObj->backend(); + VERIFY(entry); + + auto secret = + wire::Secret{ + QDBusObjectPath(sess->path()), + {}, + "NewPassword", + "text/plain", + } + .unmarshal(m_plugin->dbus()); + auto encrypted = m_cipher->encrypt(secret).marshal(); + + // when access confirmation is disabled, item is unlocked when the collection is unlocked + FdoSecrets::settings()->setConfirmAccessItem(false); + DBUS_COMPARE(item->locked(), false); + + // when access confirmation is enabled, item is locked if the client has no authorization + FdoSecrets::settings()->setConfirmAccessItem(true); + DBUS_COMPARE(item->locked(), true); + // however, item properties are still accessible as long as the collection is unlocked + DBUS_VERIFY(item->attributes()); + DBUS_VERIFY(item->setAttributes({})); + DBUS_VERIFY(item->label()); + DBUS_VERIFY(item->setLabel("abc")); + DBUS_VERIFY(item->created()); + DBUS_VERIFY(item->modified()); + // except secret, which is locked { - args = spyItemDeleted.takeFirst(); - QCOMPARE(args.size(), 1); - QCOMPARE(args.at(0).value(), itemPath); + auto reply = item->GetSecret(QDBusObjectPath(sess->path())); + VERIFY(reply.isError()); + COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED); } + { + auto reply = item->SetSecret(encrypted); + VERIFY(reply.isError()); + COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED); + } + + // item is unlocked if the client is authorized + m_client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed); + DBUS_COMPARE(item->locked(), false); + DBUS_VERIFY(item->GetSecret(QDBusObjectPath(sess->path()))); + DBUS_VERIFY(item->SetSecret(encrypted)); } void TestGuiFdoSecrets::testAlias() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); // read default alias - CHECKED_DBUS_LOCAL_CALL(coll, service->readAlias("default")); - QVERIFY(coll); + DBUS_GET(collPath, service->ReadAlias("default")); + auto coll = getProxy(collPath); + VERIFY(coll); // set extra alias - QVERIFY(!service->setAlias("another", coll).isError()); + DBUS_VERIFY(service->SetAlias("another", QDBusObjectPath(collPath))); // get using extra alias - CHECKED_DBUS_LOCAL_CALL(coll2, service->readAlias("another")); - QVERIFY(coll2); - QCOMPARE(coll, coll2); + DBUS_GET(collPath2, service->ReadAlias("another")); + COMPARE(collPath2, collPath); } void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent() { auto service = enableService(); - QVERIFY(service); + VERIFY(service); // one collection, which is default alias auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); // after locking, the collection is still there, but locked lockDatabaseInBackend(); coll = getDefaultCollection(service); - QVERIFY(coll); - COMPARE_DBUS_LOCAL_CALL(coll->locked(), true); + VERIFY(coll); + DBUS_COMPARE(coll->locked(), true); // unlock the database, the alias and collection is present unlockDatabaseInBackend(); coll = getDefaultCollection(service); - QVERIFY(coll); - COMPARE_DBUS_LOCAL_CALL(coll->locked(), false); + VERIFY(coll); + DBUS_COMPARE(coll->locked(), false); } void TestGuiFdoSecrets::testExposeSubgroup() { auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup"); - QVERIFY(subgroup); + VERIFY(subgroup); FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid()); auto service = enableService(); - QVERIFY(service); + VERIFY(service); auto coll = getDefaultCollection(service); - QVERIFY(coll); + VERIFY(coll); // exposing subgroup does not expose entries in other groups - auto items = coll->items(); - QVERIFY(!items.isError()); - QList exposedEntries; - for (const auto& item : items.value()) { - exposedEntries << item->backend(); + DBUS_GET(itemPaths, coll->items()); + QSet exposedEntries; + for (const auto& itemPath : itemPaths) { + exposedEntries << m_plugin->dbus()->pathToObject(itemPath)->backend(); } - QCOMPARE(exposedEntries, subgroup->entries()); + COMPARE(exposedEntries, QSet::fromList(subgroup->entries())); } void TestGuiFdoSecrets::testModifyingExposedGroup() { // test when exposed group is removed the collection is not exposed anymore auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking"); - QVERIFY(subgroup); + VERIFY(subgroup); FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid()); auto service = enableService(); - QVERIFY(service); + VERIFY(service); { - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 1); + DBUS_GET(collPaths, service->collections()); + COMPARE(collPaths.size(), 1); } m_db->metadata()->setRecycleBinEnabled(true); @@ -1142,102 +1311,19 @@ void TestGuiFdoSecrets::testModifyingExposedGroup() QApplication::processEvents(); { - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 0); + DBUS_GET(collPaths, service->collections()); + COMPARE(collPaths, {}); } // test setting another exposed group, the collection will be exposed again FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid()); QApplication::processEvents(); { - CHECKED_DBUS_LOCAL_CALL(colls, service->collections()); - QCOMPARE(colls.size(), 1); + DBUS_GET(collPaths, service->collections()); + COMPARE(collPaths.size(), 1); } } -QPointer TestGuiFdoSecrets::enableService() -{ - FdoSecrets::settings()->setEnabled(true); - VERIFY(m_plugin); - m_plugin->updateServiceState(); - return m_plugin->serviceInstance(); -} - -QPointer TestGuiFdoSecrets::openSession(Service* service, const QString& algo) -{ - // open session has to be called actually over DBUS to get peer info - - VERIFY(service); - auto iservice = interfaceOf(service); - VERIFY(iservice); - - if (algo == PlainCipher::Algorithm) { - auto replyMsg = iservice->call(QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(""))); - auto reply = QDBusPendingReply(replyMsg); - - VERIFY(reply.isValid()); - return FdoSecrets::pathToObject(reply.argumentAt<1>()); - } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { - - DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)), - MpiFromBytes(MpiToBytes(m_serverPublic))); - - auto replyMsg = iservice->call( - QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(m_cipher->m_publicKey))); - auto reply = QDBusPendingReply(replyMsg); - VERIFY(reply.isValid()); - COMPARE(qvariant_cast(reply.argumentAt<0>().variant()), MpiToBytes(m_serverPublic)); - return FdoSecrets::pathToObject(reply.argumentAt<1>()); - } - FAIL("Unsupported algorithm"); -} - -QPointer TestGuiFdoSecrets::getDefaultCollection(Service* service) -{ - VERIFY(service); - auto coll = service->readAlias("default"); - VERIFY(!coll.isError()); - return coll.value(); -} - -QPointer TestGuiFdoSecrets::getFirstItem(Collection* coll) -{ - VERIFY(coll); - auto items = coll->items(); - VERIFY(!items.isError()); - VERIFY(!items.value().isEmpty()); - return items.value().at(0); -} - -QPointer TestGuiFdoSecrets::createItem(Session* sess, - Collection* coll, - const QString& label, - const QString& pass, - const StringStringMap& attr, - bool replace) -{ - VERIFY(sess); - VERIFY(coll); - - QVariantMap properties{ - {DBUS_INTERFACE_SECRET_ITEM ".Label", QVariant::fromValue(label)}, - {DBUS_INTERFACE_SECRET_ITEM ".Attributes", QVariant::fromValue(attr)}, - }; - - SecretStruct ss; - ss.session = sess->objectPath(); - ss.value = pass.toLocal8Bit(); - ss.contentType = "plain/text"; - ss = m_cipher->encrypt(ss); - - PromptBase* prompt = nullptr; - auto item = coll->createItem(properties, ss, replace, prompt); - VERIFY(!item.isError()); - // creating item does not have a prompt to show - VERIFY(!prompt); - return item.value(); -} - void TestGuiFdoSecrets::lockDatabaseInBackend() { m_dbWidget->lock(); @@ -1251,3 +1337,127 @@ void TestGuiFdoSecrets::unlockDatabaseInBackend() m_db = m_dbWidget->database(); QApplication::processEvents(); } + +// the following functions have return value, switch macros to the version supporting that +#undef VERIFY +#undef VERIFY2 +#undef COMPARE +#define VERIFY(stmt) VERIFY2_RET(stmt, "") +#define VERIFY2 VERIFY2_RET +#define COMPARE COMPARE_RET + +QSharedPointer TestGuiFdoSecrets::enableService() +{ + FdoSecrets::settings()->setEnabled(true); + VERIFY(m_plugin); + m_plugin->updateServiceState(); + return getProxy(QDBusObjectPath(DBUS_PATH_SECRETS)); +} + +QSharedPointer TestGuiFdoSecrets::openSession(const QSharedPointer& service, + const QString& algo) +{ + VERIFY(service); + + if (algo == PlainCipher::Algorithm) { + DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(""))); + + return getProxy(sessPath); + } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) { + + DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)), + MpiFromBytes(MpiToBytes(m_serverPublic))); + + DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(m_cipher->m_publicKey))); + + COMPARE(qvariant_cast(output.variant()), MpiToBytes(m_serverPublic)); + return getProxy(sessPath); + } + QTest::qFail("Unsupported algorithm", __FILE__, __LINE__); + return {}; +} + +QSharedPointer TestGuiFdoSecrets::getDefaultCollection(const QSharedPointer& service) +{ + VERIFY(service); + DBUS_GET(collPath, service->ReadAlias("default")); + return getProxy(collPath); +} + +QSharedPointer TestGuiFdoSecrets::getFirstItem(const QSharedPointer& coll) +{ + VERIFY(coll); + DBUS_GET(itemPaths, coll->items()); + VERIFY(!itemPaths.isEmpty()); + return getProxy(itemPaths.first()); +} + +QSharedPointer TestGuiFdoSecrets::createItem(const QSharedPointer& sess, + const QSharedPointer& coll, + const QString& label, + const QString& pass, + const StringStringMap& attr, + bool replace, + bool expectPrompt) +{ + VERIFY(sess); + VERIFY(coll); + + QVariantMap properties{ + {DBUS_INTERFACE_SECRET_ITEM + ".Label", QVariant::fromValue(label)}, + {DBUS_INTERFACE_SECRET_ITEM + ".Attributes", QVariant::fromValue(attr)}, + }; + + wire::Secret ss; + ss.session = QDBusObjectPath(sess->path()); + ss.value = pass.toLocal8Bit(); + ss.contentType = "plain/text"; + auto encrypted = m_cipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal(); + + DBUS_GET2(itemPath, promptPath, coll->CreateItem(properties, encrypted, replace)); + + auto prompt = getProxy(promptPath); + VERIFY(prompt); + QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant))); + VERIFY(spyPromptCompleted.isValid()); + + // drive the prompt + DBUS_VERIFY(prompt->Prompt("")); + bool found = driveAccessControlDialog(); + COMPARE(found, expectPrompt); + + // wait for signal + VERIFY(spyPromptCompleted.wait()); + COMPARE(spyPromptCompleted.count(), 1); + auto args = spyPromptCompleted.takeFirst(); + COMPARE(args.size(), 2); + COMPARE(args.at(0).toBool(), false); + itemPath = getSignalVariantArgument(args.at(1)); + + return getProxy(itemPath); +} + +bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember) +{ + QApplication::processEvents(); + for (auto w : qApp->allWidgets()) { + if (!w->isWindow()) { + continue; + } + auto dlg = qobject_cast(w); + if (dlg) { + auto rememberCheck = dlg->findChild("rememberCheck"); + VERIFY(rememberCheck); + rememberCheck->setChecked(remember); + QTest::keyClick(dlg, Qt::Key_Enter); + QApplication::processEvents(); + return true; + } + } + return false; +} + +#undef VERIFY +#define VERIFY QVERIFY +#undef COMPARE +#define COMPARE QCOMPARE diff --git a/tests/gui/TestGuiFdoSecrets.h b/tests/gui/TestGuiFdoSecrets.h index 84f7147e7..8ded86586 100644 --- a/tests/gui/TestGuiFdoSecrets.h +++ b/tests/gui/TestGuiFdoSecrets.h @@ -19,14 +19,14 @@ #define KEEPASSXC_TESTGUIFDOSECRETS_H #include -#include +#include #include #include #include #include #include "fdosecrets/GcryptMPI.h" -#include "fdosecrets/objects/DBusTypes.h" +#include "fdosecrets/dbus/DBusTypes.h" class MainWindow; class Database; @@ -42,7 +42,13 @@ namespace FdoSecrets class Item; class Prompt; class DhIetf1024Sha256Aes128CbcPkcs7; + class DBusClient; } // namespace FdoSecrets +class ServiceProxy; +class CollectionProxy; +class ItemProxy; +class SessionProxy; +class PromptProxy; class QAbstractItemView; @@ -59,12 +65,11 @@ private slots: void cleanup(); void cleanupTestCase(); - void testDBusSpec(); - void testServiceEnable(); void testServiceEnableNoExposedDatabase(); void testServiceSearch(); void testServiceUnlock(); + void testServiceUnlockItems(); void testServiceLock(); void testSessionOpen(); @@ -72,11 +77,15 @@ private slots: void testCollectionCreate(); void testCollectionDelete(); + void testCollectionChange(); void testItemCreate(); + void testItemChange(); void testItemReplace(); + void testItemReplaceExistingLocked(); void testItemSecret(); void testItemDelete(); + void testItemLockState(); void testAlias(); void testDefaultAliasAlwaysPresent(); @@ -88,21 +97,38 @@ private slots: void testDuplicateName(); protected slots: - void createDatabaseCallback(); + void driveNewDatabaseWizard(); + bool driveAccessControlDialog(bool remember = true); private: void lockDatabaseInBackend(); void unlockDatabaseInBackend(); - QPointer enableService(); - QPointer openSession(FdoSecrets::Service* service, const QString& algo); - QPointer getDefaultCollection(FdoSecrets::Service* service); - QPointer getFirstItem(FdoSecrets::Collection* coll); - QPointer createItem(FdoSecrets::Session* sess, - FdoSecrets::Collection* coll, - const QString& label, - const QString& pass, - const StringStringMap& attr, - bool replace); + QSharedPointer enableService(); + QSharedPointer openSession(const QSharedPointer& service, const QString& algo); + QSharedPointer getDefaultCollection(const QSharedPointer& service); + QSharedPointer getFirstItem(const QSharedPointer& coll); + QSharedPointer createItem(const QSharedPointer& sess, + const QSharedPointer& coll, + const QString& label, + const QString& pass, + const FdoSecrets::wire::StringStringMap& attr, + bool replace, + bool expectPrompt = false); + template QSharedPointer getProxy(const QDBusObjectPath& path) const + { + auto ret = QSharedPointer{ + new Proxy(QStringLiteral("org.freedesktop.secrets"), path.path(), QDBusConnection::sessionBus())}; + if (!ret->isValid()) { + return {}; + } + return ret; + } + + template T getSignalVariantArgument(const QVariant& arg) + { + const auto& in = arg.value().variant(); + return qdbus_cast(in); + } private: QScopedPointer m_mainWindow; @@ -111,6 +137,7 @@ private: QSharedPointer m_db; QPointer m_plugin; + QSharedPointer m_client; // For DH session tests GcryptMPI m_serverPrivate; diff --git a/tests/util/FdoSecretsProxy.cpp b/tests/util/FdoSecretsProxy.cpp new file mode 100644 index 000000000..b48f28c5a --- /dev/null +++ b/tests/util/FdoSecretsProxy.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 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 "FdoSecretsProxy.h" + +#define IMPL_PROXY(name) \ + name##Proxy::name##Proxy( \ + const QString& service, const QString& path, const QDBusConnection& connection, QObject* parent) \ + : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) \ + { \ + } \ + name##Proxy::~name##Proxy() = default; + +IMPL_PROXY(Service) +IMPL_PROXY(Collection) +IMPL_PROXY(Item) +IMPL_PROXY(Session) +IMPL_PROXY(Prompt) + +#undef IMPL_PROXY diff --git a/tests/util/FdoSecretsProxy.h b/tests/util/FdoSecretsProxy.h new file mode 100644 index 000000000..c8bcafb7b --- /dev/null +++ b/tests/util/FdoSecretsProxy.h @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2020 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_FDOSECRETSPROXY_H +#define KEEPASSXC_FDOSECRETSPROXY_H + +#include "fdosecrets/dbus/DBusTypes.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/** + * Mimic the interface of QDBusPendingReply so the same code can be used in test + */ +template class PropertyReply +{ + QDBusPendingReply m_reply; + +public: + /*implicit*/ PropertyReply(const QDBusMessage& reply) + : m_reply(reply) + { + } + bool isFinished() const + { + return m_reply.isFinished(); + } + bool isValid() const + { + return m_reply.isValid(); + } + bool isError() const + { + return m_reply.isError(); + } + QDBusError error() const + { + return m_reply.error(); + } + T value() const + { + return qdbus_cast(m_reply.value().variant()); + } + template T argumentAt() const + { + return value(); + } +}; + +#define IMPL_GET_PROPERTY(name) \ + QDBusMessage msg = QDBusMessage::createMethodCall( \ + service(), path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Get")); \ + msg << interface() << QStringLiteral(#name); \ + return \ + { \ + connection().call(msg, QDBus::BlockWithGui) \ + } + +#define IMPL_SET_PROPERTY(name, value) \ + QDBusMessage msg = QDBusMessage::createMethodCall( \ + service(), path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set")); \ + msg << interface() << QStringLiteral(#name) << QVariant::fromValue(QDBusVariant(QVariant::fromValue(value))); \ + return \ + { \ + connection().call(msg, QDBus::BlockWithGui) \ + } + +/* + * Proxy class for interface org.freedesktop.Secret.Service + */ +class ServiceProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Service"; + } + +public: + ServiceProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~ServiceProxy() override; + + inline PropertyReply> collections() const + { + IMPL_GET_PROPERTY(Collections); + } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply CreateCollection(const QVariantMap& properties, + const QString& alias) + { + QList argumentList; + argumentList << QVariant::fromValue(properties) << QVariant::fromValue(alias); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("CreateCollection"), argumentList)}; + } + + inline QDBusPendingReply GetSecrets(const QList& items, + const QDBusObjectPath& session) + { + QList argumentList; + argumentList << QVariant::fromValue(items) << QVariant::fromValue(session); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("GetSecrets"), argumentList)}; + } + + inline QDBusPendingReply, QDBusObjectPath> Lock(const QList& paths) + { + QList argumentList; + argumentList << QVariant::fromValue(paths); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Lock"), argumentList)}; + } + inline QDBusPendingReply OpenSession(const QString& algorithm, + const QDBusVariant& input) + { + QList argumentList; + argumentList << QVariant::fromValue(algorithm) << QVariant::fromValue(input); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("OpenSession"), argumentList)}; + } + inline QDBusPendingReply ReadAlias(const QString& name) + { + QList argumentList; + argumentList << QVariant::fromValue(name); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("ReadAlias"), argumentList)}; + } + + inline QDBusPendingReply, QList> + SearchItems(FdoSecrets::wire::StringStringMap attributes) + { + QList argumentList; + argumentList << QVariant::fromValue(attributes); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SearchItems"), argumentList)}; + } + inline QDBusPendingReply<> SetAlias(const QString& name, const QDBusObjectPath& collection) + { + QList argumentList; + argumentList << QVariant::fromValue(name) << QVariant::fromValue(collection); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SetAlias"), argumentList)}; + } + + inline QDBusPendingReply, QDBusObjectPath> Unlock(const QList& paths) + { + QList argumentList; + argumentList << QVariant::fromValue(paths); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Unlock"), argumentList)}; + } +Q_SIGNALS: // SIGNALS + void CollectionChanged(const QDBusObjectPath& collection); + void CollectionCreated(const QDBusObjectPath& collection); + void CollectionDeleted(const QDBusObjectPath& collection); +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Collection + */ +class CollectionProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Collection"; + } + +public: + CollectionProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~CollectionProxy() override; + + inline PropertyReply created() const + { + IMPL_GET_PROPERTY(Created); + } + + inline PropertyReply> items() const + { + IMPL_GET_PROPERTY(Items); + } + + inline PropertyReply label() const + { + IMPL_GET_PROPERTY(Label); + } + inline QDBusPendingReply<> setLabel(const QString& value) + { + IMPL_SET_PROPERTY(Label, value); + } + + inline PropertyReply locked() const + { + IMPL_GET_PROPERTY(Locked); + } + + inline PropertyReply modified() const + { + IMPL_GET_PROPERTY(Modified); + } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply + CreateItem(const QVariantMap& properties, FdoSecrets::wire::Secret secret, bool replace) + { + QList argumentList; + argumentList << QVariant::fromValue(properties) << QVariant::fromValue(secret) << QVariant::fromValue(replace); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("CreateItem"), argumentList)}; + } + inline QDBusPendingReply Delete() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Delete"), argumentList)}; + } + + inline QDBusPendingReply> SearchItems(FdoSecrets::wire::StringStringMap attributes) + { + QList argumentList; + argumentList << QVariant::fromValue(attributes); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SearchItems"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS + void ItemChanged(const QDBusObjectPath& item); + void ItemCreated(const QDBusObjectPath& item); + void ItemDeleted(const QDBusObjectPath& item); +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Item + */ +class ItemProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Item"; + } + +public: + ItemProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~ItemProxy() override; + + inline PropertyReply attributes() const + { + IMPL_GET_PROPERTY(Attributes); + } + inline QDBusPendingReply<> setAttributes(FdoSecrets::wire::StringStringMap value) + { + IMPL_SET_PROPERTY(Attributes, value); + } + + inline PropertyReply created() const + { + IMPL_GET_PROPERTY(Created); + } + + inline PropertyReply label() const + { + IMPL_GET_PROPERTY(Label); + } + inline QDBusPendingReply<> setLabel(const QString& value) + { + IMPL_SET_PROPERTY(Label, value); + } + + inline PropertyReply locked() const + { + IMPL_GET_PROPERTY(Locked); + } + + inline PropertyReply modified() const + { + IMPL_GET_PROPERTY(Modified); + } + +public Q_SLOTS: // METHODS + inline QDBusPendingReply Delete() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Delete"), argumentList)}; + } + + inline QDBusPendingReply GetSecret(const QDBusObjectPath& session) + { + QList argumentList; + argumentList << QVariant::fromValue(session); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("GetSecret"), argumentList)}; + } + + inline QDBusPendingReply<> SetSecret(FdoSecrets::wire::Secret secret) + { + QList argumentList; + argumentList << QVariant::fromValue(secret); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("SetSecret"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Session + */ +class SessionProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Session"; + } + +public: + SessionProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~SessionProxy() override; + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> Close() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Close"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS +}; + +/* + * Proxy class for interface org.freedesktop.Secret.Prompt + */ +class PromptProxy : public QDBusAbstractInterface +{ + Q_OBJECT +public: + static inline const char* staticInterfaceName() + { + return "org.freedesktop.Secret.Prompt"; + } + +public: + PromptProxy(const QString& service, + const QString& path, + const QDBusConnection& connection, + QObject* parent = nullptr); + + ~PromptProxy() override; + +public Q_SLOTS: // METHODS + inline QDBusPendingReply<> Dismiss() + { + QList argumentList; + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Dismiss"), argumentList)}; + } + + inline QDBusPendingReply<> Prompt(const QString& windowId) + { + QList argumentList; + argumentList << QVariant::fromValue(windowId); + return {callWithArgumentList(QDBus::BlockWithGui, QStringLiteral("Prompt"), argumentList)}; + } + +Q_SIGNALS: // SIGNALS + void Completed(bool dismissed, const QDBusVariant& result); +}; + +#undef IMPL_GET_PROPERTY +#undef IMPL_SET_PROPERTY + +#endif // KEEPASSXC_FDOSECRETSPROXY_H