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"