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"