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"