From: Sam Hartman Date: Mon, 11 Sep 2023 14:00:42 -0600 Subject: pam_unix: obscure checks * Bring in the obscure checks that used to live in shadow so we can still support them --- modules/module-meson.build | 1 + modules/pam_unix/obscure.c | 199 +++++++++++++++++++++++++++++++++++++ modules/pam_unix/pam_unix.8.xml | 75 +++++++++++++- modules/pam_unix/pam_unix_passwd.c | 10 +- modules/pam_unix/support.h | 79 ++++++++------- 5 files changed, 324 insertions(+), 40 deletions(-) create mode 100644 modules/pam_unix/obscure.c diff --git a/modules/module-meson.build b/modules/module-meson.build index d55dad2..edf9d57 100644 --- a/modules/module-meson.build +++ b/modules/module-meson.build @@ -106,6 +106,7 @@ if module == 'pam_unix' 'pam_unix_auth.c', 'pam_unix_passwd.c', 'pam_unix_sess.c', + 'obscure.c', 'support.c', 'passverify.c', 'md5_good.c', diff --git a/modules/pam_unix/obscure.c b/modules/pam_unix/obscure.c new file mode 100644 index 0000000..9dbbe6e --- /dev/null +++ b/modules/pam_unix/obscure.c @@ -0,0 +1,199 @@ +/* + * Copyright 1989 - 1994, Julianne Frances Haugh + * 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. + * 3. Neither the name of Julianne F. Haugh nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY JULIE HAUGH 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 JULIE HAUGH 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. + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "pam_i18n.h" +#include "support.h" + +/* can't be a palindrome - like `R A D A R' or `M A D A M' */ +static int palindrome(const char *old, const char *new) { + int i, j; + + i = strlen (new); + + for (j = 0;j < i;j++) + if (new[i - j - 1] != new[j]) + return 0; + + return 1; +} + +/* more than half of the characters are different ones. */ +static int similar(const char *old, const char *new) { + int i, j; + + /* + * XXX - sometimes this fails when changing from a simple password + * to a really long one (MD5). For now, I just return success if + * the new password is long enough. Please feel free to suggest + * something better... --marekm + */ + if (strlen(new) >= 8) + return 0; + + for (i = j = 0; new[i] && old[i]; i++) + if (strchr(new, old[i])) + j++; + + if (i >= j * 2) + return 0; + + return 1; +} + +/* a nice mix of characters. */ +static int simple(const char *old, const char *new) { + int digits = 0; + int uppers = 0; + int lowers = 0; + int others = 0; + int size; + int i; + + for (i = 0;new[i];i++) { + if (isdigit (new[i])) + digits++; + else if (isupper (new[i])) + uppers++; + else if (islower (new[i])) + lowers++; + else + others++; + } + + /* + * The scam is this - a password of only one character type + * must be 8 letters long. Two types, 7, and so on. + */ + + size = 9; + if (digits) size--; + if (uppers) size--; + if (lowers) size--; + if (others) size--; + + if (size <= i) + return 0; + + return 1; +} + +static char *str_lower(char *string) { + char *cp; + + for (cp = string; *cp; cp++) + *cp = tolower(*cp); + return string; +} + +static const char * password_check(const char *old, const char *new, + const struct passwd *pwdp) { + const char *msg = NULL; + char *oldmono, *newmono, *wrapped; + + if (strcmp(new, old) == 0) + return _("Bad: new password must be different than the old one"); + + newmono = str_lower(strdup(new)); + oldmono = str_lower(strdup(old)); + wrapped = (char *)malloc(strlen(oldmono) * 2 + 1); + strcpy (wrapped, oldmono); + strcat (wrapped, oldmono); + + if (palindrome(oldmono, newmono)) { + msg = _("Bad: new password cannot be a palindrome"); + } else if (strcmp(oldmono, newmono) == 0) { + msg = _("Bad: new and old password must differ by more than just case"); + } else if (similar(oldmono, newmono)) { + msg = _("Bad: new and old password are too similar"); + } else if (simple(old, new)) { + msg = _("Bad: new password is too simple"); + } else if (strstr(wrapped, newmono)) { + msg = _("Bad: new password is just a wrapped version of the old one"); + } + + _pam_delete(newmono); + _pam_delete(oldmono); + _pam_delete(wrapped); + + return msg; +} + +const char *obscure_msg(const char *old, const char *new, + const struct passwd *pwdp, unsigned int ctrl) { + int oldlen, newlen; + char *new1, *old1; + const char *msg; + + if (old == NULL) + return NULL; /* no check if old is NULL */ + + oldlen = strlen(old); + newlen = strlen(new); + + /* Remaining checks are optional. */ + if (off(UNIX_OBSCURE_CHECKS,ctrl)) + return NULL; + + if ((msg = password_check(old, new, pwdp)) != NULL) + return msg; + + /* The traditional crypt() truncates passwords to 8 chars. It is + possible to circumvent the above checks by choosing an easy + 8-char password and adding some random characters to it... + Example: "password$%^&*123". So check it again, this time + truncated to the maximum length. Idea from npasswd. --marekm */ + + if (!UNIX_DES_CRYPT(ctrl)) + return NULL; /* unlimited password length */ + + if (oldlen <= 8 && newlen <= 8) + return NULL; + + new1 = strndup(new,8); + old1 = strndup(old,8); + + msg = password_check(old1, new1, pwdp); + + _pam_delete(new1); + _pam_delete(old1); + + return msg; +} diff --git a/modules/pam_unix/pam_unix.8.xml b/modules/pam_unix/pam_unix.8.xml index d2cd198..d02320b 100644 --- a/modules/pam_unix/pam_unix.8.xml +++ b/modules/pam_unix/pam_unix.8.xml @@ -402,6 +402,79 @@ + + + + + + + Enable some extra checks on password strength. These checks + are based on the "obscure" checks in the original shadow + package. The behavior is similar to the pam_cracklib + module, but for non-dictionary-based checks. The following + checks are implemented: + + + + + + + + Verifies that the new password is not a palindrome + of (i.e., the reverse of) the previous one. + + + + + + + + + + Verifies that the new password isn't the same as the + old one with a change of case. + + + + + + + + + + Verifies that the new password isn't too much like + the previous one. + + + + + + + + + + Is the new password too simple? This is based on + the length of the password and the number of + different types of characters (alpha, numeric, etc.) + used. + + + + + + + + + + Is the new password a rotated version of the old + password? (E.g., "billy" and "illyb") + + + + + + + no_pass_expiry @@ -495,4 +568,4 @@ session required pam_unix.so - \ No newline at end of file + diff --git a/modules/pam_unix/pam_unix_passwd.c b/modules/pam_unix/pam_unix_passwd.c index 4a3784a..ea941fe 100644 --- a/modules/pam_unix/pam_unix_passwd.c +++ b/modules/pam_unix/pam_unix_passwd.c @@ -87,6 +87,9 @@ extern int getrpcport(const char *host, unsigned long prognum, # endif /* GNU libc 2.1 */ #endif +extern const char *obscure_msg(const char *, const char *, const struct passwd *, + unsigned int); + /* How it works: Gets in username (has to be done) from the calling program @@ -588,6 +591,11 @@ static int _pam_unix_approve_pass(pam_handle_t * pamh return retval; } } + if (!remark && pass_old != NULL) { /* only check if we don't already have a failure */ + struct passwd *pwd; + pwd = pam_modutil_getpwnam(pamh, user); + remark = (char *)obscure_msg(pass_old,pass_new,pwd,ctrl); /* do obscure checks */ + } } if (remark) { _make_remark(pamh, ctrl, PAM_ERROR_MSG, remark); @@ -603,7 +611,7 @@ pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) int retval; int remember = -1; int rounds = 0; - int pass_min_len = 0; + int pass_min_len = 6; struct passwd *pwd; /* */ diff --git a/modules/pam_unix/support.h b/modules/pam_unix/support.h index e8f629d..425ff66 100644 --- a/modules/pam_unix/support.h +++ b/modules/pam_unix/support.h @@ -6,6 +6,7 @@ #define _PAM_UNIX_SUPPORT_H #include +#include "pam_inline.h" /* * File to read value of ENCRYPT_METHOD from. @@ -101,50 +102,52 @@ typedef struct { #define UNIX_GOST_YESCRYPT_PASS 31 /* new password hashes will use gost-yescrypt */ #define UNIX_YESCRYPT_PASS 32 /* new password hashes will use yescrypt */ #define UNIX_NULLRESETOK 33 /* allow empty password if password reset is enforced */ +#define UNIX_OBSCURE_CHECKS 34 /* enable obscure checks on passwords */ /* -------------- */ -#define UNIX_CTRLS_ 34 /* number of ctrl arguments defined */ +#define UNIX_CTRLS_ 35 /* number of ctrl arguments defined */ #define UNIX_DES_CRYPT(ctrl) (off(UNIX_MD5_PASS,ctrl)&&off(UNIX_BIGCRYPT,ctrl)&&off(UNIX_SHA256_PASS,ctrl)&&off(UNIX_SHA512_PASS,ctrl)&&off(UNIX_BLOWFISH_PASS,ctrl)&&off(UNIX_GOST_YESCRYPT_PASS,ctrl)&&off(UNIX_YESCRYPT_PASS,ctrl)) static const UNIX_Ctrls unix_args[UNIX_CTRLS_] = { -/* symbol token name ctrl mask ctrl * - * --------------------------- -------------------- ------------------------- ---------------- */ - -/* UNIX__OLD_PASSWD */ {NULL, _ALL_ON_, 01, 0}, -/* UNIX__VERIFY_PASSWD */ {NULL, _ALL_ON_, 02, 0}, -/* UNIX__IAMROOT */ {NULL, _ALL_ON_, 04, 0}, -/* UNIX_AUDIT */ {"audit", _ALL_ON_, 010, 0}, -/* UNIX_USE_FIRST_PASS */ {"use_first_pass", _ALL_ON_^(060ULL), 020, 0}, -/* UNIX_TRY_FIRST_PASS */ {"try_first_pass", _ALL_ON_^(060ULL), 040, 0}, -/* UNIX_AUTHTOK_TYPE */ {"authtok_type=", _ALL_ON_, 0100, 0}, -/* UNIX__PRELIM */ {NULL, _ALL_ON_^(0600ULL), 0200, 0}, -/* UNIX__UPDATE */ {NULL, _ALL_ON_^(0600ULL), 0400, 0}, -/* UNIX__NONULL */ {NULL, _ALL_ON_, 01000, 0}, -/* UNIX__QUIET */ {NULL, _ALL_ON_, 02000, 0}, -/* UNIX_USE_AUTHTOK */ {"use_authtok", _ALL_ON_, 04000, 0}, -/* UNIX_SHADOW */ {"shadow", _ALL_ON_, 010000, 0}, -/* UNIX_MD5_PASS */ {"md5", _ALL_ON_^(015660420000ULL), 020000, 1}, -/* UNIX__NULLOK */ {"nullok", _ALL_ON_^(01000ULL), 0, 0}, -/* UNIX_DEBUG */ {"debug", _ALL_ON_, 040000, 0}, -/* UNIX_NODELAY */ {"nodelay", _ALL_ON_, 0100000, 0}, -/* UNIX_NIS */ {"nis", _ALL_ON_, 0200000, 0}, -/* UNIX_BIGCRYPT */ {"bigcrypt", _ALL_ON_^(015660420000ULL), 0400000, 1}, -/* UNIX_LIKE_AUTH */ {"likeauth", _ALL_ON_, 01000000, 0}, -/* UNIX_REMEMBER_PASSWD */ {"remember=", _ALL_ON_, 02000000, 0}, -/* UNIX_NOREAP */ {"noreap", _ALL_ON_, 04000000, 0}, -/* UNIX_BROKEN_SHADOW */ {"broken_shadow", _ALL_ON_, 010000000, 0}, -/* UNIX_SHA256_PASS */ {"sha256", _ALL_ON_^(015660420000ULL), 020000000, 1}, -/* UNIX_SHA512_PASS */ {"sha512", _ALL_ON_^(015660420000ULL), 040000000, 1}, -/* UNIX_ALGO_ROUNDS */ {"rounds=", _ALL_ON_, 0100000000, 0}, -/* UNIX_BLOWFISH_PASS */ {"blowfish", _ALL_ON_^(015660420000ULL), 0200000000, 1}, -/* UNIX_MIN_PASS_LEN */ {"minlen=", _ALL_ON_, 0400000000, 0}, -/* UNIX_QUIET */ {"quiet", _ALL_ON_, 01000000000, 0}, -/* UNIX_NO_PASS_EXPIRY */ {"no_pass_expiry", _ALL_ON_, 02000000000, 0}, -/* UNIX_DES */ {"des", _ALL_ON_^(015660420000ULL), 0, 1}, -/* UNIX_GOST_YESCRYPT_PASS */ {"gost_yescrypt", _ALL_ON_^(015660420000ULL), 04000000000, 1}, -/* UNIX_YESCRYPT_PASS */ {"yescrypt", _ALL_ON_^(015660420000ULL), 010000000000, 1}, -/* UNIX_NULLRESETOK */ {"nullresetok", _ALL_ON_, 020000000000, 0}, +/* symbol token name ctrl mask ctrl * + * --------------------------- -------------------- ------------------------- ------------ */ + +/* UNIX__OLD_PASSWD */ {NULL, _ALL_ON_, 0x1, 0}, +/* UNIX__VERIFY_PASSWD */ {NULL, _ALL_ON_, 0x2, 0}, +/* UNIX__IAMROOT */ {NULL, _ALL_ON_, 0x4, 0}, +/* UNIX_AUDIT */ {"audit", _ALL_ON_, 0x8, 0}, +/* UNIX_USE_FIRST_PASS */ {"use_first_pass", _ALL_ON_^(0x30ULL), 0x10, 0}, +/* UNIX_TRY_FIRST_PASS */ {"try_first_pass", _ALL_ON_^(0x30ULL), 0x20, 0}, +/* UNIX_AUTHTOK_TYPE */ {"authtok_type=", _ALL_ON_, 0x40, 0}, +/* UNIX__PRELIM */ {NULL, _ALL_ON_^(0x180ULL), 0x80, 0}, +/* UNIX__UPDATE */ {NULL, _ALL_ON_^(0x180ULL), 0x100, 0}, +/* UNIX__NONULL */ {NULL, _ALL_ON_, 0x200, 0}, +/* UNIX__QUIET */ {NULL, _ALL_ON_, 0x400, 0}, +/* UNIX_USE_AUTHTOK */ {"use_authtok", _ALL_ON_, 0x800, 0}, +/* UNIX_SHADOW */ {"shadow", _ALL_ON_, 0x1000, 0}, +/* UNIX_MD5_PASS */ {"md5", _ALL_ON_^(0x6EC22000ULL), 0x2000, 1}, +/* UNIX__NULLOK */ {"nullok", _ALL_ON_^(0x200ULL), 0, 0}, +/* UNIX_DEBUG */ {"debug", _ALL_ON_, 0x4000, 0}, +/* UNIX_NODELAY */ {"nodelay", _ALL_ON_, 0x8000, 0}, +/* UNIX_NIS */ {"nis", _ALL_ON_, 0x10000, 0}, +/* UNIX_BIGCRYPT */ {"bigcrypt", _ALL_ON_^(0x6EC22000ULL), 0x20000, 1}, +/* UNIX_LIKE_AUTH */ {"likeauth", _ALL_ON_, 0x40000, 0}, +/* UNIX_REMEMBER_PASSWD */ {"remember=", _ALL_ON_, 0x80000, 0}, +/* UNIX_NOREAP */ {"noreap", _ALL_ON_, 0x100000, 0}, +/* UNIX_BROKEN_SHADOW */ {"broken_shadow", _ALL_ON_, 0x200000, 0}, +/* UNIX_SHA256_PASS */ {"sha256", _ALL_ON_^(0x6EC22000ULL), 0x400000, 1}, +/* UNIX_SHA512_PASS */ {"sha512", _ALL_ON_^(0x6EC22000ULL), 0x800000, 1}, +/* UNIX_ALGO_ROUNDS */ {"rounds=", _ALL_ON_, 0x1000000, 0}, +/* UNIX_BLOWFISH_PASS */ {"blowfish", _ALL_ON_^(0x6EC22000ULL), 0x2000000, 1}, +/* UNIX_MIN_PASS_LEN */ {"minlen=", _ALL_ON_, 0x4000000, 0}, +/* UNIX_QUIET */ {"quiet", _ALL_ON_, 0x8000000, 0}, +/* UNIX_NO_PASS_EXPIRY */ {"no_pass_expiry", _ALL_ON_, 0x10000000, 0}, +/* UNIX_DES */ {"des", _ALL_ON_^(0x6EC22000ULL), 0, 1}, +/* UNIX_GOST_YESCRYPT_PASS */ {"gost_yescrypt", _ALL_ON_^(0x6EC22000ULL), 0x20000000, 1}, +/* UNIX_YESCRYPT_PASS */ {"yescrypt", _ALL_ON_^(0x6EC22000ULL), 0x40000000, 1}, +/* UNIX_NULLRESETOK */ {"nullresetok", _ALL_ON_, 0x80000000, 0}, +/* UNIX_OBSCURE_CHECKS */ {"obscure", _ALL_ON_, 0x100000000, 0}, }; #define UNIX_DEFAULTS (unix_args[UNIX__NONULL].flag)