diff options
Diffstat (limited to 'contrib/pam_modules/pam_passwdqc/passwdqc_check.c')
-rw-r--r-- | contrib/pam_modules/pam_passwdqc/passwdqc_check.c | 565 |
1 files changed, 420 insertions, 145 deletions
diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_check.c b/contrib/pam_modules/pam_passwdqc/passwdqc_check.c index 01265ff495e5..bb405af8cc48 100644 --- a/contrib/pam_modules/pam_passwdqc/passwdqc_check.c +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_check.c @@ -1,37 +1,57 @@ /* - * Copyright (c) 2000-2002 by Solar Designer. See LICENSE. + * Copyright (c) 2000-2002,2010,2013,2016,2020 by Solar Designer. See LICENSE. */ +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS /* we use fopen(), sprintf(), strncat() */ +#endif + +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> -#include <pwd.h> -#include "passwdqc.h" +#include "passwdqc.h" /* also provides <pwd.h> or equivalent "struct passwd" */ +#include "passwdqc_filter.h" +#include "wordset_4k.h" + +#include "passwdqc_i18n.h" #define REASON_ERROR \ - "check failed" + _("check failed") #define REASON_SAME \ - "is the same as the old one" + _("is the same as the old one") #define REASON_SIMILAR \ - "is based on the old one" + _("is based on the old one") #define REASON_SHORT \ - "too short" + _("too short") #define REASON_LONG \ - "too long" + _("too long") #define REASON_SIMPLESHORT \ - "not enough different characters or classes for this length" + _("not enough different characters or classes for this length") #define REASON_SIMPLE \ - "not enough different characters or classes" + _("not enough different characters or classes") #define REASON_PERSONAL \ - "based on personal login information" + _("based on personal login information") #define REASON_WORD \ - "based on a dictionary word and not a passphrase" + _("based on a dictionary word and not a passphrase") + +#define REASON_SEQ \ + _("based on a common sequence of characters and not a passphrase") + +#define REASON_WORDLIST \ + _("based on a word list entry") + +#define REASON_DENYLIST \ + _("is in deny list") + +#define REASON_FILTER \ + _("appears to be in a database") #define FIXED_BITS 15 @@ -39,7 +59,7 @@ typedef unsigned long fixed; /* * Calculates the expected number of different characters for a random - * password of a given length. The result is rounded down. We use this + * password of a given length. The result is rounded down. We use this * with the _requested_ minimum length (so longer passwords don't have * to meet this strict requirement for their length). */ @@ -49,7 +69,8 @@ static int expected_different(int charset, int length) x = ((fixed)(charset - 1) << FIXED_BITS) / charset; y = x; - while (--length > 0) y = (y * x) >> FIXED_BITS; + while (--length > 0) + y = (y * x) >> FIXED_BITS; z = (fixed)charset * (((fixed)1 << FIXED_BITS) - y); return (int)(z >> FIXED_BITS); @@ -59,8 +80,16 @@ static int expected_different(int charset, int length) * A password is too simple if it is too short for its class, or doesn't * contain enough different characters for its class, or doesn't contain * enough words for a passphrase. + * + * The biases are added to the length, and they may be positive or negative. + * The passphrase length check uses passphrase_bias instead of bias so that + * zero may be passed for this parameter when the (other) bias is non-zero + * because of a dictionary word, which is perfectly normal for a passphrase. + * The biases do not affect the number of different characters, character + * classes, and word count. */ -static int is_simple(passwdqc_params_t *params, const char *newpass) +static int is_simple(const passwdqc_params_qc_t *params, const char *newpass, + int bias, int passphrase_bias) { int length, classes, words, chars; int digits, lowers, uppers, others, unknowns; @@ -72,66 +101,89 @@ static int is_simple(passwdqc_params_t *params, const char *newpass) while ((c = (unsigned char)newpass[length])) { length++; - if (!isascii(c)) unknowns++; else - if (isdigit(c)) digits++; else - if (islower(c)) lowers++; else - if (isupper(c)) uppers++; else + if (!isascii(c)) + unknowns++; + else if (isdigit(c)) + digits++; + else if (islower(c)) + lowers++; + else if (isupper(c)) + uppers++; + else others++; - if (isascii(c) && isalpha(c) && isascii(p) && !isalpha(p)) - words++; +/* A word starts when a letter follows a non-letter or when a non-ASCII + * character follows a space character. We treat all non-ASCII characters + * as non-spaces, which is not entirely correct (there's the non-breaking + * space character at 0xa0, 0x9a, or 0xff), but it should not hurt. */ + if (isascii(p)) { + if (isascii(c)) { + if (isalpha(c) && !isalpha(p)) + words++; + } else if (isspace(p)) + words++; + } p = c; +/* Count this character just once: when we're not going to see it anymore */ if (!strchr(&newpass[length], c)) chars++; } - if (!length) return 1; + if (!length) + return 1; /* Upper case characters and digits used in common ways don't increase the * strength of a password */ c = (unsigned char)newpass[0]; - if (uppers && isascii(c) && isupper(c)) uppers--; + if (uppers && isascii(c) && isupper(c)) + uppers--; c = (unsigned char)newpass[length - 1]; - if (digits && isascii(c) && isdigit(c)) digits--; + if (digits && isascii(c) && isdigit(c)) + digits--; -/* Count the number of different character classes we've seen. We assume - * that there're no non-ASCII characters for digits. */ +/* Count the number of different character classes we've seen. We assume + * that there are no non-ASCII characters for digits. */ classes = 0; - if (digits) classes++; - if (lowers) classes++; - if (uppers) classes++; - if (others) classes++; - if (unknowns && (!classes || (digits && classes == 1))) classes++; + if (digits) + classes++; + if (lowers) + classes++; + if (uppers) + classes++; + if (others) + classes++; + if (unknowns && classes <= 1 && (!classes || digits || words >= 2)) + classes++; for (; classes > 0; classes--) switch (classes) { case 1: - if (length >= params->min[0] && + if (length + bias >= params->min[0] && chars >= expected_different(10, params->min[0]) - 1) return 0; return 1; case 2: - if (length >= params->min[1] && + if (length + bias >= params->min[1] && chars >= expected_different(36, params->min[1]) - 1) return 0; if (!params->passphrase_words || words < params->passphrase_words) continue; - if (length >= params->min[2] && + if (length + passphrase_bias >= params->min[2] && chars >= expected_different(27, params->min[2]) - 1) return 0; continue; case 3: - if (length >= params->min[3] && + if (length + bias >= params->min[3] && chars >= expected_different(62, params->min[3]) - 1) return 0; continue; case 4: - if (length >= params->min[4] && + if (length + bias >= params->min[4] && chars >= expected_different(95, params->min[4]) - 1) return 0; continue; @@ -140,13 +192,13 @@ static int is_simple(passwdqc_params_t *params, const char *newpass) return 1; } -static char *unify(const char *src) +static char *unify(char *dst, const char *src) { const char *sptr; - char *dst, *dptr; + char *dptr; int c; - if (!(dst = malloc(strlen(src) + 1))) + if (!dst && !(dst = malloc(strlen(src) + 1))) return NULL; sptr = src; @@ -154,9 +206,28 @@ static char *unify(const char *src) do { c = (unsigned char)*sptr; if (isascii(c) && isupper(c)) - *dptr++ = tolower(c); - else - *dptr++ = *sptr; + c = tolower(c); + switch (c) { + case 'a': case '@': + c = '4'; break; + case 'e': + c = '3'; break; +/* Unfortunately, if we translate both 'i' and 'l' to '1', this would + * associate these two letters with each other - e.g., "mile" would + * match "MLLE", which is undesired. To solve this, we'd need to test + * different translations separately, which is not implemented yet. */ + case 'i': case '|': + c = '!'; break; + case 'l': + c = '1'; break; + case 'o': + c = '0'; break; + case 's': case '$': + c = '5'; break; + case 't': case '+': + c = '7'; break; + } + *dptr++ = c; } while (*sptr++); return dst; @@ -181,25 +252,27 @@ static char *reverse(const char *src) static void clean(char *dst) { - if (dst) { - memset(dst, 0, strlen(dst)); - free(dst); - } + if (!dst) + return; + _passwdqc_memzero(dst, strlen(dst)); + free(dst); } /* * Needle is based on haystack if both contain a long enough common * substring and needle would be too simple for a password with the - * substring removed. + * substring either removed with partial length credit for it added + * or partially discounted for the purpose of the length check. */ -static int is_based(passwdqc_params_t *params, - const char *haystack, const char *needle, const char *original) +static int is_based(const passwdqc_params_qc_t *params, + const char *haystack, const char *needle, const char *original, + int mode) { char *scratch; int length; int i, j; const char *p; - int match; + int worst_bias; if (!params->match_length) /* disabled */ return 0; @@ -207,31 +280,76 @@ static int is_based(passwdqc_params_t *params, if (params->match_length < 0) /* misconfigured */ return 1; - if (strstr(haystack, needle)) /* based on haystack entirely */ - return 1; - scratch = NULL; + worst_bias = 0; - length = strlen(needle); + length = (int)strlen(needle); for (i = 0; i <= length - params->match_length; i++) for (j = params->match_length; i + j <= length; j++) { - match = 0; + int bias = 0, j1 = j - 1; + const char q0 = needle[i], *q1 = &needle[i + 1]; for (p = haystack; *p; p++) - if (*p == needle[i] && !strncmp(p, &needle[i], j)) { - match = 1; - if (!scratch) { - if (!(scratch = malloc(length + 1))) + if (*p == q0 && !strncmp(p + 1, q1, j1)) { /* or memcmp() */ + if ((mode & 0xff) == 0) { /* remove & credit */ + if (!scratch) { + if (!(scratch = malloc(length + 1))) + return 1; + } + /* remove j chars */ + { + int pos = length - (i + j); + if (!(mode & 0x100)) /* not reversed */ + pos = i; + memcpy(scratch, original, pos); + memcpy(&scratch[pos], + &original[pos + j], + length + 1 - (pos + j)); + } + /* add credit for match_length - 1 chars */ + bias = params->match_length - 1; + if (is_simple(params, scratch, bias, bias)) { + clean(scratch); return 1; - } - memcpy(scratch, original, i); - memcpy(&scratch[i], &original[i + j], - length + 1 - (i + j)); - if (is_simple(params, scratch)) { - clean(scratch); - return 1; + } + } else { /* discount */ +/* Require a 1 character longer match for substrings containing leetspeak + * when matching against dictionary words */ + bias = -1; + if ((mode & 0xff) == 1) { /* words */ + int pos = i, end = i + j; + if (mode & 0x100) { /* reversed */ + pos = length - end; + end = length - i; + } + for (; pos < end; pos++) + if (!isalpha((int)(unsigned char) + original[pos])) { + if (j == params->match_length) + goto next_match_length; + bias = 0; + break; + } + } + + /* discount j - (match_length + bias) chars */ + bias += (int)params->match_length - j; + /* bias <= -1 */ + if (bias < worst_bias) { + if (is_simple(params, original, bias, + (mode & 0xff) == 1 ? 0 : bias)) + return 1; + worst_bias = bias; + } } } - if (!match) break; +/* Zero bias implies that there were no matches for this length. If so, + * there's no reason to try the next substring length (it would result in + * no matches as well). We break out of the substring length loop and + * proceed with all substring lengths for the next position in needle. */ + if (!bias) + break; +next_match_length: + ; } clean(scratch); @@ -239,123 +357,280 @@ static int is_based(passwdqc_params_t *params, return 0; } +#define READ_LINE_MAX 8192 +#define READ_LINE_SIZE (READ_LINE_MAX + 2) + +static char *read_line(FILE *f, char *buf) +{ + buf[READ_LINE_MAX] = '\n'; + + if (!fgets(buf, READ_LINE_SIZE, f)) + return NULL; + + if (buf[READ_LINE_MAX] != '\n') { + int c; + do { + c = getc(f); + } while (c != EOF && c != '\n'); + if (ferror(f)) + return NULL; + } + + char *p; + if ((p = strpbrk(buf, "\r\n"))) + *p = '\0'; + + return buf; +} + +/* + * Common sequences of characters. + * We don't need to list any of the entire strings in reverse order because the + * code checks the new password in both "unified" and "unified and reversed" + * form against these strings (unifying them first indeed). We also don't have + * to include common repeats of characters (e.g., "777", "!!!", "1000") because + * these are often taken care of by the requirement on the number of different + * characters. + */ +const char * const seq[] = { + "0123456789", + "`1234567890-=", + "~!@#$%^&*()_+", + "abcdefghijklmnopqrstuvwxyz", + "a1b2c3d4e5f6g7h8i9j0", + "1a2b3c4d5e6f7g8h9i0j", + "abc123", + "qwertyuiop[]\\asdfghjkl;'zxcvbnm,./", + "qwertyuiop{}|asdfghjkl:\"zxcvbnm<>?", + "qwertyuiopasdfghjklzxcvbnm", + "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik,9ol.0p;/-['=]\\", + "!qaz@wsx#edc$rfv%tgb^yhn&ujm*ik<(ol>)p:?_{\"+}|", + "qazwsxedcrfvtgbyhnujmikolp", + "1q2w3e4r5t6y7u8i9o0p-[=]", + "q1w2e3r4t5y6u7i8o9p0[-]=\\", + "1qaz1qaz", + "1qaz!qaz", /* can't unify '1' and '!' - see comment in unify() */ + "1qazzaq1", + "zaq!1qaz", + "zaq!2wsx" +}; + /* * This wordlist check is now the least important given the checks above * and the support for passphrases (which are based on dictionary words, - * and checked by other means). It is still useful to trap simple short + * and checked by other means). It is still useful to trap simple short * passwords (if short passwords are allowed) that are word-based, but * passed the other checks due to uncommon capitalization, digits, and - * special characters. We (mis)use the same set of words that are used - * to generate random passwords. This list is much smaller than those + * special characters. We (mis)use the same set of words that are used + * to generate random passwords. This list is much smaller than those * used for password crackers, and it doesn't contain common passwords - * that aren't short English words. Perhaps support for large wordlists - * should still be added, even though this is now of little importance. + * that aren't short English words. We also support optional external + * wordlist (for inexact matching) and deny list (for exact matching). */ -static int is_word_based(passwdqc_params_t *params, - const char *needle, const char *original) +static const char *is_word_based(const passwdqc_params_qc_t *params, + const char *unified, const char *reversed, const char *original) { - char word[7]; - char *unified; - int i; - - word[6] = '\0'; - for (i = 0; i < 0x1000; i++) { - memcpy(word, _passwdqc_wordset_4k[i], 6); - if ((int)strlen(word) < params->match_length) continue; - unified = unify(word); - if (is_based(params, unified, needle, original)) { - clean(unified); - return 1; + const char *reason = REASON_ERROR; + char word[WORDSET_4K_LENGTH_MAX + 1], *buf = NULL; + FILE *f = NULL; + unsigned int i; + + word[WORDSET_4K_LENGTH_MAX] = '\0'; + if (params->match_length) + for (i = 0; _passwdqc_wordset_4k[i][0]; i++) { + memcpy(word, _passwdqc_wordset_4k[i], WORDSET_4K_LENGTH_MAX); + int length = (int)strlen(word); + if (length < params->match_length) + continue; + if (!memcmp(word, _passwdqc_wordset_4k[i + 1], length)) + continue; + unify(word, word); + if (is_based(params, word, unified, original, 1) || + is_based(params, word, reversed, original, 0x101)) { + reason = REASON_WORD; + goto out; } - clean(unified); } - return 0; -} + if (params->match_length) + for (i = 0; i < sizeof(seq) / sizeof(seq[0]); i++) { + char *seq_i = unify(NULL, seq[i]); + if (!seq_i) + goto out; + if (is_based(params, seq_i, unified, original, 2) || + is_based(params, seq_i, reversed, original, 0x102)) { + clean(seq_i); + reason = REASON_SEQ; + goto out; + } + clean(seq_i); + } -const char *_passwdqc_check(passwdqc_params_t *params, - const char *newpass, const char *oldpass, struct passwd *pw) -{ - char truncated[9], *reversed; - char *u_newpass, *u_reversed; - char *u_oldpass; - char *u_name, *u_gecos; - const char *reason; - int length; + if (params->match_length && params->match_length <= 4) + for (i = 1900; i <= 2039; i++) { + sprintf(word, "%u", i); + if (is_based(params, word, unified, original, 2) || + is_based(params, word, reversed, original, 0x102)) { + reason = REASON_SEQ; + goto out; + } + } - reversed = NULL; - u_newpass = u_reversed = NULL; - u_oldpass = NULL; - u_name = u_gecos = NULL; + if (params->wordlist || params->denylist) + if (!(buf = malloc(READ_LINE_SIZE))) + goto out; + + if (params->wordlist) { + if (!(f = fopen(params->wordlist, "r"))) + goto out; + while (read_line(f, buf)) { + unify(buf, buf); + if (!strcmp(buf, unified) || !strcmp(buf, reversed)) + goto out_wordlist; + if (!params->match_length || + strlen(buf) < (size_t)params->match_length) + continue; + if (is_based(params, buf, unified, original, 1) || + is_based(params, buf, reversed, original, 0x101)) { +out_wordlist: + reason = REASON_WORDLIST; + goto out; + } + } + if (ferror(f)) + goto out; + fclose(f); f = NULL; + } + + if (params->denylist) { + if (!(f = fopen(params->denylist, "r"))) + goto out; + while (read_line(f, buf)) { + if (!strcmp(buf, original)) { + reason = REASON_DENYLIST; + goto out; + } + } + if (ferror(f)) + goto out; + } reason = NULL; - if (oldpass && !strcmp(oldpass, newpass)) - reason = REASON_SAME; +out: + if (f) + fclose(f); + if (buf) { + _passwdqc_memzero(buf, READ_LINE_SIZE); + free(buf); + } + _passwdqc_memzero(word, sizeof(word)); + return reason; +} - length = strlen(newpass); +const char *passwdqc_check(const passwdqc_params_qc_t *params, + const char *newpass, const char *oldpass, const struct passwd *pw) +{ + char truncated[9]; + char *u_newpass = NULL, *u_reversed = NULL; + char *u_oldpass = NULL; + char *u_name = NULL, *u_gecos = NULL, *u_dir = NULL; + const char *reason = REASON_ERROR; + + size_t length = strlen(newpass); - if (!reason && length < params->min[4]) + if (length < (size_t)params->min[4]) { reason = REASON_SHORT; + goto out; + } + + if (length > 10000) { + reason = REASON_LONG; + goto out; + } - if (!reason && length > params->max) { + if (length > (size_t)params->max) { if (params->max == 8) { truncated[0] = '\0'; strncat(truncated, newpass, 8); newpass = truncated; - if (oldpass && !strncmp(oldpass, newpass, 8)) + length = 8; + if (oldpass && !strncmp(oldpass, newpass, 8)) { reason = REASON_SAME; - } else + goto out; + } + } else { reason = REASON_LONG; + goto out; + } + } + + if (oldpass && !strcmp(oldpass, newpass)) { + reason = REASON_SAME; + goto out; } - if (!reason && is_simple(params, newpass)) { - if (length < params->min[1] && params->min[1] <= params->max) + if (is_simple(params, newpass, 0, 0)) { + reason = REASON_SIMPLE; + if (length < (size_t)params->min[1] && + params->min[1] <= params->max) reason = REASON_SIMPLESHORT; - else - reason = REASON_SIMPLE; + goto out; } - if (!reason) { - if ((reversed = reverse(newpass))) { - u_newpass = unify(newpass); - u_reversed = unify(reversed); - if (oldpass) - u_oldpass = unify(oldpass); - if (pw) { - u_name = unify(pw->pw_name); - u_gecos = unify(pw->pw_gecos); - } - } - if (!reversed || - !u_newpass || !u_reversed || - (oldpass && !u_oldpass) || - (pw && (!u_name || !u_gecos))) - reason = REASON_ERROR; + if (!(u_newpass = unify(NULL, newpass))) + goto out; /* REASON_ERROR */ + if (!(u_reversed = reverse(u_newpass))) + goto out; + if (oldpass && !(u_oldpass = unify(NULL, oldpass))) + goto out; + if (pw) { + if (!(u_name = unify(NULL, pw->pw_name)) || + !(u_gecos = unify(NULL, pw->pw_gecos)) || + !(u_dir = unify(NULL, pw->pw_dir))) + goto out; } - if (!reason && oldpass && params->similar_deny && - (is_based(params, u_oldpass, u_newpass, newpass) || - is_based(params, u_oldpass, u_reversed, reversed))) + if (oldpass && params->similar_deny && + (is_based(params, u_oldpass, u_newpass, newpass, 0) || + is_based(params, u_oldpass, u_reversed, newpass, 0x100))) { reason = REASON_SIMILAR; + goto out; + } - if (!reason && pw && - (is_based(params, u_name, u_newpass, newpass) || - is_based(params, u_name, u_reversed, reversed) || - is_based(params, u_gecos, u_newpass, newpass) || - is_based(params, u_gecos, u_reversed, reversed))) + if (pw && + (is_based(params, u_name, u_newpass, newpass, 0) || + is_based(params, u_name, u_reversed, newpass, 0x100) || + is_based(params, u_gecos, u_newpass, newpass, 0) || + is_based(params, u_gecos, u_reversed, newpass, 0x100) || + is_based(params, u_dir, u_newpass, newpass, 0) || + is_based(params, u_dir, u_reversed, newpass, 0x100))) { reason = REASON_PERSONAL; + goto out; + } - if (!reason && (int)strlen(newpass) < params->min[2] && - (is_word_based(params, u_newpass, newpass) || - is_word_based(params, u_reversed, reversed))) - reason = REASON_WORD; + reason = is_word_based(params, u_newpass, u_reversed, newpass); + + if (!reason && params->filter) { + passwdqc_filter_t flt; + reason = REASON_ERROR; + if (passwdqc_filter_open(&flt, params->filter)) + goto out; + int result = passwdqc_filter_lookup(&flt, newpass); + passwdqc_filter_close(&flt); + if (result < 0) + goto out; + reason = result ? REASON_FILTER : NULL; + } - memset(truncated, 0, sizeof(truncated)); - clean(reversed); - clean(u_newpass); clean(u_reversed); +out: + _passwdqc_memzero(truncated, sizeof(truncated)); + clean(u_newpass); + clean(u_reversed); clean(u_oldpass); - clean(u_name); clean(u_gecos); + clean(u_name); + clean(u_gecos); + clean(u_dir); return reason; } |