Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'tests/gui/TestGuiFdoSecrets.cpp')
-rw-r--r--tests/gui/TestGuiFdoSecrets.cpp1176
1 files changed, 1176 insertions, 0 deletions
diff --git a/tests/gui/TestGuiFdoSecrets.cpp b/tests/gui/TestGuiFdoSecrets.cpp
new file mode 100644
index 000000000..9dffa6ba5
--- /dev/null
+++ b/tests/gui/TestGuiFdoSecrets.cpp
@@ -0,0 +1,1176 @@
+/*
+ * Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "TestGuiFdoSecrets.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/Service.h"
+#include "fdosecrets/objects/Session.h"
+#include "fdosecrets/objects/SessionCipher.h"
+
+#include "TestGlobal.h"
+#include "config-keepassx-tests.h"
+
+#include "core/Bootstrap.h"
+#include "core/Config.h"
+#include "core/Tools.h"
+#include "crypto/Crypto.h"
+#include "gui/DatabaseTabWidget.h"
+#include "gui/DatabaseWidget.h"
+#include "gui/FileDialog.h"
+#include "gui/MessageBox.h"
+#include "gui/wizard/NewDatabaseWizard.h"
+#include "util/TemporaryFile.h"
+
+#include <QAbstractItemView>
+#include <QDBusConnection>
+#include <QDBusConnectionInterface>
+#include <QDBusInterface>
+#include <QLineEdit>
+#include <QPointer>
+#include <QSignalSpy>
+#include <QXmlStreamReader>
+
+#include <memory>
+#include <type_traits>
+
+int main(int argc, char* argv[])
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
+ QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+#endif
+ Application app(argc, argv);
+ app.setApplicationName("KeePassXC");
+ app.setApplicationVersion(KEEPASSXC_VERSION);
+ app.setQuitOnLastWindowClosed(false);
+ app.setAttribute(Qt::AA_Use96Dpi, true);
+ app.applyTheme();
+ QTEST_DISABLE_KEYPAD_NAVIGATION
+ TestGuiFdoSecrets tc;
+ QTEST_SET_MAIN_SOURCE_PATH
+ return QTest::qExec(&tc, argc, argv);
+}
+
+#define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default"
+
+#define VERIFY(statement) \
+ do { \
+ if (!QTest::qVerify(static_cast<bool>(statement), #statement, "", __FILE__, __LINE__)) \
+ return {}; \
+ } while (false)
+
+#define COMPARE(actual, expected) \
+ do { \
+ if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
+ return {}; \
+ } while (false)
+
+#define FAIL(message) \
+ do { \
+ QTest::qFail(static_cast<const char*>(message), __FILE__, __LINE__); \
+ return {}; \
+ } while (false)
+
+#define COMPARE_DBUS_LOCAL_CALL(actual, expected) \
+ do { \
+ const auto a = (actual); \
+ QVERIFY(!a.isError()); \
+ QCOMPARE(a.value(), (expected)); \
+ } while (false)
+
+#define CHECKED_DBUS_LOCAL_CALL(name, stmt) \
+ std::remove_cv<decltype(stmt)::value_type>::type name; \
+ do { \
+ const auto rep = stmt; \
+ QVERIFY(!rep.isError()); \
+ name = rep.value(); \
+ } while (false)
+
+namespace
+{
+ std::unique_ptr<QDBusInterface> interfaceOf(const QDBusObjectPath& objPath, const QString& interface)
+ {
+ std::unique_ptr<QDBusInterface> iface(new QDBusInterface(DBUS_SERVICE_SECRET, objPath.path(), interface));
+ iface->setTimeout(5);
+ VERIFY(iface->isValid());
+ return iface;
+ }
+
+ std::unique_ptr<QDBusInterface> interfaceOf(FdoSecrets::DBusObject* obj)
+ {
+ VERIFY(obj);
+ auto metaAdaptor = obj->dbusAdaptor().metaObject();
+ auto ifaceName = metaAdaptor->classInfo(metaAdaptor->indexOfClassInfo("D-Bus Interface")).value();
+
+ return interfaceOf(obj->objectPath(), ifaceName);
+ }
+
+ template <typename T> QString extractElement(const QString& doc, T cond)
+ {
+ QXmlStreamReader reader(doc);
+ while (!reader.atEnd()) {
+ int st = reader.characterOffset();
+
+ if (reader.readNext() != QXmlStreamReader::StartElement || !cond(reader)) {
+ continue;
+ }
+
+ reader.skipCurrentElement();
+ if (reader.hasError()) {
+ break;
+ }
+
+ // remove whitespaces between elements to be a little bit flexible
+ int ed = reader.characterOffset();
+ return doc.mid(st - 1, ed - st + 1).replace(QRegularExpression(R"(>[\s\n]+<)"), "><");
+ }
+ VERIFY(!reader.hasError());
+ return {};
+ }
+
+ bool checkDBusSpec(const QString& path, const QString& interface)
+ {
+ QFile f(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/dbus/interfaces/%1.xml").arg(interface));
+ VERIFY(f.open(QFile::ReadOnly | QFile::Text));
+ QTextStream in(&f);
+ auto spec = in.readAll().replace(QRegularExpression(R"(>[\s\n]+<)"), "><").trimmed();
+
+ auto bus = QDBusConnection::sessionBus();
+ auto msg = QDBusMessage::createMethodCall(
+ DBUS_SERVICE_SECRET, path, "org.freedesktop.DBus.Introspectable", "Introspect");
+
+ // BlockWithGui enters event loop
+ auto reply = QDBusPendingReply<QString>(bus.call(msg, QDBus::BlockWithGui, 5));
+ VERIFY(reply.isValid());
+ auto actual = extractElement(reply.argumentAt<0>(), [&](const QXmlStreamReader& reader) {
+ return reader.name() == "interface" && reader.attributes().value("name") == interface;
+ });
+
+ COMPARE(actual, spec);
+ return true;
+ }
+} // namespace
+
+using namespace FdoSecrets;
+
+// for use in QSignalSpy
+Q_DECLARE_METATYPE(Collection*);
+Q_DECLARE_METATYPE(Item*);
+
+TestGuiFdoSecrets::~TestGuiFdoSecrets() = default;
+
+void TestGuiFdoSecrets::initTestCase()
+{
+ // for use in QSignalSpy
+ qRegisterMetaType<Collection*>();
+ qRegisterMetaType<Item*>();
+
+ QVERIFY(Crypto::init());
+ Config::createTempFileInstance();
+ config()->set(Config::AutoSaveAfterEveryChange, false);
+ config()->set(Config::AutoSaveOnExit, false);
+ config()->set(Config::GUI_ShowTrayIcon, true);
+ config()->set(Config::UpdateCheckMessageShown, true);
+ // Disable secret service integration (activate within individual tests to test the plugin)
+ FdoSecrets::settings()->setEnabled(false);
+ // activate within individual tests
+ FdoSecrets::settings()->setShowNotification(false);
+
+ Bootstrap::bootstrapApplication();
+
+ m_mainWindow.reset(new MainWindow());
+ m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
+ QVERIFY(m_tabWidget);
+ m_plugin = FdoSecretsPlugin::getPlugin();
+ QVERIFY(m_plugin);
+ m_mainWindow->show();
+
+ // Load the NewDatabase.kdbx file into temporary storage
+ QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx"));
+ QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
+ QVERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
+ sourceDbFile.close();
+
+ // set keys for session encryption
+ m_serverPublic = MpiFromHex("e407997e8b918419cf851cf3345358fdf"
+ "ffb9564a220ac9c3934efd277cea20d17"
+ "467ecdc56e817f75ac39501f38a4a04ff"
+ "64d627e16c09981c7ad876da255b61c8e"
+ "6a8408236c2a4523cfe6961c26dbdfc77"
+ "c1a27a5b425ca71a019e829fae32c0b42"
+ "0e1b3096b48bc2ce9ccab1d1ff13a5eb4"
+ "b263cee30bdb1a57af9bfa93f");
+ m_serverPrivate = MpiFromHex("013f4f3381ef0ca11c4c7363079577b56"
+ "99b238644e0aba47e24bdba6173590216"
+ "4f1e12dd0944800a373e090e63192f53b"
+ "93583e9a9e50bb9d792aafaa3a0f5ae77"
+ "de0c3423f5820848d88ee3bdd01c889f2"
+ "7af58a02f5b6693d422b9d189b300d7b1"
+ "be5076b5795cf8808c31e2e2898368d18"
+ "ab5c26b0ea3480c9aba8154cf");
+ // use the same cipher to do the client side encryption, but exchange the position of client/server keys
+ m_cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7);
+ QVERIFY(m_cipher->initialize(MpiFromBytes(MpiToBytes(m_serverPublic)),
+ MpiFromHex("30d18c6b328bac970c05bda6af2e708b9"
+ "d6bbbb6dc136c1a2d96e870fabc86ad74"
+ "1846a26a4197f32f65ea2e7580ad2afe3"
+ "dd5d6c1224b8368b0df2cd75d520a9ff9"
+ "7fe894cc7da71b7bd285b4633359c16c8"
+ "d341f822fa4f0fdf59b5d3448658c46a2"
+ "a86dbb14ff85823873f8a259ccc52bbb8"
+ "2b5a4c2a75447982553b42221"),
+ MpiFromHex("84aafe9c9f356f7762307f4d791acb59e"
+ "8e3fd562abdbb481d0587f8400ad6c51d"
+ "af561a1beb9a22c8cd4d2807367c5787b"
+ "2e06d631ccbb5194b6bb32211583ce688"
+ "f9c2cebc22a9e4d494d12ebdd570c61a1"
+ "62a94e88561d25ccd0415339d1f59e1b0"
+ "6bc6b6b5fde46e23b2410eb034be390d3"
+ "2407ec7ae90f0831f24afd5ac")));
+}
+
+// Every test starts with opening the temp database
+void TestGuiFdoSecrets::init()
+{
+ m_dbFile.reset(new TemporaryFile());
+ // Write the temp storage to a temp database file for use in our tests
+ QVERIFY(m_dbFile->open());
+ QCOMPARE(m_dbFile->write(m_dbData), static_cast<qint64>((m_dbData.size())));
+ m_dbFile->close();
+
+ // make sure window is activated or focus tests may fail
+ m_mainWindow->activateWindow();
+ QApplication::processEvents();
+
+ // open and unlock the database
+ m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
+ m_dbWidget = m_tabWidget->currentDatabaseWidget();
+ m_db = m_dbWidget->database();
+
+ // by default expsoe the root group
+ FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
+ QVERIFY(m_dbWidget->save());
+}
+
+// Every test ends with closing the temp database without saving
+void TestGuiFdoSecrets::cleanup()
+{
+ // restore to default settings
+ FdoSecrets::settings()->setShowNotification(false);
+ FdoSecrets::settings()->setEnabled(false);
+ if (m_plugin) {
+ m_plugin->updateServiceState();
+ }
+
+ // DO NOT save the database
+ for (int i = 0; i != m_tabWidget->count(); ++i) {
+ m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean();
+ }
+ QVERIFY(m_tabWidget->closeAllDatabaseTabs());
+ QApplication::processEvents();
+
+ if (m_dbFile) {
+ m_dbFile->remove();
+ }
+}
+
+void TestGuiFdoSecrets::cleanupTestCase()
+{
+ if (m_dbFile) {
+ m_dbFile->remove();
+ }
+}
+
+void TestGuiFdoSecrets::testDBusSpec()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ // service
+ QCOMPARE(service->objectPath().path(), QStringLiteral(DBUS_PATH_SECRETS));
+ QVERIFY(checkDBusSpec(service->objectPath().path(), DBUS_INTERFACE_SECRET_SERVICE));
+
+ // default alias
+ QVERIFY(checkDBusSpec(DBUS_PATH_DEFAULT_ALIAS, DBUS_INTERFACE_SECRET_COLLECTION));
+
+ // collection
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ QVERIFY(checkDBusSpec(coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION));
+
+ // item
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+ QVERIFY(checkDBusSpec(item->objectPath().path(), DBUS_INTERFACE_SECRET_ITEM));
+
+ // session
+ auto sess = openSession(service, PlainCipher::Algorithm);
+ QVERIFY(sess);
+ QVERIFY(checkDBusSpec(sess->objectPath().path(), DBUS_INTERFACE_SECRET_SESSION));
+
+ // prompt
+ FdoSecrets::settings()->setNoConfirmDeleteItem(true);
+ PromptBase* prompt = nullptr;
+ {
+ auto rep = item->deleteItem();
+ QVERIFY(!rep.isError());
+ prompt = rep.value();
+ }
+ QVERIFY(prompt);
+ QVERIFY(checkDBusSpec(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT));
+}
+
+void TestGuiFdoSecrets::testServiceEnable()
+{
+ QSignalSpy sigError(m_plugin, SIGNAL(error(QString)));
+ QVERIFY(sigError.isValid());
+
+ QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted()));
+ QVERIFY(sigStarted.isValid());
+
+ // make sure no one else is holding the service
+ QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
+
+ // enable the service
+ auto service = enableService();
+ QVERIFY(service);
+
+ // service started without error
+ QVERIFY(sigError.isEmpty());
+ QCOMPARE(sigStarted.size(), 1);
+
+ QApplication::processEvents();
+
+ QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
+
+ // there will be one default collection
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ COMPARE_DBUS_LOCAL_CALL(coll->locked(), false);
+ COMPARE_DBUS_LOCAL_CALL(coll->label(), m_db->metadata()->name());
+ COMPARE_DBUS_LOCAL_CALL(
+ coll->created(),
+ static_cast<qulonglong>(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000));
+ COMPARE_DBUS_LOCAL_CALL(
+ coll->modified(),
+ static_cast<qulonglong>(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000));
+}
+
+void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase()
+{
+ // reset the exposed group and then enable the service
+ FdoSecrets::settings()->setExposedGroup(m_db, {});
+ auto service = enableService();
+ QVERIFY(service);
+
+ // no collections
+ COMPARE_DBUS_LOCAL_CALL(service->collections(), QList<Collection*>{});
+}
+
+void TestGuiFdoSecrets::testServiceSearch()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+
+ item->backend()->attributes()->set("fdosecrets-test", "1");
+ item->backend()->attributes()->set("fdosecrets-test-protected", "2", true);
+ const QString crazyKey = "_a:bc&-+'-e%12df_d";
+ const QString crazyValue = "[v]al@-ue";
+ item->backend()->attributes()->set(crazyKey, crazyValue);
+
+ // search by title
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Title", item->backend()->title()}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked, {item});
+ }
+
+ // search by attribute
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test", "1"}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked, {item});
+ }
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{crazyKey, crazyValue}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked, {item});
+ }
+
+ // searching using empty terms returns nothing
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked.size(), 0);
+ }
+
+ // searching using protected attributes or password returns nothing
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"Password", item->backend()->password()}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked.size(), 0);
+ }
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"fdosecrets-test-protected", "2"}}, locked));
+ QCOMPARE(locked.size(), 0);
+ QCOMPARE(unlocked.size(), 0);
+ }
+}
+
+void TestGuiFdoSecrets::testServiceUnlock()
+{
+ lockDatabaseInBackend();
+
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*)));
+ QVERIFY(spyCollectionCreated.isValid());
+ QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*)));
+ QVERIFY(spyCollectionDeleted.isValid());
+ QSignalSpy spyCollectionChanged(service, SIGNAL(collectionChanged(Collection*)));
+ QVERIFY(spyCollectionChanged.isValid());
+
+ PromptBase* prompt = nullptr;
+ {
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->unlock({coll.data()}, prompt));
+ // nothing is unlocked immediately without user's action
+ QVERIFY(unlocked.isEmpty());
+ }
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // nothing is unlocked yet
+ QCOMPARE(spyPromptCompleted.count(), 0);
+ QVERIFY(coll);
+ QVERIFY(coll->backend()->isLocked());
+
+ // drive the prompt
+ QVERIFY(!prompt->prompt("").isError());
+
+ // still not unlocked before user action
+ QCOMPARE(spyPromptCompleted.count(), 0);
+ QVERIFY(coll);
+ QVERIFY(coll->backend()->isLocked());
+
+ // interact with the dialog
+ QApplication::processEvents();
+ {
+ auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>();
+ QVERIFY(dbOpenDlg);
+ auto editPassword = dbOpenDlg->findChild<QLineEdit*>("editPassword");
+ QVERIFY(editPassword);
+ editPassword->setFocus();
+ QTest::keyClicks(editPassword, "a");
+ QTest::keyClick(editPassword, Qt::Key_Enter);
+ }
+ QApplication::processEvents();
+
+ // unlocked
+ QVERIFY(coll);
+ QVERIFY(!coll->backend()->isLocked());
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ {
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.size(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {coll->objectPath()});
+ }
+ QCOMPARE(spyCollectionCreated.count(), 0);
+ QCOMPARE(spyCollectionChanged.count(), 1);
+ {
+ auto args = spyCollectionChanged.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), coll.data());
+ }
+ QCOMPARE(spyCollectionDeleted.count(), 0);
+}
+
+void TestGuiFdoSecrets::testServiceLock()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*)));
+ QVERIFY(spyCollectionCreated.isValid());
+ QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*)));
+ QVERIFY(spyCollectionDeleted.isValid());
+ QSignalSpy spyCollectionChanged(service, SIGNAL(collectionChanged(Collection*)));
+ QVERIFY(spyCollectionChanged.isValid());
+
+ // if the db is modified, prompt user
+ m_db->markAsModified();
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt));
+ QCOMPARE(locked.size(), 0);
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click cancel
+ MessageBox::setNextAnswer(MessageBox::Cancel);
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QVERIFY(!coll->backend()->isLocked());
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), true);
+ QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {});
+ }
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(locked, service->lock({coll}, prompt));
+ QCOMPARE(locked.size(), 0);
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click save
+ MessageBox::setNextAnswer(MessageBox::Save);
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QVERIFY(coll->backend()->isLocked());
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).value<QList<QDBusObjectPath>>(), {coll->objectPath()});
+ }
+
+ QCOMPARE(spyCollectionCreated.count(), 0);
+ QCOMPARE(spyCollectionChanged.count(), 1);
+ {
+ auto args = spyCollectionChanged.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), coll.data());
+ }
+ QCOMPARE(spyCollectionDeleted.count(), 0);
+
+ // locking item locks the whole db
+ unlockDatabaseInBackend();
+ {
+ auto item = getFirstItem(coll);
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(locked, service->lock({item}, prompt));
+ QCOMPARE(locked.size(), 0);
+ QVERIFY(prompt);
+
+ MessageBox::setNextAnswer(MessageBox::Save);
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QVERIFY(coll->backend()->isLocked());
+ }
+}
+
+void TestGuiFdoSecrets::testSessionOpen()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ auto sess = openSession(service, PlainCipher::Algorithm);
+ QVERIFY(sess);
+ QCOMPARE(service->sessions().size(), 1);
+
+ sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+ QCOMPARE(service->sessions().size(), 2);
+}
+
+void TestGuiFdoSecrets::testSessionClose()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ auto sess = openSession(service, PlainCipher::Algorithm);
+ QVERIFY(sess);
+
+ QCOMPARE(service->sessions().size(), 1);
+
+ auto rep = sess->close();
+ QVERIFY(!rep.isError());
+
+ QCOMPARE(service->sessions().size(), 0);
+}
+
+void TestGuiFdoSecrets::testCollectionCreate()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ QSignalSpy spyCollectionCreated(service, SIGNAL(collectionCreated(Collection*)));
+ QVERIFY(spyCollectionCreated.isValid());
+
+ // returns existing if alias is nonempty and exists
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(
+ coll, service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "NewDB"}}, "default", prompt));
+ QVERIFY(!prompt);
+ QCOMPARE(coll, getDefaultCollection(service).data());
+ }
+ QCOMPARE(spyCollectionCreated.count(), 0);
+
+ // create new one and set properties
+ {
+ PromptBase* prompt = nullptr;
+ CHECKED_DBUS_LOCAL_CALL(
+ created,
+ service->createCollection({{DBUS_INTERFACE_SECRET_COLLECTION ".Label", "Test NewDB"}}, "mydatadb", prompt));
+ QVERIFY(!created);
+ QVERIFY(prompt);
+
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ QTimer::singleShot(50, this, SLOT(createDatabaseCallback()));
+ QVERIFY(!prompt->prompt("").isError());
+ QApplication::processEvents();
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.size(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ auto coll = FdoSecrets::pathToObject<Collection>(args.at(1).value<QDBusObjectPath>());
+ QVERIFY(coll);
+
+ QCOMPARE(coll->backend()->database()->metadata()->name(), QStringLiteral("Test NewDB"));
+
+ QCOMPARE(spyCollectionCreated.count(), 1);
+ {
+ args = spyCollectionCreated.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), coll);
+ }
+ }
+}
+
+void TestGuiFdoSecrets::createDatabaseCallback()
+{
+ auto wizard = m_tabWidget->findChild<NewDatabaseWizard*>();
+ QVERIFY(wizard);
+
+ QCOMPARE(wizard->currentId(), 0);
+ wizard->next();
+ wizard->next();
+ QCOMPARE(wizard->currentId(), 2);
+
+ // enter password
+ auto* passwordEdit = wizard->findChild<QLineEdit*>("enterPasswordEdit");
+ auto* passwordRepeatEdit = wizard->findChild<QLineEdit*>("repeatPasswordEdit");
+ QTest::keyClicks(passwordEdit, "test");
+ QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
+ QTest::keyClicks(passwordRepeatEdit, "test");
+
+ // save database to temporary file
+ TemporaryFile tmpFile;
+ QVERIFY(tmpFile.open());
+ tmpFile.close();
+ fileDialog()->setNextFileName(tmpFile.fileName());
+
+ wizard->accept();
+
+ tmpFile.remove();
+}
+
+void TestGuiFdoSecrets::testCollectionDelete()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ // closing the tab calls coll->deleteLater()
+ // but deleteLater is not processed in QApplication::processEvent
+ // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
+ auto rawColl = coll.data();
+
+ QSignalSpy spyCollectionDeleted(service, SIGNAL(collectionDeleted(Collection*)));
+ QVERIFY(spyCollectionDeleted.isValid());
+
+ m_db->markAsModified();
+ CHECKED_DBUS_LOCAL_CALL(prompt, coll->deleteCollection());
+ QVERIFY(prompt);
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click save
+ MessageBox::setNextAnswer(MessageBox::Save);
+ QVERIFY(!prompt->prompt("").isError());
+
+ QApplication::processEvents();
+
+ // closing the tab should have deleted coll if not in testing
+ // but deleteLater is not processed in QApplication::processEvent
+ // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
+ // QVERIFY(!coll);
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).toString(), QStringLiteral(""));
+
+ QCOMPARE(spyCollectionDeleted.count(), 1);
+ {
+ args = spyCollectionDeleted.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Collection*>(), rawColl);
+ }
+}
+
+void TestGuiFdoSecrets::testItemCreate()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+
+ // create item
+ StringStringMap attributes{
+ {"application", "fdosecrets-test"},
+ {"attr-i[bute]", "![some] -value*"},
+ };
+
+ auto item = createItem(sess, coll, "abc", "Password", attributes, false);
+ QVERIFY(item);
+
+ // attributes
+ {
+ CHECKED_DBUS_LOCAL_CALL(actual, item->attributes());
+ for (const auto& key : attributes.keys()) {
+ QVERIFY(actual.contains(key));
+ QCOMPARE(actual[key], attributes[key]);
+ }
+ }
+
+ // label
+ COMPARE_DBUS_LOCAL_CALL(item->label(), QStringLiteral("abc"));
+
+ // secrets
+ {
+ CHECKED_DBUS_LOCAL_CALL(ss, item->getSecret(sess));
+ ss = m_cipher->decrypt(ss);
+ QCOMPARE(ss.value, QByteArray("Password"));
+ }
+
+ // searchable
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems(attributes, locked));
+ QCOMPARE(locked, QList<Item*>{});
+ QCOMPARE(unlocked, QList<Item*>{item});
+ }
+ {
+ CHECKED_DBUS_LOCAL_CALL(unlocked, coll->searchItems(attributes));
+ QVERIFY(unlocked.contains(item));
+ }
+}
+
+void TestGuiFdoSecrets::testItemReplace()
+{
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+
+ // create item
+ StringStringMap attr1{
+ {"application", "fdosecrets-test"},
+ {"attr-i[bute]", "![some] -value*"},
+ {"fdosecrets-attr", "1"},
+ };
+ StringStringMap attr2{
+ {"application", "fdosecrets-test"},
+ {"attr-i[bute]", "![some] -value*"},
+ {"fdosecrets-attr", "2"},
+ };
+
+ auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false);
+ QVERIFY(item1);
+ auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false);
+ QVERIFY(item2);
+
+ {
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked));
+ QCOMPARE(unlocked.size(), 2);
+ }
+
+ {
+ // when replace, existing item with matching attr is updated
+ auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true);
+ QVERIFY(item3);
+ QCOMPARE(item2, item3);
+ COMPARE_DBUS_LOCAL_CALL(item3->label(), QStringLiteral("abc3"));
+ // there is still 2 entries
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked));
+ QCOMPARE(unlocked.size(), 2);
+ }
+
+ {
+ // when NOT replace, another entry is created
+ auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false);
+ QVERIFY(item4);
+ COMPARE_DBUS_LOCAL_CALL(item2->label(), QStringLiteral("abc3"));
+ COMPARE_DBUS_LOCAL_CALL(item4->label(), QStringLiteral("abc4"));
+ // there is 3 entries
+ QList<Item*> locked;
+ CHECKED_DBUS_LOCAL_CALL(unlocked, service->searchItems({{"application", "fdosecrets-test"}}, locked));
+ QCOMPARE(unlocked.size(), 3);
+ }
+}
+
+void TestGuiFdoSecrets::testItemSecret()
+{
+ const QString TEXT_PLAIN = "text/plain";
+ const QString APPLICATION_OCTET_STREAM = "application/octet-stream";
+
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+ auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
+ QVERIFY(sess);
+
+ // plain text secret
+ {
+ CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess));
+ auto ss = m_cipher->decrypt(encrypted);
+ QCOMPARE(ss.contentType, TEXT_PLAIN);
+ QCOMPARE(ss.value, item->backend()->password().toUtf8());
+ }
+
+ // get secret with notification (only works when called from DBUS)
+ FdoSecrets::settings()->setShowNotification(true);
+ {
+ QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int)));
+ QVERIFY(spyShowNotification.isValid());
+
+ auto iitem = interfaceOf(item);
+ QVERIFY(static_cast<bool>(iitem));
+
+ auto replyMsg = iitem->call(QDBus::BlockWithGui, "GetSecret", QVariant::fromValue(sess->objectPath()));
+ auto reply = QDBusPendingReply<SecretStruct>(replyMsg);
+ QVERIFY(reply.isValid());
+ auto ss = m_cipher->decrypt(reply.argumentAt<0>());
+
+ QCOMPARE(ss.contentType, TEXT_PLAIN);
+ QCOMPARE(ss.value, item->backend()->password().toUtf8());
+
+ QCOMPARE(spyShowNotification.count(), 1);
+ }
+ FdoSecrets::settings()->setShowNotification(false);
+
+ // set secret with plain text
+ {
+ SecretStruct ss;
+ ss.contentType = TEXT_PLAIN;
+ ss.value = "NewPassword";
+ ss.session = sess->objectPath();
+ QVERIFY(!item->setSecret(m_cipher->encrypt(ss)).isError());
+
+ QCOMPARE(item->backend()->password().toUtf8(), ss.value);
+ }
+
+ // set secret with something else is saved as attachment
+ {
+ SecretStruct expected;
+ expected.contentType = APPLICATION_OCTET_STREAM;
+ expected.value = "NewPasswordBinary";
+ expected.session = sess->objectPath();
+ QVERIFY(!item->setSecret(m_cipher->encrypt(expected)).isError());
+
+ QCOMPARE(item->backend()->password(), QStringLiteral(""));
+
+ CHECKED_DBUS_LOCAL_CALL(encrypted, item->getSecret(sess));
+ auto ss = m_cipher->decrypt(encrypted);
+ QCOMPARE(ss.contentType, expected.contentType);
+ QCOMPARE(ss.value, expected.value);
+ }
+}
+
+void TestGuiFdoSecrets::testItemDelete()
+{
+ FdoSecrets::settings()->setNoConfirmDeleteItem(false);
+
+ auto service = enableService();
+ QVERIFY(service);
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ auto item = getFirstItem(coll);
+ QVERIFY(item);
+ auto rawItem = item.data();
+
+ QSignalSpy spyItemDeleted(coll, SIGNAL(itemDeleted(Item*)));
+ QVERIFY(spyItemDeleted.isValid());
+
+ CHECKED_DBUS_LOCAL_CALL(prompt, item->deleteItem());
+ QVERIFY(prompt);
+
+ QSignalSpy spyPromptCompleted(prompt, SIGNAL(completed(bool, QVariant)));
+ QVERIFY(spyPromptCompleted.isValid());
+
+ // prompt and click save
+ if (item->isDeletePermanent()) {
+ MessageBox::setNextAnswer(MessageBox::Delete);
+ } else {
+ MessageBox::setNextAnswer(MessageBox::Move);
+ }
+ QVERIFY(!prompt->prompt("").isError());
+
+ QApplication::processEvents();
+
+ QCOMPARE(spyPromptCompleted.count(), 1);
+ auto args = spyPromptCompleted.takeFirst();
+ QCOMPARE(args.count(), 2);
+ QCOMPARE(args.at(0).toBool(), false);
+ QCOMPARE(args.at(1).toString(), QStringLiteral(""));
+
+ QCOMPARE(spyItemDeleted.count(), 1);
+ {
+ args = spyItemDeleted.takeFirst();
+ QCOMPARE(args.size(), 1);
+ QCOMPARE(args.at(0).value<Item*>(), rawItem);
+ }
+}
+
+void TestGuiFdoSecrets::testAlias()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ // read default alias
+ CHECKED_DBUS_LOCAL_CALL(coll, service->readAlias("default"));
+ QVERIFY(coll);
+
+ // set extra alias
+ QVERIFY(!service->setAlias("another", coll).isError());
+
+ // get using extra alias
+ CHECKED_DBUS_LOCAL_CALL(coll2, service->readAlias("another"));
+ QVERIFY(coll2);
+ QCOMPARE(coll, coll2);
+}
+
+void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent()
+{
+ auto service = enableService();
+ QVERIFY(service);
+
+ // one collection, which is default alias
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ // after locking, the collection is still there, but locked
+ lockDatabaseInBackend();
+
+ coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ COMPARE_DBUS_LOCAL_CALL(coll->locked(), true);
+
+ // unlock the database, the alias and collection is present
+ unlockDatabaseInBackend();
+
+ coll = getDefaultCollection(service);
+ QVERIFY(coll);
+ COMPARE_DBUS_LOCAL_CALL(coll->locked(), false);
+}
+
+void TestGuiFdoSecrets::testExposeSubgroup()
+{
+ auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
+ QVERIFY(subgroup);
+ FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
+ auto service = enableService();
+ QVERIFY(service);
+
+ auto coll = getDefaultCollection(service);
+ QVERIFY(coll);
+
+ // exposing subgroup does not expose entries in other groups
+ auto items = coll->items();
+ QVERIFY(!items.isError());
+ QList<Entry*> exposedEntries;
+ for (const auto& item : items.value()) {
+ exposedEntries << item->backend();
+ }
+ QCOMPARE(exposedEntries, subgroup->entries());
+}
+
+void TestGuiFdoSecrets::testModifiyingExposedGroup()
+{
+ // test when exposed group is removed the collection is not exposed anymore
+ auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking");
+ QVERIFY(subgroup);
+ FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
+ auto service = enableService();
+ QVERIFY(service);
+
+ {
+ CHECKED_DBUS_LOCAL_CALL(colls, service->collections());
+ QCOMPARE(colls.size(), 1);
+ }
+
+ m_db->metadata()->setRecycleBinEnabled(true);
+ m_db->recycleGroup(subgroup);
+ QApplication::processEvents();
+
+ {
+ CHECKED_DBUS_LOCAL_CALL(colls, service->collections());
+ QCOMPARE(colls.size(), 0);
+ }
+
+ // test setting another exposed group, the collection will be exposed again
+ FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
+ QApplication::processEvents();
+ {
+ CHECKED_DBUS_LOCAL_CALL(colls, service->collections());
+ QCOMPARE(colls.size(), 1);
+ }
+}
+
+QPointer<Service> TestGuiFdoSecrets::enableService()
+{
+ FdoSecrets::settings()->setEnabled(true);
+ VERIFY(m_plugin);
+ m_plugin->updateServiceState();
+ return m_plugin->serviceInstance();
+}
+
+QPointer<Session> TestGuiFdoSecrets::openSession(Service* service, const QString& algo)
+{
+ // open session has to be called actually over DBUS to get peer info
+
+ VERIFY(service);
+ auto iservice = interfaceOf(service);
+ VERIFY(iservice);
+
+ if (algo == PlainCipher::Algorithm) {
+ auto replyMsg = iservice->call(QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant("")));
+ auto reply = QDBusPendingReply<QDBusVariant, QDBusObjectPath>(replyMsg);
+
+ VERIFY(reply.isValid());
+ return FdoSecrets::pathToObject<Session>(reply.argumentAt<1>());
+ } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) {
+
+ DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(MpiFromBytes(MpiToBytes(m_serverPrivate)),
+ MpiFromBytes(MpiToBytes(m_serverPublic)));
+
+ auto replyMsg = iservice->call(
+ QDBus::BlockWithGui, "OpenSession", algo, QVariant::fromValue(QDBusVariant(m_cipher->m_publicKey)));
+ auto reply = QDBusPendingReply<QDBusVariant, QDBusObjectPath>(replyMsg);
+ VERIFY(reply.isValid());
+ COMPARE(qvariant_cast<QByteArray>(reply.argumentAt<0>().variant()), MpiToBytes(m_serverPublic));
+ return FdoSecrets::pathToObject<Session>(reply.argumentAt<1>());
+ }
+ FAIL("Unsupported algorithm");
+}
+
+QPointer<Collection> TestGuiFdoSecrets::getDefaultCollection(Service* service)
+{
+ VERIFY(service);
+ auto coll = service->readAlias("default");
+ VERIFY(!coll.isError());
+ return coll.value();
+}
+
+QPointer<Item> TestGuiFdoSecrets::getFirstItem(Collection* coll)
+{
+ VERIFY(coll);
+ auto items = coll->items();
+ VERIFY(!items.isError());
+ VERIFY(!items.value().isEmpty());
+ return items.value().at(0);
+}
+
+QPointer<Item> TestGuiFdoSecrets::createItem(Session* sess,
+ Collection* coll,
+ const QString& label,
+ const QString& pass,
+ const StringStringMap& attr,
+ bool replace)
+{
+ VERIFY(sess);
+ VERIFY(coll);
+
+ QVariantMap properties{
+ {DBUS_INTERFACE_SECRET_ITEM ".Label", QVariant::fromValue(label)},
+ {DBUS_INTERFACE_SECRET_ITEM ".Attributes", QVariant::fromValue(attr)},
+ };
+
+ SecretStruct ss;
+ ss.session = sess->objectPath();
+ ss.value = pass.toLocal8Bit();
+ ss.contentType = "plain/text";
+ ss = m_cipher->encrypt(ss);
+
+ PromptBase* prompt = nullptr;
+ auto item = coll->createItem(properties, ss, replace, prompt);
+ VERIFY(!item.isError());
+ // creating item does not have a prompt to show
+ VERIFY(!prompt);
+ return item.value();
+}
+
+void TestGuiFdoSecrets::lockDatabaseInBackend()
+{
+ m_dbWidget->lock();
+ m_db.reset();
+ QApplication::processEvents();
+}
+
+void TestGuiFdoSecrets::unlockDatabaseInBackend()
+{
+ m_dbWidget->performUnlockDatabase("a");
+ m_db = m_dbWidget->database();
+ QApplication::processEvents();
+}