/* 
 * $Id: passwd.c,v 1.8 1997/06/24 14:17:36 morgan Exp morgan $
 *
 * passwd.c
 *
 * Written for Linux-PAM by Andrew G. Morgan <morgan@parc.power.net>
 *
 * $Log: passwd.c,v $
 * Revision 1.8  1997/06/24 14:17:36  morgan
 * update for .55 release
 *
 * Revision 1.7  1997/02/24 05:59:09  morgan
 * delay on failure if available
 *
 * Revision 1.6  1997/01/29 03:35:15  morgan
 * update for release
 *
 * Revision 1.5  1996/11/10 00:07:24  morgan
 * little has changed, I've just changed my C style from 5 to 4 spaces
 */

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <security/pam_appl.h>
#include <security/pam_misc.h>

#ifdef HAVE_PWDB
#include <pwdb/pwdb_public.h>
#else
#include <pwd.h>
#endif

/* ------ some static data objects ------- */

static struct pam_conv conv = {
    misc_conv,
    NULL
};

#define PASSWD_MAX_TOKEN                100
#define PASSWD_COMMENT_CHAR             '#'
#define PASSWD_SUFFIX_FILE              "/etc/security/passwd.conf"
#define PASSWD_SERVICE_FORMAT           "passwd%s"
#define PASSWD_SERVICE_DEFAULT          "passwd"
#define PASSWD_FAIL_DELAY               2000000 /* usec delay on failure */
#define PASSWD_KEEP_UPTODATE            01
#define PASSWD_SERVICE_SUFFIX           02
#define PASSWD_GARBLED                  04

#define PASSWD_TRUE                     1
#define PASSWD_FALSE                    0

static int parse_args(int argc, const char **argv
		      , const char **suffix, const char **user)
{
    int passwd_flags = 0;

    *suffix = *user = NULL;

    while (--argc > 0) {
	if ( (*++argv)[0] == '-' ) {
	    switch ((*argv)[1]) {
	    case 'k':
		passwd_flags |= PASSWD_KEEP_UPTODATE;
		break;
	    case 'N':
		passwd_flags |= PASSWD_SERVICE_SUFFIX;
		*suffix = 2+*argv;
		if (*suffix[0] || (--argc > 0 && (*suffix = *++argv))) {
		    break;
		}
		*suffix = NULL;
		passwd_flags |= PASSWD_GARBLED;
		break;
	    default:
		fprintf(stderr,"passwd: unrecognized request; %s\n", argv[0]);
		passwd_flags |= PASSWD_GARBLED;
	    }
	} else {
	    if (getuid() == 0) {
		*user = *argv;
	    } else {
		fprintf(stderr, "passwd: only superuser can give username\n");
		passwd_flags |= PASSWD_GARBLED;
	    }
	}
    }

    if ((passwd_flags & PASSWD_GARBLED)) {
	fprintf(stderr, "usage: passwd [-k] [-N\"name\"] [\"username\"]\n"
		"\t-k          - keep non-expired authentication tokens\n"
		"\t-N\"name\"    - add suffix \"name\" to PAM service name\n"
		"\t\"username\"  "
		            "- (superuser may) update tokens for named user\n"
	    );
	exit(1);
    }

    return passwd_flags;
}

static int get_token(FILE *fin, char *buffer)
{
    static int eof=PASSWD_FALSE;
    static int incomment=PASSWD_FALSE;
    int c;

    while (!eof) {
	while ((c = getc(fin)) != EOF && isspace(c)) {
	    if (c == '\n')
		incomment = PASSWD_FALSE;
	}

	if (c == EOF) {
	    eof = PASSWD_TRUE;
	} else if ( incomment || c == PASSWD_COMMENT_CHAR ) {

	    /* c indicates a comment.. */
	    while ((c = getc(fin)) != EOF && c != '\n');
	    incomment = PASSWD_FALSE;
	    if (c == EOF)
		eof = PASSWD_TRUE;

	} else {                                      /* this is a token */
	    int i=0;

	    while (i<PASSWD_MAX_TOKEN-1 && c != EOF
		   && !isspace(c) && c != PASSWD_COMMENT_CHAR) {
		buffer[i++] = c;
		c = getc(fin);
	    }
	    buffer[i] = '\0';                         /* <NUL> terminate */
	    D(("read: [%s]\n", buffer));
	    if (c == EOF)
		eof = PASSWD_TRUE;
	    else if (c == PASSWD_COMMENT_CHAR) {
		incomment = PASSWD_TRUE;
	    }
	    return PASSWD_TRUE;
	}
    }

    return PASSWD_FALSE;
}

static int service_ok(const char *suffix)
{
    char buffer[PASSWD_MAX_TOKEN];
    FILE *fin;
    int retval = PASSWD_FALSE;

    fin = fopen(PASSWD_SUFFIX_FILE, "r");
    if (fin == NULL) {
	return PASSWD_FALSE;
    }

    while (get_token(fin, buffer)) {
	if (!strcmp(suffix, buffer)) {
	    D(("a match for suffix=%s!\n", suffix));
	    retval = PASSWD_TRUE;
	}
    }
    fclose(fin);

    return retval;
}

/* ------- the application itself -------- */

void main(int argc, const char **argv)
{
    const char *service, *user, *suffix;
    int passwd_flags, retval;
    pam_handle_t *pamh=NULL;

    /* obtain user's specific request */

    passwd_flags = parse_args(argc, argv, &suffix, &user);

    /* obtain the correct name for the service - check suffix is in list */

    if (suffix == NULL || !(passwd_flags & PASSWD_SERVICE_SUFFIX) ) {
	service = PASSWD_SERVICE_DEFAULT;
    } else if (service_ok(suffix)) {
	char *tmp;

	tmp = malloc(sizeof(PASSWD_SERVICE_FORMAT) + strlen(suffix));
	if (tmp == NULL) {
	    fprintf(stderr, "passwd: no memory for service name\n");
	    exit(1);
	}
	sprintf(tmp, PASSWD_SERVICE_FORMAT, suffix);
	service = tmp;
    } else {
	fprintf(stderr, "passwd: invalid scheme-suffix \"%s\"\n", suffix);
	exit(1);
    }

    if (user == NULL) {
#ifndef HAVE_PWDB
	struct passwd *pwent;
#endif /* HAVE_PWDB */
	if ((user = getlogin()) == NULL) {
	    fprintf(stderr, "passwd: cannot retrieve user's name\n");
	    exit(1);
	}
#ifndef HAVE_PWDB
	/* attempt to patch over libc's inability to handle longer that
	   fixed length login names from the utmp file */

	if ((pwent = getpwnam(user)) == NULL
	    || (pwent = getpwuid(getuid())) == NULL
	    || !(pwent->pw_name
		 && !strncmp(pwent->pw_name, user, strlen(user)))) {
	    fprintf(stderr, "passwd: cannot retrieve user's name\n");
	    exit(1);
	} else {
	    user = pwent->pw_name;
	}
#endif /* HAVE_PWDB */
    }

    /* here we know whose passwords are to be changed and whether
       we'll change everything or just the expired ones */

    D(("service=%s, user=%s\n", service, user));
    retval = pam_start(service, user, &conv, &pamh);
    user = NULL;                         /* can no longer rely on this */

#ifdef HAVE_PAM_FAIL_DELAY
    /* have to pause on failure. At least this long (doubles..) */
    retval = pam_fail_delay(pamh, PASSWD_FAIL_DELAY);
    if (retval != PAM_SUCCESS) {
	fprintf(stderr, "passwd: unable to set failure delay\n");
	exit(1);
    }
#endif /* HAVE_PAM_FAIL_DELAY */

    while (retval == PAM_SUCCESS) {      /* use loop to avoid goto... */

	/* the user is authenticated by the passwd module; change
	   the password(s) too. */

	retval = pam_chauthtok(pamh, (passwd_flags & PASSWD_KEEP_UPTODATE)
			       ? PAM_CHANGE_EXPIRED_AUTHTOK : 0 );
	if (retval != PAM_SUCCESS)
	    break;

	/* all done */

	retval = pam_end(pamh, PAM_SUCCESS);
	if (retval != PAM_SUCCESS)
	    break;

	/* quit gracefully */

	fprintf(stderr,
		"passwd: %s authentication tokens updated successfully\n"
		, (passwd_flags & PASSWD_KEEP_UPTODATE) ? "expired":"all" );

	exit(0);
    }

    if (pamh != NULL) {
	(void) pam_end(pamh,PAM_SUCCESS);
	pamh = NULL;
    }

    if (retval != PAM_SUCCESS)
	fprintf(stderr, "passwd: %s\n", pam_strerror(pamh, retval));

    exit(1);
}
