Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FdoSecrets: only show unlock dialog when there is no unlocked & exposed database #8028

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 1 addition & 20 deletions src/fdosecrets/objects/Collection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,7 @@ namespace FdoSecrets
return {};
}

DBusResult
Collection::searchItems(const DBusClientPtr& client, const StringStringMap& attributes, QList<Item*>& items)
DBusResult Collection::searchItems(const DBusClientPtr&, const StringStringMap& attributes, QList<Item*>& items)
{
items.clear();

Expand All @@ -220,24 +219,6 @@ namespace FdoSecrets
return ret;
}

if (backendLocked() && settings()->unlockBeforeSearch()) {
// do a blocking unlock prompt first.
// while technically not correct, this should improve compatibility.
// see issue #4443
auto prompt = PromptBase::Create<UnlockPrompt>(service(), QSet<Collection*>{this}, QSet<Item*>{});
if (!prompt) {
return QDBusError::InternalError;
}
// we don't know the windowId to show the prompt correctly,
// but the default of showing over the KPXC main window should be good enough
ret = prompt->prompt(client, "");
// blocking wait
QEventLoop loop;
connect(prompt, &PromptBase::completed, &loop, &QEventLoop::quit);
loop.exec();
}

// check again because the prompt may be cancelled
if (backendLocked()) {
// searchItems should work, whether `this` is locked or not.
// however, we can't search items the same way as in gnome-keying,
Expand Down
76 changes: 70 additions & 6 deletions src/fdosecrets/objects/Service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,30 @@ namespace FdoSecrets
return {};
}

DBusResult Service::unlockedCollections(QList<Collection*>& unlocked) const
{
auto ret = collections(unlocked);
if (ret.err()) {
return ret;
}

// filter out locked collections
auto it = unlocked.begin();
while (it != unlocked.end()) {
bool isLocked = true;
ret = (*it)->locked(isLocked);
if (ret.err()) {
return ret;
}
if (isLocked) {
it = unlocked.erase(it);
} else {
++it;
}
}
return {};
}

DBusResult Service::openSession(const DBusClientPtr& client,
const QString& algorithm,
const QVariant& input,
Expand Down Expand Up @@ -242,15 +266,43 @@ namespace FdoSecrets
DBusResult Service::searchItems(const DBusClientPtr& client,
const StringStringMap& attributes,
QList<Item*>& unlocked,
QList<Item*>& locked) const
QList<Item*>& locked)
{
QList<Collection*> colls;
auto ret = collections(colls);
// we can only search unlocked collections
QList<Collection*> unlockedColls;
auto ret = unlockedCollections(unlockedColls);
if (ret.err()) {
return ret;
}

for (const auto& coll : asConst(colls)) {
while (unlockedColls.isEmpty() && settings()->unlockBeforeSearch()) {
// enable compatibility mode by making sure at least one database is unlocked
QEventLoop loop;
bool wasAccepted = false;
connect(this, &Service::doneUnlockDatabaseInDialog, &loop, [&](bool accepted) {
wasAccepted = accepted;
loop.quit();
});

doUnlockAnyDatabaseInDialog();

// blocking wait
loop.exec();

if (!wasAccepted) {
// user cancelled, do not proceed
qWarning() << "user cancelled";
return {};
}

// need to recompute this because collections may disappear while in event loop
ret = unlockedCollections(unlockedColls);
if (ret.err()) {
return ret;
}
}

for (const auto& coll : asConst(unlockedColls)) {
QList<Item*> items;
ret = coll->searchItems(client, attributes, items);
if (ret.err()) {
Expand Down Expand Up @@ -524,7 +576,7 @@ namespace FdoSecrets
}

// check if the db is already being unlocked to prevent multiple dialogs for the same db
if (m_unlockingDb.contains(dbWidget)) {
if (m_unlockingAnyDatabase || m_unlockingDb.contains(dbWidget)) {
return;
}

Expand All @@ -536,21 +588,33 @@ namespace FdoSecrets
m_databases->unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::None);
}

void Service::doUnlockAnyDatabaseInDialog()
{
if (m_unlockingAnyDatabase || !m_unlockingDb.isEmpty()) {
return;
}
m_unlockingAnyDatabase = true;

m_databases->unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::None);
}

void Service::onDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget)
{
if (!m_unlockingDb.contains(dbWidget)) {
if (!m_unlockingAnyDatabase && !m_unlockingDb.contains(dbWidget)) {
// not our concern
return;
}

if (!accepted) {
emit doneUnlockDatabaseInDialog(false, dbWidget);
m_unlockingAnyDatabase = false;
m_unlockingDb.remove(dbWidget);
} else {
// delay the done signal to when the database is actually done with unlocking
// this is a oneshot connection to prevent superfluous signals
auto conn = connect(dbWidget, &DatabaseWidget::databaseUnlocked, this, [dbWidget, this]() {
emit doneUnlockDatabaseInDialog(true, dbWidget);
m_unlockingAnyDatabase = false;
disconnect(m_unlockingDb.take(dbWidget));
});
m_unlockingDb[dbWidget] = conn;
Expand Down
11 changes: 10 additions & 1 deletion src/fdosecrets/objects/Service.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ namespace FdoSecrets
Q_INVOKABLE DBusResult searchItems(const DBusClientPtr& client,
const StringStringMap& attributes,
QList<Item*>& unlocked,
QList<Item*>& locked) const;
QList<Item*>& locked);

Q_INVOKABLE DBusResult unlock(const DBusClientPtr& client,
const QList<DBusObject*>& objects,
Expand Down Expand Up @@ -125,6 +125,12 @@ namespace FdoSecrets
*/
void doUnlockDatabaseInDialog(DatabaseWidget* dbWidget);

/**
* Async, connect to signal doneUnlockDatabaseInDialog for finish notification
* @param dbWidget
*/
void doUnlockAnyDatabaseInDialog();

private slots:
void ensureDefaultAlias();

Expand Down Expand Up @@ -154,6 +160,8 @@ namespace FdoSecrets
*/
Collection* findCollection(const DatabaseWidget* db) const;

DBusResult unlockedCollections(QList<Collection*>& unlocked) const;

private:
FdoSecretsPlugin* m_plugin{nullptr};
QPointer<DatabaseTabWidget> m_databases{};
Expand All @@ -165,6 +173,7 @@ namespace FdoSecrets
QList<Session*> m_sessions{};

bool m_insideEnsureDefaultAlias{false};
bool m_unlockingAnyDatabase{false};
// list of db currently has unlock dialog shown
QHash<const DatabaseWidget*, QMetaObject::Connection> m_unlockingDb{};
QSet<const DatabaseWidget*> m_lockingDb{}; // list of db being locking
Expand Down
2 changes: 1 addition & 1 deletion src/gui/DatabaseTabWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public slots:
void closeDatabaseFromSender();
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent);
void unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent, const QString& filePath);
void unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent);
void relockPendingDatabase();

void showDatabaseSecurity();
Expand Down Expand Up @@ -106,7 +107,6 @@ private slots:
QSharedPointer<Database> execNewDatabaseWizard();
void updateLastDatabases(const QString& filename);
bool warnOnExport();
void unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent);
void displayUnlockDialog();

QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
Expand Down
70 changes: 67 additions & 3 deletions tests/gui/TestGuiFdoSecrets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ void TestGuiFdoSecrets::init()
m_dbFile.reset(new TemporaryFile());
// Write the temp storage to a temp database file for use in our tests
VERIFY(m_dbFile->open());
COMPARE(m_dbFile->write(m_dbData), static_cast<qint64>((m_dbData.size())));
COMPARE(m_dbFile->write(m_dbData), static_cast<qint64>(m_dbData.size()));
m_dbFile->close();

// make sure window is activated or focus tests may fail
Expand All @@ -198,6 +198,12 @@ void TestGuiFdoSecrets::init()
// by default expose the root group
FdoSecrets::settings()->setExposedGroup(m_db, m_db->rootGroup()->uuid());
VERIFY(m_dbWidget->save());

// enforce consistent default settings at the beginning
FdoSecrets::settings()->setUnlockBeforeSearch(false);
FdoSecrets::settings()->setShowNotification(false);
FdoSecrets::settings()->setConfirmAccessItem(false);
FdoSecrets::settings()->setEnabled(false);
}

// Every test ends with closing the temp database without saving
Expand Down Expand Up @@ -388,6 +394,62 @@ void TestGuiFdoSecrets::testServiceSearchBlockingUnlock()
}
}

void TestGuiFdoSecrets::testServiceSearchBlockingUnlockMultiple()
{
// setup: two databases, both locked, one with exposed db, the other not.

// add another database tab with a database with no exposed group
// to avoid modify the original, copy to a temp file first
QFile sourceDbFile(QStringLiteral(KEEPASSX_TEST_DATA_DIR "/NewDatabase2.kdbx"));
QByteArray dbData;
VERIFY(sourceDbFile.open(QIODevice::ReadOnly));
VERIFY(Tools::readAllFromDevice(&sourceDbFile, dbData));
sourceDbFile.close();

QTemporaryFile anotherFile;
VERIFY(anotherFile.open());
COMPARE(anotherFile.write(dbData), static_cast<qint64>(dbData.size()));
anotherFile.close();

m_tabWidget->addDatabaseTab(anotherFile.fileName(), false);
auto anotherWidget = m_tabWidget->currentDatabaseWidget();

auto service = enableService();
VERIFY(service);

// when there are multiple locked databases,
// repeatly show the dialog until there is at least one unlocked collection
FdoSecrets::settings()->setUnlockBeforeSearch(true);

// when only unlocking the one with no exposed group, a second dialog is shown
lockDatabaseInBackend();
{
bool unlockDialogWorks = false;
QTimer::singleShot(50, [&]() {
unlockDialogWorks = driveUnlockDialog(anotherWidget);
QTimer::singleShot(50, [&]() { unlockDialogWorks &= driveUnlockDialog(); });
});

DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
VERIFY(unlockDialogWorks);
COMPARE(locked, {});
COMPARE(unlocked.size(), 1);
}

// when unlocking the one with exposed group, the other one remains locked
lockDatabaseInBackend();
{
bool unlockDialogWorks = false;
QTimer::singleShot(50, [&]() { unlockDialogWorks = driveUnlockDialog(m_dbWidget); });

DBUS_GET2(unlocked, locked, service->SearchItems({{"Title", "Sample Entry"}}));
VERIFY(unlockDialogWorks);
COMPARE(locked, {});
COMPARE(unlocked.size(), 1);
VERIFY(anotherWidget->isLocked());
}
}

void TestGuiFdoSecrets::testServiceSearchForce()
{
auto service = enableService();
Expand Down Expand Up @@ -1625,7 +1687,7 @@ void TestGuiFdoSecrets::testNoExposeRecycleBin()

void TestGuiFdoSecrets::lockDatabaseInBackend()
{
m_dbWidget->lock();
m_tabWidget->lockDatabases();
m_db.reset();
processEvents();
}
Expand Down Expand Up @@ -1825,14 +1887,16 @@ bool TestGuiFdoSecrets::driveNewDatabaseWizard()
return ret;
}

bool TestGuiFdoSecrets::driveUnlockDialog()
bool TestGuiFdoSecrets::driveUnlockDialog(DatabaseWidget* target)
{
processEvents();
auto dbOpenDlg = m_tabWidget->findChild<DatabaseOpenDialog*>();
VERIFY(dbOpenDlg);
if (!dbOpenDlg->isVisible()) {
return false;
}
dbOpenDlg->setActiveDatabaseTab(target);

auto editPassword = dbOpenDlg->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
VERIFY(editPassword);
editPassword->setFocus();
Expand Down
3 changes: 2 additions & 1 deletion tests/gui/TestGuiFdoSecrets.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private slots:
void testServiceEnableNoExposedDatabase();
void testServiceSearch();
void testServiceSearchBlockingUnlock();
void testServiceSearchBlockingUnlockMultiple();
void testServiceSearchForce();
void testServiceUnlock();
void testServiceUnlockDatabaseConcurrent();
Expand Down Expand Up @@ -104,7 +105,7 @@ private slots:
void testDuplicateName();

private:
bool driveUnlockDialog();
bool driveUnlockDialog(DatabaseWidget* target = nullptr);
bool driveNewDatabaseWizard();
bool driveAccessControlDialog(bool remember = true, bool includeFutureEntries = false);
bool waitForSignal(QSignalSpy& spy, int expectedCount);
Expand Down