File indexing completed on 2024-04-28 16:45:16

0001 /*
0002  *   SPDX-FileCopyrightText: 2006 Hans van Leeuwen <hanz@hanz.nl>
0003  *   SPDX-FileCopyrightText: 2008-2010 Armin Berres <armin@space-based.de>
0004  *   SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include <memory>
0008 #include <sys/resource.h>
0009 
0010 #include <KAboutData>
0011 #include <KLocalizedString>
0012 #include <KMessageBox>
0013 #include <KPasswordDialog>
0014 #include <kwallet.h>
0015 #include <kwidgetsaddons_version.h>
0016 
0017 #include <QApplication>
0018 #include <QCommandLineOption>
0019 #include <QCommandLineParser>
0020 #include <QInputDialog>
0021 #include <QLoggingCategory>
0022 #include <QPointer>
0023 #include <QRegularExpression>
0024 #include <QTextStream>
0025 
0026 Q_LOGGING_CATEGORY(LOG_KSSHASKPASS, "ksshaskpass")
0027 
0028 enum Type {
0029     TypePassword,
0030     TypeClearText,
0031     TypeConfirm,
0032 };
0033 
0034 // Try to understand what we're asked for by parsing the phrase. Unfortunately, sshaskpass interface does not
0035 // include any saner methods to pass the action or the name of the keyfile. Fortunately, openssh and git
0036 // has no i18n, so this should work for all languages as long as the string is unchanged.
0037 static void parsePrompt(const QString &prompt, QString &identifier, bool &ignoreWallet, enum Type &type)
0038 {
0039     QRegularExpressionMatch match;
0040 
0041     // openssh sshconnect2.c
0042     // Case: password for authentication on remote ssh server
0043     match = QRegularExpression(QStringLiteral("^(.*@.*)'s password( \\(JPAKE\\))?: $")).match(prompt);
0044     if (match.hasMatch()) {
0045         identifier = match.captured(1);
0046         type = TypePassword;
0047         ignoreWallet = false;
0048         return;
0049     }
0050 
0051     // openssh sshconnect2.c
0052     // Case: password change request
0053     match = QRegularExpression(QStringLiteral("^(Enter|Retype) (.*@.*)'s (old|new) password: $")).match(prompt);
0054     if (match.hasMatch()) {
0055         identifier = match.captured(2);
0056         type = TypePassword;
0057         ignoreWallet = true;
0058         return;
0059     }
0060 
0061     // openssh sshconnect2.c and sshconnect1.c
0062     // Case: asking for passphrase for a certain keyfile
0063     match = QRegularExpression(QStringLiteral("^Enter passphrase for( RSA)? key '(.*)': $")).match(prompt);
0064     if (match.hasMatch()) {
0065         identifier = match.captured(2);
0066         type = TypePassword;
0067         ignoreWallet = false;
0068         return;
0069     }
0070 
0071     // openssh ssh-add.c
0072     // Case: asking for passphrase for a certain keyfile for the first time => we should try a password from the wallet
0073     match = QRegularExpression(QStringLiteral("^Enter passphrase for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
0074     if (match.hasMatch()) {
0075         identifier = match.captured(1);
0076         type = TypePassword;
0077         ignoreWallet = false;
0078         return;
0079     }
0080 
0081     // openssh ssh-add.c
0082     // Case: re-asking for passphrase for a certain keyfile => probably we've tried a password from the wallet, no point
0083     // in trying it again
0084     match = QRegularExpression(QStringLiteral("^Bad passphrase, try again for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
0085     if (match.hasMatch()) {
0086         identifier = match.captured(1);
0087         type = TypePassword;
0088         ignoreWallet = true;
0089         return;
0090     }
0091 
0092     // openssh ssh-pkcs11.c
0093     // Case: asking for PIN for some token label
0094     match = QRegularExpression(QStringLiteral("Enter PIN for '(.*)': $")).match(prompt);
0095     if (match.hasMatch()) {
0096         identifier = match.captured(1);
0097         type = TypePassword;
0098         ignoreWallet = false;
0099         return;
0100     }
0101 
0102     // openssh mux.c
0103     match = QRegularExpression(QStringLiteral("^(Allow|Terminate) shared connection to (.*)\\? $")).match(prompt);
0104     if (match.hasMatch()) {
0105         identifier = match.captured(2);
0106         type = TypeConfirm;
0107         ignoreWallet = true;
0108         return;
0109     }
0110 
0111     // openssh mux.c
0112     match = QRegularExpression(QStringLiteral("^Open (.* on .*)?$")).match(prompt);
0113     if (match.hasMatch()) {
0114         identifier = match.captured(1);
0115         type = TypeConfirm;
0116         ignoreWallet = true;
0117         return;
0118     }
0119 
0120     // openssh mux.c
0121     match = QRegularExpression(QStringLiteral("^Allow forward to (.*:.*)\\? $")).match(prompt);
0122     if (match.hasMatch()) {
0123         identifier = match.captured(1);
0124         type = TypeConfirm;
0125         ignoreWallet = true;
0126         return;
0127     }
0128 
0129     // openssh mux.c
0130     match = QRegularExpression(QStringLiteral("^Disable further multiplexing on shared connection to (.*)? $")).match(prompt);
0131     if (match.hasMatch()) {
0132         identifier = match.captured(1);
0133         type = TypeConfirm;
0134         ignoreWallet = true;
0135         return;
0136     }
0137 
0138     // openssh ssh-agent.c
0139     match = QRegularExpression(QStringLiteral("^Allow use of key (.*)?\\nKey fingerprint .*\\.$")).match(prompt);
0140     if (match.hasMatch()) {
0141         identifier = match.captured(1);
0142         type = TypeConfirm;
0143         ignoreWallet = true;
0144         return;
0145     }
0146 
0147     // openssh sshconnect.c
0148     match = QRegularExpression(QStringLiteral("^Add key (.*) \\(.*\\) to agent\\?$")).match(prompt);
0149     if (match.hasMatch()) {
0150         identifier = match.captured(1);
0151         type = TypeConfirm;
0152         ignoreWallet = true;
0153         return;
0154     }
0155 
0156     // git imap-send.c
0157     // Case: asking for password by git imap-send
0158     match = QRegularExpression(QStringLiteral("^Password \\((.*@.*)\\): $")).match(prompt);
0159     if (match.hasMatch()) {
0160         identifier = match.captured(1);
0161         type = TypePassword;
0162         ignoreWallet = false;
0163         return;
0164     }
0165 
0166     // git credential.c
0167     // Case: asking for username by git without specifying any other information
0168     match = QRegularExpression(QStringLiteral("^Username: $")).match(prompt);
0169     if (match.hasMatch()) {
0170         identifier = QString();
0171         type = TypeClearText;
0172         ignoreWallet = true;
0173         return;
0174     }
0175 
0176     // git credential.c
0177     // Case: asking for password by git without specifying any other information
0178     match = QRegularExpression(QStringLiteral("^Password: $")).match(prompt);
0179     if (match.hasMatch()) {
0180         identifier = QString();
0181         type = TypePassword;
0182         ignoreWallet = true;
0183         return;
0184     }
0185 
0186     // git credential.c
0187     // Case: asking for username by git for some identifier
0188     match = QRegularExpression(QStringLiteral("^Username for '(.*)': $")).match(prompt);
0189     if (match.hasMatch()) {
0190         identifier = match.captured(1);
0191         type = TypeClearText;
0192         ignoreWallet = false;
0193         return;
0194     }
0195 
0196     // git credential.c
0197     // Case: asking for password by git for some identifier
0198     match = QRegularExpression(QStringLiteral("^Password for '(.*)': $")).match(prompt);
0199     if (match.hasMatch()) {
0200         identifier = match.captured(1);
0201         type = TypePassword;
0202         ignoreWallet = false;
0203         return;
0204     }
0205 
0206     // Case: username extraction from git-lfs
0207     match = QRegularExpression(QStringLiteral("^Username for \"(.*?)\"$")).match(prompt);
0208     if (match.hasMatch()) {
0209         identifier = match.captured(1);
0210         type = TypeClearText;
0211         ignoreWallet = false;
0212         return;
0213     }
0214 
0215     // Case: password extraction from git-lfs
0216     match = QRegularExpression(QStringLiteral("^Password for \"(.*?)\"$")).match(prompt);
0217     if (match.hasMatch()) {
0218         identifier = match.captured(1);
0219         type = TypePassword;
0220         ignoreWallet = false;
0221         return;
0222     }
0223 
0224     // Case: password extraction from mercurial, see bug 380085
0225     match = QRegularExpression(QStringLiteral("^(.*?)'s password: $")).match(prompt);
0226     if (match.hasMatch()) {
0227         identifier = match.captured(1);
0228         type = TypePassword;
0229         ignoreWallet = false;
0230         return;
0231     }
0232 
0233     // Nothing matched; either it was called by some sort of a script with a custom prompt (i.e. not ssh-add), or
0234     // strings we're looking for were broken. Issue a warning and continue without identifier.
0235     qCWarning(LOG_KSSHASKPASS) << "Unable to parse phrase" << prompt;
0236 }
0237 
0238 int main(int argc, char **argv)
0239 {
0240 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0241     QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true);
0242 #endif
0243     QApplication app(argc, argv);
0244     KLocalizedString::setApplicationDomain("ksshaskpass");
0245 
0246     // TODO update it.
0247     KAboutData about(QStringLiteral("ksshaskpass"),
0248                      i18n("Ksshaskpass"),
0249                      QStringLiteral(PROJECT_VERSION),
0250                      i18n("KDE version of ssh-askpass"),
0251                      KAboutLicense::GPL,
0252                      i18n("(c) 2006 Hans van Leeuwen\n(c) 2008-2010 Armin Berres\n(c) 2013 Pali Rohár"),
0253                      i18n("Ksshaskpass allows you to interactively prompt users for a passphrase for ssh-add"),
0254                      QStringLiteral("https://commits.kde.org/ksshaskpass"),
0255                      QStringLiteral("armin@space-based.de"));
0256 
0257     about.addAuthor(i18n("Armin Berres"), i18n("Current author"), QStringLiteral("armin@space-based.de"));
0258     about.addAuthor(i18n("Hans van Leeuwen"), i18n("Original author"), QStringLiteral("hanz@hanz.nl"));
0259     about.addAuthor(i18n("Pali Rohár"), i18n("Contributor"), QStringLiteral("pali.rohar@gmail.com"));
0260     about.addAuthor(i18n("Armin Berres"), i18n("Current author"), QStringLiteral("armin@space-based.de"), QString());
0261     about.addAuthor(i18n("Hans van Leeuwen"), i18n("Original author"), QStringLiteral("hanz@hanz.nl"), QString());
0262     about.addAuthor(i18n("Pali Rohár"), i18n("Contributor"), QStringLiteral("pali.rohar@gmail.com"), QString());
0263     KAboutData::setApplicationData(about);
0264 
0265     QCommandLineParser parser;
0266     about.setupCommandLine(&parser);
0267     parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+[prompt]"), i18nc("Name of a prompt for a password", "Prompt")));
0268 
0269     parser.process(app);
0270     about.processCommandLine(&parser);
0271 
0272     const QString walletFolder = app.applicationName();
0273     QString dialog = i18n("Please enter passphrase"); // Default dialog text.
0274     QString identifier;
0275     QString item;
0276     bool ignoreWallet = false;
0277     enum Type type = TypePassword;
0278 
0279     // Parse commandline arguments
0280     if (!parser.positionalArguments().isEmpty()) {
0281         dialog = parser.positionalArguments().at(0);
0282         parsePrompt(dialog, identifier, ignoreWallet, type);
0283     }
0284 
0285     // Open KWallet to see if an item was previously stored
0286     std::unique_ptr<KWallet::Wallet> wallet(ignoreWallet ? nullptr : KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), 0));
0287 
0288     if ((!ignoreWallet) && (!identifier.isNull()) && wallet.get() && wallet->hasFolder(walletFolder)) {
0289         wallet->setFolder(walletFolder);
0290 
0291         wallet->readPassword(identifier, item);
0292 
0293         if (item.isEmpty()) {
0294             // There was a bug in previous versions of ksshaskpass that caused it to create keys with single quotes
0295             // around the identifier and even older versions have an extra space appended to the identifier.
0296             // key file name. Try these keys too, and, if there's a match, ensure that it's properly
0297             // replaced with proper one.
0298             for (auto templ : QStringList{QStringLiteral("'%0'"), QStringLiteral("%0 "), QStringLiteral("'%0' ")}) {
0299                 const QString keyFile = templ.arg(identifier);
0300                 wallet->readPassword(keyFile, item);
0301                 if (!item.isEmpty()) {
0302                     qCWarning(LOG_KSSHASKPASS) << "Detected legacy key for " << identifier << ", enabling workaround";
0303                     wallet->renameEntry(keyFile, identifier);
0304                     break;
0305                 }
0306             }
0307         }
0308     }
0309 
0310     if (!item.isEmpty()) {
0311         QTextStream(stdout) << item;
0312         return 0;
0313     }
0314 
0315     // Item could not be retrieved from wallet. Open dialog
0316     switch (type) {
0317     case TypeConfirm: {
0318 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0319         if (KMessageBox::questionTwoActions(nullptr,
0320                                             dialog,
0321                                             i18n("Ksshaskpass"),
0322                                             KGuiItem(i18nc("@action:button", "Accept"), QStringLiteral("dialog-ok")),
0323                                             KStandardGuiItem::cancel())
0324             != KMessageBox::PrimaryAction) {
0325 #else
0326         if (KMessageBox::questionYesNo(nullptr, dialog, i18n("Ksshaskpass")) != KMessageBox::Yes) {
0327 #endif
0328             // dialog has been canceled
0329             return 1;
0330         }
0331         item = QStringLiteral("yes\n");
0332         break;
0333     }
0334     case TypeClearText:
0335         // Should use a dialog with visible input, but KPasswordDialog doesn't support that and
0336         // other available dialog types don't have a "Keep" checkbox.
0337         /* fallthrough */
0338     case TypePassword: {
0339         // create the password dialog, but only show "Enable Keep" button, if the wallet is open
0340         KPasswordDialog::KPasswordDialogFlag flag(KPasswordDialog::NoFlags);
0341         if (wallet.get()) {
0342             flag = KPasswordDialog::ShowKeepPassword;
0343         }
0344         QPointer<KPasswordDialog> kpd = new KPasswordDialog(nullptr, flag);
0345 
0346         kpd->setPrompt(dialog);
0347         kpd->setWindowTitle(i18n("Ksshaskpass"));
0348         // We don't want to dump core when the password dialog is shown, because it could contain the entered password.
0349         // KPasswordDialog::disableCoreDumps() seems to be gone in KDE 4 -- do it manually
0350         struct rlimit rlim;
0351         rlim.rlim_cur = rlim.rlim_max = 0;
0352         setrlimit(RLIMIT_CORE, &rlim);
0353 
0354         if (kpd->exec() == QDialog::Accepted) {
0355             item = kpd->password();
0356             // If "Enable Keep" is enabled, open/create a folder in KWallet and store the password.
0357             if ((!identifier.isNull()) && wallet.get() && kpd->keepPassword()) {
0358                 if (!wallet->hasFolder(walletFolder)) {
0359                     wallet->createFolder(walletFolder);
0360                 }
0361                 wallet->setFolder(walletFolder);
0362                 wallet->writePassword(identifier, item);
0363             }
0364         } else {
0365             // dialog has been canceled
0366             return 1;
0367         }
0368         break;
0369     }
0370     }
0371 
0372     QTextStream out(stdout);
0373     out << item << "\n";
0374     return 0;
0375 }