/* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Derived from checkpassword-pam by Alexey Mahotkin 2002-2004 Modified and enhanced by Manuel Mausz 2017 */ #define _DEFAULT_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" #include "dovecot-auth.h" #define PACKAGE "checkpassword-dovecot" #define VERSION "0.3" /* command line options */ static int opt_dont_set_env = 0; static int opt_dont_chdir = 0; int opt_debug = 0; int opt_use_syslog = 0; #define AUTH_LOGIN_URI "unix:///run/dovecot/auth-client" static const char *opt_auth_login_uri = AUTH_LOGIN_URI; static const char* short_options = "a:dehHs:t:V"; enum { OPT_SYSLOG = 1 }; static struct option long_options[] = { { "auth", required_argument, NULL, 'a' }, { "debug", no_argument, NULL, 'd' }, { "noenv", no_argument, NULL, 'e' }, { "help", no_argument, NULL, 'h' }, { "no-chdir", no_argument, NULL, 'H' }, { "service", required_argument, NULL, 's' }, { "syslog", no_argument, NULL, OPT_SYSLOG }, { "timeout", required_argument, NULL, 't' }, { "version", no_argument, NULL, 'V' }, { NULL, 0, NULL, 0 } }; static const char *usage = "Usage: " PACKAGE " [OPTION]... -- prog...\n" "\n" "Authenticate using Dovecot and the checkpassword protocol:\n" " https://wiki2.dovecot.org/Design/AuthProtocol\n" " http://cr.yp.to/checkpwd/interface.html\n" "and optional run the program specified as 'prog'\n" "\n" "Options are:\n" " -a, --auth=URI\tURI to dovecot auth login socket\n" "\t\t\te.g. unix:///path/to/socket or tcp://ip:port\n" "\t\t\tdefault is " AUTH_LOGIN_URI "\n" " -d, --debug\t\tturn on debugging output\n" " -e, --noenv\t\tdo not set uid, gid, environment variables,\n" "\t\t\tand home directory\n" " -H, --no-chdir\tdo not change to home directory\n" " -h, --help\t\tdisplay this help and exit\n" " -s, --service=SERVICE\tspecify service name to use\n" " --syslog\t\tlog to syslog instead of stderr\n" " -t, --timeout=SECS\tconnection timeout in seconds\n" " -V, --version\t\tdisplay version information and exit\n"; /* checkpassword exit code */ #define PROTOCOL_EXIT_SUCCESS 0 #define PROTOCOL_EXIT_INVALID 1 /* invalid credentials */ #define PROTOCOL_EXIT_FAILURE 2 /* unexpected failure */ #define PROTOCOL_EXIT_DISABLED 110 /* login disabled */ #define PROTOCOL_EXIT_TEMPFAIL 111 /* checkpassword protocol support */ #define PROTOCOL_FD 3 #define PROTOCOL_BACK_FD 4 #define PROTOCOL_LEN 512 static char upbuf[PROTOCOL_LEN]; /* pointers into upbuf[] */ static char *username = NULL; static char *password = NULL; #define V4MAPPREFIX "::ffff:" static char *tcpserver_ipaddress(const char *name) { char *remote_ip = getenv(name); if (!remote_ip) return NULL; /* check for ipv4 mapped ipv6 address */ const char *tmp = getenv("PROTO"); if (tmp && strcmp(tmp, "TCP6") == 0) { if (!strncmp(remote_ip, V4MAPPREFIX, strlen(V4MAPPREFIX))) remote_ip += strlen(V4MAPPREFIX); } return remote_ip; } int main(int argc, char *argv[]) { char *service_name = NULL; int exit_status = PROTOCOL_EXIT_FAILURE, auth_timeout = 10; struct auth_reply reply = {0}; log_init(argv[0]); /* process command line options */ opterr = 0; while (1) { int option_index = 0; int c = getopt_long(argc, argv, short_options, long_options, &option_index); if (c == -1) break; switch (c) { case OPT_SYSLOG: opt_use_syslog = 1; break; case 'a': opt_auth_login_uri = strdup(optarg); break; case 'd': opt_debug = 1; break; case 'e': opt_dont_set_env = 1; break; case 'h': puts(usage); exit(EXIT_SUCCESS); case 'H': opt_dont_chdir = 1; break; case 's': service_name = strdup(optarg); break; case 't': auth_timeout = atoi(optarg); break; case 'V': puts(PACKAGE " " VERSION); exit(EXIT_SUCCESS); case '?': log_error("Invalid command line, see --help"); exit(PROTOCOL_EXIT_FAILURE); } } if (service_name == NULL) { log_error("Missing service name. Use --service=SERVICE"); goto out; } log_close(); log_init(service_name); /* read the username/password */ FILE *protocol = fdopen(PROTOCOL_FD, "r"); if (protocol == NULL) { log_error("Error opening fd %d: %s", PROTOCOL_FD, strerror(errno)); goto out; } log_debug("Reading username and password"); size_t uplen = fread(upbuf, 1, PROTOCOL_LEN, protocol); (void)fclose(protocol); if (uplen == 0) { log_error("Checkpassword protocol failure: zero bytes read"); goto out; } /* extract username */ size_t i = 0; username = upbuf + i; while (upbuf[i++]) { if (i >= uplen) { log_error("Checkpassword protocol failure: username not provided"); goto out; } } log_debug("Username '%s'", username); /* extract password */ password = upbuf + i; while (upbuf[i++]) { if (i >= uplen) { log_error("Checkpassword protocol failure: password not provided"); goto out; } } log_debug("Password read successfully"); FILE *auth = auth_connect(opt_auth_login_uri, auth_timeout); if (auth == NULL) { goto out; } char *tmp, *buf = auth_begin(); buf = auth_add_parameter(buf, "service", service_name); if ((tmp = tcpserver_ipaddress("TCPLOCALIP")) != NULL) { buf = auth_add_parameter(buf, "lip", tmp); if ((tmp = tcpserver_ipaddress("TCPREMOTEIP")) != NULL) buf = auth_add_parameter(buf, "rip", tmp); if ((tmp = getenv("TCPLOCALPORT")) != NULL) buf = auth_add_parameter(buf, "lport", tmp); if ((tmp = getenv("TCPREMOTEPORT")) != NULL) buf = auth_add_parameter(buf, "rport", tmp); if (getenv("SMTPSECURED")) buf = auth_add_parameter(buf, "secured", NULL); } int res = auth_login(auth, buf, username, password, &reply); (void)fclose(auth); if (res == AUTH_FAIL) { log_debug("Login failed: Invalid credentials"); exit_status = PROTOCOL_EXIT_INVALID; } else if (res == AUTH_TEMP) { log_debug("Login failed: Temporary failure"); exit_status = PROTOCOL_EXIT_TEMPFAIL; } else if (res == AUTH_NOLOGIN) { log_debug("Login failed: Login has been disabled"); exit_status = PROTOCOL_EXIT_DISABLED; } else if (res == AUTH_OK) { log_debug("Login successful"); exit_status = PROTOCOL_EXIT_SUCCESS; } bool username_changed = false; if (reply.username && strlen(reply.username) > 0 && strcmp(username, reply.username) != 0) { log_debug("Username got changed to '%s'", reply.username); username_changed = true; username = reply.username; } if (reply.reason) log_debug("Got optional reason: %s", reply.reason); if (fcntl(PROTOCOL_BACK_FD, F_GETFD) != -1 || errno != EBADF) { log_debug("Sending back login parameters"); if (username_changed) { (void)write(PROTOCOL_BACK_FD, "USER=", 5); (void)write(PROTOCOL_BACK_FD, username, strlen(username)); (void)write(PROTOCOL_BACK_FD, "\0", 1); } if (reply.reason) { (void)write(PROTOCOL_BACK_FD, "REASON=", 7); (void)write(PROTOCOL_BACK_FD, reply.reason, strlen(reply.reason)); (void)write(PROTOCOL_BACK_FD, "\0", 1); } if (close(PROTOCOL_BACK_FD) == -1) { log_error("close() error: %s", strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; } } if (exit_status != PROTOCOL_EXIT_SUCCESS) goto out; if (opt_dont_set_env) goto execute_program; /* skip setting up process environment */ /* switch to proper uid/gid/groups */ struct passwd *pw = getpwnam(username); if (!pw) { if (opt_debug) log_error("Error getting information about %s from /etc/passwd: %s", username, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } /* set supplementary groups */ if (initgroups(username, pw->pw_gid) == -1) { log_error("Error setting supplementary groups for user %s: %s", username, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } /* set gid */ if (setgid(pw->pw_gid) == -1) { log_error("setgid(%d) error: %s", pw->pw_gid, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } /* set uid */ if (setuid(pw->pw_uid) == -1) { log_error("setuid(%d) error: %s", pw->pw_uid, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } if (!opt_dont_chdir) { /* switch to user home directory */ if (chdir(pw->pw_dir) == -1) { log_error("Error changing directory %s: %s", pw->pw_dir, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } } /* set $USER */ if (setenv("USER", username, 1) == -1) { log_error("Error setting $USER to %s: %s", username, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } /* set $HOME */ if (setenv("HOME", pw->pw_dir, 1) == -1) { log_error("Error setting $HOME to %s: %s", pw->pw_dir, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } /* set $SHELL */ if (setenv("SHELL", pw->pw_shell, 1) == -1) { log_error("Error setting $SHELL to %s: %s", pw->pw_shell, strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } execute_program: /* execute the program, if any */ if (optind < argc) { auth_reply_free(&reply); log_debug("Executing %s", argv[optind]); log_close(); execvp(argv[optind], argv + optind); log_init(service_name); log_error("Cannot exec(%s): %s\n", argv[optind], strerror(errno)); exit_status = PROTOCOL_EXIT_FAILURE; goto out; } /* if no program was provided in command line, simply exit */ out: auth_reply_free(&reply); log_debug("Exiting with status %d", exit_status); log_close(); exit(exit_status); }