[plasma/ksshaskpass] /: Split prompt into its own static library
Kai Uwe Broulik <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit d3c16e7589099097d5e59c452d5d2574a685d4cd by Kai Uwe Broulik.
Committed on 17/08/2026 at 14:09.
Pushed by broulik into branch 'master'.
Split prompt into its own static library
So we can reuse it for unit testing.
M +2 -10 CMakeLists.txt
A +23 -0 src/CMakeLists.txt
M +2 -202 src/main.cpp
A +194 -0 src/prompt.cpp [License: GPL(v2.0+)]
A +27 -0 src/prompt.h [License: GPL(v2.0+)]
https://invent.kde.org/plasma/ksshaskpass/-/commit/d3c16e7589099097d5e59c452d5d2574a685d4cd
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6785ec7..c459a85 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,6 +21,7 @@ include(ECMQtDeclareLoggingCategory)
include(KDEClangFormat)
include(KDEGitCommitHooks)
include(ECMDeprecationSettings)
+include(ECMQtDeclareLoggingCategory)
find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core)
@@ -42,16 +43,7 @@ ecm_set_disabled_deprecation_versions(QT 6.8.1
KF 6.27.0
)
-set(ksshaskpass_SRCS src/main.cpp)
-
-add_executable(ksshaskpass ${ksshaskpass_SRCS})
-target_compile_definitions(ksshaskpass PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}")
-target_link_libraries(ksshaskpass
- KF6::CoreAddons
- KF6::I18n
- KF6::WidgetsAddons
- Qt6Keychain::Qt6Keychain
-)
+add_subdirectory(src)
# add clang-format target for all our real source files
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..2ec6914
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
+
+add_library(ksshaskpass_prompt OBJECT prompt.cpp prompt.h)
+
+target_include_directories(ksshaskpass_prompt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+ecm_qt_declare_logging_category(ksshaskpass_prompt HEADER debug.h
+ IDENTIFIER LOG_KSSHASKPASS
+ CATEGORY_NAME ksshaskpass
+ DESCRIPTION "ksshaskpass")
+
+target_link_libraries(ksshaskpass_prompt PUBLIC Qt6::Core)
+
+add_executable(ksshaskpass main.cpp)
+target_compile_definitions(ksshaskpass PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}")
+target_link_libraries(ksshaskpass
+ KF6::CoreAddons
+ KF6::I18n
+ KF6::WidgetsAddons
+ Qt6Keychain::Qt6Keychain
+ ksshaskpass_prompt
+)
diff --git a/src/main.cpp b/src/main.cpp
index a901d59..0f35f61 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -15,217 +15,17 @@
#include <QApplication>
#include <QCommandLineParser>
#include <QInputDialog>
-#include <QLoggingCategory>
#include <QPointer>
#include <QRegularExpression>
#include <QTextStream>
#include <qt6keychain/keychain.h>
-Q_LOGGING_CATEGORY(LOG_KSSHASKPASS, "ksshaskpass")
+#include "debug.h"
+#include "prompt.h"
constexpr const char *PROMPT_TYPE_ENV_VAR = "SSH_ASKPASS_PROMPT";
-// Standard prompt types defined by openssh.
-enum class PromptType {
- Confirm,
- Entry,
- None,
-};
-
-// Implemented UI display types.
-enum class DisplayType {
- Password,
- ClearText,
- Confirm,
- ConfirmCancel,
- UnknownSshHost
-};
-
-static void parsePrompt(PromptType promptType, const QString &prompt, QString &identifier, bool &ignoreKeychain, DisplayType &displayType)
-{
- if (promptType == PromptType::Confirm) {
- displayType = DisplayType::Confirm;
- ignoreKeychain = true;
- return;
- }
-
- if (promptType == PromptType::None) {
- displayType = DisplayType::ConfirmCancel;
- ignoreKeychain = true;
- return;
- }
-
- // "Entry" prompt type: password or text input. We parse several known prompts from openssh and git (which have no i18n)
- // to extract credential names and determine whether to use cleartext.
- QRegularExpressionMatch match;
-
- // openssh sshconnect2.c
- // Case: password for authentication on remote ssh server
- match = QRegularExpression(QStringLiteral("^(.*@.*)'s password: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // Case: password for authentication on remote ssh server, also supports PAM format
- match = QRegularExpression(QStringLiteral("^\\((.*@.*)\\) Password: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // openssh sshconnect2.c
- // Case: password change request
- match = QRegularExpression(QStringLiteral("^(Enter|Retype) (.*@.*)'s (old|new) password: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(2);
- displayType = DisplayType::Password;
- ignoreKeychain = true;
- return;
- }
-
- // openssh sshconnect2.c and sshconnect1.c
- // Case: asking for passphrase for a certain keyfile
- match = QRegularExpression(QStringLiteral("^Enter passphrase for( RSA)? key '(.*)': $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(2);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // openssh ssh-add.c
- // Case: asking for passphrase for a certain keyfile for the first time => we should try a password from the keychain
- match = QRegularExpression(QStringLiteral("^Enter passphrase for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // openssh ssh-add.c
- // Case: re-asking for passphrase for a certain keyfile => probably we’ve tried a password from the keychain, no point
- // in trying it again
- match = QRegularExpression(QStringLiteral("^Bad passphrase, try again for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = true;
- return;
- }
-
- // openssh ssh-pkcs11.c
- // Case: asking for PIN for some token label
- match = QRegularExpression(QStringLiteral("Enter PIN for '(.*)': $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // openssh ssh-agent.c
- // Case: asking to provide the PIN of the security key device
- // match after "for" is key type, match after "key" is SHA digest of key
- match = QRegularExpression(QStringLiteral("^Enter PIN( and confirm user presence)? for (.*?) key (.*?): $")).match(prompt);
- if (match.hasMatch()) {
- identifier = QStringLiteral("PIN:") + match.captured(3);
- displayType = DisplayType::Password;
- ignoreKeychain = true;
- return;
- }
-
- // google-authenticator-libpam pam_google_authenticator.c
- // Case: OTP verification code request from remote ssh server through PAM module
- match = QRegularExpression(QStringLiteral("Verification code: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = QString();
- displayType = DisplayType::ClearText;
- ignoreKeychain = true;
- return;
- }
-
- // git credential.c
- // Case: asking for username by git without specifying any other information
- match = QRegularExpression(QStringLiteral("^Username: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = QString();
- displayType = DisplayType::ClearText;
- ignoreKeychain = true;
- return;
- }
-
- // git credential.c
- // Case: asking for password by git without specifying any other information
- match = QRegularExpression(QStringLiteral("^Password: $")).match(prompt);
- if (match.hasMatch()) {
- identifier = QString();
- displayType = DisplayType::Password;
- ignoreKeychain = true;
- return;
- }
-
- // git credential.c
- // Case: asking for username by git for some identifier
- match = QRegularExpression(QStringLiteral("^Username for '(.*)': $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::ClearText;
- ignoreKeychain = false;
- return;
- }
-
- // git credential.c
- // Case: asking for password by git for some identifier
- match = QRegularExpression(QStringLiteral("^Password for '(.*)': $")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // Case: username extraction from git-lfs
- match = QRegularExpression(QStringLiteral("^Username for \"(.*?)\"$")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::ClearText;
- ignoreKeychain = false;
- return;
- }
-
- // Case: password extraction from git-lfs
- match = QRegularExpression(QStringLiteral("^Password for \"(.*?)\"$")).match(prompt);
- if (match.hasMatch()) {
- identifier = match.captured(1);
- displayType = DisplayType::Password;
- ignoreKeychain = false;
- return;
- }
-
- // Case: unknown SSH host
- match = QRegularExpression(QStringLiteral("^The authenticity of host '([^']+)(?: \\(([^)]+)\\))?' can't be established\\.\n"
- "([A-Z0-9_-]+) key fingerprint is ([A-Za-z0-9:+/=]+)\\.\n"
- "This key is not known by any other names\\.\n"
- "Are you sure you want to continue connecting \\(yes/no/\\[fingerprint\\]\\)\\?\\s*$"))
- .match(prompt);
- if (match.hasMatch()) {
- displayType = DisplayType::UnknownSshHost;
- ignoreKeychain = true;
- return;
- }
-
- // Nothing matched; either it was called by some sort of a script with a custom prompt (i.e. not ssh-add), or
- // strings we're looking for were broken. Issue a warning and continue without identifier.
- qCWarning(LOG_KSSHASKPASS) << "Unable to parse phrase" << prompt;
-}
-
static void execQKeychainJobBlocking(QKeychain::Job &job)
{
QEventLoop loop;
diff --git a/src/prompt.cpp b/src/prompt.cpp
new file mode 100644
index 0000000..2573768
--- /dev/null
+++ b/src/prompt.cpp
@@ -0,0 +1,194 @@
+/*
+ * SPDX-FileCopyrightText: 2006 Hans van Leeuwen <[email protected]>
+ * SPDX-FileCopyrightText: 2008-2010 Armin Berres <[email protected]>
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "prompt.h"
+#include "debug.h"
+
+#include <QRegularExpression>
+
+void parsePrompt(PromptType promptType, const QString &prompt, QString &identifier, bool &ignoreKeychain, DisplayType &displayType)
+{
+ if (promptType == PromptType::Confirm) {
+ displayType = DisplayType::Confirm;
+ ignoreKeychain = true;
+ return;
+ }
+
+ if (promptType == PromptType::None) {
+ displayType = DisplayType::ConfirmCancel;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // "Entry" prompt type: password or text input. We parse several known prompts from openssh and git (which have no i18n)
+ // to extract credential names and determine whether to use cleartext.
+ QRegularExpressionMatch match;
+
+ // openssh sshconnect2.c
+ // Case: password for authentication on remote ssh server
+ match = QRegularExpression(QStringLiteral("^(.*@.*)'s password: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // Case: password for authentication on remote ssh server, also supports PAM format
+ match = QRegularExpression(QStringLiteral("^\\((.*@.*)\\) Password: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // openssh sshconnect2.c
+ // Case: password change request
+ match = QRegularExpression(QStringLiteral("^(Enter|Retype) (.*@.*)'s (old|new) password: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(2);
+ displayType = DisplayType::Password;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // openssh sshconnect2.c and sshconnect1.c
+ // Case: asking for passphrase for a certain keyfile
+ match = QRegularExpression(QStringLiteral("^Enter passphrase for( RSA)? key '(.*)': $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(2);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // openssh ssh-add.c
+ // Case: asking for passphrase for a certain keyfile for the first time => we should try a password from the keychain
+ match = QRegularExpression(QStringLiteral("^Enter passphrase for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // openssh ssh-add.c
+ // Case: re-asking for passphrase for a certain keyfile => probably we’ve tried a password from the keychain, no point
+ // in trying it again
+ match = QRegularExpression(QStringLiteral("^Bad passphrase, try again for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // openssh ssh-pkcs11.c
+ // Case: asking for PIN for some token label
+ match = QRegularExpression(QStringLiteral("Enter PIN for '(.*)': $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // openssh ssh-agent.c
+ // Case: asking to provide the PIN of the security key device
+ // match after "for" is key type, match after "key" is SHA digest of key
+ match = QRegularExpression(QStringLiteral("^Enter PIN( and confirm user presence)? for (.*?) key (.*?): $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = QStringLiteral("PIN:") + match.captured(3);
+ displayType = DisplayType::Password;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // google-authenticator-libpam pam_google_authenticator.c
+ // Case: OTP verification code request from remote ssh server through PAM module
+ match = QRegularExpression(QStringLiteral("Verification code: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = QString();
+ displayType = DisplayType::ClearText;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // git credential.c
+ // Case: asking for username by git without specifying any other information
+ match = QRegularExpression(QStringLiteral("^Username: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = QString();
+ displayType = DisplayType::ClearText;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // git credential.c
+ // Case: asking for password by git without specifying any other information
+ match = QRegularExpression(QStringLiteral("^Password: $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = QString();
+ displayType = DisplayType::Password;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // git credential.c
+ // Case: asking for username by git for some identifier
+ match = QRegularExpression(QStringLiteral("^Username for '(.*)': $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::ClearText;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // git credential.c
+ // Case: asking for password by git for some identifier
+ match = QRegularExpression(QStringLiteral("^Password for '(.*)': $")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // Case: username extraction from git-lfs
+ match = QRegularExpression(QStringLiteral("^Username for \"(.*?)\"$")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::ClearText;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // Case: password extraction from git-lfs
+ match = QRegularExpression(QStringLiteral("^Password for \"(.*?)\"$")).match(prompt);
+ if (match.hasMatch()) {
+ identifier = match.captured(1);
+ displayType = DisplayType::Password;
+ ignoreKeychain = false;
+ return;
+ }
+
+ // Case: unknown SSH host
+ match = QRegularExpression(QStringLiteral("^The authenticity of host '([^']+)(?: \\(([^)]+)\\))?' can't be established\\.\n"
+ "([A-Z0-9_-]+) key fingerprint is ([A-Za-z0-9:+/=]+)\\.\n"
+ "This key is not known by any other names\\.\n"
+ "Are you sure you want to continue connecting \\(yes/no/\\[fingerprint\\]\\)\\?\\s*$"))
+ .match(prompt);
+ if (match.hasMatch()) {
+ displayType = DisplayType::UnknownSshHost;
+ ignoreKeychain = true;
+ return;
+ }
+
+ // Nothing matched; either it was called by some sort of a script with a custom prompt (i.e. not ssh-add), or
+ // strings we're looking for were broken. Issue a warning and continue without identifier.
+ qCWarning(LOG_KSSHASKPASS) << "Unable to parse phrase" << prompt;
+}
diff --git a/src/prompt.h b/src/prompt.h
new file mode 100644
index 0000000..682ffa6
--- /dev/null
+++ b/src/prompt.h
@@ -0,0 +1,27 @@
+/*
+ * SPDX-FileCopyrightText: 2006 Hans van Leeuwen <[email protected]>
+ * SPDX-FileCopyrightText: 2008-2010 Armin Berres <[email protected]>
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+class QString;
+
+// Standard prompt types defined by openssh.
+enum class PromptType {
+ Confirm,
+ Entry,
+ None,
+};
+
+// Implemented UI display types.
+enum class DisplayType {
+ Password,
+ ClearText,
+ Confirm,
+ ConfirmCancel,
+ UnknownSshHost
+};
+
+void parsePrompt(PromptType promptType, const QString &prompt, QString &identifier, bool &ignoreKeychain, DisplayType &displayType);