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 }