summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/Makefile.am15
-rw-r--r--modules/pam_pwhistory/.cvsignore8
-rw-r--r--modules/pam_pwhistory/Makefile.am35
-rw-r--r--modules/pam_pwhistory/README.xml41
-rw-r--r--modules/pam_pwhistory/opasswd.c473
-rw-r--r--modules/pam_pwhistory/opasswd.h45
-rw-r--r--modules/pam_pwhistory/pam_pwhistory.8.xml226
-rw-r--r--modules/pam_pwhistory/pam_pwhistory.c319
-rwxr-xr-xmodules/pam_pwhistory/tst-pam_pwhistory2
9 files changed, 1157 insertions, 7 deletions
diff --git a/modules/Makefile.am b/modules/Makefile.am
index c79f5957..f21d52e8 100644
--- a/modules/Makefile.am
+++ b/modules/Makefile.am
@@ -1,15 +1,16 @@
#
-# Copyright (c) 2005, 2006 Thorsten Kukuk <kukuk@thkukuk.de>
+# Copyright (c) 2005, 2006, 2008 Thorsten Kukuk <kukuk@thkukuk.de>
#
SUBDIRS = pam_access pam_cracklib pam_debug pam_deny pam_echo \
- pam_env pam_filter pam_ftp pam_group pam_issue pam_keyinit \
- pam_lastlog pam_limits pam_listfile pam_localuser pam_mail \
- pam_mkhomedir pam_motd pam_nologin pam_permit pam_rhosts pam_rootok \
- pam_securetty pam_selinux pam_sepermit pam_shells pam_stress \
+ pam_env pam_exec pam_faildelay pam_filter pam_ftp \
+ pam_group pam_issue pam_keyinit pam_lastlog pam_limits \
+ pam_listfile pam_localuser pam_loginuid pam_mail \
+ pam_mkhomedir pam_motd pam_namespace pam_nologin \
+ pam_permit pam_pwhistory pam_rhosts pam_rootok pam_securetty \
+ pam_selinux pam_sepermit pam_shells pam_stress \
pam_succeed_if pam_tally pam_time pam_tty_audit pam_umask \
- pam_unix pam_userdb pam_warn pam_wheel pam_xauth pam_exec \
- pam_namespace pam_loginuid pam_faildelay
+ pam_unix pam_userdb pam_warn pam_wheel pam_xauth
CLEANFILES = *~
diff --git a/modules/pam_pwhistory/.cvsignore b/modules/pam_pwhistory/.cvsignore
new file mode 100644
index 00000000..c0d3c72c
--- /dev/null
+++ b/modules/pam_pwhistory/.cvsignore
@@ -0,0 +1,8 @@
+*.la
+*.lo
+.deps
+.libs
+Makefile
+Makefile.in
+README
+pam_pwhistory.8
diff --git a/modules/pam_pwhistory/Makefile.am b/modules/pam_pwhistory/Makefile.am
new file mode 100644
index 00000000..018d0b52
--- /dev/null
+++ b/modules/pam_pwhistory/Makefile.am
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2008 Thorsten Kukuk <kukuk@suse.de>
+#
+
+CLEANFILES = *~
+
+EXTRA_DIST = README $(MANS) $(XMLS) tst-pam_pwhistory
+
+TESTS = tst-pam_pwhistory
+
+man_MANS = pam_pwhistory.8
+
+XMLS = README.xml pam_pwhistory.8.xml
+
+securelibdir = $(SECUREDIR)
+secureconfdir = $(SCONFIGDIR)
+
+AM_CFLAGS = -I$(top_srcdir)/libpam/include -I$(top_srcdir)/libpamc/include
+AM_LDFLAGS = -no-undefined -avoid-version -module
+if HAVE_VERSIONING
+ AM_LDFLAGS += -Wl,--version-script=$(srcdir)/../modules.map
+endif
+
+noinst_HEADERS = opasswd.h
+
+securelib_LTLIBRARIES = pam_pwhistory.la
+pam_pwhistory_la_LIBADD = -L$(top_builddir)/libpam -lpam @LIBCRYPT@
+pam_pwhistory_la_SOURCES = pam_pwhistory.c opasswd.c
+
+if ENABLE_REGENERATE_MAN
+noinst_DATA = README
+README: pam_pwhistory.8.xml
+-include $(top_srcdir)/Make.xml.rules
+endif
+
diff --git a/modules/pam_pwhistory/README.xml b/modules/pam_pwhistory/README.xml
new file mode 100644
index 00000000..f048e321
--- /dev/null
+++ b/modules/pam_pwhistory/README.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding='UTF-8'?>
+<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+"http://www.docbook.org/xml/4.3/docbookx.dtd"
+[
+<!--
+<!ENTITY pamaccess SYSTEM "pam_pwhistory.8.xml">
+-->
+]>
+
+<article>
+
+ <articleinfo>
+
+ <title>
+ <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
+ href="pam_pwhistory.8.xml" xpointer='xpointer(//refnamediv[@id = "pam_pwhistory-name"]/*)'/>
+ </title>
+
+ </articleinfo>
+
+ <section>
+ <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
+ href="pam_pwhistory.8.xml" xpointer='xpointer(//refsect1[@id = "pam_pwhistory-description"]/*)'/>
+ </section>
+
+ <section>
+ <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
+ href="pam_pwhistory.8.xml" xpointer='xpointer(//refsect1[@id = "pam_pwhistory-options"]/*)'/>
+ </section>
+
+ <section>
+ <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
+ href="pam_pwhistory.8.xml" xpointer='xpointer(//refsect1[@id = "pam_pwhistory-examples"]/*)'/>
+ </section>
+
+ <section>
+ <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
+ href="pam_pwhistory.8.xml" xpointer='xpointer(//refsect1[@id = "pam_pwhistory-author"]/*)'/>
+ </section>
+
+</article>
diff --git a/modules/pam_pwhistory/opasswd.c b/modules/pam_pwhistory/opasswd.c
new file mode 100644
index 00000000..89452d3f
--- /dev/null
+++ b/modules/pam_pwhistory/opasswd.c
@@ -0,0 +1,473 @@
+/*
+ * Copyright (c) 2008 Thorsten Kukuk <kukuk@suse.de>
+ *
+ * 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, and the entire permission notice in its entirety,
+ * including the disclaimer of warranties.
+ * 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.
+ * 3. The name of the author may not be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * ALTERNATIVELY, this product may be distributed under the terms of
+ * the GNU Public License, in which case the provisions of the GPL are
+ * required INSTEAD OF the above restrictions. (This clause is
+ * necessary due to a potential bad interaction between the GPL and
+ * the restrictions contained in a BSD-style copyright.)
+ *
+ * THIS SOFTWARE IS PROVIDED ``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 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.
+ */
+
+#if defined(HAVE_CONFIG_H)
+#include <config.h>
+#endif
+
+#include <pwd.h>
+#include <time.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <syslog.h>
+#include <sys/stat.h>
+
+#if defined (HAVE_XCRYPT_H)
+#include <xcrypt.h>
+#elif defined (HAVE_CRYPT_H)
+#include <crypt.h>
+#endif
+
+#include <security/pam_ext.h>
+#include <security/pam_modules.h>
+
+#include "opasswd.h"
+
+#ifndef RANDOM_DEVICE
+#define RANDOM_DEVICE "/dev/urandom"
+#endif
+
+#define OLD_PASSWORDS_FILE "/etc/security/opasswd"
+#define TMP_PASSWORDS_FILE OLD_PASSWORDS_FILE".tmpXXXXXX"
+
+#define DEFAULT_BUFLEN 4096
+
+typedef struct {
+ char *user;
+ char *uid;
+ int count;
+ char *old_passwords;
+} opwd;
+
+
+static int
+parse_entry (char *line, opwd *data)
+{
+ const char delimiters[] = ":";
+ char *endptr;
+
+ data->user = strsep (&line, delimiters);
+ data->uid = strsep (&line, delimiters);
+ data->count = strtol (strsep (&line, delimiters), &endptr, 10);
+ if (endptr != NULL && *endptr != '\0')
+ return 1;
+
+ data->old_passwords = strsep (&line, delimiters);
+
+ return 0;
+}
+
+/* Check, if the new password is already in the opasswd file. */
+int
+check_old_password (pam_handle_t *pamh, const char *user,
+ const char *newpass, int debug)
+{
+ int retval = PAM_SUCCESS;
+ FILE *oldpf;
+ char *buf = NULL;
+ size_t buflen = 0;
+ opwd entry;
+ int found = 0;
+
+ if ((oldpf = fopen (OLD_PASSWORDS_FILE, "r")) == NULL)
+ {
+ if (errno != ENOENT)
+ pam_syslog (pamh, LOG_ERR, "Cannot open %s: %m", OLD_PASSWORDS_FILE);
+ return PAM_SUCCESS;
+ }
+
+ while (!feof (oldpf))
+ {
+ char *cp, *tmp;
+#if defined(HAVE_GETLINE)
+ ssize_t n = getline (&buf, &buflen, oldpf);
+#elif defined (HAVE_GETDELIM)
+ ssize_t n = getdelim (&buf, &buflen, '\n', oldpf);
+#else
+ ssize_t n;
+
+ if (buf == NULL)
+ {
+ buflen = DEFAULT_BUFLEN;
+ buf = malloc (buflen);
+ if (buf == NULL)
+ return PAM_BUF_ERR;
+ }
+ buf[0] = '\0';
+ fgets (buf, buflen - 1, oldpf);
+ n = strlen (buf);
+#endif /* HAVE_GETLINE / HAVE_GETDELIM */
+ cp = buf;
+
+ if (n < 1)
+ break;
+
+ tmp = strchr (cp, '#'); /* remove comments */
+ if (tmp)
+ *tmp = '\0';
+ while (isspace ((int)*cp)) /* remove spaces and tabs */
+ ++cp;
+ if (*cp == '\0') /* ignore empty lines */
+ continue;
+
+ if (cp[strlen (cp) - 1] == '\n')
+ cp[strlen (cp) - 1] = '\0';
+
+ if (strncmp (cp, user, strlen (user)) == 0 &&
+ cp[strlen (user)] == ':')
+ {
+ /* We found the line we needed */
+ if (parse_entry (cp, &entry) == 0)
+ {
+ found = 1;
+ break;
+ }
+ }
+ }
+
+ fclose (oldpf);
+
+ if (found)
+ {
+ const char delimiters[] = ",";
+ struct crypt_data output;
+ char *running;
+ char *oldpass;
+
+ memset (&output, 0, sizeof (output));
+
+ running = strdupa (entry.old_passwords);
+ if (running == NULL)
+ return PAM_BUF_ERR;
+
+ do {
+ oldpass = strsep (&running, delimiters);
+ if (oldpass && strlen (oldpass) > 0 &&
+ strcmp (crypt_r (newpass, oldpass, &output), oldpass) == 0)
+ {
+ if (debug)
+ pam_syslog (pamh, LOG_DEBUG, "New password already used");
+ retval = PAM_AUTHTOK_ERR;
+ break;
+ }
+ } while (oldpass != NULL);
+ }
+
+ if (buf)
+ free (buf);
+
+ return retval;
+}
+
+int
+save_old_password (pam_handle_t *pamh, const char *user, uid_t uid,
+ const char *oldpass, int howmany, int debug UNUSED)
+{
+ char opasswd_tmp[] = TMP_PASSWORDS_FILE;
+ struct stat opasswd_stat;
+ FILE *oldpf, *newpf;
+ int newpf_fd;
+ int do_create = 0;
+ int retval = PAM_SUCCESS;
+ char *buf = NULL;
+ size_t buflen = 0;
+ int found = 0;
+
+ if (howmany <= 0)
+ return PAM_SUCCESS;
+
+ if (oldpass == NULL || *oldpass == '\0')
+ return PAM_SUCCESS;
+
+ if ((oldpf = fopen (OLD_PASSWORDS_FILE, "r")) == NULL)
+ {
+ if (errno == ENOENT)
+ {
+ pam_syslog (pamh, LOG_NOTICE, "Creating %s",
+ OLD_PASSWORDS_FILE);
+ do_create = 1;
+ }
+ else
+ {
+ pam_syslog (pamh, LOG_ERR, "Cannot open %s: %m",
+ OLD_PASSWORDS_FILE);
+ return PAM_AUTHTOK_ERR;
+ }
+ }
+ else if (fstat (fileno (oldpf), &opasswd_stat) < 0)
+ {
+ pam_syslog (pamh, LOG_ERR, "Cannot stat %s: %m", OLD_PASSWORDS_FILE);
+ fclose (oldpf);
+ return PAM_AUTHTOK_ERR;
+ }
+
+ /* Open a temp passwd file */
+ newpf_fd = mkstemp (opasswd_tmp);
+ if (newpf_fd == -1)
+ {
+ pam_syslog (pamh, LOG_ERR, "Cannot create %s temp file: %m",
+ OLD_PASSWORDS_FILE);
+ fclose (oldpf);
+ return PAM_AUTHTOK_ERR;
+ }
+ if (do_create)
+ {
+ if (fchmod (newpf_fd, S_IRUSR|S_IWUSR) != 0)
+ pam_syslog (pamh, LOG_ERR,
+ "Cannot set permissions of %s temp file: %m",
+ OLD_PASSWORDS_FILE);
+ if (fchown (newpf_fd, 0, 0) != 0)
+ pam_syslog (pamh, LOG_ERR,
+ "Cannot set owner/group of %s temp file: %m",
+ OLD_PASSWORDS_FILE);
+ }
+ else
+ {
+ if (fchmod (newpf_fd, opasswd_stat.st_mode) != 0)
+ pam_syslog (pamh, LOG_ERR,
+ "Cannot set permissions of %s temp file: %m",
+ OLD_PASSWORDS_FILE);
+ if (fchown (newpf_fd, opasswd_stat.st_uid, opasswd_stat.st_gid) != 0)
+ pam_syslog (pamh, LOG_ERR,
+ "Cannot set owner/group of %s temp file: %m",
+ OLD_PASSWORDS_FILE);
+ }
+ newpf = fdopen (newpf_fd, "w+");
+ if (newpf == NULL)
+ {
+ pam_syslog (pamh, LOG_ERR, "Cannot fdopen %s: %m", opasswd_tmp);
+ fclose (oldpf);
+ close (newpf_fd);
+ retval = PAM_AUTHTOK_ERR;
+ goto error_opasswd;
+ }
+
+ if (!do_create)
+ while (!feof (oldpf))
+ {
+ char *cp, *tmp, *save;
+#if defined(HAVE_GETLINE)
+ ssize_t n = getline (&buf, &buflen, oldpf);
+#elif defined (HAVE_GETDELIM)
+ ssize_t n = getdelim (&buf, &buflen, '\n', oldpf);
+#else
+ ssize_t n;
+
+ if (buf == NULL)
+ {
+ buflen = DEFAULT_BUFLEN;
+ buf = malloc (buflen);
+ if (buf == NULL)
+ return PAM_BUF_ERR;
+
+ }
+ buf[0] = '\0';
+ fgets (buf, buflen - 1, oldpf);
+ n = strlen (buf);
+#endif /* HAVE_GETLINE / HAVE_GETDELIM */
+
+ cp = buf;
+ save = strdup (buf); /* Copy to write the original data back. */
+ if (save == NULL)
+ return PAM_BUF_ERR;
+
+ if (n < 1)
+ break;
+
+ tmp = strchr (cp, '#'); /* remove comments */
+ if (tmp)
+ *tmp = '\0';
+ while (isspace ((int)*cp)) /* remove spaces and tabs */
+ ++cp;
+ if (*cp == '\0') /* ignore empty lines */
+ goto write_old_data;
+
+ if (cp[strlen (cp) - 1] == '\n')
+ cp[strlen (cp) - 1] = '\0';
+
+ if (strncmp (cp, user, strlen (user)) == 0 &&
+ cp[strlen (user)] == ':')
+ {
+ /* We found the line we needed */
+ opwd entry;
+
+ if (parse_entry (cp, &entry) == 0)
+ {
+ char *out = NULL;
+
+ found = 1;
+
+ /* Don't save the current password twice */
+ if (entry.old_passwords)
+ {
+ /* there is only one password */
+ if (strcmp (entry.old_passwords, oldpass) == 0)
+ goto write_old_data;
+ else
+ {
+ /* check last entry */
+ cp = strstr (entry.old_passwords, oldpass);
+
+ if (cp && strcmp (cp, oldpass) == 0)
+ { /* the end is the same, check that there
+ is a "," before. */
+ --cp;
+ if (*cp == ',')
+ goto write_old_data;
+ }
+ }
+ }
+
+ /* increase count. */
+ entry.count++;
+
+ /* check that we don't remember to many passwords. */
+ while (entry.count > howmany)
+ {
+ char *p = strpbrk (entry.old_passwords, ",");
+ if (p != NULL)
+ entry.old_passwords = ++p;
+ entry.count--;
+ }
+
+ if (entry.old_passwords == NULL)
+ {
+ if (asprintf (&out, "%s:%s:%d:%s\n",
+ entry.user, entry.uid, entry.count,
+ oldpass) < 0)
+ {
+ retval = PAM_AUTHTOK_ERR;
+ fclose (oldpf);
+ fclose (newpf);
+ goto error_opasswd;
+ }
+ }
+ else
+ {
+ if (asprintf (&out, "%s:%si%d:%s,%s\n",
+ entry.user, entry.uid, entry.count,
+ entry.old_passwords, oldpass) < 0)
+ {
+ retval = PAM_AUTHTOK_ERR;
+ fclose (oldpf);
+ fclose (newpf);
+ goto error_opasswd;
+ }
+ }
+
+ if (fputs (out, newpf) < 0)
+ {
+ free (out);
+ free (save);
+ retval = PAM_AUTHTOK_ERR;
+ fclose (oldpf);
+ fclose (newpf);
+ goto error_opasswd;
+ }
+ free (out);
+ }
+ }
+ else
+ {
+ write_old_data:
+ if (fputs (save, newpf) < 0)
+ {
+ free (save);
+ retval = PAM_AUTHTOK_ERR;
+ fclose (oldpf);
+ fclose (newpf);
+ goto error_opasswd;
+ }
+ }
+ free (save);
+ }
+
+ if (!found)
+ {
+ char *out;
+
+ if (asprintf (&out, "%s:%d:1:%s\n", user, uid, oldpass) < 0)
+ {
+ retval = PAM_AUTHTOK_ERR;
+ if (oldpf)
+ fclose (oldpf);
+ fclose (newpf);
+ goto error_opasswd;
+ }
+ if (fputs (out, newpf) < 0)
+ {
+ free (out);
+ retval = PAM_AUTHTOK_ERR;
+ if (oldpf)
+ fclose (oldpf);
+ fclose (newpf);
+ goto error_opasswd;
+ }
+ free (out);
+ }
+
+ if (oldpf)
+ if (fclose (oldpf) != 0)
+ {
+ pam_syslog (pamh, LOG_ERR, "Error while closing old opasswd file: %m");
+ retval = PAM_AUTHTOK_ERR;
+ fclose (newpf);
+ goto error_opasswd;
+ }
+
+ if (fclose (newpf) != 0)
+ {
+ pam_syslog (pamh, LOG_ERR,
+ "Error while closing temporary opasswd file: %m");
+ retval = PAM_AUTHTOK_ERR;
+ goto error_opasswd;
+ }
+
+ unlink (OLD_PASSWORDS_FILE".old");
+ if (link (OLD_PASSWORDS_FILE, OLD_PASSWORDS_FILE".old") != 0 &&
+ errno != ENOENT)
+ pam_syslog (pamh, LOG_ERR, "Cannot create backup file of %s: %m",
+ OLD_PASSWORDS_FILE);
+ rename (opasswd_tmp, OLD_PASSWORDS_FILE);
+ error_opasswd:
+ unlink (opasswd_tmp);
+
+ return retval;
+}
diff --git a/modules/pam_pwhistory/opasswd.h b/modules/pam_pwhistory/opasswd.h
new file mode 100644
index 00000000..e8a20139
--- /dev/null
+++ b/modules/pam_pwhistory/opasswd.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2008 Thorsten Kukuk <kukuk@suse.de>
+ *
+ * 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, and the entire permission notice in its entirety,
+ * including the disclaimer of warranties.
+ * 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.
+ * 3. The name of the author may not be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * ALTERNATIVELY, this product may be distributed under the terms of
+ * the GNU Public License, in which case the provisions of the GPL are
+ * required INSTEAD OF the above restrictions. (This clause is
+ * necessary due to a potential bad interaction between the GPL and
+ * the restrictions contained in a BSD-style copyright.)
+ *
+ * THIS SOFTWARE IS PROVIDED ``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 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.
+ */
+
+#ifndef __OPASSWD_H__
+#define __OPASSWD_H__
+
+extern int check_old_password (pam_handle_t *pamh, const char *user,
+ const char *newpass, int debug);
+extern int save_old_password (pam_handle_t *pamh, const char *user,
+ uid_t uid, const char *oldpass,
+ int howmany, int debug);
+
+#endif /* __OPASSWD_H__ */
diff --git a/modules/pam_pwhistory/pam_pwhistory.8.xml b/modules/pam_pwhistory/pam_pwhistory.8.xml
new file mode 100644
index 00000000..26d6bd15
--- /dev/null
+++ b/modules/pam_pwhistory/pam_pwhistory.8.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding='UTF-8'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+
+<refentry id="pam_pwhistory">
+
+ <refmeta>
+ <refentrytitle>pam_pwhistory</refentrytitle>
+ <manvolnum>8</manvolnum>
+ <refmiscinfo class="sectdesc">Linux-PAM Manual</refmiscinfo>
+ </refmeta>
+
+ <refnamediv id="pam_pwhistory-name">
+ <refname>pam_pwhistory</refname>
+ <refpurpose>PAM module to remember last passwords</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis id="pam_pwhistory-cmdsynopsis">
+ <command>pam_pwhistory.so</command>
+ <arg choice="opt">
+ debug
+ </arg>
+ <arg choice="opt">
+ use_authtok
+ </arg>
+ <arg choice="opt">
+ enforce_for_root
+ </arg>
+ <arg choice="opt">
+ remember=<replaceable>N</replaceable>
+ </arg>
+ <arg choice="opt">
+ retry=<replaceable>N</replaceable>
+ </arg>
+
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1 id="pam_pwhistory-description">
+
+ <title>DESCRIPTION</title>
+
+ <para>
+ This module saves the last passwords for each user in order
+ to force password change history and keep the user from
+ alternating between the same password too frequently.
+ </para>
+ <para>
+ This module does not work togehter with kerberos. In general,
+ it does not make much sense to use this module in conjuction
+ with NIS or LDAP, since the old passwords are stored on the
+ local machine and are not available on another machine for
+ password history checking.
+ </para>
+ </refsect1>
+
+ <refsect1 id="pam_pwhistory-options">
+ <title>OPTIONS</title>
+ <variablelist>
+ <varlistentry>
+ <term>
+ <option>debug</option>
+ </term>
+ <listitem>
+ <para>
+ Turns on debugging via
+ <citerefentry>
+ <refentrytitle>syslog</refentrytitle><manvolnum>3</manvolnum>
+ </citerefentry>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>use_authtok</option>
+ </term>
+ <listitem>
+ <para>
+ When password changing enforce the module to use the new password
+ provided by a previously stacked <option>password</option>
+ module (this is used in the example of the stacking of the
+ <command>pam_cracklib</command> module documented below).
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>enforce_for_root</option>
+ </term>
+ <listitem>
+ <para>
+ If this option is set, the check is enforced for root, too.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>remember=<replaceable>N</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ The last <replaceable>N</replaceable> passwords for each
+ user are saved in <filename>/etc/security/opasswd</filename>.
+ The default is <emphasis>10</emphasis>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>
+ <option>retry=<replaceable>N</replaceable></option>
+ </term>
+ <listitem>
+ <para>
+ Prompt user at most <replaceable>N</replaceable> times
+ before returning with error. The default is
+ <emphasis>1</emphasis>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="pam_pwhistory-types">
+ <title>MODULE TYPES PROVIDED</title>
+ <para>
+ Only the <option>password</option> module type is provided.
+ </para>
+ </refsect1>
+
+ <refsect1 id='pam_pwhistory-return_values'>
+ <title>RETURN VALUES</title>
+ <variablelist>
+ <varlistentry>
+ <term>PAM_AUTHTOK_ERR</term>
+ <listitem>
+ <para>
+ No new password was entered, the user aborted password
+ change or new password couldn't be set.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>PAM_IGNORE</term>
+ <listitem>
+ <para>
+ Password history was disabled.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>PAM_MAXTRIES</term>
+ <listitem>
+ <para>
+ Password was rejected too often.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>PAM_USER_UNKNOWN</term>
+ <listitem>
+ <para>
+ User is not known to system.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id='pam_pwhistory-examples'>
+ <title>EXAMPLES</title>
+ <para>
+ An example password section would be:
+ <programlisting>
+#%PAM-1.0
+password required pam_pwhistory.so
+password required pam_unix.so use_authtok
+ </programlisting>
+ </para>
+ <para>
+ In combination with <command>pam_cracklib</command>:
+ <programlisting>
+#%PAM-1.0
+password required pam_cracklib.so retry=3
+password required pam_pwhistory.so use_authtok
+password required pam_unix.so use_authtok
+ </programlisting>
+ </para>
+ </refsect1>
+
+ <refsect1 id="pam_pwhistory-files">
+ <title>FILES</title>
+ <variablelist>
+ <varlistentry>
+ <term><filename>/etc/security/opasswd</filename></term>
+ <listitem>
+ <para>File with password history</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id='pam_pwhistory-see_also'>
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>pam.conf</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>,
+ <citerefentry>
+ <refentrytitle>pam.d</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>,
+ <citerefentry>
+ <refentrytitle>pam</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>
+ </para>
+ </refsect1>
+
+ <refsect1 id='pam_pwhistory-author'>
+ <title>AUTHOR</title>
+ <para>
+ pam_pwhistory was written by Thorsten Kukuk &lt;kukuk@thkukuk.de&gt;
+ </para>
+ </refsect1>
+
+</refentry>
diff --git a/modules/pam_pwhistory/pam_pwhistory.c b/modules/pam_pwhistory/pam_pwhistory.c
new file mode 100644
index 00000000..d3cce728
--- /dev/null
+++ b/modules/pam_pwhistory/pam_pwhistory.c
@@ -0,0 +1,319 @@
+/*
+ * Copyright (c) 2008 Thorsten Kukuk
+ * Author: Thorsten Kukuk <kukuk@suse.de>
+ *
+ * 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, and the entire permission notice in its entirety,
+ * including the disclaimer of warranties.
+ * 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.
+ * 3. The name of the author may not be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * ALTERNATIVELY, this product may be distributed under the terms of
+ * the GNU Public License, in which case the provisions of the GPL are
+ * required INSTEAD OF the above restrictions. (This clause is
+ * necessary due to a potential bad interaction between the GPL and
+ * the restrictions contained in a BSD-style copyright.)
+ *
+ * THIS SOFTWARE IS PROVIDED ``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 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.
+ */
+
+#if defined(HAVE_CONFIG_H)
+#include <config.h>
+#endif
+
+#define PAM_SM_PASSWORD
+
+#include <pwd.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <shadow.h>
+#include <syslog.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <security/pam_modules.h>
+#include <security/pam_modutil.h>
+#include <security/pam_ext.h>
+#include <security/_pam_macros.h>
+
+#include "opasswd.h"
+
+#define NEW_PASSWORD_PROMPT _("New %s%spassword: ")
+#define AGAIN_PASSWORD_PROMPT _("Retype new %s%spassword: ")
+#define MISTYPED_PASSWORD _("Sorry, passwords do not match.")
+
+#define DEFAULT_BUFLEN 2048
+
+struct options_t {
+ int debug;
+ int use_authtok;
+ int enforce_for_root;
+ int remember;
+ int tries;
+};
+typedef struct options_t options_t;
+
+
+static void
+parse_option (pam_handle_t *pamh, const char *argv, options_t *options)
+{
+ if (strcasecmp (argv, "use_first_pass") == 0)
+ /* ignore */;
+ else if (strcasecmp (argv, "use_first_pass") == 0)
+ /* ignore */;
+ else if (strcasecmp (argv, "use_authtok") == 0)
+ options->use_authtok = 1;
+ else if (strcasecmp (argv, "debug") == 0)
+ options->debug = 1;
+ else if (strncasecmp (argv, "remember=", 9) == 0)
+ {
+ options->remember = strtol(&argv[9], NULL, 10);
+ if (options->remember < 0)
+ options->remember = 0;
+ if (options->remember > 400)
+ options->remember = 400;
+ }
+ else if (strncasecmp (argv, "retry=", 6) == 0)
+ {
+ options->tries = strtol(&argv[6], NULL, 10);
+ if (options->tries < 0)
+ options->tries = 1;
+ }
+ else if (strcasecmp (argv, "enforce_for_root") == 0)
+ options->enforce_for_root = 1;
+ else
+ pam_syslog (pamh, LOG_ERR, "pam_pwhistory: unknown option: %s", argv);
+}
+
+
+PAM_EXTERN int
+pam_sm_chauthtok (pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+ struct passwd *pwd;
+ char *newpass;
+ const char *user;
+ void *newpass_void;
+ int retval, tries;
+ options_t options;
+
+ memset (&options, 0, sizeof (options));
+
+ /* Set some default values, which could be overwritten later. */
+ options.remember = 10;
+ options.tries = 1;
+
+ /* Parse parameters for module */
+ for ( ; argc-- > 0; argv++)
+ parse_option (pamh, *argv, &options);
+
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG, "pam_sm_chauthtok entered");
+
+
+ if (options.remember == 0)
+ return PAM_IGNORE;
+
+ retval = pam_get_user (pamh, &user, NULL);
+ if (retval != PAM_SUCCESS)
+ return retval;
+
+ if (user == NULL || strlen (user) == 0)
+ {
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG,
+ "User is not known to system");
+
+ return PAM_USER_UNKNOWN;
+ }
+
+ if (flags & PAM_PRELIM_CHECK)
+ {
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG,
+ "pam_sm_chauthtok(PAM_PRELIM_CHECK)");
+
+ return PAM_SUCCESS;
+ }
+
+ pwd = pam_modutil_getpwnam (pamh, user);
+ if (pwd == NULL)
+ return PAM_USER_UNKNOWN;
+
+ /* Ignore root if not enforced */
+ if (pwd->pw_uid == 0 && !options.enforce_for_root)
+ return PAM_SUCCESS;
+
+ if ((strcmp(pwd->pw_passwd, "x") == 0) ||
+ ((pwd->pw_passwd[0] == '#') &&
+ (pwd->pw_passwd[1] == '#') &&
+ (strcmp(pwd->pw_name, pwd->pw_passwd + 2) == 0)))
+ {
+ struct spwd *spw = pam_modutil_getspnam (pamh, user);
+ if (spw == NULL)
+ return PAM_USER_UNKNOWN;
+
+ retval = save_old_password (pamh, user, pwd->pw_uid, spw->sp_pwdp,
+ options.remember, options.debug);
+ if (retval != PAM_SUCCESS)
+ return retval;
+ }
+ else
+ {
+ retval = save_old_password (pamh, user, pwd->pw_uid, pwd->pw_passwd,
+ options.remember, options.debug);
+ if (retval != PAM_SUCCESS)
+ return retval;
+ }
+
+ retval = pam_get_item (pamh, PAM_AUTHTOK, (const void **) &newpass_void);
+ newpass = (char *) newpass_void;
+ if (retval != PAM_SUCCESS)
+ return retval;
+ if (options.debug)
+ {
+ if (newpass)
+ pam_syslog (pamh, LOG_DEBUG, "got new auth token");
+ else
+ pam_syslog (pamh, LOG_DEBUG, "new auth token not set");
+ }
+
+ /* If we haven't been given a password yet, prompt for one... */
+ if (newpass == NULL)
+ {
+ if (options.use_authtok)
+ /* We are not allowed to ask for a new password */
+ return PAM_AUTHTOK_ERR;
+
+ tries = 0;
+
+ while ((newpass == NULL) && (tries++ < options.tries))
+ {
+ retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &newpass,
+ NEW_PASSWORD_PROMPT, "UNIX", " ");
+ if (retval != PAM_SUCCESS)
+ {
+ _pam_drop (newpass);
+ if (retval == PAM_CONV_AGAIN)
+ retval = PAM_INCOMPLETE;
+ return retval;
+ }
+
+ if (newpass == NULL)
+ {
+ /* We want to abort the password change */
+ pam_error (pamh, _("Password change aborted."));
+ return PAM_AUTHTOK_ERR;
+ }
+
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG, "check against old password file");
+
+ if (check_old_password (pamh, user, newpass,
+ options.debug) != PAM_SUCCESS)
+ {
+ pam_error (pamh,
+ _("Password has been already used. Choose another."));
+ _pam_overwrite (newpass);
+ _pam_drop (newpass);
+ if (tries >= options.tries)
+ {
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG,
+ "Aborted, too many tries");
+ return PAM_MAXTRIES;
+ }
+ }
+ else
+ {
+ int failed;
+ char *new2;
+
+ retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &new2,
+ AGAIN_PASSWORD_PROMPT, "UNIX", " ");
+ if (retval != PAM_SUCCESS)
+ return retval;
+
+ if (new2 == NULL)
+ { /* Aborting password change... */
+ pam_error (pamh, _("Password change aborted."));
+ return PAM_AUTHTOK_ERR;
+ }
+
+ failed = (strcmp (newpass, new2) != 0);
+
+ _pam_overwrite (new2);
+ _pam_drop (new2);
+
+ if (failed)
+ {
+ pam_error (pamh, MISTYPED_PASSWORD);
+ _pam_overwrite (newpass);
+ _pam_drop (newpass);
+ if (tries >= options.tries)
+ {
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG,
+ "Aborted, too many tries");
+ return PAM_MAXTRIES;
+ }
+ }
+ }
+ }
+
+ /* Remember new password */
+ pam_set_item (pamh, PAM_AUTHTOK, (void *) newpass);
+ }
+ else /* newpass != NULL, we found an old password */
+ {
+ if (options.debug)
+ pam_syslog (pamh, LOG_DEBUG, "look in old password file");
+
+ if (check_old_password (pamh, user, newpass,
+ options.debug) != PAM_SUCCESS)
+ {
+ pam_error (pamh,
+ _("Password has been already used. Choose another."));
+ /* We are only here, because old password was set.
+ So overwrite it, else it will be stored! */
+ pam_set_item (pamh, PAM_AUTHTOK, (void *) NULL);
+
+ return PAM_AUTHTOK_ERR;
+ }
+ }
+
+ return PAM_SUCCESS;
+}
+
+
+#ifdef PAM_STATIC
+/* static module data */
+struct pam_module _pam_pwhistory_modstruct = {
+ "pam_pwhistory",
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ pam_sm_chauthtok
+};
+#endif
diff --git a/modules/pam_pwhistory/tst-pam_pwhistory b/modules/pam_pwhistory/tst-pam_pwhistory
new file mode 100755
index 00000000..3531a88a
--- /dev/null
+++ b/modules/pam_pwhistory/tst-pam_pwhistory
@@ -0,0 +1,2 @@
+#!/bin/sh
+../../tests/tst-dlopen .libs/pam_pwhistory.so