402783abd1
Submitted by: Solar Designer <solar@openwall.com>
362 lines
8.4 KiB
C
362 lines
8.4 KiB
C
/*
|
|
* Copyright (c) 2000-2002 by Solar Designer. See LICENSE.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <pwd.h>
|
|
|
|
#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, const 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(const char *src)
|
|
{
|
|
const char *sptr;
|
|
char *dst, *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(const char *src)
|
|
{
|
|
const char *sptr;
|
|
char *dst, *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,
|
|
const char *haystack, const char *needle, const char *original)
|
|
{
|
|
char *scratch;
|
|
int length;
|
|
int i, j;
|
|
const 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,
|
|
const char *needle, 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;
|
|
}
|
|
clean(unified);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
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;
|
|
|
|
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 && (int)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;
|
|
}
|