KIMAP

loginjob.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Kevin Ottens <[email protected]>
3  SPDX-FileCopyrightText: 2009 Andras Mantia <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "loginjob.h"
9 
10 #include <KLocalizedString>
11 
12 #include "kimap_debug.h"
13 
14 #include "capabilitiesjob.h"
15 #include "job_p.h"
16 #include "response_p.h"
17 #include "rfccodecs.h"
18 #include "session_p.h"
19 
20 #include "common.h"
21 
22 extern "C" {
23 #include <sasl/sasl.h>
24 }
25 
26 static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
27  {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
28  {SASL_CB_GETREALM, nullptr, nullptr},
29  {SASL_CB_USER, nullptr, nullptr},
30  {SASL_CB_AUTHNAME, nullptr, nullptr},
31  {SASL_CB_PASS, nullptr, nullptr},
32  {SASL_CB_CANON_USER, nullptr, nullptr},
33  {SASL_CB_LIST_END, nullptr, nullptr}};
34 
35 namespace KIMAP
36 {
37 class LoginJobPrivate : public JobPrivate
38 {
39 public:
40  enum AuthState { PreStartTlsCapability = 0, StartTls, Capability, Login, Authenticate };
41 
42  LoginJobPrivate(LoginJob *job, Session *session, const QString &name)
43  : JobPrivate(session, name)
44  , q(job)
45  , encryptionMode(LoginJob::Unencrypted)
46  , authState(Login)
47  , plainLoginDisabled(false)
48  {
49  conn = nullptr;
50  client_interact = nullptr;
51  }
52  ~LoginJobPrivate()
53  {
54  }
55  bool sasl_interact();
56 
57  bool startAuthentication();
58  bool answerChallenge(const QByteArray &data);
59  void sslResponse(bool response);
60  void saveServerGreeting(const Response &response);
61 
62  LoginJob *const q;
63 
64  QString userName;
65  QString authorizationName;
66  QString password;
67  QString serverGreeting;
68 
69  LoginJob::EncryptionMode encryptionMode;
70  QString authMode;
71  AuthState authState;
73  bool plainLoginDisabled;
74 
75  sasl_conn_t *conn;
76  sasl_interact_t *client_interact;
77 };
78 }
79 
80 using namespace KIMAP;
81 
82 bool LoginJobPrivate::sasl_interact()
83 {
84  qCDebug(KIMAP_LOG) << "sasl_interact";
85  sasl_interact_t *interact = client_interact;
86 
87  // some mechanisms do not require username && pass, so it doesn't need a popup
88  // window for getting this info
89  for (; interact->id != SASL_CB_LIST_END; interact++) {
90  if (interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS) {
91  // TODO: dialog for use name??
92  break;
93  }
94  }
95 
96  interact = client_interact;
97  while (interact->id != SASL_CB_LIST_END) {
98  qCDebug(KIMAP_LOG) << "SASL_INTERACT id:" << interact->id;
99  switch (interact->id) {
100  case SASL_CB_AUTHNAME:
101  if (!authorizationName.isEmpty()) {
102  qCDebug(KIMAP_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'";
103  interact->result = strdup(authorizationName.toUtf8().constData());
104  interact->len = strlen((const char *)interact->result);
105  break;
106  }
107  [[fallthrough]];
108  case SASL_CB_USER:
109  qCDebug(KIMAP_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'";
110  interact->result = strdup(userName.toUtf8().constData());
111  interact->len = strlen((const char *)interact->result);
112  break;
113  case SASL_CB_PASS:
114  qCDebug(KIMAP_LOG) << "SASL_CB_PASS: [hidden]";
115  interact->result = strdup(password.toUtf8().constData());
116  interact->len = strlen((const char *)interact->result);
117  break;
118  default:
119  interact->result = nullptr;
120  interact->len = 0;
121  break;
122  }
123  interact++;
124  }
125  return true;
126 }
127 
128 LoginJob::LoginJob(Session *session)
129  : Job(*new LoginJobPrivate(this, session, i18n("Login")))
130 {
131  Q_D(LoginJob);
132  qCDebug(KIMAP_LOG) << this;
133 }
134 
135 LoginJob::~LoginJob()
136 {
137  qCDebug(KIMAP_LOG) << this;
138 }
139 
140 QString LoginJob::userName() const
141 {
142  Q_D(const LoginJob);
143  return d->userName;
144 }
145 
146 void LoginJob::setUserName(const QString &userName)
147 {
148  Q_D(LoginJob);
149  d->userName = userName;
150 }
151 
152 QString LoginJob::authorizationName() const
153 {
154  Q_D(const LoginJob);
155  return d->authorizationName;
156 }
157 
158 void LoginJob::setAuthorizationName(const QString &authorizationName)
159 {
160  Q_D(LoginJob);
161  d->authorizationName = authorizationName;
162 }
163 
164 QString LoginJob::password() const
165 {
166  Q_D(const LoginJob);
167  return d->password;
168 }
169 
170 void LoginJob::setPassword(const QString &password)
171 {
172  Q_D(LoginJob);
173  d->password = password;
174 }
175 
176 void LoginJob::doStart()
177 {
178  Q_D(LoginJob);
179 
180  qCDebug(KIMAP_LOG) << this;
181  // Don't authenticate on a session in the authenticated state
182  if (session()->state() == Session::Authenticated || session()->state() == Session::Selected) {
183  setError(UserDefinedError);
184  setErrorText(i18n("IMAP session in the wrong state for authentication"));
185  emitResult();
186  return;
187  }
188 
189  // Get notified once encryption is successfully negotiated
190  connect(d->sessionInternal(), &KIMAP::SessionPrivate::encryptionNegotiationResult, this, [d](bool result) {
191  d->sslResponse(result);
192  });
193 
194  // Trigger encryption negotiation only if needed
195  EncryptionMode encryptionMode = d->encryptionMode;
196 
197  const auto negotiatedEncryption = d->sessionInternal()->negotiatedEncryption();
198  if (negotiatedEncryption != QSsl::UnknownProtocol) {
199  // If the socket is already encrypted, proceed to the next state
200  d->sslResponse(true);
201  return;
202  }
203 
204  if (encryptionMode == SSLorTLS) {
205  // Negotiation got started by Session, but didn't complete yet. Continue in sslResponse.
206  } else if (encryptionMode == STARTTLS) {
207  // Check if STARTTLS is supported
208  d->authState = LoginJobPrivate::PreStartTlsCapability;
209  d->tags << d->sessionInternal()->sendCommand("CAPABILITY");
210  } else if (encryptionMode == Unencrypted) {
211  if (d->authMode.isEmpty()) {
212  d->authState = LoginJobPrivate::Login;
213  qCDebug(KIMAP_LOG) << "sending LOGIN";
214  d->tags << d->sessionInternal()->sendCommand("LOGIN",
215  '"' + quoteIMAP(d->userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(d->password).toUtf8() + '"');
216  } else {
217  if (!d->startAuthentication()) {
218  emitResult();
219  }
220  }
221  }
222 }
223 
224 void LoginJob::handleResponse(const Response &response)
225 {
226  Q_D(LoginJob);
227 
228  if (response.content.isEmpty()) {
229  return;
230  }
231 
232  // set the actual command name for standard responses
233  QString commandName = i18n("Login");
234  if (d->authState == LoginJobPrivate::Capability) {
235  commandName = i18n("Capability");
236  } else if (d->authState == LoginJobPrivate::StartTls) {
237  commandName = i18n("StartTls");
238  }
239 
240  enum ResponseCode { OK, ERR, UNTAGGED, CONTINUATION, MALFORMED };
241 
242  QByteArray tag = response.content.first().toString();
243  ResponseCode code = OK;
244 
245  qCDebug(KIMAP_LOG) << commandName << tag;
246 
247  if (tag == "+") {
248  code = CONTINUATION;
249  } else if (tag == "*") {
250  if (response.content.size() < 2) {
251  code = MALFORMED; // Received empty untagged response
252  } else {
253  code = UNTAGGED;
254  }
255  } else if (d->tags.contains(tag)) {
256  if (response.content.size() < 2) {
257  code = MALFORMED;
258  } else if (response.content[1].toString() == "OK") {
259  code = OK;
260  } else {
261  code = ERR;
262  }
263  }
264 
265  switch (code) {
266  case MALFORMED:
267  // We'll handle it later
268  break;
269 
270  case ERR:
271  // server replied with NO or BAD for SASL authentication
272  if (d->authState == LoginJobPrivate::Authenticate) {
273  sasl_dispose(&d->conn);
274  }
275 
276  setError(UserDefinedError);
277  setErrorText(i18n("%1 failed, server replied: %2", commandName, QLatin1String(response.toString().constData())));
278  emitResult();
279  return;
280 
281  case UNTAGGED:
282  // The only untagged response interesting for us here is CAPABILITY
283  if (response.content[1].toString() == "CAPABILITY") {
284  d->capabilities.clear();
285  QList<Response::Part>::const_iterator p = response.content.begin() + 2;
286  while (p != response.content.end()) {
287  QString capability = QLatin1String(p->toString());
288  d->capabilities << capability;
289  if (capability == QLatin1String("LOGINDISABLED")) {
290  d->plainLoginDisabled = true;
291  }
292  ++p;
293  }
294  qCDebug(KIMAP_LOG) << "Capabilities updated: " << d->capabilities;
295  }
296  break;
297 
298  case CONTINUATION:
299  if (d->authState != LoginJobPrivate::Authenticate) {
300  // Received unexpected continuation response for something
301  // other than AUTHENTICATE command
302  code = MALFORMED;
303  break;
304  }
305 
306  if (d->authMode == QLatin1String("PLAIN")) {
307  if (response.content.size() > 1 && response.content.at(1).toString() == "OK") {
308  return;
309  }
310  QByteArray challengeResponse;
311  if (!d->authorizationName.isEmpty()) {
312  challengeResponse += d->authorizationName.toUtf8();
313  }
314  challengeResponse += '\0';
315  challengeResponse += d->userName.toUtf8();
316  challengeResponse += '\0';
317  challengeResponse += d->password.toUtf8();
318  challengeResponse = challengeResponse.toBase64();
319  d->sessionInternal()->sendData(challengeResponse);
320  } else if (response.content.size() >= 2) {
321  if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
322  emitResult(); // error, we're done
323  }
324  } else {
325  // Received empty continuation for authMode other than PLAIN
326  code = MALFORMED;
327  }
328  break;
329 
330  case OK:
331 
332  switch (d->authState) {
333  case LoginJobPrivate::PreStartTlsCapability:
334  if (d->capabilities.contains(QLatin1String("STARTTLS"))) {
335  d->authState = LoginJobPrivate::StartTls;
336  d->tags << d->sessionInternal()->sendCommand("STARTTLS");
337  } else {
338  qCWarning(KIMAP_LOG) << "STARTTLS not supported by server!";
339  setError(UserDefinedError);
340  setErrorText(i18n("STARTTLS is not supported by the server, try using SSL/TLS instead."));
341  emitResult();
342  }
343  break;
344 
345  case LoginJobPrivate::StartTls:
346  d->sessionInternal()->startSsl(QSsl::SecureProtocols);
347  break;
348 
349  case LoginJobPrivate::Capability:
350  // If encryption was requested, verify that it's negotiated before logging in
351  if (d->encryptionMode != Unencrypted && d->sessionInternal()->negotiatedEncryption() == QSsl::UnknownProtocol) {
352  setError(LoginJob::UserDefinedError);
353  setErrorText(i18n("Internal error, tried to login before encryption"));
354  emitResult();
355  break;
356  }
357 
358  // cleartext login, if enabled
359  if (d->authMode.isEmpty()) {
360  if (d->plainLoginDisabled) {
361  setError(UserDefinedError);
362  setErrorText(i18n("Login failed, plain login is disabled by the server."));
363  emitResult();
364  } else {
365  d->authState = LoginJobPrivate::Login;
366  d->tags << d->sessionInternal()->sendCommand("LOGIN",
367  '"' + quoteIMAP(d->userName).toUtf8() + '"' + ' ' + '"' + quoteIMAP(d->password).toUtf8()
368  + '"');
369  }
370  } else {
371  bool authModeSupported = false;
372  // find the selected SASL authentication method
373  for (const QString &capability : std::as_const(d->capabilities)) {
374  if (capability.startsWith(QLatin1String("AUTH="))) {
375  if (QStringView(capability).mid(5) == d->authMode) {
376  authModeSupported = true;
377  break;
378  }
379  }
380  }
381  if (!authModeSupported) {
382  setError(UserDefinedError);
383  setErrorText(i18n("Login failed, authentication mode %1 is not supported by the server.", d->authMode));
384  emitResult();
385  } else if (!d->startAuthentication()) {
386  emitResult(); // problem, we're done
387  }
388  }
389  break;
390 
391  case LoginJobPrivate::Authenticate:
392  sasl_dispose(&d->conn); // SASL authentication done
393  // Fall through
394  [[fallthrough]];
395  case LoginJobPrivate::Login:
396  d->saveServerGreeting(response);
397  emitResult(); // got an OK, command done
398  break;
399  }
400  }
401 
402  if (code == MALFORMED) {
403  setErrorText(i18n("%1 failed, malformed reply from the server.", commandName));
404  emitResult();
405  }
406 }
407 
408 bool LoginJobPrivate::startAuthentication()
409 {
410  // SASL authentication
411  if (!initSASL()) {
412  q->setError(LoginJob::UserDefinedError);
413  q->setErrorText(i18n("Login failed, client cannot initialize the SASL library."));
414  return false;
415  }
416 
417  authState = LoginJobPrivate::Authenticate;
418  const char *out = nullptr;
419  uint outlen = 0;
420  const char *mechusing = nullptr;
421 
422  int result = sasl_client_new("imap", m_session->hostName().toLatin1().constData(), nullptr, nullptr, callbacks, 0, &conn);
423  if (result != SASL_OK) {
424  const QString saslError = QString::fromUtf8(sasl_errdetail(conn));
425  qCWarning(KIMAP_LOG) << "sasl_client_new failed with:" << result << saslError;
426  q->setError(LoginJob::UserDefinedError);
427  q->setErrorText(saslError);
428  return false;
429  }
430 
431  do {
432  qCDebug(KIMAP_LOG) << "Trying authmod" << authMode.toLatin1();
433  result = sasl_client_start(conn,
434  authMode.toLatin1().constData(),
435  &client_interact,
436  capabilities.contains(QLatin1String("SASL-IR")) ? &out : nullptr,
437  &outlen,
438  &mechusing);
439 
440  if (result == SASL_INTERACT) {
441  if (!sasl_interact()) {
442  sasl_dispose(&conn);
443  q->setError(LoginJob::UserDefinedError); // TODO: check up the actual error
444  return false;
445  }
446  }
447  } while (result == SASL_INTERACT);
448 
449  if (result != SASL_CONTINUE && result != SASL_OK) {
450  const QString saslError = QString::fromUtf8(sasl_errdetail(conn));
451  qCWarning(KIMAP_LOG) << "sasl_client_start failed with:" << result << saslError;
452  q->setError(LoginJob::UserDefinedError);
453  q->setErrorText(saslError);
454  sasl_dispose(&conn);
455  return false;
456  }
457 
458  QByteArray tmp = QByteArray::fromRawData(out, outlen);
459  QByteArray challenge = tmp.toBase64();
460 
461  if (challenge.isEmpty()) {
462  tags << sessionInternal()->sendCommand("AUTHENTICATE", authMode.toLatin1());
463  } else {
464  tags << sessionInternal()->sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge);
465  }
466 
467  return true;
468 }
469 
470 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
471 {
472  QByteArray challenge = data;
473  int result = -1;
474  const char *out = nullptr;
475  uint outlen = 0;
476  do {
477  result = sasl_client_step(conn, challenge.isEmpty() ? nullptr : challenge.data(), challenge.size(), &client_interact, &out, &outlen);
478 
479  if (result == SASL_INTERACT) {
480  if (!sasl_interact()) {
481  q->setError(LoginJob::UserDefinedError); // TODO: check up the actual error
482  sasl_dispose(&conn);
483  return false;
484  }
485  }
486  } while (result == SASL_INTERACT);
487 
488  if (result != SASL_CONTINUE && result != SASL_OK) {
489  const QString saslError = QString::fromUtf8(sasl_errdetail(conn));
490  qCWarning(KIMAP_LOG) << "sasl_client_step failed with:" << result << saslError;
491  q->setError(LoginJob::UserDefinedError); // TODO: check up the actual error
492  q->setErrorText(saslError);
493  sasl_dispose(&conn);
494  return false;
495  }
496 
497  QByteArray tmp = QByteArray::fromRawData(out, outlen);
498  challenge = tmp.toBase64();
499 
500  sessionInternal()->sendData(challenge);
501 
502  return true;
503 }
504 
505 void LoginJobPrivate::sslResponse(bool response)
506 {
507  if (response) {
508  authState = LoginJobPrivate::Capability;
509  tags << sessionInternal()->sendCommand("CAPABILITY");
510  } else {
511  q->setError(LoginJob::UserDefinedError);
512  q->setErrorText(i18n("Login failed, TLS negotiation failed."));
513  encryptionMode = LoginJob::Unencrypted;
514  q->emitResult();
515  }
516 }
517 
518 void LoginJob::setEncryptionMode(EncryptionMode mode)
519 {
520  Q_D(LoginJob);
521  d->encryptionMode = mode;
522 }
523 
524 LoginJob::EncryptionMode LoginJob::encryptionMode()
525 {
526  Q_D(LoginJob);
527  return d->encryptionMode;
528 }
529 
530 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
531 {
532  Q_D(LoginJob);
533  switch (mode) {
534  case ClearText:
535  d->authMode = QLatin1String("");
536  break;
537  case Login:
538  d->authMode = QStringLiteral("LOGIN");
539  break;
540  case Plain:
541  d->authMode = QStringLiteral("PLAIN");
542  break;
543  case CramMD5:
544  d->authMode = QStringLiteral("CRAM-MD5");
545  break;
546  case DigestMD5:
547  d->authMode = QStringLiteral("DIGEST-MD5");
548  break;
549  case GSSAPI:
550  d->authMode = QStringLiteral("GSSAPI");
551  break;
552  case Anonymous:
553  d->authMode = QStringLiteral("ANONYMOUS");
554  break;
555  case XOAuth2:
556  d->authMode = QStringLiteral("XOAUTH2");
557  break;
558  default:
559  d->authMode = QString();
560  }
561 }
562 
563 void LoginJob::connectionLost()
564 {
565  Q_D(LoginJob);
566 
567  qCWarning(KIMAP_LOG) << "Connection to server lost " << d->m_socketError;
568  if (d->m_socketError == QAbstractSocket::SslHandshakeFailedError) {
569  setError(KJob::UserDefinedError);
570  setErrorText(i18n("SSL handshake failed."));
571  emitResult();
572  } else {
573  setError(ERR_COULD_NOT_CONNECT);
574  setErrorText(i18n("Connection to server lost."));
575  emitResult();
576  }
577 }
578 
579 void LoginJobPrivate::saveServerGreeting(const Response &response)
580 {
581  // Concatenate the parts of the server response into a string, while dropping the first two parts
582  // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
583 
584  for (int i = 2; i < response.content.size(); i++) {
585  if (response.content.at(i).type() == Response::Part::List) {
586  serverGreeting += QLatin1Char('(');
587  const QList<QByteArray> itemLst = response.content.at(i).toList();
588  for (const QByteArray &item : itemLst) {
589  serverGreeting += QLatin1String(item) + QLatin1Char(' ');
590  }
591  serverGreeting.chop(1);
592  serverGreeting += QStringLiteral(") ");
593  } else {
594  serverGreeting += QLatin1String(response.content.at(i).toString()) + QLatin1Char(' ');
595  }
596  }
597  serverGreeting.chop(1);
598 }
599 
600 QString LoginJob::serverGreeting() const
601 {
602  Q_D(const LoginJob);
603  return d->serverGreeting;
604 }
605 
606 #include "moc_loginjob.cpp"
QString fromUtf8(const char *str, int size)
QByteArray fromRawData(const char *data, int size)
Capabilities capabilities()
QByteArray toLatin1() const const
QByteArray toBase64(QByteArray::Base64Options options) const const
UnknownProtocol
QString i18n(const char *text, const TYPE &arg...)
const T & at(int i) const const
QByteArray fromBase64(const QByteArray &base64, QByteArray::Base64Options options)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
const char * name(StandardAction id)
int size() const const
KIMAP2_EXPORT QByteArray quoteIMAP(const QByteArray &src)
Replaces " with \" and \ with \ " and \ characters.
Definition: rfccodecs.cpp:149
Provides handlers for various RFC/MIME encodings.
Q_D(Todo)
char * data()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 3 2023 03:51:44 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.