From 17b164f0cf0c42306a68385e0600c212b339cd87 Mon Sep 17 00:00:00 2001 From: Nikos Apostolakis Date: Mon, 24 Aug 2026 10:27:03 +0200 Subject: [PATCH 1/3] Add Reload SSH Agent action Signed-off-by: Nikos Apostolakis --- src/gui/DatabaseWidget.cpp | 3 +- src/gui/MainWindow.cpp | 25 ++++++ src/gui/MainWindow.h | 1 + src/gui/MainWindow.ui | 12 +++ src/gui/entry/EditEntryWidget.cpp | 4 +- src/sshagent/SSHAgent.cpp | 121 ++++++++++++++++++++++++++---- src/sshagent/SSHAgent.h | 32 +++++++- tests/TestSSHAgent.cpp | 18 ++--- 8 files changed, 186 insertions(+), 30 deletions(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 8a29d1822a..1cd81935f0 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -842,7 +842,8 @@ void DatabaseWidget::addToAgent() SSHAgent* agent = SSHAgent::instance(); OpenSSHKey key; if (settings.toOpenSSHKey(currentEntry, key, true)) { - if (!agent->addIdentity(key, settings, database()->uuid())) { + SshKeySettings sshKeySettings = keeAgentToSshKeySettings(settings, database()->uuid(), currentEntry->uuid()); + if (!agent->addIdentity(key, sshKeySettings)) { m_messageWidget->showMessage(agent->errorString(), MessageWidget::Error); } } else { diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 9b39a62e4a..b23fcd9aec 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -213,6 +213,7 @@ MainWindow::MainWindow() connect(sshAgent(), SIGNAL(error(QString)), this, SLOT(showErrorMessage(QString))); connect(sshAgent(), SIGNAL(enabledChanged(bool)), this, SLOT(agentEnabled(bool))); connect(m_ui->actionClearSSHAgent, SIGNAL(triggered()), SLOT(clearSSHAgent())); + connect(m_ui->actionReloadSSHAgentKeys, SIGNAL(triggered()), SLOT(reloadSSHAgentKeys())); m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage()); #else agentEnabled(false); @@ -394,6 +395,7 @@ MainWindow::MainWindow() m_ui->actionSettings->setIcon(icons()->icon("configure")); m_ui->actionPasswordGenerator->setIcon(icons()->icon("password-generator")); m_ui->actionClearSSHAgent->setIcon(icons()->icon("utilities-terminal")); + m_ui->actionReloadSSHAgentKeys->setIcon(icons()->icon("utilities-terminal")); m_ui->actionAbout->setIcon(icons()->icon("help-about")); m_ui->actionDonate->setIcon(icons()->icon("donate")); @@ -970,6 +972,8 @@ void MainWindow::updateMenuActionState() m_ui->actionEntryRemoveFromAgent->setEnabled(hasSSHKey); m_ui->actionClearSSHAgent->setVisible(sshAgent()->isEnabled()); m_ui->actionClearSSHAgent->setEnabled(sshAgent()->isEnabled()); + m_ui->actionReloadSSHAgentKeys->setVisible(sshAgent()->isEnabled()); + m_ui->actionReloadSSHAgentKeys->setVisible(sshAgent()->isEnabled()); #endif m_ui->actionGroupNew->setEnabled(groupSelected && !inRecycleBin); @@ -1482,6 +1486,24 @@ void MainWindow::clearSSHAgent() #endif } +void MainWindow::reloadSSHAgentKeys() +{ +#ifdef KPXC_FEATURE_SSHAGENT + auto agent = SSHAgent::instance(); + + QList> openDatabases; + for (int i = 0; i != m_ui->tabWidget->count(); ++i) { + auto dbWidget = m_ui->tabWidget->databaseWidgetFromIndex(i); + if (dbWidget && !dbWidget->isLocked()) { + openDatabases << dbWidget->database(); + } + } + + auto ret = agent->reloadAllAgentIdentities(openDatabases); + displayGlobalMessage(agent->errorString(), ret ? MessageWidget::Positive : KMessageWidget::Error, false); +#endif +} + void MainWindow::saveWindowInformation() { if (isVisible()) { @@ -1615,6 +1637,8 @@ void MainWindow::agentEnabled(bool enabled) m_ui->actionEntryRemoveFromAgent->setVisible(enabled); m_ui->actionClearSSHAgent->setEnabled(enabled); m_ui->actionClearSSHAgent->setVisible(enabled); + m_ui->actionReloadSSHAgentKeys->setEnabled(enabled); + m_ui->actionReloadSSHAgentKeys->setVisible(enabled); } void MainWindow::showEntryContextMenu(const QPoint& globalPos) @@ -2106,6 +2130,7 @@ void MainWindow::initActionCollection() // Tools Menu m_ui->actionPasswordGenerator, m_ui->actionClearSSHAgent, + m_ui->actionReloadSSHAgentKeys, m_ui->actionSettings, // View Menu m_ui->actionThemeAuto, diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 2fa874e0ad..12da15a5de 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -155,6 +155,7 @@ private slots: void enableMenuAndToolbar(); void disableMenuAndToolbar(); void clearSSHAgent(); + void reloadSSHAgentKeys(); private: static const QString BaseWindowTitle; diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 79f2ab36ca..7ffdc25115 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -373,6 +373,7 @@ + @@ -1367,6 +1368,17 @@ QAction::TextHeuristicRole + + + Reload SSH Keys + + + Reload all identities in ssh-agent + + + QAction::TextHeuristicRole + + diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 5270a3e1e1..bcf9db78d2 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -828,8 +828,8 @@ void EditEntryWidget::addKeyToAgent() KeeAgentSettings settings; toKeeAgentSettings(settings); - - if (!sshAgent()->addIdentity(key, settings, m_db->uuid())) { + SshKeySettings sshKeySettings = keeAgentToSshKeySettings(settings, m_db->uuid(), m_entry->uuid()); + if (!sshAgent()->addIdentity(key, sshKeySettings)) { showMessage(sshAgent()->errorString(), MessageWidget::Error); return; } diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index ae63d11922..5e5a2a96b7 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -266,17 +266,17 @@ bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out) * * @param key identity / key to add * @param settings constraints (lifetime, confirm), remove-on-lock - * @param databaseUuid database that owns the key for remove-on-lock + * @param checkInAddedKeys check within the cached keys whether the key exists. Should be false in reload. * @return true on success */ -bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, const QUuid& databaseUuid) +bool SSHAgent::addIdentity(OpenSSHKey& key, const SshKeySettings& settings, bool checkInAddedKeys) { if (!isAgentRunning()) { m_error = tr("No agent running, cannot add identity."); return false; } - if (m_addedKeys.contains(key) && m_addedKeys[key].first != databaseUuid) { + if (checkInAddedKeys && m_addedKeys.contains(key) && m_addedKeys[key].m_databaseUuid != settings.m_databaseUuid) { m_error = tr("Key identity ownership conflict. Refusing to add."); return false; } @@ -286,17 +286,17 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co bool isSecurityKey = key.type().startsWith("sk-"); request.write( - (settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding() || isSecurityKey) + (settings.m_useLifetimeConstraintWhenAdding || settings.m_useConfirmConstraintWhenAdding || isSecurityKey) ? SSH_AGENTC_ADD_ID_CONSTRAINED : SSH_AGENTC_ADD_IDENTITY); key.writePrivate(request); - if (settings.useLifetimeConstraintWhenAdding()) { + if (settings.m_useLifetimeConstraintWhenAdding) { request.write(SSH_AGENT_CONSTRAIN_LIFETIME); - request.write(static_cast(settings.lifetimeConstraintDuration())); + request.write(static_cast(settings.m_lifetimeConstraintDuration)); } - if (settings.useConfirmConstraintWhenAdding()) { + if (settings.m_useConfirmConstraintWhenAdding) { request.write(SSH_AGENT_CONSTRAIN_CONFIRM); } @@ -315,11 +315,11 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co m_error = tr("Agent refused this identity. Possible reasons include:") + "\n" + tr("The key has already been added."); - if (settings.useLifetimeConstraintWhenAdding()) { + if (settings.m_useLifetimeConstraintWhenAdding) { m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options)."); } - if (settings.useConfirmConstraintWhenAdding()) { + if (settings.m_useConfirmConstraintWhenAdding) { m_error += "\n" + tr("A confirmation request is not supported by the agent (check options)."); } @@ -333,7 +333,7 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co OpenSSHKey keyCopy = key; keyCopy.clearPrivate(); - m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose()); + m_addedKeys[keyCopy] = settings; return true; } @@ -406,6 +406,96 @@ bool SSHAgent::clearAllAgentIdentities() return ret; } +/** + * Re-add any previously added identity that is no longer loaded in the SSH agent + * (e.g. because the agent was restarted). + * + * @param openDatabases databases that are currently open and unlocked + * @return true if every reloadable identity was successfully re-added + */ +bool SSHAgent::reloadAllAgentIdentities(const QList>& openDatabases) +{ + if (!isAgentRunning()) { + m_error = tr("No agent running, cannot reload identities."); + return false; + } + + bool ret = true; + + QList> identityList; + if (!listIdentities(identityList)) { + m_error = tr("Could not list identities."); + return false; + } + + auto it = m_addedKeys.begin(); + while (it != m_addedKeys.end()) { + const OpenSSHKey& openSshKey = it.key(); + const SshKeySettings& sshKeySettings = it.value(); + + // Skip identities that are already loaded in the agent. + bool loaded = false; + for (const auto& identityListIt : identityList) { + if (*identityListIt == openSshKey) { + loaded = true; + break; + } + } + if (loaded) { + ++it; + continue; + } + + // Not found just means the database is locked/closed right now (may be transient). + // keep tracking the identity and retry next time. + QSharedPointer db; + for (const auto& openDb : openDatabases) { + if (openDb && openDb->uuid() == sshKeySettings.m_databaseUuid) { + db = openDb; + break; + } + } + if (!db) { + ++it; + continue; + } + + // The entry is gone, stop tracking it. + Entry* entry = db->rootGroup()->findEntryByUuid(sshKeySettings.m_entryUuid); + if (!entry || entry->isRecycled()) { + it = m_addedKeys.erase(it); + continue; + } + + // The user turned off SSH agent use for this entry, stop tracking it. + KeeAgentSettings keeAgentSettings; + if (!keeAgentSettings.fromEntry(entry) || !keeAgentSettings.allowUseOfSshKey()) { + it = m_addedKeys.erase(it); + continue; + } + + // Decryption failure (may be transient). keep tracking the identity and retry next time. + OpenSSHKey freshKey; + if (!keeAgentSettings.toOpenSSHKey(entry, freshKey, true)) { + ++it; + continue; + } + + // The agent may have rejected this for a retryable reason- keep tracking the identity and retry next time. + if (!addIdentity(freshKey, sshKeySettings, false)) { + ret = false; + } + ++it; + } + + if (ret) { + m_error = tr("All SSH identities reloaded to agent."); + } else { + m_error = tr("One or more SSH identities could not be reloaded to agent."); + } + return ret; +} + /** * Get a list of identities from the SSH agent. * @@ -507,7 +597,7 @@ void SSHAgent::removeAllIdentities() auto it = m_addedKeys.begin(); while (it != m_addedKeys.end()) { // Remove key if requested to remove on lock - if (it.value().second) { + if (it.value().m_removeAtDatabaseClose) { OpenSSHKey key = it.key(); removeIdentity(key); } @@ -525,7 +615,7 @@ void SSHAgent::removeAllIdentities() void SSHAgent::setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove) { if (m_addedKeys.contains(key)) { - m_addedKeys[key].second = autoRemove; + m_addedKeys[key].m_removeAtDatabaseClose = autoRemove; } } @@ -537,12 +627,12 @@ void SSHAgent::databaseLocked(const QSharedPointer& db) auto it = m_addedKeys.begin(); while (it != m_addedKeys.end()) { - if (it.value().first != db->uuid()) { + if (it.value().m_databaseUuid != db->uuid()) { ++it; continue; } OpenSSHKey key = it.key(); - if (it.value().second) { + if (it.value().m_removeAtDatabaseClose) { if (!removeIdentity(key)) { emit error(m_error); } @@ -580,7 +670,8 @@ void SSHAgent::databaseUnlocked(const QSharedPointer& db) // Add key to agent; ignore errors if we have previously added the key bool known_key = m_addedKeys.contains(key); - if (!addIdentity(key, settings, db->uuid()) && !known_key) { + SshKeySettings sshKeySettings = keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()); + if (!addIdentity(key, sshKeySettings) && !known_key) { emit error(m_error); } } diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index d3eeb4ebc9..bf324571e1 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -20,12 +20,22 @@ #define KEEPASSXC_SSHAGENT_H #include +#include #include "OpenSSHKey.h" +#include "sshagent/KeeAgentSettings.h" -class KeeAgentSettings; class Database; +struct SshKeySettings { + QUuid m_databaseUuid; + QUuid m_entryUuid; + bool m_useLifetimeConstraintWhenAdding; + bool m_useConfirmConstraintWhenAdding; + int m_lifetimeConstraintDuration; + bool m_removeAtDatabaseClose; +}; + class SSHAgent : public QObject { Q_OBJECT @@ -51,12 +61,13 @@ class SSHAgent : public QObject const QString errorString() const; bool isAgentRunning() const; - bool addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, const QUuid& databaseUuid); + bool addIdentity(OpenSSHKey& key, const SshKeySettings& settings, bool checkInAddedKeys = true); bool listIdentities(QList>& list); bool checkIdentity(const OpenSSHKey& key, bool& loaded); bool removeIdentity(OpenSSHKey& key); void removeAllIdentities(); bool clearAllAgentIdentities(); + bool reloadAllAgentIdentities(const QList>& openDatabases); void setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove); signals: @@ -91,7 +102,7 @@ public slots: const quint32 AGENT_COPYDATA_ID = 0x804e50ba; #endif - QHash> m_addedKeys; + QHash m_addedKeys; QString m_error; }; @@ -100,4 +111,19 @@ static inline SSHAgent* sshAgent() return SSHAgent::instance(); } +static inline SshKeySettings keeAgentToSshKeySettings(const KeeAgentSettings& keeAgentSettings, + const QUuid& databaseUuid, + const QUuid& entryUuid) { + SshKeySettings sshKeySettings; + sshKeySettings.m_databaseUuid = databaseUuid; + sshKeySettings.m_entryUuid = entryUuid; + sshKeySettings.m_useLifetimeConstraintWhenAdding = keeAgentSettings.useLifetimeConstraintWhenAdding(); + if (sshKeySettings.m_useLifetimeConstraintWhenAdding) { + sshKeySettings.m_lifetimeConstraintDuration = keeAgentSettings.lifetimeConstraintDuration(); + } + sshKeySettings.m_useConfirmConstraintWhenAdding = keeAgentSettings.useConfirmConstraintWhenAdding(); + sshKeySettings.m_removeAtDatabaseClose = keeAgentSettings.removeAtDatabaseClose(); + return sshKeySettings; +} + #endif // KEEPASSXC_SSHAGENT_H diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp index 6ff0f18a7a..ed943e7e56 100644 --- a/tests/TestSSHAgent.cpp +++ b/tests/TestSSHAgent.cpp @@ -131,15 +131,15 @@ void TestSSHAgent::testIdentity() bool keyInAgent; // test adding a key works - QVERIFY(agent.addIdentity(m_key, settings, m_uuid)); + QVERIFY(agent.addIdentity(m_key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); // test non-conflicting key ownership doesn't throw an error - QVERIFY(agent.addIdentity(m_key, settings, m_uuid)); + QVERIFY(agent.addIdentity(m_key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); // test conflicting key ownership throws an error QUuid secondUuid("{11111111-1111-1111-1111-111111111111}"); - QVERIFY(!agent.addIdentity(m_key, settings, secondUuid)); + QVERIFY(!agent.addIdentity(m_key, keeAgentToSshKeySettings(settings, secondUuid, QUuid::createUuid()))); // test removing a key works QVERIFY(agent.removeIdentity(m_key)); @@ -158,7 +158,7 @@ void TestSSHAgent::testRemoveOnClose() bool keyInAgent; settings.setRemoveAtDatabaseClose(true); - QVERIFY(agent.addIdentity(m_key, settings, m_uuid)); + QVERIFY(agent.addIdentity(m_key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); agent.setEnabled(false); QVERIFY(agent.checkIdentity(m_key, keyInAgent) && !keyInAgent); @@ -179,7 +179,7 @@ void TestSSHAgent::testLifetimeConstraint() settings.setLifetimeConstraintDuration(2); // two seconds // identity should be in agent immediately after adding - QVERIFY(agent.addIdentity(m_key, settings, m_uuid)); + QVERIFY(agent.addIdentity(m_key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); QElapsedTimer timer; @@ -212,7 +212,7 @@ void TestSSHAgent::testConfirmConstraint() settings.setUseConfirmConstraintWhenAdding(true); - QVERIFY(agent.addIdentity(m_key, settings, m_uuid)); + QVERIFY(agent.addIdentity(m_key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); // we can't test confirmation itself is working but we can test the agent accepts the key QVERIFY(agent.checkIdentity(m_key, keyInAgent) && keyInAgent); @@ -247,7 +247,7 @@ void TestSSHAgent::testKeyGenRSA() QVERIFY(OpenSSHKeyGen::generateRSA(key, 2048)); - QVERIFY(agent.addIdentity(key, settings, m_uuid)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); QVERIFY(agent.removeIdentity(key)); QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); @@ -267,7 +267,7 @@ void TestSSHAgent::testKeyGenECDSA() QVERIFY(OpenSSHKeyGen::generateECDSA(key, 256)); - QVERIFY(agent.addIdentity(key, settings, m_uuid)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); QVERIFY(agent.removeIdentity(key)); QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); @@ -287,7 +287,7 @@ void TestSSHAgent::testKeyGenEd25519() QVERIFY(OpenSSHKeyGen::generateEd25519(key)); - QVERIFY(agent.addIdentity(key, settings, m_uuid)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, m_uuid, QUuid::createUuid()))); QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); QVERIFY(agent.removeIdentity(key)); QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); From 204d4c2696f61ec4017ea9e9dc268dc1d7ffd397 Mon Sep 17 00:00:00 2001 From: Nikos Apostolakis Date: Mon, 24 Aug 2026 14:10:05 +0200 Subject: [PATCH 2/3] Add unit tests for the reload ssh agent keys action Signed-off-by: Nikos Apostolakis --- tests/TestSSHAgent.cpp | 198 +++++++++++++++++++++++++++++++++++++++++ tests/TestSSHAgent.h | 12 +++ 2 files changed, 210 insertions(+) diff --git a/tests/TestSSHAgent.cpp b/tests/TestSSHAgent.cpp index ed943e7e56..71cdc62885 100644 --- a/tests/TestSSHAgent.cpp +++ b/tests/TestSSHAgent.cpp @@ -18,6 +18,10 @@ #include "TestSSHAgent.h" #include "config-keepassx-tests.h" #include "core/Config.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/EntryAttachments.h" +#include "core/Group.h" #include "crypto/Crypto.h" #include "sshagent/KeeAgentSettings.h" #include "sshagent/OpenSSHKeyGen.h" @@ -293,6 +297,200 @@ void TestSSHAgent::testKeyGenEd25519() QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); } +bool TestSSHAgent::buildTestEntry(QSharedPointer& db, Entry*& entry, OpenSSHKey& key) +{ + db = QSharedPointer::create(); + entry = new Entry(); + entry->setUuid(QUuid::createUuid()); + entry->setGroup(db->rootGroup()); + + if (!OpenSSHKeyGen::generateEd25519(key)) { + return false; + } + entry->attachments()->set("id_ed25519", key.privateKey().toUtf8()); + + KeeAgentSettings settings; + settings.setAllowUseOfSshKey(true); + settings.setAddAtDatabaseOpen(true); + settings.setSelectedType("attachment"); + settings.setAttachmentName("id_ed25519"); + settings.toEntry(entry); + + return true; +} + +void TestSSHAgent::testReloadReAddsMissingIdentity() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + QVERIFY(agent.isAgentRunning()); + + QSharedPointer db; + Entry* entry; + OpenSSHKey key; + QVERIFY(buildTestEntry(db, entry, key)); + + KeeAgentSettings settings; + QVERIFY(settings.fromEntry(entry)); + auto sshKeySettings = keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()); + QVERIFY(agent.addIdentity(key, sshKeySettings)); + + bool keyInAgent = false; + QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); + + QVERIFY(agent.removeIdentity(key)); + QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); + + QVERIFY(agent.reloadAllAgentIdentities({db})); + QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); +} + +void TestSSHAgent::testReloadTerminatesWhenAlreadyLoaded() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + QVERIFY(agent.isAgentRunning()); + + QSharedPointer db; + Entry* entry; + OpenSSHKey key; + QVERIFY(buildTestEntry(db, entry, key)); + + KeeAgentSettings settings; + QVERIFY(settings.fromEntry(entry)); + auto sshKeySettings = keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()); + QVERIFY(agent.addIdentity(key, sshKeySettings)); + + // Regression test: reloading an already loaded entity has no effect. + QVERIFY(agent.reloadAllAgentIdentities({db})); + + bool keyInAgent = false; + QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); +} + +void TestSSHAgent::testReloadProcessesAllIdentitiesDespiteEarlierFailure() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + QVERIFY(agent.isAgentRunning()); + + // First identity: its database will not be passed to reloadAllAgentIdentities(), + // so it can't be recovered. + QSharedPointer unresolvableDb; + Entry* unresolvableEntry; + OpenSSHKey unresolvableKey; + QVERIFY(buildTestEntry(unresolvableDb, unresolvableEntry, unresolvableKey)); + KeeAgentSettings unresolvableSettings; + QVERIFY(unresolvableSettings.fromEntry(unresolvableEntry)); + QVERIFY(agent.addIdentity( + unresolvableKey, keeAgentToSshKeySettings(unresolvableSettings, unresolvableDb->uuid(), unresolvableEntry->uuid()))); + QVERIFY(agent.removeIdentity(unresolvableKey)); + + // Second identity: fully reloadable. + QSharedPointer db; + Entry* entry; + OpenSSHKey key; + QVERIFY(buildTestEntry(db, entry, key)); + KeeAgentSettings settings; + QVERIFY(settings.fromEntry(entry)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()))); + QVERIFY(agent.removeIdentity(key)); + + // Regression test: an identity that can't be resolved (since unresolvableDb is not passed as an argument here) + // must not stop later identities from being reloaded. + agent.reloadAllAgentIdentities({db}); + + bool keyInAgent = false; + QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); +} + +void TestSSHAgent::testReloadKeepsTrackingWhenDatabaseClosed() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + QVERIFY(agent.isAgentRunning()); + + QSharedPointer db; + Entry* entry; + OpenSSHKey key; + QVERIFY(buildTestEntry(db, entry, key)); + + KeeAgentSettings settings; + QVERIFY(settings.fromEntry(entry)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()))); + QVERIFY(agent.removeIdentity(key)); + + bool keyInAgent = false; + + // Database closed: reload can't recover the key right now + QVERIFY(agent.reloadAllAgentIdentities({})); + QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); + + // Database open: reload should recover the key now + QVERIFY(agent.reloadAllAgentIdentities({db})); + QVERIFY(agent.checkIdentity(key, keyInAgent) && keyInAgent); +} + +void TestSSHAgent::testReloadForgetsDeletedEntry() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + QVERIFY(agent.isAgentRunning()); + + QSharedPointer db; + Entry* entry; + OpenSSHKey key; + QVERIFY(buildTestEntry(db, entry, key)); + + KeeAgentSettings settings; + QVERIFY(settings.fromEntry(entry)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()))); + QVERIFY(agent.removeIdentity(key)); + + delete entry; + + bool keyInAgent = false; + QVERIFY(agent.reloadAllAgentIdentities({db})); + QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); +} + +void TestSSHAgent::testReloadForgetsWhenSshKeyUseDisabled() +{ + SSHAgent agent; + agent.setEnabled(true); + agent.setAuthSockOverride(m_agentSocketFileName); + QVERIFY(agent.isAgentRunning()); + + QSharedPointer db; + Entry* entry; + OpenSSHKey key; + QVERIFY(buildTestEntry(db, entry, key)); + + KeeAgentSettings settings; + QVERIFY(settings.fromEntry(entry)); + QVERIFY(agent.addIdentity(key, keeAgentToSshKeySettings(settings, db->uuid(), entry->uuid()))); + QVERIFY(agent.removeIdentity(key)); + + // User disables SSH agent use for this entry before the next reload. + settings.setAllowUseOfSshKey(false); + settings.toEntry(entry); + + bool keyInAgent = false; + QVERIFY(agent.reloadAllAgentIdentities({db})); + QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); + + // Re-enabling afterwards must NOT resurrect it - the tracked identity was already dropped. + settings.setAllowUseOfSshKey(true); + settings.toEntry(entry); + QVERIFY(agent.reloadAllAgentIdentities({db})); + QVERIFY(agent.checkIdentity(key, keyInAgent) && !keyInAgent); +} + void TestSSHAgent::cleanupTestCase() { if (m_agentProcess.state() != QProcess::NotRunning) { diff --git a/tests/TestSSHAgent.h b/tests/TestSSHAgent.h index db06fd8069..92e7b7a78a 100644 --- a/tests/TestSSHAgent.h +++ b/tests/TestSSHAgent.h @@ -21,8 +21,12 @@ #include "sshagent/OpenSSHKey.h" #include "util/TemporaryFile.h" #include +#include #include +class Database; +class Entry; + class TestSSHAgent : public QObject { Q_OBJECT @@ -39,9 +43,17 @@ private slots: void testKeyGenRSA(); void testKeyGenECDSA(); void testKeyGenEd25519(); + void testReloadReAddsMissingIdentity(); + void testReloadTerminatesWhenAlreadyLoaded(); + void testReloadProcessesAllIdentitiesDespiteEarlierFailure(); + void testReloadKeepsTrackingWhenDatabaseClosed(); + void testReloadForgetsDeletedEntry(); + void testReloadForgetsWhenSshKeyUseDisabled(); void cleanupTestCase(); private: + bool buildTestEntry(QSharedPointer& db, Entry*& entry, OpenSSHKey& key); + QScopedPointer m_agentSocketFile; QString m_agentSocketFileName; QProcess m_agentProcess; From df1c2e805d3d1a1525f85a181086a171e51c9612 Mon Sep 17 00:00:00 2001 From: Nikos Apostolakis Date: Fri, 28 Aug 2026 18:49:42 +0200 Subject: [PATCH 3/3] Reload SSH Agent Identities: Update header and minor fixes Signed-off-by: Nikos Apostolakis --- src/gui/MainWindow.cpp | 4 ++-- src/gui/MainWindow.h | 2 +- src/gui/MainWindow.ui | 2 +- src/sshagent/SSHAgent.cpp | 10 ++++++---- src/sshagent/SSHAgent.h | 16 ++++++++-------- tests/TestSSHAgent.h | 2 +- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index b23fcd9aec..897468ff52 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2010 Felix Geyer * * This program is free software: you can redistribute it and/or modify @@ -973,7 +973,7 @@ void MainWindow::updateMenuActionState() m_ui->actionClearSSHAgent->setVisible(sshAgent()->isEnabled()); m_ui->actionClearSSHAgent->setEnabled(sshAgent()->isEnabled()); m_ui->actionReloadSSHAgentKeys->setVisible(sshAgent()->isEnabled()); - m_ui->actionReloadSSHAgentKeys->setVisible(sshAgent()->isEnabled()); + m_ui->actionReloadSSHAgentKeys->setEnabled(sshAgent()->isEnabled()); #endif m_ui->actionGroupNew->setEnabled(groupSelected && !inRecycleBin); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 12da15a5de..8023775d8b 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2010 Felix Geyer * * This program is free software: you can redistribute it and/or modify diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 7ffdc25115..acc9c2c0c3 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -1373,7 +1373,7 @@ Reload SSH Keys - Reload all identities in ssh-agent + Re-add missing KeePassXC identities to ssh-agent QAction::TextHeuristicRole diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 5e5a2a96b7..0d96c623df 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2025 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2017 Toni Spets * * This program is free software: you can redistribute it and/or modify @@ -331,9 +331,11 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const SshKeySettings& settings, bool return false; } - OpenSSHKey keyCopy = key; - keyCopy.clearPrivate(); - m_addedKeys[keyCopy] = settings; + if (checkInAddedKeys) { + OpenSSHKey keyCopy = key; + keyCopy.clearPrivate(); + m_addedKeys[keyCopy] = settings; + } return true; } diff --git a/src/sshagent/SSHAgent.h b/src/sshagent/SSHAgent.h index bf324571e1..209a2484b7 100644 --- a/src/sshagent/SSHAgent.h +++ b/src/sshagent/SSHAgent.h @@ -1,6 +1,6 @@ /* + * Copyright (C) 2026 KeePassXC Team * Copyright (C) 2017 Toni Spets - * Copyright (C) 2017 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,7 +20,7 @@ #define KEEPASSXC_SSHAGENT_H #include -#include +#include #include "OpenSSHKey.h" #include "sshagent/KeeAgentSettings.h" @@ -28,12 +28,12 @@ class Database; struct SshKeySettings { - QUuid m_databaseUuid; - QUuid m_entryUuid; - bool m_useLifetimeConstraintWhenAdding; - bool m_useConfirmConstraintWhenAdding; - int m_lifetimeConstraintDuration; - bool m_removeAtDatabaseClose; + QUuid m_databaseUuid{}; + QUuid m_entryUuid{}; + bool m_useLifetimeConstraintWhenAdding{false}; + bool m_useConfirmConstraintWhenAdding{false}; + int m_lifetimeConstraintDuration{0}; + bool m_removeAtDatabaseClose{false}; }; class SSHAgent : public QObject diff --git a/tests/TestSSHAgent.h b/tests/TestSSHAgent.h index 92e7b7a78a..4aebcc6a9f 100644 --- a/tests/TestSSHAgent.h +++ b/tests/TestSSHAgent.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 KeePassXC Team + * Copyright (C) 2026 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by