1 /**
2 * @file
3 * POP authentication
4 *
5 * @authors
6 * Copyright (C) 2000-2001 Vsevolod Volkov <vvv@mutt.org.ua>
7 * Copyright (C) 2018 Richard Russon <rich@flatcap.org>
8 *
9 * @copyright
10 * This program is free software: you can redistribute it and/or modify it under
11 * the terms of the GNU General Public License as published by the Free Software
12 * Foundation, either version 2 of the License, or (at your option) any later
13 * version.
14 *
15 * This program is distributed in the hope that it will be useful, but WITHOUT
16 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18 * details.
19 *
20 * You should have received a copy of the GNU General Public License along with
21 * this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24 /**
25 * @page pop_auth POP authentication
26 *
27 * POP authentication
28 */
29
30 #include "config.h"
31 #include <stdbool.h>
32 #include <stdio.h>
33 #include <string.h>
34 #include "private.h"
35 #include "mutt/lib.h"
36 #include "address/lib.h"
37 #include "config/lib.h"
38 #include "core/lib.h"
39 #include "conn/lib.h"
40 #include "adata.h"
41 #include "mutt_socket.h"
42 #ifdef USE_SASL
43 #include <sasl/sasl.h>
44 #include <sasl/saslutil.h>
45 #endif
46
47 #ifdef USE_SASL
48 /**
49 * pop_auth_sasl - POP SASL authenticator - Implements PopAuth::authenticate()
50 */
pop_auth_sasl(struct PopAccountData * adata,const char * method)51 static enum PopAuthRes pop_auth_sasl(struct PopAccountData *adata, const char *method)
52 {
53 sasl_conn_t *saslconn = NULL;
54 sasl_interact_t *interaction = NULL;
55 int rc;
56 char inbuf[1024];
57 const char *mech = NULL;
58 const char *pc = NULL;
59 unsigned int len = 0, olen = 0;
60
61 if (mutt_account_getpass(&adata->conn->account) || !adata->conn->account.pass[0])
62 return POP_A_FAILURE;
63
64 if (mutt_sasl_client_new(adata->conn, &saslconn) < 0)
65 {
66 mutt_debug(LL_DEBUG1, "Error allocating SASL connection\n");
67 return POP_A_FAILURE;
68 }
69
70 if (!method)
71 method = adata->auth_list.data;
72
73 while (true)
74 {
75 rc = sasl_client_start(saslconn, method, &interaction, &pc, &olen, &mech);
76 if (rc != SASL_INTERACT)
77 break;
78 mutt_sasl_interact(interaction);
79 }
80
81 if ((rc != SASL_OK) && (rc != SASL_CONTINUE))
82 {
83 mutt_debug(
84 LL_DEBUG1,
85 "Failure starting authentication exchange. No shared mechanisms?\n");
86
87 /* SASL doesn't support suggested mechanisms, so fall back */
88 sasl_dispose(&saslconn);
89 return POP_A_UNAVAIL;
90 }
91
92 /* About client_start: If sasl_client_start() returns data via pc/olen,
93 * the client is expected to send this first (after the AUTH string is sent).
94 * sasl_client_start() may in fact return SASL_OK in this case. */
95 unsigned int client_start = olen;
96
97 // L10N: (%s) is the method name, e.g. Anonymous, CRAM-MD5, GSSAPI, SASL
98 mutt_message(_("Authenticating (%s)..."), "SASL");
99
100 size_t bufsize = MAX((olen * 2), 1024);
101 char *buf = mutt_mem_malloc(bufsize);
102
103 snprintf(buf, bufsize, "AUTH %s", mech);
104 olen = strlen(buf);
105
106 /* looping protocol */
107 while (true)
108 {
109 mutt_str_copy(buf + olen, "\r\n", bufsize - olen);
110 mutt_socket_send(adata->conn, buf);
111 if (mutt_socket_readln_d(inbuf, sizeof(inbuf), adata->conn, MUTT_SOCK_LOG_FULL) < 0)
112 {
113 sasl_dispose(&saslconn);
114 adata->status = POP_DISCONNECTED;
115 FREE(&buf);
116 return POP_A_SOCKET;
117 }
118
119 /* Note we don't exit if rc==SASL_OK when client_start is true.
120 * This is because the first loop has only sent the AUTH string, we
121 * need to loop at least once more to send the pc/olen returned
122 * by sasl_client_start(). */
123 if (!client_start && (rc != SASL_CONTINUE))
124 break;
125
126 if (mutt_str_startswith(inbuf, "+ ") &&
127 (sasl_decode64(inbuf + 2, strlen(inbuf + 2), buf, bufsize - 1, &len) != SASL_OK))
128 {
129 mutt_debug(LL_DEBUG1, "error base64-decoding server response\n");
130 goto bail;
131 }
132
133 if (!client_start)
134 {
135 while (true)
136 {
137 rc = sasl_client_step(saslconn, buf, len, &interaction, &pc, &olen);
138 if (rc != SASL_INTERACT)
139 break;
140 mutt_sasl_interact(interaction);
141 }
142 }
143 else
144 {
145 olen = client_start;
146 client_start = 0;
147 }
148
149 /* Even if sasl_client_step() returns SASL_OK, we should send at
150 * least one more line to the server. */
151 if ((rc != SASL_CONTINUE) && (rc != SASL_OK))
152 break;
153
154 /* send out response, or line break if none needed */
155 if (pc)
156 {
157 if ((olen * 2) > bufsize)
158 {
159 bufsize = olen * 2;
160 mutt_mem_realloc(&buf, bufsize);
161 }
162 if (sasl_encode64(pc, olen, buf, bufsize, &olen) != SASL_OK)
163 {
164 mutt_debug(LL_DEBUG1, "error base64-encoding client response\n");
165 goto bail;
166 }
167 }
168 }
169
170 if (rc != SASL_OK)
171 goto bail;
172
173 if (mutt_str_startswith(inbuf, "+OK"))
174 {
175 mutt_sasl_setup_conn(adata->conn, saslconn);
176 FREE(&buf);
177 return POP_A_SUCCESS;
178 }
179
180 bail:
181 sasl_dispose(&saslconn);
182
183 /* terminate SASL session if the last response is not +OK nor -ERR */
184 if (mutt_str_startswith(inbuf, "+ "))
185 {
186 snprintf(buf, bufsize, "*\r\n");
187 if (pop_query(adata, buf, bufsize) == -1)
188 {
189 FREE(&buf);
190 return POP_A_SOCKET;
191 }
192 }
193
194 FREE(&buf);
195 // L10N: %s is the method name, e.g. Anonymous, CRAM-MD5, GSSAPI, SASL
196 mutt_error(_("%s authentication failed"), "SASL");
197
198 return POP_A_FAILURE;
199 }
200 #endif
201
202 /**
203 * pop_apop_timestamp - Get the server timestamp for APOP authentication
204 * @param adata POP Account data
205 * @param buf Timestamp string
206 */
pop_apop_timestamp(struct PopAccountData * adata,char * buf)207 void pop_apop_timestamp(struct PopAccountData *adata, char *buf)
208 {
209 char *p1 = NULL, *p2 = NULL;
210
211 FREE(&adata->timestamp);
212
213 if ((p1 = strchr(buf, '<')) && (p2 = strchr(p1, '>')))
214 {
215 p2[1] = '\0';
216 adata->timestamp = mutt_str_dup(p1);
217 }
218 }
219
220 /**
221 * pop_auth_apop - APOP authenticator - Implements PopAuth::authenticate()
222 */
pop_auth_apop(struct PopAccountData * adata,const char * method)223 static enum PopAuthRes pop_auth_apop(struct PopAccountData *adata, const char *method)
224 {
225 struct Md5Ctx md5ctx;
226 unsigned char digest[16];
227 char hash[33];
228 char buf[1024];
229
230 if (mutt_account_getpass(&adata->conn->account) || !adata->conn->account.pass[0])
231 return POP_A_FAILURE;
232
233 if (!adata->timestamp)
234 return POP_A_UNAVAIL;
235
236 if (!mutt_addr_valid_msgid(adata->timestamp))
237 {
238 mutt_error(_("POP timestamp is invalid"));
239 return POP_A_UNAVAIL;
240 }
241
242 // L10N: (%s) is the method name, e.g. Anonymous, CRAM-MD5, GSSAPI, SASL
243 mutt_message(_("Authenticating (%s)..."), "APOP");
244
245 /* Compute the authentication hash to send to the server */
246 mutt_md5_init_ctx(&md5ctx);
247 mutt_md5_process(adata->timestamp, &md5ctx);
248 mutt_md5_process(adata->conn->account.pass, &md5ctx);
249 mutt_md5_finish_ctx(&md5ctx, digest);
250 mutt_md5_toascii(digest, hash);
251
252 /* Send APOP command to server */
253 snprintf(buf, sizeof(buf), "APOP %s %s\r\n", adata->conn->account.user, hash);
254
255 switch (pop_query(adata, buf, sizeof(buf)))
256 {
257 case 0:
258 return POP_A_SUCCESS;
259 case -1:
260 return POP_A_SOCKET;
261 }
262
263 // L10N: %s is the method name, e.g. Anonymous, CRAM-MD5, GSSAPI, SASL
264 mutt_error(_("%s authentication failed"), "APOP");
265
266 return POP_A_FAILURE;
267 }
268
269 /**
270 * pop_auth_user - USER authenticator - Implements PopAuth::authenticate()
271 */
pop_auth_user(struct PopAccountData * adata,const char * method)272 static enum PopAuthRes pop_auth_user(struct PopAccountData *adata, const char *method)
273 {
274 if (!adata->cmd_user)
275 return POP_A_UNAVAIL;
276
277 if (mutt_account_getpass(&adata->conn->account) || !adata->conn->account.pass[0])
278 return POP_A_FAILURE;
279
280 mutt_message(_("Logging in..."));
281
282 char buf[1024];
283 snprintf(buf, sizeof(buf), "USER %s\r\n", adata->conn->account.user);
284 int ret = pop_query(adata, buf, sizeof(buf));
285
286 if (adata->cmd_user == 2)
287 {
288 if (ret == 0)
289 {
290 adata->cmd_user = 1;
291
292 mutt_debug(LL_DEBUG1, "set USER capability\n");
293 }
294
295 if (ret == -2)
296 {
297 adata->cmd_user = 0;
298
299 mutt_debug(LL_DEBUG1, "unset USER capability\n");
300 snprintf(adata->err_msg, sizeof(adata->err_msg), "%s",
301 _("Command USER is not supported by server"));
302 }
303 }
304
305 if (ret == 0)
306 {
307 snprintf(buf, sizeof(buf), "PASS %s\r\n", adata->conn->account.pass);
308 const short c_debug_level = cs_subset_number(NeoMutt->sub, "debug_level");
309 ret = pop_query_d(adata, buf, sizeof(buf),
310 /* don't print the password unless we're at the ungodly debugging level */
311 (c_debug_level < MUTT_SOCK_LOG_FULL) ? "PASS *\r\n" : NULL);
312 }
313
314 switch (ret)
315 {
316 case 0:
317 return POP_A_SUCCESS;
318 case -1:
319 return POP_A_SOCKET;
320 }
321
322 mutt_error("%s %s", _("Login failed"), adata->err_msg);
323
324 return POP_A_FAILURE;
325 }
326
327 /**
328 * pop_auth_oauth - Authenticate a POP connection using OAUTHBEARER - Implements PopAuth::authenticate()
329 */
pop_auth_oauth(struct PopAccountData * adata,const char * method)330 static enum PopAuthRes pop_auth_oauth(struct PopAccountData *adata, const char *method)
331 {
332 /* If they did not explicitly request or configure oauth then fail quietly */
333 const char *const c_pop_oauth_refresh_command =
334 cs_subset_string(NeoMutt->sub, "pop_oauth_refresh_command");
335 if (!method && !c_pop_oauth_refresh_command)
336 return POP_A_UNAVAIL;
337
338 // L10N: (%s) is the method name, e.g. Anonymous, CRAM-MD5, GSSAPI, SASL
339 mutt_message(_("Authenticating (%s)..."), "OAUTHBEARER");
340
341 char *oauthbearer = mutt_account_getoauthbearer(&adata->conn->account, false);
342 if (!oauthbearer)
343 return POP_A_FAILURE;
344
345 size_t auth_cmd_len = strlen(oauthbearer) + 30;
346 char *auth_cmd = mutt_mem_malloc(auth_cmd_len);
347 snprintf(auth_cmd, auth_cmd_len, "AUTH OAUTHBEARER %s\r\n", oauthbearer);
348 FREE(&oauthbearer);
349
350 int ret = pop_query_d(adata, auth_cmd, strlen(auth_cmd),
351 #ifdef DEBUG
352 /* don't print the bearer token unless we're at the ungodly debugging level */
353 (cs_subset_number(NeoMutt->sub, "debug_level") < MUTT_SOCK_LOG_FULL) ?
354 "AUTH OAUTHBEARER *\r\n" :
355 #endif
356 NULL);
357 FREE(&auth_cmd);
358
359 switch (ret)
360 {
361 case 0:
362 return POP_A_SUCCESS;
363 case -1:
364 return POP_A_SOCKET;
365 }
366
367 /* The error response was a SASL continuation, so "continue" it.
368 * See RFC7628 3.2.3 */
369 mutt_socket_send(adata->conn, "\001");
370
371 char *err = adata->err_msg;
372 char decoded_err[1024];
373 int len = mutt_b64_decode(adata->err_msg, decoded_err, sizeof(decoded_err) - 1);
374 if (len >= 0)
375 {
376 decoded_err[len] = '\0';
377 err = decoded_err;
378 }
379 mutt_error("%s %s", _("Authentication failed"), err);
380
381 return POP_A_FAILURE;
382 }
383
384 /**
385 * PopAuthenticators - Accepted authentication methods
386 */
387 static const struct PopAuth PopAuthenticators[] = {
388 // clang-format off
389 { pop_auth_oauth, "oauthbearer" },
390 #ifdef USE_SASL
391 { pop_auth_sasl, NULL },
392 #endif
393 { pop_auth_apop, "apop" },
394 { pop_auth_user, "user" },
395 { NULL, NULL },
396 // clang-format on
397 };
398
399 /**
400 * pop_auth_is_valid - Check if string is a valid pop authentication method
401 * @param authenticator Authenticator string to check
402 * @retval true Argument is a valid auth method
403 *
404 * Validate whether an input string is an accepted pop authentication method as
405 * defined by #PopAuthenticators.
406 */
pop_auth_is_valid(const char * authenticator)407 bool pop_auth_is_valid(const char *authenticator)
408 {
409 for (size_t i = 0; i < mutt_array_size(PopAuthenticators); i++)
410 {
411 const struct PopAuth *auth = &PopAuthenticators[i];
412 if (auth->method && mutt_istr_equal(auth->method, authenticator))
413 return true;
414 }
415
416 return false;
417 }
418
419 /**
420 * pop_authenticate - Authenticate with a POP server
421 * @param adata POP Account data
422 * @retval num Result, e.g. #POP_A_SUCCESS
423 * @retval 0 Successful
424 * @retval -1 Connection lost
425 * @retval -2 Login failed
426 * @retval -3 Authentication cancelled
427 */
pop_authenticate(struct PopAccountData * adata)428 int pop_authenticate(struct PopAccountData *adata)
429 {
430 struct ConnAccount *cac = &adata->conn->account;
431 const struct PopAuth *authenticator = NULL;
432 int attempts = 0;
433 int ret = POP_A_UNAVAIL;
434
435 if ((mutt_account_getuser(cac) < 0) || (cac->user[0] == '\0'))
436 {
437 return -3;
438 }
439
440 const struct Slist *c_pop_authenticators =
441 cs_subset_slist(NeoMutt->sub, "pop_authenticators");
442 const bool c_pop_auth_try_all =
443 cs_subset_bool(NeoMutt->sub, "pop_auth_try_all");
444 if (c_pop_authenticators && (c_pop_authenticators->count > 0))
445 {
446 /* Try user-specified list of authentication methods */
447 struct ListNode *np = NULL;
448 STAILQ_FOREACH(np, &c_pop_authenticators->head, entries)
449 {
450 mutt_debug(LL_DEBUG2, "Trying method %s\n", np->data);
451 authenticator = PopAuthenticators;
452
453 while (authenticator->authenticate)
454 {
455 if (!authenticator->method || mutt_istr_equal(authenticator->method, np->data))
456 {
457 ret = authenticator->authenticate(adata, np->data);
458 if (ret == POP_A_SOCKET)
459 {
460 switch (pop_connect(adata))
461 {
462 case 0:
463 {
464 ret = authenticator->authenticate(adata, np->data);
465 break;
466 }
467 case -2:
468 ret = POP_A_FAILURE;
469 }
470 }
471
472 if (ret != POP_A_UNAVAIL)
473 attempts++;
474 if ((ret == POP_A_SUCCESS) || (ret == POP_A_SOCKET) ||
475 ((ret == POP_A_FAILURE) && !c_pop_auth_try_all))
476 {
477 break;
478 }
479 }
480 authenticator++;
481 }
482 }
483 }
484 else
485 {
486 /* Fall back to default: any authenticator */
487 mutt_debug(LL_DEBUG2, "Using any available method\n");
488 authenticator = PopAuthenticators;
489
490 while (authenticator->authenticate)
491 {
492 ret = authenticator->authenticate(adata, NULL);
493 if (ret == POP_A_SOCKET)
494 {
495 switch (pop_connect(adata))
496 {
497 case 0:
498 {
499 ret = authenticator->authenticate(adata, NULL);
500 break;
501 }
502 case -2:
503 ret = POP_A_FAILURE;
504 }
505 }
506
507 if (ret != POP_A_UNAVAIL)
508 attempts++;
509 if ((ret == POP_A_SUCCESS) || (ret == POP_A_SOCKET) ||
510 ((ret == POP_A_FAILURE) && !c_pop_auth_try_all))
511 {
512 break;
513 }
514
515 authenticator++;
516 }
517 }
518
519 switch (ret)
520 {
521 case POP_A_SUCCESS:
522 return 0;
523 case POP_A_SOCKET:
524 return -1;
525 case POP_A_UNAVAIL:
526 if (attempts == 0)
527 mutt_error(_("No authenticators available"));
528 }
529
530 return -2;
531 }
532