File indexing completed on 2024-05-12 05:21:35

0001 /*
0002   SPDX-FileCopyrightText: 2010 BetterInbox <contact@betterinbox.com>
0003   SPDX-FileContributor: Christophe Laveault <christophe@betterinbox.com>
0004   SPDX-FileContributor: Gregory Schlomoff <gregory.schlomoff@gmail.com>
0005 
0006   SPDX-License-Identifier: LGPL-2.1-or-later
0007 */
0008 
0009 #include "loginjob.h"
0010 #include "job_p.h"
0011 #include "ksmtp_debug.h"
0012 #include "serverresponse_p.h"
0013 #include "session_p.h"
0014 
0015 #include <KLocalizedString>
0016 
0017 #include <QJsonDocument>
0018 #include <QJsonObject>
0019 
0020 extern "C" {
0021 #include <sasl/sasl.h>
0022 }
0023 
0024 namespace
0025 {
0026 static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
0027                                             {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
0028                                             {SASL_CB_GETREALM, nullptr, nullptr},
0029                                             {SASL_CB_USER, nullptr, nullptr},
0030                                             {SASL_CB_AUTHNAME, nullptr, nullptr},
0031                                             {SASL_CB_PASS, nullptr, nullptr},
0032                                             {SASL_CB_CANON_USER, nullptr, nullptr},
0033                                             {SASL_CB_LIST_END, nullptr, nullptr}};
0034 }
0035 
0036 namespace KSmtp
0037 {
0038 class LoginJobPrivate : public JobPrivate
0039 {
0040 public:
0041     LoginJobPrivate(LoginJob *job, Session *session, const QString &name)
0042         : JobPrivate(session, name)
0043         , m_preferedAuthMode(LoginJob::Login)
0044         , m_actualAuthMode(LoginJob::UnknownAuth)
0045         , q(job)
0046     {
0047     }
0048 
0049     ~LoginJobPrivate() override = default;
0050 
0051     [[nodiscard]] bool sasl_interact();
0052     [[nodiscard]] bool sasl_init();
0053     [[nodiscard]] bool sasl_challenge(const QByteArray &data);
0054 
0055     [[nodiscard]] bool authenticate();
0056     [[nodiscard]] bool selectAuthentication();
0057 
0058     [[nodiscard]] LoginJob::AuthMode authModeFromCommand(const QByteArray &mech) const;
0059     [[nodiscard]] QByteArray authCommand(LoginJob::AuthMode mode) const;
0060 
0061     QString m_userName;
0062     QString m_password;
0063     LoginJob::AuthMode m_preferedAuthMode;
0064     LoginJob::AuthMode m_actualAuthMode;
0065 
0066     sasl_conn_t *m_saslConn = nullptr;
0067     sasl_interact_t *m_saslClient = nullptr;
0068 
0069 private:
0070     LoginJob *const q;
0071 };
0072 }
0073 
0074 using namespace KSmtp;
0075 
0076 LoginJob::LoginJob(Session *session)
0077     : Job(*new LoginJobPrivate(this, session, i18n("Login")))
0078 {
0079 }
0080 
0081 LoginJob::~LoginJob() = default;
0082 
0083 void LoginJob::setUserName(const QString &userName)
0084 {
0085     Q_D(LoginJob);
0086     d->m_userName = userName;
0087 }
0088 
0089 void LoginJob::setPassword(const QString &password)
0090 {
0091     Q_D(LoginJob);
0092     d->m_password = password;
0093 }
0094 
0095 void LoginJob::setPreferedAuthMode(AuthMode mode)
0096 {
0097     Q_D(LoginJob);
0098 
0099     if (mode == UnknownAuth) {
0100         qCWarning(KSMTP_LOG) << "LoginJob: Cannot set preferred authentication mode to Unknown";
0101         return;
0102     }
0103     d->m_preferedAuthMode = mode;
0104 }
0105 
0106 LoginJob::AuthMode LoginJob::usedAuthMode() const
0107 {
0108     return d_func()->m_actualAuthMode;
0109 }
0110 
0111 void LoginJob::doStart()
0112 {
0113     Q_D(LoginJob);
0114     if (d->sessionInternal()->negotiatedEncryption() == QSsl::UnknownProtocol && d->m_session->encryptionMode() != Session::Unencrypted) {
0115         qFatal("LoginJob started despite session not being encrypted!");
0116     }
0117 
0118     if (!d->authenticate()) {
0119         emitResult();
0120     }
0121 }
0122 
0123 void LoginJob::handleResponse(const ServerResponse &r)
0124 {
0125     Q_D(LoginJob);
0126 
0127     // Handle server errors
0128     handleErrors(r);
0129 
0130     // Send account data
0131     if (r.isCode(334)) {
0132         if (d->m_actualAuthMode == Plain) {
0133             const QByteArray challengeResponse = '\0' + d->m_userName.toUtf8() + '\0' + d->m_password.toUtf8();
0134             sendCommand(challengeResponse.toBase64());
0135         } else {
0136             if (!d->sasl_challenge(QByteArray::fromBase64(r.text()))) {
0137                 emitResult();
0138             }
0139         }
0140         return;
0141     }
0142 
0143     // Final agreement
0144     if (r.isCode(235)) {
0145         d->sessionInternal()->setState(Session::Authenticated);
0146         emitResult();
0147     }
0148 }
0149 
0150 bool LoginJobPrivate::selectAuthentication()
0151 {
0152     const QStringList availableModes = m_session->availableAuthModes();
0153 
0154     if (availableModes.contains(QString::fromLatin1(authCommand(m_preferedAuthMode)))) {
0155         m_actualAuthMode = m_preferedAuthMode;
0156     } else if (availableModes.contains(QString::fromLatin1(authCommand(LoginJob::Login)))) {
0157         m_actualAuthMode = LoginJob::Login;
0158     } else if (availableModes.contains(QString::fromLatin1(authCommand(LoginJob::Plain)))) {
0159         m_actualAuthMode = LoginJob::Plain;
0160     } else {
0161         qCWarning(KSMTP_LOG) << "LoginJob: Couldn't choose an authentication method. Please retry with : " << availableModes;
0162         q->setError(KJob::UserDefinedError);
0163         q->setErrorText(i18n("Could not authenticate to the SMTP server because no matching authentication method has been found"));
0164         return false;
0165     }
0166 
0167     return true;
0168 }
0169 
0170 bool LoginJobPrivate::sasl_init()
0171 {
0172     if (sasl_client_init(nullptr) != SASL_OK) {
0173         qCWarning(KSMTP_LOG) << "Failed to initialize SASL";
0174         return false;
0175     }
0176     return true;
0177 }
0178 
0179 bool LoginJobPrivate::sasl_interact()
0180 {
0181     sasl_interact_t *interact = m_saslClient;
0182 
0183     while (interact->id != SASL_CB_LIST_END) {
0184         qCDebug(KSMTP_LOG) << "SASL_INTERACT Id" << interact->id;
0185         switch (interact->id) {
0186         case SASL_CB_AUTHNAME: {
0187             // case SASL_CB_USER:
0188             qCDebug(KSMTP_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << m_userName << "'";
0189             const auto username = m_userName.toUtf8();
0190             interact->result = strdup(username.constData());
0191             interact->len = username.size();
0192             break;
0193         }
0194         case SASL_CB_PASS: {
0195             qCDebug(KSMTP_LOG) << "SASL_CB_PASS: [hidden]";
0196             const auto pass = m_password.toUtf8();
0197             interact->result = strdup(pass.constData());
0198             interact->len = pass.size();
0199             break;
0200         }
0201         default:
0202             interact->result = nullptr;
0203             interact->len = 0;
0204             break;
0205         }
0206         ++interact;
0207     }
0208 
0209     return true;
0210 }
0211 
0212 bool LoginJobPrivate::sasl_challenge(const QByteArray &challenge)
0213 {
0214     int result = -1;
0215     const char *out = nullptr;
0216     uint outLen = 0;
0217 
0218     if (m_actualAuthMode == LoginJob::XOAuth2) {
0219         QJsonDocument doc = QJsonDocument::fromJson(challenge);
0220         if (!doc.isNull() && doc.isObject()) {
0221             const auto obj = doc.object();
0222             if (obj.value(QLatin1StringView("status")).toString() == QLatin1StringView("400")) {
0223                 q->setError(LoginJob::TokenExpired);
0224                 q->setErrorText(i18n("Token expired"));
0225                 // https://developers.google.com/gmail/imap/xoauth2-protocol#error_response_2
0226                 // "The client sends an empty response ("\r\n") to the challenge containing the error message."
0227                 q->sendCommand("");
0228                 return false;
0229             }
0230         }
0231     }
0232 
0233     for (;;) {
0234         result = sasl_client_step(m_saslConn, challenge.isEmpty() ? nullptr : challenge.constData(), challenge.size(), &m_saslClient, &out, &outLen);
0235         if (result == SASL_INTERACT) {
0236             if (!sasl_interact()) {
0237                 q->setError(LoginJob::UserDefinedError);
0238                 sasl_dispose(&m_saslConn);
0239                 return false;
0240             }
0241         } else {
0242             break;
0243         }
0244     }
0245 
0246     if (result != SASL_OK && result != SASL_CONTINUE) {
0247         const QString saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
0248         qCWarning(KSMTP_LOG) << "sasl_client_step failed: " << result << saslError;
0249         q->setError(LoginJob::UserDefinedError);
0250         q->setErrorText(saslError);
0251         sasl_dispose(&m_saslConn);
0252         return false;
0253     }
0254 
0255     q->sendCommand(QByteArray::fromRawData(out, outLen).toBase64());
0256 
0257     return true;
0258 }
0259 
0260 bool LoginJobPrivate::authenticate()
0261 {
0262     if (!selectAuthentication()) {
0263         return false;
0264     }
0265 
0266     if (!sasl_init()) {
0267         q->setError(LoginJob::UserDefinedError);
0268         q->setErrorText(i18n("Login failed, cannot initialize the SASL library"));
0269         return false;
0270     }
0271 
0272     int result = sasl_client_new("smtp", m_session->hostName().toUtf8().constData(), nullptr, nullptr, callbacks, 0, &m_saslConn);
0273     if (result != SASL_OK) {
0274         const auto saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
0275         q->setError(LoginJob::UserDefinedError);
0276         q->setErrorText(saslError);
0277         return false;
0278     }
0279 
0280     uint outLen = 0;
0281     const char *out = nullptr;
0282     const char *actualMech = nullptr;
0283     const auto authMode = authCommand(m_actualAuthMode);
0284 
0285     for (;;) {
0286         qCDebug(KSMTP_LOG) << "Trying authmod" << authMode;
0287         result = sasl_client_start(m_saslConn, authMode.constData(), &m_saslClient, &out, &outLen, &actualMech);
0288         if (result == SASL_INTERACT) {
0289             if (!sasl_interact()) {
0290                 sasl_dispose(&m_saslConn);
0291                 q->setError(LoginJob::UserDefinedError);
0292                 return false;
0293             }
0294         } else {
0295             break;
0296         }
0297     }
0298 
0299     m_actualAuthMode = authModeFromCommand(actualMech);
0300 
0301     if (result != SASL_CONTINUE && result != SASL_OK) {
0302         const auto saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
0303         qCWarning(KSMTP_LOG) << "sasl_client_start failed with:" << result << saslError;
0304         q->setError(LoginJob::UserDefinedError);
0305         q->setErrorText(saslError);
0306         sasl_dispose(&m_saslConn);
0307         return false;
0308     }
0309 
0310     if (outLen == 0) {
0311         q->sendCommand("AUTH " + authMode);
0312     } else {
0313         q->sendCommand("AUTH " + authMode + ' ' + QByteArray::fromRawData(out, outLen).toBase64());
0314     }
0315 
0316     return true;
0317 }
0318 
0319 LoginJob::AuthMode LoginJobPrivate::authModeFromCommand(const QByteArray &mech) const
0320 {
0321     if (qstrnicmp(mech.constData(), "PLAIN", 5) == 0) {
0322         return LoginJob::Plain;
0323     } else if (qstrnicmp(mech.constData(), "LOGIN", 5) == 0) {
0324         return LoginJob::Login;
0325     } else if (qstrnicmp(mech.constData(), "CRAM-MD5", 8) == 0) {
0326         return LoginJob::CramMD5;
0327     } else if (qstrnicmp(mech.constData(), "DIGEST-MD5", 10) == 0) {
0328         return LoginJob::DigestMD5;
0329     } else if (qstrnicmp(mech.constData(), "GSSAPI", 6) == 0) {
0330         return LoginJob::GSSAPI;
0331     } else if (qstrnicmp(mech.constData(), "NTLM", 4) == 0) {
0332         return LoginJob::NTLM;
0333     } else if (qstrnicmp(mech.constData(), "ANONYMOUS", 9) == 0) {
0334         return LoginJob::Anonymous;
0335     } else if (qstrnicmp(mech.constData(), "XOAUTH2", 7) == 0) {
0336         return LoginJob::XOAuth2;
0337     } else {
0338         return LoginJob::UnknownAuth;
0339     }
0340 }
0341 
0342 QByteArray LoginJobPrivate::authCommand(LoginJob::AuthMode mode) const
0343 {
0344     switch (mode) {
0345     case LoginJob::Plain:
0346         return QByteArrayLiteral("PLAIN");
0347     case LoginJob::Login:
0348         return QByteArrayLiteral("LOGIN");
0349     case LoginJob::CramMD5:
0350         return QByteArrayLiteral("CRAM-MD5");
0351     case LoginJob::DigestMD5:
0352         return QByteArrayLiteral("DIGEST-MD5");
0353     case LoginJob::GSSAPI:
0354         return QByteArrayLiteral("GSSAPI");
0355     case LoginJob::NTLM:
0356         return QByteArrayLiteral("NTLM");
0357     case LoginJob::Anonymous:
0358         return QByteArrayLiteral("ANONYMOUS");
0359     case LoginJob::XOAuth2:
0360         return QByteArrayLiteral("XOAUTH2");
0361     case LoginJob::UnknownAuth:
0362         return ""; // Should not happen
0363     }
0364     return {};
0365 }
0366 
0367 #include "moc_loginjob.cpp"