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 8b2b16cf..3b3adbfb 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 8b9a02a5..fc8af88e 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 aebdb455..5e737514 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 a9750bc2..35728a17 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 8004de24..20247a57 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 28233460..13f8669f 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 20eff4a0..c2ebf9d4 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 5a902887..24a37a8d 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 00000000..4fa47465 --- /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 00000000..994a9d4f --- /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 00000000..74c13d72 --- /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 00000000..eecce574 --- /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 00000000..cd44ce2f --- /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 00000000..ce4a88fc --- /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 fb7533c1..63a8df60 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 00000000..d1177890 --- /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 00000000..715c9523 --- /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 00000000..01171e53 --- /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 0f856d87..f4341ef8 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 80940d5a..d80fc0e3 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 d51642a8..00000000 --- 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 ffd10add..00000000 --- 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 889b8e11..00000000 --- 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 c249eaee..00000000 --- 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 ef1e2276..00000000 --- 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 adf4f3d4..b7964937 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 8c753a3d..f246a31e 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 efed6317..78cc3efa 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 9a972567..ea5afc5d 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 957203d8..38dc0aff 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 5b1ff5ac..674e5c22 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 0c643f2f..04c9f607 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 3bb6ea25..f3366d68 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 26f080c3..efb6da1f 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 4d656c0a..e1450784 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 275145b4..00000000 --- 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 f5220108..00000000 --- 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 93bbc72f..00000000 --- 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 7116041b..00000000 --- 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 4a6da4bf..00000000 --- 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 ff8a945c..00000000 --- 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 9f439081..00000000 --- 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 cacf9a99..00000000 --- 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 b369c127..00000000 --- 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 6597bfff..00000000 --- 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 40806170..00000000 --- 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 00000000..1cc3a983 --- /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 00000000..0c394b12 --- /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 00000000..2de17d5b --- /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 00000000..b2f81680 --- /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 00000000..47678713 --- /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 70372a2a..aa2b33ad 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 b07bb163..e933f5cf 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 73169593..5b7b8054 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 c323b390..c4a58a5e 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 abc15d56..7034d7bd 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 2d640aeb..41c1cc6a 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 efe60212..fc8d9ee7 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 299c3f7b..eba97f67 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 1cecbbea..c41a6578 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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 NB)%UEn7S1h@7fwt7oY$D literal 20334 zcmV()K;ORu*`k_f`%AR}00RI55CAd3^5(yBLr}h01tDtuTK@wC0000000bZa5FInm zEjusuy#zSYI7NXE26dzs1(zQ3#TJXNwNwAb^P+29La!e_Z>;2_OK#M(IpbB+!OUp;FOnQOASe(`WmFYwc}Z zdfhnyiS7Lg1ONg60000401XNa3atP%^BC0@!5tgetU+G40#@$sl;QKgP| z*KFM-KqHF@(9TB$QlEr_J~pJ`e&=#LHJaE#V);#;Qc|wke3Pz&YQ=6VWhhTMeFfR^as_nbbAYI@F%ooyo zbgcopgsehK*=u%KG;DQGXqS5#*$_Y7T!OmcuJt$YPc5lU{9#{QXjZIDx=;m|Wf&|I zW3{G$jb&WTpqwi-NW%x1R1w#p*Ekl?h-eI33k@MzC??_;m}?3@)*4`uq$m2#^u_vH za6h&I>+zbumH@nIjE#5H9@4tSw6xk;at_(OU-^Q_CjY&?R|d;{Ku^5-EVcZEam$E_ z7}~2U1ZR_&B8_SqY1uTTeMUV7FUX;1eJ9kgJ}4i%NZ*g)8AyW z9@l@AwLrd5JS~n8BM7iRTDolBt*(zZK>ISa^B%xM_!5ZOn->M#J73v%L(?8v@0g2fACl&#$Uq@glU zkj_$0zW}7Gy+Hns+$Jr52;M_#W5#Ug(>ZC+`cSNw=em3kndBK@jl-1`R_h*LWd`Fy zJ7s!`0T=COGmlJj!jKReqM5_QSQRHzMe=DWbzTZR6J-!iLyJO-D%gEu1gw3U>h ztiYU_P^Y~WYea_#*VKqsV4ScVkx?nTB$u9WBQhwFFWR{iol%#Yw=<%fW69{4fV3m+imm7iqV$ zI=+qDRMTB6B*j5lERM=6|KAeO#=l&LmWbQ78kDCve}QsYEDzWac#Bo{2h-3`t-TQd z%HsH=l^;>F;lp!+ywBR$sIaEZ&mBIvS%U3qJKnNA59?u{HB)I{ zhh8Lv!5wKs1g#7{$AAjb06~(@e#3JR_vE2#4z$|?6@;SXDu9qZU>gTBzF--V^kThW zyWBCfesNzzzlqbF=Bot7=yTBd+_nyMQH`%ehBQV3rmw(8Vz6(FJQpJtp_FX;WnK3~ zl6&7@l!$DLOg2uGN;h>RdyvMQWg%)F%lgts$0c-Fd(gaO^Zaf_nP&i7QqwMCx+x8TBsk&kBK*bV0QwkNm-WXm0}aX-`9b&4UJk zf`YeEH#xMZD$iq&bUY8m?DfN?g$XAIdP(AeokzAO$1zxgaT^;v;nlk$&4h?TUQ8?E z=XE|%-&$VoWJr@In>{7{2X2Z8*N$?>*|Ar$Uj~K4oztS#g0hhSGibd~z9s{yPdAa7 zVDtk8=MB7pQXU_nF%ipA9YdysPSgtN>f=?BK$8n_43p1;hT{gIDx1FLuYnR!WoZ2a z$O7PXK?I9U`Up;=_;g7@eS@d1YKO!$n?S2mQ zasSc)mq*CxB1vI*nz{Pv#PGDkGiPb?hW5o5JS0BAcS>h=EOyJ6nk?jqZ=n0wa?fsI zah17`Cm$bjp@jY#;akoC(hq*k*qsY|^XMr%BGJ|LDsV?NC;BBnvw)IqG2RwTf0};l z$4+?%C1!m`Di5}*N0J&%xjQKNL;RJ+v~bh+^woJ49oE?g?2ioR<5P38qEXSp+^&|y z5-lIXvf{7u(lygDorYR^W_|f$`$;ZUyD{|!6$(ARh-2vumM=xiYOr|IK3Ql-#R}=y zbAJh#iy(Wmgnw{GO=h&y4`&j}(EU}Ar#SF-bqe4Xt)AW5{`8Owh{hw-YHz`<{^$m*;6+k5)o%0-*E11I1h`a5-%L7vQY&8@O?E+=7G*X6rB)Uv)TOkTmvqCn2A=?Q1H}-{lNX^AIbRXw$0*G8;gC3 z?oGT6O{r`;>&k%cqGmmOQ06vYKqXhQ!L~Dvmy7S)g8eo434Lbv^OSJB9~Tjch6zaTF!Xv zFsx%!S-pk_Lfwi(&6^Z9CJr%}hw92a;qAlJkXd&-8Lu=4-eHO6dK#@xSVVJJ|8aji(2e&d*UaM$l`V%-Nm^`S|{Rh7YZ(6O{E$5GsOZC)0HKUHOAuH@L3e}SM z=(yz`Vz9Ee9RpCYwRlk!rs47b5Q>VQDVQk0@f?stxy)LD}LjJztR2{=kB(!6wlmSP(uxh?mEXSAT18m=FTBWzv} zAqXsPzBl~)Q#FdaXqXkzZGP4@O~ot9XU--_Y@7?BXa38eAD&09C$mE6b~r4-Dwfd{ zc7=K)jubi=ajyv2ebXA2lM!hya@-6)+z29}(PxQ!XX1GW25IFT^N#k&*UhjB3=ZnI zkjSJ)f9dprmYDWWi&CORNhIy*4vuhIT+$7_u7IytkNn~A+(~r-jlue@ckZaygSuEi zJL-5WD^?JP_&_jAs9zk|@)XiM2>q5K+OHxg7uiZNkC{6*x4jbEPb_$?n8{!J3+&(f(}CP7R$`rg z?&r9&+_>$|>){iUf2J7J&6PrH$lz^RTrr6BLbgd_9L6S9dfDAq5=nBaZ7!1{$-+#F z5Oe2+#aN*t#@Yzkk01Mfc%Bh;{X-jn0CBiXuKPoB0%YO$={V30H^KQ?xq(EGHJ{4g zrm-oovj9}N?37DLS9i2b!V!YSwDvV@F|~hA+;N{X6D>EfCKgnM83CN<*@TYJ_*zE= z#@dAO;`x30wv$75y17HZ0z>i%kB7Jq!0W;64Ax8a)BPjpx7Q(49LYpCY&JF zh3{{!v7Q>KxhB?qdw8(na{twd3l@J2RX)~qcDP4o*zse*do0@{nYcgt3~w78 z_J%%3rUjjIC=?m5=Mkq|4ZBcn+tmQu0YvQ76wR65qou-t(*tym@kk92`iYlNB0|rh z6r*95WO%$rKJxd4Op?Anw37$qK1^SVBMEv3Km+QcFlw~%XHvsYJ%gJ4VqiA3*QMB? zo|2o(nIAB<9bud}ppSi7RH1aLu1bDTll3_t-@(b8E|4=n`zGcSE7;J6!FoCH_%TFjvwD#ur&>SNd8h`7#tc&(=mN7ThPu>Uar8`fq z#;|Kzs!lsJ9Rkxc(QoG$%BZc8)3WZhr0dQKi|ypiwCC0{Z}p0*Oz%_qm628A0)F3% zzLT2LbtE66b!QQ%P>aWAL5@w`d8LB+qYKk`?xrt9Pi+?5muM8-El&k&@H_e-#O9^d zTK2`={p{);(0L2&fBy03e{*{Q-gqwkCVc_+TD2c-<=ceLKLe$&t65u5L{gFLH3*bl zV3AJ8#D(2*sl2|4Bq%2FoZ_^!Wc(moaTpALkySQeKNdpE^>|Mu5is%Cgf*grfpVAg zQ)iv931a||n_(&|%Cs6SHy95Q3)rEg&t6;J@p_o-BsoYG(y}D`%ve7|XJyOiY1U(7 zQ_CUDx4ZQyvyXQsAP_SE7?dXBHg#*-%q0V?{qtbkv6DR>K7oJEc;KGKQlPcYcpgxkD);ix%2X(|M>RREE`75X;XLV%Uz2xe4x~@sxxUhl3Xh2b-W|L_eLc z;N}AO!jiTILZ;VR@E{_Jpcee*Z(!T-Xq6)L>&-N|@%n3Y0($C2fy`b~dtzQ=OVT~Jk5?$w2 zTp=Uy*_O$uzbYm-Nt~x|4(-q4v*nMg;30JcWB?7B7QF!^5qs{!kVyG3fH)cNAHNUb zV-y3u-0$rkEo80CD`|&_{(c<+wr zA#`iP26#AH16$R^o9NPQ^k-3a>BQk~<9k?$`Fn65%iYmrvJ#UafmD*}?qbeP!1{9f zG!3nrgmO;94`Z@V8}ro=%7I=v)P3~$GlA`|SWw3cI+X=<{AW&W9eEpR$z}nOq7TTI z=rC`u%4peo{&wca&^-oDweB$dAP=AxTKvAzT#Fb~rF*7>qGw%U%$R z`XP90vU}dL-GlW$1Zuh>P+ab9T94x9Ux{vhE+gQgsJa$8Sg=KXWB^bc17lw^QX@Q5 z6OfWMIk#UToPxvOr`(kbYkN+8n8ZP+Q>~w_m|vt+evWa_?K6z%vA9fahUByM4^*5L zZ;MCKfS?q&KUfZU-rx+qg=beB5!8|Wb$OH0*?-xjg0)02#N9|?G#ZQTzZ^2MzX=#a zw{M2cf1mN&yBHdR>0s1f`hibf6f>(r#YA^{B&gYK^!m*%_2X4=inXM9MPU;0(e%K; zFJ_;sZ#(t-yOSFqsWH#;*Iuam*e3$=6(F||36lR7>e503_|^hX26oq!O9As^mtHgx zTib{>+?|^yn6dU{OGhC;`6|=8zukOLZdb;UmjKR93g;49Ze#v3rw}nJVRjL`c!~!q zd5vx5i@^Xpp2(`ni_K9x@QgS)7)tupVI{(Bj|%(+Y_I%yPm9M);8WW-X(ttlJJ}VX ztEsenv>_2DLZb?O2!_7W+g0GtJ{_zPE`Gg!?|ltHVv>^U6VAfHs9;W&wo@0cgxudZ z9y(KE2fT-eazvAu4>G6cPT1V^&K4&YY&GXOlarN(2cs}3@ zfgl~guT4BgY`8kEn~Iob*K6{D3Z#6>NOLM{w0*Lf$Wf=(Q&t*(j_ZLO*A|OAFijYw zE_#zY+@YbfrkwNy6&-{7U=l5M6fDu;Ywlk~h4n94m&K73N${+k4}D!U0_?L6SW6Q!(hW&uFlBKfJK7>Mi# z&oNn;U41U-(^>KPd3H#L!R26uB7^xtHJHTfLIS&lyRb4IeLL-nT8#PJnEFKFFy3yX zr>@|5+4AehAca#kxoZ3Y%nhkRW%g9e%%QLWzdZd$bQK$$@+CI_QW#%&q}gBi<`% zBV410u&Sx2%m|Xu6~y^#Gj+Q!R5Z7a-zzUZYrLuE^IJj0R!ZdOwb;QL#=>V#&G3(o_aF=(ySPPendD(FP#{3C6 z!eFsE;6NJcK0G}R6{Hoacsm+6`AkFCUf=&zXw&tf?ih(yoi9DEo8uc20*S!e8x{kO z64Pv>+tp4ubYIf3ksr{ml}$HkGAYXwfa^NM2L|VkAv#Sm1?lyzS+)Nk6zG3lXQ^uS zPUg^-rig8VO@R!eTkOYBHnTwLNu{1CgLONEA-8pE=QZ-{<4_rnABGq;6nVUp5fM2l z$LhOi=CGnxbOdDTy@6=;Qr17rlR58y*Ez41rSQzGJ;xUBoeMoSLiL(9cha1L2hP+c z>pUqu&|vWU=h}4)OAP0}<}*F83sUb)WILm{PKw>fJ}$srd-2b4VoOWZlHKq#<$t8L zm3>%S!M4PC;8;{pH|oFw6ClFv)yu_8_;H)Jsc3z2^uvPyet2B3|LHyyW(B zGB*&EeqV%hgQr**>IIeg=jRw)`612o7Z2>3JV!v?*{}y@!&aWdyhw5y&C4Ri0nb?Y z8#;AYx8bO-M36G!?U@*D+R_F@XH(@@?8j=tiQCiniE*AVi9n`cG?)n-qd$&fMwNhp z&G>yCF9P+I37}3U8QcW!1Ci7s85LHqKekS-2o$#b8CU@?)=I1g^<&GUPO~4j)wfy4 z|A;_GI6rx~H%x@R?=mF5ZIar%$fQVBR)um9*E2ikWq7`h<#t`ri^6;~9E~d{Av7%L z{MTDHIcuHT^!}(~eOyZomTjd;>!mNYYE|isY7_R%_HN`HY!Kq~$oeR<%hepnqkAfn zSD;4_W~p`V~AnH>%m?M9m+m`UR0t_vfw7fGG>kgHkT+ zn)K9U3UqOKec{@aEi>{T9o8v?{%+mY#$|LMLGv1(JF25$=XZf8+YmOI)&=py+?=e8|?8-OT4|BXdupC2zA0n^E6(ycUS+AAT@DHJNT{^dK3HZ8mkfzc(J z2;Z9UxCUHb?++E6N+eDrF^)caAJz3t7T&_^`SLR3RID{n{kMy5Dx}RPj8^N1j_uoz zdJ~s|U62rh?(Lpc!5>SNIdnm5JdLQ@%#9wyER zgB4|*K8)PvQ8f6QUZCR`3iKVTk}e!l{Db=#hzpH$cgbXvxB1*p_;OfiYD&3}B=-=N zrMu|IJ}0o=4z(b?ydh40WtOwQ*-X}5ea#C*8b?Hs?vI$?v7r~h^DSU&LKs1J?;ffW z4w`;e;(HWca?I4Hl)?jfOz8}p&J4T8`)-F-a2hJCLo6xfL<2q*tz#!FV+D zWw~LZp!bJs|2NF~(Ok=haNIGgB+RK{51V2PRgvyM(da)byqQ8V4@T~KI+Hg7ollap zQUfu{EjqAZ1Iv7bbTzO)pn8MeR0sj*+r0ZF(IJ#XfF7B~YsCY4oc9o?#5i1#Rs_Zf ziP)dCC-UzSopZXxqp9&Z4<~alDEIuqJj7?FK~m${<(M#|A5urlQaY}EmRh!EYj5_Z z7DxEKd%XJRqE?CIDUgyf$zhEP>q1Prl}|L4b(2?k2dz&JQHImd^d&jeeUaf|e!mpt zq-5{oBXxgc6*@yut_5T;`dMl#UhZpT=nfuoaN<(SyB@pEa97OxbbWINgGnGV+I}M} zmCZ}fK|U-O5>a0q(}8W(T}j84t+STD#^=z~q|^chW5A@s+0i1YYR@d(I#duZ_tH1q zPIDKoQI?}AKhG5-69@`p>R6%4U@<*9&AGl=k`)gb_8CA`QNk{Uk`G*{7A>%HCx%(z zms+h*M`^9(7h0VVyJYiZ;oSVC}(_s}~o{y}GfGScxJ| z-bUfQS;`|GP@f$^;sCZ%QY2kRUTYtw^ED5)bhHkFqv>mrfdf9Og(cOajQgfQ+>1B7 z(RhNI6zVSMOEZfp%nKDiNEKJY$|-=lpGsS%XO0_}1r_#`hU=|F@(4AyE-OoGY|eSfawX^jy4^`((#VtydPsR^bknp4li)RNcUzO>Lue1_1z$15Q> zF9Y+U*Wbp^o;7f*nVnBa*(xx{W!50ay{hW{Y)v-26BEL!EwnOBQ?vWnI%vdLhx!i@ z6*e)bEMQW~tdIz)rCrV{!CL@R-}$Y@e5bcVf72KW8mixu&t?nIc-s*8LQqHf(D6{W zTJ06u!U`M3S9W{#uzTc)uy9G~?Q)1FKAqS+b9bQ)DU>BPObkm~A6qVlADg-hLGI)X zMj^wK4U!Y6+s-+uxxbz-gG7@PSA7)Ir$MS!Y?R(+3pu!{QSRJnHLTUTtLeef3AKLs8iuXRew7^+ z>`cw^^#&=j%SwK0sH}O5l7~@f1lSOe2G%S(q^H0Xj5WbwEnrMPKGzzv#f%fT6cE^Y zIh=~a!bdlPQG@L-W_D} z8YGm%W8pY%OdZH>8}rVi>uf%z0=-F7N5?D}3_n|>{q@u6jb(=mt&-5zbn_D}l@DC2 zv|bR8@e5@qsxg9+7a1ff_16HYS1$VR%BSCCxguM5m~4plfWY|sTLUBG|$#7*e=VT9xp`$V@alQ-Vt-kl9w)v z)V^5U`tN18KuTTNGJ+%Jlg$aqsl!*Z&!^;|o0oBgrMWna?JlwS_AzKHys!n*{`MLe zwn+}@CK)#75&FQ0@8G}UZr8KHp!DY_dYqW3fP5PELZf4e3Z!A2dctyx8<=FnzmuAs z!}<)4NHE-E>YIdTbo&UZ#?kh1ko&AV<3dqK*KxAF(dUA{{(%#+Scd^zS9$7t z5_CZWZJb}{nTv{~Hrq1{1`v*a#cfG1dB@l-IYkWn79I!r4^1}*~|>D zm!XX3dZY$_nq}%_83R2OEy(3_fzzbsu6T-|T4D;94ft4VwF`>VNgn3l`O^`IG&?Dn zX0qSx*!e772Y~N}i@v8p&#>y`mnmN0i~e5l(qR&O)#h2glJjZ=H8HWPsyf7ZWTO&t zvTZP*UB|4<#Z$QSC!nM;`vO~k$Tx@EfubAyW>hY6mHAR;(rgkw591W_YWz}}H7aR9 zbTp zks&W}9v`(Ohq_2;KG^m-6~?=~ucZK163u(I1@E7+Lql{7)tUa=+co^ui_r`)j37I= zG{)zM>d>2ZJIuY5+qlTvjkgr)-El!gqNnt|lN|>M5HuVe3*($nzd2Q$wi!7FiHzHF zH>>jjr`Y&D%3B|9i(NxV1vqMC2d2t<3hO^$2mH-PAl|QHh$o<&fm5*R5YagvUU#A} zod7A6vn3xH?ML=Y`4-x?HY>X-wlqA)VaEb(m(E3mW0>iFCZ<5tcE-8C#xK}By9|6L zbS{er7YH-G&}6e)2T@;TurQ>EJm|Di0-`C?Js);r&MZSM)&<;vJjHUgWh+Nj&ZdXMQ^mBc&5INk#-uhfsorPWR6qxCX%Ymwnz4dTHsInM zQ4)Y<+SZV6s&UqCe>bTiMmRfgWcHwQTZbUu%D_$A5|m28U`%THdXtNOz)Gg0lHh9CmMKCdDxRU+2r%1l0DyLtCPsv!Jq$*Q#Pt4C| ztWBRMxA!4jg<5$Uld0iFqxe&)Tr`xbgMW6A(vr_KHR3Ivb(%(;5D06_Jk9)?GaF`_ zZ53*QE%$S&2S#XOuF9)Mf3-M5Ixu*lG-2%U9lJy(9fZOaw~Q^5^eT-Rc~BVhxL6jr zwhMBK1Xf@Q_&N{+wNbKZvj1b1VOti*(WjGpEI4!JC2a6g9AW3fs zSc;oRbT3wP2_N4qzAH+G<+ogI?n?Ik>RgY*M~dBAFYUtAz@ztI^Y-7nHUQF;vxLAF z!64n=b*}u8M?r!19spyy=z!v(?#u|8dG%%$xqA*B_FD<{l)Z`x9=Bn#oIgKbYV_C_ z+<1sS_7AzC1y2UN!7hcf;AKn3;3PZrAF6a*eCo35crQiZEI$ifkx|#g&uE#Z9z(ymAry0K~cB&wv!;W?B=-5iCjh&}s-7oqg=hSM!JWH}r@=J>Mb3S#$6KrnrVbK!S z34<;cbMCF;0F#5|VD~jCu;lb#-s&#ZQ4K;?5w5yjTt!X;l2iuu!jPqPPyaMhI-r3_ z3OTeAa|VrfhbF{QG8|uYiAqC@vNCW*Pd`tU0BQXhf~SSD=fFPym~3e>qM@uDI{87?7;`=>ZazT&CVCx#s4jh2%^Fx8 zXKR}Y4XGQsHQt8H9F{*%?-h+SN{WW7bvdcM@~1~AQQ zHH?}8$*Jk2yT0C&2HP^T`G}l>KHv5{SD$_pc?BW8hQ6y2i1!u|970b3dFJXQ=&>ZNss|yC0xnm%HJy+jD;}x zum)$@Y1ktHl&UsN*6^&G-eJmY58qEm?yTqE@*Ukj)CC77;N=kbP@cue0RiC9#C{-5=*P%GKXv@>ih4o zL30asb0LUoz5ZzPyG@GvvpMTGO~ZD&`HtK7PvTx+zgWcd^02K{TeYiT0({M9x9rlY z{8Lp?4L|lL5%*~m?f&H{OhuM7T3)l*Y{{l~c7^}Hkj(*Ztf8KMGy1@d9Wv~HTpL7h3wc&xgX z@s(ctHpLx!6bxqFkSJnITEJ0ENl#5>X``W4{+8W7_TK z_#R>Mx7xzx2-7YqQh!bQIq?Apk}3NkB{5}1+kHa4_tIdNcH{KffB(5HQZNIJCQ(sX zbKvsB;=oiMTBJdrfEhX;p!DCz{z~>u&O%{=cSwniw2~>JJDPu+UIA4MJB^MFOeYl~Q1>Lw}>S8MD{Q6!@v}-I)Hy%ep$#)!rC3 z<M-afi^StHpqu1lo+BiTgXY} zhqXp`k<9*~I{AfZ6VzQH33k7#U{rXSib;iEwCe<8M>^di0CRhr9475(znF4uU|5}pQv!%6_)xt^D!5%V;j;wr~_Z`M@6TQZ zCByiYRIsBFeRPE;NrIkrrDxk^QHx#PoJ;@C$o?S~3!vyVyKDs(T($6^-3M_s->af?PaiwNc=eC*1>t363 zWChymQqDTqBr!!bNc9AdtPc=#=eT(MUc1dF0}m|PKJ}t!1-O0#G#d^@bWt2iX^Aa4 z?kFAjO(0dFe6UBcnA+88Sot=ZS+dKgTxQ#>1K28mMLdGI$x z)QicB5`G=auRjvdYs0&?+*0Wm@mJ_jymN#CuXE-%wr^u3)6g2LtH+?FYdZw&ux$CQ zPsLPG9+`;=mwB}vV(hS|U@4o+jJ}k*GGk~HOgpUt_DnpS-iSV6vFRhC0!fx2_yucU zz(Mwq5QR87l~KLPk|${w1DkG3pBTqx^D%kt8NjuhqmgRvcH7k`s7f`@|4NiXXx^xt zUX!(-XizYFcEfrD372Wfecnx3k184jXx{@6LoPrD-YrsPQuB6wW5WQ=YW!zvVufn@Pygc z-6vn`Ecc+h3GHVUXJHW;h#N)XJqGV=iB=?_oP#1j)n9Ni%6bN5lWnpG6hh*mQ5vY{hA|AC*>l&hkHEa+E%n{AvVSRO;^|k!*3}N z^9K|*W>>RvI<(brYLs15HwBKQ*bKf1Z5_}7a!{i7Z8NKt^ScWG2-DL_qjLjT#|`e+ zs1Nq5&K&~S>^krHk(>SNL2efTN{=5ApR|&-#Q@W|MAvp6?Sy;eGhl#4k{_`CPt?KG zkZCG5oubIvRr849;a_|8NYksa#2&t;VShP*YP|i)-=wA3^MqeZc|R+1`Uncc(3}j& zZMkWk`HxBa*KiK&lDMF>@?Q}dxfW}*bx`ub7@8bS4#U9wj`T^l5(aCh0R>W&M)9dg z46=ytX9V*#e;nP54qAITpO_8b1O^cx%c686esx(IC@H`)?*Eumzl(>bgzP{%{}a7N zc34nneq`T)A6iONx;Xp{&C=4!yPqJF_BVt!?!hA8ubyd}QW50(HIgNi`d+#;|IdV? zbSRxkqk@Tn-l_~$2+>J?9Pe6OkV3vPVuPzGxEHgYtFT^_vSVoU>`w7kn^)S^&nHkW^--vPHbH)U zRLqsBEc)+92VEvBa?{5XAIL0D_=7r3w?-F-yQ@zimhU7)9a{nJ|SvS7PG+i6Lb2k;34Yi(2CPJw_9>5F%+oiGWP>L8`t|+xF!J-fu3_ zy+S7bgkYN)lDR5=(1AWcKYr{;2IivRx{~G^JMml4${3eeSX(K1SZm;3V?GMdep#^s zXCLbs#)Em^5C`wtSyx68!^l0ePijF}%8!)`ktSCvO#4Nt!!4e$BDud!J|<~N7VrZT zUtd#9<3@@D;fq@QE5Mc8%`RdR}W*65>fYgjRzHUw2-$=jE2S%Rw|hM7ZnI6(*K9m%H$Pa_dUM|wEGI} zPcO^n55`}#X=lgI_g~*Z&aGm?zVJ!-_ECSQitKQeKA6hNS^-7;wh4U9^PAO{TME_$ z5Vph*;*3@Qv1oC=&my-UzT%nO$fl>lHQDwrggPouI+VAqCI)2_Vp4JzeY!qg2e??L zG$wx0y?~UJx3i@tlCjlCx~1)`6Oq~z`nh2y!(Ci=zb;blUze%FO1vP^8SBAn8K!~K zWA0Z_Tnjfp98ejP;nxmZE~gm;wQM+H-^Wk`yS%PCMrT-*GstLG#z%d)+lp2F2_7Z zfZIGR*AUtVxcJnHtwM0J~uM+76BBgo+5>m9omfw7S2Yq8hP54 z?%+!a)tVfT!DzV|Y|q4(n9j~*w!JtRsS)P-zI zXOv)$tRKInsf^wI?vIb+*t5b?W8|d90q0jFdp65~$}{raXuVR|J?uK6ZbLR<6!?b` zj&5kMSII_;82U<^KG^`=Y{zotq<7>twyD{LTxnCLgEzBm;5PD~Uk*$jn}cCgC2tIh z4o7!K+GTYR-Fz{JkRXvUf{+Z2*aF;0$Z>C~ow;@`Wp}KAIvh&*d|iM}w$@n9rRSNPx`G24Q!oX_d0d zS>jEO?#J|OFGo=97_~kgHvkNB?4si}UZ&`_w(3$0MOUqUoTau6K)PMw921^>+>lVD zixryOgGPYLsOL#K*yek%H%pDhwZ}vpmLDK%Ndn7tdnsb@VH8 z;m^kTw^>>GtR)C{B;Si`LaWqjYd>uHqC64QLNP#wVRF?`?oUIJ2m8k-Upk-FiRyxp z2w8B#ko3ZSnDn%6la*BQz63Os_CsFpG@zj?QibbTj${=mi&Kj8Hf$>#np8wh4H$87 z*1zgQgz3yB);W05B1-ux)*5z@Qx?u&$*wQ8T6+hkRjJ%%(uw3`bmj`nL;5dEA`!j3 zssqq`XigsfUVi3z_U8<%325EJe8=J z_)#P^HpEhcHw0kFO7}*apRlTQNI+%smxD1og9f^RX8myaY2QkYihxo8*HEnAbw$u- zq%*?~AyPJ4J^X(^+AL4qPc&Y1bBslj^OzPX4*)+9RilMtc`<4KduK9{zkXtk9G%l~ zB>4zCmDqvr^;_BF$l-=Y#!SYZ@$(kZ8L6zsD&7!n?Z=6|&*x(>Z9`|2I2lC6aPc5o zmu~8M+<_c|2wMA#tuU#(yFiZ^dUN8x)*A%hnHTpBk&xAP-%^SW+kuAZJThE!9?1LG zK^p(q>*8O(;t$O5zBrcH`uO#(b0Kco+?RxU(mU_XK65v`Mp~k!wNxJ@4?k5dP_R?S zRXhen9p3%uH&FR=9Fw%IW!P+4&Me`T`TdF}FUo%PHJrJNQ?L)%j5`r1lGvUQlhIup zFVVX`GGP8I(9ro~l?bi56bBl{y<*zIe_l}3Lk6p;oO6$bIwblG8S*kyo%cvyCRMw! z=#=YWK7@=>Mta6#EKKZ2Cc_D+E`VV09Ymi+* ziMx#ky1!bC;o}roD_La%8X0i1<5{@fqSX74VZ&04t?J^f5sX;SAhz`|LIiscs0ffb zd155a1)elgmzb?7qKNyi%Qp^KzfM(V(R+aad2dH#iGwR2zCt8IB9Z0#d|IY;7~RfY z@SV`hhhYV0WumB)V0-Mv{_(!vf^d`U(D~5xC~JU(#vpRAz*GjQsfEe9%hArCfr)YaZ4CPNAa}A4nnSJ`m#Y&l}w#yluDxwapM?=*EHdc=E=wsiA=n z8OMJsjvk$aklv@lP${KGSaQq*<*G{Hct0KBN$ z>%}lX9s|~t`?eGz5L7d3nYrJdMH5DEY*9*mPi&4JMIRV9+r|6BPg091%vPNMMOO9L zO*n~smtk0@!v}00Up0LvR$S{*cdsbR;4?(IDepkz+^;l6tX-ZUE_r1W*MMJg{BjkS zASDKdtNHAp2n={_3 z{^jkyBJK%k28q`ODS*74jTi0SpNP7aEH~k{;hwBL6ospU3w0(VB&U|AQWThtJsb4N zw|N%aEko6JS@jROQ>?4cZYs8NGVr2J)B`OHNAcjo0)1XHM|dFfeC2%>=e7n%J7y>$ z=1um1qnM|u=sIUzD+_>`Q&D56#qnzgq8uVg+n!pdk~fsz*|jhvleoE5bkVmT_orh zhi_p{oMpvv`UrRdOdoPDyT9?j6;9sCjL1`MpuxrVdW%D8A8wQ#9|?FFi+lUcNi&mA z07m+_I;$76N)Tuk3UAnLWmgG%Z{3m`>jEa`n=kBJo6fM*cuZ-^9hczJdc_$tYlI_ zkNh1iJVZ(#JIp3;x7*yn+4E46yEK$TV_yAG#^p?rY@~W$p4FD`bHygLi8ViW15ChQ zD!ioR+>`@;>RX(m^6V?3@DzTx(Yf%x zbC2*Mk)Nj=U8sXj4A(1|Y4PPtmcR_A%`mKAY#{e`0&!ydVEkFZ;qd^Wf zf_>8Qs?L3!gxCwJcuYkHt!~937uhLN)ePg#1)h)TD(O)7Mk=dso<=9j`}O?q>Dm|l zQ5;)(rmhl3UIHUbE|eP=H0Y`e_f;fKTym6i5ZsOgg`l8N!{yeUUl9i)jaZUOy9gPt zp7i!q?iqg()RWjO@Y`Cn9XlaLRG7Vbo5NxH`amWO`3`ZLtDh|&UrRQh$<1a>A0)5H z`q}~ENllbv6RSd|iotbk4t_t=fBODe=o)n-b88(HVqC-VuY68n;*P$lKuFcqJ%e-N zP6w%1d%yo6dlm;r8!ohP``$7rO%r9yPNR(QS|`6 zw(7y*e8`~Em$HDJ{#*JB#HKlP?KX$l9tnG)VR+pNfWA7Vx*_(9AKmgl; zRM~rRAxPBhr*Dl9k+!J8uQgr7q<|QgdZVwuFIde@RCTDs-Lt}SqpK#07#0sljDJe# zIii_btG$t`MtPahGmFh>M#kS0=TJ{1_gAZ(}$#_dH}Ke$drrDmE`Q4z>jw+WCNZoS#f;4rWhQnS~mf!lu!_Xajdil++R7XfjRbiTb7Dw`h zQHj;c~PnSKw-{xgDCCl zogW)RXh~**`?M42o_EUZ&9aRr4bj@c06CvFSgX24-y4Rc60E^|G+l2rVZ7dcpkEIDQwZLEG%-aPXB^8 zM0Bg_rfN!A!CqcBb;Tf;lmbrfPIj6@s;)Pj_2U@a_QqJnhr?U^1%rRjE3ozBJvtH7 zrA~nMGvYRlM#ec8tpqn3`lW61A#z`rLtwfNgexoOIxD;a;5k!ew=70o<*bNcNqMXs z2E8_*%$Jg!XcC^H6jRx2s`n}&%486|lC;gl$RBTJQf~n(7YUJOg^_RpP7ks0&3*;a z8?9QL*5yb~HwMjXr;!~lh*Q&GlFsm%?9Gy|mHq?fRp9_6423g#Z5sm4$4tmULetffxu*uWp|@YKxwZM_VX=&IbsX@Jx!-!u{{oi|MeNah#E zw=gwTwd0R?o1ip1f0}src^U+lhNn!&6t^eB$93*Q$kmI0*#;s#)$NNbUjqgg`vQ#X zf&PzYz%pQuX7A0z`!h(ME4;Z%ABIdkk)6Y4KcqQzqT`_Y27gvrXq@L{3k5ydcn|yI zBz31(g%jZIOkR2pdok}i3e`#DPIt5hvJ1q0fvUf-fL~(=i!8x<8V4;GF`si~BBHs1)yAI@=~;A1CA@XM0dr2wX?PZ2?gYR6Y1PJrBXRx`dF$B@3F*bK(KMIka zwC{!U_4MnC%mwL0Pd_Pnx9Wz6d5P!<#Jx1t@Z-rq<@5}14PaZF2y*(88}hyL9%PLT zP3b1nkEW1XjZ4?f*X*bZUgo#~+A%6%E0T7o02Ngik*8k4bQp2^0@aLIJ>8?<5-_H3 zi3(kBORTVK!2aO$f+-1VpF}wO3dI)KY|=_@r$* z3+}a9_?8Wc#6;>KsMgefOP|Ki*v8(bFPnbiU!-c5rffe`t;W~aI6usxscpZ{&_K2x zE_hSes_Hje`M`G4wLwCB*50!-3O+t)Bh1^jBL6*LqDw^~<;BibS1J@2DT1(Us;?u^uUiOg!57fk5 z<&y@LLnE^Z7Ur@=Oz-?#miqDvnGIa{5JdP{1XiQysP*H_Gv-S*{ihn#Gtc9yBH z6GUs6ghbydo){6yXRxQE|9vdaNV)%oaNHA`g_b3zX(BRaR+HuJA3oKe#o&oJK{xnw z=_c|HP~+iqge)ZG#wCd=jYfI_Tfs{(Rg}MQtka=oaYWdtzc;wu{I21?U04ATE6np! zaL;IR)4HN6R1*(b!6#U-R$qoS#|C67xSgMmRG8#Ruapjby=$<=UB8YH-q%1DVNO1z zjl>|BnThXVDR?)XkNcocqwU7ws1s9T^> z#C|8!n##x##QK~-RgELZ7W|HZt3oV39%C6<6-AW1aL&k=J#Cog1uy7l%~a;!ov9GE z1Q>%JlDbCmrm_iAFkBsX1o<09$?FFbEblo+g39MehX39C7nZq)RrUO*r{j z;m75JX&_0dY$j?N@4J_W!>lz3?Bz!0+&ED>>50^*o~1l2#2Z?CTP8XLSsoTU80}%u zVZ}@xeox`WCiQFOXePI#-B>g~^XoNl=%|R*)?7`#_4ATS9os7hgZ!ezw*gb%RYtKT z`5RJ<$F{%R8|0il5M0pOUB6OGFv?jB(o6D&D1Ze&CTmBq2O4TUz z4-FwI3&D(g+m_GKN=Je3JO(0UkB;vqOi+pCWxgC!OG`-Ob^cm+?(wY~Usr4!qJBHk z&SA%hQH$jp1m&=iV-2agG22~s)a`ezB?ZL;&@1x8ArD7>d*HM0d;K*Qe6Hx4hqW%xB*O*TXkjZML2q?GuJ%4>}kIWt*l)*y$^VqSx z@|Kf2gwLK{r<=aJr%O#G1MZr#qOn;$cN5VKzfvZ%gUHQxzoZMzF=cRa2y~xA+d&J| z2fyA*V+s)~4e%6e&r^L%P26?P54Zl!CO|y$PPLi--k~Td&4(HZP$fFK!?nLnSL*E~ z{R)o<|Eq4!kp&{yzG>X-qqln)OQW$97uFV8@q~Dc=lM6w@>@Z=Yt=rt5Q{{el+b24#($tJ z#d9?=D?TL4(!H9cnk?VThgpGqmP4y|aMsQNRG}baTft;+$hJ0+>*KERUw_$Vb+FNY_eVfH_4l^PqrjUKI_-Z5pZVGoUsMjTb@a)( z0^i6=Iq3~F_gD{r@Uy^Zgzg`&UVGPkstNBjyKDonZx5&qk@d;w34dLP<=q0qUI$V+ z;vQX6vMe%2-@E@mlXx{i$rb??QizLQk<_l;7rDcZYKUJ2cMcU3K$BxB10qFR3WL=? zjkajT8@)m{DJA?*ws|}$aN3wcP@S}lKtz;yCz75FlQG`xXhxvtC0*i3|J#`*vz&|i zht0`mPXT6m1q?PX>;y=H<(K;xtcZL)_O*7G7|0OVx-%?=K)U_n)dLRu8Z>{jjtO)+?}wpBm^6M3a%uh|X>- zQAd8x84L0d-+nvtI|Q=mQ5-STP=Uxd_rDS2)#L7{7RoVs-~GH^_+X(%;e~+8v*DBl zxf5IU+eXeEoX$2IlkR~mxf)bbVr3g66GdRfjSt=f%GIWmq~QFhoVX#vm){!^>&Qj} z0N)Wft^1sd^R}wh2+_af;CW3cJ`tpbX~QEfKNgk=h`pghL(nExgJojr1kd3ejmNw@ ztp~~Zg5NLIqr0wfT82e88Q2KN_`2 z!*BZmFGXExal?V`OX4nJJmsurA(VVYXNh!to~It7>>Q8hrYSLiEfgp|^mzd^q&_}Y zhfCa&RZh|fhyK60(mmp$jdo59gctRz*}r%J>&1idmOT6W-zP*nPa0`l*@_$HZRG8r zeG3Kf)&L(A+(R#uEYdvLO4Iv=(N34hY_SwS#ZGwnMbZE~*rlam?T4V<2G0y-zfN3v z?E7>Z8V3@OsZesQ{Wl+BP`+~&*blxQ%%81-DwbqPYd$pYpi|^{=u#pi`zlpjs!^zD zLz0_e9w3UMGGk)E5W8+JK2aBCU#y?ZijVmg&t~S0^df{bR4&ujgj}WzdYBX&rV=R& z=|wP+t&CDKqxU;Jrj(A5maX6jCd^em<;x4%Ki&_rh5jqauSE6vS9_!BHoxdFVg4VE zm7Fd|=NFsaX~v z!UyXhN1a==?1%`w(JQAdDyJV5U}cm3bqvDS6z - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 d9c39a2e..00000000 --- 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 92aa8df8..00000000 --- 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 40240bb4..00000000 --- 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 7d358df7..00000000 --- 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 1d5822d2..3264da51 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 eb8192d5..55502127 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 84f7147e..8ded8658 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 00000000..b48f28c5 --- /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 00000000..c8bcafb7 --- /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