File indexing completed on 2024-05-12 05:17:15

0001 /*
0002     SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
0003     SPDX-FileCopyrightText: 2009 Andras Mantia <amantia@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "loginjob.h"
0009 
0010 #include <KLocalizedString>
0011 
0012 #include "kimap_debug.h"
0013 
0014 #include "capabilitiesjob.h"
0015 #include "job_p.h"
0016 #include "response_p.h"
0017 #include "rfccodecs.h"
0018 #include "session_p.h"
0019 
0020 #include "common.h"
0021 
0022 extern "C" {
0023 #include <sasl/sasl.h>
0024 }
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 namespace KIMAP
0036 {
0037 class LoginJobPrivate : public JobPrivate
0038 {
0039 public:
0040     enum AuthState { PreStartTlsCapability = 0, StartTls, Capability, Login, Authenticate };
0041 
0042     LoginJobPrivate(LoginJob *job, Session *session, const QString &name)
0043         : JobPrivate(session, name)
0044         , q(job)
0045         , encryptionMode(LoginJob::Unencrypted)
0046         , authState(Login)
0047         , plainLoginDisabled(false)
0048     {
0049         conn = nullptr;
0050         client_interact = nullptr;
0051     }
0052     ~LoginJobPrivate()
0053     {
0054     }
0055     bool sasl_interact();
0056 
0057     bool startAuthentication();
0058     bool answerChallenge(const QByteArray &data);
0059     void sslResponse(bool response);
0060     void saveServerGreeting(const Response &response);
0061 
0062     LoginJob *const q;
0063 
0064     QString userName;
0065     QString authorizationName;
0066     QString password;
0067     QString serverGreeting;
0068 
0069     LoginJob::EncryptionMode encryptionMode;
0070     QString authMode;
0071     AuthState authState;
0072     QStringList capabilities;
0073     bool plainLoginDisabled;
0074 
0075     sasl_conn_t *conn;
0076     sasl_interact_t *client_interact;
0077 };
0078 }
0079 
0080 using namespace KIMAP;
0081 
0082 bool LoginJobPrivate::sasl_interact()
0083 {
0084     qCDebug(KIMAP_LOG) << "sasl_interact";
0085     sasl_interact_t *interact = client_interact;
0086 
0087     // some mechanisms do not require username && pass, so it doesn't need a popup
0088     // window for getting this info
0089     for (; interact->id != SASL_CB_LIST_END; interact++) {
0090         if (interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS) {
0091             // TODO: dialog for use name??
0092             break;
0093         }
0094     }
0095 
0096     interact = client_interact;
0097     while (interact->id != SASL_CB_LIST_END) {
0098         qCDebug(KIMAP_LOG) << "SASL_INTERACT id:" << interact->id;
0099         switch (interact->id) {
0100         case SASL_CB_AUTHNAME:
0101             if (!authorizationName.isEmpty()) {
0102                 qCDebug(KIMAP_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'";
0103                 interact->result = strdup(authorizationName.toUtf8().constData());
0104                 interact->len = strlen((const char *)interact->result);
0105                 break;
0106             }
0107             [[fallthrough]];
0108         case SASL_CB_USER:
0109             qCDebug(KIMAP_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'";
0110             interact->result = strdup(userName.toUtf8().constData());
0111             interact->len = strlen((const char *)interact->result);
0112             break;
0113         case SASL_CB_PASS:
0114             qCDebug(KIMAP_LOG) << "SASL_CB_PASS: [hidden]";
0115             interact->result = strdup(password.toUtf8().constData());
0116             interact->len = strlen((const char *)interact->result);
0117             break;
0118         default:
0119             interact->result = nullptr;
0120             interact->len = 0;
0121             break;
0122         }
0123         interact++;
0124     }
0125     return true;
0126 }
0127 
0128 LoginJob::LoginJob(Session *session)
0129     : Job(*new LoginJobPrivate(this, session, i18n("Login")))
0130 {
0131     Q_D(LoginJob);
0132     qCDebug(KIMAP_LOG) << this;
0133 }
0134 
0135 LoginJob::~LoginJob()
0136 {
0137     qCDebug(KIMAP_LOG) << this;
0138 }
0139 
0140 QString LoginJob::userName() const
0141 {
0142     Q_D(const LoginJob);
0143     return d->userName;
0144 }
0145 
0146 void LoginJob::setUserName(const QString &userName)
0147 {
0148     Q_D(LoginJob);
0149     d->userName = userName;
0150 }
0151 
0152 QString LoginJob::authorizationName() const
0153 {
0154     Q_D(const LoginJob);
0155     return d->authorizationName;
0156 }
0157 
0158 void LoginJob::setAuthorizationName(const QString &authorizationName)
0159 {
0160     Q_D(LoginJob);
0161     d->authorizationName = authorizationName;
0162 }
0163 
0164 QString LoginJob::password() const
0165 {
0166     Q_D(const LoginJob);
0167     return d->password;
0168 }
0169 
0170 void LoginJob::setPassword(const QString &password)
0171 {
0172     Q_D(LoginJob);
0173     d->password = password;
0174 }
0175 
0176 void LoginJob::doStart()
0177 {
0178     Q_D(LoginJob);
0179 
0180     qCDebug(KIMAP_LOG) << this;
0181     // Don't authenticate on a session in the authenticated state
0182     if (session()->state() == Session::Authenticated || session()->state() == Session::Selected) {
0183         setError(UserDefinedError);
0184         setErrorText(i18n("IMAP session in the wrong state for authentication"));
0185         emitResult();
0186         return;
0187     }
0188 
0189     // Get notified once encryption is successfully negotiated
0190     connect(d->sessionInternal(), &KIMAP::SessionPrivate::encryptionNegotiationResult, this, [d](bool result) {
0191         d->sslResponse(result);
0192     });
0193 
0194     // Trigger encryption negotiation only if needed
0195     EncryptionMode encryptionMode = d->encryptionMode;
0196 
0197     const auto negotiatedEncryption = d->sessionInternal()->negotiatedEncryption();
0198     if (negotiatedEncryption != QSsl::UnknownProtocol) {
0199         // If the socket is already encrypted, proceed to the next state
0200         d->sslResponse(true);
0201         return;
0202     }
0203 
0204     if (encryptionMode == SSLorTLS) {
0205         // Negotiation got started by Session, but didn't complete yet. Continue in sslResponse.
0206     } else if (encryptionMode == STARTTLS) {
0207         // Check if STARTTLS is supported
0208         d->authState = LoginJobPrivate::PreStartTlsCapability;
0209         d->tags << d->sessionInternal()->sendCommand("CAPABILITY");
0210     } else if (encryptionMode == Unencrypted) {
0211         if (d->authMode.isEmpty()) {
0212             d->authState = LoginJobPrivate::Login;
0213             qCDebug(KIMAP_LOG) << "sending LOGIN";
0214             d->tags << d->sessionInternal()->sendCommand("LOGIN",
0215                                                          '"' + quoteIMAP(d->userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(d->password).toUtf8() + '"');
0216         } else {
0217             if (!d->startAuthentication()) {
0218                 emitResult();
0219             }
0220         }
0221     }
0222 }
0223 
0224 void LoginJob::handleResponse(const Response &response)
0225 {
0226     Q_D(LoginJob);
0227 
0228     if (response.content.isEmpty()) {
0229         return;
0230     }
0231 
0232     // set the actual command name for standard responses
0233     QString commandName = i18n("Login");
0234     if (d->authState == LoginJobPrivate::Capability) {
0235         commandName = i18n("Capability");
0236     } else if (d->authState == LoginJobPrivate::StartTls) {
0237         commandName = i18n("StartTls");
0238     }
0239 
0240     enum ResponseCode { OK, ERR, UNTAGGED, CONTINUATION, MALFORMED };
0241 
0242     QByteArray tag = response.content.first().toString();
0243     ResponseCode code = OK;
0244 
0245     qCDebug(KIMAP_LOG) << commandName << tag;
0246 
0247     if (tag == "+") {
0248         code = CONTINUATION;
0249     } else if (tag == "*") {
0250         if (response.content.size() < 2) {
0251             code = MALFORMED; // Received empty untagged response
0252         } else {
0253             code = UNTAGGED;
0254         }
0255     } else if (d->tags.contains(tag)) {
0256         if (response.content.size() < 2) {
0257             code = MALFORMED;
0258         } else if (response.content[1].toString() == "OK") {
0259             code = OK;
0260         } else {
0261             code = ERR;
0262         }
0263     }
0264 
0265     switch (code) {
0266     case MALFORMED:
0267         // We'll handle it later
0268         break;
0269 
0270     case ERR:
0271         // server replied with NO or BAD for SASL authentication
0272         if (d->authState == LoginJobPrivate::Authenticate) {
0273             sasl_dispose(&d->conn);
0274         }
0275 
0276         setError(UserDefinedError);
0277         setErrorText(i18n("%1 failed, server replied: %2", commandName, QLatin1StringView(response.toString().constData())));
0278         emitResult();
0279         return;
0280 
0281     case UNTAGGED:
0282         // The only untagged response interesting for us here is CAPABILITY
0283         if (response.content[1].toString() == "CAPABILITY") {
0284             d->capabilities.clear();
0285             QList<Response::Part>::const_iterator p = response.content.begin() + 2;
0286             while (p != response.content.end()) {
0287                 QString capability = QLatin1StringView(p->toString());
0288                 d->capabilities << capability;
0289                 if (capability == QLatin1StringView("LOGINDISABLED")) {
0290                     d->plainLoginDisabled = true;
0291                 }
0292                 ++p;
0293             }
0294             qCDebug(KIMAP_LOG) << "Capabilities updated: " << d->capabilities;
0295         }
0296         break;
0297 
0298     case CONTINUATION:
0299         if (d->authState != LoginJobPrivate::Authenticate) {
0300             // Received unexpected continuation response for something
0301             // other than AUTHENTICATE command
0302             code = MALFORMED;
0303             break;
0304         }
0305 
0306         if (d->authMode == QLatin1StringView("PLAIN")) {
0307             if (response.content.size() > 1 && response.content.at(1).toString() == "OK") {
0308                 return;
0309             }
0310             QByteArray challengeResponse;
0311             if (!d->authorizationName.isEmpty()) {
0312                 challengeResponse += d->authorizationName.toUtf8();
0313             }
0314             challengeResponse += '\0';
0315             challengeResponse += d->userName.toUtf8();
0316             challengeResponse += '\0';
0317             challengeResponse += d->password.toUtf8();
0318             challengeResponse = challengeResponse.toBase64();
0319             d->sessionInternal()->sendData(challengeResponse);
0320         } else if (response.content.size() >= 2) {
0321             if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
0322                 emitResult(); // error, we're done
0323             }
0324         } else {
0325             // Received empty continuation for authMode other than PLAIN
0326             code = MALFORMED;
0327         }
0328         break;
0329 
0330     case OK:
0331 
0332         switch (d->authState) {
0333         case LoginJobPrivate::PreStartTlsCapability:
0334             if (d->capabilities.contains(QLatin1StringView("STARTTLS"))) {
0335                 d->authState = LoginJobPrivate::StartTls;
0336                 d->tags << d->sessionInternal()->sendCommand("STARTTLS");
0337             } else {
0338                 qCWarning(KIMAP_LOG) << "STARTTLS not supported by server!";
0339                 setError(UserDefinedError);
0340                 setErrorText(i18n("STARTTLS is not supported by the server, try using SSL/TLS instead."));
0341                 emitResult();
0342             }
0343             break;
0344 
0345         case LoginJobPrivate::StartTls:
0346             d->sessionInternal()->startSsl(QSsl::SecureProtocols);
0347             break;
0348 
0349         case LoginJobPrivate::Capability:
0350             // If encryption was requested, verify that it's negotiated before logging in
0351             if (d->encryptionMode != Unencrypted && d->sessionInternal()->negotiatedEncryption() == QSsl::UnknownProtocol) {
0352                 setError(LoginJob::UserDefinedError);
0353                 setErrorText(i18n("Internal error, tried to login before encryption"));
0354                 emitResult();
0355                 break;
0356             }
0357 
0358             // cleartext login, if enabled
0359             if (d->authMode.isEmpty()) {
0360                 if (d->plainLoginDisabled) {
0361                     setError(UserDefinedError);
0362                     setErrorText(i18n("Login failed, plain login is disabled by the server."));
0363                     emitResult();
0364                 } else {
0365                     d->authState = LoginJobPrivate::Login;
0366                     d->tags << d->sessionInternal()->sendCommand("LOGIN",
0367                                                                  '"' + quoteIMAP(d->userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(d->password).toUtf8()
0368                                                                      + '"');
0369                 }
0370             } else {
0371                 bool authModeSupported = false;
0372                 // find the selected SASL authentication method
0373                 for (const QString &capability : std::as_const(d->capabilities)) {
0374                     if (capability.startsWith(QLatin1StringView("AUTH="))) {
0375                         if (QStringView(capability).mid(5) == d->authMode) {
0376                             authModeSupported = true;
0377                             break;
0378                         }
0379                     }
0380                 }
0381                 if (!authModeSupported) {
0382                     setError(UserDefinedError);
0383                     setErrorText(i18n("Login failed, authentication mode %1 is not supported by the server.", d->authMode));
0384                     emitResult();
0385                 } else if (!d->startAuthentication()) {
0386                     emitResult(); // problem, we're done
0387                 }
0388             }
0389             break;
0390 
0391         case LoginJobPrivate::Authenticate:
0392             sasl_dispose(&d->conn); // SASL authentication done
0393             // Fall through
0394             [[fallthrough]];
0395         case LoginJobPrivate::Login:
0396             d->saveServerGreeting(response);
0397             emitResult(); // got an OK, command done
0398             break;
0399         }
0400     }
0401 
0402     if (code == MALFORMED) {
0403         setErrorText(i18n("%1 failed, malformed reply from the server.", commandName));
0404         emitResult();
0405     }
0406 }
0407 
0408 bool LoginJobPrivate::startAuthentication()
0409 {
0410     // SASL authentication
0411     if (!initSASL()) {
0412         q->setError(LoginJob::UserDefinedError);
0413         q->setErrorText(i18n("Login failed, client cannot initialize the SASL library."));
0414         return false;
0415     }
0416 
0417     authState = LoginJobPrivate::Authenticate;
0418     const char *out = nullptr;
0419     uint outlen = 0;
0420     const char *mechusing = nullptr;
0421 
0422     int result = sasl_client_new("imap", m_session->hostName().toLatin1().constData(), nullptr, nullptr, callbacks, 0, &conn);
0423     if (result != SASL_OK) {
0424         const QString saslError = QString::fromUtf8(sasl_errdetail(conn));
0425         qCWarning(KIMAP_LOG) << "sasl_client_new failed with:" << result << saslError;
0426         q->setError(LoginJob::UserDefinedError);
0427         q->setErrorText(saslError);
0428         return false;
0429     }
0430 
0431     do {
0432         qCDebug(KIMAP_LOG) << "Trying authmod" << authMode.toLatin1();
0433         result = sasl_client_start(conn,
0434                                    authMode.toLatin1().constData(),
0435                                    &client_interact,
0436                                    capabilities.contains(QLatin1StringView("SASL-IR")) ? &out : nullptr,
0437                                    &outlen,
0438                                    &mechusing);
0439 
0440         if (result == SASL_INTERACT) {
0441             if (!sasl_interact()) {
0442                 sasl_dispose(&conn);
0443                 q->setError(LoginJob::UserDefinedError); // TODO: check up the actual error
0444                 return false;
0445             }
0446         }
0447     } while (result == SASL_INTERACT);
0448 
0449     if (result != SASL_CONTINUE && result != SASL_OK) {
0450         const QString saslError = QString::fromUtf8(sasl_errdetail(conn));
0451         qCWarning(KIMAP_LOG) << "sasl_client_start failed with:" << result << saslError;
0452         q->setError(LoginJob::UserDefinedError);
0453         q->setErrorText(saslError);
0454         sasl_dispose(&conn);
0455         return false;
0456     }
0457 
0458     QByteArray tmp = QByteArray::fromRawData(out, outlen);
0459     QByteArray challenge = tmp.toBase64();
0460 
0461     if (challenge.isEmpty()) {
0462         tags << sessionInternal()->sendCommand("AUTHENTICATE", authMode.toLatin1());
0463     } else {
0464         tags << sessionInternal()->sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge);
0465     }
0466 
0467     return true;
0468 }
0469 
0470 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
0471 {
0472     QByteArray challenge = data;
0473     int result = -1;
0474     const char *out = nullptr;
0475     uint outlen = 0;
0476     do {
0477         result = sasl_client_step(conn, challenge.isEmpty() ? nullptr : challenge.data(), challenge.size(), &client_interact, &out, &outlen);
0478 
0479         if (result == SASL_INTERACT) {
0480             if (!sasl_interact()) {
0481                 q->setError(LoginJob::UserDefinedError); // TODO: check up the actual error
0482                 sasl_dispose(&conn);
0483                 return false;
0484             }
0485         }
0486     } while (result == SASL_INTERACT);
0487 
0488     if (result != SASL_CONTINUE && result != SASL_OK) {
0489         const QString saslError = QString::fromUtf8(sasl_errdetail(conn));
0490         qCWarning(KIMAP_LOG) << "sasl_client_step failed with:" << result << saslError;
0491         q->setError(LoginJob::UserDefinedError); // TODO: check up the actual error
0492         q->setErrorText(saslError);
0493         sasl_dispose(&conn);
0494         return false;
0495     }
0496 
0497     QByteArray tmp = QByteArray::fromRawData(out, outlen);
0498     challenge = tmp.toBase64();
0499 
0500     sessionInternal()->sendData(challenge);
0501 
0502     return true;
0503 }
0504 
0505 void LoginJobPrivate::sslResponse(bool response)
0506 {
0507     if (response) {
0508         authState = LoginJobPrivate::Capability;
0509         tags << sessionInternal()->sendCommand("CAPABILITY");
0510     } else {
0511         q->setError(LoginJob::UserDefinedError);
0512         q->setErrorText(i18n("Login failed, TLS negotiation failed."));
0513         encryptionMode = LoginJob::Unencrypted;
0514         q->emitResult();
0515     }
0516 }
0517 
0518 void LoginJob::setEncryptionMode(EncryptionMode mode)
0519 {
0520     Q_D(LoginJob);
0521     d->encryptionMode = mode;
0522 }
0523 
0524 LoginJob::EncryptionMode LoginJob::encryptionMode()
0525 {
0526     Q_D(LoginJob);
0527     return d->encryptionMode;
0528 }
0529 
0530 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
0531 {
0532     Q_D(LoginJob);
0533     switch (mode) {
0534     case ClearText:
0535         d->authMode = QLatin1StringView("");
0536         break;
0537     case Login:
0538         d->authMode = QStringLiteral("LOGIN");
0539         break;
0540     case Plain:
0541         d->authMode = QStringLiteral("PLAIN");
0542         break;
0543     case CramMD5:
0544         d->authMode = QStringLiteral("CRAM-MD5");
0545         break;
0546     case DigestMD5:
0547         d->authMode = QStringLiteral("DIGEST-MD5");
0548         break;
0549     case GSSAPI:
0550         d->authMode = QStringLiteral("GSSAPI");
0551         break;
0552     case Anonymous:
0553         d->authMode = QStringLiteral("ANONYMOUS");
0554         break;
0555     case XOAuth2:
0556         d->authMode = QStringLiteral("XOAUTH2");
0557         break;
0558     default:
0559         d->authMode = QString();
0560     }
0561 }
0562 
0563 void LoginJob::connectionLost()
0564 {
0565     Q_D(LoginJob);
0566 
0567     qCWarning(KIMAP_LOG) << "Connection to server lost " << d->m_socketError;
0568     if (d->m_socketError == QAbstractSocket::SslHandshakeFailedError) {
0569         setError(KJob::UserDefinedError);
0570         setErrorText(i18n("SSL handshake failed."));
0571         emitResult();
0572     } else {
0573         setError(ERR_COULD_NOT_CONNECT);
0574         setErrorText(i18n("Connection to server lost."));
0575         emitResult();
0576     }
0577 }
0578 
0579 void LoginJobPrivate::saveServerGreeting(const Response &response)
0580 {
0581     // Concatenate the parts of the server response into a string, while dropping the first two parts
0582     // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
0583 
0584     for (int i = 2; i < response.content.size(); i++) {
0585         if (response.content.at(i).type() == Response::Part::List) {
0586             serverGreeting += QLatin1Char('(');
0587             const QList<QByteArray> itemLst = response.content.at(i).toList();
0588             for (const QByteArray &item : itemLst) {
0589                 serverGreeting += QLatin1StringView(item) + QLatin1Char(' ');
0590             }
0591             serverGreeting.chop(1);
0592             serverGreeting += QStringLiteral(") ");
0593         } else {
0594             serverGreeting += QLatin1StringView(response.content.at(i).toString()) + QLatin1Char(' ');
0595         }
0596     }
0597     serverGreeting.chop(1);
0598 }
0599 
0600 QString LoginJob::serverGreeting() const
0601 {
0602     Q_D(const LoginJob);
0603     return d->serverGreeting;
0604 }
0605 
0606 #include "moc_loginjob.cpp"