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

0001 /*
0002     Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
0003     Copyright (c) 2009 Andras Mantia <amantia@kde.org>
0004     Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
0005 
0006     This library is free software; you can redistribute it and/or modify it
0007     under the terms of the GNU Library General Public License as published by
0008     the Free Software Foundation; either version 2 of the License, or (at your
0009     option) any later version.
0010 
0011     This library is distributed in the hope that it will be useful, but WITHOUT
0012     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0013     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0014     License for more details.
0015 
0016     You should have received a copy of the GNU Library General Public License
0017     along with this library; see the file COPYING.LIB.  If not, write to the
0018     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0019     02110-1301, USA.
0020 */
0021 
0022 #include "loginjob.h"
0023 
0024 #include "kimap_debug.h"
0025 
0026 #include "job_p.h"
0027 #include "message_p.h"
0028 #include "session_p.h"
0029 #include "rfccodecs.h"
0030 
0031 #include "common.h"
0032 
0033 extern "C" {
0034 #include <sasl/sasl.h>
0035 }
0036 
0037 static const sasl_callback_t callbacks[] = {
0038     { SASL_CB_ECHOPROMPT, Q_NULLPTR, nullptr },
0039     { SASL_CB_NOECHOPROMPT, Q_NULLPTR, nullptr },
0040     { SASL_CB_GETREALM, Q_NULLPTR, nullptr },
0041     { SASL_CB_USER, Q_NULLPTR, nullptr },
0042     { SASL_CB_AUTHNAME, Q_NULLPTR, nullptr },
0043     { SASL_CB_PASS, Q_NULLPTR, nullptr },
0044     { SASL_CB_CANON_USER, Q_NULLPTR, nullptr },
0045     { SASL_CB_LIST_END, Q_NULLPTR, nullptr }
0046 };
0047 
0048 namespace KIMAP2
0049 {
0050 class LoginJobPrivate : public JobPrivate
0051 {
0052 public:
0053     enum AuthState {
0054         StartTls = 0,
0055         Capability,
0056         Login,
0057         Authenticate
0058     };
0059 
0060     LoginJobPrivate(LoginJob *job, Session *session, const QString &name) : JobPrivate(session, name), q(job)
0061     {
0062         conn = Q_NULLPTR;
0063         client_interact = Q_NULLPTR;
0064     }
0065     ~LoginJobPrivate() { }
0066     bool sasl_interact();
0067 
0068     bool startAuthentication();
0069     void sendPlainLogin();
0070     bool answerChallenge(const QByteArray &data);
0071     void sslResponse(bool response);
0072     void saveServerGreeting(const Message &response);
0073     void login();
0074     void retrieveCapabilities();
0075 
0076     LoginJob *q;
0077 
0078     QString userName;
0079     QString authorizationName;
0080     QString password;
0081     QString serverGreeting;
0082 
0083     QSsl::SslProtocol encryptionMode = QSsl::UnknownProtocol;
0084     bool startTls = false;
0085     QString authMode;
0086     AuthState authState = Login;
0087     QStringList capabilities;
0088     bool plainLoginDisabled = false;
0089     bool connectionIsEncrypted = false;
0090 
0091     sasl_conn_t *conn;
0092     sasl_interact_t *client_interact;
0093 };
0094 }
0095 
0096 using namespace KIMAP2;
0097 
0098 bool LoginJobPrivate::sasl_interact()
0099 {
0100     qCDebug(KIMAP2_LOG) << "sasl_interact";
0101     sasl_interact_t *interact = client_interact;
0102 
0103     //some mechanisms do not require username && pass, so it doesn't need a popup
0104     //window for getting this info
0105     for (; interact->id != SASL_CB_LIST_END; interact++) {
0106         if (interact->id == SASL_CB_AUTHNAME ||
0107                 interact->id == SASL_CB_PASS) {
0108             //TODO: dialog for use name??
0109             break;
0110         }
0111     }
0112 
0113     interact = client_interact;
0114     while (interact->id != SASL_CB_LIST_END) {
0115         qCDebug(KIMAP2_LOG) << "SASL_INTERACT id:" << interact->id;
0116         switch (interact->id) {
0117         case SASL_CB_AUTHNAME:
0118             if (!authorizationName.isEmpty()) {
0119                 qCDebug(KIMAP2_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'";
0120                 interact->result = strdup(authorizationName.toUtf8());
0121                 interact->len = strlen((const char *) interact->result);
0122                 break;
0123             }
0124         case SASL_CB_USER:
0125             qCDebug(KIMAP2_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'";
0126             interact->result = strdup(userName.toUtf8());
0127             interact->len = strlen((const char *) interact->result);
0128             break;
0129         case SASL_CB_PASS:
0130             qCDebug(KIMAP2_LOG) << "SASL_CB_PASS: [hidden]";
0131             interact->result = strdup(password.toUtf8());
0132             interact->len = strlen((const char *) interact->result);
0133             break;
0134         default:
0135             interact->result = Q_NULLPTR;
0136             interact->len = 0;
0137             break;
0138         }
0139         interact++;
0140     }
0141     //FIXME This should return false at least in some cases
0142     return true;
0143 }
0144 
0145 LoginJob::LoginJob(Session *session)
0146     : Job(*new LoginJobPrivate(this, session, QString::fromUtf8("Login")))
0147 {
0148     qCDebug(KIMAP2_LOG) << this;
0149 }
0150 
0151 LoginJob::~LoginJob()
0152 {
0153     qCDebug(KIMAP2_LOG) << this;
0154 }
0155 
0156 QString LoginJob::userName() const
0157 {
0158     Q_D(const LoginJob);
0159     return d->userName;
0160 }
0161 
0162 void LoginJob::setUserName(const QString &userName)
0163 {
0164     Q_D(LoginJob);
0165     d->userName = userName;
0166 }
0167 
0168 QString LoginJob::authorizationName() const
0169 {
0170     Q_D(const LoginJob);
0171     return d->authorizationName;
0172 }
0173 
0174 void LoginJob::setAuthorizationName(const QString &authorizationName)
0175 {
0176     Q_D(LoginJob);
0177     d->authorizationName = authorizationName;
0178 }
0179 
0180 QString LoginJob::password() const
0181 {
0182     Q_D(const LoginJob);
0183     return d->password;
0184 }
0185 
0186 void LoginJob::setPassword(const QString &password)
0187 {
0188     Q_D(LoginJob);
0189     d->password = password;
0190 }
0191 
0192 /*
0193  * The IMAP authentication procedure is unfortunately ridiculously complicated due to the many different options:
0194  *
0195  * An IMAP Session always has the following structure:
0196  * * Connection is established.
0197  * * Server sends greeting.
0198  * * Client authenticates somehow.
0199  * * .....
0200  *
0201  * If the we have a plain connection it's simple:
0202  * * Wait for the greeting
0203  * * Login using the chosen authentication mechanism
0204  *
0205  * If we're using TLS (without STARTTLS, so directly):
0206  * * Immediately initiate TLS handshake.
0207  * * Wait for the greeting
0208  * * Get CAPABILITIES to figure out which AUTH mechs are supported
0209  * * Login using the chosen authentication mechanism
0210  *
0211  * If we're using TLS with STARTTLS:
0212  * * Wait for the greeting (on the unencrypted connection)
0213  * * Send STARTTLS and wait for OK
0214  * * Initiate TLS handshake
0215  * * Get CAPABILITIES to figure out which AUTH mechs are supported
0216  * * Login using the chosen authentication mechanism
0217  */
0218 void LoginJob::doStart()
0219 {
0220     Q_D(LoginJob);
0221 
0222     qCDebug(KIMAP2_LOG) << "doStart" << this;
0223 
0224     connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool)));
0225 
0226     if (session()->state() == Session::Disconnected) {
0227         auto guard = new QObject(this);
0228         QObject::connect(session(), &Session::stateChanged, guard, [d, guard](KIMAP2::Session::State newState, KIMAP2::Session::State) {
0229             qCDebug(KIMAP2_LOG) << "Session state changed" << newState;
0230             d->login();
0231             delete guard;
0232         });
0233         if (!d->startTls && d->encryptionMode != QSsl::UnknownProtocol) {
0234             //We have to encrypt for the greeting
0235             d->sessionInternal()->startSsl(d->encryptionMode);
0236         }
0237         //We wait for the server greeting
0238         return;
0239     } else {
0240         qCInfo(KIMAP2_LOG) << "Session is ready, carring on";
0241         //The session is ready, we can carry on.
0242         d->login();
0243     }
0244 
0245 }
0246 
0247 void LoginJobPrivate::login()
0248 {
0249     // Don't authenticate on a session in the authenticated state
0250     if (q->session()->isConnected()) {
0251         q->setError(LoginJob::UserDefinedError);
0252         q->setErrorText(QString::fromUtf8("IMAP session in the wrong state for authentication"));
0253         q->emitResult();
0254         return;
0255     }
0256 
0257     if (startTls) {
0258         //With STARTTLS we have to try to upgrade our connection before the login
0259         qCInfo(KIMAP2_LOG) << "Starting with tls";
0260         authState = LoginJobPrivate::StartTls;
0261         sendCommand("STARTTLS", {});
0262         return;
0263     } else {
0264         //If this is supposed to be unecrypted or already encrypted we can retrieve capabilties. Otherwise we wait for the sslResponse.
0265         if (encryptionMode == QSsl::UnknownProtocol || connectionIsEncrypted) {
0266             retrieveCapabilities();
0267         } else {
0268             qCInfo(KIMAP2_LOG) << "Waiting for encryption before retrieveing capabilities.";
0269         }
0270     }
0271 
0272 }
0273 
0274 void LoginJobPrivate::sslResponse(bool response)
0275 {
0276     qCDebug(KIMAP2_LOG) << "Got an ssl response " << response;
0277     connectionIsEncrypted = response;
0278     if (response) {
0279         //It's possible that we receive the ssl info before we receive the server greeting.
0280         //In that case we're still in the Disconnected state and shouldn't retrieve the capabilities just yet.
0281         //We'll try again via login once the state changes.
0282         if (m_session->state() != Session::Disconnected) {
0283             retrieveCapabilities();
0284         }
0285     } else {
0286         q->setError(LoginFailed);
0287         q->setErrorText(QString::fromUtf8("Login failed, TLS negotiation failed."));
0288         encryptionMode = QSsl::UnknownProtocol;
0289         q->emitResult();
0290     }
0291 }
0292 
0293 void LoginJobPrivate::retrieveCapabilities()
0294 {
0295     qCDebug(KIMAP2_LOG) << "Retrieving capabilities.";
0296     authState = LoginJobPrivate::Capability;
0297     sendCommand("CAPABILITY", {});
0298 }
0299 
0300 void LoginJob::handleResponse(const Message &response)
0301 {
0302     Q_D(LoginJob);
0303 
0304     if (response.content.isEmpty()) {
0305         return;
0306     }
0307 
0308     //set the actual command name for standard responses
0309     QString commandName = QStringLiteral("Login");
0310     if (d->authState == LoginJobPrivate::Capability) {
0311         commandName = QStringLiteral("Capability");
0312     } else if (d->authState == LoginJobPrivate::StartTls) {
0313         commandName = QStringLiteral("StartTls");
0314     }
0315 
0316     enum ResponseCode {
0317         OK,
0318         ERR,
0319         UNTAGGED,
0320         CONTINUATION,
0321         MALFORMED
0322     };
0323 
0324     QByteArray tag = response.content.first().toString();
0325     ResponseCode code = OK;
0326 
0327     qCDebug(KIMAP2_LOG) << commandName << tag;
0328 
0329     if (tag == "+") {
0330         code = CONTINUATION;
0331     } else if (tag == "*") {
0332         if (response.content.size() < 2) {
0333             code = MALFORMED; // Received empty untagged response
0334         } else {
0335             code = UNTAGGED;
0336         }
0337     } else if (d->tags.contains(tag)) {
0338         if (response.content.size() < 2) {
0339             code = MALFORMED;
0340         } else if (response.content[1].toString() == "OK") {
0341             code = OK;
0342         } else {
0343             code = ERR;
0344         }
0345     }
0346 
0347     switch (code) {
0348     case MALFORMED:
0349         // We'll handle it later
0350         break;
0351 
0352     case ERR:
0353         //server replied with NO or BAD for SASL authentication
0354         if (d->authState == LoginJobPrivate::Authenticate) {
0355             sasl_dispose(&d->conn);
0356         }
0357 
0358         setError(LoginFailed);
0359         setErrorText(QString("%1 failed, server replied: %2").arg(commandName).arg(QLatin1String(response.toString().constData())));
0360         emitResult();
0361         return;
0362 
0363     case UNTAGGED:
0364         // The only untagged response interesting for us here is CAPABILITY
0365         if (response.content[1].toString() == "CAPABILITY") {
0366             QList<Message::Part>::const_iterator p = response.content.begin() + 2;
0367             while (p != response.content.end()) {
0368                 QString capability = QLatin1String(p->toString());
0369                 d->capabilities << capability;
0370                 if (capability == QLatin1String("LOGINDISABLED")) {
0371                     d->plainLoginDisabled = true;
0372                 }
0373                 ++p;
0374             }
0375             qCInfo(KIMAP2_LOG) << "Capabilities updated: " << d->capabilities;
0376         }
0377         break;
0378 
0379     case CONTINUATION:
0380         if (d->authState != LoginJobPrivate::Authenticate) {
0381             // Received unexpected continuation response for something
0382             // other than AUTHENTICATE command
0383             code = MALFORMED;
0384             break;
0385         }
0386 
0387         if (d->authMode == QLatin1String("PLAIN")) {
0388             if (response.content.size() > 1 && response.content.at(1).toString() == "OK") {
0389                 return;
0390             }
0391             QByteArray challengeResponse;
0392             if (!d->authorizationName.isEmpty()) {
0393                 challengeResponse += d->authorizationName.toUtf8();
0394             }
0395             challengeResponse += '\0';
0396             challengeResponse += d->userName.toUtf8();
0397             challengeResponse += '\0';
0398             challengeResponse += d->password.toUtf8();
0399             challengeResponse = challengeResponse.toBase64();
0400             d->sessionInternal()->sendData(challengeResponse);
0401         } else if (response.content.size() >= 2) {
0402             if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
0403                 emitResult(); //error, we're done
0404             }
0405         } else {
0406             // Received empty continuation for authMode other than PLAIN
0407             code = MALFORMED;
0408         }
0409         break;
0410 
0411     case OK:
0412         switch (d->authState) {
0413         case LoginJobPrivate::StartTls:
0414             //Start encryption and wait for sslResponse
0415             d->sessionInternal()->startSsl(d->encryptionMode);
0416             break;
0417         case LoginJobPrivate::Capability:
0418             //cleartext login, if enabled
0419             if (d->authMode.isEmpty()) {
0420                 if (d->plainLoginDisabled) {
0421                     setError(LoginFailed);
0422                     setErrorText(QString("Login failed, plain login is disabled by the server."));
0423                     emitResult();
0424                 } else {
0425                     d->sendPlainLogin();
0426                 }
0427             } else {
0428                 bool authModeSupported = false;
0429                 //PLAIN is always supported as defined in the standard. We should also get an AUTH= capability, but in case a server doesn't properly announce it we'll just accept it anyways.
0430                 if (d->authMode == "PLAIN") {
0431                     authModeSupported = true;
0432                 }
0433                 //find the selected SASL authentication method
0434                 Q_FOREACH (const QString &capability, d->capabilities) {
0435                     if (capability.startsWith(QLatin1String("AUTH="))) {
0436                         if (capability.mid(5) == d->authMode) {
0437                             authModeSupported = true;
0438                             break;
0439                         }
0440                     }
0441                 }
0442                 if (!authModeSupported) {
0443                     setError(LoginFailed);
0444                     setErrorText(QString("Login failed, authentication mode %1 is not supported by the server.").arg(d->authMode));
0445                     emitResult();
0446                 } else if (!d->startAuthentication()) {
0447                     emitResult(); //problem, we're done
0448                 }
0449             }
0450             break;
0451 
0452         case LoginJobPrivate::Authenticate:
0453             sasl_dispose(&d->conn);   //SASL authentication done
0454         // Fall through
0455         case LoginJobPrivate::Login:
0456             d->saveServerGreeting(response);
0457             emitResult(); //got an OK, command done
0458             break;
0459 
0460         }
0461 
0462     }
0463 
0464     if (code == MALFORMED) {
0465         setErrorText(QString("%1 failed, malformed reply from the server.").arg(commandName));
0466         emitResult();
0467     }
0468 }
0469 
0470 bool LoginJobPrivate::startAuthentication()
0471 {
0472     //SASL authentication
0473     if (!initSASL()) {
0474         q->setError(LoginFailed);
0475         q->setErrorText(QString("Login failed, client cannot initialize the SASL library."));
0476         return false;
0477     }
0478 
0479     authState = LoginJobPrivate::Authenticate;
0480     const char *out = Q_NULLPTR;
0481     uint outlen = 0;
0482     const char *mechusing = Q_NULLPTR;
0483 
0484     int result = sasl_client_new("imap", m_session->hostName().toLatin1(), Q_NULLPTR, nullptr, callbacks, 0, &conn);
0485     if (result != SASL_OK) {
0486         qCWarning(KIMAP2_LOG) << "sasl_client_new failed with:" << result;
0487         q->setError(LoginFailed);
0488         q->setErrorText(QString::fromUtf8(sasl_errdetail(conn)));
0489         return false;
0490     }
0491 
0492     do {
0493         result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains(QStringLiteral("SASL-IR")) ? &out : Q_NULLPTR, &outlen, &mechusing);
0494 
0495         if (result == SASL_INTERACT) {
0496             if (!sasl_interact()) {
0497                 sasl_dispose(&conn);
0498                 q->setError(LoginFailed);   //TODO: check up the actual error
0499                 q->setErrorText(QString("sasl_interact failed"));
0500                 return false;
0501             }
0502         }
0503     } while (result == SASL_INTERACT);
0504 
0505     if (result != SASL_CONTINUE && result != SASL_OK) {
0506         qCWarning(KIMAP2_LOG) << "sasl_client_start failed with:" << result;
0507         q->setError(LoginFailed);
0508         q->setErrorText(QString::fromUtf8(sasl_errdetail(conn)));
0509         sasl_dispose(&conn);
0510         return false;
0511     }
0512 
0513     QByteArray tmp = QByteArray::fromRawData(out, outlen);
0514     QByteArray challenge = tmp.toBase64();
0515 
0516     if (challenge.isEmpty()) {
0517         sendCommand("AUTHENTICATE", authMode.toLatin1());
0518     } else {
0519         sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge);
0520     }
0521 
0522     return true;
0523 }
0524 
0525 void LoginJobPrivate::sendPlainLogin()
0526 {
0527     authState = LoginJobPrivate::Login;
0528     qCDebug(KIMAP2_LOG) << "sending LOGIN";
0529     sendCommand("LOGIN",
0530             '"' + quoteIMAP(userName).toUtf8() + '"' +
0531             ' ' +
0532             '"' + quoteIMAP(password).toUtf8() + '"');
0533 }
0534 
0535 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
0536 {
0537     QByteArray challenge = data;
0538     int result = -1;
0539     const char *out = Q_NULLPTR;
0540     uint outlen = 0;
0541     do {
0542         result = sasl_client_step(conn, challenge.isEmpty() ? Q_NULLPTR : challenge.data(),
0543                                   challenge.size(),
0544                                   &client_interact,
0545                                   &out, &outlen);
0546 
0547         if (result == SASL_INTERACT) {
0548             if (!sasl_interact()) {
0549                 q->setError(LoginFailed);   //TODO: check up the actual error
0550                 q->setErrorText(QString("sasl_interact failed"));
0551                 sasl_dispose(&conn);
0552                 return false;
0553             }
0554         }
0555     } while (result == SASL_INTERACT);
0556 
0557     if (result != SASL_CONTINUE && result != SASL_OK) {
0558         qCWarning(KIMAP2_LOG) << "sasl_client_step failed with:" << result;
0559         q->setError(LoginFailed);   //TODO: check up the actual error
0560         q->setErrorText(QString::fromUtf8(sasl_errdetail(conn)));
0561         sasl_dispose(&conn);
0562         return false;
0563     }
0564 
0565     QByteArray tmp = QByteArray::fromRawData(out, outlen);
0566     challenge = tmp.toBase64();
0567 
0568     sessionInternal()->sendData(challenge);
0569 
0570     return true;
0571 }
0572 
0573 void LoginJob::setEncryptionMode(QSsl::SslProtocol mode, bool startTls)
0574 {
0575     Q_D(LoginJob);
0576     d->encryptionMode = mode;
0577     d->startTls = startTls;
0578 }
0579 
0580 QSsl::SslProtocol LoginJob::encryptionMode()
0581 {
0582     Q_D(LoginJob);
0583     return d->encryptionMode;
0584 }
0585 
0586 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
0587 {
0588     Q_D(LoginJob);
0589     switch (mode) {
0590     case ClearText: d->authMode = QLatin1String("");
0591         break;
0592     case Login: d->authMode = QStringLiteral("LOGIN");
0593         break;
0594     case Plain: d->authMode = QStringLiteral("PLAIN");
0595         break;
0596     case CramMD5: d->authMode = QStringLiteral("CRAM-MD5");
0597         break;
0598     case DigestMD5: d->authMode = QStringLiteral("DIGEST-MD5");
0599         break;
0600     case GSSAPI: d->authMode = QStringLiteral("GSSAPI");
0601         break;
0602     case Anonymous: d->authMode = QStringLiteral("ANONYMOUS");
0603         break;
0604     case XOAuth2: d->authMode = QStringLiteral("XOAUTH2");
0605         break;
0606     default:
0607         d->authMode = QStringLiteral("");
0608     }
0609 }
0610 
0611 void LoginJob::connectionLost()
0612 {
0613     Q_D(LoginJob);
0614 
0615     qCWarning(KIMAP2_LOG) << "Connection to server lost " << d->m_socketError;
0616     if (d->m_socketError == QSslSocket::SslHandshakeFailedError) {
0617         setError(SslHandshakeFailed);
0618         setErrorText(QString::fromUtf8("SSL handshake failed."));
0619         emitResult();
0620     } else if (d->m_socketError == QSslSocket::HostNotFoundError) {
0621         setError(HostNotFound);
0622         setErrorText(QString::fromUtf8("Host not found."));
0623         emitResult();
0624     } else {
0625         setError(CouldNotConnect);
0626         setErrorText(QString::fromUtf8("Connection to server lost."));
0627         emitResult();
0628     }
0629 }
0630 
0631 void LoginJobPrivate::saveServerGreeting(const Message &response)
0632 {
0633     // Concatenate the parts of the server response into a string, while dropping the first two parts
0634     // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
0635 
0636     for (int i = 2; i < response.content.size(); i++) {
0637         if (response.content.at(i).type() == Message::Part::List) {
0638             serverGreeting += QLatin1Char('(');
0639             foreach (const QByteArray &item, response.content.at(i).toList()) {
0640                 serverGreeting += QLatin1String(item) + QLatin1Char(' ');
0641             }
0642             serverGreeting.chop(1);
0643             serverGreeting += QStringLiteral(") ");
0644         } else {
0645             serverGreeting += QLatin1String(response.content.at(i).toString()) + QLatin1Char(' ');
0646         }
0647     }
0648     serverGreeting.chop(1);
0649 }
0650 
0651 QString LoginJob::serverGreeting() const
0652 {
0653     Q_D(const LoginJob);
0654     return d->serverGreeting;
0655 }
0656 
0657 #include "moc_loginjob.cpp"