diff --git a/CMakeLists.txt b/CMakeLists.txt
index abac73c94a..0b6af4abfa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -50,6 +50,9 @@ option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
option(WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)" OFF)
option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
+if(UNIX AND NOT APPLE)
+ option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF)
+endif()
if(APPLE)
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
endif()
@@ -65,6 +68,9 @@ if(WITH_XC_ALL)
if(APPLE)
set(WITH_XC_TOUCHID ON)
endif()
+ if(UNIX AND NOT APPLE)
+ set(WITH_XC_FDOSECRETS ON)
+ endif()
endif()
if(WITH_XC_SSHAGENT OR WITH_XC_KEESHARE)
diff --git a/COPYING b/COPYING
index b3eddd7669..ed2f73a4f0 100644
--- a/COPYING
+++ b/COPYING
@@ -241,3 +241,7 @@ License: LGPL-2.1
Files: share/macosx/dmg-background.tiff
Copyright: 2008-2014, Andrey Tarantsov
License: MIT
+
+Files: share/icons/application/scalable/apps/freedesktop.svg
+Copyright: GPL-2+
+Comment: from Freedesktop.org website
diff --git a/INSTALL.md b/INSTALL.md
index c0c3a7a086..1ea2268550 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -102,6 +102,7 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
-DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF)
+ -DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF)
-DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group synchronization extension (default: OFF)
-DWITH_XC_KEESHARE_SECURE=[ON|OFF] Enable/Disable KeeShare signed containers, requires libquazip5 (default: OFF)
-DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF)
diff --git a/share/icons/application/scalable/apps/freedesktop.svg b/share/icons/application/scalable/apps/freedesktop.svg
new file mode 100644
index 0000000000..455a0b3a52
--- /dev/null
+++ b/share/icons/application/scalable/apps/freedesktop.svg
@@ -0,0 +1,92 @@
+
+
+
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5ddcb76eab..0407008f42 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -194,6 +194,9 @@ add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible wit
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare (requires quazip5 for secure containers)")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
add_feature_info(UpdateCheck WITH_XC_UPDATECHECK "Automatic update checking")
+if(UNIX AND NOT APPLE)
+ add_feature_info(FdoSecrets WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API.")
+endif()
if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
endif()
@@ -226,6 +229,11 @@ if(WITH_XC_SSHAGENT)
set(sshagent_LIB sshagent)
endif()
+add_subdirectory(fdosecrets)
+if(WITH_XC_FDOSECRETS)
+ set(fdosecrets_LIB fdosecrets)
+endif()
+
set(autotype_SOURCES
core/Tools.cpp
autotype/AutoType.cpp
@@ -270,6 +278,7 @@ target_link_libraries(keepassx_core
autotype
${keepassxcbrowser_LIB}
${qrcode_LIB}
+ ${fdosecrets_LIB}
Qt5::Core
Qt5::Concurrent
Qt5::Network
diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake
index 2acff44666..6aceaa2a0a 100644
--- a/src/config-keepassx.h.cmake
+++ b/src/config-keepassx.h.cmake
@@ -22,6 +22,7 @@
#cmakedefine WITH_XC_KEESHARE_SECURE
#cmakedefine WITH_XC_UPDATECHECK
#cmakedefine WITH_XC_TOUCHID
+#cmakedefine WITH_XC_FDOSECRETS
#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@"
#cmakedefine KEEPASSXC_BUILD_TYPE_RELEASE
diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp
index f6c67ad502..cf6a4b5f3f 100644
--- a/src/core/EntrySearcher.cpp
+++ b/src/core/EntrySearcher.cpp
@@ -140,12 +140,20 @@ bool EntrySearcher::searchEntryImpl(Entry* entry)
case Field::Notes:
found = term->regex.match(entry->notes()).hasMatch();
break;
- case Field::Attribute:
+ case Field::AttributeKey:
found = !attributes.filter(term->regex).empty();
break;
case Field::Attachment:
found = !attachments.filter(term->regex).empty();
break;
+ case Field::AttributeValue:
+ // skip protected attributes
+ if (entry->attributes()->isProtected(term->word)) {
+ continue;
+ }
+ found = entry->attributes()->contains(term->word)
+ && term->regex.match(entry->attributes()->value(term->word)).hasMatch();
+ break;
default:
// Terms without a specific field try to match title, username, url, and notes
found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch()
@@ -207,12 +215,18 @@ void EntrySearcher::parseSearchTerms(const QString& searchString)
} else if (field.compare("notes", cs) == 0) {
term->field = Field::Notes;
} else if (field.startsWith("attr", cs)) {
- term->field = Field::Attribute;
+ term->field = Field::AttributeKey;
} else if (field.startsWith("attach", cs)) {
term->field = Field::Attachment;
- } else {
- term->field = Field::Undefined;
+ } else if (field.startsWith("_", cs)) {
+ term->field = Field::AttributeValue;
+ // searching a custom attribute
+ // in this case term->word is the attribute key (removing the leading "_")
+ // and term->regex is used to match attribute value
+ term->word = field.mid(1);
}
+ } else {
+ term->field = Field::Undefined;
}
m_searchTerms.append(term);
diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h
index 153a0612ee..4a33949244 100644
--- a/src/core/EntrySearcher.h
+++ b/src/core/EntrySearcher.h
@@ -48,8 +48,9 @@ class EntrySearcher
Password,
Url,
Notes,
- Attribute,
- Attachment
+ AttributeKey,
+ Attachment,
+ AttributeValue
};
struct SearchTerm
diff --git a/src/core/Group.cpp b/src/core/Group.cpp
index 87741ee0cd..814ac2f3b5 100644
--- a/src/core/Group.cpp
+++ b/src/core/Group.cpp
@@ -1057,6 +1057,23 @@ Entry* Group::addEntryWithPath(const QString& entryPath)
return entry;
}
+void Group::applyGroupIconTo(Entry* entry)
+{
+ if (!config()->get("UseGroupIconOnEntryCreation").toBool()) {
+ return;
+ }
+
+ if (iconNumber() == Group::DefaultIconNumber && iconUuid().isNull()) {
+ return;
+ }
+
+ if (iconUuid().isNull()) {
+ entry->setIcon(iconNumber());
+ } else {
+ entry->setIcon(iconUuid());
+ }
+}
+
bool Group::GroupData::operator==(const Group::GroupData& other) const
{
return equals(other, CompareItemDefault);
diff --git a/src/core/Group.h b/src/core/Group.h
index 4b85692074..59e455ac07 100644
--- a/src/core/Group.h
+++ b/src/core/Group.h
@@ -167,6 +167,8 @@ class Group : public QObject
void addEntry(Entry* entry);
void removeEntry(Entry* entry);
+ void applyGroupIconTo(Entry* entry);
+
signals:
void groupDataChanged(Group* group);
void groupAboutToAdd(Group* group, int index);
diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp
index cdb0d1f562..ee4295eec5 100644
--- a/src/crypto/SymmetricCipher.cpp
+++ b/src/crypto/SymmetricCipher.cpp
@@ -126,6 +126,8 @@ int SymmetricCipher::algorithmIvSize(Algorithm algo)
switch (algo) {
case ChaCha20:
return 12;
+ case Aes128:
+ return 16;
case Aes256:
return 16;
case Twofish:
diff --git a/src/fdosecrets/CMakeLists.txt b/src/fdosecrets/CMakeLists.txt
new file mode 100644
index 0000000000..9d3fcb6a92
--- /dev/null
+++ b/src/fdosecrets/CMakeLists.txt
@@ -0,0 +1,36 @@
+if(WITH_XC_FDOSECRETS)
+ include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
+
+ add_library(fdosecrets STATIC
+ # app settings page
+ FdoSecretsPlugin.cpp
+ widgets/SettingsWidgetFdoSecrets.cpp
+
+ # per database settings page
+ DatabaseSettingsPageFdoSecrets.cpp
+ widgets/DatabaseSettingsWidgetFdoSecrets.cpp
+
+ # setting storage
+ FdoSecretsSettings.cpp
+
+ # gcrypt MPI wrapper
+ GcryptMPI.cpp
+
+ # dbus objects
+ objects/DBusObject.cpp
+ objects/Service.cpp
+ objects/Session.cpp
+ objects/SessionCipher.cpp
+ objects/Collection.cpp
+ objects/Item.cpp
+ objects/Prompt.cpp
+ objects/adaptors/ServiceAdaptor.cpp
+ objects/adaptors/SessionAdaptor.cpp
+ objects/adaptors/CollectionAdaptor.cpp
+ objects/adaptors/ItemAdaptor.cpp
+ objects/adaptors/PromptAdaptor.cpp
+ objects/DBusReturn.cpp
+ objects/DBusTypes.cpp
+ )
+ target_link_libraries(fdosecrets Qt5::Core Qt5::Widgets Qt5::DBus ${GCRYPT_LIBRARIES})
+endif()
diff --git a/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp
new file mode 100644
index 0000000000..afd888ed27
--- /dev/null
+++ b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "DatabaseSettingsPageFdoSecrets.h"
+
+#include "fdosecrets/widgets/DatabaseSettingsWidgetFdoSecrets.h"
+
+#include "core/FilePath.h"
+
+QString DatabaseSettingsPageFdoSecrets::name()
+{
+ return QObject::tr("Secret Service Integration");
+}
+
+QIcon DatabaseSettingsPageFdoSecrets::icon()
+{
+ return filePath()->icon(QStringLiteral("apps"), QStringLiteral("freedesktop"));
+}
+
+QWidget* DatabaseSettingsPageFdoSecrets::createWidget()
+{
+ return new DatabaseSettingsWidgetFdoSecrets;
+}
+
+void DatabaseSettingsPageFdoSecrets::loadSettings(QWidget* widget, QSharedPointer db)
+{
+ auto settingsWidget = qobject_cast(widget);
+ settingsWidget->loadSettings(db);
+}
+
+void DatabaseSettingsPageFdoSecrets::saveSettings(QWidget* widget)
+{
+ auto settingsWidget = qobject_cast(widget);
+ settingsWidget->saveSettings();
+}
diff --git a/src/fdosecrets/DatabaseSettingsPageFdoSecrets.h b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.h
new file mode 100644
index 0000000000..c54f5a2761
--- /dev/null
+++ b/src/fdosecrets/DatabaseSettingsPageFdoSecrets.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H
+#define KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H
+
+#include "gui/dbsettings/DatabaseSettingsDialog.h"
+
+class DatabaseSettingsPageFdoSecrets : public IDatabaseSettingsPage
+{
+ Q_DISABLE_COPY(DatabaseSettingsPageFdoSecrets)
+public:
+ DatabaseSettingsPageFdoSecrets() = default;
+
+ QString name() override;
+ QIcon icon() override;
+ QWidget* createWidget() override;
+ void loadSettings(QWidget* widget, QSharedPointer db) override;
+ void saveSettings(QWidget* widget) override;
+};
+
+#endif // KEEPASSXC_DATABASESETTINGSPAGEFDOSECRETS_H
diff --git a/src/fdosecrets/FdoSecretsPlugin.cpp b/src/fdosecrets/FdoSecretsPlugin.cpp
new file mode 100644
index 0000000000..668b5fb046
--- /dev/null
+++ b/src/fdosecrets/FdoSecretsPlugin.cpp
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "FdoSecretsPlugin.h"
+
+#include "fdosecrets/FdoSecretsSettings.h"
+#include "fdosecrets/objects/DBusTypes.h"
+#include "fdosecrets/objects/Service.h"
+#include "fdosecrets/widgets/SettingsWidgetFdoSecrets.h"
+
+#include "gui/DatabaseTabWidget.h"
+
+#include
+
+using FdoSecrets::Service;
+
+FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget)
+ : m_dbTabs(tabWidget)
+{
+ FdoSecrets::registerDBusTypes();
+}
+
+QWidget* FdoSecretsPlugin::createWidget()
+{
+ return new SettingsWidgetFdoSecrets(this);
+}
+
+void FdoSecretsPlugin::loadSettings(QWidget* widget)
+{
+ qobject_cast(widget)->loadSettings();
+}
+
+void FdoSecretsPlugin::saveSettings(QWidget* widget)
+{
+ qobject_cast(widget)->saveSettings();
+ updateServiceState();
+}
+
+void FdoSecretsPlugin::updateServiceState()
+{
+ if (FdoSecrets::settings()->isEnabled()) {
+ if (!m_secretService && m_dbTabs) {
+ m_secretService.reset(new Service(this, m_dbTabs));
+ connect(m_secretService.data(), &Service::error, this, [this](const QString& msg) {
+ emit error(tr("Fdo Secret Service: %1").arg(msg));
+ });
+ if (!m_secretService->initialize()) {
+ m_secretService.reset();
+ }
+ }
+ } else {
+ if (m_secretService) {
+ m_secretService.reset();
+ }
+ }
+}
+
+Service* FdoSecretsPlugin::serviceInstance() const
+{
+ return m_secretService.data();
+}
+
+void FdoSecretsPlugin::emitRequestSwitchToDatabases()
+{
+ emit requestSwitchToDatabases();
+}
+
+void FdoSecretsPlugin::emitRequestShowNotification(const QString& msg, const QString& title)
+{
+ if (!FdoSecrets::settings()->showNotification()) {
+ return;
+ }
+ emit requestShowNotification(msg, title, 10000);
+}
diff --git a/src/fdosecrets/FdoSecretsPlugin.h b/src/fdosecrets/FdoSecretsPlugin.h
new file mode 100644
index 0000000000..2a57ea0db3
--- /dev/null
+++ b/src/fdosecrets/FdoSecretsPlugin.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETSPLUGIN_H
+#define KEEPASSXC_FDOSECRETSPLUGIN_H
+
+#include "core/FilePath.h"
+#include "gui/ApplicationSettingsWidget.h"
+
+#include
+#include
+
+class DatabaseTabWidget;
+
+namespace FdoSecrets
+{
+ class Service;
+} // namespace FdoSecrets
+
+class FdoSecretsPlugin : public QObject, public ISettingsPage
+{
+ Q_OBJECT
+public:
+ explicit FdoSecretsPlugin(DatabaseTabWidget* tabWidget);
+ ~FdoSecretsPlugin() override = default;
+
+ QString name() override
+ {
+ return QObject::tr("Secret Service Integration");
+ }
+
+ QIcon icon() override
+ {
+ return FilePath::instance()->icon("apps", "freedesktop");
+ }
+
+ QWidget* createWidget() override;
+ void loadSettings(QWidget* widget) override;
+ void saveSettings(QWidget* widget) override;
+
+ void updateServiceState();
+
+ /**
+ * @return The service instance, can be nullptr if the service is disabled.
+ */
+ FdoSecrets::Service* serviceInstance() const;
+
+public slots:
+ void emitRequestSwitchToDatabases();
+ void emitRequestShowNotification(const QString& msg, const QString& title = {});
+
+signals:
+ void error(const QString& msg);
+ void requestSwitchToDatabases();
+ void requestShowNotification(const QString& msg, const QString& title, int msTimeoutHint);
+
+private:
+ QPointer m_dbTabs;
+ QScopedPointer m_secretService;
+};
+
+#endif // KEEPASSXC_FDOSECRETSPLUGIN_H
diff --git a/src/fdosecrets/FdoSecretsSettings.cpp b/src/fdosecrets/FdoSecretsSettings.cpp
new file mode 100644
index 0000000000..36c41705f3
--- /dev/null
+++ b/src/fdosecrets/FdoSecretsSettings.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "FdoSecretsSettings.h"
+
+#include "core/Config.h"
+#include "core/CustomData.h"
+#include "core/Database.h"
+#include "core/Metadata.h"
+
+namespace Keys
+{
+
+ constexpr auto FdoSecretsEnabled = "FdoSecrets/Enabled";
+ constexpr auto FdoSecretsShowNotification = "FdoSecrets/ShowNotification";
+ constexpr auto FdoSecretsNoConfirmDeleteItem = "FdoSecrets/NoConfirmDeleteItem";
+
+ namespace Db
+ {
+ constexpr auto FdoSecretsExposedGroup = "FDO_SECRETS_EXPOSED_GROUP";
+ } // namespace Db
+
+} // namespace Keys
+
+namespace FdoSecrets
+{
+
+ FdoSecretsSettings* FdoSecretsSettings::m_instance = nullptr;
+
+ FdoSecretsSettings* FdoSecretsSettings::instance()
+ {
+ if (!m_instance) {
+ m_instance = new FdoSecretsSettings;
+ }
+ return m_instance;
+ }
+
+ bool FdoSecretsSettings::isEnabled() const
+ {
+ return config()->get(Keys::FdoSecretsEnabled, false).toBool();
+ }
+
+ void FdoSecretsSettings::setEnabled(bool enabled)
+ {
+ config()->set(Keys::FdoSecretsEnabled, enabled);
+ }
+
+ bool FdoSecretsSettings::showNotification() const
+ {
+ return config()->get(Keys::FdoSecretsShowNotification, true).toBool();
+ }
+
+ void FdoSecretsSettings::setShowNotification(bool show)
+ {
+ config()->set(Keys::FdoSecretsShowNotification, show);
+ }
+
+ bool FdoSecretsSettings::noConfirmDeleteItem() const
+ {
+ return config()->get(Keys::FdoSecretsNoConfirmDeleteItem, false).toBool();
+ }
+
+ void FdoSecretsSettings::setNoConfirmDeleteItem(bool noConfirm)
+ {
+ config()->set(Keys::FdoSecretsNoConfirmDeleteItem, noConfirm);
+ }
+
+ QUuid FdoSecretsSettings::exposedGroup(const QSharedPointer& db) const
+ {
+ return exposedGroup(db.data());
+ }
+
+ void FdoSecretsSettings::setExposedGroup(const QSharedPointer& db,
+ const QUuid& group) // clazy:exclude=function-args-by-value
+ {
+ setExposedGroup(db.data(), group);
+ }
+
+ QUuid FdoSecretsSettings::exposedGroup(Database* db) const
+ {
+ return {db->metadata()->customData()->value(Keys::Db::FdoSecretsExposedGroup)};
+ }
+
+ void FdoSecretsSettings::setExposedGroup(Database* db, const QUuid& group) // clazy:exclude=function-args-by-value
+ {
+ db->metadata()->customData()->set(Keys::Db::FdoSecretsExposedGroup, group.toString());
+ }
+
+} // namespace FdoSecrets
diff --git a/src/fdosecrets/FdoSecretsSettings.h b/src/fdosecrets/FdoSecretsSettings.h
new file mode 100644
index 0000000000..5a90288763
--- /dev/null
+++ b/src/fdosecrets/FdoSecretsSettings.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETSSETTINGS_H
+#define KEEPASSXC_FDOSECRETSSETTINGS_H
+
+#include
+#include
+
+class Database;
+
+namespace FdoSecrets
+{
+
+ class FdoSecretsSettings
+ {
+ public:
+ FdoSecretsSettings() = default;
+ static FdoSecretsSettings* instance();
+
+ bool isEnabled() const;
+ void setEnabled(bool enabled);
+
+ bool showNotification() const;
+ void setShowNotification(bool show);
+
+ bool noConfirmDeleteItem() const;
+ void setNoConfirmDeleteItem(bool noConfirm);
+
+ // Per db settings
+
+ QUuid exposedGroup(const QSharedPointer& db) const;
+ void setExposedGroup(const QSharedPointer& db, const QUuid& group);
+ QUuid exposedGroup(Database* db) const;
+ void setExposedGroup(Database* db, const QUuid& group);
+
+ private:
+ static FdoSecretsSettings* m_instance;
+ };
+
+ inline FdoSecretsSettings* settings()
+ {
+ return FdoSecretsSettings::instance();
+ }
+
+} // namespace FdoSecrets
+
+#endif // KEEPASSXC_FDOSECRETSSETTINGS_H
diff --git a/src/fdosecrets/GcryptMPI.cpp b/src/fdosecrets/GcryptMPI.cpp
new file mode 100644
index 0000000000..4fc792deba
--- /dev/null
+++ b/src/fdosecrets/GcryptMPI.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "GcryptMPI.h"
+
+GcryptMPI MpiFromBytes(const QByteArray& bytes, bool secure, gcry_mpi_format format)
+{
+ auto bufLen = static_cast(bytes.size());
+
+ const char* buf = nullptr;
+
+ // gcry_mpi_scan uses secure memory only if input buffer is secure memory, so we have to make a copy
+ GcryptMemPtr secureBuf;
+ if (secure) {
+ secureBuf.reset(static_cast(gcry_malloc_secure(bufLen)));
+ memcpy(secureBuf.get(), bytes.data(), bufLen);
+ buf = secureBuf.get();
+ } else {
+ buf = bytes.data();
+ }
+
+ gcry_mpi_t rawMpi;
+ auto err = gcry_mpi_scan(&rawMpi, format, buf, format == GCRYMPI_FMT_HEX ? 0 : bufLen, nullptr);
+ GcryptMPI mpi(rawMpi);
+
+ if (gcry_err_code(err) != GPG_ERR_NO_ERROR) {
+ mpi.reset();
+ }
+
+ return mpi;
+}
+
+GcryptMPI MpiFromHex(const char* hex, bool secure)
+{
+ return MpiFromBytes(QByteArray::fromRawData(hex, static_cast(strlen(hex) + 1)), secure, GCRYMPI_FMT_HEX);
+}
+
+QByteArray MpiToBytes(const GcryptMPI& mpi)
+{
+ unsigned char* buf = nullptr;
+ size_t buflen = 0;
+ gcry_mpi_aprint(GCRYMPI_FMT_USG, &buf, &buflen, mpi.get());
+
+ QByteArray bytes(reinterpret_cast(buf), static_cast(buflen));
+
+ gcry_free(buf);
+
+ return bytes;
+}
diff --git a/src/fdosecrets/GcryptMPI.h b/src/fdosecrets/GcryptMPI.h
new file mode 100644
index 0000000000..bbc3d04397
--- /dev/null
+++ b/src/fdosecrets/GcryptMPI.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_GCRYPTMPI_H
+#define KEEPASSXC_GCRYPTMPI_H
+
+#include
+
+#include
+
+#include
+#include
+
+template using deleter_from_fn = std::integral_constant;
+
+/**
+ * A simple RAII wrapper for gcry_mpi_t
+ */
+using GcryptMPI = std::unique_ptr>;
+
+/**
+ * A simple RAII wrapper for libgcrypt allocated memory
+ */
+using GcryptMemPtr = std::unique_ptr>;
+
+/**
+ * Parse a QByteArray as MPI
+ * @param bytes
+ * @param secure
+ * @param format
+ * @return
+ */
+GcryptMPI MpiFromBytes(const QByteArray& bytes, bool secure = true, gcry_mpi_format format = GCRYMPI_FMT_USG);
+
+/**
+ * Parse MPI from hex data
+ * @param hex
+ * @param secure
+ * @return
+ */
+GcryptMPI MpiFromHex(const char* hex, bool secure = true);
+
+/**
+ * Dump MPI to bytes in USG format
+ * @param mpi
+ * @return
+ */
+QByteArray MpiToBytes(const GcryptMPI& mpi);
+
+#endif // KEEPASSXC_GCRYPTMPI_H
diff --git a/src/fdosecrets/README.md b/src/fdosecrets/README.md
new file mode 100644
index 0000000000..22278860c8
--- /dev/null
+++ b/src/fdosecrets/README.md
@@ -0,0 +1,42 @@
+# Freedesktop.org Secret Storage Spec Server Side API
+
+This plugin implements the [Secret Storage specification][secrets] version 0.2. While running KeePassXC, it acts as a
+Secret Service server, registered on DBus, so clients like seahorse, python-secretstorage, or other implementations
+can connect and access the exposed database in KeePassXC.
+
+[secrets]: (https://www.freedesktop.org/wiki/Specifications/secret-storage-spec/)
+
+## Configurable settings
+
+* The user can specify if a database is exposed on DBus, and which group is exposed.
+* Whether to show desktop notification is shown when an entry is retrived.
+* Whether to skip confirmation for entries deleted from DBus
+
+## Implemented Attributes on Item Object
+
+The following attributes are exposed:
+
+|Key|Value|
+|:---:|:---:|
+|Title|The entry title|
+|UserName|The entry user name|
+|URL|The entry URL|
+|Notes|The entry notes|
+
+In addition, all non-protected custom attributes are also exposed.
+
+## Architecture
+
+* `FdoSecrets::Service` is the top level DBus service
+* There is one and only one `FdoSecrets::Collection` per opened database tab
+* Each entry under the exposed database group has a corresponding `FdoSecrets::Item` DBus object.
+
+### Signal connections
+
+- Collections are created when a corresponding database tab opened
+- If the database is locked, a collection is created
+- When the database is unlocked, collection populates its children
+- If the unlocked database's exposed group is none, collection deletes itself
+- If the database's exposed group changes, collection repopulates
+- If the database's exposed group changes to none, collection deletes itself
+- If the database's exposed group changes from none, the service recreates a collection
diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp
new file mode 100644
index 0000000000..119ec4ad26
--- /dev/null
+++ b/src/fdosecrets/objects/Collection.cpp
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "Collection.h"
+
+#include "fdosecrets/FdoSecretsSettings.h"
+#include "fdosecrets/objects/Item.h"
+#include "fdosecrets/objects/Prompt.h"
+#include "fdosecrets/objects/Service.h"
+
+#include "core/Config.h"
+#include "core/Database.h"
+#include "core/EntrySearcher.h"
+#include "gui/DatabaseTabWidget.h"
+#include "gui/DatabaseWidget.h"
+
+#include
+
+namespace FdoSecrets
+{
+
+ Collection::Collection(Service* parent, DatabaseWidget* backend)
+ : DBusObject(parent)
+ , m_backend(backend)
+ , m_exposedGroup(nullptr)
+ , m_registered(false)
+ {
+ // whenever the file path or the database object itself change, we do a full reload.
+ connect(backend, &DatabaseWidget::databaseFilePathChanged, this, &Collection::reloadBackend);
+ connect(backend, &DatabaseWidget::databaseReplaced, this, &Collection::reloadBackend);
+
+ // also remember to clear/populate the database when lock state changes.
+ connect(backend, &DatabaseWidget::databaseUnlocked, this, &Collection::onDatabaseLockChanged);
+ connect(backend, &DatabaseWidget::databaseLocked, this, &Collection::onDatabaseLockChanged);
+
+ reloadBackend();
+ }
+
+ void Collection::reloadBackend()
+ {
+ if (m_registered) {
+ // delete all items
+ // this has to be done because the backend is actually still there, just we don't expose them
+ // NOTE: Do NOT use a for loop, because Item::doDelete will remove itself from m_items.
+ while (!m_items.isEmpty()) {
+ m_items.first()->doDelete();
+ }
+ cleanupConnections();
+
+ unregisterCurrentPath();
+ m_registered = false;
+ }
+
+ // make sure we have updated copy of the filepath, which is used to identify the database.
+ m_backendPath = m_backend->database()->filePath();
+
+ // the database may not have a name (derived from filePath) yet, which may happen if it's newly created.
+ // defer the registration to next time a file path change happens.
+ if (!name().isEmpty()) {
+ registerWithPath(
+ QStringLiteral(DBUS_PATH_TEMPLATE_COLLECTION).arg(p()->objectPath().path(), encodePath(name())),
+ new CollectionAdaptor(this));
+ m_registered = true;
+ }
+
+ // populate contents after expose on dbus, because items rely on parent's dbus object path
+ if (!backendLocked()) {
+ populateContents();
+ } else {
+ cleanupConnections();
+ }
+ }
+
+ DBusReturn Collection::ensureBackend() const
+ {
+ if (!m_backend) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
+ }
+ return {};
+ }
+
+ DBusReturn Collection::ensureUnlocked() const
+ {
+ if (backendLocked()) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED));
+ }
+ return {};
+ }
+
+ DBusReturn> Collection::items() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ return m_items;
+ }
+
+ DBusReturn Collection::label() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ if (backendLocked()) {
+ return name();
+ }
+ return m_backend->database()->metadata()->name();
+ }
+
+ DBusReturn Collection::setLabel(const QString& label)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ m_backend->database()->metadata()->setName(label);
+ return {};
+ }
+
+ DBusReturn Collection::locked() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ return backendLocked();
+ }
+
+ DBusReturn Collection::created() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+ return static_cast(
+ m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000);
+ }
+
+ DBusReturn Collection::modified() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+ // FIXME: there seems not to have a global modified time.
+ // Use a more accurate time, considering all metadata, group, entry.
+ return static_cast(
+ m_backend->database()->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000);
+ }
+
+ DBusReturn Collection::deleteCollection()
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ // Delete means close database
+ auto prompt = new DeleteCollectionPrompt(service(), this);
+ if (backendLocked()) {
+ // this won't raise a dialog, immediate execute
+ auto pret = prompt->prompt({});
+ if (pret.isError()) {
+ return pret;
+ }
+ prompt = nullptr;
+ }
+ // defer the close to the prompt
+ return prompt;
+ }
+
+ DBusReturn> Collection::searchItems(const StringStringMap& attributes)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ // searchItems should work, whether `this` is locked or not.
+ // however, we can't search items the same way as in gnome-keying,
+ // because there's no database at all when locked.
+ return QList{};
+ }
+
+ // shortcut logic for Uuid/Path attributes, as they can uniquely identify an item.
+ if (attributes.contains(ItemAttributes::UuidKey)) {
+ auto uuid = QUuid::fromRfc4122(QByteArray::fromHex(attributes.value(ItemAttributes::UuidKey).toLatin1()));
+ auto entry = m_exposedGroup->findEntryByUuid(uuid);
+ if (entry) {
+ return QList{m_entryToItem.value(entry)};
+ } else {
+ return QList{};
+ }
+ }
+
+ if (attributes.contains(ItemAttributes::PathKey)) {
+ auto path = attributes.value(ItemAttributes::PathKey);
+ auto entry = m_exposedGroup->findEntryByPath(path);
+ if (entry) {
+ return QList{m_entryToItem.value(entry)};
+ } else {
+ return QList{};
+ }
+ }
+
+ static QMap attrKeyToField{
+ {EntryAttributes::TitleKey, QStringLiteral("title")},
+ {EntryAttributes::UserNameKey, QStringLiteral("user")},
+ {EntryAttributes::URLKey, QStringLiteral("url")},
+ {EntryAttributes::NotesKey, QStringLiteral("notes")},
+ };
+
+ QStringList terms;
+ for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
+ if (it.key() == EntryAttributes::PasswordKey) {
+ continue;
+ }
+ auto field = attrKeyToField.value(it.key(), QStringLiteral("_") + Item::encodeAttributeKey(it.key()));
+ terms << QStringLiteral(R"raw(+%1:"%2")raw").arg(field, it.value());
+ }
+
+ QList items;
+ const auto foundEntries = EntrySearcher().search(terms.join(' '), m_exposedGroup);
+ items.reserve(foundEntries.size());
+ for (const auto& entry : foundEntries) {
+ items << m_entryToItem.value(entry);
+ }
+ return items;
+ }
+
+ DBusReturn
+ Collection::createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ prompt = nullptr;
+
+ Item* item = nullptr;
+ QString itemPath;
+ StringStringMap attributes;
+
+ // check existing item using attributes
+ auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes"));
+ if (iterAttr != properties.end()) {
+ attributes = qdbus_cast(iterAttr.value().value());
+
+ itemPath = attributes.value(ItemAttributes::PathKey);
+
+ auto existings = searchItems(attributes);
+ if (existings.isError()) {
+ return existings;
+ }
+ if (!existings.value().isEmpty() && replace) {
+ item = existings.value().front();
+ }
+ }
+
+ if (!item) {
+ // normalize itemPath
+ itemPath = itemPath.startsWith('/') ? QString{} : QStringLiteral("/") + itemPath;
+
+ // split itemPath to groupPath and itemName
+ auto components = itemPath.split('/');
+ Q_ASSERT(components.size() >= 2);
+
+ auto itemName = components.takeLast();
+ Group* group = findCreateGroupByPath(components.join('/'));
+
+ // create new Entry in backend
+ auto* entry = new Entry();
+ entry->setUuid(QUuid::createUuid());
+ entry->setTitle(itemName);
+ entry->setUsername(m_backend->database()->metadata()->defaultUserName());
+ group->applyGroupIconTo(entry);
+
+ entry->setGroup(group);
+
+ // when creation finishes in backend, we will already have item
+ item = m_entryToItem.value(entry, nullptr);
+ Q_ASSERT(item);
+ }
+
+ ret = item->setProperties(properties);
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = item->setSecret(secret);
+ if (ret.isError()) {
+ return ret;
+ }
+
+ return item;
+ }
+
+ DBusReturn Collection::setProperties(const QVariantMap& properties)
+ {
+ auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_COLLECTION ".Label")).toString();
+
+ auto ret = setLabel(label);
+ if (ret.isError()) {
+ return ret;
+ }
+
+ return {};
+ }
+
+ const QSet Collection::aliases() const
+ {
+ return m_aliases;
+ }
+
+ DBusReturn Collection::addAlias(QString alias)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ alias = encodePath(alias);
+
+ if (m_aliases.contains(alias)) {
+ return {};
+ }
+
+ emit aliasAboutToAdd(alias);
+
+ bool ok = QDBusConnection::sessionBus().registerObject(
+ QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias), this);
+ if (ok) {
+ m_aliases.insert(alias);
+ emit aliasAdded(alias);
+ }
+
+ return {};
+ }
+
+ DBusReturn Collection::removeAlias(QString alias)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ alias = encodePath(alias);
+
+ if (!m_aliases.contains(alias)) {
+ return {};
+ }
+
+ QDBusConnection::sessionBus().unregisterObject(
+ QStringLiteral(DBUS_PATH_TEMPLATE_ALIAS).arg(p()->objectPath().path(), alias));
+
+ m_aliases.remove(alias);
+ emit aliasRemoved(alias);
+
+ return {};
+ }
+
+ QString Collection::name() const
+ {
+ return QFileInfo(m_backendPath).baseName();
+ }
+
+ DatabaseWidget* Collection::backend() const
+ {
+ return m_backend;
+ }
+
+ void Collection::onDatabaseLockChanged()
+ {
+ auto locked = backendLocked();
+ if (!locked) {
+ populateContents();
+ } else {
+ cleanupConnections();
+ }
+ emit collectionLockChanged(locked);
+ emit collectionChanged();
+ }
+
+ void Collection::populateContents()
+ {
+ if (!m_registered) {
+ return;
+ }
+
+ // we have an unlocked db
+
+ auto newUuid = FdoSecrets::settings()->exposedGroup(m_backend->database());
+ auto newGroup = m_backend->database()->rootGroup()->findGroupByUuid(newUuid);
+ if (!newGroup) {
+ // no exposed group, delete self
+ doDelete();
+ return;
+ }
+
+ // clean up old group connections
+ cleanupConnections();
+
+ m_exposedGroup = newGroup;
+
+ // Attach signal to update exposed group settings if the group was removed.
+ // The lifetime of the connection is bound to the database object, because
+ // in Database::~Database, groups are also deleted, but we don't want to
+ // trigger this.
+ // This rely on the fact that QObject disconnects signals BEFORE deleting
+ // children.
+ QPointer db = m_backend->database().data();
+ connect(m_exposedGroup.data(), &Group::groupAboutToRemove, db, [db](Group* toBeRemoved) {
+ if (!db) {
+ return;
+ }
+ auto uuid = FdoSecrets::settings()->exposedGroup(db);
+ auto exposedGroup = db->rootGroup()->findGroupByUuid(uuid);
+ if (toBeRemoved == exposedGroup) {
+ // reset the exposed group to none
+ FdoSecrets::settings()->setExposedGroup(db, {});
+ }
+ });
+
+ // Monitor exposed group settings
+ connect(m_backend->database()->metadata()->customData(), &CustomData::customDataModified, this, [this]() {
+ if (!m_exposedGroup || !m_backend) {
+ return;
+ }
+ if (m_exposedGroup->uuid() == FdoSecrets::settings()->exposedGroup(m_backend->database())) {
+ // no change
+ return;
+ }
+ onDatabaseExposedGroupChanged();
+ });
+
+ // Add items for existing entry
+ const auto entries = m_exposedGroup->entriesRecursive(false);
+ for (const auto& entry : entries) {
+ onEntryAdded(entry, false);
+ }
+
+ connectGroupSignalRecursive(m_exposedGroup);
+ }
+
+ void Collection::onDatabaseExposedGroupChanged()
+ {
+ // delete all items
+ // this has to be done because the backend is actually still there
+ // just we don't expose them
+ for (const auto& item : asConst(m_items)) {
+ item->doDelete();
+ }
+
+ // repopulate
+ Q_ASSERT(!backendLocked());
+ populateContents();
+ }
+
+ void Collection::onEntryAdded(Entry* entry, bool emitSignal)
+ {
+ if (inRecycleBin(entry)) {
+ return;
+ }
+
+ auto item = new Item(this, entry);
+ m_items << item;
+ m_entryToItem[entry] = item;
+
+ // forward delete signals
+ connect(entry->group(), &Group::entryAboutToRemove, item, [item](Entry* toBeRemoved) {
+ if (item->backend() == toBeRemoved) {
+ item->doDelete();
+ }
+ });
+
+ // relay signals
+ connect(item, &Item::itemChanged, this, [this, item]() { emit itemChanged(item); });
+ connect(item, &Item::itemAboutToDelete, this, [this, item]() {
+ m_items.removeAll(item);
+ m_entryToItem.remove(item->backend());
+ emit itemDeleted(item);
+ });
+
+ if (emitSignal) {
+ emit itemCreated(item);
+ }
+ }
+
+ void Collection::connectGroupSignalRecursive(Group* group)
+ {
+ if (inRecycleBin(group)) {
+ return;
+ }
+
+ connect(group, &Group::groupModified, this, &Collection::collectionChanged);
+ connect(group, &Group::entryAdded, this, [this](Entry* entry) { onEntryAdded(entry, true); });
+
+ const auto children = group->children();
+ for (const auto& cg : children) {
+ connectGroupSignalRecursive(cg);
+ }
+ }
+
+ Service* Collection::service() const
+ {
+ return qobject_cast(parent());
+ }
+
+ void Collection::doLock()
+ {
+ Q_ASSERT(m_backend);
+
+ m_backend->lock();
+ }
+
+ void Collection::doUnlock()
+ {
+ Q_ASSERT(m_backend);
+
+ service()->doUnlockDatabaseInDialog(m_backend);
+ }
+
+ void Collection::doDelete()
+ {
+ emit collectionAboutToDelete();
+
+ unregisterCurrentPath();
+
+ // remove alias manually to trigger signal
+ for (const auto& a : aliases()) {
+ removeAlias(a).okOrDie();
+ }
+
+ cleanupConnections();
+
+ m_exposedGroup = nullptr;
+
+ // reset backend and delete self
+ m_backend = nullptr;
+ deleteLater();
+ }
+
+ void Collection::cleanupConnections()
+ {
+ if (m_exposedGroup) {
+ m_exposedGroup->disconnect(this);
+ }
+ m_items.clear();
+ }
+
+ QString Collection::backendFilePath() const
+ {
+ return m_backendPath;
+ }
+
+ Group* Collection::exposedRootGroup() const
+ {
+ return m_exposedGroup;
+ }
+
+ bool Collection::backendLocked() const
+ {
+ return !m_backend || !m_backend->database()->isInitialized() || m_backend->isLocked();
+ }
+
+ void Collection::doDeleteEntries(QList entries)
+ {
+ m_backend->deleteEntries(std::move(entries));
+ }
+
+ Group* Collection::findCreateGroupByPath(const QString& groupPath)
+ {
+ auto group = m_exposedGroup->findGroupByPath(groupPath);
+ if (group) {
+ return group;
+ }
+
+ // groupPath can't be empty here, because otherwise it will match m_exposedGroup and was returned above
+ Q_ASSERT(!groupPath.isEmpty());
+
+ auto groups = groupPath.split('/', QString::SkipEmptyParts);
+ auto groupName = groups.takeLast();
+
+ // create parent group
+ auto parentGroup = findCreateGroupByPath(groups.join('/'));
+
+ // create this group
+ group = new Group();
+ group->setUuid(QUuid::createUuid());
+ group->setName(groupName);
+ group->setIcon(Group::DefaultIconNumber);
+ group->setParent(parentGroup);
+
+ return group;
+ }
+
+ bool Collection::inRecycleBin(Group* group) const
+ {
+ Q_ASSERT(m_backend);
+
+ if (!m_backend->database()->metadata()->recycleBin()) {
+ return false;
+ }
+
+ while (group) {
+ if (group->uuid() == m_backend->database()->metadata()->recycleBin()->uuid()) {
+ return true;
+ }
+ group = group->parentGroup();
+ }
+ return false;
+ }
+
+ bool Collection::inRecycleBin(Entry* entry) const
+ {
+ Q_ASSERT(entry);
+ return inRecycleBin(entry->group());
+ }
+
+} // namespace FdoSecrets
diff --git a/src/fdosecrets/objects/Collection.h b/src/fdosecrets/objects/Collection.h
new file mode 100644
index 0000000000..f11669b7de
--- /dev/null
+++ b/src/fdosecrets/objects/Collection.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETS_COLLECTION_H
+#define KEEPASSXC_FDOSECRETS_COLLECTION_H
+
+#include "DBusObject.h"
+
+#include "adaptors/CollectionAdaptor.h"
+
+#include
+#include
+
+class Database;
+class DatabaseWidget;
+class Entry;
+class Group;
+
+namespace FdoSecrets
+{
+ class Item;
+ class PromptBase;
+ class Service;
+ class Collection : public DBusObject
+ {
+ Q_OBJECT
+ public:
+ explicit Collection(Service* parent, DatabaseWidget* backend);
+
+ DBusReturn> items() const;
+
+ DBusReturn label() const;
+ DBusReturn setLabel(const QString& label);
+
+ DBusReturn locked() const;
+
+ DBusReturn created() const;
+
+ DBusReturn modified() const;
+
+ DBusReturn deleteCollection();
+ DBusReturn> searchItems(const StringStringMap& attributes);
+ DBusReturn
+ createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt);
+
+ signals:
+ void itemCreated(const Item* item);
+ void itemDeleted(const Item* item);
+ void itemChanged(const Item* item);
+
+ void collectionChanged();
+ void collectionAboutToDelete();
+ void collectionLockChanged(bool newLocked);
+
+ void aliasAboutToAdd(const QString& alias);
+ void aliasAdded(const QString& alias);
+ void aliasRemoved(const QString& alias);
+
+ public:
+ DBusReturn setProperties(const QVariantMap& properties);
+
+ DBusReturn removeAlias(QString alias);
+ DBusReturn addAlias(QString alias);
+ const QSet aliases() const;
+
+ /**
+ * A human readable name of the collection, available even if the db is locked
+ * @return
+ */
+ QString name() const;
+
+ Group* exposedRootGroup() const;
+ DatabaseWidget* backend() const;
+ QString backendFilePath() const;
+ Service* service() const;
+ bool inRecycleBin(Group* group) const;
+ bool inRecycleBin(Entry* entry) const;
+
+ public slots:
+ // expose some methods for Prmopt to use
+ void doLock();
+ void doUnlock();
+ // will remove self
+ void doDelete();
+
+ // delete the Entry in backend from this collection
+ void doDeleteEntries(QList entries);
+
+ private slots:
+ void onDatabaseLockChanged();
+ void onDatabaseExposedGroupChanged();
+ void reloadBackend();
+
+ private:
+ friend class DeleteCollectionPrompt;
+ friend class CreateCollectionPrompt;
+
+ void onEntryAdded(Entry* entry, bool emitSignal);
+ void populateContents();
+ void connectGroupSignalRecursive(Group* group);
+ void cleanupConnections();
+
+ bool backendLocked() const;
+
+ /**
+ * Check if the backend is a valid object, send error reply if not.
+ * @return true if the backend is valid.
+ */
+ DBusReturn ensureBackend() const;
+
+ /**
+ * Ensure the database is unlocked, send error reply if locked.
+ * @return true if the database is locked
+ */
+ DBusReturn ensureUnlocked() const;
+
+ /**
+ * Like mkdir -p, find or create the group by path, under m_exposedGroup
+ * @param groupPath
+ * @return created or found group
+ */
+ Group* findCreateGroupByPath(const QString& groupPath);
+
+ private:
+ QPointer m_backend;
+ QString m_backendPath;
+ QPointer m_exposedGroup;
+
+ QSet m_aliases;
+ QList m_items;
+ QMap m_entryToItem;
+
+ bool m_registered;
+ };
+
+} // namespace FdoSecrets
+
+#endif // KEEPASSXC_FDOSECRETS_COLLECTION_H
diff --git a/src/fdosecrets/objects/DBusObject.cpp b/src/fdosecrets/objects/DBusObject.cpp
new file mode 100644
index 0000000000..8bf1ae4e59
--- /dev/null
+++ b/src/fdosecrets/objects/DBusObject.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "DBusObject.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+namespace FdoSecrets
+{
+
+ DBusObject::DBusObject(DBusObject* parent)
+ : QObject(parent)
+ {
+ }
+
+ void DBusObject::registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor)
+ {
+ m_objectPath.setPath(path);
+ m_dbusAdaptor = adaptor;
+ adaptor->setParent(this);
+ auto ok = QDBusConnection::sessionBus().registerObject(m_objectPath.path(), this);
+ Q_UNUSED(ok);
+ Q_ASSERT(ok);
+ }
+
+ QString DBusObject::callingPeerName() const
+ {
+ auto pid = callingPeerPid();
+ QFile proc(QStringLiteral("/proc/%1/comm").arg(pid));
+ if (!proc.open(QFile::ReadOnly)) {
+ return callingPeer();
+ }
+ QTextStream stream(&proc);
+ return stream.readAll().trimmed();
+ }
+
+ QString encodePath(const QString& value)
+ {
+ // force "-.~_" to be encoded
+ return QUrl::toPercentEncoding(value, "", "-.~_").replace('%', '_');
+ }
+
+} // namespace FdoSecrets
diff --git a/src/fdosecrets/objects/DBusObject.h b/src/fdosecrets/objects/DBusObject.h
new file mode 100644
index 0000000000..539a2dfd7f
--- /dev/null
+++ b/src/fdosecrets/objects/DBusObject.h
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETS_DBUSOBJECT_H
+#define KEEPASSXC_FDOSECRETS_DBUSOBJECT_H
+
+#include "fdosecrets/objects/DBusReturn.h"
+#include "fdosecrets/objects/DBusTypes.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QDBusAbstractAdaptor;
+
+namespace FdoSecrets
+{
+ class Service;
+
+ /**
+ * @brief A common base class for all dbus-exposed objects
+ */
+ class DBusObject : public QObject, public QDBusContext
+ {
+ Q_OBJECT
+ public:
+ explicit DBusObject(DBusObject* parent = nullptr);
+
+ const QDBusObjectPath& objectPath() const
+ {
+ return m_objectPath;
+ }
+
+ protected:
+ void registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor);
+
+ void unregisterCurrentPath()
+ {
+ QDBusConnection::sessionBus().unregisterObject(m_objectPath.path());
+ m_dbusAdaptor = nullptr;
+ m_objectPath.setPath(QStringLiteral("/"));
+ }
+
+ QString callingPeer() const
+ {
+ Q_ASSERT(calledFromDBus());
+ return message().service();
+ }
+
+ uint callingPeerPid() const
+ {
+ return connection().interface()->servicePid(callingPeer());
+ }
+
+ QString callingPeerName() const;
+
+ template Adaptor& dbusAdaptor() const
+ {
+ return *static_cast(m_dbusAdaptor);
+ }
+
+ DBusObject* p() const
+ {
+ return qobject_cast(parent());
+ }
+
+ private:
+ /**
+ * Derived class should not directly use sendErrorReply.
+ * Instead, use raiseError
+ */
+ using QDBusContext::sendErrorReply;
+
+ template friend class DBusReturn;
+
+ private:
+ QDBusAbstractAdaptor* m_dbusAdaptor;
+ QDBusObjectPath m_objectPath;
+ };
+
+ /**
+ * Return the object path of the pointed DBusObject, or "/" if the pointer is null
+ * @tparam T
+ * @param object
+ * @return
+ */
+ template QDBusObjectPath objectPathSafe(T* object)
+ {
+ if (object) {
+ return object->objectPath();
+ }
+ return QDBusObjectPath(QStringLiteral("/"));
+ }
+
+ /**
+ * Convert a list of DBusObjects to object path
+ * @tparam T
+ * @param objects
+ * @return
+ */
+ template QList objectsToPath(QList objects)
+ {
+ QList res;
+ res.reserve(objects.size());
+ for (auto object : objects) {
+ res.append(objectPathSafe(object));
+ }
+ return res;
+ }
+
+ /**
+ * Convert an object path to a pointer of the object
+ * @tparam T
+ * @param path
+ * @return the pointer of the object, or nullptr if path is "/"
+ */
+ template T* pathToObject(const QDBusObjectPath& path)
+ {
+ if (path.path() == QStringLiteral("/")) {
+ return nullptr;
+ }
+ return qobject_cast(QDBusConnection::sessionBus().objectRegisteredAt(path.path()));
+ }
+
+ /**
+ * Convert a list of object paths to a list of objects.
+ * "/" paths (i.e. nullptrs) will be skipped in the resulting list
+ * @tparam T
+ * @param paths
+ * @return
+ */
+ template QList pathsToObject(const QList& paths)
+ {
+ QList res;
+ res.reserve(paths.size());
+ for (const auto& path : paths) {
+ auto object = pathToObject(path);
+ if (object) {
+ res.append(object);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Encode the string value to a DBus object path safe representation,
+ * using a schema similar to URI encoding, but with percentage(%) replaced with
+ * underscore(_). All characters except [A-Za-z0-9] are encoded. For non-ascii
+ * characters, UTF-8 encoding is first applied and each of the resulting byte
+ * value is encoded.
+ * @param value
+ * @return encoded string
+ */
+ QString encodePath(const QString& value);
+
+} // namespace FdoSecrets
+
+#endif // KEEPASSXC_FDOSECRETS_DBUSOBJECT_H
diff --git a/src/fdosecrets/objects/DBusReturn.cpp b/src/fdosecrets/objects/DBusReturn.cpp
new file mode 100644
index 0000000000..ffd10add91
--- /dev/null
+++ b/src/fdosecrets/objects/DBusReturn.cpp
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "DBusReturn.h"
diff --git a/src/fdosecrets/objects/DBusReturn.h b/src/fdosecrets/objects/DBusReturn.h
new file mode 100644
index 0000000000..6c94eab183
--- /dev/null
+++ b/src/fdosecrets/objects/DBusReturn.h
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETS_DBUSRETURN_H
+#define KEEPASSXC_FDOSECRETS_DBUSRETURN_H
+
+#include
+#include
+#include
+
+#include
+
+namespace FdoSecrets
+{
+
+ namespace details
+ {
+ class DBusReturnImpl
+ {
+ public:
+ /**
+ * Check if this object contains an error
+ * @return true if it contains an error, false otherwise.
+ */
+ bool isError() const
+ {
+ return !m_errorName.isEmpty();
+ }
+
+ /**
+ * Get the error name
+ * @return
+ */
+ QString errorName() const
+ {
+ return m_errorName;
+ }
+
+ void okOrDie() const
+ {
+ Q_ASSERT(!isError());
+ }
+
+ protected:
+ struct WithErrorTag
+ {
+ };
+
+ /**
+ * Construct from an error
+ * @param errorName
+ * @param value
+ */
+ DBusReturnImpl(QString errorName, WithErrorTag)
+ : m_errorName(std::move(errorName))
+ {
+ }
+
+ DBusReturnImpl() = default;
+
+ protected:
+ QString m_errorName;
+ };
+ } // namespace details
+
+ /**
+ * Either a return value or a DBus error
+ * @tparam T
+ */
+ template class DBusReturn : public details::DBusReturnImpl
+ {
+ protected:
+ using DBusReturnImpl::DBusReturnImpl;
+
+ public:
+ using value_type = T;
+
+ DBusReturn() = default;
+
+ /**
+ * Implicitly construct from a value
+ * @param value
+ */
+ DBusReturn(T&& value) // NOLINT(google-explicit-constructor)
+ : m_value(std::move(value))
+ {
+ }
+
+ DBusReturn(const T& value) // NOLINT(google-explicit-constructor)
+ : m_value(std::move(value))
+ {
+ }
+
+ /**
+ * Implicitly convert from another error of different value type.
+ *
+ * @tparam U must not be the same as T
+ * @param other
+ */
+ template ::value>::type>
+ DBusReturn(const DBusReturn& other) // NOLINT(google-explicit-constructor)
+ : DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{})
+ {
+ Q_ASSERT(other.isError());
+ }
+
+ /**
+ * Construct from error
+ * @param errorType
+ * @return a DBusReturn object containing the error
+ */
+ static DBusReturn Error(QDBusError::ErrorType errorType)
+ {
+ return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}};
+ }
+
+ /**
+ * Overloaded version
+ * @param errorName
+ * @return a DBusReturnImpl object containing the error
+ */
+ static DBusReturn Error(QString errorName)
+ {
+ return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}};
+ }
+
+ /**
+ * Get a reference to the enclosed value
+ * @return
+ */
+ const T& value() const&
+ {
+ okOrDie();
+ return m_value;
+ }
+
+ /**
+ * Get a rvalue reference to the enclosed value if this object is rvalue
+ * @return a rvalue reference to the enclosed value
+ */
+ T value() &&
+ {
+ okOrDie();
+ return std::move(m_value);
+ }
+
+ template T valueOrHandle(P* p) const&
+ {
+ if (isError()) {
+ if (p->calledFromDBus()) {
+ p->sendErrorReply(errorName());
+ }
+ return {};
+ }
+ return m_value;
+ }
+
+ template T&& valueOrHandle(P* p) &&
+ {
+ if (isError()) {
+ if (p->calledFromDBus()) {
+ p->sendErrorReply(errorName());
+ }
+ }
+ return std::move(m_value);
+ }
+
+ private:
+ T m_value{};
+ };
+
+ template <> class DBusReturn : public details::DBusReturnImpl
+ {
+ protected:
+ using DBusReturnImpl::DBusReturnImpl;
+
+ public:
+ using value_type = void;
+
+ DBusReturn() = default;
+
+ /**
+ * Implicitly convert from another error of different value type.
+ *
+ * @tparam U must not be the same as T
+ * @param other
+ */
+ template ::value>::type>
+ DBusReturn(const DBusReturn& other) // NOLINT(google-explicit-constructor)
+ : DBusReturn(other.errorName(), DBusReturnImpl::WithErrorTag{})
+ {
+ Q_ASSERT(other.isError());
+ }
+
+ /**
+ * Construct from error
+ * @param errorType
+ * @return a DBusReturn object containing the error
+ */
+ static DBusReturn Error(QDBusError::ErrorType errorType)
+ {
+ return DBusReturn{QDBusError::errorString(errorType), DBusReturnImpl::WithErrorTag{}};
+ }
+
+ /**
+ * Overloaded version
+ * @param errorName
+ * @return a DBusReturnImpl object containing the error
+ */
+ static DBusReturn Error(QString errorName)
+ {
+ return DBusReturn{std::move(errorName), DBusReturnImpl::WithErrorTag{}};
+ }
+
+ /**
+ * If this is return contains an error, handle it if we were called from DBus
+ * @tparam P
+ * @param p
+ */
+ template void handle(P* p) const
+ {
+ if (isError()) {
+ if (p->calledFromDBus()) {
+ p->sendErrorReply(errorName());
+ }
+ }
+ }
+ };
+
+} // namespace FdoSecrets
+
+#endif // KEEPASSXC_FDOSECRETS_DBUSRETURN_H
diff --git a/src/fdosecrets/objects/DBusTypes.cpp b/src/fdosecrets/objects/DBusTypes.cpp
new file mode 100644
index 0000000000..fff25124e6
--- /dev/null
+++ b/src/fdosecrets/objects/DBusTypes.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2019 Aetf
+ * Copyright 2010, Michael Leupold
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "DBusTypes.h"
+
+#include
+
+namespace FdoSecrets
+{
+
+ void registerDBusTypes()
+ {
+ // register meta-types needed for this adaptor
+ qRegisterMetaType();
+ qDBusRegisterMetaType();
+
+ qRegisterMetaType();
+ qDBusRegisterMetaType();
+
+ qRegisterMetaType();
+ qDBusRegisterMetaType();
+
+ // NOTE: this is already registered by Qt in qtextratypes.h
+ // qRegisterMetaType >();
+ // qDBusRegisterMetaType >();
+ }
+
+} // namespace FdoSecrets
diff --git a/src/fdosecrets/objects/DBusTypes.h b/src/fdosecrets/objects/DBusTypes.h
new file mode 100644
index 0000000000..384a1e6b20
--- /dev/null
+++ b/src/fdosecrets/objects/DBusTypes.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 Aetf
+ * Copyright 2010, Michael Leupold
+ * Copyright 2010-2011, Valentin Rusu
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETS_DBUSTYPES_H
+#define KEEPASSXC_FDOSECRETS_DBUSTYPES_H
+
+#include
+#include
+#include
+#include
+
+#define DBUS_SERVICE_SECRET "org.freedesktop.secrets"
+
+#define DBUS_INTERFACE_SECRET_SERVICE "org.freedesktop.Secret.Service"
+#define DBUS_INTERFACE_SECRET_SESSION "org.freedesktop.Secret.Session"
+#define DBUS_INTERFACE_SECRET_COLLECTION "org.freedesktop.Secret.Collection"
+#define DBUS_INTERFACE_SECRET_ITEM "org.freedesktop.Secret.Item"
+#define DBUS_INTERFACE_SECRET_PROMPT "org.freedesktop.Secret.Prompt"
+
+#define DBUS_ERROR_SECRET_NO_SESSION "org.freedesktop.Secret.Error.NoSession"
+#define DBUS_ERROR_SECRET_NO_SUCH_OBJECT "org.freedesktop.Secret.Error.NoSuchObject"
+#define DBUS_ERROR_SECRET_IS_LOCKED "org.freedesktop.Secret.Error.IsLocked"
+
+#define DBUS_PATH_SECRETS "/org/freedesktop/secrets"
+
+#define DBUS_PATH_TEMPLATE_ALIAS "%1/aliases/%2"
+#define DBUS_PATH_TEMPLATE_SESSION "%1/session/%2"
+#define DBUS_PATH_TEMPLATE_COLLECTION "%1/collection/%2"
+#define DBUS_PATH_TEMPLATE_ITEM "%1/%2"
+
+namespace FdoSecrets
+{
+ /**
+ * This is the basic Secret structure exchanged via the dbus API
+ * See the spec for more details
+ */
+ struct SecretStruct
+ {
+ QDBusObjectPath session{};
+ QByteArray parameters{};
+ QByteArray value{};
+ QString contentType{};
+ };
+
+ inline QDBusArgument& operator<<(QDBusArgument& argument, const SecretStruct& secret)
+ {
+ argument.beginStructure();
+ argument << secret.session << secret.parameters << secret.value << secret.contentType;
+ argument.endStructure();
+ return argument;
+ }
+
+ inline const QDBusArgument& operator>>(const QDBusArgument& argument, SecretStruct& secret)
+ {
+ argument.beginStructure();
+ argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType;
+ argument.endStructure();
+ return argument;
+ }
+
+ /**
+ * Register the types needed for the fd.o Secrets D-Bus interface.
+ */
+ void registerDBusTypes();
+
+} // namespace FdoSecrets
+
+typedef QMap StringStringMap;
+typedef QMap ObjectPathSecretMap;
+
+Q_DECLARE_METATYPE(FdoSecrets::SecretStruct)
+Q_DECLARE_METATYPE(StringStringMap);
+Q_DECLARE_METATYPE(ObjectPathSecretMap);
+
+#endif // KEEPASSXC_FDOSECRETS_DBUSTYPES_H
diff --git a/src/fdosecrets/objects/Item.cpp b/src/fdosecrets/objects/Item.cpp
new file mode 100644
index 0000000000..fa37564a26
--- /dev/null
+++ b/src/fdosecrets/objects/Item.cpp
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "Item.h"
+
+#include "fdosecrets/FdoSecretsPlugin.h"
+#include "fdosecrets/objects/Collection.h"
+#include "fdosecrets/objects/Prompt.h"
+#include "fdosecrets/objects/Service.h"
+#include "fdosecrets/objects/Session.h"
+
+#include "core/Entry.h"
+#include "core/EntryAttributes.h"
+#include "core/Group.h"
+#include "core/Tools.h"
+
+#include
+#include
+#include
+#include
+
+namespace FdoSecrets
+{
+
+ const QSet Item::ReadOnlyAttributes(QSet() << ItemAttributes::UuidKey << ItemAttributes::PathKey);
+
+ static void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType);
+ static SecretStruct getEntrySecret(Entry* entry);
+
+ namespace
+ {
+ constexpr auto FDO_SECRETS_DATA = "FDO_SECRETS_DATA";
+ constexpr auto FDO_SECRETS_CONTENT_TYPE = "FDO_SECRETS_CONTENT_TYPE";
+ } // namespace
+
+ Item::Item(Collection* parent, Entry* backend)
+ : DBusObject(parent)
+ , m_backend(backend)
+ {
+ Q_ASSERT(!p()->objectPath().path().isEmpty());
+
+ registerWithPath(QStringLiteral(DBUS_PATH_TEMPLATE_ITEM).arg(p()->objectPath().path(), m_backend->uuidToHex()),
+ new ItemAdaptor(this));
+
+ connect(m_backend.data(), &Entry::entryModified, this, &Item::itemChanged);
+ }
+
+ DBusReturn Item::locked() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ return collection()->locked();
+ }
+
+ DBusReturn Item::attributes() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ StringStringMap attrs;
+
+ // add default attributes except password
+ auto entryAttrs = m_backend->attributes();
+ for (const auto& attr : EntryAttributes::DefaultAttributes) {
+ if (entryAttrs->isProtected(attr) || attr == EntryAttributes::PasswordKey) {
+ continue;
+ }
+
+ auto value = entryAttrs->value(attr);
+ if (entryAttrs->isReference(attr)) {
+ value = m_backend->maskPasswordPlaceholders(value);
+ value = m_backend->resolveMultiplePlaceholders(value);
+ }
+ attrs[attr] = value;
+ }
+
+ // add custom attributes
+ const auto customKeys = entryAttrs->customKeys();
+ for (const auto& attr : customKeys) {
+ // decode attr key
+ auto decoded = decodeAttributeKey(attr);
+
+ attrs[decoded] = entryAttrs->value(attr);
+ }
+
+ // add some informative and readonly attributes
+ attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex();
+ attrs[ItemAttributes::PathKey] = path();
+ return attrs;
+ }
+
+ DBusReturn Item::setAttributes(const StringStringMap& attrs)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ m_backend->beginUpdate();
+
+ auto entryAttrs = m_backend->attributes();
+ for (auto it = attrs.constBegin(); it != attrs.constEnd(); ++it) {
+ if (entryAttrs->isProtected(it.key()) || it.key() == EntryAttributes::PasswordKey) {
+ continue;
+ }
+
+ if (ReadOnlyAttributes.contains(it.key())) {
+ continue;
+ }
+
+ auto encoded = encodeAttributeKey(it.key());
+ entryAttrs->set(encoded, it.value());
+ }
+
+ m_backend->endUpdate();
+
+ return {};
+ }
+
+ DBusReturn Item::label() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ return m_backend->title();
+ }
+
+ DBusReturn Item::setLabel(const QString& label)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ m_backend->beginUpdate();
+ m_backend->setTitle(label);
+ m_backend->endUpdate();
+
+ return {};
+ }
+
+ DBusReturn Item::created() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ return static_cast(m_backend->timeInfo().creationTime().toMSecsSinceEpoch() / 1000);
+ }
+
+ DBusReturn Item::modified() const
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ return static_cast(m_backend->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000);
+ }
+
+ DBusReturn Item::deleteItem()
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ auto prompt = new DeleteItemPrompt(service(), this);
+ return prompt;
+ }
+
+ DBusReturn Item::getSecret(Session* session)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ if (!session) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION));
+ }
+
+ auto secret = getEntrySecret(m_backend);
+
+ // encode using session
+ secret = session->encode(secret);
+
+ // show notification is this was directly called from DBus
+ if (calledFromDBus()) {
+ service()->plugin()->emitRequestShowNotification(
+ tr(R"(Entry "%1" from database "%2" was used by %3)")
+ .arg(m_backend->title(), collection()->name(), callingPeerName()));
+ }
+ return secret;
+ }
+
+ DBusReturn Item::setSecret(const SecretStruct& secret)
+ {
+ auto ret = ensureBackend();
+ if (ret.isError()) {
+ return ret;
+ }
+ ret = ensureUnlocked();
+ if (ret.isError()) {
+ return ret;
+ }
+
+ auto session = pathToObject(secret.session);
+ if (!session) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SESSION));
+ }
+
+ // decode using session
+ auto decoded = session->decode(secret);
+
+ // set in backend
+ m_backend->beginUpdate();
+ setEntrySecret(m_backend, decoded.value, decoded.contentType);
+ m_backend->endUpdate();
+
+ return {};
+ }
+
+ DBusReturn Item::setProperties(const QVariantMap& properties)
+ {
+ auto label = properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Label")).toString();
+
+ auto ret = setLabel(label);
+ if (ret.isError()) {
+ return ret;
+ }
+
+ auto attributes = qdbus_cast(
+ properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value());
+ ret = setAttributes(attributes);
+ if (ret.isError()) {
+ return ret;
+ }
+
+ return {};
+ }
+
+ Collection* Item::collection() const
+ {
+ return qobject_cast(p());
+ }
+
+ DBusReturn Item::ensureBackend() const
+ {
+ if (!m_backend) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
+ }
+ return {};
+ }
+
+ DBusReturn Item::ensureUnlocked() const
+ {
+ auto locked = collection()->locked();
+ if (locked.isError()) {
+ return locked;
+ }
+ if (locked.value()) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_IS_LOCKED));
+ }
+ return {};
+ }
+
+ Entry* Item::backend() const
+ {
+ return m_backend;
+ }
+
+ void Item::doDelete()
+ {
+ emit itemAboutToDelete();
+
+ // Unregister current path early, do not rely on deleteLater's call to destructor
+ // as in case of Entry moving between groups, new Item will be created at the same DBus path
+ // before the current Item is deleted in the event loop.
+ unregisterCurrentPath();
+
+ m_backend = nullptr;
+ deleteLater();
+ }
+
+ Service* Item::service() const
+ {
+ return collection()->service();
+ }
+
+ QString Item::path() const
+ {
+ QStringList pathComponents{m_backend->title()};
+
+ Group* group = m_backend->group();
+ while (group && group != collection()->exposedRootGroup()) {
+ pathComponents.prepend(group->name());
+ group = group->parentGroup();
+ }
+ // we should always reach the exposed root group
+ Q_ASSERT(group);
+
+ // root group is represented by a single slash, thus adding an empty component.
+ pathComponents.prepend(QLatin1Literal(""));
+
+ return pathComponents.join('/');
+ }
+
+ QString Item::encodeAttributeKey(const QString &key)
+ {
+ return QUrl::toPercentEncoding(key, "", "_:").replace('%', '_');
+ }
+
+ QString Item::decodeAttributeKey(const QString &key)
+ {
+ return QString::fromUtf8(QByteArray::fromPercentEncoding(key.toLatin1(), '_'));
+ }
+
+ void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType)
+ {
+ auto mimeName = contentType.split(';').takeFirst().trimmed();
+
+ // find the mime type
+ QMimeDatabase db;
+ auto mimeType = db.mimeTypeForName(mimeName);
+
+ // find a suitable codec
+ QTextCodec* codec = nullptr;
+ static const QRegularExpression charsetPattern(QStringLiteral(R"re(charset=(?.+)$)re"));
+ auto match = charsetPattern.match(contentType);
+ if (match.hasMatch()) {
+ codec = QTextCodec::codecForName(match.captured(QStringLiteral("encode")).toLatin1());
+ } else {
+ codec = QTextCodec::codecForName(QByteArrayLiteral("utf-8"));
+ }
+
+ if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) {
+ // we can't handle this content type, save the data as attachment
+ entry->attachments()->set(FDO_SECRETS_DATA, data);
+ entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType);
+ return;
+ }
+
+ // save the data to password field
+ if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
+ entry->attachments()->remove(FDO_SECRETS_DATA);
+ }
+ if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) {
+ entry->attributes()->remove(FDO_SECRETS_CONTENT_TYPE);
+ }
+
+ Q_ASSERT(codec);
+ entry->setPassword(codec->toUnicode(data));
+ }
+
+ SecretStruct getEntrySecret(Entry* entry)
+ {
+ SecretStruct ss;
+
+ if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
+ ss.value = entry->attachments()->value(FDO_SECRETS_DATA);
+ Q_ASSERT(entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE));
+ ss.contentType = entry->attributes()->value(FDO_SECRETS_CONTENT_TYPE);
+ return ss;
+ }
+
+ ss.value = entry->resolveMultiplePlaceholders(entry->password()).toUtf8();
+ ss.contentType = QStringLiteral("text/plain");
+ return ss;
+ }
+
+} // namespace FdoSecrets
diff --git a/src/fdosecrets/objects/Item.h b/src/fdosecrets/objects/Item.h
new file mode 100644
index 0000000000..cfec77fe58
--- /dev/null
+++ b/src/fdosecrets/objects/Item.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETS_ITEM_H
+#define KEEPASSXC_FDOSECRETS_ITEM_H
+
+#include "fdosecrets/objects/DBusObject.h"
+#include "fdosecrets/objects/adaptors/ItemAdaptor.h"
+
+#include
+
+class Entry;
+
+namespace FdoSecrets
+{
+
+ namespace ItemAttributes
+ {
+ constexpr const auto UuidKey = "Uuid";
+ constexpr const auto PathKey = "Path";
+ } // namespace ItemAttributes
+
+ class Session;
+ class Collection;
+ class PromptBase;
+
+ class Item : public DBusObject
+ {
+ Q_OBJECT
+ public:
+ explicit Item(Collection* parent, Entry* backend);
+
+ DBusReturn locked() const;
+
+ DBusReturn attributes() const;
+ DBusReturn setAttributes(const StringStringMap& attrs);
+
+ DBusReturn label() const;
+ DBusReturn setLabel(const QString& label);
+
+ DBusReturn created() const;
+
+ DBusReturn modified() const;
+
+ DBusReturn deleteItem();
+ DBusReturn getSecret(Session* session);
+ DBusReturn setSecret(const SecretStruct& secret);
+
+ signals:
+ void itemChanged();
+ void itemAboutToDelete();
+
+ public:
+ static const QSet ReadOnlyAttributes;
+
+ /**
+ * Due to the limitation in EntrySearcher, custom attr key cannot contain ':',
+ * Thus we encode the key when saving and decode it when returning.
+ * @param key
+ * @return
+ */
+ static QString encodeAttributeKey(const QString& key);
+ static QString decodeAttributeKey(const QString& key);
+
+ DBusReturn setProperties(const QVariantMap& properties);
+
+ Entry* backend() const;
+ Collection* collection() const;
+ Service* service() const;
+
+ /**
+ * Compute the entry path relative to the exposed group
+ * @return the entry path
+ */
+ QString path() const;
+
+ public slots:
+ void doDelete();
+
+ private:
+ /**
+ * Check if the backend is a valid object, send error reply if not.
+ * @return No error if the backend is valid.
+ */
+ DBusReturn ensureBackend() const;
+
+ /**
+ * Ensure the database is unlocked, send error reply if locked.
+ * @return true if the database is locked
+ */
+ DBusReturn ensureUnlocked() const;
+
+ private:
+ QPointer m_backend;
+ };
+
+} // namespace FdoSecrets
+
+#endif // KEEPASSXC_FDOSECRETS_ITEM_H
diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp
new file mode 100644
index 0000000000..14bc66b687
--- /dev/null
+++ b/src/fdosecrets/objects/Prompt.cpp
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "Prompt.h"
+
+#include "fdosecrets/FdoSecretsSettings.h"
+#include "fdosecrets/objects/Collection.h"
+#include "fdosecrets/objects/Item.h"
+#include "fdosecrets/objects/Service.h"
+
+#include "core/Tools.h"
+#include "gui/DatabaseWidget.h"
+#include "gui/MessageBox.h"
+
+#include
+#include
+
+namespace FdoSecrets
+{
+
+ PromptBase::PromptBase(Service* parent)
+ : DBusObject(parent)
+ {
+ registerWithPath(
+ QStringLiteral("%1/prompt/%2").arg(p()->objectPath().path(), Tools::uuidToHex(QUuid::createUuid())),
+ new PromptAdaptor(this));
+
+ connect(this, &PromptBase::completed, this, &PromptBase::deleteLater);
+ }
+
+ QWindow* PromptBase::findWindow(const QString& windowId)
+ {
+ // find parent window, or nullptr if not found
+ bool ok = false;
+ WId wid = windowId.toULongLong(&ok, 0);
+ QWindow* parent = nullptr;
+ if (ok) {
+ parent = QWindow::fromWinId(wid);
+ }
+ if (parent) {
+ // parent is not the child of any object, so make sure it gets deleted at some point
+ QObject::connect(this, &QObject::destroyed, parent, &QObject::deleteLater);
+ }
+ return parent;
+ }
+
+ Service* PromptBase::service() const
+ {
+ return qobject_cast(parent());
+ }
+
+ DBusReturn PromptBase::dismiss()
+ {
+ emit completed(true, {});
+ return {};
+ }
+
+ DeleteCollectionPrompt::DeleteCollectionPrompt(Service* parent, Collection* coll)
+ : PromptBase(parent)
+ , m_collection(coll)
+ {
+ }
+
+ DBusReturn DeleteCollectionPrompt::prompt(const QString& windowId)
+ {
+ if (thread() != QThread::currentThread()) {
+ DBusReturn ret;
+ QMetaObject::invokeMethod(this,
+ "prompt",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(QString, windowId),
+ Q_RETURN_ARG(DBusReturn, ret));
+ return ret;
+ }
+
+ if (!m_collection) {
+ return DBusReturn<>::Error(QStringLiteral(DBUS_ERROR_SECRET_NO_SUCH_OBJECT));
+ }
+
+ MessageBox::OverrideParent override(findWindow(windowId));
+ // only need to delete in backend, collection will react itself.
+ service()->doCloseDatabase(m_collection->backend());
+
+ emit completed(false, {});
+
+ return {};
+ }
+
+ CreateCollectionPrompt::CreateCollectionPrompt(Service* parent)
+ : PromptBase(parent)
+ {
+ }
+
+ DBusReturn CreateCollectionPrompt::prompt(const QString& windowId)
+ {
+ if (thread() != QThread::currentThread()) {
+ DBusReturn ret;
+ QMetaObject::invokeMethod(this,
+ "prompt",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(QString, windowId),
+ Q_RETURN_ARG(DBusReturn, ret));
+ return ret;
+ }
+
+ MessageBox::OverrideParent override(findWindow(windowId));
+
+ auto coll = service()->doNewDatabase();
+ if (!coll) {
+ return dismiss();
+ }
+
+ emit collectionCreated(coll);
+ emit completed(false, coll->objectPath().path());
+
+ return {};
+ }
+
+ LockCollectionsPrompt::LockCollectionsPrompt(Service* parent, const QList& colls)
+ : PromptBase(parent)
+ {
+ m_collections.reserve(colls.size());
+ for (const auto& c : asConst(colls)) {
+ m_collections << c;
+ }
+ }
+
+ DBusReturn LockCollectionsPrompt::prompt(const QString& windowId)
+ {
+ if (thread() != QThread::currentThread()) {
+ DBusReturn ret;
+ QMetaObject::invokeMethod(this,
+ "prompt",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(QString, windowId),
+ Q_RETURN_ARG(DBusReturn, ret));
+ return ret;
+ }
+
+ MessageBox::OverrideParent override(findWindow(windowId));
+
+ QList locked;
+ for (const auto& c : asConst(m_collections)) {
+ if (c) {
+ c->doLock();
+ locked << c->objectPath();
+ }
+ }
+
+ emit completed(false, QVariant::fromValue(locked));
+
+ return {};
+ }
+
+ UnlockCollectionsPrompt::UnlockCollectionsPrompt(Service* parent, const QList& colls)
+ : PromptBase(parent)
+ {
+ m_collections.reserve(colls.size());
+ for (const auto& c : asConst(colls)) {
+ m_collections << c;
+ }
+ }
+
+ DBusReturn UnlockCollectionsPrompt::prompt(const QString& windowId)
+ {
+ if (thread() != QThread::currentThread()) {
+ DBusReturn ret;
+ QMetaObject::invokeMethod(this,
+ "prompt",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(QString, windowId),
+ Q_RETURN_ARG(DBusReturn, ret));
+ return ret;
+ }
+
+ MessageBox::OverrideParent override(findWindow(windowId));
+
+ QList unlocked;
+ for (const auto& c : asConst(m_collections)) {
+ if (c) {
+ c->doUnlock();
+ unlocked << c->objectPath();
+ }
+ }
+
+ emit completed(false, QVariant::fromValue(unlocked));
+
+ return {};
+ }
+
+ DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item)
+ : PromptBase(parent)
+ , m_item(item)
+ {
+ }
+
+ DBusReturn DeleteItemPrompt::prompt(const QString& windowId)
+ {
+ if (thread() != QThread::currentThread()) {
+ DBusReturn ret;
+ QMetaObject::invokeMethod(this,
+ "prompt",
+ Qt::BlockingQueuedConnection,
+ Q_ARG(QString, windowId),
+ Q_RETURN_ARG(DBusReturn, ret));
+ return ret;
+ }
+
+ MessageBox::OverrideParent override(findWindow(windowId));
+
+ // delete item's backend. Item will be notified after the backend is deleted.
+ if (m_item) {
+ if (FdoSecrets::settings()->noConfirmDeleteItem()) {
+ MessageBox::setNextAnswer(MessageBox::Move);
+ }
+ m_item->collection()->doDeleteEntries({m_item->backend()});
+ }
+
+ emit completed(false, {});
+
+ return {};
+ }
+
+} // namespace FdoSecrets
diff --git a/src/fdosecrets/objects/Prompt.h b/src/fdosecrets/objects/Prompt.h
new file mode 100644
index 0000000000..2d13084f6e
--- /dev/null
+++ b/src/fdosecrets/objects/Prompt.h
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_FDOSECRETS_PROMPT_H
+#define KEEPASSXC_FDOSECRETS_PROMPT_H
+
+#include "fdosecrets/objects/DBusObject.h"
+#include "fdosecrets/objects/adaptors/PromptAdaptor.h"
+
+#include
+
+class QWindow;
+
+class DatabaseWidget;
+
+namespace FdoSecrets
+{
+
+ class Service;
+
+ class PromptBase : public DBusObject
+ {
+ Q_OBJECT
+ public:
+ explicit PromptBase(Service* parent);
+
+ virtual DBusReturn prompt(const QString& windowId) = 0;
+
+ virtual DBusReturn dismiss();
+
+ signals:
+ void completed(bool dismissed, const QVariant& result);
+
+ protected:
+ QWindow* findWindow(const QString& windowId);
+ Service* service() const;
+ };
+
+ class Collection;
+
+ class DeleteCollectionPrompt : public PromptBase
+ {
+ Q_OBJECT
+
+ public:
+ explicit DeleteCollectionPrompt(Service* parent, Collection* coll);
+
+ DBusReturn prompt(const QString& windowId) override;
+
+ private:
+ QPointer m_collection;
+ };
+
+ class CreateCollectionPrompt : public PromptBase
+ {
+ Q_OBJECT
+
+ public:
+ explicit CreateCollectionPrompt(Service* parent);
+
+ DBusReturn prompt(const QString& windowId) override;
+
+ signals:
+ void collectionCreated(Collection* coll);
+ };
+
+ class LockCollectionsPrompt : public PromptBase
+ {
+ Q_OBJECT
+ public:
+ explicit LockCollectionsPrompt(Service* parent, const QList& colls);
+
+ DBusReturn prompt(const QString& windowId) override;
+
+ private:
+ QList> m_collections;
+ };
+
+ class UnlockCollectionsPrompt : public PromptBase
+ {
+ Q_OBJECT
+ public:
+ explicit UnlockCollectionsPrompt(Service* parent, const QList& coll);
+
+ DBusReturn prompt(const QString& windowId) override;
+
+ private:
+ QList> m_collections;
+ };
+
+ class Item;
+ class DeleteItemPrompt : public PromptBase
+ {
+ Q_OBJECT
+
+ public:
+ explicit DeleteItemPrompt(Service* parent, Item* item);
+
+ DBusReturn prompt(const QString& windowId) override;
+
+ private:
+ QPointer m_item;
+ };
+
+} // namespace FdoSecrets
+
+#endif // KEEPASSXC_FDOSECRETS_PROMPT_H
diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp
new file mode 100644
index 0000000000..c15d4052c9
--- /dev/null
+++ b/src/fdosecrets/objects/Service.cpp
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2018 Aetf
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "Service.h"
+
+#include "fdosecrets/FdoSecretsPlugin.h"
+#include "fdosecrets/FdoSecretsSettings.h"
+#include "fdosecrets/objects/Collection.h"
+#include "fdosecrets/objects/Item.h"
+#include "fdosecrets/objects/Prompt.h"
+#include "fdosecrets/objects/Session.h"
+
+#include "gui/DatabaseTabWidget.h"
+#include "gui/DatabaseWidget.h"
+
+#include
+#include
+#include
+
+namespace
+{
+ constexpr auto DEFAULT_ALIAS = "default";
+}
+
+namespace FdoSecrets
+{
+
+ Service::Service(FdoSecretsPlugin* plugin,
+ QPointer dbTabs) // clazy: exclude=ctor-missing-parent-argument
+ : DBusObject(nullptr)
+ , m_plugin(plugin)
+ , m_databases(std::move(dbTabs))
+ , m_insdieEnsureDefaultAlias(false)
+ , m_serviceWatcher(nullptr)
+ {
+ registerWithPath(QStringLiteral(DBUS_PATH_SECRETS), new ServiceAdaptor(this));
+ }
+
+ Service::~Service()
+ {
+ QDBusConnection::sessionBus().unregisterService(QStringLiteral(DBUS_SERVICE_SECRET));
+ }
+
+ bool Service::initialize()
+ {
+ if (!QDBusConnection::sessionBus().registerService(QStringLiteral(DBUS_SERVICE_SECRET))) {
+ qDebug() << "Another secret service is running";
+ emit error(tr("Failed to register DBus service at %1: another secret service is running.")
+ .arg(QLatin1Literal(DBUS_SERVICE_SECRET)));
+ return false;
+ }
+
+ // Connect to service unregistered signal
+ m_serviceWatcher.reset(new QDBusServiceWatcher());
+ connect(
+ m_serviceWatcher.data(), &QDBusServiceWatcher::serviceUnregistered, this, &Service::dbusServiceUnregistered);
+
+ m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
+
+ // Add existing database tabs
+ for (int idx = 0; idx != m_databases->count(); ++idx) {
+ auto dbWidget = m_databases->databaseWidgetFromIndex(idx);
+ onDatabaseTabOpened(dbWidget, false);
+ }
+
+ // Connect to new database signal
+ // No need to connect to close signal, as collection will remove itself when backend delete/close database tab.
+ connect(m_databases.data(), &DatabaseTabWidget::databaseOpened, this, [this](DatabaseWidget* dbWidget) {
+ onDatabaseTabOpened(dbWidget, true);
+ });
+
+ // make default alias track current activated database
+ connect(m_databases.data(), &DatabaseTabWidget::activateDatabaseChanged, this, &Service::ensureDefaultAlias);
+
+ return true;
+ }
+
+ void Service::onDatabaseTabOpened(DatabaseWidget* dbWidget, bool emitSignal)
+ {
+ auto coll = new Collection(this, dbWidget);
+
+ m_collections << coll;
+ m_dbToCollection[dbWidget] = coll;
+
+ // handle alias
+ connect(coll, &Collection::aliasAboutToAdd, this, &Service::onCollectionAliasAboutToAdd);
+ connect(coll, &Collection::aliasAdded, this, &Service::onCollectionAliasAdded);
+ connect(coll, &Collection::aliasRemoved, this, &Service::onCollectionAliasRemoved);
+
+ ensureDefaultAlias();
+
+ // Forward delete signal, we have to rely on filepath to identify the database being closed,
+ // but we can not access m_backend safely because during the databaseClosed signal,
+ // m_backend may already be reset to nullptr
+ // We want to remove the collection object from dbus as early as possible, to avoid
+ // race conditions when deleteLater was called on the m_backend, but not delivered yet,
+ // and new method calls from dbus occurred. Therefore we can't rely on the destroyed
+ // signal on m_backend.
+ // bind to coll lifespan
+ connect(m_databases.data(), &DatabaseTabWidget::databaseClosed, coll, [coll](const QString& filePath) {
+ if (filePath == coll->backendFilePath()) {
+ coll->doDelete();
+ }
+ });
+
+ // relay signals
+ connect(coll, &Collection::collectionChanged, this, [this, coll]() { emit collectionChanged(coll); });
+ connect(coll, &Collection::collectionAboutToDelete, this, [this, coll]() {
+ m_collections.removeAll(coll);
+ m_dbToCollection.remove(coll->backend());
+ emit collectionDeleted(coll);
+ });
+
+ // a special case: the database changed from no expose to expose something.
+ // in this case, there is no collection out there monitoring it, so create a new collection
+ if (!dbWidget->isLocked()) {
+ monitorDatabaseExposedGroup(dbWidget);
+ }
+ connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [this, dbWidget]() {
+ monitorDatabaseExposedGroup(dbWidget);
+ });
+
+ if (emitSignal) {
+ emit collectionCreated(coll);
+ }
+ }
+
+ void Service::monitorDatabaseExposedGroup(DatabaseWidget* dbWidget)
+ {
+ Q_ASSERT(dbWidget);
+ connect(
+ dbWidget->database()->metadata()->customData(), &CustomData::customDataModified, this, [this, dbWidget]() {
+ if (!FdoSecrets::settings()->exposedGroup(dbWidget->database()).isNull() && !findCollection(dbWidget)) {
+ onDatabaseTabOpened(dbWidget, true);
+ }
+ });
+ }
+
+ void Service::ensureDefaultAlias()
+ {
+ if (m_insdieEnsureDefaultAlias) {
+ return;
+ }
+
+ m_insdieEnsureDefaultAlias = true;
+
+ auto coll = findCollection(m_databases->currentDatabaseWidget());
+ if (coll) {
+ // adding alias will automatically remove the association with previous collection.
+ coll->addAlias(DEFAULT_ALIAS).okOrDie();
+ }
+
+ m_insdieEnsureDefaultAlias = false;
+ }
+
+ void Service::dbusServiceUnregistered(const QString& service)
+ {
+ Q_ASSERT(m_serviceWatcher);
+
+ auto removed = m_serviceWatcher->removeWatchedService(service);
+ Q_UNUSED(removed);
+ Q_ASSERT(removed);
+
+ Session::CleanupNegotiation(service);
+ auto sess = m_peerToSession.value(service, nullptr);
+ if (sess) {
+ sess->close().okOrDie();
+ }
+ }
+
+ DBusReturn> Service::collections() const
+ {
+ return m_collections;
+ }
+
+ DBusReturn Service::openSession(const QString& algorithm, const QVariant& input, Session*& result)
+ {
+ QVariant output;
+ bool incomplete = false;
+ auto peer = callingPeer();
+
+ // watch for service unregister to cleanup
+ Q_ASSERT(m_serviceWatcher);
+ m_serviceWatcher->addWatchedService(peer);
+
+ // negotiate cipher
+ auto ciphers = Session::CreateCiphers(peer, algorithm, input, output, incomplete);
+ if (incomplete) {
+ result = nullptr;
+ return output;
+ }
+ if (!ciphers) {
+ return DBusReturn<>::Error(QDBusError::NotSupported);
+ }
+ result = new Session(std::move(ciphers), callingPeerName(), this);
+
+ m_sessions.append(result);
+ m_peerToSession[peer] = result;
+ connect(result, &Session::aboutToClose, this, [this, peer, result]() {
+ emit sessionClosed(result);
+ m_sessions.removeAll(result);
+ m_peerToSession.remove(peer);
+ });
+ emit sessionOpened(result);
+
+ return output;
+ }
+
+ DBusReturn
+ Service::createCollection(const QVariantMap& properties, const QString& alias, PromptBase*& prompt)
+ {
+ prompt = nullptr;
+
+ // return existing collection if alias is non-empty and exists.
+ auto collection = findCollection(alias);
+ if (!collection) {
+ auto cp = new CreateCollectionPrompt(this);
+ prompt = cp;
+
+ // collection will be created when the prompt complets.
+ // once it's done, we set additional properties on the collection
+ connect(cp, &CreateCollectionPrompt::collectionCreated, cp, [alias, properties](Collection* coll) {
+ coll->setProperties(properties).okOrDie();
+ if (!alias.isEmpty()) {
+ coll->addAlias(alias).okOrDie();
+ }
+ });
+ }
+ return collection;
+ }
+
+ DBusReturn> Service::searchItems(const StringStringMap& attributes, QList