From 4532f219fa85b9c06b3f4caaa4dcbbdbf3bd32f5 Mon Sep 17 00:00:00 2001 From: des Date: Thu, 4 Apr 2002 15:50:47 +0000 Subject: Vendor import of Solar Designer's pam_passwdqc module. --- contrib/pam_modules/pam_passwdqc/passwdqc_check.c | 361 ++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 contrib/pam_modules/pam_passwdqc/passwdqc_check.c (limited to 'contrib/pam_modules/pam_passwdqc/passwdqc_check.c') diff --git a/contrib/pam_modules/pam_passwdqc/passwdqc_check.c b/contrib/pam_modules/pam_passwdqc/passwdqc_check.c new file mode 100644 index 0000000..08e2b49 --- /dev/null +++ b/contrib/pam_modules/pam_passwdqc/passwdqc_check.c @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2000,2001 by Solar Designer. See LICENSE. + */ + +#include +#include +#include +#include + +#include "passwdqc.h" + +#define REASON_ERROR \ + "check failed" + +#define REASON_SAME \ + "is the same as the old one" +#define REASON_SIMILAR \ + "is based on the old one" + +#define REASON_SHORT \ + "too short" +#define REASON_LONG \ + "too long" + +#define REASON_SIMPLESHORT \ + "not enough different characters or classes for this length" +#define REASON_SIMPLE \ + "not enough different characters or classes" + +#define REASON_PERSONAL \ + "based on personal login information" + +#define REASON_WORD \ + "based on a dictionary word and not a passphrase" + +#define FIXED_BITS 15 + +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 + * with the _requested_ minimum length (so longer passwords don't have + * to meet this strict requirement for their length). + */ +static int expected_different(int charset, int length) +{ + fixed x, y, z; + + x = ((fixed)(charset - 1) << FIXED_BITS) / charset; + y = x; + while (--length > 0) y = (y * x) >> FIXED_BITS; + z = (fixed)charset * (((fixed)1 << FIXED_BITS) - y); + + return (int)(z >> FIXED_BITS); +} + +/* + * 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. + */ +static int is_simple(passwdqc_params_t *params, char *newpass) +{ + int length, classes, words, chars; + int digits, lowers, uppers, others, unknowns; + int c, p; + + length = classes = words = chars = 0; + digits = lowers = uppers = others = unknowns = 0; + p = ' '; + 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 + others++; + + if (isascii(c) && isalpha(c) && isascii(p) && !isalpha(p)) + words++; + p = c; + + if (!strchr(&newpass[length], c)) + chars++; + } + + 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--; + c = (unsigned char)newpass[length - 1]; + 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. */ + classes = 0; + if (digits) classes++; + if (lowers) classes++; + if (uppers) classes++; + if (others) classes++; + if (unknowns && (!classes || (digits && classes == 1))) classes++; + + for (; classes > 0; classes--) + switch (classes) { + case 1: + if (length >= params->min[0] && + chars >= expected_different(10, params->min[0]) - 1) + return 0; + return 1; + + case 2: + if (length >= 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] && + chars >= expected_different(27, params->min[2]) - 1) + return 0; + continue; + + case 3: + if (length >= params->min[3] && + chars >= expected_different(62, params->min[3]) - 1) + return 0; + continue; + + case 4: + if (length >= params->min[4] && + chars >= expected_different(95, params->min[4]) - 1) + return 0; + continue; + } + + return 1; +} + +static char *unify(char *src) +{ + char *dst; + char *sptr, *dptr; + int c; + + if (!(dst = malloc(strlen(src) + 1))) + return NULL; + + sptr = src; + dptr = dst; + do { + c = (unsigned char)*sptr; + if (isascii(c) && isupper(c)) + *dptr++ = tolower(c); + else + *dptr++ = *sptr; + } while (*sptr++); + + return dst; +} + +static char *reverse(char *src) +{ + char *dst; + char *sptr, *dptr; + + if (!(dst = malloc(strlen(src) + 1))) + return NULL; + + sptr = &src[strlen(src)]; + dptr = dst; + while (sptr > src) + *dptr++ = *--sptr; + *dptr = '\0'; + + return dst; +} + +static void clean(char *dst) +{ + if (dst) { + memset(dst, 0, 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. + */ +static int is_based(passwdqc_params_t *params, + char *haystack, char *needle, char *original) +{ + char *scratch; + int length; + int i, j; + char *p; + int match; + + if (!params->match_length) /* disabled */ + return 0; + + if (params->match_length < 0) /* misconfigured */ + return 1; + + if (strstr(haystack, needle)) /* based on haystack entirely */ + return 1; + + scratch = NULL; + + length = strlen(needle); + for (i = 0; i <= length - params->match_length; i++) + for (j = params->match_length; i + j <= length; j++) { + match = 0; + for (p = haystack; *p; p++) + if (*p == needle[i] && !strncmp(p, &needle[i], j)) { + match = 1; + if (!scratch) { + if (!(scratch = malloc(length + 1))) + 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; + } + } + if (!match) break; + } + + clean(scratch); + + return 0; +} + +/* + * 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 + * 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 + * 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. + */ +static int is_word_based(passwdqc_params_t *params, + char *needle, char *original) +{ + char word[7]; + char *unified; + int index; + + word[6] = '\0'; + for (index = 0; index < 0x1000; index++) { + memcpy(word, _passwdqc_wordset_4k[index], 6); + if (strlen(word) < params->match_length) continue; + unified = unify(word); + if (is_based(params, unified, needle, original)) { + clean(unified); + return 1; + } + clean(unified); + } + + return 0; +} + +char *_passwdqc_check(passwdqc_params_t *params, + char *newpass, char *oldpass, struct passwd *pw) +{ + char truncated[9], *reversed; + char *u_newpass, *u_reversed; + char *u_oldpass; + char *u_name, *u_gecos; + char *reason; + int length; + + reversed = NULL; + u_newpass = u_reversed = NULL; + u_oldpass = NULL; + u_name = u_gecos = NULL; + + reason = NULL; + + if (oldpass && !strcmp(oldpass, newpass)) + reason = REASON_SAME; + + length = strlen(newpass); + + if (!reason && length < params->min[4]) + reason = REASON_SHORT; + + if (!reason && length > params->max) { + if (params->max == 8) { + truncated[0] = '\0'; + strncat(truncated, newpass, 8); + newpass = truncated; + if (oldpass && !strncmp(oldpass, newpass, 8)) + reason = REASON_SAME; + } else + reason = REASON_LONG; + } + + if (!reason && is_simple(params, newpass)) { + if (length < params->min[1] && params->min[1] <= params->max) + reason = REASON_SIMPLESHORT; + else + reason = REASON_SIMPLE; + } + + 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 (!reason && oldpass && params->similar_deny && + (is_based(params, u_oldpass, u_newpass, newpass) || + is_based(params, u_oldpass, u_reversed, reversed))) + reason = REASON_SIMILAR; + + 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))) + reason = REASON_PERSONAL; + + if (!reason && strlen(newpass) < params->min[2] && + (is_word_based(params, u_newpass, newpass) || + is_word_based(params, u_reversed, reversed))) + reason = REASON_WORD; + + memset(truncated, 0, sizeof(truncated)); + clean(reversed); + clean(u_newpass); clean(u_reversed); + clean(u_oldpass); + clean(u_name); clean(u_gecos); + + return reason; +} -- cgit v1.1