File indexing completed on 2024-04-28 16:45:13
0001 /* 0002 SPDX-FileCopyrightText: 2020 David Edmundson <davidedmundson@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0005 */ 0006 0007 #include "pamauthenticator.h" 0008 0009 #include <QDebug> 0010 #include <QEventLoop> 0011 #include <QMetaMethod> 0012 #include <security/pam_appl.h> 0013 0014 #include "kscreenlocker_greet_logging.h" 0015 0016 class PamWorker : public QObject 0017 { 0018 Q_OBJECT 0019 public: 0020 PamWorker(); 0021 ~PamWorker(); 0022 void start(const QString &service, const QString &user); 0023 void authenticate(); 0024 0025 Q_SIGNALS: 0026 void busyChanged(bool busy); 0027 void promptForSecret(const QString &msg); 0028 void prompt(const QString &msg); 0029 void infoMessage(const QString &msg); 0030 void errorMessage(const QString &msg); 0031 void failed(); 0032 void succeeded(); 0033 0034 // internal 0035 void promptResponseReceived(const QByteArray &prompt); 0036 void cancelled(); 0037 0038 private: 0039 static int converse(int n, const struct pam_message **msg, struct pam_response **resp, void *data); 0040 0041 pam_handle_t *m_handle = nullptr; //< the actual PAM handle 0042 struct pam_conv m_conv; 0043 0044 bool m_inAuthenticate = false; 0045 int m_result; 0046 }; 0047 0048 int PamWorker::converse(int n, const struct pam_message **msg, struct pam_response **resp, void *data) 0049 { 0050 PamWorker *c = static_cast<PamWorker *>(data); 0051 0052 if (!resp) { 0053 return PAM_BUF_ERR; 0054 } 0055 0056 *resp = (struct pam_response *)calloc(n, sizeof(struct pam_response)); 0057 0058 for (int i = 0; i < n; i++) { 0059 bool isSecret = false; 0060 switch (msg[i]->msg_style) { 0061 case PAM_PROMPT_ECHO_OFF: { 0062 isSecret = true; 0063 Q_FALLTHROUGH(); 0064 case PAM_PROMPT_ECHO_ON: 0065 Q_EMIT c->busyChanged(false); 0066 0067 const QString prompt = QString::fromLocal8Bit(msg[i]->msg); 0068 if (isSecret) { 0069 Q_EMIT c->promptForSecret(prompt); 0070 } else { 0071 Q_EMIT c->prompt(prompt); 0072 } 0073 0074 QByteArray response; 0075 QEventLoop e; 0076 QObject::connect(c, &PamWorker::promptResponseReceived, &e, [&](const QByteArray &_response) { 0077 response = _response; 0078 e.exit(0); 0079 }); 0080 QObject::connect(c, &PamWorker::cancelled, &e, [&]() { 0081 e.exit(PAM_CONV_ERR); 0082 }); 0083 0084 int rc = e.exec(); 0085 if (rc != 0) { 0086 return rc; 0087 } 0088 0089 Q_EMIT c->busyChanged(true); 0090 0091 resp[i]->resp = (char *)malloc(response.length() + 1); 0092 // on error, get rid of everything 0093 if (!resp[i]->resp) { 0094 for (int j = 0; j < n; j++) { 0095 free(resp[i]->resp); 0096 resp[i]->resp = nullptr; 0097 } 0098 free(*resp); 0099 *resp = nullptr; 0100 return PAM_BUF_ERR; 0101 } 0102 0103 memcpy(resp[i]->resp, response.constData(), response.length()); 0104 resp[i]->resp[response.length()] = '\0'; 0105 0106 break; 0107 } 0108 case PAM_ERROR_MSG: 0109 qCDebug(KSCREENLOCKER_GREET) << QString::fromLocal8Bit(msg[i]->msg); 0110 Q_EMIT c->errorMessage(QString::fromLocal8Bit(msg[i]->msg)); 0111 break; 0112 case PAM_TEXT_INFO: 0113 // if there's only the info message, let's predict the prompts too 0114 qCDebug(KSCREENLOCKER_GREET) << QString::fromLocal8Bit(msg[i]->msg); 0115 Q_EMIT c->infoMessage(QString::fromLocal8Bit(msg[i]->msg)); 0116 default: 0117 break; 0118 } 0119 } 0120 0121 return PAM_SUCCESS; 0122 } 0123 0124 PamWorker::PamWorker() 0125 : QObject(nullptr) 0126 { 0127 m_conv = {&PamWorker::converse, this}; 0128 } 0129 0130 PamWorker::~PamWorker() 0131 { 0132 if (m_handle) { 0133 pam_end(m_handle, PAM_SUCCESS); 0134 } 0135 } 0136 0137 void PamWorker::authenticate() 0138 { 0139 if (m_inAuthenticate) { 0140 return; 0141 } 0142 m_inAuthenticate = true; 0143 qCDebug(KSCREENLOCKER_GREET) << "Start auth"; 0144 int rc = pam_authenticate(m_handle, 0); // PAM_SILENT); 0145 qCDebug(KSCREENLOCKER_GREET) << "Auth done RC" << rc; 0146 0147 Q_EMIT busyChanged(false); 0148 0149 if (rc == PAM_SUCCESS) { 0150 rc = pam_setcred(m_handle, PAM_REFRESH_CRED); 0151 /* ignore errors on refresh credentials. If this did not work we use the old ones. */ 0152 Q_EMIT succeeded(); 0153 } else { 0154 Q_EMIT failed(); 0155 } 0156 m_inAuthenticate = false; 0157 } 0158 0159 static void fail_delay(int retval, unsigned usec_delay, void *appdata_ptr) 0160 { 0161 Q_UNUSED(retval); 0162 Q_UNUSED(usec_delay); 0163 Q_UNUSED(appdata_ptr); 0164 } 0165 0166 void PamWorker::start(const QString &service, const QString &user) 0167 { 0168 if (user.isEmpty()) 0169 m_result = pam_start(qPrintable(service), nullptr, &m_conv, &m_handle); 0170 else 0171 m_result = pam_start(qPrintable(service), qPrintable(user), &m_conv, &m_handle); 0172 0173 // get errors quicker 0174 #ifdef PAM_FAIL_DELAY 0175 pam_set_item(m_handle, PAM_FAIL_DELAY, (void *)fail_delay); 0176 #endif 0177 0178 if (m_result != PAM_SUCCESS) { 0179 qCWarning(KSCREENLOCKER_GREET) << "[PAM] start" << pam_strerror(m_handle, m_result); 0180 return; 0181 } else { 0182 qCDebug(KSCREENLOCKER_GREET) << "[PAM] Starting..."; 0183 } 0184 } 0185 0186 PamAuthenticator::PamAuthenticator(const QString &service, const QString &user, QObject *parent) 0187 : QObject(parent) 0188 , m_signalsToMembers({ 0189 {QMetaMethod::fromSignal(&PamAuthenticator::prompt), m_prompt}, 0190 {QMetaMethod::fromSignal(&PamAuthenticator::promptForSecret), m_promptForSecret}, 0191 {QMetaMethod::fromSignal(&PamAuthenticator::infoMessage), m_infoMessage}, 0192 {QMetaMethod::fromSignal(&PamAuthenticator::errorMessage), m_errorMessage}, 0193 }) 0194 { 0195 d = new PamWorker; 0196 0197 d->moveToThread(&m_thread); 0198 0199 connect(&m_thread, &QThread::finished, d, &QObject::deleteLater); 0200 0201 connect(d, &PamWorker::busyChanged, this, &PamAuthenticator::setBusy); 0202 connect(d, &PamWorker::prompt, this, [this](const QString &msg) { 0203 m_prompt = msg; 0204 Q_EMIT prompt(msg); 0205 }); 0206 connect(d, &PamWorker::promptForSecret, this, [this](const QString &msg) { 0207 m_promptForSecret = msg; 0208 Q_EMIT promptForSecret(msg); 0209 }); 0210 connect(d, &PamWorker::infoMessage, this, [this](const QString &msg) { 0211 m_infoMessage = msg; 0212 Q_EMIT infoMessage(msg); 0213 }); 0214 connect(d, &PamWorker::errorMessage, this, [this](const QString &msg) { 0215 m_errorMessage = msg; 0216 Q_EMIT errorMessage(msg); 0217 }); 0218 0219 connect(d, &PamWorker::succeeded, this, [this]() { 0220 m_unlocked = true; 0221 Q_EMIT succeeded(); 0222 }); 0223 // Failed is not a persistent state. When a view provides authentication that will either result in failure or success, 0224 // failure simply means that the prompt is getting delayed. 0225 connect(d, &PamWorker::failed, this, &PamAuthenticator::failed); 0226 0227 m_thread.start(); 0228 init(service, user); 0229 } 0230 0231 PamAuthenticator::~PamAuthenticator() 0232 { 0233 cancel(); 0234 m_thread.quit(); 0235 m_thread.wait(); 0236 } 0237 0238 void PamAuthenticator::init(const QString &service, const QString &user) 0239 { 0240 QMetaObject::invokeMethod(d, [this, service, user]() { 0241 d->start(service, user); 0242 }); 0243 } 0244 0245 bool PamAuthenticator::isBusy() const 0246 { 0247 return m_busy; 0248 } 0249 0250 void PamAuthenticator::setBusy(bool busy) 0251 { 0252 if (m_busy != busy) { 0253 m_busy = busy; 0254 Q_EMIT busyChanged(); 0255 } 0256 } 0257 0258 bool PamAuthenticator::isUnlocked() const 0259 { 0260 return m_unlocked; 0261 } 0262 0263 void PamAuthenticator::tryUnlock() 0264 { 0265 m_unlocked = false; 0266 QMetaObject::invokeMethod(d, &PamWorker::authenticate); 0267 } 0268 0269 void PamAuthenticator::respond(const QByteArray &response) 0270 { 0271 QMetaObject::invokeMethod( 0272 d, 0273 [this, response]() { 0274 Q_EMIT d->promptResponseReceived(response); 0275 }, 0276 Qt::QueuedConnection); 0277 } 0278 0279 void PamAuthenticator::cancel() 0280 { 0281 m_prompt.clear(); 0282 m_promptForSecret.clear(); 0283 m_infoMessage.clear(); 0284 m_errorMessage.clear(); 0285 QMetaObject::invokeMethod(d, &PamWorker::cancelled); 0286 } 0287 0288 // Force emit the signals when a view connects to them. This prevents a race condition where screens appear after 0289 // stateful signals (such as prompt) had been emitted and end up in an incorrect state. 0290 // (e.g. https://bugs.kde.org/show_bug.cgi?id=456210 where the view ends up in a no-prompt state) 0291 void PamAuthenticator::connectNotify(const QMetaMethod &signal) 0292 { 0293 // TODO remove this function for Plasma 6. The properties should be bound to so we don't need to force 0294 // emit them every time a view connects. 0295 0296 // NOTE signals are queued because during connect qml is not yet ready to receive them 0297 0298 if (m_unlocked && signal == QMetaMethod::fromSignal(&PamAuthenticator::succeeded)) { 0299 signal.invoke(this, Qt::QueuedConnection); 0300 return; 0301 } 0302 0303 for (const auto &[signalMethod, memberString] : m_signalsToMembers) 0304 { 0305 if (signal != signalMethod) { 0306 continue; 0307 } 0308 0309 if (!memberString.isNull()) { 0310 signalMethod.invoke(this, Qt::QueuedConnection, Q_ARG(QString, memberString)); 0311 return; 0312 } 0313 } 0314 } 0315 0316 QString PamAuthenticator::getPrompt() const 0317 { 0318 return m_prompt; 0319 } 0320 0321 QString PamAuthenticator::getPromptForSecret() const 0322 { 0323 return m_promptForSecret; 0324 } 0325 0326 QString PamAuthenticator::getInfoMessage() const 0327 { 0328 return m_infoMessage; 0329 } 0330 0331 QString PamAuthenticator::getErrorMessage() const 0332 { 0333 return m_errorMessage; 0334 } 0335 0336 #include "pamauthenticator.moc"