xref: /freebsd/contrib/pam_modules/pam_passwdqc/passwdqc_check.c (revision aa24f48b361effe51163877d84f1b70d32b77e04)
1 /*
2  * Copyright (c) 2000-2002 by Solar Designer. See LICENSE.
3  */
4 
5 #include <stdlib.h>
6 #include <string.h>
7 #include <ctype.h>
8 #include <pwd.h>
9 
10 #include "passwdqc.h"
11 
12 #define REASON_ERROR \
13 	"check failed"
14 
15 #define REASON_SAME \
16 	"is the same as the old one"
17 #define REASON_SIMILAR \
18 	"is based on the old one"
19 
20 #define REASON_SHORT \
21 	"too short"
22 #define REASON_LONG \
23 	"too long"
24 
25 #define REASON_SIMPLESHORT \
26 	"not enough different characters or classes for this length"
27 #define REASON_SIMPLE \
28 	"not enough different characters or classes"
29 
30 #define REASON_PERSONAL \
31 	"based on personal login information"
32 
33 #define REASON_WORD \
34 	"based on a dictionary word and not a passphrase"
35 
36 #define FIXED_BITS			15
37 
38 typedef unsigned long fixed;
39 
40 /*
41  * Calculates the expected number of different characters for a random
42  * password of a given length. The result is rounded down. We use this
43  * with the _requested_ minimum length (so longer passwords don't have
44  * to meet this strict requirement for their length).
45  */
46 static int expected_different(int charset, int length)
47 {
48 	fixed x, y, z;
49 
50 	x = ((fixed)(charset - 1) << FIXED_BITS) / charset;
51 	y = x;
52 	while (--length > 0) y = (y * x) >> FIXED_BITS;
53 	z = (fixed)charset * (((fixed)1 << FIXED_BITS) - y);
54 
55 	return (int)(z >> FIXED_BITS);
56 }
57 
58 /*
59  * A password is too simple if it is too short for its class, or doesn't
60  * contain enough different characters for its class, or doesn't contain
61  * enough words for a passphrase.
62  */
63 static int is_simple(passwdqc_params_t *params, const char *newpass)
64 {
65 	int length, classes, words, chars;
66 	int digits, lowers, uppers, others, unknowns;
67 	int c, p;
68 
69 	length = classes = words = chars = 0;
70 	digits = lowers = uppers = others = unknowns = 0;
71 	p = ' ';
72 	while ((c = (unsigned char)newpass[length])) {
73 		length++;
74 
75 		if (!isascii(c)) unknowns++; else
76 		if (isdigit(c)) digits++; else
77 		if (islower(c)) lowers++; else
78 		if (isupper(c)) uppers++; else
79 			others++;
80 
81 		if (isascii(c) && isalpha(c) && isascii(p) && !isalpha(p))
82 			words++;
83 		p = c;
84 
85 		if (!strchr(&newpass[length], c))
86 			chars++;
87 	}
88 
89 	if (!length) return 1;
90 
91 /* Upper case characters and digits used in common ways don't increase the
92  * strength of a password */
93 	c = (unsigned char)newpass[0];
94 	if (uppers && isascii(c) && isupper(c)) uppers--;
95 	c = (unsigned char)newpass[length - 1];
96 	if (digits && isascii(c) && isdigit(c)) digits--;
97 
98 /* Count the number of different character classes we've seen. We assume
99  * that there're no non-ASCII characters for digits. */
100 	classes = 0;
101 	if (digits) classes++;
102 	if (lowers) classes++;
103 	if (uppers) classes++;
104 	if (others) classes++;
105 	if (unknowns && (!classes || (digits && classes == 1))) classes++;
106 
107 	for (; classes > 0; classes--)
108 	switch (classes) {
109 	case 1:
110 		if (length >= params->min[0] &&
111 		    chars >= expected_different(10, params->min[0]) - 1)
112 			return 0;
113 		return 1;
114 
115 	case 2:
116 		if (length >= params->min[1] &&
117 		    chars >= expected_different(36, params->min[1]) - 1)
118 			return 0;
119 		if (!params->passphrase_words ||
120 		    words < params->passphrase_words)
121 			continue;
122 		if (length >= params->min[2] &&
123 		    chars >= expected_different(27, params->min[2]) - 1)
124 			return 0;
125 		continue;
126 
127 	case 3:
128 		if (length >= params->min[3] &&
129 		    chars >= expected_different(62, params->min[3]) - 1)
130 			return 0;
131 		continue;
132 
133 	case 4:
134 		if (length >= params->min[4] &&
135 		    chars >= expected_different(95, params->min[4]) - 1)
136 			return 0;
137 		continue;
138 	}
139 
140 	return 1;
141 }
142 
143 static char *unify(const char *src)
144 {
145 	const char *sptr;
146 	char *dst, *dptr;
147 	int c;
148 
149 	if (!(dst = malloc(strlen(src) + 1)))
150 		return NULL;
151 
152 	sptr = src;
153 	dptr = dst;
154 	do {
155 		c = (unsigned char)*sptr;
156 		if (isascii(c) && isupper(c))
157 			*dptr++ = tolower(c);
158 		else
159 			*dptr++ = *sptr;
160 	} while (*sptr++);
161 
162 	return dst;
163 }
164 
165 static char *reverse(const char *src)
166 {
167 	const char *sptr;
168 	char *dst, *dptr;
169 
170 	if (!(dst = malloc(strlen(src) + 1)))
171 		return NULL;
172 
173 	sptr = &src[strlen(src)];
174 	dptr = dst;
175 	while (sptr > src)
176 		*dptr++ = *--sptr;
177 	*dptr = '\0';
178 
179 	return dst;
180 }
181 
182 static void clean(char *dst)
183 {
184 	if (dst) {
185 		memset(dst, 0, strlen(dst));
186 		free(dst);
187 	}
188 }
189 
190 /*
191  * Needle is based on haystack if both contain a long enough common
192  * substring and needle would be too simple for a password with the
193  * substring removed.
194  */
195 static int is_based(passwdqc_params_t *params,
196     const char *haystack, const char *needle, const char *original)
197 {
198 	char *scratch;
199 	int length;
200 	int i, j;
201 	const char *p;
202 	int match;
203 
204 	if (!params->match_length)	/* disabled */
205 		return 0;
206 
207 	if (params->match_length < 0)	/* misconfigured */
208 		return 1;
209 
210 	if (strstr(haystack, needle))	/* based on haystack entirely */
211 		return 1;
212 
213 	scratch = NULL;
214 
215 	length = strlen(needle);
216 	for (i = 0; i <= length - params->match_length; i++)
217 	for (j = params->match_length; i + j <= length; j++) {
218 		match = 0;
219 		for (p = haystack; *p; p++)
220 		if (*p == needle[i] && !strncmp(p, &needle[i], j)) {
221 			match = 1;
222 			if (!scratch) {
223 				if (!(scratch = malloc(length + 1)))
224 					return 1;
225 			}
226 			memcpy(scratch, original, i);
227 			memcpy(&scratch[i], &original[i + j],
228 			    length + 1 - (i + j));
229 			if (is_simple(params, scratch)) {
230 				clean(scratch);
231 				return 1;
232 			}
233 		}
234 		if (!match) break;
235 	}
236 
237 	clean(scratch);
238 
239 	return 0;
240 }
241 
242 /*
243  * This wordlist check is now the least important given the checks above
244  * and the support for passphrases (which are based on dictionary words,
245  * and checked by other means). It is still useful to trap simple short
246  * passwords (if short passwords are allowed) that are word-based, but
247  * passed the other checks due to uncommon capitalization, digits, and
248  * special characters. We (mis)use the same set of words that are used
249  * to generate random passwords. This list is much smaller than those
250  * used for password crackers, and it doesn't contain common passwords
251  * that aren't short English words. Perhaps support for large wordlists
252  * should still be added, even though this is now of little importance.
253  */
254 static int is_word_based(passwdqc_params_t *params,
255     const char *needle, const char *original)
256 {
257 	char word[7];
258 	char *unified;
259 	int i;
260 
261 	word[6] = '\0';
262 	for (i = 0; i < 0x1000; i++) {
263 		memcpy(word, _passwdqc_wordset_4k[i], 6);
264 		if ((int)strlen(word) < params->match_length) continue;
265 		unified = unify(word);
266 		if (is_based(params, unified, needle, original)) {
267 			clean(unified);
268 			return 1;
269 		}
270 		clean(unified);
271 	}
272 
273 	return 0;
274 }
275 
276 const char *_passwdqc_check(passwdqc_params_t *params,
277     const char *newpass, const char *oldpass, struct passwd *pw)
278 {
279 	char truncated[9], *reversed;
280 	char *u_newpass, *u_reversed;
281 	char *u_oldpass;
282 	char *u_name, *u_gecos;
283 	const char *reason;
284 	int length;
285 
286 	reversed = NULL;
287 	u_newpass = u_reversed = NULL;
288 	u_oldpass = NULL;
289 	u_name = u_gecos = NULL;
290 
291 	reason = NULL;
292 
293 	if (oldpass && !strcmp(oldpass, newpass))
294 		reason = REASON_SAME;
295 
296 	length = strlen(newpass);
297 
298 	if (!reason && length < params->min[4])
299 		reason = REASON_SHORT;
300 
301 	if (!reason && length > params->max) {
302 		if (params->max == 8) {
303 			truncated[0] = '\0';
304 			strncat(truncated, newpass, 8);
305 			newpass = truncated;
306 			if (oldpass && !strncmp(oldpass, newpass, 8))
307 				reason = REASON_SAME;
308 		} else
309 			reason = REASON_LONG;
310 	}
311 
312 	if (!reason && is_simple(params, newpass)) {
313 		if (length < params->min[1] && params->min[1] <= params->max)
314 			reason = REASON_SIMPLESHORT;
315 		else
316 			reason = REASON_SIMPLE;
317 	}
318 
319 	if (!reason) {
320 		if ((reversed = reverse(newpass))) {
321 			u_newpass = unify(newpass);
322 			u_reversed = unify(reversed);
323 			if (oldpass)
324 				u_oldpass = unify(oldpass);
325 			if (pw) {
326 				u_name = unify(pw->pw_name);
327 				u_gecos = unify(pw->pw_gecos);
328 			}
329 		}
330 		if (!reversed ||
331 		    !u_newpass || !u_reversed ||
332 		    (oldpass && !u_oldpass) ||
333 		    (pw && (!u_name || !u_gecos)))
334 			reason = REASON_ERROR;
335 	}
336 
337 	if (!reason && oldpass && params->similar_deny &&
338 	    (is_based(params, u_oldpass, u_newpass, newpass) ||
339 	    is_based(params, u_oldpass, u_reversed, reversed)))
340 		reason = REASON_SIMILAR;
341 
342 	if (!reason && pw &&
343 	    (is_based(params, u_name, u_newpass, newpass) ||
344 	    is_based(params, u_name, u_reversed, reversed) ||
345 	    is_based(params, u_gecos, u_newpass, newpass) ||
346 	    is_based(params, u_gecos, u_reversed, reversed)))
347 		reason = REASON_PERSONAL;
348 
349 	if (!reason && (int)strlen(newpass) < params->min[2] &&
350 	    (is_word_based(params, u_newpass, newpass) ||
351 	    is_word_based(params, u_reversed, reversed)))
352 		reason = REASON_WORD;
353 
354 	memset(truncated, 0, sizeof(truncated));
355 	clean(reversed);
356 	clean(u_newpass); clean(u_reversed);
357 	clean(u_oldpass);
358 	clean(u_name); clean(u_gecos);
359 
360 	return reason;
361 }
362