Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/gui/DatabaseWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 26 additions & 1 deletion src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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->setEnabled(sshAgent()->isEnabled());
#endif

m_ui->actionGroupNew->setEnabled(groupSelected && !inRecycleBin);
Expand Down Expand Up @@ -1482,6 +1486,24 @@ void MainWindow::clearSSHAgent()
#endif
}

void MainWindow::reloadSSHAgentKeys()
{
#ifdef KPXC_FEATURE_SSHAGENT
auto agent = SSHAgent::instance();

QList<QSharedPointer<Database>> 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()) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/gui/MainWindow.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -155,6 +155,7 @@ private slots:
void enableMenuAndToolbar();
void disableMenuAndToolbar();
void clearSSHAgent();
void reloadSSHAgentKeys();

private:
static const QString BaseWindowTitle;
Expand Down
12 changes: 12 additions & 0 deletions src/gui/MainWindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@
</property>
<addaction name="actionPasswordGenerator"/>
<addaction name="actionClearSSHAgent"/>
<addaction name="actionReloadSSHAgentKeys"/>
<addaction name="actionSettings"/>
</widget>
<widget class="QMenu" name="menuView">
Expand Down Expand Up @@ -1367,6 +1368,17 @@
<enum>QAction::TextHeuristicRole</enum>
</property>
</action>
<action name="actionReloadSSHAgentKeys">
<property name="text">
<string>Reload SSH Keys</string>
</property>
<property name="toolTip">
<string>Re-add missing KeePassXC identities to ssh-agent</string>
</property>
<property name="menuRole">
<enum>QAction::TextHeuristicRole</enum>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
Expand Down
4 changes: 2 additions & 2 deletions src/gui/entry/EditEntryWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
129 changes: 111 additions & 18 deletions src/sshagent/SSHAgent.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Toni Spets <toni.spets@iki.fi>
*
* This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<quint32>(settings.lifetimeConstraintDuration()));
request.write(static_cast<quint32>(settings.m_lifetimeConstraintDuration));
}

if (settings.useConfirmConstraintWhenAdding()) {
if (settings.m_useConfirmConstraintWhenAdding) {
request.write(SSH_AGENT_CONSTRAIN_CONFIRM);
}

Expand All @@ -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).");
}

Expand All @@ -331,9 +331,11 @@ bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, co
return false;
}

OpenSSHKey keyCopy = key;
keyCopy.clearPrivate();
m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose());
if (checkInAddedKeys) {
OpenSSHKey keyCopy = key;
keyCopy.clearPrivate();
m_addedKeys[keyCopy] = settings;
}
return true;
}

Expand Down Expand Up @@ -406,6 +408,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<QSharedPointer<Database>>& openDatabases)
{
if (!isAgentRunning()) {
m_error = tr("No agent running, cannot reload identities.");
return false;
}

bool ret = true;

QList<QSharedPointer<OpenSSHKey>> 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<Database> 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.
*
Expand Down Expand Up @@ -507,7 +599,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);
}
Expand All @@ -525,7 +617,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;
}
}

Expand All @@ -537,12 +629,12 @@ void SSHAgent::databaseLocked(const QSharedPointer<Database>& 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);
}
Expand Down Expand Up @@ -580,7 +672,8 @@ void SSHAgent::databaseUnlocked(const QSharedPointer<Database>& 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);
}
}
Expand Down
Loading
Loading