File indexing completed on 2024-04-28 05:27:45

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