2
* Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
4
* This program is free software: you can redistribute it and/or modify
5
* it under the terms of the GNU General Public License as published by
6
* the Free Software Foundation, either version 2 or (at your option)
7
* version 3 of the License.
9
* This program is distributed in the hope that it will be useful,
10
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
* GNU General Public License for more details.
14
* You should have received a copy of the GNU General Public License
15
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18
#include "TestGuiFdoSecrets.h"
20
#include "fdosecrets/FdoSecretsPlugin.h"
21
#include "fdosecrets/FdoSecretsSettings.h"
22
#include "fdosecrets/objects/Collection.h"
23
#include "fdosecrets/objects/Item.h"
24
#include "fdosecrets/objects/SessionCipher.h"
25
#include "fdosecrets/widgets/AccessControlDialog.h"
27
#include "config-keepassx-tests.h"
29
#include "core/Tools.h"
30
#include "crypto/Crypto.h"
31
#include "gui/Application.h"
32
#include "gui/DatabaseTabWidget.h"
33
#include "gui/FileDialog.h"
34
#include "gui/MainWindow.h"
35
#include "gui/MessageBox.h"
36
#include "gui/PasswordWidget.h"
37
#include "gui/wizard/NewDatabaseWizard.h"
38
#include "util/FdoSecretsProxy.h"
39
#include "util/TemporaryFile.h"
47
int main(int argc, char* argv[])
49
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
50
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
51
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
53
Application app(argc, argv);
54
app.setApplicationName("KeePassXC");
55
app.setApplicationVersion(KEEPASSXC_VERSION);
56
app.setQuitOnLastWindowClosed(false);
57
app.setAttribute(Qt::AA_Use96Dpi, true);
59
QTEST_DISABLE_KEYPAD_NAVIGATION
61
QTEST_SET_MAIN_SOURCE_PATH
62
return QTest::qExec(&tc, argc, argv);
65
#define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default"
67
// assert macros compatible with function having return values
68
#define VERIFY2_RET(statement, msg) \
70
if (!QTest::qVerify(static_cast<bool>(statement), #statement, (msg), __FILE__, __LINE__)) \
74
#define COMPARE_RET(actual, expected) \
76
if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
80
// by default use these with Qt macros
82
#define COMPARE QCOMPARE
83
#define VERIFY2 QVERIFY2
85
#define DBUS_COMPARE(actual, expected) \
87
auto reply = (actual); \
88
VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \
89
COMPARE(reply.value(), (expected)); \
92
#define DBUS_VERIFY(stmt) \
94
auto reply = (stmt); \
95
VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \
98
#define DBUS_GET(var, stmt) \
99
std::remove_cv<decltype((stmt).argumentAt<0>())>::type var; \
101
const auto rep = (stmt); \
102
VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \
103
var = rep.argumentAt<0>(); \
106
#define DBUS_GET2(name1, name2, stmt) \
107
std::remove_cv<decltype((stmt).argumentAt<0>())>::type name1; \
108
std::remove_cv<decltype((stmt).argumentAt<1>())>::type name2; \
110
const auto rep = (stmt); \
111
VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \
112
name1 = rep.argumentAt<0>(); \
113
name2 = rep.argumentAt<1>(); \
116
using namespace FdoSecrets;
118
class FakeClient : public DBusClient
121
explicit FakeClient(DBusMgr* dbus)
124
{QStringLiteral("local"), 0, true, {ProcInfo{0, 0, QStringLiteral("fake-client"), QString{}, QString{}}}})
129
// pretty print QDBusObjectPath in QCOMPARE
130
char* toString(const QDBusObjectPath& path)
132
return QTest::toString("ObjectPath(" + path.path() + ")");
135
TestGuiFdoSecrets::~TestGuiFdoSecrets() = default;
137
void TestGuiFdoSecrets::initTestCase()
139
VERIFY(Crypto::init());
140
Config::createTempFileInstance();
141
config()->set(Config::AutoSaveAfterEveryChange, false);
142
config()->set(Config::AutoSaveOnExit, false);
143
config()->set(Config::GUI_ShowTrayIcon, true);
144
config()->set(Config::UpdateCheckMessageShown, true);
145
// Disable quick unlock
146
config()->set(Config::Security_QuickUnlock, false);
147
// Disable secret service integration (activate within individual tests to test the plugin)
148
FdoSecrets::settings()->setEnabled(false);
149
// activate within individual tests
150
FdoSecrets::settings()->setShowNotification(false);
152
Application::bootstrap();
154
m_mainWindow.reset(new MainWindow());
155
m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
157
m_plugin = FdoSecretsPlugin::getPlugin();
159
m_mainWindow->show();
161
auto key = QByteArray::fromHex("e407997e8b918419cf851cf3345358fdf"
162
"ffb9564a220ac9c3934efd277cea20d17"
163
"467ecdc56e817f75ac39501f38a4a04ff"
164
"64d627e16c09981c7ad876da255b61c8e"
165
"6a8408236c2a4523cfe6961c26dbdfc77"
166
"c1a27a5b425ca71a019e829fae32c0b42"
167
"0e1b3096b48bc2ce9ccab1d1ff13a5eb4"
168
"b263cee30bdb1a57af9bfa93f");
169
m_clientCipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(key));
171
// Load the NewDatabase.kdbx file into temporary storage
172
QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx"));
173
VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
174
VERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
175
sourceDbFile.close();
177
// set a fake dbus client all the time so we can freely access DBusMgr anywhere
178
m_client.reset(new FakeClient(m_plugin->dbus().data()));
179
m_plugin->dbus()->overrideClient(m_client);
182
// Every test starts with opening the temp database
183
void TestGuiFdoSecrets::init()
185
m_dbFile.reset(new TemporaryFile());
186
// Write the temp storage to a temp database file for use in our tests
187
VERIFY(m_dbFile->open());
188
COMPARE(m_dbFile->write(m_dbData), static_cast<qint64>(m_dbData.size()));
191
// make sure window is activated or focus tests may fail
192
m_mainWindow->activateWindow();
195
// open and unlock the database
196
m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
197
m_dbWidget = m_tabWidget->currentDatabaseWidget();
198
m_db = m_dbWidget->database();
200
// by default expose the root group
201
FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
202
VERIFY(m_dbWidget->save());
204
// enforce consistent default settings at the beginning
205
FdoSecrets::settings()->setUnlockBeforeSearch(false);
206
FdoSecrets::settings()->setShowNotification(false);
207
FdoSecrets::settings()->setConfirmAccessItem(false);
208
FdoSecrets::settings()->setEnabled(false);
211
// Every test ends with closing the temp database without saving
212
void TestGuiFdoSecrets::cleanup()
214
// restore to default settings
215
FdoSecrets::settings()->setUnlockBeforeSearch(false);
216
FdoSecrets::settings()->setShowNotification(false);
217
FdoSecrets::settings()->setConfirmAccessItem(false);
218
FdoSecrets::settings()->setEnabled(false);
220
m_plugin->updateServiceState();
223
// DO NOT save the database
224
for (int i = 0; i != m_tabWidget->count(); ++i) {
225
m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean();
229
while (auto w = QApplication::activeModalWidget()) {
233
VERIFY(m_tabWidget->closeAllDatabaseTabs());
240
m_client->clearAuthorization();
243
void TestGuiFdoSecrets::cleanupTestCase()
245
m_plugin->dbus()->overrideClient({});
251
void TestGuiFdoSecrets::testServiceEnable()
253
QSignalSpy sigError(m_plugin, SIGNAL(error(QString)));
254
VERIFY(sigError.isValid());
256
QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted()));
257
VERIFY(sigStarted.isValid());
259
// make sure no one else is holding the service
260
VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
262
// enable the service
263
auto service = enableService();
266
// service started without error
267
VERIFY(sigError.isEmpty());
268
COMPARE(sigStarted.size(), 1);
272
VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
274
// there will be one default collection
275
auto coll = getDefaultCollection(service);
278
DBUS_COMPARE(coll->locked(), false);
279
DBUS_COMPARE(coll->label(), m_db->metadata()->name());
281
DBUS_COMPARE(coll->created(),
282
static_cast<qulonglong>(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000));
285
static_cast<qulonglong>(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000));
288
void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase()
290
// reset the exposed group and then enable the service
291
FdoSecrets::settings()->setExposedGroup(m_db, {});
292
auto service = enableService();
296
DBUS_COMPARE(service->collections(), QList<QDBusObjectPath>{});
299
void TestGuiFdoSecrets::testServiceSearch()
301
auto service = enableService();
303
auto coll = getDefaultCollection(service);
305
auto item = getFirstItem(coll);
308
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
310
auto entry = itemObj->backend();
313
entry->attributes()->set("fdosecrets-test", "1");
314
entry->attributes()->set("fdosecrets-test-protected", "2", true);
315
const QString crazyKey = "_a:bc&-+'-e%12df_d";
316
const QString crazyValue = "[v]al@-ue";
317
entry->attributes()->set(crazyKey, crazyValue);
321
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
323
COMPARE(unlocked, {QDBusObjectPath(item->path())});
326
// search by attribute
328
DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test", "1"}}));
330
COMPARE(unlocked, {QDBusObjectPath(item->path())});
333
DBUS_GET2(unlocked, locked, service->SearchItems({{crazyKey, crazyValue}}));
335
COMPARE(unlocked, {QDBusObjectPath(item->path())});
338
// searching using empty terms returns nothing
340
DBUS_GET2(unlocked, locked, service->SearchItems({}));
342
COMPARE(unlocked, {});
345
// searching using protected attributes or password returns nothing
347
DBUS_GET2(unlocked, locked, service->SearchItems({{"Password", entry->password()}}));
349
COMPARE(unlocked, {});
352
DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test-protected", "2"}}));
354
COMPARE(unlocked, {});
358
void TestGuiFdoSecrets::testServiceSearchBlockingUnlock()
360
auto service = enableService();
362
auto coll = getDefaultCollection(service);
365
auto entries = m_db->rootGroup()->entriesRecursive();
366
VERIFY(!entries.isEmpty());
367
// assumes the db is not empty
368
auto title = entries.first()->title();
370
// NOTE: entries are no longer valid after locking
371
lockDatabaseInBackend();
373
// when database is locked, nothing is returned
374
FdoSecrets::settings()->setUnlockBeforeSearch(false);
376
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}}));
378
COMPARE(unlocked, {});
381
// when database is locked, nothing is returned
382
FdoSecrets::settings()->setUnlockBeforeSearch(true);
384
// SearchItems will block because the blocking wait is implemented
385
// using a local QEventLoop.
386
// so we do a little trick here to get the return value back
387
bool unlockDialogWorks = false;
388
QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(); });
390
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}}));
391
VERIFY(unlockDialogWorks);
393
COMPARE(unlocked.size(), 1);
394
auto item = getProxy<ItemProxy>(unlocked.first());
395
DBUS_COMPARE(item->label(), title);
399
void TestGuiFdoSecrets::testServiceSearchBlockingUnlockMultiple()
401
// setup: two databases, both locked, one with exposed db, the other not.
403
// add another database tab with a database with no exposed group
404
// to avoid modify the original, copy to a temp file first
405
QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase2.kdbx"));
407
VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
408
VERIFY(Tools::readAllFromDevice(&sourceDbFile, dbData));
409
sourceDbFile.close();
411
QTemporaryFile anotherFile;
412
VERIFY(anotherFile.open());
413
COMPARE(anotherFile.write(dbData), static_cast<qint64>(dbData.size()));
416
m_tabWidget->addDatabaseTab(anotherFile.fileName(), false);
417
auto anotherWidget = m_tabWidget->currentDatabaseWidget();
419
auto service = enableService();
422
// when there are multiple locked databases,
423
// repeatedly show the dialog until there is at least one unlocked collection
424
FdoSecrets::settings()->setUnlockBeforeSearch(true);
426
// when only unlocking the one with no exposed group, a second dialog is shown
427
lockDatabaseInBackend();
429
bool unlockDialogWorks = false;
430
QTimer::singleShot(50, [&]() {
431
unlockDialogWorks = driveUnlockDialog(anotherWidget);
432
QTimer::singleShot(50, [&]() { unlockDialogWorks &= driveUnlockDialog(); });
435
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
436
VERIFY(unlockDialogWorks);
438
COMPARE(unlocked.size(), 1);
441
// when unlocking the one with exposed group, the other one remains locked
442
lockDatabaseInBackend();
444
bool unlockDialogWorks = false;
445
QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(m_dbWidget); });
447
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
448
VERIFY(unlockDialogWorks);
450
COMPARE(unlocked.size(), 1);
451
VERIFY(anotherWidget->isLocked());
455
void TestGuiFdoSecrets::testServiceSearchForce()
457
auto service = enableService();
459
auto coll = getDefaultCollection(service);
461
auto item = getFirstItem(coll);
464
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
466
auto entry = itemObj->backend();
469
// fdosecrets should still find the item even if searching is disabled
470
entry->group()->setSearchingEnabled(Group::Disable);
474
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
476
COMPARE(unlocked, {QDBusObjectPath(item->path())});
480
void TestGuiFdoSecrets::testServiceUnlock()
482
lockDatabaseInBackend();
484
auto service = enableService();
486
auto coll = getDefaultCollection(service);
489
QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
490
VERIFY(spyCollectionCreated.isValid());
491
QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
492
VERIFY(spyCollectionDeleted.isValid());
493
QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
494
VERIFY(spyCollectionChanged.isValid());
496
DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())}));
497
// nothing is unlocked immediately without user's action
498
COMPARE(unlocked, {});
500
auto prompt = getProxy<PromptProxy>(promptPath);
502
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
503
VERIFY(spyPromptCompleted.isValid());
505
// nothing is unlocked yet
506
VERIFY(waitForSignal(spyPromptCompleted, 0));
507
DBUS_COMPARE(coll->locked(), true);
510
DBUS_VERIFY(prompt->Prompt(""));
512
// still not unlocked before user action
513
VERIFY(waitForSignal(spyPromptCompleted, 0));
514
DBUS_COMPARE(coll->locked(), true);
516
VERIFY(driveUnlockDialog());
518
VERIFY(waitForSignal(spyPromptCompleted, 1));
520
auto args = spyPromptCompleted.takeFirst();
521
COMPARE(args.size(), 2);
522
COMPARE(args.at(0).toBool(), false);
523
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
526
// check unlocked *AFTER* the prompt signal
527
DBUS_COMPARE(coll->locked(), false);
529
VERIFY(waitForSignal(spyCollectionCreated, 0));
530
QTRY_VERIFY(!spyCollectionChanged.isEmpty());
531
for (const auto& args : spyCollectionChanged) {
532
COMPARE(args.size(), 1);
533
COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
535
VERIFY(waitForSignal(spyCollectionDeleted, 0));
538
void TestGuiFdoSecrets::testServiceUnlockDatabaseConcurrent()
540
lockDatabaseInBackend();
542
auto service = enableService();
544
auto coll = getDefaultCollection(service);
547
DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())}));
548
auto prompt = getProxy<PromptProxy>(promptPath);
550
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
551
VERIFY(spyPromptCompleted.isValid());
552
DBUS_VERIFY(prompt->Prompt(""));
554
// while the first prompt is running, another request come in
555
DBUS_GET2(unlocked2, promptPath2, service->Unlock({QDBusObjectPath(coll->path())}));
556
auto prompt2 = getProxy<PromptProxy>(promptPath2);
558
QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
559
VERIFY(spyPromptCompleted2.isValid());
560
DBUS_VERIFY(prompt2->Prompt(""));
562
// there should be only one unlock dialog
563
VERIFY(driveUnlockDialog());
565
// both prompts should complete
566
VERIFY(waitForSignal(spyPromptCompleted, 1));
568
auto args = spyPromptCompleted.takeFirst();
569
COMPARE(args.size(), 2);
570
COMPARE(args.at(0).toBool(), false);
571
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
573
VERIFY(waitForSignal(spyPromptCompleted2, 1));
575
auto args = spyPromptCompleted2.takeFirst();
576
COMPARE(args.size(), 2);
577
COMPARE(args.at(0).toBool(), false);
578
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
581
// check unlocked *AFTER* prompt signal
582
DBUS_COMPARE(coll->locked(), false);
585
void TestGuiFdoSecrets::testServiceUnlockItems()
587
FdoSecrets::settings()->setConfirmAccessItem(true);
589
auto service = enableService();
591
auto coll = getDefaultCollection(service);
593
auto item = getFirstItem(coll);
595
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
598
DBUS_COMPARE(item->locked(), true);
601
DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
602
// nothing is unlocked immediately without user's action
603
COMPARE(unlocked, {});
605
auto prompt = getProxy<PromptProxy>(promptPath);
607
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
608
VERIFY(spyPromptCompleted.isValid());
610
// nothing is unlocked yet
611
COMPARE(spyPromptCompleted.count(), 0);
612
DBUS_COMPARE(item->locked(), true);
615
DBUS_VERIFY(prompt->Prompt(""));
617
VERIFY(driveAccessControlDialog(false));
619
VERIFY(waitForSignal(spyPromptCompleted, 1));
621
auto args = spyPromptCompleted.takeFirst();
622
COMPARE(args.size(), 2);
623
COMPARE(args.at(0).toBool(), false);
624
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
628
DBUS_COMPARE(item->locked(), false);
631
// access the secret should reset the locking state
633
DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
635
DBUS_COMPARE(item->locked(), true);
637
// unlock again with remember
639
DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
640
// nothing is unlocked immediately without user's action
641
COMPARE(unlocked, {});
643
auto prompt = getProxy<PromptProxy>(promptPath);
645
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
646
VERIFY(spyPromptCompleted.isValid());
648
// nothing is unlocked yet
649
COMPARE(spyPromptCompleted.count(), 0);
650
DBUS_COMPARE(item->locked(), true);
653
DBUS_VERIFY(prompt->Prompt(""));
654
// only allow and remember
655
VERIFY(driveAccessControlDialog(true));
657
VERIFY(waitForSignal(spyPromptCompleted, 1));
659
auto args = spyPromptCompleted.takeFirst();
660
COMPARE(args.size(), 2);
661
COMPARE(args.at(0).toBool(), false);
662
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
666
DBUS_COMPARE(item->locked(), false);
669
// access the secret does not reset the locking state
671
DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
673
DBUS_COMPARE(item->locked(), false);
676
void TestGuiFdoSecrets::testServiceUnlockItemsIncludeFutureEntries()
678
FdoSecrets::settings()->setConfirmAccessItem(true);
680
auto service = enableService();
682
auto coll = getDefaultCollection(service);
684
auto item = getFirstItem(coll);
686
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
689
DBUS_COMPARE(item->locked(), true);
692
DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
693
// nothing is unlocked immediately without user's action
694
COMPARE(unlocked, {});
696
auto prompt = getProxy<PromptProxy>(promptPath);
698
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
699
VERIFY(spyPromptCompleted.isValid());
701
// nothing is unlocked yet
702
COMPARE(spyPromptCompleted.count(), 0);
703
DBUS_COMPARE(item->locked(), true);
706
DBUS_VERIFY(prompt->Prompt(""));
707
// remember and include future entries
708
VERIFY(driveAccessControlDialog(true, true));
710
VERIFY(waitForSignal(spyPromptCompleted, 1));
712
auto args = spyPromptCompleted.takeFirst();
713
COMPARE(args.size(), 2);
714
COMPARE(args.at(0).toBool(), false);
715
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
719
DBUS_COMPARE(item->locked(), false);
722
// check other entries are also unlocked
724
DBUS_GET(itemPaths, coll->items());
725
VERIFY(itemPaths.size() > 1);
726
auto anotherItem = getProxy<ItemProxy>(itemPaths.last());
728
DBUS_COMPARE(anotherItem->locked(), false);
732
void TestGuiFdoSecrets::testServiceLock()
734
auto service = enableService();
736
auto coll = getDefaultCollection(service);
739
QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
740
VERIFY(spyCollectionCreated.isValid());
741
QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
742
VERIFY(spyCollectionDeleted.isValid());
743
QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
744
VERIFY(spyCollectionChanged.isValid());
746
// if the db is modified, prompt user
747
m_db->markAsModified();
749
DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
751
auto prompt = getProxy<PromptProxy>(promptPath);
753
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
754
VERIFY(spyPromptCompleted.isValid());
756
// prompt and click cancel
757
MessageBox::setNextAnswer(MessageBox::Cancel);
758
DBUS_VERIFY(prompt->Prompt(""));
761
VERIFY(waitForSignal(spyPromptCompleted, 1));
762
auto args = spyPromptCompleted.takeFirst();
763
COMPARE(args.count(), 2);
764
COMPARE(args.at(0).toBool(), true);
765
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {});
767
DBUS_COMPARE(coll->locked(), false);
770
DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
772
auto prompt = getProxy<PromptProxy>(promptPath);
774
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
775
VERIFY(spyPromptCompleted.isValid());
777
// prompt and click save
778
MessageBox::setNextAnswer(MessageBox::Save);
779
DBUS_VERIFY(prompt->Prompt(""));
782
VERIFY(waitForSignal(spyPromptCompleted, 1));
783
auto args = spyPromptCompleted.takeFirst();
784
COMPARE(args.count(), 2);
785
COMPARE(args.at(0).toBool(), false);
786
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
788
DBUS_COMPARE(coll->locked(), true);
791
VERIFY(waitForSignal(spyCollectionCreated, 0));
792
QTRY_VERIFY(!spyCollectionChanged.isEmpty());
793
for (const auto& args : spyCollectionChanged) {
794
COMPARE(args.size(), 1);
795
COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
797
VERIFY(waitForSignal(spyCollectionDeleted, 0));
799
// locking item locks the whole db
800
unlockDatabaseInBackend();
802
auto item = getFirstItem(coll);
803
DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(item->path())}));
805
auto prompt = getProxy<PromptProxy>(promptPath);
808
MessageBox::setNextAnswer(MessageBox::Save);
809
DBUS_VERIFY(prompt->Prompt(""));
812
DBUS_COMPARE(coll->locked(), true);
816
void TestGuiFdoSecrets::testServiceLockConcurrent()
818
auto service = enableService();
820
auto coll = getDefaultCollection(service);
823
m_db->markAsModified();
825
DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
826
auto prompt = getProxy<PromptProxy>(promptPath);
828
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
829
VERIFY(spyPromptCompleted.isValid());
831
DBUS_GET2(locked2, promptPath2, service->Lock({QDBusObjectPath(coll->path())}));
832
auto prompt2 = getProxy<PromptProxy>(promptPath2);
834
QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
835
VERIFY(spyPromptCompleted2.isValid());
837
// prompt and click save
838
MessageBox::setNextAnswer(MessageBox::Save);
839
DBUS_VERIFY(prompt->Prompt(""));
841
// second prompt should not show dialog
842
DBUS_VERIFY(prompt2->Prompt(""));
844
VERIFY(waitForSignal(spyPromptCompleted, 1));
846
auto args = spyPromptCompleted.takeFirst();
847
COMPARE(args.count(), 2);
848
COMPARE(args.at(0).toBool(), false);
849
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
852
VERIFY(waitForSignal(spyPromptCompleted2, 1));
854
auto args = spyPromptCompleted2.takeFirst();
855
COMPARE(args.count(), 2);
856
COMPARE(args.at(0).toBool(), false);
857
COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
860
DBUS_COMPARE(coll->locked(), true);
863
void TestGuiFdoSecrets::testSessionOpen()
865
auto service = enableService();
868
auto sess = openSession(service, PlainCipher::Algorithm);
871
sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
875
void TestGuiFdoSecrets::testSessionClose()
877
auto service = enableService();
880
auto sess = openSession(service, PlainCipher::Algorithm);
883
DBUS_VERIFY(sess->Close());
886
void TestGuiFdoSecrets::testCollectionCreate()
888
auto service = enableService();
891
QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
892
VERIFY(spyCollectionCreated.isValid());
894
// returns existing if alias is nonempty and exists
896
auto existing = getDefaultCollection(service);
899
service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "NewDB"}}, "default"));
900
COMPARE(promptPath, QDBusObjectPath("/"));
901
COMPARE(collPath.path(), existing->path());
903
VERIFY(waitForSignal(spyCollectionCreated, 0));
905
// create new one and set properties
909
service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "Test NewDB"}}, "mydatadb"));
910
COMPARE(collPath, QDBusObjectPath("/"));
911
auto prompt = getProxy<PromptProxy>(promptPath);
914
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
915
VERIFY(spyPromptCompleted.isValid());
917
DBUS_VERIFY(prompt->Prompt(""));
918
VERIFY(driveNewDatabaseWizard());
920
VERIFY(waitForSignal(spyPromptCompleted, 1));
921
auto args = spyPromptCompleted.takeFirst();
922
COMPARE(args.size(), 2);
923
COMPARE(args.at(0).toBool(), false);
924
auto coll = getProxy<CollectionProxy>(getSignalVariantArgument<QDBusObjectPath>(args.at(1)));
927
DBUS_COMPARE(coll->label(), QStringLiteral("Test NewDB"));
929
VERIFY(waitForSignal(spyCollectionCreated, 1));
931
args = spyCollectionCreated.takeFirst();
932
COMPARE(args.size(), 1);
933
COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
938
void TestGuiFdoSecrets::testCollectionDelete()
940
auto service = enableService();
942
auto coll = getDefaultCollection(service);
944
// save the path which will be gone after the deletion.
945
auto collPath = coll->path();
947
QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
948
VERIFY(spyCollectionDeleted.isValid());
950
m_db->markAsModified();
951
DBUS_GET(promptPath, coll->Delete());
952
auto prompt = getProxy<PromptProxy>(promptPath);
954
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
955
VERIFY(spyPromptCompleted.isValid());
957
// prompt and click save
958
MessageBox::setNextAnswer(MessageBox::Save);
959
DBUS_VERIFY(prompt->Prompt(""));
961
// closing the tab should have deleted the database if not in testing
962
// but deleteLater is not processed in QApplication::processEvent
963
// see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
965
VERIFY(waitForSignal(spyPromptCompleted, 1));
966
auto args = spyPromptCompleted.takeFirst();
967
COMPARE(args.count(), 2);
968
COMPARE(args.at(0).toBool(), false);
969
COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
971
// however, the object should already be taken down from dbus
973
auto reply = coll->locked();
974
VERIFY(reply.isFinished() && reply.isError());
975
COMPARE(reply.error().type(), QDBusError::UnknownObject);
978
VERIFY(waitForSignal(spyCollectionDeleted, 1));
980
args = spyCollectionDeleted.takeFirst();
981
COMPARE(args.size(), 1);
982
COMPARE(args.at(0).value<QDBusObjectPath>().path(), collPath);
986
void TestGuiFdoSecrets::testCollectionDeleteConcurrent()
988
auto service = enableService();
990
auto coll = getDefaultCollection(service);
993
m_db->markAsModified();
994
DBUS_GET(promptPath, coll->Delete());
995
auto prompt = getProxy<PromptProxy>(promptPath);
997
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
998
VERIFY(spyPromptCompleted.isValid());
1000
// before interacting with the prompt, another request come in
1001
DBUS_GET(promptPath2, coll->Delete());
1002
auto prompt2 = getProxy<PromptProxy>(promptPath);
1004
QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
1005
VERIFY(spyPromptCompleted2.isValid());
1007
// prompt and click save
1008
MessageBox::setNextAnswer(MessageBox::Save);
1009
DBUS_VERIFY(prompt->Prompt(""));
1011
// there should be no prompt
1012
DBUS_VERIFY(prompt2->Prompt(""));
1014
VERIFY(waitForSignal(spyPromptCompleted, 1));
1016
auto args = spyPromptCompleted.takeFirst();
1017
COMPARE(args.count(), 2);
1018
COMPARE(args.at(0).toBool(), false);
1019
COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
1022
VERIFY(waitForSignal(spyPromptCompleted2, 1));
1024
auto args = spyPromptCompleted2.takeFirst();
1025
COMPARE(args.count(), 2);
1026
COMPARE(args.at(0).toBool(), false);
1027
COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
1031
auto reply = coll->locked();
1032
VERIFY(reply.isFinished() && reply.isError());
1033
COMPARE(reply.error().type(), QDBusError::UnknownObject);
1037
void TestGuiFdoSecrets::testCollectionChange()
1039
auto service = enableService();
1041
auto coll = getDefaultCollection(service);
1044
QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
1045
VERIFY(spyCollectionChanged.isValid());
1047
DBUS_VERIFY(coll->setLabel("anotherLabel"));
1048
COMPARE(m_db->metadata()->name(), QStringLiteral("anotherLabel"));
1049
QTRY_COMPARE(spyCollectionChanged.size(), 1);
1051
auto args = spyCollectionChanged.takeFirst();
1052
COMPARE(args.size(), 1);
1053
COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
1057
void TestGuiFdoSecrets::testHiddenFilename()
1059
// when file name contains leading dot, all parts excepting the last should be used
1060
// for collection name, and the registration should success
1061
VERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx"));
1063
// reset is necessary to not hold database longer and cause connections
1064
// not cleaned up when the database tab is closed.
1066
VERIFY(m_tabWidget->closeAllDatabaseTabs());
1067
m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
1068
m_dbWidget = m_tabWidget->currentDatabaseWidget();
1069
m_db = m_dbWidget->database();
1071
// enable the service
1072
auto service = enableService();
1075
// collection is properly registered
1076
auto coll = getDefaultCollection(service);
1077
auto collObj = m_plugin->dbus()->pathToObject<Collection>(QDBusObjectPath(coll->path()));
1079
COMPARE(collObj->name(), QStringLiteral(".Name"));
1082
void TestGuiFdoSecrets::testDuplicateName()
1085
VERIFY(dir.isValid());
1086
// create another file under different path but with the same filename
1087
QString anotherFile = dir.path() + "/" + QFileInfo(*m_dbFile).fileName();
1088
m_dbFile->copy(anotherFile);
1089
m_tabWidget->addDatabaseTab(anotherFile, false, "a");
1091
auto service = enableService();
1094
// when two databases have the same name, one of it will have part of its uuid suffixed
1095
const QString pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC");
1096
DBUS_GET(colls, service->collections());
1097
COMPARE(colls.size(), 2);
1098
COMPARE(colls[0].path(), pathNoSuffix);
1099
VERIFY(colls[1].path() != pathNoSuffix);
1102
void TestGuiFdoSecrets::testItemCreate()
1104
auto service = enableService();
1106
auto coll = getDefaultCollection(service);
1108
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1111
QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
1112
VERIFY(spyItemCreated.isValid());
1115
StringStringMap attributes{
1116
{"application", "fdosecrets-test"},
1117
{"attr-i[bute]", "![some] -value*"},
1120
auto item = createItem(sess, coll, "abc", "Password", attributes, false);
1125
VERIFY(waitForSignal(spyItemCreated, 1));
1126
auto args = spyItemCreated.takeFirst();
1127
COMPARE(args.size(), 1);
1128
COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
1133
DBUS_GET(actual, item->attributes());
1134
for (const auto& key : attributes.keys()) {
1135
COMPARE(actual[key], attributes[key]);
1140
DBUS_COMPARE(item->label(), QStringLiteral("abc"));
1144
DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
1145
auto decrypted = m_clientCipher->decrypt(ss.unmarshal(m_plugin->dbus()));
1146
COMPARE(decrypted.value, QByteArrayLiteral("Password"));
1151
DBUS_GET2(unlocked, locked, service->SearchItems(attributes));
1152
COMPARE(locked, {});
1153
COMPARE(unlocked, {QDBusObjectPath(item->path())});
1156
DBUS_GET(unlocked, coll->SearchItems(attributes));
1157
VERIFY(unlocked.contains(QDBusObjectPath(item->path())));
1161
void TestGuiFdoSecrets::testItemCreateUnlock()
1163
auto service = enableService();
1165
auto coll = getDefaultCollection(service);
1167
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1170
// NOTE: entries are no longer valid after locking
1171
lockDatabaseInBackend();
1173
QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
1174
VERIFY(spyItemCreated.isValid());
1177
StringStringMap attributes{
1178
{"application", "fdosecrets-test"},
1179
{"attr-i[bute]", "![some] -value*"},
1182
auto item = createItem(sess, coll, "abc", "Password", attributes, false, false, true);
1186
void TestGuiFdoSecrets::testItemChange()
1188
auto service = enableService();
1190
auto coll = getDefaultCollection(service);
1192
auto item = getFirstItem(coll);
1194
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1196
auto entry = itemObj->backend();
1199
QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath)));
1200
VERIFY(spyItemChanged.isValid());
1202
DBUS_VERIFY(item->setLabel("anotherLabel"));
1203
COMPARE(entry->title(), QStringLiteral("anotherLabel"));
1204
QTRY_VERIFY(!spyItemChanged.isEmpty());
1205
for (const auto& args : spyItemChanged) {
1206
COMPARE(args.size(), 1);
1207
COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
1210
spyItemChanged.clear();
1211
DBUS_VERIFY(item->setAttributes({
1214
COMPARE(entry->attributes()->value("abc"), QStringLiteral("def"));
1215
QTRY_VERIFY(!spyItemChanged.isEmpty());
1216
for (const auto& args : spyItemChanged) {
1217
COMPARE(args.size(), 1);
1218
COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
1222
void TestGuiFdoSecrets::testItemReplace()
1224
auto service = enableService();
1226
auto coll = getDefaultCollection(service);
1228
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1232
StringStringMap attr1{
1233
{"application", "fdosecrets-test"},
1234
{"attr-i[bute]", "![some] -value*"},
1235
{"fdosecrets-attr", "1"},
1237
StringStringMap attr2{
1238
{"application", "fdosecrets-test"},
1239
{"attr-i[bute]", "![some] -value*"},
1240
{"fdosecrets-attr", "2"},
1243
auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false);
1245
auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false);
1249
DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
1250
QSet<QDBusObjectPath> expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())};
1251
COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
1254
QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
1255
VERIFY(spyItemCreated.isValid());
1256
QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath)));
1257
VERIFY(spyItemChanged.isValid());
1260
// when replace, existing item with matching attr is updated
1261
auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true);
1263
COMPARE(item2->path(), item3->path());
1264
DBUS_COMPARE(item3->label(), QStringLiteral("abc3"));
1265
// there are still 2 entries
1266
DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
1267
QSet<QDBusObjectPath> expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())};
1268
COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
1270
VERIFY(waitForSignal(spyItemCreated, 0));
1271
// there may be multiple changed signals, due to each item attribute is set separately
1272
QTRY_VERIFY(!spyItemChanged.isEmpty());
1273
for (const auto& args : spyItemChanged) {
1274
COMPARE(args.size(), 1);
1275
COMPARE(args.at(0).value<QDBusObjectPath>().path(), item3->path());
1279
spyItemCreated.clear();
1280
spyItemChanged.clear();
1282
// when NOT replace, another entry is created
1283
auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false);
1285
DBUS_COMPARE(item2->label(), QStringLiteral("abc3"));
1286
DBUS_COMPARE(item4->label(), QStringLiteral("abc4"));
1287
// there are 3 entries
1288
DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
1289
QSet<QDBusObjectPath> expected{
1290
QDBusObjectPath(item1->path()),
1291
QDBusObjectPath(item2->path()),
1292
QDBusObjectPath(item4->path()),
1294
COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
1296
VERIFY(waitForSignal(spyItemCreated, 1));
1298
auto args = spyItemCreated.takeFirst();
1299
COMPARE(args.size(), 1);
1300
COMPARE(args.at(0).value<QDBusObjectPath>().path(), item4->path());
1302
// there may be multiple changed signals, due to each item attribute is set separately
1303
VERIFY(!spyItemChanged.isEmpty());
1304
for (const auto& args : spyItemChanged) {
1305
COMPARE(args.size(), 1);
1306
COMPARE(args.at(0).value<QDBusObjectPath>().path(), item4->path());
1311
void TestGuiFdoSecrets::testItemReplaceExistingLocked()
1313
auto service = enableService();
1315
auto coll = getDefaultCollection(service);
1317
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1321
StringStringMap attr1{
1322
{"application", "fdosecrets-test"},
1323
{"attr-i[bute]", "![some] -value*"},
1324
{"fdosecrets-attr", "1"},
1327
auto item = createItem(sess, coll, "abc1", "Password", attr1, false);
1330
// make sure the item is locked
1332
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1334
auto entry = itemObj->backend();
1336
FdoSecrets::settings()->setConfirmAccessItem(true);
1337
m_client->setItemAuthorized(entry->uuid(), AuthDecision::Undecided);
1338
DBUS_COMPARE(item->locked(), true);
1341
// when replace with a locked item, there will be a prompt
1342
auto item2 = createItem(sess, coll, "abc2", "PasswordUpdated", attr1, true, true);
1344
COMPARE(item2->path(), item->path());
1345
DBUS_COMPARE(item2->label(), QStringLiteral("abc2"));
1348
void TestGuiFdoSecrets::testItemSecret()
1350
const QString TEXT_PLAIN = "text/plain";
1351
const QString APPLICATION_OCTET_STREAM = "application/octet-stream";
1353
auto service = enableService();
1355
auto coll = getDefaultCollection(service);
1357
auto item = getFirstItem(coll);
1359
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1362
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1364
auto entry = itemObj->backend();
1367
// plain text secret
1369
DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
1370
auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
1371
COMPARE(ss.contentType, TEXT_PLAIN);
1372
COMPARE(ss.value, entry->password().toUtf8());
1375
// get secret with notification
1376
FdoSecrets::settings()->setShowNotification(true);
1378
QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int)));
1379
VERIFY(spyShowNotification.isValid());
1381
DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
1382
auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
1383
COMPARE(ss.contentType, TEXT_PLAIN);
1384
COMPARE(ss.value, entry->password().toUtf8());
1386
COMPARE(ss.contentType, TEXT_PLAIN);
1387
COMPARE(ss.value, entry->password().toUtf8());
1389
VERIFY(waitForSignal(spyShowNotification, 1));
1391
FdoSecrets::settings()->setShowNotification(false);
1393
// set secret with plain text
1395
// first create Secret in wire format,
1396
// then convert to internal format and encrypt
1397
// finally convert encrypted internal format back to wire format to pass to SetSecret
1398
const QByteArray expected = QByteArrayLiteral("NewPassword");
1399
auto encrypted = encryptPassword(expected, TEXT_PLAIN, sess);
1400
DBUS_VERIFY(item->SetSecret(encrypted));
1401
COMPARE(entry->password().toUtf8(), expected);
1404
// set secret with something else is saved as attachment
1405
const QByteArray expected = QByteArrayLiteral("NewPasswordBinary");
1407
auto encrypted = encryptPassword(expected, APPLICATION_OCTET_STREAM, sess);
1408
DBUS_VERIFY(item->SetSecret(encrypted));
1409
COMPARE(entry->password(), QStringLiteral(""));
1412
DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
1413
auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
1414
COMPARE(ss.contentType, APPLICATION_OCTET_STREAM);
1415
COMPARE(ss.value, expected);
1419
void TestGuiFdoSecrets::testItemDelete()
1421
FdoSecrets::settings()->setConfirmDeleteItem(true);
1423
auto service = enableService();
1425
auto coll = getDefaultCollection(service);
1427
auto item = getFirstItem(coll);
1429
// save the path which will be gone after the deletion.
1430
auto itemPath = item->path();
1432
QSignalSpy spyItemDeleted(coll.data(), SIGNAL(ItemDeleted(QDBusObjectPath)));
1433
VERIFY(spyItemDeleted.isValid());
1435
DBUS_GET(promptPath, item->Delete());
1436
auto prompt = getProxy<PromptProxy>(promptPath);
1439
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
1440
VERIFY(spyPromptCompleted.isValid());
1442
// prompt and click save
1443
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1445
MessageBox::setNextAnswer(MessageBox::Delete);
1446
DBUS_VERIFY(prompt->Prompt(""));
1448
VERIFY(waitForSignal(spyPromptCompleted, 1));
1449
auto args = spyPromptCompleted.takeFirst();
1450
COMPARE(args.count(), 2);
1451
COMPARE(args.at(0).toBool(), false);
1452
COMPARE(args.at(1).toString(), QStringLiteral(""));
1454
VERIFY(waitForSignal(spyItemDeleted, 1));
1455
args = spyItemDeleted.takeFirst();
1456
COMPARE(args.size(), 1);
1457
COMPARE(args.at(0).value<QDBusObjectPath>().path(), itemPath);
1460
void TestGuiFdoSecrets::testItemLockState()
1462
auto service = enableService();
1464
auto coll = getDefaultCollection(service);
1466
auto item = getFirstItem(coll);
1468
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1470
auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
1472
auto entry = itemObj->backend();
1477
QDBusObjectPath(sess->path()),
1482
.unmarshal(m_plugin->dbus());
1483
auto encrypted = m_clientCipher->encrypt(secret).marshal();
1485
// when access confirmation is disabled, item is unlocked when the collection is unlocked
1486
FdoSecrets::settings()->setConfirmAccessItem(false);
1487
DBUS_COMPARE(item->locked(), false);
1489
// when access confirmation is enabled, item is locked if the client has no authorization
1490
FdoSecrets::settings()->setConfirmAccessItem(true);
1491
DBUS_COMPARE(item->locked(), true);
1492
// however, item properties are still accessible as long as the collection is unlocked
1493
DBUS_VERIFY(item->attributes());
1494
DBUS_VERIFY(item->setAttributes({}));
1495
DBUS_VERIFY(item->label());
1496
DBUS_VERIFY(item->setLabel("abc"));
1497
DBUS_VERIFY(item->created());
1498
DBUS_VERIFY(item->modified());
1499
// except secret, which is locked
1501
auto reply = item->GetSecret(QDBusObjectPath(sess->path()));
1502
VERIFY(reply.isError());
1503
COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED);
1506
auto reply = item->SetSecret(encrypted);
1507
VERIFY(reply.isError());
1508
COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED);
1511
// item is unlocked if the client is authorized
1512
m_client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed);
1513
DBUS_COMPARE(item->locked(), false);
1514
DBUS_VERIFY(item->GetSecret(QDBusObjectPath(sess->path())));
1515
DBUS_VERIFY(item->SetSecret(encrypted));
1518
void TestGuiFdoSecrets::testItemRejectSetReferenceFields()
1520
// expose a subgroup, entries in it should not be able to retrieve data from entries outside it
1521
auto rootEntry = m_db->rootGroup()->entries().first();
1523
auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
1525
FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
1526
auto service = enableService();
1528
auto coll = getDefaultCollection(service);
1530
auto item = getFirstItem(coll);
1532
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
1535
const auto refText = QStringLiteral("{REF:P@T:%1}").arg(rootEntry->title());
1537
// reject ref in label
1539
auto reply = item->setLabel(refText);
1540
VERIFY(reply.isFinished() && reply.isError());
1541
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1543
// reject ref in custom attributes
1545
auto reply = item->setAttributes({{"steal", refText}});
1546
VERIFY(reply.isFinished() && reply.isError());
1547
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1549
// reject ref in password
1551
auto reply = item->SetSecret(encryptPassword(refText.toUtf8(), "text/plain", sess));
1552
VERIFY(reply.isFinished() && reply.isError());
1553
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1555
// reject ref in content type
1557
auto reply = item->SetSecret(encryptPassword("dummy", refText, sess));
1558
VERIFY(reply.isFinished() && reply.isError());
1559
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
1563
void TestGuiFdoSecrets::testAlias()
1565
auto service = enableService();
1568
// read default alias
1569
DBUS_GET(collPath, service->ReadAlias("default"));
1570
auto coll = getProxy<CollectionProxy>(collPath);
1574
DBUS_VERIFY(service->SetAlias("another", QDBusObjectPath(collPath)));
1576
// get using extra alias
1577
DBUS_GET(collPath2, service->ReadAlias("another"));
1578
COMPARE(collPath2, collPath);
1581
void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent()
1583
auto service = enableService();
1586
// one collection, which is default alias
1587
auto coll = getDefaultCollection(service);
1590
// after locking, the collection is still there, but locked
1591
lockDatabaseInBackend();
1593
coll = getDefaultCollection(service);
1595
DBUS_COMPARE(coll->locked(), true);
1597
// unlock the database, the alias and collection is present
1598
unlockDatabaseInBackend();
1600
coll = getDefaultCollection(service);
1602
DBUS_COMPARE(coll->locked(), false);
1605
void TestGuiFdoSecrets::testExposeSubgroup()
1607
auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
1609
FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
1610
auto service = enableService();
1613
auto coll = getDefaultCollection(service);
1616
// exposing subgroup does not expose entries in other groups
1617
DBUS_GET(itemPaths, coll->items());
1618
QSet<Entry*> exposedEntries;
1619
for (const auto& itemPath : itemPaths) {
1620
exposedEntries << m_plugin->dbus()->pathToObject<Item>(itemPath)->backend();
1622
COMPARE(exposedEntries, QSet<Entry*>::fromList(subgroup->entries()));
1625
void TestGuiFdoSecrets::testModifyingExposedGroup()
1627
// test when exposed group is removed the collection is not exposed anymore
1628
auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking");
1630
FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
1631
auto service = enableService();
1635
DBUS_GET(collPaths, service->collections());
1636
COMPARE(collPaths.size(), 1);
1639
m_db->metadata()->setRecycleBinEnabled(true);
1640
m_db->recycleGroup(subgroup);
1644
DBUS_GET(collPaths, service->collections());
1645
COMPARE(collPaths, {});
1648
// test setting another exposed group, the collection will be exposed again
1649
FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
1652
DBUS_GET(collPaths, service->collections());
1653
COMPARE(collPaths.size(), 1);
1657
void TestGuiFdoSecrets::testNoExposeRecycleBin()
1659
// when the recycle bin is underneath the exposed group
1660
// be careful not to expose entries in there
1662
FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
1663
m_db->metadata()->setRecycleBinEnabled(true);
1665
auto entry = m_db->rootGroup()->entries().first();
1667
m_db->recycleEntry(entry);
1670
auto service = enableService();
1673
auto coll = getDefaultCollection(service);
1676
// exposing subgroup does not expose entries in other groups
1677
DBUS_GET(itemPaths, coll->items());
1678
QSet<Entry*> exposedEntries;
1679
for (const auto& itemPath : itemPaths) {
1680
exposedEntries << m_plugin->dbus()->pathToObject<Item>(itemPath)->backend();
1682
VERIFY(!exposedEntries.contains(entry));
1684
// searching should not return the entry
1685
DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
1686
COMPARE(locked, {});
1687
COMPARE(unlocked, {});
1690
void TestGuiFdoSecrets::lockDatabaseInBackend()
1692
m_tabWidget->lockDatabases();
1697
void TestGuiFdoSecrets::unlockDatabaseInBackend()
1699
m_dbWidget->performUnlockDatabase("a");
1700
m_db = m_dbWidget->database();
1704
void TestGuiFdoSecrets::processEvents()
1706
// Couldn't use QApplication::processEvents, because per Qt documentation:
1707
// events that are posted while the function runs will be queued until a later round of event processing.
1708
// and we may post QTimer single shot events during event handling to achieve async method.
1709
// So we directly call event dispatcher in a loop until no events can be handled
1710
while (QAbstractEventDispatcher::instance()->processEvents(QEventLoop::AllEvents)) {
1715
// the following functions have return value, switch macros to the version supporting that
1719
#define VERIFY(stmt) VERIFY2_RET(stmt, "")
1720
#define VERIFY2 VERIFY2_RET
1721
#define COMPARE COMPARE_RET
1723
QSharedPointer<ServiceProxy> TestGuiFdoSecrets::enableService()
1725
FdoSecrets::settings()->setEnabled(true);
1727
m_plugin->updateServiceState();
1728
return getProxy<ServiceProxy>(QDBusObjectPath(DBUS_PATH_SECRETS));
1731
QSharedPointer<SessionProxy> TestGuiFdoSecrets::openSession(const QSharedPointer<ServiceProxy>& service,
1732
const QString& algo)
1736
if (algo == PlainCipher::Algorithm) {
1737
DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant("")));
1739
return getProxy<SessionProxy>(sessPath);
1740
} else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) {
1741
DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(m_clientCipher->negotiationOutput())));
1742
m_clientCipher->updateClientPublicKey(output.variant().toByteArray());
1743
return getProxy<SessionProxy>(sessPath);
1745
QTest::qFail("Unsupported algorithm", __FILE__, __LINE__);
1749
QSharedPointer<CollectionProxy> TestGuiFdoSecrets::getDefaultCollection(const QSharedPointer<ServiceProxy>& service)
1752
DBUS_GET(collPath, service->ReadAlias("default"));
1753
return getProxy<CollectionProxy>(collPath);
1756
QSharedPointer<ItemProxy> TestGuiFdoSecrets::getFirstItem(const QSharedPointer<CollectionProxy>& coll)
1759
DBUS_GET(itemPaths, coll->items());
1760
VERIFY(!itemPaths.isEmpty());
1761
return getProxy<ItemProxy>(itemPaths.first());
1764
QSharedPointer<ItemProxy> TestGuiFdoSecrets::createItem(const QSharedPointer<SessionProxy>& sess,
1765
const QSharedPointer<CollectionProxy>& coll,
1766
const QString& label,
1767
const QString& pass,
1768
const StringStringMap& attr,
1771
bool expectUnlockPrompt)
1776
QVariantMap properties{
1777
{DBUS_INTERFACE_SECRET_ITEM + ".Label", QVariant::fromValue(label)},
1778
{DBUS_INTERFACE_SECRET_ITEM + ".Attributes", QVariant::fromValue(attr)},
1782
ss.session = QDBusObjectPath(sess->path());
1783
ss.value = pass.toLocal8Bit();
1784
ss.contentType = "plain/text";
1785
auto encrypted = m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
1787
DBUS_GET2(itemPath, promptPath, coll->CreateItem(properties, encrypted, replace));
1789
auto prompt = getProxy<PromptProxy>(promptPath);
1791
QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
1792
VERIFY(spyPromptCompleted.isValid());
1795
DBUS_VERIFY(prompt->Prompt(""));
1797
bool unlockFound = driveUnlockDialog();
1798
COMPARE(unlockFound, expectUnlockPrompt);
1800
bool found = driveAccessControlDialog();
1801
COMPARE(found, expectPrompt);
1803
VERIFY(waitForSignal(spyPromptCompleted, 1));
1804
auto args = spyPromptCompleted.takeFirst();
1805
COMPARE(args.size(), 2);
1806
COMPARE(args.at(0).toBool(), false);
1807
itemPath = getSignalVariantArgument<QDBusObjectPath>(args.at(1));
1809
return getProxy<ItemProxy>(itemPath);
1812
FdoSecrets::wire::Secret
1813
TestGuiFdoSecrets::encryptPassword(QByteArray value, QString contentType, const QSharedPointer<SessionProxy>& sess)
1816
ss.contentType = std::move(contentType);
1817
ss.value = std::move(value);
1818
ss.session = QDBusObjectPath(sess->path());
1819
return m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
1822
bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember, bool includeFutureEntries)
1825
for (auto w : QApplication::topLevelWidgets()) {
1826
if (!w->isWindow()) {
1829
auto dlg = qobject_cast<AccessControlDialog*>(w);
1830
if (dlg && dlg->isVisible()) {
1831
auto rememberCheck = dlg->findChild<QCheckBox*>("rememberCheck");
1832
VERIFY(rememberCheck);
1833
rememberCheck->setChecked(remember);
1835
if (includeFutureEntries) {
1836
dlg->done(AccessControlDialog::AllowAll);
1838
dlg->done(AccessControlDialog::AllowSelected);
1842
VERIFY(dlg->isHidden());
1849
bool TestGuiFdoSecrets::driveNewDatabaseWizard()
1851
// processEvents will block because the NewDatabaseWizard is shown using exec
1852
// which creates a local QEventLoop.
1853
// so we do a little trick here to get the return value back
1855
QTimer::singleShot(0, this, [this, &ret]() {
1856
ret = [this]() -> bool {
1857
auto wizard = m_tabWidget->findChild<NewDatabaseWizard*>();
1860
COMPARE(wizard->currentId(), 0);
1863
COMPARE(wizard->currentId(), 2);
1866
auto* passwordEdit =
1867
wizard->findChild<PasswordWidget*>("enterPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
1868
auto* passwordRepeatEdit =
1869
wizard->findChild<PasswordWidget*>("repeatPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
1870
VERIFY(passwordEdit);
1871
VERIFY(passwordRepeatEdit);
1872
QTest::keyClicks(passwordEdit, "test");
1873
QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
1874
QTest::keyClicks(passwordRepeatEdit, "test");
1876
// save database to temporary file
1877
TemporaryFile tmpFile;
1878
VERIFY(tmpFile.open());
1880
fileDialog()->setNextFileName(tmpFile.fileName());
1882
// click Continue on the warning due to weak password
1883
MessageBox::setNextAnswer(MessageBox::ContinueWithWeakPass);
1894
bool TestGuiFdoSecrets::driveUnlockDialog(DatabaseWidget* target)
1897
auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>();
1899
if (!dbOpenDlg->isVisible()) {
1902
dbOpenDlg->setActiveDatabaseTab(target);
1904
auto editPassword = dbOpenDlg->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
1905
VERIFY(editPassword);
1906
editPassword->setFocus();
1907
QTest::keyClicks(editPassword, "a");
1908
QTest::keyClick(editPassword, Qt::Key_Enter);
1913
bool TestGuiFdoSecrets::waitForSignal(QSignalSpy& spy, int expectedCount)
1916
// If already expected count, do not wait and return immediately
1917
if (spy.count() == expectedCount) {
1919
} else if (spy.count() > expectedCount) {
1923
COMPARE(spy.count(), expectedCount);
1928
#define VERIFY QVERIFY
1930
#define COMPARE QCOMPARE