diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/doc/man/pam_pwquality.8.pod b/doc/man/pam_pwquality.8.pod --- a/doc/man/pam_pwquality.8.pod 2026-06-24 13:39:51.505182998 +0200 +++ b/doc/man/pam_pwquality.8.pod 2026-06-24 13:47:32.042603712 +0200 @@ -250,6 +250,13 @@ a new password but use the one provided by the previously stacked B module. +=item BI<< "ludo" >> + +The list of letters that represent allowed character classes that +are allowed in a password (digits, uppercase, lowercase, others). +"d" is for digits, "u" is for uppercase letters, "l" is for +lowercase letters, "o" is for other characters. + =back =head1 MODULE TYPES PROVIDED diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/doc/man/pwquality.conf.5.pod b/doc/man/pwquality.conf.5.pod --- a/doc/man/pwquality.conf.5.pod 2026-06-24 13:39:51.505235241 +0200 +++ b/doc/man/pwquality.conf.5.pod 2026-06-24 13:50:30.694360330 +0200 @@ -85,7 +85,7 @@ Examples of such sequence are '12345' or 'fedcb'. Note that most such passwords will not pass the simplicity check unless the sequence is only a minor part of the password. -The check is disabled if the value is 0. (default 0) +The check is disabled if the value is 0. (default 0) =item B @@ -155,6 +155,13 @@ the following modules in the stack can use the B option. This option is off by default. +=item B + +The list of letters that represent allowed character classes that +are allowed in a password (digits, uppercase, lowercase, others). +"d" is for digits, "u" is for uppercase letters, "l" is for +lowercase letters, "o" is for other characters. (default "ludo") + =back =head1 SEE ALSO diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/check.c b/src/check.c --- a/src/check.c 2026-06-24 13:39:51.504779475 +0200 +++ b/src/check.c 2026-06-24 13:45:06.118168891 +0200 @@ -49,7 +49,7 @@ * the other */ -static int +static int distdifferent(const char *old, const char *new, size_t i, size_t j) { @@ -202,6 +202,7 @@ int others = 0; int size; int i; + const char *allow_classes; enum { NONE, DIGIT, UCASE, LCASE, OTHER } prevclass = NONE; int sameclass = 0; @@ -244,6 +245,35 @@ return PWQ_ERROR_MAX_CLASS_REPEAT; } } + pwquality_get_str_value(pwq, PWQ_SETTING_ALLOW_CLASSES, &allow_classes); + if (digits > 0) { + if (strpbrk(allow_classes, "d") == NULL) { + if (auxerror) + *auxerror = strdup("digits"); + return PWQ_ERROR_DISALLOWED_CLASS; + } + } + if (uppers > 0) { + if (strpbrk(allow_classes, "u") == NULL) { + if (auxerror) + *auxerror = strdup("uppercase"); + return PWQ_ERROR_DISALLOWED_CLASS; + } + } + if (lowers > 0) { + if (strpbrk(allow_classes, "l") == NULL) { + if (auxerror) + *auxerror = strdup("lowercase"); + return PWQ_ERROR_DISALLOWED_CLASS; + } + } + if (others > 0) { + if (strpbrk(allow_classes, "o") == NULL) { + if (auxerror) + *auxerror = strdup("other"); + return PWQ_ERROR_DISALLOWED_CLASS; + } + } if ((pwq->dig_credit >= 0) && (digits > pwq->dig_credit)) digits = pwq->dig_credit; diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/error.c b/src/error.c --- a/src/error.c 2026-06-24 13:39:51.504839606 +0200 +++ b/src/error.c 2026-06-24 13:45:29.032394205 +0200 @@ -149,6 +149,13 @@ return _("The configuration file is malformed"); case PWQ_ERROR_FATAL_FAILURE: return _("Fatal failure"); + case PWQ_ERROR_DISALLOWED_CLASS: + if (auxerror) { + snprintf(buf, len, _("The %s class of characters is not allowed"), (const char *)auxerror); + free(auxerror); + return buf; + } + return _("The password contains disallowed character class"); default: return _("Unknown error"); } @@ -188,4 +195,3 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ - diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/generate.c b/src/generate.c --- a/src/generate.c 2026-06-24 13:39:51.504750650 +0200 +++ b/src/generate.c 2026-06-24 13:45:38.556487851 +0200 @@ -95,7 +95,7 @@ } *offset += bits; - return low; + return low; } /* generate a random password according to the settings */ diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pam_pwquality.c b/src/pam_pwquality.c --- a/src/pam_pwquality.c 2026-06-24 13:39:51.504861183 +0200 +++ b/src/pam_pwquality.c 2026-06-24 13:46:16.791863795 +0200 @@ -88,7 +88,7 @@ } else if (!strncmp(*argv, "try_first_pass", 14)) { /* for pam_get_authtok, ignore */; } else if (pwquality_set_option(pwq, *argv)) { - pam_syslog(pamh, LOG_ERR, + pam_syslog(pamh, LOG_ERR, "pam_parse: unknown or broken option; %s", *argv); } } @@ -213,7 +213,7 @@ if (retval != PAM_SUCCESS || newtoken == NULL) { if (retval == PAM_AUTHTOK_ERR || newtoken == NULL) pam_syslog(pamh, LOG_INFO, "user aborted password change"); - else + else pam_syslog(pamh, LOG_ERR, "pam_get_authtok_noverify returned error: %s", pam_strerror(pamh, retval)); pwquality_free_settings(options.pwq); @@ -262,7 +262,7 @@ continue; if (retval == PAM_AUTHTOK_ERR || newtoken == NULL) pam_syslog(pamh, LOG_INFO, "user aborted password change"); - else + else pam_syslog(pamh, LOG_ERR, "pam_get_authtok_verify returned error: %s", pam_strerror(pamh, retval)); pwquality_free_settings(options.pwq); diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pwqprivate.h b/src/pwqprivate.h --- a/src/pwqprivate.h 2026-06-24 13:39:51.504670235 +0200 +++ b/src/pwqprivate.h 2026-06-24 13:43:52.963449598 +0200 @@ -33,6 +33,7 @@ int local_users_only; char *bad_words; char *dict_path; + char *allow_classes; }; struct setting_mapping { @@ -47,6 +48,7 @@ #define PWQ_DEFAULT_UP_CREDIT 0 #define PWQ_DEFAULT_LOW_CREDIT 0 #define PWQ_DEFAULT_OTH_CREDIT 0 +#define PWQ_DEFAULT_ALLOWCLASSES "ludo" #ifdef HAVE_CRACK_H #define PWQ_DEFAULT_DICT_CHECK 1 diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pwquality.conf b/src/pwquality.conf --- a/src/pwquality.conf 2026-06-24 13:39:51.504930249 +0200 +++ b/src/pwquality.conf 2026-06-24 13:50:46.129512097 +0200 @@ -58,6 +58,10 @@ # The check is enabled if the value is greater than 0 and usercheck is enabled. # usersubstr = 0 # +# The allowed classes of characters (digits, uppercase, lowercase, others) +# to be used in the password. +# allowclasses = "ludo" +# # Whether the check is enforced by the PAM module and possibly other # applications. # The new password is rejected if it fails the check and the value is not 0. diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pwquality.h b/src/pwquality.h --- a/src/pwquality.h 2026-06-24 13:39:51.504650547 +0200 +++ b/src/pwquality.h 2026-06-24 13:47:07.084358309 +0200 @@ -36,6 +36,7 @@ #define PWQ_SETTING_ENFORCE_ROOT 19 #define PWQ_SETTING_LOCAL_USERS 20 #define PWQ_SETTING_USER_SUBSTR 21 +#define PWQ_SETTING_ALLOW_CLASSES 22 #define PWQ_MAX_ENTROPY_BITS 256 #define PWQ_MIN_ENTROPY_BITS 56 @@ -72,6 +73,7 @@ #define PWQ_ERROR_MAX_CLASS_REPEAT -27 #define PWQ_ERROR_BAD_WORDS -28 #define PWQ_ERROR_MAX_SEQUENCE -29 +#define PWQ_ERROR_DISALLOWED_CLASS -30 typedef struct pwquality_settings pwquality_settings_t; @@ -135,7 +137,7 @@ * is not returned. * Not passing the *auxerror into pwquality_strerror() can lead to memory leaks. * The score depends on PWQ_SETTING_MIN_LENGTH. If it is set higher, - * the score for the same passwords will be lower. */ + * the score for the same passwords will be lower. */ int pwquality_check(pwquality_settings_t *pwq, const char *password, const char *oldpassword, const char *user, void **auxerror); diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/settings.c b/src/settings.c --- a/src/settings.c 2026-06-24 13:39:51.504810433 +0200 +++ b/src/settings.c 2026-06-24 13:44:29.312806994 +0200 @@ -26,9 +26,15 @@ { pwquality_settings_t *pwq; + char *allow_classes; + pwq = calloc(1, sizeof(*pwq)); - if (!pwq) + allow_classes = strdup(PWQ_DEFAULT_ALLOWCLASSES); + if (!pwq || !allow_classes) { + free(pwq); + free(allow_classes); return NULL; + } pwq->diff_ok = PWQ_DEFAULT_DIFF_OK; pwq->min_length = PWQ_DEFAULT_MIN_LENGTH; @@ -43,6 +49,7 @@ pwq->retry_times = PWQ_DEFAULT_RETRY_TIMES; pwq->enforce_for_root = PWQ_DEFAULT_ENFORCE_ROOT; pwq->local_users_only = PWQ_DEFAULT_LOCAL_USERS; + pwq->allow_classes = allow_classes; return pwq; } @@ -54,6 +61,7 @@ if (pwq) { free(pwq->dict_path); free(pwq->bad_words); + free(pwq->allow_classes); free(pwq); } } @@ -79,7 +87,8 @@ { "dictpath", PWQ_SETTING_DICT_PATH, PWQ_TYPE_STR}, { "retry", PWQ_SETTING_RETRY_TIMES, PWQ_TYPE_INT}, { "enforce_for_root", PWQ_SETTING_ENFORCE_ROOT, PWQ_TYPE_SET}, - { "local_users_only", PWQ_SETTING_LOCAL_USERS, PWQ_TYPE_SET} + { "local_users_only", PWQ_SETTING_LOCAL_USERS, PWQ_TYPE_SET}, + { "allowclasses", PWQ_SETTING_ALLOW_CLASSES, PWQ_TYPE_STR} }; /* set setting name with value */ @@ -399,6 +408,10 @@ free(pwq->dict_path); pwq->dict_path = dup; break; + case PWQ_SETTING_ALLOW_CLASSES: + free(pwq->allow_classes); + pwq->allow_classes = dup; + break; default: free(dup); return PWQ_ERROR_NON_STR_SETTING; @@ -490,6 +503,12 @@ *value = NULL; #endif break; + case PWQ_SETTING_ALLOW_CLASSES: + if (pwq->allow_classes) + *value = pwq->allow_classes; + else + *value = PWQ_DEFAULT_ALLOWCLASSES; + break; default: return PWQ_ERROR_NON_STR_SETTING; }