/*
 * Copyright (c) 1999 Ian Freislich
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *	$Id: authenticate.c,v 1.34 2003/01/07 12:25:55 ianf Exp $
 */

#include <sys/param.h>
#include <sys/stat.h>
#include <netinet/in.h>
#ifdef USE_PAM
#include <security/pam_appl.h>
#endif

#ifdef SOLARIS
#include <crypt.h>
#endif
#include <db.h>
#include <fcntl.h>
#include <md5.h>
#include <netdb.h>
#include <pwd.h>
#include <radlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#include <unistd.h>

#include <poputil.h>
#include "config.h"
#include "popd.h"
#include "authenticate.h"

/* global variables */

extern struct config config;

/* function prototypes */

static void	dosetuid(struct connection *user);
static int	db_file_pass(struct connection *cxn),
#ifndef USE_PAM
		unix_pass(struct connection *cxn),
#endif
		radius_pass(struct connection *cxn),
		checkpassword(struct connection *cxn),
		checkchallenge(struct connection *cxn, char *ch);
static char	*getsecret(struct connection *cxn);

#ifdef USE_PAM
static void	freeresponse(int num, struct pam_response *response);
static int	conversation(int num, const struct pam_message **msg,
		    struct pam_response **result, void *appdata),
		pam_pass(struct connection *cxn);

static void
freeresponse(int num, struct pam_response *response)
{
	int i;

	for (i = 0; i < num; i++)
		if (response[i].resp != NULL)
			free(response[i].resp);
	free(response);
}

static int
conversation(int num, const struct pam_message **msg,
    struct pam_response **result, void *appdata)
{
	struct pam_response	*response;
	int			 i;

	response = xmalloc(num * sizeof(struct pam_response));

	for (i = 0; i < num; i++)
		switch (msg[i]->msg_style) {
		    case PAM_PROMPT_ECHO_ON:
			response[i].resp = strdup(((char **)appdata)[0]);
			if (response[i].resp == NULL) {
				freeresponse(i, response);
				return PAM_CONV_ERR;
			}
			response[i].resp_retcode = PAM_SUCCESS;
			break;
		    case PAM_PROMPT_ECHO_OFF:
			response[i].resp = strdup(((char **)appdata)[1]);
			if (response[i].resp == NULL) {
				freeresponse(i, response);
				return PAM_CONV_ERR;
			}
			response[i].resp_retcode = PAM_SUCCESS;
			break;
		    case PAM_TEXT_INFO:
		    case PAM_ERROR_MSG:
			response[i].resp_retcode = PAM_SUCCESS;
			response[i].resp = NULL;
			break;
		    default:
			freeresponse(i, response);
			return PAM_CONV_ERR;
		}
	*result = response;
	return PAM_SUCCESS;
}
#endif

/*
 *  Set the effective userid of this proccess to
 *  the given username. Root privilage is too great for
 *  access to mailboxes.
 *  ( assumption here, we are runing as root )
 *  
 *  Return on success, exits on failures - no permission and
 *  no such user
 */  
static void
dosetuid(struct connection *cxn)
{
	char		*username;
	struct passwd	*pw;
	int		facility;

	if (config.localuser) {
		username = cxn->username;
	}
	else {
		username = config.user;
	}

	facility = LOG_CRIT;
	if ((pw = getpwnam(username)) != (struct passwd *)NULL) {
		if (!seteuid(pw->pw_uid)) {
			cxn->uid = getuid();
			cxn->euid = pw->pw_uid;
			return;
		}
		else {
			syslog(facility, "Unable to setuid to %s", username);
			exit_error(EX_NOPERM, "Unable to setuid to %s",
			    username);
		}
	}
	else {
		syslog(facility, "No such user %s", username);
		exit_error(EX_NOUSER, "No such user %s", username);
	}
}

#ifndef USE_PAM
static int
unix_pass(struct connection *cxn)
{
	struct passwd	*pw;
	if ((pw = getpwnam(cxn->username)) != (struct passwd *)NULL) {
		if (strcmp(pw->pw_passwd, crypt(cxn->password, pw->pw_passwd)))
			return(FALSE);
		return(TRUE);
	}
	return(FALSE);
}
#endif

static int
radius_pass(struct connection *cxn)
{
	struct rad_handle	*authreq;
	const void		*data;
	char			*p;
	int			i;
	size_t			len;

	authreq = rad_auth_open();
	for (i = 0; config.radius && config.radius[i].host; i++) {
		rad_add_server(authreq, config.radius[i].host,
		    config.radius[i].port, config.radius[i].secret,
		    config.radius[i].timeout, config.radius[i].tries);
	}
	rad_create_request(authreq, RAD_ACCESS_REQUEST);
	rad_put_int(authreq, RAD_NAS_PORT_TYPE, RAD_VIRTUAL);
	rad_put_string(authreq, RAD_USER_NAME, cxn->auth_string);
	rad_put_string(authreq, RAD_USER_PASSWORD, cxn->password);

	switch(rad_send_request(authreq)) {
		case RAD_ACCESS_ACCEPT:
			while ((i = rad_get_attr(authreq, &data, &len)) > 0) {
				switch(i) {
				    case RAD_LOGIN_IP_HOST:
					config.proxy_addr = rad_cvt_addr(data);
					break;
				    case RAD_LOGIN_TCP_PORT:
					config.proxy_port =
					    htons(rad_cvt_int(data));
					break;
				    case RAD_REPLY_MESSAGE:
					config.proxy_name = rad_cvt_string(data,
					    len);
					if ((p = strchr(config.proxy_name,
					    ':'))) {
						*p++ = '\0';
						config.proxy_passwd = p;
					}
					break;
				}
			}
			rad_close(authreq);
			return(TRUE);
		case RAD_ACCESS_REJECT:
		case RAD_ACCESS_CHALLENGE:
			/* Fall through */
		break;
	}
	rad_close(authreq);
	return(FALSE);
}

/*
 * Check the username and password against an entry
 * in a db file
 * 
 * Return true on success, false on failure, 
 */
static int
db_file_pass(struct connection *cxn)
{
	DB	*dp;
	DBT	key, data;
	char	*encpw;
	char	*encrypted_pass;
	char	*path;

	path = xmalloc(strlen(config.etcdir) + strlen(config.dbfile) + 5);
	sprintf(path, "%s/%s", config.etcdir, config.dbfile);
		
	if (!(dp = dbopen(path, O_RDONLY, S_IRUSR|S_IWUSR, DB_HASH, NULL))) {
		free(path);
		return(FALSE);
	}

	key.data = (u_char *)cxn->auth_string;
	key.size = strlen(key.data);

	if ((dp->get)(dp, &key, &data, 0)) {
		(dp->close)(dp);
		free(path);
		return(FALSE);	
	}

	encrypted_pass = (char*)data.data;
	encrypted_pass[data.size] = '\0';
	encpw = crypt(cxn->password, encrypted_pass);
	
	if (strcmp(encpw, encrypted_pass)) {
		(dp->close)(dp);
		free(path);
		return(FALSE);
	}
	
	(dp->close)(dp);
	free(path);
	return(TRUE);
}

#ifdef USE_PAM
static int
pam_pass(struct connection *cxn)
{
	struct pam_conv	 pamconv;
	pam_handle_t	*pamhandle;
	char		*data[2];
	int		 result;

	data[0] = strdup(cxn->auth_string);
	data[1] = strdup(cxn->password);

	pamconv.conv = &conversation;
	pamconv.appdata_ptr = data;

	if ((result = pam_start ("popd", NULL, &pamconv, &pamhandle)) !=
	    PAM_SUCCESS)
		return(FALSE);
	if ((result = pam_authenticate(pamhandle, 0)) != PAM_SUCCESS)
		return(FALSE);
	if ((result = pam_acct_mgmt(pamhandle, 0)) != PAM_SUCCESS)
		return(FALSE);
/*	if ((result = pam_setcred(pamhandle, PAM_ESTABLISH_CRED)) !=
 *	    PAM_SUCCESS)
 *		return(FALSE);
 *	if ((result = pam_open_session(pamhandle, 0)) != PAM_SUCCESS)
 *		return(FALSE);
 *	pam_end(pamhandle, pam_close_session(pamhandle, 0)); */
	pam_end(pamhandle, result);
	return TRUE;
}
#endif

/* 
 * Decide which method to use to authenticate the user,
 * perform the authentication and on success setuid to the user.
 *
 * This routine is the hook for authentication methods.
 * 
 * Return true on success and false on failure
 */
static int
checkpassword(struct connection *cxn)
{
	if (config.radius && radius_pass(cxn))
		return(TRUE);
	else if (config.virtual && db_file_pass(cxn))
		return(TRUE);
	else if (!config.radius && !config.virtual &&
#ifndef USE_PAM
	    unix_pass(cxn))
#else
	    pam_pass(cxn))
#endif
		return(TRUE);
	return(FALSE);
}

/*
 * Retrieve the secret for a given user.
 *
 * Returns the secret on success and false on failure to 
 * open the db file, * false if the key is not found 
 * in the * db file.  
 * 
 * Exits if memory can't be allocated for the path.
 */
static char
*getsecret(struct connection *cxn)
{
	DB	*dp;
	DBT	key, data;
	char	*secret;
	char	*path;

	path = xmalloc(strlen(config.etcdir) + strlen(config.secrets) + 5);
	sprintf(path, "%s/%s", config.etcdir, config.secrets);

	if (!(dp = dbopen(path, O_RDONLY, S_IRUSR|S_IWUSR, DB_HASH, NULL))) {
		free(path);
		return(NULL);
	}

	key.data = (u_char *)cxn->auth_string;
	key.size = strlen(key.data);

	if ((dp->get)(dp, &key, &data, 0)) {
		(dp->close)(dp);
		free(path);
		return(NULL);	
	}

	secret = xcalloc(data.size + 1, 1);
	strncpy(secret, (char*)data.data, data.size);
	(dp->close)(dp);
	free(path);
	return(secret);
}

/*
 * Check the md5 challenge. 
 *
 * Return true if the challenge is valid,
 * False on falure.
 * 
 * Exit if memory can't be allocated for challenge string
 */
static int
checkchallenge(struct connection *cxn, char *ch)
{
	char		*secret, *string;
	MD5_CTX		context;
	unsigned char	digest[16];

	if (!(secret = getsecret(cxn)))
		return(FALSE);
	if (!*secret) {
		free(secret);
		return(FALSE);
	}
	string = xmalloc(strlen(config.timestamp) + strlen(secret) + 1);
	strcpy(string, config.timestamp);
	strcat(string, secret);
	MD5Init(&context);
	MD5Update(&context, (unsigned char *)string, strlen(string));
	MD5Final(digest, &context);
	if (!strcmp(binhex(digest, sizeof(digest)), ch)) {
		free(string);
		free(secret);
		dosetuid(cxn);
		return(TRUE);
	}
	free(string);
	free(secret);
	return(FALSE);
}

/*
 * Do the authentication
 *
 * Commands that we deal with here are:
 * 		auth
 * 		apop
 * 		user
 * 		pass
 * 		quit
 * 	
 * Loop continually unitil we have enough information.
 * Exit on quit and time out.
 */
int
authenticate(struct connection *cxn)
{
	char	*arg1, *arg2;
	int	counter = 0;

	for (;;) {
		switch (recvcmd(&arg1, &arg2)) {
			case AUTH:
				message(CMDINVAL);
				break;
			case APOP:
			    if (!config.secrets) {
			    	message(CMDISABLE);
			    	break;
			    }
			    if (arg1 && arg2) {
			    	cxndetails(cxn, arg1, config.defaultrealm,
				    config.maildir, config.bulletindir,
				    config.virtual, config.hashdepth);
			    	if (checkchallenge(cxn, arg2))
			    		return(TRUE);
			    	else
			    		freeconnection(cxn);
			    	sleep(++counter);
			    	if (counter == 4)
			    		return FALSE;
			    	message(CHALLENGEINVAL);
			    	break;
			    }
			    message(TOOFEWARGUMENTS);
			    break;
			case USER:
			    if (cxn->auth_string) {
			    	sendline(SEND_FLUSH, "-ERR Already selected %s",
			    		cxn->auth_string);
			    	break;
			    }
			    if (arg1) {
			    	if (cxndetails(cxn, arg1, config.defaultrealm,
				    config.maildir, config.bulletindir,
				    config.virtual, config.hashdepth))
					sendline(SEND_FLUSH, "+OK enter "
					    "password for %s",
			    		    cxn->auth_string);
			    	break;
			    }
			    message(NEEDUSERNAME);
			    break;
			case PASS:
			    if (arg1 && cxn->auth_string) {
			        cxn->password = xmalloc(strlen(arg1) + 1);
			        strcpy(cxn->password, arg1);
			 		if (!config.pwcheck) {
					dosetuid(cxn);
					return(TRUE);
			        }
			    	if (checkpassword(cxn)) {
					dosetuid(cxn);
			    		return(TRUE);
			    	}
				else
					freeconnection(cxn);
			    	sleep(++counter);
			    	if (counter == 4) {
			    		freeconnection(cxn);
			    		return FALSE;
				}
			    	message(BADPASSWORD);
			    	break;
			    }
			    else if (cxn->auth_string == NULL)
				    message(NEEDUSERNAME);
			    else if (arg1 == NULL)
			    	message(NEEDPASSWORD);
			    break;
			case QUIT:
			    return(QUIT);
			    break;
			case TIMEDOUT:
			    exit_error(EX_NOINPUT,
			        "timed out waiting for input");
			case INVALCMD:
			default:
			    message(CMDINVAL);
		}
	}
}


syntax highlighted by Code2HTML, v. 0.9.1