root/usr/src/lib/passwdutil/files_attr.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 * Copyright (c) 2016 by Delphix. All rights reserved.
 * Copyright 2024 OmniOS Community Edition (OmniOSce) Association.
 */

#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <pwd.h>
#include <shadow.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <unistd.h>
#include <nss_dbdefs.h>
#include <macros.h>
#include <syslog.h>

#include <limits.h>             /* LOGNAME_MAX -- max Solaris user name */

#include "passwdutil.h"

int files_lock(void);
int files_unlock(void);
int files_checkhistory(const char *user, const char *passwd,
    pwu_repository_t *rep);
int files_getattr(const char *name, attrlist *item, pwu_repository_t *rep);
int files_getpwnam(const char *name, attrlist *items, pwu_repository_t *rep,
    void **buf);
int files_update(attrlist *items, pwu_repository_t *rep, void *buf);
int files_putpwnam(const char *name, const char *oldpw, pwu_repository_t *rep,
    void *buf);
int files_user_to_authenticate(const char *name, pwu_repository_t *rep,
        char **auth_user, int *privileged);

static int files_update_history(const char *name, struct spwd *spwd);

/*
 * files function pointer table, used by passwdutil_init to initialize
 * the global Repository-OPerations table "rops"
 */
struct repops files_repops = {
        files_checkhistory,
        files_getattr,
        files_getpwnam,
        files_update,
        files_putpwnam,
        files_user_to_authenticate,
        files_lock,
        files_unlock
};

/*
 * this structure defines the buffer used to keep state between
 * get/update/put calls
 */
struct pwbuf {
        int     update_history;
        struct passwd *pwd;
        char   *pwd_scratch;
        struct spwd *spwd;
        char   *spwd_scratch;
        char   *new_sp_pwdp;
};

/*
 * We should use sysconf, but there is no sysconf name for SHADOW
 * so we use these from nss_dbdefs
 */
#define PWD_SCRATCH_SIZE NSS_LINELEN_PASSWD
#define SPW_SCRATCH_SIZE NSS_LINELEN_SHADOW

/*
 * lock functions for files repository
 */
int
files_lock(void)
{
        int res;

        if (lckpwdf()) {
                switch (errno) {
                case EINTR:
                        res = PWU_BUSY;
                        break;
                case EACCES:
                        res = PWU_DENIED;
                        break;
                case 0:
                        res = PWU_SUCCESS;
                        break;
                }
        } else
                res = PWU_SUCCESS;

        return (res);
}

int
files_unlock(void)
{
        if (ulckpwdf())
                return (PWU_SYSTEM_ERROR);

        return (PWU_SUCCESS);
}

/*
 * files_privileged
 *
 * Are we a privileged user with regard to the files repository?
 */
int
files_privileged(void)
{
        return (getuid() == 0);
}

/*
 *
 * private_getpwnam_r()
 *
 * A private implementation of getpwnam_r which does *not* fall back to
 * other services possibly defined in nsswitch.conf
 *
 * behaves like getpwnam_r().
 */
struct passwd *
private_getpwnam_r(const char *name, struct passwd *result, char *buffer,
    int buflen)
{
        FILE *fp;
        int found;

        if ((fp = fopen(PASSWD, "rF")) == NULL)
                return (NULL);

        found = 0;
        while (!found && fgetpwent_r(fp, result, buffer, buflen) != NULL) {
                if (strcmp(name, result->pw_name) == 0)
                        found = 1;
        }

        (void) fclose(fp);

        if (!found) {
                (void) memset(buffer, 0, buflen);
                (void) memset(result, 0, sizeof (*result));
                return (NULL);
        }

        return (result);
}

/*
 * private_getspnam_r()
 *
 * A private implementation of getspnam_r which does *not* fall back to
 * other services possibly defined in nsswitch.conf.
 *
 * Behaves like getspnam_r(). Since we use fgetspent_t(), all numeric
 * fields that are undefined in /etc/shadow will be set to -1.
 *
 */
struct spwd *
private_getspnam_r(const char *name, struct spwd *result, char *buffer,
    int buflen)
{
        FILE *fp;
        int found;

        fp = fopen(SHADOW, "rF");
        if (fp == NULL)
                return (NULL);

        found = 0;
        while (!found && fgetspent_r(fp, result, buffer, buflen) != NULL) {
                if (strcmp(name, result->sp_namp) == 0)
                        found = 1;
        }

        (void) fclose(fp);

        if (!found) {
                (void) memset(buffer, 0, buflen);
                (void) memset(result, 0, sizeof (*result));
                return (NULL);
        }
        return (result);
}

/*
 * files_getpwnam(name, items, rep, buf)
 *
 */
/*ARGSUSED*/
int
files_getpwnam(const char *name, attrlist *items, pwu_repository_t *rep,
    void **buf)
{
        attrlist *p;
        struct pwbuf *pwbuf;
        int err = PWU_SUCCESS;

        *buf = calloc(1, sizeof (struct pwbuf));
        pwbuf = (struct pwbuf *)*buf;
        if (pwbuf == NULL)
                return (PWU_NOMEM);

        /*
         * determine which password structure (/etc/passwd or /etc/shadow)
         * we need for the items we need to update
         */
        for (p = items; p != NULL; p = p->next) {
                switch (p->type) {
                case ATTR_NAME:
                case ATTR_UID:
                case ATTR_GID:
                case ATTR_AGE:
                case ATTR_COMMENT:
                case ATTR_GECOS:
                case ATTR_HOMEDIR:
                case ATTR_SHELL:
                        if (pwbuf->pwd == NULL) {
                                pwbuf->pwd = malloc(sizeof (struct passwd));
                                if (pwbuf->pwd == NULL) {
                                        err = PWU_NOMEM;
                                        goto error;
                                }
                        }
                        break;
                case ATTR_PASSWD:
                case ATTR_PASSWD_SERVER_POLICY:
                case ATTR_LSTCHG:
                case ATTR_MIN:
                case ATTR_MAX:
                case ATTR_WARN:
                case ATTR_INACT:
                case ATTR_EXPIRE:
                case ATTR_FLAG:
                case ATTR_LOCK_ACCOUNT:
                case ATTR_EXPIRE_PASSWORD:
                case ATTR_FAILED_LOGINS:
                case ATTR_INCR_FAILED_LOGINS:
                case ATTR_RST_FAILED_LOGINS:
                case ATTR_NOLOGIN_ACCOUNT:
                case ATTR_UNLOCK_ACCOUNT:
                        if (pwbuf->spwd == NULL) {
                                pwbuf->spwd = malloc(sizeof (struct spwd));
                                if (pwbuf->spwd == NULL) {
                                        err = PWU_NOMEM;
                                        goto error;
                                }
                        }
                        break;
                default:
                        /*
                         * Some other repository might have different values
                         * so we ignore those.
                         */
                        break;
                }
        }

        if (pwbuf->pwd) {
                if ((pwbuf->pwd_scratch = malloc(PWD_SCRATCH_SIZE)) == NULL) {
                        err = PWU_NOMEM;
                        goto error;
                }
                if (private_getpwnam_r(name, pwbuf->pwd, pwbuf->pwd_scratch,
                    PWD_SCRATCH_SIZE) == NULL) {
                        err = PWU_NOT_FOUND;
                        goto error;
                }
        }

        if (pwbuf->spwd) {
                if ((pwbuf->spwd_scratch = malloc(SPW_SCRATCH_SIZE)) == NULL) {
                        err = PWU_NOMEM;
                        goto error;
                }
                if (private_getspnam_r(name, pwbuf->spwd, pwbuf->spwd_scratch,
                    SPW_SCRATCH_SIZE) == NULL) {
                        err = PWU_NOT_FOUND;
                        goto error;
                }
        }

        return (PWU_SUCCESS);
error:
        if (pwbuf->pwd) free(pwbuf->pwd);
        if (pwbuf->pwd_scratch) free(pwbuf->pwd_scratch);
        if (pwbuf->spwd) free(pwbuf->spwd);
        if (pwbuf->spwd_scratch) free(pwbuf->spwd_scratch);
        free(pwbuf);
        *buf = NULL;

        return (err);
}

/*
 * int files_user_to_authenticate(name, rep, auth_user, privileged)
 * Determine which user needs to be authenticated. For files, the
 * possible return values are:
 *      PWU_NOT_FOUND
 *      PWU_SUCCESS     and (auth_user == NULL || auth_user = user)
 *      PWU_DENIED
 *      PWU_NOMEM
 */
/*ARGSUSED*/
int
files_user_to_authenticate(const char *user, pwu_repository_t *rep,
        char **auth_user, int *privileged)
{
        struct pwbuf *pwbuf;
        int res;
        attrlist attr_tmp[1] = { { ATTR_UID, NULL, NULL } };

        /* check to see if target user is present in files */
        res = files_getpwnam(user, &attr_tmp[0], rep, (void **)&pwbuf);
        if (res != PWU_SUCCESS)
                return (res);

        if (files_privileged()) {
                *auth_user = NULL;
                *privileged = 1;
                res = PWU_SUCCESS;
        } else {
                *privileged = 0;
                if (getuid() == pwbuf->pwd->pw_uid) {
                        if ((*auth_user = strdup(user)) == NULL) {
                                res = PWU_NOMEM;
                        } else {
                                res = PWU_SUCCESS;
                        }
                } else {
                        res = PWU_DENIED;
                }
        }

        if (pwbuf->pwd) free(pwbuf->pwd);
        if (pwbuf->pwd_scratch) free(pwbuf->pwd_scratch);
        if (pwbuf->spwd) free(pwbuf->spwd);
        if (pwbuf->spwd_scratch) free(pwbuf->spwd_scratch);
        free(pwbuf);

        return (res);
}

/*
 *      Password history file format:
 *              user:crypw1: ... crypwn: such that n <= MAXHISTORY
 */
#define HISTORY         "/etc/security/passhistory"
#define HISTEMP         "/etc/security/pwhistemp"
#define OHISTORY        "/etc/security/opwhistory"
#define HISTMODE        S_IRUSR /* mode to create history file */
/*
 * XXX
 *      3*LOGNAME_MAX just in case there are long user names.
 *      Traditionally Solaris LOGNAME_MAX (_POSIX_LOGIN_NAME_MAX) is 13,
 *      but some sites often user more.
 *      If LOGNAME_MAX ever becomes reasonable (128) and actually enforced,
 *      fix up here.
 * XXX
 */
#define MAX_LOGNAME (3 * LOGNAME_MAX)

/*
 *      files_checkhistory - check if a user's new password is in the user's
 *              old password history.
 *
 *      Entry
 *              user = username.
 *              passwd = new clear text password.
 *
 *      Exit
 *              PWU_SUCCESS, passwd found in user's old password history.
 *                      The caller should only be interested and fail if
 *                      PWU_SUCCESS is returned.
 *              PWU_NOT_FOUND, passwd not in user's old password history.
 *              PWU_errors, PWU_ errors from other routines.
 *
 */
int
files_checkhistory(const char *user, const char *passwd, pwu_repository_t *rep)
{
        attrlist attr;
        int res;

        attr.type = ATTR_HISTORY;
        attr.data.val_s = NULL;
        attr.next = NULL;

        debug("files_checkhistory(user=%s)", user);

        /*
         * XXX
         *      This depends on the underlying files_getattr implementation
         *      treating user not found in backing store or no history as
         *      an error.
         * XXX
         */

        if ((res = files_getattr(user, &attr, rep)) == PWU_SUCCESS) {
                char    *s;
                char    *crypt_passwd;
                int     histsize;
                char    *last = attr.data.val_s;

                if ((histsize = def_getint("HISTORY=", DEFHISTORY)) == 0) {
                        debug("files_checkhistory: no history requested");
                        res = PWU_NOT_FOUND;
                        goto out;
                }

                debug("files_checkhistory: histsize = %d", histsize);
                if (histsize > MAXHISTORY)
                        histsize = MAXHISTORY;

                debug("line to test\n\t%s", last);

                /* compare crypt_passwd to attr.data.val_s strings. */
                res = PWU_NOT_FOUND;
                while ((histsize-- > 0) &&
                    (((s = strtok_r(NULL, ":", &last)) != NULL) &&
                    (*s != '\n'))) {

                        crypt_passwd = crypt(passwd, s);
                        debug("files_checkhistory: user_pw=%s, history_pw=%s",
                            crypt_passwd, s);
                        if (strcmp(crypt_passwd, s) == 0) {
                                res = PWU_SUCCESS;
                                break;
                        }
                }
                debug("files_checkhistory(%s, %s) = %d", user, crypt_passwd,
                    res);
        }
out:
        if (attr.data.val_s != NULL)
                free(attr.data.val_s);

        return (res);
}

/*
 * files_getattr(name, items, rep)
 *
 * Get attributes specified in list 'items'
 */
int
files_getattr(const char *name, attrlist *items, pwu_repository_t *rep)
{
        struct pwbuf *pwbuf;
        struct passwd *pw;
        struct spwd *spw;
        attrlist *w;
        int res;

        res = files_getpwnam(name, items, rep, (void **)&pwbuf);
        if (res != PWU_SUCCESS)
                return (res);

        pw = pwbuf->pwd;
        spw = pwbuf->spwd;

        for (w = items; res == PWU_SUCCESS && w != NULL; w = w->next) {
                switch (w->type) {
                case ATTR_NAME:
                        if ((w->data.val_s = strdup(pw->pw_name)) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_COMMENT:
                        if ((w->data.val_s = strdup(pw->pw_comment)) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_GECOS:
                        if ((w->data.val_s = strdup(pw->pw_gecos)) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_HOMEDIR:
                        if ((w->data.val_s = strdup(pw->pw_dir)) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_SHELL:
                        if ((w->data.val_s = strdup(pw->pw_shell)) == NULL)
                                res = PWU_NOMEM;
                        break;
                /*
                 * Nothing special needs to be done for
                 * server policy
                 */
                case ATTR_PASSWD:
                case ATTR_PASSWD_SERVER_POLICY:
                        if ((w->data.val_s = strdup(spw->sp_pwdp)) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_AGE:
                        if ((w->data.val_s = strdup(pw->pw_age)) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_REP_NAME:
                        if ((w->data.val_s = strdup("files")) == NULL)
                                res = PWU_NOMEM;
                        break;
                case ATTR_HISTORY: {
                        FILE    *history;
                        char    buf[MAX_LOGNAME + MAXHISTORY +
                            (MAXHISTORY * CRYPT_MAXCIPHERTEXTLEN)+1];
                        char    *s, *s1;

                        debug("files_getattr: Get password history for %s ",
                            name);

                        if ((history = fopen(HISTORY, "rF")) == NULL) {
                                debug("files_getattr: %s not found", HISTORY);
                                res = PWU_OPEN_FAILED;
                                goto getattr_exit;
                        }
                        res = PWU_NOT_FOUND;
                        while ((s = fgets(buf, sizeof (buf), history)) !=
                            NULL) {
                                s1 = strchr(s, ':');
                                if (s1 != NULL) {
                                        *s1 = '\0';
                                } else {
                                        res = PWU_NOT_FOUND;
                                        break;
                                }
#ifdef  DEBUG
                                debug("got history line for %s", s);
#endif  /* DEBUG */
                                if (strcmp(s, name) == 0) {
                                        /* found user */
                                        if ((items->data.val_s =
                                            strdup(s1+1)) == NULL)
                                                res = PWU_NOMEM;
                                        else
                                                res = PWU_SUCCESS;
                                        break;
                                }
                        }
                        (void) fclose(history);
                        break;
                }

                /* integer values */
                case ATTR_UID:
                        w->data.val_i = pw->pw_uid;
                        break;
                case ATTR_GID:
                        w->data.val_i = pw->pw_gid;
                        break;
                case ATTR_LSTCHG:
                        w->data.val_i = spw->sp_lstchg;
                        break;
                case ATTR_MIN:
                        w->data.val_i = spw->sp_min;
                        break;
                case ATTR_MAX:
                        w->data.val_i = spw->sp_max;
                        break;
                case ATTR_WARN:
                        w->data.val_i = spw->sp_warn;
                        break;
                case ATTR_INACT:
                        w->data.val_i = spw->sp_inact;
                        break;
                case ATTR_EXPIRE:
                        w->data.val_i = spw->sp_expire;
                        break;
                case ATTR_FLAG:
                        w->data.val_i = spw->sp_flag;
                        break;
                case ATTR_FAILED_LOGINS:
                        w->data.val_i = spw->sp_flag & FAILCOUNT_MASK;
                        break;
                default:
                        break;
                }
        }

getattr_exit:
        if (pwbuf->pwd) free(pwbuf->pwd);
        if (pwbuf->pwd_scratch) free(pwbuf->pwd_scratch);
        if (pwbuf->spwd) free(pwbuf->spwd);
        if (pwbuf->spwd_scratch) free(pwbuf->spwd_scratch);
        free(pwbuf);

        return (res);
}

/*
 * max_present(list)
 *
 * see if attribute ATTR_MAX, with value != -1, is present in
 * attribute-list "list".
 *
 * returns 1 if present, 0 otherwise.
 */
static int
max_present(attrlist *list)
{
        while (list != NULL)
                if (list->type == ATTR_MAX && list->data.val_i != -1)
                        return (1);
                else
                        list = list->next;

        return (0);
}

/*
 * files_update(items, rep, buf)
 *
 * update the information in buf with the attributes specified in
 * items.
 */
/*ARGSUSED*/
int
files_update(attrlist *items, pwu_repository_t *rep, void *buf)
{
        struct pwbuf *pwbuf = (struct pwbuf *)buf;
        struct passwd *pw;
        struct spwd *spw;
        attrlist *p;
        int aging_needed = 0;
        int aging_set = 0;
        int disable_aging;
        char *pword;
        int len;

        pw = pwbuf->pwd;
        spw = pwbuf->spwd;
        pwbuf->update_history = 0;

        /*
         * if sp_max==0 : disable passwd aging after updating the password
         */
        disable_aging = (spw != NULL && spw->sp_max == 0);

        for (p = items; p != NULL; p = p->next) {
                switch (p->type) {
                case ATTR_NAME:
                        break;  /* We are able to handle this, but... */
                case ATTR_UID:
                        pw->pw_uid = (uid_t)p->data.val_i;
                        break;
                case ATTR_GID:
                        pw->pw_gid = (gid_t)p->data.val_i;
                        break;
                case ATTR_AGE:
                        pw->pw_age = p->data.val_s;
                        break;
                case ATTR_COMMENT:
                        pw->pw_comment = p->data.val_s;
                        break;
                case ATTR_GECOS:
                        pw->pw_gecos = p->data.val_s;
                        break;
                case ATTR_HOMEDIR:
                        pw->pw_dir = p->data.val_s;
                        break;
                case ATTR_SHELL:
                        pw->pw_shell = p->data.val_s;
                        break;

                /*
                 * Nothing special needs to be done for
                 * server policy
                 */
                case ATTR_PASSWD:
                case ATTR_PASSWD_SERVER_POLICY:
                        /*
                         * There is a special case only for files: if the
                         * password is to be deleted (-d to passwd),
                         * p->data.val_s will be NULL.
                         */
                        if (p->data.val_s == NULL) {
                                spw->sp_pwdp = "";
                        } else {
                                char *salt = NULL;
                                char *hash = NULL;

                                salt = crypt_gensalt(spw->sp_pwdp, pw);

                                if (salt == NULL) {
                                        if (errno == ENOMEM)
                                                return (PWU_NOMEM);
                                        /* algorithm problem? */
                                        syslog(LOG_AUTH | LOG_ALERT,
                                            "passwdutil: crypt_gensalt %m");
                                        return (PWU_UPDATE_FAILED);
                                }
                                hash = crypt(p->data.val_s, salt);
                                free(salt);
                                if (hash == NULL) {
                                        errno = ENOMEM;
                                        return (PWU_NOMEM);
                                }
                                pword = strdup(hash);
                                if (pword == NULL) {
                                        errno = ENOMEM;
                                        return (PWU_NOMEM);
                                }

                                if (pwbuf->new_sp_pwdp)
                                        free(pwbuf->new_sp_pwdp);
                                pwbuf->new_sp_pwdp = pword;
                                spw->sp_pwdp = pword;
                                aging_needed = 1;
                                pwbuf->update_history = 1;
                        }
                        spw->sp_flag &= ~FAILCOUNT_MASK; /* reset count */
                        spw->sp_lstchg = DAY_NOW_32;
                        break;
                case ATTR_LOCK_ACCOUNT:
                        if (spw->sp_pwdp == NULL) {
                                spw->sp_pwdp = LOCKSTRING;
                        } else if ((strncmp(spw->sp_pwdp, LOCKSTRING,
                            sizeof (LOCKSTRING)-1) != 0) &&
                            (strcmp(spw->sp_pwdp, NOLOGINSTRING) != 0)) {
                                len = sizeof (LOCKSTRING)-1 +
                                    strlen(spw->sp_pwdp) + 1;
                                pword = malloc(len);
                                if (pword == NULL) {
                                        errno = ENOMEM;
                                        return (PWU_NOMEM);
                                }
                                (void) strlcpy(pword, LOCKSTRING, len);
                                (void) strlcat(pword, spw->sp_pwdp, len);
                                if (pwbuf->new_sp_pwdp)
                                        free(pwbuf->new_sp_pwdp);
                                pwbuf->new_sp_pwdp = pword;
                                spw->sp_pwdp = pword;
                        }
                        spw->sp_lstchg = DAY_NOW_32;
                        break;
                case ATTR_UNLOCK_ACCOUNT:
                        if (spw->sp_pwdp != NULL &&
                            strncmp(spw->sp_pwdp, LOCKSTRING,
                            sizeof (LOCKSTRING)-1) == 0) {
                                (void) strcpy(spw->sp_pwdp, spw->sp_pwdp +
                                    sizeof (LOCKSTRING)-1);
                        }
                        spw->sp_lstchg = DAY_NOW_32;
                        break;
                case ATTR_NOLOGIN_ACCOUNT:
                        spw->sp_pwdp = NOLOGINSTRING;
                        if (pwbuf->new_sp_pwdp) {
                                free(pwbuf->new_sp_pwdp);
                                pwbuf->new_sp_pwdp = NULL;
                        }
                        spw->sp_lstchg = DAY_NOW_32;
                        break;
                case ATTR_EXPIRE_PASSWORD:
                        spw->sp_lstchg = 0;
                        break;
                case ATTR_LSTCHG:
                        spw->sp_lstchg = p->data.val_i;
                        break;
                case ATTR_MIN:
                        if (spw->sp_max == -1 &&
                            p->data.val_i != -1 && max_present(p->next) == 0)
                                return (PWU_AGING_DISABLED);
                        spw->sp_min = p->data.val_i;
                        aging_set = 1;
                        break;
                case ATTR_MAX:
                        if (p->data.val_i == -1) {
                                /* Turn aging off -> Reset min and warn too */

                                spw->sp_min = -1;
                                spw->sp_warn = -1;
                        } else {
                                /* Turn aging on */

                                if (spw->sp_min == -1) {
                                        /*
                                         * If minage has not been set with
                                         * a command-line option, we set it
                                         * to zero.
                                         */
                                        spw->sp_min = 0;
                                }

                                /*
                                 * If aging was turned off, we update lstchg.
                                 *
                                 * We take care not to update lstchg if the
                                 * user has no password, otherwise the user
                                 * might not be required to provide a password
                                 * the next time they log-in.
                                 *
                                 * Also, if lstchg != -1 (i.e., not set in
                                 * /etc/shadow), we keep the old value.
                                 */
                                if (spw->sp_max == -1 &&
                                    spw->sp_pwdp != NULL && *spw->sp_pwdp &&
                                    spw->sp_lstchg == -1) {
                                        spw->sp_lstchg = DAY_NOW_32;
                                }
                        }

                        spw->sp_max = p->data.val_i;

                        aging_set = 1;

                        break;
                case ATTR_WARN:
                        if (spw->sp_max == -1 && p->data.val_i != -1 &&
                            max_present(p->next) == 0)
                                return (PWU_AGING_DISABLED);
                        spw->sp_warn =  p->data.val_i;
                        break;
                case ATTR_INACT:
                        spw->sp_inact = p->data.val_i;
                        break;
                case ATTR_EXPIRE:
                        spw->sp_expire = p->data.val_i;
                        break;
                case ATTR_FLAG:
                        spw->sp_flag = p->data.val_i;
                        break;
                case ATTR_INCR_FAILED_LOGINS:
                        {
                        int count = (spw->sp_flag & FAILCOUNT_MASK) + 1;
                        spw->sp_flag &= ~FAILCOUNT_MASK;
                        spw->sp_flag |= min(FAILCOUNT_MASK, count);
                        p->data.val_i = count;
                        }
                        break;
                case ATTR_RST_FAILED_LOGINS:
                        p->data.val_i = spw->sp_flag & FAILCOUNT_MASK;
                        spw->sp_flag &= ~FAILCOUNT_MASK;
                        break;
                default:
                        break;
                }
        }

        /*
         * What should the new aging values look like?
         *
         * There are a number of different conditions
         *
         *  a) aging is already configured: don't touch it
         *
         *  b) disable_aging is set: disable aging
         *
         *  c) aging is not configured: turn on default aging;
         *
         *  b) and c) of course only if aging_needed and !aging_set.
         *  (i.e., password changed, and aging values not changed)
         */

        if (spw != NULL && spw->sp_max <= 0) {
                /* a) aging not yet configured */
                if (aging_needed && !aging_set) {
                        if (disable_aging) {
                                /* b) turn off aging */
                                spw->sp_min = spw->sp_max = spw->sp_warn = -1;
                        } else {
                                /* c) */
                                turn_on_default_aging(spw);
                        }
                }
        }

        return (PWU_SUCCESS);
}

/*
 * files_update_shadow(char *name, struct spwd *spwd)
 *
 * update the shadow password file SHADOW to contain the spwd structure
 * "spwd" for user "name"
 */
int
files_update_shadow(const char *name, struct spwd *spwd)
{
        struct stat64 stbuf;
        FILE *dst;
        FILE *src;
        struct spwd cur;
        char buf[SPW_SCRATCH_SIZE];
        int tempfd;
        mode_t filemode;
        int result = -1;
        int err = PWU_SUCCESS;

        /* Mode of the shadow file should be 400 or 000 */
        if (stat64(SHADOW, &stbuf) < 0) {
                err = PWU_STAT_FAILED;
                goto shadow_exit;
        }

        /* copy mode from current shadow file (0400 or 0000) */
        filemode = stbuf.st_mode & S_IRUSR;

        /*
         * we can't specify filemodes to fopen(), and we SHOULD NOT
         * set umask in multi-thread safe libraries, so we use
         * a combination of open() and fdopen()
         */
        tempfd = open(SHADTEMP, O_WRONLY|O_CREAT|O_TRUNC, filemode);
        if (tempfd < 0) {
                err = PWU_OPEN_FAILED;
                goto shadow_exit;
        }
        (void) fchown(tempfd, (uid_t)0, stbuf.st_gid);

        if ((dst = fdopen(tempfd, "wF")) == NULL) {
                err = PWU_OPEN_FAILED;
                goto shadow_exit;
        }

        if ((src = fopen(SHADOW, "rF")) == NULL) {
                err = PWU_OPEN_FAILED;
                (void) fclose(dst);
                (void) unlink(SHADTEMP);
                goto shadow_exit;
        }

        /*
         * copy old shadow to temporary file while replacing the entry
         * that matches "name".
         */
        while (fgetspent_r(src, &cur, buf, sizeof (buf)) != NULL) {

                if (strcmp(cur.sp_namp, name) == 0)
                        result = putspent(spwd, dst);
                else
                        result = putspent(&cur, dst);

                if (result != 0) {
                        err = PWU_WRITE_FAILED;
                        (void) fclose(src);
                        (void) fclose(dst);
                        goto shadow_exit;
                }
        }

        (void) fclose(src);

        if (fclose(dst) != 0) {
                /*
                 * Something went wrong (ENOSPC for example). Don't
                 * use the resulting temporary file!
                 */
                err = PWU_CLOSE_FAILED;
                (void) unlink(SHADTEMP);
                goto shadow_exit;
        }

        /*
         * Rename stmp to shadow:
         *   1. make sure /etc/oshadow is gone
         *   2. ln /etc/shadow /etc/oshadow
         *   3. mv /etc/stmp /etc/shadow
         */
        if (unlink(OSHADOW) && access(OSHADOW, 0) == 0) {
                err = PWU_UPDATE_FAILED;
                (void) unlink(SHADTEMP);
                goto shadow_exit;
        }

        if (link(SHADOW, OSHADOW) == -1) {
                err = PWU_UPDATE_FAILED;
                (void) unlink(SHADTEMP);
                goto shadow_exit;
        }

        if (rename(SHADTEMP, SHADOW) == -1) {
                err = PWU_UPDATE_FAILED;
                (void) unlink(SHADTEMP);
                goto shadow_exit;
        }
        (void) unlink(OSHADOW);

shadow_exit:
        return (err);
}

int
files_update_passwd(const char *name, struct passwd *pwd)
{
        struct stat64 stbuf;
        FILE *src, *dst;
        int tempfd;
        struct passwd cur;
        char buf[PWD_SCRATCH_SIZE];
        int result;
        int err = PWU_SUCCESS;

        if (stat64(PASSWD, &stbuf) < 0) {
                err = PWU_STAT_FAILED;
                goto passwd_exit;
        }

        /* see files_update_shadow() for open()+fdopen() rationale */

        if ((tempfd = open(PASSTEMP, O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
                err = PWU_OPEN_FAILED;
                goto passwd_exit;
        }
        if ((dst = fdopen(tempfd, "wF")) == NULL) {
                err = PWU_OPEN_FAILED;
                goto passwd_exit;
        }
        if ((src = fopen(PASSWD, "rF")) == NULL) {
                err = PWU_OPEN_FAILED;
                (void) fclose(dst);
                (void) unlink(PASSTEMP);
                goto passwd_exit;
        }

        /*
         * copy old password entries to temporary file while replacing
         * the entry that matches "name"
         */
        while (fgetpwent_r(src, &cur, buf, sizeof (buf)) != NULL) {
                if (strcmp(cur.pw_name, name) == 0)
                        result = putpwent(pwd, dst);
                else
                        result = putpwent(&cur, dst);
                if (result != 0) {
                        err = PWU_WRITE_FAILED;
                        (void) fclose(src);
                        (void) fclose(dst);
                        goto passwd_exit;
                }
        }

        (void) fclose(src);
        if (fclose(dst) != 0) {
                err = PWU_CLOSE_FAILED;
                goto passwd_exit; /* Don't trust the temporary file */
        }

        /* Rename temp to passwd */
        if (unlink(OPASSWD) && access(OPASSWD, 0) == 0) {
                err = PWU_UPDATE_FAILED;
                (void) unlink(PASSTEMP);
                goto passwd_exit;
        }

        if (link(PASSWD, OPASSWD) == -1) {
                err = PWU_UPDATE_FAILED;
                (void) unlink(PASSTEMP);
                goto passwd_exit;
        }

        if (rename(PASSTEMP, PASSWD) == -1) {
                err = PWU_UPDATE_FAILED;
                (void) unlink(PASSTEMP);
                goto passwd_exit;
        }

        (void) chmod(PASSWD, 0644);

passwd_exit:
        return (err);

}

/*
 * files_putpwnam(name, oldpw, rep, buf)
 *
 * store the password attributes contained in "buf" in /etc/passwd and
 * /etc/shadow.
 */
/*ARGSUSED*/
int
files_putpwnam(const char *name, const char *oldpw, pwu_repository_t *rep,
    void *buf)
{
        struct pwbuf *pwbuf = (struct pwbuf *)buf;
        int result = PWU_SUCCESS;

        if (pwbuf->pwd) {
                result = files_update_passwd(name, pwbuf->pwd);
        }

        if (result == PWU_SUCCESS && pwbuf->spwd) {
                if (pwbuf->update_history != 0) {
                        debug("update_history = %d", pwbuf->update_history);
                        result = files_update_history(name, pwbuf->spwd);
                } else {
                        debug("no password change");
                }
                if (result == PWU_SUCCESS) {
                        result = files_update_shadow(name, pwbuf->spwd);
                }
        }

        if (pwbuf->pwd) {
                (void) memset(pwbuf->pwd, 0, sizeof (struct passwd));
                (void) memset(pwbuf->pwd_scratch, 0, PWD_SCRATCH_SIZE);
                free(pwbuf->pwd);
                free(pwbuf->pwd_scratch);
        }
        if (pwbuf->spwd) {
                (void) memset(pwbuf->spwd, 0, sizeof (struct spwd));
                (void) memset(pwbuf->spwd_scratch, 0, SPW_SCRATCH_SIZE);
                free(pwbuf->spwd);
                free(pwbuf->spwd_scratch);
        }
        if (pwbuf->new_sp_pwdp) {
                free(pwbuf->new_sp_pwdp);
        }

        return (result);
}

/*
 *      NOTE:  This is all covered under the repository lock held for updating
 *      passwd(5) and shadow(5).
 */
int
files_update_history(const char *name, struct spwd *spwd)
{
        int     histsize;
        int     tmpfd;
        FILE    *src;   /* history database file */
        FILE    *dst;   /* temp history database being updated */
        struct  stat64 statbuf;
        char buf[MAX_LOGNAME + MAXHISTORY +
            (MAXHISTORY * CRYPT_MAXCIPHERTEXTLEN)+1];
        int     found;

        if ((histsize = def_getint("HISTORY=", DEFHISTORY)) == 0) {
                debug("files_update_history(%s) no history, unlinking", name);
                (void) unlink(HISTORY);
                return (PWU_SUCCESS);   /* no history update defined */
        }
        debug("files_update_history(%s, %s) histsize = %d", name, spwd->sp_pwdp,
            histsize);

        if (histsize > MAXHISTORY)
                histsize = MAXHISTORY;
        if ((tmpfd = open(HISTEMP, O_WRONLY|O_CREAT|O_TRUNC, HISTMODE)) < 0) {
                return (PWU_OPEN_FAILED);
        }
        (void) fchown(tmpfd, (uid_t)0, (gid_t)0);

        /* get ready to copy */
        if (((src = fopen(HISTORY, "rF")) == NULL) &&
            (errno != ENOENT)) {
                (void) unlink(HISTEMP);
                return (PWU_OPEN_FAILED);
        }
        if ((dst = fdopen(tmpfd, "wF")) == NULL) {
                (void) fclose(src);
                (void) unlink(HISTEMP);
                return (PWU_OPEN_FAILED);
        }

        /* Copy and update if found.  Add if not found. */

        found = 0;

        while ((src != NULL) &&
            (fgets(buf, sizeof (buf), src) != NULL)) {
                char    *user;
                char    *last;

                /* get username field */
                user = strtok_r(buf, ":", &last);

#ifdef  DEBUG
                debug("files_update_history: read=\"%s\"", user);
#endif  /* DEBUG */

                if (strcmp(user, name) == 0) {
                        char    *crypt;
                        int     i;

                        /* found user, update */
                        found++;
                        (void) fprintf(dst, "%s:%s:", name, spwd->sp_pwdp);
                        debug("files_update_history: update user\n"
                            "\t%s:%s:", name, spwd->sp_pwdp);

                        /* get old crypted password history */
                        for (i = 0; i < MAXHISTORY-1; i++) {
                                crypt = strtok_r(NULL, ":", &last);
                                if (crypt == NULL ||
                                    *crypt == '\n') {
                                        break;
                                }
                                (void) fprintf(dst, "%s:", crypt);
                                debug("\t%d = %s:", i+1, crypt);
                        }
                        (void) fprintf(dst, "\n");
                } else {

                        /* copy other users to updated file */
                        (void) fprintf(dst, "%s:%s", user, last);
#ifdef  DEBUG
                        debug("files_update_history: copy line %s",
                            user);
#endif  /* DEBUG */
                }
        }

        if (found == 0) {

                /* user not found, add to history file */
                (void) fprintf(dst, "%s:%s:\n", name, spwd->sp_pwdp);
                debug("files_update_history: add line\n"
                    "\t%s:%s:", name, spwd->sp_pwdp);
        }

        (void) fclose(src);

        /* If something messed up in file system, loose the update */
        if (fclose(dst) != 0) {

                debug("files_update_history: update file close failed %d",
                    errno);
                (void) unlink(HISTEMP);
                return (PWU_CLOSE_FAILED);
        }

        /*
         * rename history to ohistory,
         * rename tmp to history,
         * unlink ohistory.
         */

        (void) unlink(OHISTORY);

        if (stat64(OHISTORY, &statbuf) == 0 ||
            ((src != NULL) && (link(HISTORY, OHISTORY) != 0)) ||
            rename(HISTEMP, HISTORY) != 0) {

                /* old history won't go away, loose the update */
                debug("files_update_history: update file rename failed %d",
                    errno);
                (void) unlink(HISTEMP);
                return (PWU_UPDATE_FAILED);
        }

        (void) unlink(OHISTORY);
        return (PWU_SUCCESS);
}