TestGuiFdoSecrets.cpp 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931
  1. /*
  2. * Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
  3. *
  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.
  8. *
  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.
  13. *
  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/>.
  16. */
  17. #include "TestGuiFdoSecrets.h"
  18. #include "fdosecrets/FdoSecretsPlugin.h"
  19. #include "fdosecrets/FdoSecretsSettings.h"
  20. #include "fdosecrets/objects/Collection.h"
  21. #include "fdosecrets/objects/Item.h"
  22. #include "fdosecrets/objects/SessionCipher.h"
  23. #include "fdosecrets/widgets/AccessControlDialog.h"
  24. #include "config-keepassx-tests.h"
  25. #include "core/Tools.h"
  26. #include "crypto/Crypto.h"
  27. #include "gui/Application.h"
  28. #include "gui/DatabaseTabWidget.h"
  29. #include "gui/FileDialog.h"
  30. #include "gui/MainWindow.h"
  31. #include "gui/MessageBox.h"
  32. #include "gui/PasswordWidget.h"
  33. #include "gui/wizard/NewDatabaseWizard.h"
  34. #include "util/FdoSecretsProxy.h"
  35. #include "util/TemporaryFile.h"
  36. #include <QCheckBox>
  37. #include <QLineEdit>
  38. #include <QSignalSpy>
  39. #include <QTest>
  40. #include <utility>
  41. int main(int argc, char* argv[])
  42. {
  43. #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
  44. QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
  45. QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
  46. #endif
  47. Application app(argc, argv);
  48. app.setApplicationName("KeePassXC");
  49. app.setApplicationVersion(KEEPASSXC_VERSION);
  50. app.setQuitOnLastWindowClosed(false);
  51. app.setAttribute(Qt::AA_Use96Dpi, true);
  52. app.applyTheme();
  53. QTEST_DISABLE_KEYPAD_NAVIGATION
  54. TestGuiFdoSecrets tc;
  55. QTEST_SET_MAIN_SOURCE_PATH
  56. return QTest::qExec(&tc, argc, argv);
  57. }
  58. #define DBUS_PATH_DEFAULT_ALIAS "/org/freedesktop/secrets/aliases/default"
  59. // assert macros compatible with function having return values
  60. #define VERIFY2_RET(statement, msg) \
  61. do { \
  62. if (!QTest::qVerify(static_cast<bool>(statement), #statement, (msg), __FILE__, __LINE__)) \
  63. return {}; \
  64. } while (false)
  65. #define COMPARE_RET(actual, expected) \
  66. do { \
  67. if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
  68. return {}; \
  69. } while (false)
  70. // by default use these with Qt macros
  71. #define VERIFY QVERIFY
  72. #define COMPARE QCOMPARE
  73. #define VERIFY2 QVERIFY2
  74. #define DBUS_COMPARE(actual, expected) \
  75. do { \
  76. auto reply = (actual); \
  77. VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \
  78. COMPARE(reply.value(), (expected)); \
  79. } while (false)
  80. #define DBUS_VERIFY(stmt) \
  81. do { \
  82. auto reply = (stmt); \
  83. VERIFY2(reply.isValid(), reply.error().name().toLocal8Bit()); \
  84. } while (false)
  85. #define DBUS_GET(var, stmt) \
  86. std::remove_cv<decltype((stmt).argumentAt<0>())>::type var; \
  87. do { \
  88. const auto rep = (stmt); \
  89. VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \
  90. var = rep.argumentAt<0>(); \
  91. } while (false)
  92. #define DBUS_GET2(name1, name2, stmt) \
  93. std::remove_cv<decltype((stmt).argumentAt<0>())>::type name1; \
  94. std::remove_cv<decltype((stmt).argumentAt<1>())>::type name2; \
  95. do { \
  96. const auto rep = (stmt); \
  97. VERIFY2(rep.isValid(), rep.error().name().toLocal8Bit()); \
  98. name1 = rep.argumentAt<0>(); \
  99. name2 = rep.argumentAt<1>(); \
  100. } while (false)
  101. using namespace FdoSecrets;
  102. class FakeClient : public DBusClient
  103. {
  104. public:
  105. explicit FakeClient(DBusMgr* dbus)
  106. : DBusClient(
  107. dbus,
  108. {QStringLiteral("local"), 0, true, {ProcInfo{0, 0, QStringLiteral("fake-client"), QString{}, QString{}}}})
  109. {
  110. }
  111. };
  112. // pretty print QDBusObjectPath in QCOMPARE
  113. char* toString(const QDBusObjectPath& path)
  114. {
  115. return QTest::toString("ObjectPath(" + path.path() + ")");
  116. }
  117. TestGuiFdoSecrets::~TestGuiFdoSecrets() = default;
  118. void TestGuiFdoSecrets::initTestCase()
  119. {
  120. VERIFY(Crypto::init());
  121. Config::createTempFileInstance();
  122. config()->set(Config::AutoSaveAfterEveryChange, false);
  123. config()->set(Config::AutoSaveOnExit, false);
  124. config()->set(Config::GUI_ShowTrayIcon, true);
  125. config()->set(Config::UpdateCheckMessageShown, true);
  126. // Disable quick unlock
  127. config()->set(Config::Security_QuickUnlock, false);
  128. // Disable secret service integration (activate within individual tests to test the plugin)
  129. FdoSecrets::settings()->setEnabled(false);
  130. // activate within individual tests
  131. FdoSecrets::settings()->setShowNotification(false);
  132. Application::bootstrap();
  133. m_mainWindow.reset(new MainWindow());
  134. m_tabWidget = m_mainWindow->findChild<DatabaseTabWidget*>("tabWidget");
  135. VERIFY(m_tabWidget);
  136. m_plugin = FdoSecretsPlugin::getPlugin();
  137. VERIFY(m_plugin);
  138. m_mainWindow->show();
  139. auto key = QByteArray::fromHex("e407997e8b918419cf851cf3345358fdf"
  140. "ffb9564a220ac9c3934efd277cea20d17"
  141. "467ecdc56e817f75ac39501f38a4a04ff"
  142. "64d627e16c09981c7ad876da255b61c8e"
  143. "6a8408236c2a4523cfe6961c26dbdfc77"
  144. "c1a27a5b425ca71a019e829fae32c0b42"
  145. "0e1b3096b48bc2ce9ccab1d1ff13a5eb4"
  146. "b263cee30bdb1a57af9bfa93f");
  147. m_clientCipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(key));
  148. // Load the NewDatabase.kdbx file into temporary storage
  149. QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase.kdbx"));
  150. VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
  151. VERIFY(Tools::readAllFromDevice(&sourceDbFile, m_dbData));
  152. sourceDbFile.close();
  153. // set a fake dbus client all the time so we can freely access DBusMgr anywhere
  154. m_client.reset(new FakeClient(m_plugin->dbus().data()));
  155. m_plugin->dbus()->overrideClient(m_client);
  156. }
  157. // Every test starts with opening the temp database
  158. void TestGuiFdoSecrets::init()
  159. {
  160. m_dbFile.reset(new TemporaryFile());
  161. // Write the temp storage to a temp database file for use in our tests
  162. VERIFY(m_dbFile->open());
  163. COMPARE(m_dbFile->write(m_dbData), static_cast<qint64>(m_dbData.size()));
  164. m_dbFile->close();
  165. // make sure window is activated or focus tests may fail
  166. m_mainWindow->activateWindow();
  167. processEvents();
  168. // open and unlock the database
  169. m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
  170. m_dbWidget = m_tabWidget->currentDatabaseWidget();
  171. m_db = m_dbWidget->database();
  172. // by default expose the root group
  173. FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
  174. VERIFY(m_dbWidget->save());
  175. // enforce consistent default settings at the beginning
  176. FdoSecrets::settings()->setUnlockBeforeSearch(false);
  177. FdoSecrets::settings()->setShowNotification(false);
  178. FdoSecrets::settings()->setConfirmAccessItem(false);
  179. FdoSecrets::settings()->setEnabled(false);
  180. }
  181. // Every test ends with closing the temp database without saving
  182. void TestGuiFdoSecrets::cleanup()
  183. {
  184. // restore to default settings
  185. FdoSecrets::settings()->setUnlockBeforeSearch(false);
  186. FdoSecrets::settings()->setShowNotification(false);
  187. FdoSecrets::settings()->setConfirmAccessItem(false);
  188. FdoSecrets::settings()->setEnabled(false);
  189. if (m_plugin) {
  190. m_plugin->updateServiceState();
  191. }
  192. // DO NOT save the database
  193. for (int i = 0; i != m_tabWidget->count(); ++i) {
  194. m_tabWidget->databaseWidgetFromIndex(i)->database()->markAsClean();
  195. }
  196. // Close any dialogs
  197. while (auto w = QApplication::activeModalWidget()) {
  198. w->close();
  199. }
  200. VERIFY(m_tabWidget->closeAllDatabaseTabs());
  201. processEvents();
  202. if (m_dbFile) {
  203. m_dbFile->remove();
  204. }
  205. m_client->clearAuthorization();
  206. }
  207. void TestGuiFdoSecrets::cleanupTestCase()
  208. {
  209. m_plugin->dbus()->overrideClient({});
  210. if (m_dbFile) {
  211. m_dbFile->remove();
  212. }
  213. }
  214. void TestGuiFdoSecrets::testServiceEnable()
  215. {
  216. QSignalSpy sigError(m_plugin, SIGNAL(error(QString)));
  217. VERIFY(sigError.isValid());
  218. QSignalSpy sigStarted(m_plugin, SIGNAL(secretServiceStarted()));
  219. VERIFY(sigStarted.isValid());
  220. // make sure no one else is holding the service
  221. VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
  222. // enable the service
  223. auto service = enableService();
  224. VERIFY(service);
  225. // service started without error
  226. VERIFY(sigError.isEmpty());
  227. COMPARE(sigStarted.size(), 1);
  228. processEvents();
  229. VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(DBUS_SERVICE_SECRET));
  230. // there will be one default collection
  231. auto coll = getDefaultCollection(service);
  232. VERIFY(coll);
  233. DBUS_COMPARE(coll->locked(), false);
  234. DBUS_COMPARE(coll->label(), m_db->metadata()->name());
  235. DBUS_COMPARE(coll->created(),
  236. static_cast<qulonglong>(m_db->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000));
  237. DBUS_COMPARE(
  238. coll->modified(),
  239. static_cast<qulonglong>(m_db->rootGroup()->timeInfo().lastModificationTime().toMSecsSinceEpoch() / 1000));
  240. }
  241. void TestGuiFdoSecrets::testServiceEnableNoExposedDatabase()
  242. {
  243. // reset the exposed group and then enable the service
  244. FdoSecrets::settings()->setExposedGroup(m_db, {});
  245. auto service = enableService();
  246. VERIFY(service);
  247. // no collections
  248. DBUS_COMPARE(service->collections(), QList<QDBusObjectPath>{});
  249. }
  250. void TestGuiFdoSecrets::testServiceSearch()
  251. {
  252. auto service = enableService();
  253. VERIFY(service);
  254. auto coll = getDefaultCollection(service);
  255. VERIFY(coll);
  256. auto item = getFirstItem(coll);
  257. VERIFY(item);
  258. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  259. VERIFY(itemObj);
  260. auto entry = itemObj->backend();
  261. VERIFY(entry);
  262. entry->attributes()->set("fdosecrets-test", "1");
  263. entry->attributes()->set("fdosecrets-test-protected", "2", true);
  264. const QString crazyKey = "_a:bc&-+'-e%12df_d";
  265. const QString crazyValue = "[v]al@-ue";
  266. entry->attributes()->set(crazyKey, crazyValue);
  267. // search by title
  268. {
  269. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
  270. COMPARE(locked, {});
  271. COMPARE(unlocked, {QDBusObjectPath(item->path())});
  272. }
  273. // search by attribute
  274. {
  275. DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test", "1"}}));
  276. COMPARE(locked, {});
  277. COMPARE(unlocked, {QDBusObjectPath(item->path())});
  278. }
  279. {
  280. DBUS_GET2(unlocked, locked, service->SearchItems({{crazyKey, crazyValue}}));
  281. COMPARE(locked, {});
  282. COMPARE(unlocked, {QDBusObjectPath(item->path())});
  283. }
  284. // searching using empty terms returns nothing
  285. {
  286. DBUS_GET2(unlocked, locked, service->SearchItems({}));
  287. COMPARE(locked, {});
  288. COMPARE(unlocked, {});
  289. }
  290. // searching using protected attributes or password returns nothing
  291. {
  292. DBUS_GET2(unlocked, locked, service->SearchItems({{"Password", entry->password()}}));
  293. COMPARE(locked, {});
  294. COMPARE(unlocked, {});
  295. }
  296. {
  297. DBUS_GET2(unlocked, locked, service->SearchItems({{"fdosecrets-test-protected", "2"}}));
  298. COMPARE(locked, {});
  299. COMPARE(unlocked, {});
  300. }
  301. }
  302. void TestGuiFdoSecrets::testServiceSearchBlockingUnlock()
  303. {
  304. auto service = enableService();
  305. VERIFY(service);
  306. auto coll = getDefaultCollection(service);
  307. VERIFY(coll);
  308. auto entries = m_db->rootGroup()->entriesRecursive();
  309. VERIFY(!entries.isEmpty());
  310. // assumes the db is not empty
  311. auto title = entries.first()->title();
  312. // NOTE: entries are no longer valid after locking
  313. lockDatabaseInBackend();
  314. // when database is locked, nothing is returned
  315. FdoSecrets::settings()->setUnlockBeforeSearch(false);
  316. {
  317. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}}));
  318. COMPARE(locked, {});
  319. COMPARE(unlocked, {});
  320. }
  321. // when database is locked, nothing is returned
  322. FdoSecrets::settings()->setUnlockBeforeSearch(true);
  323. {
  324. // SearchItems will block because the blocking wait is implemented
  325. // using a local QEventLoop.
  326. // so we do a little trick here to get the return value back
  327. bool unlockDialogWorks = false;
  328. QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(); });
  329. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", title}}));
  330. VERIFY(unlockDialogWorks);
  331. COMPARE(locked, {});
  332. COMPARE(unlocked.size(), 1);
  333. auto item = getProxy<ItemProxy>(unlocked.first());
  334. DBUS_COMPARE(item->label(), title);
  335. }
  336. }
  337. void TestGuiFdoSecrets::testServiceSearchBlockingUnlockMultiple()
  338. {
  339. // setup: two databases, both locked, one with exposed db, the other not.
  340. // add another database tab with a database with no exposed group
  341. // to avoid modify the original, copy to a temp file first
  342. QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase2.kdbx"));
  343. QByteArray dbData;
  344. VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
  345. VERIFY(Tools::readAllFromDevice(&sourceDbFile, dbData));
  346. sourceDbFile.close();
  347. QTemporaryFile anotherFile;
  348. VERIFY(anotherFile.open());
  349. COMPARE(anotherFile.write(dbData), static_cast<qint64>(dbData.size()));
  350. anotherFile.close();
  351. m_tabWidget->addDatabaseTab(anotherFile.fileName(), false);
  352. auto anotherWidget = m_tabWidget->currentDatabaseWidget();
  353. auto service = enableService();
  354. VERIFY(service);
  355. // when there are multiple locked databases,
  356. // repeatly show the dialog until there is at least one unlocked collection
  357. FdoSecrets::settings()->setUnlockBeforeSearch(true);
  358. // when only unlocking the one with no exposed group, a second dialog is shown
  359. lockDatabaseInBackend();
  360. {
  361. bool unlockDialogWorks = false;
  362. QTimer::singleShot(50, [&]() {
  363. unlockDialogWorks = driveUnlockDialog(anotherWidget);
  364. QTimer::singleShot(50, [&]() { unlockDialogWorks &= driveUnlockDialog(); });
  365. });
  366. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
  367. VERIFY(unlockDialogWorks);
  368. COMPARE(locked, {});
  369. COMPARE(unlocked.size(), 1);
  370. }
  371. // when unlocking the one with exposed group, the other one remains locked
  372. lockDatabaseInBackend();
  373. {
  374. bool unlockDialogWorks = false;
  375. QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(m_dbWidget); });
  376. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
  377. VERIFY(unlockDialogWorks);
  378. COMPARE(locked, {});
  379. COMPARE(unlocked.size(), 1);
  380. VERIFY(anotherWidget->isLocked());
  381. }
  382. }
  383. void TestGuiFdoSecrets::testServiceSearchForce()
  384. {
  385. auto service = enableService();
  386. VERIFY(service);
  387. auto coll = getDefaultCollection(service);
  388. VERIFY(coll);
  389. auto item = getFirstItem(coll);
  390. VERIFY(item);
  391. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  392. VERIFY(itemObj);
  393. auto entry = itemObj->backend();
  394. VERIFY(entry);
  395. // fdosecrets should still find the item even if searching is disabled
  396. entry->group()->setSearchingEnabled(Group::Disable);
  397. // search by title
  398. {
  399. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
  400. COMPARE(locked, {});
  401. COMPARE(unlocked, {QDBusObjectPath(item->path())});
  402. }
  403. }
  404. void TestGuiFdoSecrets::testServiceUnlock()
  405. {
  406. lockDatabaseInBackend();
  407. auto service = enableService();
  408. VERIFY(service);
  409. auto coll = getDefaultCollection(service);
  410. VERIFY(coll);
  411. QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
  412. VERIFY(spyCollectionCreated.isValid());
  413. QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
  414. VERIFY(spyCollectionDeleted.isValid());
  415. QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
  416. VERIFY(spyCollectionChanged.isValid());
  417. DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())}));
  418. // nothing is unlocked immediately without user's action
  419. COMPARE(unlocked, {});
  420. auto prompt = getProxy<PromptProxy>(promptPath);
  421. VERIFY(prompt);
  422. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  423. VERIFY(spyPromptCompleted.isValid());
  424. // nothing is unlocked yet
  425. VERIFY(waitForSignal(spyPromptCompleted, 0));
  426. DBUS_COMPARE(coll->locked(), true);
  427. // show the prompt
  428. DBUS_VERIFY(prompt->Prompt(""));
  429. // still not unlocked before user action
  430. VERIFY(waitForSignal(spyPromptCompleted, 0));
  431. DBUS_COMPARE(coll->locked(), true);
  432. VERIFY(driveUnlockDialog());
  433. VERIFY(waitForSignal(spyPromptCompleted, 1));
  434. {
  435. auto args = spyPromptCompleted.takeFirst();
  436. COMPARE(args.size(), 2);
  437. COMPARE(args.at(0).toBool(), false);
  438. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
  439. }
  440. // check unlocked *AFTER* the prompt signal
  441. DBUS_COMPARE(coll->locked(), false);
  442. VERIFY(waitForSignal(spyCollectionCreated, 0));
  443. QTRY_VERIFY(!spyCollectionChanged.isEmpty());
  444. for (const auto& args : spyCollectionChanged) {
  445. COMPARE(args.size(), 1);
  446. COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
  447. }
  448. VERIFY(waitForSignal(spyCollectionDeleted, 0));
  449. }
  450. void TestGuiFdoSecrets::testServiceUnlockDatabaseConcurrent()
  451. {
  452. lockDatabaseInBackend();
  453. auto service = enableService();
  454. VERIFY(service);
  455. auto coll = getDefaultCollection(service);
  456. VERIFY(coll);
  457. DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(coll->path())}));
  458. auto prompt = getProxy<PromptProxy>(promptPath);
  459. VERIFY(prompt);
  460. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  461. VERIFY(spyPromptCompleted.isValid());
  462. DBUS_VERIFY(prompt->Prompt(""));
  463. // while the first prompt is running, another request come in
  464. DBUS_GET2(unlocked2, promptPath2, service->Unlock({QDBusObjectPath(coll->path())}));
  465. auto prompt2 = getProxy<PromptProxy>(promptPath2);
  466. VERIFY(prompt2);
  467. QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
  468. VERIFY(spyPromptCompleted2.isValid());
  469. DBUS_VERIFY(prompt2->Prompt(""));
  470. // there should be only one unlock dialog
  471. VERIFY(driveUnlockDialog());
  472. // both prompts should complete
  473. VERIFY(waitForSignal(spyPromptCompleted, 1));
  474. {
  475. auto args = spyPromptCompleted.takeFirst();
  476. COMPARE(args.size(), 2);
  477. COMPARE(args.at(0).toBool(), false);
  478. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
  479. }
  480. VERIFY(waitForSignal(spyPromptCompleted2, 1));
  481. {
  482. auto args = spyPromptCompleted2.takeFirst();
  483. COMPARE(args.size(), 2);
  484. COMPARE(args.at(0).toBool(), false);
  485. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
  486. }
  487. // check unlocked *AFTER* prompt signal
  488. DBUS_COMPARE(coll->locked(), false);
  489. }
  490. void TestGuiFdoSecrets::testServiceUnlockItems()
  491. {
  492. FdoSecrets::settings()->setConfirmAccessItem(true);
  493. auto service = enableService();
  494. VERIFY(service);
  495. auto coll = getDefaultCollection(service);
  496. VERIFY(coll);
  497. auto item = getFirstItem(coll);
  498. VERIFY(item);
  499. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  500. VERIFY(sess);
  501. DBUS_COMPARE(item->locked(), true);
  502. {
  503. DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
  504. // nothing is unlocked immediately without user's action
  505. COMPARE(unlocked, {});
  506. auto prompt = getProxy<PromptProxy>(promptPath);
  507. VERIFY(prompt);
  508. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  509. VERIFY(spyPromptCompleted.isValid());
  510. // nothing is unlocked yet
  511. COMPARE(spyPromptCompleted.count(), 0);
  512. DBUS_COMPARE(item->locked(), true);
  513. // drive the prompt
  514. DBUS_VERIFY(prompt->Prompt(""));
  515. // only allow once
  516. VERIFY(driveAccessControlDialog(false));
  517. VERIFY(waitForSignal(spyPromptCompleted, 1));
  518. {
  519. auto args = spyPromptCompleted.takeFirst();
  520. COMPARE(args.size(), 2);
  521. COMPARE(args.at(0).toBool(), false);
  522. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
  523. }
  524. // unlocked
  525. DBUS_COMPARE(item->locked(), false);
  526. }
  527. // access the secret should reset the locking state
  528. {
  529. DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
  530. }
  531. DBUS_COMPARE(item->locked(), true);
  532. // unlock again with remember
  533. {
  534. DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
  535. // nothing is unlocked immediately without user's action
  536. COMPARE(unlocked, {});
  537. auto prompt = getProxy<PromptProxy>(promptPath);
  538. VERIFY(prompt);
  539. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  540. VERIFY(spyPromptCompleted.isValid());
  541. // nothing is unlocked yet
  542. COMPARE(spyPromptCompleted.count(), 0);
  543. DBUS_COMPARE(item->locked(), true);
  544. // drive the prompt
  545. DBUS_VERIFY(prompt->Prompt(""));
  546. // only allow and remember
  547. VERIFY(driveAccessControlDialog(true));
  548. VERIFY(waitForSignal(spyPromptCompleted, 1));
  549. {
  550. auto args = spyPromptCompleted.takeFirst();
  551. COMPARE(args.size(), 2);
  552. COMPARE(args.at(0).toBool(), false);
  553. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
  554. }
  555. // unlocked
  556. DBUS_COMPARE(item->locked(), false);
  557. }
  558. // access the secret does not reset the locking state
  559. {
  560. DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
  561. }
  562. DBUS_COMPARE(item->locked(), false);
  563. }
  564. void TestGuiFdoSecrets::testServiceUnlockItemsIncludeFutureEntries()
  565. {
  566. FdoSecrets::settings()->setConfirmAccessItem(true);
  567. auto service = enableService();
  568. VERIFY(service);
  569. auto coll = getDefaultCollection(service);
  570. VERIFY(coll);
  571. auto item = getFirstItem(coll);
  572. VERIFY(item);
  573. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  574. VERIFY(sess);
  575. DBUS_COMPARE(item->locked(), true);
  576. {
  577. DBUS_GET2(unlocked, promptPath, service->Unlock({QDBusObjectPath(item->path())}));
  578. // nothing is unlocked immediately without user's action
  579. COMPARE(unlocked, {});
  580. auto prompt = getProxy<PromptProxy>(promptPath);
  581. VERIFY(prompt);
  582. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  583. VERIFY(spyPromptCompleted.isValid());
  584. // nothing is unlocked yet
  585. COMPARE(spyPromptCompleted.count(), 0);
  586. DBUS_COMPARE(item->locked(), true);
  587. // drive the prompt
  588. DBUS_VERIFY(prompt->Prompt(""));
  589. // remember and include future entries
  590. VERIFY(driveAccessControlDialog(true, true));
  591. VERIFY(waitForSignal(spyPromptCompleted, 1));
  592. {
  593. auto args = spyPromptCompleted.takeFirst();
  594. COMPARE(args.size(), 2);
  595. COMPARE(args.at(0).toBool(), false);
  596. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(item->path())});
  597. }
  598. // unlocked
  599. DBUS_COMPARE(item->locked(), false);
  600. }
  601. // check other entries are also unlocked
  602. {
  603. DBUS_GET(itemPaths, coll->items());
  604. VERIFY(itemPaths.size() > 1);
  605. auto anotherItem = getProxy<ItemProxy>(itemPaths.last());
  606. VERIFY(anotherItem);
  607. DBUS_COMPARE(anotherItem->locked(), false);
  608. }
  609. }
  610. void TestGuiFdoSecrets::testServiceLock()
  611. {
  612. auto service = enableService();
  613. VERIFY(service);
  614. auto coll = getDefaultCollection(service);
  615. VERIFY(coll);
  616. QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
  617. VERIFY(spyCollectionCreated.isValid());
  618. QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
  619. VERIFY(spyCollectionDeleted.isValid());
  620. QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
  621. VERIFY(spyCollectionChanged.isValid());
  622. // if the db is modified, prompt user
  623. m_db->markAsModified();
  624. {
  625. DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
  626. COMPARE(locked, {});
  627. auto prompt = getProxy<PromptProxy>(promptPath);
  628. VERIFY(prompt);
  629. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  630. VERIFY(spyPromptCompleted.isValid());
  631. // prompt and click cancel
  632. MessageBox::setNextAnswer(MessageBox::Cancel);
  633. DBUS_VERIFY(prompt->Prompt(""));
  634. processEvents();
  635. VERIFY(waitForSignal(spyPromptCompleted, 1));
  636. auto args = spyPromptCompleted.takeFirst();
  637. COMPARE(args.count(), 2);
  638. COMPARE(args.at(0).toBool(), true);
  639. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {});
  640. DBUS_COMPARE(coll->locked(), false);
  641. }
  642. {
  643. DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
  644. COMPARE(locked, {});
  645. auto prompt = getProxy<PromptProxy>(promptPath);
  646. VERIFY(prompt);
  647. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  648. VERIFY(spyPromptCompleted.isValid());
  649. // prompt and click save
  650. MessageBox::setNextAnswer(MessageBox::Save);
  651. DBUS_VERIFY(prompt->Prompt(""));
  652. processEvents();
  653. VERIFY(waitForSignal(spyPromptCompleted, 1));
  654. auto args = spyPromptCompleted.takeFirst();
  655. COMPARE(args.count(), 2);
  656. COMPARE(args.at(0).toBool(), false);
  657. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
  658. DBUS_COMPARE(coll->locked(), true);
  659. }
  660. VERIFY(waitForSignal(spyCollectionCreated, 0));
  661. QTRY_VERIFY(!spyCollectionChanged.isEmpty());
  662. for (const auto& args : spyCollectionChanged) {
  663. COMPARE(args.size(), 1);
  664. COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
  665. }
  666. VERIFY(waitForSignal(spyCollectionDeleted, 0));
  667. // locking item locks the whole db
  668. unlockDatabaseInBackend();
  669. {
  670. auto item = getFirstItem(coll);
  671. DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(item->path())}));
  672. COMPARE(locked, {});
  673. auto prompt = getProxy<PromptProxy>(promptPath);
  674. VERIFY(prompt);
  675. MessageBox::setNextAnswer(MessageBox::Save);
  676. DBUS_VERIFY(prompt->Prompt(""));
  677. processEvents();
  678. DBUS_COMPARE(coll->locked(), true);
  679. }
  680. }
  681. void TestGuiFdoSecrets::testServiceLockConcurrent()
  682. {
  683. auto service = enableService();
  684. VERIFY(service);
  685. auto coll = getDefaultCollection(service);
  686. VERIFY(coll);
  687. m_db->markAsModified();
  688. DBUS_GET2(locked, promptPath, service->Lock({QDBusObjectPath(coll->path())}));
  689. auto prompt = getProxy<PromptProxy>(promptPath);
  690. VERIFY(prompt);
  691. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  692. VERIFY(spyPromptCompleted.isValid());
  693. DBUS_GET2(locked2, promptPath2, service->Lock({QDBusObjectPath(coll->path())}));
  694. auto prompt2 = getProxy<PromptProxy>(promptPath2);
  695. VERIFY(prompt2);
  696. QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
  697. VERIFY(spyPromptCompleted2.isValid());
  698. // prompt and click save
  699. MessageBox::setNextAnswer(MessageBox::Save);
  700. DBUS_VERIFY(prompt->Prompt(""));
  701. // second prompt should not show dialog
  702. DBUS_VERIFY(prompt2->Prompt(""));
  703. VERIFY(waitForSignal(spyPromptCompleted, 1));
  704. {
  705. auto args = spyPromptCompleted.takeFirst();
  706. COMPARE(args.count(), 2);
  707. COMPARE(args.at(0).toBool(), false);
  708. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
  709. }
  710. VERIFY(waitForSignal(spyPromptCompleted2, 1));
  711. {
  712. auto args = spyPromptCompleted2.takeFirst();
  713. COMPARE(args.count(), 2);
  714. COMPARE(args.at(0).toBool(), false);
  715. COMPARE(getSignalVariantArgument<QList<QDBusObjectPath>>(args.at(1)), {QDBusObjectPath(coll->path())});
  716. }
  717. DBUS_COMPARE(coll->locked(), true);
  718. }
  719. void TestGuiFdoSecrets::testSessionOpen()
  720. {
  721. auto service = enableService();
  722. VERIFY(service);
  723. auto sess = openSession(service, PlainCipher::Algorithm);
  724. VERIFY(sess);
  725. sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  726. VERIFY(sess);
  727. }
  728. void TestGuiFdoSecrets::testSessionClose()
  729. {
  730. auto service = enableService();
  731. VERIFY(service);
  732. auto sess = openSession(service, PlainCipher::Algorithm);
  733. VERIFY(sess);
  734. DBUS_VERIFY(sess->Close());
  735. }
  736. void TestGuiFdoSecrets::testCollectionCreate()
  737. {
  738. auto service = enableService();
  739. VERIFY(service);
  740. QSignalSpy spyCollectionCreated(service.data(), SIGNAL(CollectionCreated(QDBusObjectPath)));
  741. VERIFY(spyCollectionCreated.isValid());
  742. // returns existing if alias is nonempty and exists
  743. {
  744. auto existing = getDefaultCollection(service);
  745. DBUS_GET2(collPath,
  746. promptPath,
  747. service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "NewDB"}}, "default"));
  748. COMPARE(promptPath, QDBusObjectPath("/"));
  749. COMPARE(collPath.path(), existing->path());
  750. }
  751. VERIFY(waitForSignal(spyCollectionCreated, 0));
  752. // create new one and set properties
  753. {
  754. DBUS_GET2(collPath,
  755. promptPath,
  756. service->CreateCollection({{DBUS_INTERFACE_SECRET_COLLECTION + ".Label", "Test NewDB"}}, "mydatadb"));
  757. COMPARE(collPath, QDBusObjectPath("/"));
  758. auto prompt = getProxy<PromptProxy>(promptPath);
  759. VERIFY(prompt);
  760. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  761. VERIFY(spyPromptCompleted.isValid());
  762. DBUS_VERIFY(prompt->Prompt(""));
  763. VERIFY(driveNewDatabaseWizard());
  764. VERIFY(waitForSignal(spyPromptCompleted, 1));
  765. auto args = spyPromptCompleted.takeFirst();
  766. COMPARE(args.size(), 2);
  767. COMPARE(args.at(0).toBool(), false);
  768. auto coll = getProxy<CollectionProxy>(getSignalVariantArgument<QDBusObjectPath>(args.at(1)));
  769. VERIFY(coll);
  770. DBUS_COMPARE(coll->label(), QStringLiteral("Test NewDB"));
  771. VERIFY(waitForSignal(spyCollectionCreated, 1));
  772. {
  773. args = spyCollectionCreated.takeFirst();
  774. COMPARE(args.size(), 1);
  775. COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
  776. }
  777. }
  778. }
  779. void TestGuiFdoSecrets::testCollectionDelete()
  780. {
  781. auto service = enableService();
  782. VERIFY(service);
  783. auto coll = getDefaultCollection(service);
  784. VERIFY(coll);
  785. // save the path which will be gone after the deletion.
  786. auto collPath = coll->path();
  787. QSignalSpy spyCollectionDeleted(service.data(), SIGNAL(CollectionDeleted(QDBusObjectPath)));
  788. VERIFY(spyCollectionDeleted.isValid());
  789. m_db->markAsModified();
  790. DBUS_GET(promptPath, coll->Delete());
  791. auto prompt = getProxy<PromptProxy>(promptPath);
  792. VERIFY(prompt);
  793. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  794. VERIFY(spyPromptCompleted.isValid());
  795. // prompt and click save
  796. MessageBox::setNextAnswer(MessageBox::Save);
  797. DBUS_VERIFY(prompt->Prompt(""));
  798. // closing the tab should have deleted the database if not in testing
  799. // but deleteLater is not processed in QApplication::processEvent
  800. // see https://doc.qt.io/qt-5/qcoreapplication.html#processEvents
  801. VERIFY(waitForSignal(spyPromptCompleted, 1));
  802. auto args = spyPromptCompleted.takeFirst();
  803. COMPARE(args.count(), 2);
  804. COMPARE(args.at(0).toBool(), false);
  805. COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
  806. // however, the object should already be taken down from dbus
  807. {
  808. auto reply = coll->locked();
  809. VERIFY(reply.isFinished() && reply.isError());
  810. COMPARE(reply.error().type(), QDBusError::UnknownObject);
  811. }
  812. VERIFY(waitForSignal(spyCollectionDeleted, 1));
  813. {
  814. args = spyCollectionDeleted.takeFirst();
  815. COMPARE(args.size(), 1);
  816. COMPARE(args.at(0).value<QDBusObjectPath>().path(), collPath);
  817. }
  818. }
  819. void TestGuiFdoSecrets::testCollectionDeleteConcurrent()
  820. {
  821. auto service = enableService();
  822. VERIFY(service);
  823. auto coll = getDefaultCollection(service);
  824. VERIFY(coll);
  825. m_db->markAsModified();
  826. DBUS_GET(promptPath, coll->Delete());
  827. auto prompt = getProxy<PromptProxy>(promptPath);
  828. VERIFY(prompt);
  829. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  830. VERIFY(spyPromptCompleted.isValid());
  831. // before interacting with the prompt, another request come in
  832. DBUS_GET(promptPath2, coll->Delete());
  833. auto prompt2 = getProxy<PromptProxy>(promptPath);
  834. VERIFY(prompt2);
  835. QSignalSpy spyPromptCompleted2(prompt2.data(), SIGNAL(Completed(bool, QDBusVariant)));
  836. VERIFY(spyPromptCompleted2.isValid());
  837. // prompt and click save
  838. MessageBox::setNextAnswer(MessageBox::Save);
  839. DBUS_VERIFY(prompt->Prompt(""));
  840. // there should be no prompt
  841. DBUS_VERIFY(prompt2->Prompt(""));
  842. VERIFY(waitForSignal(spyPromptCompleted, 1));
  843. {
  844. auto args = spyPromptCompleted.takeFirst();
  845. COMPARE(args.count(), 2);
  846. COMPARE(args.at(0).toBool(), false);
  847. COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
  848. }
  849. VERIFY(waitForSignal(spyPromptCompleted2, 1));
  850. {
  851. auto args = spyPromptCompleted2.takeFirst();
  852. COMPARE(args.count(), 2);
  853. COMPARE(args.at(0).toBool(), false);
  854. COMPARE(args.at(1).value<QDBusVariant>().variant().toString(), QStringLiteral(""));
  855. }
  856. {
  857. auto reply = coll->locked();
  858. VERIFY(reply.isFinished() && reply.isError());
  859. COMPARE(reply.error().type(), QDBusError::UnknownObject);
  860. }
  861. }
  862. void TestGuiFdoSecrets::testCollectionChange()
  863. {
  864. auto service = enableService();
  865. VERIFY(service);
  866. auto coll = getDefaultCollection(service);
  867. VERIFY(coll);
  868. QSignalSpy spyCollectionChanged(service.data(), SIGNAL(CollectionChanged(QDBusObjectPath)));
  869. VERIFY(spyCollectionChanged.isValid());
  870. DBUS_VERIFY(coll->setLabel("anotherLabel"));
  871. COMPARE(m_db->metadata()->name(), QStringLiteral("anotherLabel"));
  872. QTRY_COMPARE(spyCollectionChanged.size(), 1);
  873. {
  874. auto args = spyCollectionChanged.takeFirst();
  875. COMPARE(args.size(), 1);
  876. COMPARE(args.at(0).value<QDBusObjectPath>().path(), coll->path());
  877. }
  878. }
  879. void TestGuiFdoSecrets::testHiddenFilename()
  880. {
  881. // when file name contains leading dot, all parts excepting the last should be used
  882. // for collection name, and the registration should success
  883. VERIFY(m_dbFile->rename(QFileInfo(*m_dbFile).path() + "/.Name.kdbx"));
  884. // reset is necessary to not hold database longer and cause connections
  885. // not cleaned up when the database tab is closed.
  886. m_db.reset();
  887. VERIFY(m_tabWidget->closeAllDatabaseTabs());
  888. m_tabWidget->addDatabaseTab(m_dbFile->fileName(), false, "a");
  889. m_dbWidget = m_tabWidget->currentDatabaseWidget();
  890. m_db = m_dbWidget->database();
  891. // enable the service
  892. auto service = enableService();
  893. VERIFY(service);
  894. // collection is properly registered
  895. auto coll = getDefaultCollection(service);
  896. auto collObj = m_plugin->dbus()->pathToObject<Collection>(QDBusObjectPath(coll->path()));
  897. VERIFY(collObj);
  898. COMPARE(collObj->name(), QStringLiteral(".Name"));
  899. }
  900. void TestGuiFdoSecrets::testDuplicateName()
  901. {
  902. QTemporaryDir dir;
  903. VERIFY(dir.isValid());
  904. // create another file under different path but with the same filename
  905. QString anotherFile = dir.path() + "/" + QFileInfo(*m_dbFile).fileName();
  906. m_dbFile->copy(anotherFile);
  907. m_tabWidget->addDatabaseTab(anotherFile, false, "a");
  908. auto service = enableService();
  909. VERIFY(service);
  910. // when two databases have the same name, one of it will have part of its uuid suffixed
  911. const QString pathNoSuffix = QStringLiteral("/org/freedesktop/secrets/collection/KeePassXC");
  912. DBUS_GET(colls, service->collections());
  913. COMPARE(colls.size(), 2);
  914. COMPARE(colls[0].path(), pathNoSuffix);
  915. VERIFY(colls[1].path() != pathNoSuffix);
  916. }
  917. void TestGuiFdoSecrets::testItemCreate()
  918. {
  919. auto service = enableService();
  920. VERIFY(service);
  921. auto coll = getDefaultCollection(service);
  922. VERIFY(coll);
  923. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  924. VERIFY(sess);
  925. QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
  926. VERIFY(spyItemCreated.isValid());
  927. // create item
  928. StringStringMap attributes{
  929. {"application", "fdosecrets-test"},
  930. {"attr-i[bute]", "![some] -value*"},
  931. };
  932. auto item = createItem(sess, coll, "abc", "Password", attributes, false);
  933. VERIFY(item);
  934. // signals
  935. {
  936. VERIFY(waitForSignal(spyItemCreated, 1));
  937. auto args = spyItemCreated.takeFirst();
  938. COMPARE(args.size(), 1);
  939. COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
  940. }
  941. // attributes
  942. {
  943. DBUS_GET(actual, item->attributes());
  944. for (const auto& key : attributes.keys()) {
  945. COMPARE(actual[key], attributes[key]);
  946. }
  947. }
  948. // label
  949. DBUS_COMPARE(item->label(), QStringLiteral("abc"));
  950. // secrets
  951. {
  952. DBUS_GET(ss, item->GetSecret(QDBusObjectPath(sess->path())));
  953. auto decrypted = m_clientCipher->decrypt(ss.unmarshal(m_plugin->dbus()));
  954. COMPARE(decrypted.value, QByteArrayLiteral("Password"));
  955. }
  956. // searchable
  957. {
  958. DBUS_GET2(unlocked, locked, service->SearchItems(attributes));
  959. COMPARE(locked, {});
  960. COMPARE(unlocked, {QDBusObjectPath(item->path())});
  961. }
  962. {
  963. DBUS_GET(unlocked, coll->SearchItems(attributes));
  964. VERIFY(unlocked.contains(QDBusObjectPath(item->path())));
  965. }
  966. }
  967. void TestGuiFdoSecrets::testItemCreateUnlock()
  968. {
  969. auto service = enableService();
  970. VERIFY(service);
  971. auto coll = getDefaultCollection(service);
  972. VERIFY(coll);
  973. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  974. VERIFY(sess);
  975. // NOTE: entries are no longer valid after locking
  976. lockDatabaseInBackend();
  977. QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
  978. VERIFY(spyItemCreated.isValid());
  979. // create item
  980. StringStringMap attributes{
  981. {"application", "fdosecrets-test"},
  982. {"attr-i[bute]", "![some] -value*"},
  983. };
  984. auto item = createItem(sess, coll, "abc", "Password", attributes, false, false, true);
  985. VERIFY(item);
  986. }
  987. void TestGuiFdoSecrets::testItemChange()
  988. {
  989. auto service = enableService();
  990. VERIFY(service);
  991. auto coll = getDefaultCollection(service);
  992. VERIFY(coll);
  993. auto item = getFirstItem(coll);
  994. VERIFY(item);
  995. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  996. VERIFY(itemObj);
  997. auto entry = itemObj->backend();
  998. VERIFY(entry);
  999. QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath)));
  1000. VERIFY(spyItemChanged.isValid());
  1001. DBUS_VERIFY(item->setLabel("anotherLabel"));
  1002. COMPARE(entry->title(), QStringLiteral("anotherLabel"));
  1003. QTRY_VERIFY(!spyItemChanged.isEmpty());
  1004. for (const auto& args : spyItemChanged) {
  1005. COMPARE(args.size(), 1);
  1006. COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
  1007. }
  1008. spyItemChanged.clear();
  1009. DBUS_VERIFY(item->setAttributes({
  1010. {"abc", "def"},
  1011. }));
  1012. COMPARE(entry->attributes()->value("abc"), QStringLiteral("def"));
  1013. QTRY_VERIFY(!spyItemChanged.isEmpty());
  1014. for (const auto& args : spyItemChanged) {
  1015. COMPARE(args.size(), 1);
  1016. COMPARE(args.at(0).value<QDBusObjectPath>().path(), item->path());
  1017. }
  1018. }
  1019. void TestGuiFdoSecrets::testItemReplace()
  1020. {
  1021. auto service = enableService();
  1022. VERIFY(service);
  1023. auto coll = getDefaultCollection(service);
  1024. VERIFY(coll);
  1025. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  1026. VERIFY(sess);
  1027. // create item
  1028. StringStringMap attr1{
  1029. {"application", "fdosecrets-test"},
  1030. {"attr-i[bute]", "![some] -value*"},
  1031. {"fdosecrets-attr", "1"},
  1032. };
  1033. StringStringMap attr2{
  1034. {"application", "fdosecrets-test"},
  1035. {"attr-i[bute]", "![some] -value*"},
  1036. {"fdosecrets-attr", "2"},
  1037. };
  1038. auto item1 = createItem(sess, coll, "abc1", "Password", attr1, false);
  1039. VERIFY(item1);
  1040. auto item2 = createItem(sess, coll, "abc2", "Password", attr2, false);
  1041. VERIFY(item2);
  1042. {
  1043. DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
  1044. QSet<QDBusObjectPath> expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())};
  1045. COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
  1046. }
  1047. QSignalSpy spyItemCreated(coll.data(), SIGNAL(ItemCreated(QDBusObjectPath)));
  1048. VERIFY(spyItemCreated.isValid());
  1049. QSignalSpy spyItemChanged(coll.data(), SIGNAL(ItemChanged(QDBusObjectPath)));
  1050. VERIFY(spyItemChanged.isValid());
  1051. {
  1052. // when replace, existing item with matching attr is updated
  1053. auto item3 = createItem(sess, coll, "abc3", "Password", attr2, true);
  1054. VERIFY(item3);
  1055. COMPARE(item2->path(), item3->path());
  1056. DBUS_COMPARE(item3->label(), QStringLiteral("abc3"));
  1057. // there are still 2 entries
  1058. DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
  1059. QSet<QDBusObjectPath> expected{QDBusObjectPath(item1->path()), QDBusObjectPath(item2->path())};
  1060. COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
  1061. VERIFY(waitForSignal(spyItemCreated, 0));
  1062. // there may be multiple changed signals, due to each item attribute is set separately
  1063. QTRY_VERIFY(!spyItemChanged.isEmpty());
  1064. for (const auto& args : spyItemChanged) {
  1065. COMPARE(args.size(), 1);
  1066. COMPARE(args.at(0).value<QDBusObjectPath>().path(), item3->path());
  1067. }
  1068. }
  1069. spyItemCreated.clear();
  1070. spyItemChanged.clear();
  1071. {
  1072. // when NOT replace, another entry is created
  1073. auto item4 = createItem(sess, coll, "abc4", "Password", attr2, false);
  1074. VERIFY(item4);
  1075. DBUS_COMPARE(item2->label(), QStringLiteral("abc3"));
  1076. DBUS_COMPARE(item4->label(), QStringLiteral("abc4"));
  1077. // there are 3 entries
  1078. DBUS_GET2(unlocked, locked, service->SearchItems({{"application", "fdosecrets-test"}}));
  1079. QSet<QDBusObjectPath> expected{
  1080. QDBusObjectPath(item1->path()),
  1081. QDBusObjectPath(item2->path()),
  1082. QDBusObjectPath(item4->path()),
  1083. };
  1084. COMPARE(QSet<QDBusObjectPath>::fromList(unlocked), expected);
  1085. VERIFY(waitForSignal(spyItemCreated, 1));
  1086. {
  1087. auto args = spyItemCreated.takeFirst();
  1088. COMPARE(args.size(), 1);
  1089. COMPARE(args.at(0).value<QDBusObjectPath>().path(), item4->path());
  1090. }
  1091. // there may be multiple changed signals, due to each item attribute is set separately
  1092. VERIFY(!spyItemChanged.isEmpty());
  1093. for (const auto& args : spyItemChanged) {
  1094. COMPARE(args.size(), 1);
  1095. COMPARE(args.at(0).value<QDBusObjectPath>().path(), item4->path());
  1096. }
  1097. }
  1098. }
  1099. void TestGuiFdoSecrets::testItemReplaceExistingLocked()
  1100. {
  1101. auto service = enableService();
  1102. VERIFY(service);
  1103. auto coll = getDefaultCollection(service);
  1104. VERIFY(coll);
  1105. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  1106. VERIFY(sess);
  1107. // create item
  1108. StringStringMap attr1{
  1109. {"application", "fdosecrets-test"},
  1110. {"attr-i[bute]", "![some] -value*"},
  1111. {"fdosecrets-attr", "1"},
  1112. };
  1113. auto item = createItem(sess, coll, "abc1", "Password", attr1, false);
  1114. VERIFY(item);
  1115. // make sure the item is locked
  1116. {
  1117. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  1118. VERIFY(itemObj);
  1119. auto entry = itemObj->backend();
  1120. VERIFY(entry);
  1121. FdoSecrets::settings()->setConfirmAccessItem(true);
  1122. m_client->setItemAuthorized(entry->uuid(), AuthDecision::Undecided);
  1123. DBUS_COMPARE(item->locked(), true);
  1124. }
  1125. // when replace with a locked item, there will be a prompt
  1126. auto item2 = createItem(sess, coll, "abc2", "PasswordUpdated", attr1, true, true);
  1127. VERIFY(item2);
  1128. COMPARE(item2->path(), item->path());
  1129. DBUS_COMPARE(item2->label(), QStringLiteral("abc2"));
  1130. }
  1131. void TestGuiFdoSecrets::testItemSecret()
  1132. {
  1133. const QString TEXT_PLAIN = "text/plain";
  1134. const QString APPLICATION_OCTET_STREAM = "application/octet-stream";
  1135. auto service = enableService();
  1136. VERIFY(service);
  1137. auto coll = getDefaultCollection(service);
  1138. VERIFY(coll);
  1139. auto item = getFirstItem(coll);
  1140. VERIFY(item);
  1141. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  1142. VERIFY(sess);
  1143. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  1144. VERIFY(itemObj);
  1145. auto entry = itemObj->backend();
  1146. VERIFY(entry);
  1147. // plain text secret
  1148. {
  1149. DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
  1150. auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
  1151. COMPARE(ss.contentType, TEXT_PLAIN);
  1152. COMPARE(ss.value, entry->password().toUtf8());
  1153. }
  1154. // get secret with notification
  1155. FdoSecrets::settings()->setShowNotification(true);
  1156. {
  1157. QSignalSpy spyShowNotification(m_plugin, SIGNAL(requestShowNotification(QString, QString, int)));
  1158. VERIFY(spyShowNotification.isValid());
  1159. DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
  1160. auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
  1161. COMPARE(ss.contentType, TEXT_PLAIN);
  1162. COMPARE(ss.value, entry->password().toUtf8());
  1163. COMPARE(ss.contentType, TEXT_PLAIN);
  1164. COMPARE(ss.value, entry->password().toUtf8());
  1165. VERIFY(waitForSignal(spyShowNotification, 1));
  1166. }
  1167. FdoSecrets::settings()->setShowNotification(false);
  1168. // set secret with plain text
  1169. {
  1170. // first create Secret in wire format,
  1171. // then convert to internal format and encrypt
  1172. // finally convert encrypted internal format back to wire format to pass to SetSecret
  1173. const QByteArray expected = QByteArrayLiteral("NewPassword");
  1174. auto encrypted = encryptPassword(expected, TEXT_PLAIN, sess);
  1175. DBUS_VERIFY(item->SetSecret(encrypted));
  1176. COMPARE(entry->password().toUtf8(), expected);
  1177. }
  1178. // set secret with something else is saved as attachment
  1179. const QByteArray expected = QByteArrayLiteral("NewPasswordBinary");
  1180. {
  1181. auto encrypted = encryptPassword(expected, APPLICATION_OCTET_STREAM, sess);
  1182. DBUS_VERIFY(item->SetSecret(encrypted));
  1183. COMPARE(entry->password(), QStringLiteral(""));
  1184. }
  1185. {
  1186. DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
  1187. auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
  1188. COMPARE(ss.contentType, APPLICATION_OCTET_STREAM);
  1189. COMPARE(ss.value, expected);
  1190. }
  1191. }
  1192. void TestGuiFdoSecrets::testItemDelete()
  1193. {
  1194. FdoSecrets::settings()->setConfirmDeleteItem(true);
  1195. auto service = enableService();
  1196. VERIFY(service);
  1197. auto coll = getDefaultCollection(service);
  1198. VERIFY(coll);
  1199. auto item = getFirstItem(coll);
  1200. VERIFY(item);
  1201. // save the path which will be gone after the deletion.
  1202. auto itemPath = item->path();
  1203. QSignalSpy spyItemDeleted(coll.data(), SIGNAL(ItemDeleted(QDBusObjectPath)));
  1204. VERIFY(spyItemDeleted.isValid());
  1205. DBUS_GET(promptPath, item->Delete());
  1206. auto prompt = getProxy<PromptProxy>(promptPath);
  1207. VERIFY(prompt);
  1208. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  1209. VERIFY(spyPromptCompleted.isValid());
  1210. // prompt and click save
  1211. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  1212. VERIFY(itemObj);
  1213. MessageBox::setNextAnswer(MessageBox::Delete);
  1214. DBUS_VERIFY(prompt->Prompt(""));
  1215. VERIFY(waitForSignal(spyPromptCompleted, 1));
  1216. auto args = spyPromptCompleted.takeFirst();
  1217. COMPARE(args.count(), 2);
  1218. COMPARE(args.at(0).toBool(), false);
  1219. COMPARE(args.at(1).toString(), QStringLiteral(""));
  1220. VERIFY(waitForSignal(spyItemDeleted, 1));
  1221. args = spyItemDeleted.takeFirst();
  1222. COMPARE(args.size(), 1);
  1223. COMPARE(args.at(0).value<QDBusObjectPath>().path(), itemPath);
  1224. }
  1225. void TestGuiFdoSecrets::testItemLockState()
  1226. {
  1227. auto service = enableService();
  1228. VERIFY(service);
  1229. auto coll = getDefaultCollection(service);
  1230. VERIFY(coll);
  1231. auto item = getFirstItem(coll);
  1232. VERIFY(item);
  1233. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  1234. VERIFY(sess);
  1235. auto itemObj = m_plugin->dbus()->pathToObject<Item>(QDBusObjectPath(item->path()));
  1236. VERIFY(itemObj);
  1237. auto entry = itemObj->backend();
  1238. VERIFY(entry);
  1239. auto secret =
  1240. wire::Secret{
  1241. QDBusObjectPath(sess->path()),
  1242. {},
  1243. "NewPassword",
  1244. "text/plain",
  1245. }
  1246. .unmarshal(m_plugin->dbus());
  1247. auto encrypted = m_clientCipher->encrypt(secret).marshal();
  1248. // when access confirmation is disabled, item is unlocked when the collection is unlocked
  1249. FdoSecrets::settings()->setConfirmAccessItem(false);
  1250. DBUS_COMPARE(item->locked(), false);
  1251. // when access confirmation is enabled, item is locked if the client has no authorization
  1252. FdoSecrets::settings()->setConfirmAccessItem(true);
  1253. DBUS_COMPARE(item->locked(), true);
  1254. // however, item properties are still accessible as long as the collection is unlocked
  1255. DBUS_VERIFY(item->attributes());
  1256. DBUS_VERIFY(item->setAttributes({}));
  1257. DBUS_VERIFY(item->label());
  1258. DBUS_VERIFY(item->setLabel("abc"));
  1259. DBUS_VERIFY(item->created());
  1260. DBUS_VERIFY(item->modified());
  1261. // except secret, which is locked
  1262. {
  1263. auto reply = item->GetSecret(QDBusObjectPath(sess->path()));
  1264. VERIFY(reply.isError());
  1265. COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED);
  1266. }
  1267. {
  1268. auto reply = item->SetSecret(encrypted);
  1269. VERIFY(reply.isError());
  1270. COMPARE(reply.error().name(), DBUS_ERROR_SECRET_IS_LOCKED);
  1271. }
  1272. // item is unlocked if the client is authorized
  1273. m_client->setItemAuthorized(entry->uuid(), AuthDecision::Allowed);
  1274. DBUS_COMPARE(item->locked(), false);
  1275. DBUS_VERIFY(item->GetSecret(QDBusObjectPath(sess->path())));
  1276. DBUS_VERIFY(item->SetSecret(encrypted));
  1277. }
  1278. void TestGuiFdoSecrets::testItemRejectSetReferenceFields()
  1279. {
  1280. // expose a subgroup, entries in it should not be able to retrieve data from entries outside it
  1281. auto rootEntry = m_db->rootGroup()->entries().first();
  1282. VERIFY(rootEntry);
  1283. auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
  1284. VERIFY(subgroup);
  1285. FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
  1286. auto service = enableService();
  1287. VERIFY(service);
  1288. auto coll = getDefaultCollection(service);
  1289. VERIFY(coll);
  1290. auto item = getFirstItem(coll);
  1291. VERIFY(item);
  1292. auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
  1293. VERIFY(sess);
  1294. const auto refText = QStringLiteral("{REF:P@T:%1}").arg(rootEntry->title());
  1295. // reject ref in label
  1296. {
  1297. auto reply = item->setLabel(refText);
  1298. VERIFY(reply.isFinished() && reply.isError());
  1299. COMPARE(reply.error().type(), QDBusError::InvalidArgs);
  1300. }
  1301. // reject ref in custom attributes
  1302. {
  1303. auto reply = item->setAttributes({{"steal", refText}});
  1304. VERIFY(reply.isFinished() && reply.isError());
  1305. COMPARE(reply.error().type(), QDBusError::InvalidArgs);
  1306. }
  1307. // reject ref in password
  1308. {
  1309. auto reply = item->SetSecret(encryptPassword(refText.toUtf8(), "text/plain", sess));
  1310. VERIFY(reply.isFinished() && reply.isError());
  1311. COMPARE(reply.error().type(), QDBusError::InvalidArgs);
  1312. }
  1313. // reject ref in content type
  1314. {
  1315. auto reply = item->SetSecret(encryptPassword("dummy", refText, sess));
  1316. VERIFY(reply.isFinished() && reply.isError());
  1317. COMPARE(reply.error().type(), QDBusError::InvalidArgs);
  1318. }
  1319. }
  1320. void TestGuiFdoSecrets::testAlias()
  1321. {
  1322. auto service = enableService();
  1323. VERIFY(service);
  1324. // read default alias
  1325. DBUS_GET(collPath, service->ReadAlias("default"));
  1326. auto coll = getProxy<CollectionProxy>(collPath);
  1327. VERIFY(coll);
  1328. // set extra alias
  1329. DBUS_VERIFY(service->SetAlias("another", QDBusObjectPath(collPath)));
  1330. // get using extra alias
  1331. DBUS_GET(collPath2, service->ReadAlias("another"));
  1332. COMPARE(collPath2, collPath);
  1333. }
  1334. void TestGuiFdoSecrets::testDefaultAliasAlwaysPresent()
  1335. {
  1336. auto service = enableService();
  1337. VERIFY(service);
  1338. // one collection, which is default alias
  1339. auto coll = getDefaultCollection(service);
  1340. VERIFY(coll);
  1341. // after locking, the collection is still there, but locked
  1342. lockDatabaseInBackend();
  1343. coll = getDefaultCollection(service);
  1344. VERIFY(coll);
  1345. DBUS_COMPARE(coll->locked(), true);
  1346. // unlock the database, the alias and collection is present
  1347. unlockDatabaseInBackend();
  1348. coll = getDefaultCollection(service);
  1349. VERIFY(coll);
  1350. DBUS_COMPARE(coll->locked(), false);
  1351. }
  1352. void TestGuiFdoSecrets::testExposeSubgroup()
  1353. {
  1354. auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
  1355. VERIFY(subgroup);
  1356. FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
  1357. auto service = enableService();
  1358. VERIFY(service);
  1359. auto coll = getDefaultCollection(service);
  1360. VERIFY(coll);
  1361. // exposing subgroup does not expose entries in other groups
  1362. DBUS_GET(itemPaths, coll->items());
  1363. QSet<Entry*> exposedEntries;
  1364. for (const auto& itemPath : itemPaths) {
  1365. exposedEntries << m_plugin->dbus()->pathToObject<Item>(itemPath)->backend();
  1366. }
  1367. COMPARE(exposedEntries, QSet<Entry*>::fromList(subgroup->entries()));
  1368. }
  1369. void TestGuiFdoSecrets::testModifyingExposedGroup()
  1370. {
  1371. // test when exposed group is removed the collection is not exposed anymore
  1372. auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking");
  1373. VERIFY(subgroup);
  1374. FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
  1375. auto service = enableService();
  1376. VERIFY(service);
  1377. {
  1378. DBUS_GET(collPaths, service->collections());
  1379. COMPARE(collPaths.size(), 1);
  1380. }
  1381. m_db->metadata()->setRecycleBinEnabled(true);
  1382. m_db->recycleGroup(subgroup);
  1383. processEvents();
  1384. {
  1385. DBUS_GET(collPaths, service->collections());
  1386. COMPARE(collPaths, {});
  1387. }
  1388. // test setting another exposed group, the collection will be exposed again
  1389. FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
  1390. processEvents();
  1391. {
  1392. DBUS_GET(collPaths, service->collections());
  1393. COMPARE(collPaths.size(), 1);
  1394. }
  1395. }
  1396. void TestGuiFdoSecrets::testNoExposeRecycleBin()
  1397. {
  1398. // when the recycle bin is underneath the exposed group
  1399. // be careful not to expose entries in there
  1400. FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
  1401. m_db->metadata()->setRecycleBinEnabled(true);
  1402. auto entry = m_db->rootGroup()->entries().first();
  1403. VERIFY(entry);
  1404. m_db->recycleEntry(entry);
  1405. processEvents();
  1406. auto service = enableService();
  1407. VERIFY(service);
  1408. auto coll = getDefaultCollection(service);
  1409. VERIFY(coll);
  1410. // exposing subgroup does not expose entries in other groups
  1411. DBUS_GET(itemPaths, coll->items());
  1412. QSet<Entry*> exposedEntries;
  1413. for (const auto& itemPath : itemPaths) {
  1414. exposedEntries << m_plugin->dbus()->pathToObject<Item>(itemPath)->backend();
  1415. }
  1416. VERIFY(!exposedEntries.contains(entry));
  1417. // searching should not return the entry
  1418. DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", entry->title()}}));
  1419. COMPARE(locked, {});
  1420. COMPARE(unlocked, {});
  1421. }
  1422. void TestGuiFdoSecrets::lockDatabaseInBackend()
  1423. {
  1424. m_tabWidget->lockDatabases();
  1425. m_db.reset();
  1426. processEvents();
  1427. }
  1428. void TestGuiFdoSecrets::unlockDatabaseInBackend()
  1429. {
  1430. m_dbWidget->performUnlockDatabase("a");
  1431. m_db = m_dbWidget->database();
  1432. processEvents();
  1433. }
  1434. void TestGuiFdoSecrets::processEvents()
  1435. {
  1436. // Couldn't use QApplication::processEvents, because per Qt documentation:
  1437. // events that are posted while the function runs will be queued until a later round of event processing.
  1438. // and we may post QTimer single shot events during event handling to achieve async method.
  1439. // So we directly call event dispatcher in a loop until no events can be handled
  1440. while (QAbstractEventDispatcher::instance()->processEvents(QEventLoop::AllEvents)) {
  1441. // pass
  1442. }
  1443. }
  1444. // the following functions have return value, switch macros to the version supporting that
  1445. #undef VERIFY
  1446. #undef VERIFY2
  1447. #undef COMPARE
  1448. #define VERIFY(stmt) VERIFY2_RET(stmt, "")
  1449. #define VERIFY2 VERIFY2_RET
  1450. #define COMPARE COMPARE_RET
  1451. QSharedPointer<ServiceProxy> TestGuiFdoSecrets::enableService()
  1452. {
  1453. FdoSecrets::settings()->setEnabled(true);
  1454. VERIFY(m_plugin);
  1455. m_plugin->updateServiceState();
  1456. return getProxy<ServiceProxy>(QDBusObjectPath(DBUS_PATH_SECRETS));
  1457. }
  1458. QSharedPointer<SessionProxy> TestGuiFdoSecrets::openSession(const QSharedPointer<ServiceProxy>& service,
  1459. const QString& algo)
  1460. {
  1461. VERIFY(service);
  1462. if (algo == PlainCipher::Algorithm) {
  1463. DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant("")));
  1464. return getProxy<SessionProxy>(sessPath);
  1465. } else if (algo == DhIetf1024Sha256Aes128CbcPkcs7::Algorithm) {
  1466. DBUS_GET2(output, sessPath, service->OpenSession(algo, QDBusVariant(m_clientCipher->negotiationOutput())));
  1467. m_clientCipher->updateClientPublicKey(output.variant().toByteArray());
  1468. return getProxy<SessionProxy>(sessPath);
  1469. }
  1470. QTest::qFail("Unsupported algorithm", __FILE__, __LINE__);
  1471. return {};
  1472. }
  1473. QSharedPointer<CollectionProxy> TestGuiFdoSecrets::getDefaultCollection(const QSharedPointer<ServiceProxy>& service)
  1474. {
  1475. VERIFY(service);
  1476. DBUS_GET(collPath, service->ReadAlias("default"));
  1477. return getProxy<CollectionProxy>(collPath);
  1478. }
  1479. QSharedPointer<ItemProxy> TestGuiFdoSecrets::getFirstItem(const QSharedPointer<CollectionProxy>& coll)
  1480. {
  1481. VERIFY(coll);
  1482. DBUS_GET(itemPaths, coll->items());
  1483. VERIFY(!itemPaths.isEmpty());
  1484. return getProxy<ItemProxy>(itemPaths.first());
  1485. }
  1486. QSharedPointer<ItemProxy> TestGuiFdoSecrets::createItem(const QSharedPointer<SessionProxy>& sess,
  1487. const QSharedPointer<CollectionProxy>& coll,
  1488. const QString& label,
  1489. const QString& pass,
  1490. const StringStringMap& attr,
  1491. bool replace,
  1492. bool expectPrompt,
  1493. bool expectUnlockPrompt)
  1494. {
  1495. VERIFY(sess);
  1496. VERIFY(coll);
  1497. QVariantMap properties{
  1498. {DBUS_INTERFACE_SECRET_ITEM + ".Label", QVariant::fromValue(label)},
  1499. {DBUS_INTERFACE_SECRET_ITEM + ".Attributes", QVariant::fromValue(attr)},
  1500. };
  1501. wire::Secret ss;
  1502. ss.session = QDBusObjectPath(sess->path());
  1503. ss.value = pass.toLocal8Bit();
  1504. ss.contentType = "plain/text";
  1505. auto encrypted = m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
  1506. DBUS_GET2(itemPath, promptPath, coll->CreateItem(properties, encrypted, replace));
  1507. auto prompt = getProxy<PromptProxy>(promptPath);
  1508. VERIFY(prompt);
  1509. QSignalSpy spyPromptCompleted(prompt.data(), SIGNAL(Completed(bool, QDBusVariant)));
  1510. VERIFY(spyPromptCompleted.isValid());
  1511. // drive the prompt
  1512. DBUS_VERIFY(prompt->Prompt(""));
  1513. bool unlockFound = driveUnlockDialog();
  1514. COMPARE(unlockFound, expectUnlockPrompt);
  1515. bool found = driveAccessControlDialog();
  1516. COMPARE(found, expectPrompt);
  1517. VERIFY(waitForSignal(spyPromptCompleted, 1));
  1518. auto args = spyPromptCompleted.takeFirst();
  1519. COMPARE(args.size(), 2);
  1520. COMPARE(args.at(0).toBool(), false);
  1521. itemPath = getSignalVariantArgument<QDBusObjectPath>(args.at(1));
  1522. return getProxy<ItemProxy>(itemPath);
  1523. }
  1524. FdoSecrets::wire::Secret
  1525. TestGuiFdoSecrets::encryptPassword(QByteArray value, QString contentType, const QSharedPointer<SessionProxy>& sess)
  1526. {
  1527. wire::Secret ss;
  1528. ss.contentType = std::move(contentType);
  1529. ss.value = std::move(value);
  1530. ss.session = QDBusObjectPath(sess->path());
  1531. return m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
  1532. }
  1533. bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember, bool includeFutureEntries)
  1534. {
  1535. processEvents();
  1536. for (auto w : QApplication::topLevelWidgets()) {
  1537. if (!w->isWindow()) {
  1538. continue;
  1539. }
  1540. auto dlg = qobject_cast<AccessControlDialog*>(w);
  1541. if (dlg && dlg->isVisible()) {
  1542. auto rememberCheck = dlg->findChild<QCheckBox*>("rememberCheck");
  1543. VERIFY(rememberCheck);
  1544. rememberCheck->setChecked(remember);
  1545. if (includeFutureEntries) {
  1546. dlg->done(AccessControlDialog::AllowAll);
  1547. } else {
  1548. dlg->done(AccessControlDialog::AllowSelected);
  1549. }
  1550. processEvents();
  1551. VERIFY(dlg->isHidden());
  1552. return true;
  1553. }
  1554. }
  1555. return false;
  1556. }
  1557. bool TestGuiFdoSecrets::driveNewDatabaseWizard()
  1558. {
  1559. // processEvents will block because the NewDatabaseWizard is shown using exec
  1560. // which creates a local QEventLoop.
  1561. // so we do a little trick here to get the return value back
  1562. bool ret = false;
  1563. QTimer::singleShot(0, this, [this, &ret]() {
  1564. ret = [this]() -> bool {
  1565. auto wizard = m_tabWidget->findChild<NewDatabaseWizard*>();
  1566. VERIFY(wizard);
  1567. COMPARE(wizard->currentId(), 0);
  1568. wizard->next();
  1569. wizard->next();
  1570. COMPARE(wizard->currentId(), 2);
  1571. // enter password
  1572. auto* passwordEdit =
  1573. wizard->findChild<PasswordWidget*>("enterPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
  1574. auto* passwordRepeatEdit =
  1575. wizard->findChild<PasswordWidget*>("repeatPasswordEdit")->findChild<QLineEdit*>("passwordEdit");
  1576. VERIFY(passwordEdit);
  1577. VERIFY(passwordRepeatEdit);
  1578. QTest::keyClicks(passwordEdit, "test");
  1579. QTest::keyClick(passwordEdit, Qt::Key::Key_Tab);
  1580. QTest::keyClicks(passwordRepeatEdit, "test");
  1581. // save database to temporary file
  1582. TemporaryFile tmpFile;
  1583. VERIFY(tmpFile.open());
  1584. tmpFile.close();
  1585. fileDialog()->setNextFileName(tmpFile.fileName());
  1586. // click Continue on the warning due to weak password
  1587. MessageBox::setNextAnswer(MessageBox::ContinueWithWeakPass);
  1588. wizard->accept();
  1589. tmpFile.remove();
  1590. return true;
  1591. }();
  1592. });
  1593. processEvents();
  1594. return ret;
  1595. }
  1596. bool TestGuiFdoSecrets::driveUnlockDialog(DatabaseWidget* target)
  1597. {
  1598. processEvents();
  1599. auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>();
  1600. VERIFY(dbOpenDlg);
  1601. if (!dbOpenDlg->isVisible()) {
  1602. return false;
  1603. }
  1604. dbOpenDlg->setActiveDatabaseTab(target);
  1605. auto editPassword = dbOpenDlg->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
  1606. VERIFY(editPassword);
  1607. editPassword->setFocus();
  1608. QTest::keyClicks(editPassword, "a");
  1609. QTest::keyClick(editPassword, Qt::Key_Enter);
  1610. processEvents();
  1611. return true;
  1612. }
  1613. bool TestGuiFdoSecrets::waitForSignal(QSignalSpy& spy, int expectedCount)
  1614. {
  1615. processEvents();
  1616. // If already expected count, do not wait and return immediately
  1617. if (spy.count() == expectedCount) {
  1618. return true;
  1619. } else if (spy.count() > expectedCount) {
  1620. return false;
  1621. }
  1622. spy.wait();
  1623. COMPARE(spy.count(), expectedCount);
  1624. return true;
  1625. }
  1626. #undef VERIFY
  1627. #define VERIFY QVERIFY
  1628. #undef COMPARE
  1629. #define COMPARE QCOMPARE