2 * The public APIs of the password strength checking kadmind plugin.
4 * Provides the public strength_init, strength_check, and strength_close APIs
5 * for the password strength plugin.
7 * Developed by Derrick Brashear and Ken Hornstein of Sine Nomine Associates,
8 * on behalf of Stanford University.
9 * Extensive modifications by Russ Allbery <rra@stanford.edu>
10 * Copyright 2006, 2007, 2009, 2012, 2013
11 * The Board of Trustees of the Leland Stanford Junior Unversity
13 * See LICENSE for licensing terms.
17 #include <portable/kadmin.h>
18 #include <portable/krb5.h>
19 #include <portable/system.h>
29 #include <plugin/internal.h>
30 #include <util/macros.h>
32 /* Heimdal doesn't define KADM5_PASS_Q_GENERIC. */
33 #ifndef KADM5_PASS_Q_GENERIC
34 # define KADM5_PASS_Q_GENERIC KADM5_PASS_Q_DICT
37 /* If not built with CDB support, provide some stubs. */
39 # define strength_check_cdb(c, d, p) 0
40 # define strength_close_cdb(c, d) /* empty */
43 /* The public function exported by the cracklib library. */
44 extern char *FascistCheck(const char *password, const char *dict);
48 * Load a boolean option from Kerberos appdefaults. Takes the Kerberos
49 * context, the section name, the option, and the result location.
51 * The stupidity of rewriting the realm argument into a krb5_data is required
55 default_boolean(krb5_context ctx, const char *section, const char *opt,
61 #ifdef HAVE_KRB5_REALM
62 krb5_const_realm rdata = realm;
64 krb5_data realm_struct;
65 const krb5_data *rdata;
68 /* Get the default realm. This is annoying for MIT Kerberos. */
69 code = krb5_get_default_realm(ctx, &realm);
72 #ifdef HAVE_KRB5_REALM
78 rdata = &realm_struct;
79 realm_struct.magic = KV5M_DATA;
80 realm_struct.data = (void *) realm;
81 realm_struct.length = strlen(realm);
86 * The MIT version of krb5_appdefault_boolean takes an int * and the
87 * Heimdal version takes a krb5_boolean *, so hope that Heimdal always
88 * defines krb5_boolean to int or this will require more portability work.
90 krb5_appdefault_boolean(ctx, section, rdata, opt, *result, &tmp);
96 * Load a number option from Kerberos appdefaults. Takes the Kerberos
97 * context, the section name, the option, and the result location. The native
98 * interface doesn't support numbers, so we actually read a string and then
102 default_number(krb5_context ctx, const char *section, const char *opt,
109 krb5_error_code code;
110 #ifdef HAVE_KRB5_REALM
111 krb5_const_realm rdata = realm;
113 krb5_data realm_struct;
114 const krb5_data *rdata;
117 /* Get the default realm. This is annoying for MIT Kerberos. */
118 code = krb5_get_default_realm(ctx, &realm);
121 #ifdef HAVE_KRB5_REALM
127 rdata = &realm_struct;
128 realm_struct.magic = KV5M_DATA;
129 realm_struct.data = (void *) realm;
130 realm_struct.length = strlen(realm);
134 /* Obtain the string from [appdefaults]. */
135 krb5_appdefault_string(ctx, section, rdata, opt, "", &tmp);
138 * If we found anything, convert it to a number. Currently, we ignore
141 if (tmp != NULL && tmp[0] != '\0') {
143 value = strtol(tmp, &end, 10);
144 if (errno == 0 && *end == '\0')
148 krb5_free_string(ctx, tmp);
153 * Load a string option from Kerberos appdefaults. Takes the Kerberos
154 * context, the section name, the realm, the option, and the result location.
156 * This requires an annoying workaround because one cannot specify a default
157 * value of NULL with MIT Kerberos, since MIT Kerberos unconditionally calls
158 * strdup on the default value. There's also no way to determine if memory
159 * allocation failed while parsing or while setting the default value, so we
160 * don't return an error code.
163 default_string(krb5_context ctx, const char *section, const char *opt,
168 krb5_error_code code;
169 #ifdef HAVE_KRB5_REALM
170 krb5_const_realm rdata;
172 krb5_data realm_struct;
173 const krb5_data *rdata;
176 /* Get the default realm. This is annoying for MIT Kerberos. */
177 code = krb5_get_default_realm(ctx, &realm);
180 #ifdef HAVE_KRB5_REALM
186 rdata = &realm_struct;
187 realm_struct.magic = KV5M_DATA;
188 realm_struct.data = (void *) realm;
189 realm_struct.length = strlen(realm);
193 /* Obtain the string from [appdefaults]. */
194 krb5_appdefault_string(ctx, section, rdata, opt, "", &value);
196 /* If we got something back, store it in result. */
198 if (value[0] == '\0')
203 *result = strdup(value);
204 krb5_free_string(ctx, value);
208 /* Free the realm if we got one. */
210 krb5_free_default_realm(ctx, realm);
215 * Initialize the CrackLib dictionary. Ensure that the dictionary file exists
216 * and is readable and store the path in the module context. Returns 0 on
217 * success, non-zero on failure.
219 * The dictionary file should not include the trailing .pwd extension.
220 * Currently, we don't cope with a NULL dictionary path.
222 static krb5_error_code
223 init_cracklib(krb5_context ctx, krb5_pwqual_moddata data)
226 krb5_error_code code;
228 /* Sanity-check the dictionary path. */
229 if (asprintf(&file, "%s.pwd", data->dictionary) < 0)
230 return strength_error_system(ctx, "cannot allocate memory");
231 if (access(file, R_OK) != 0) {
232 code = strength_error_system(ctx, "cannot read dictionary %s", file);
243 * Initialize the CDB dictionary. Opens the dictionary and sets up the
244 * TinyCDB state. Returns 0 on success, non-zero on failure (and sets the
245 * error in the Kerberos context). If not built with CDB support, always
248 static krb5_error_code
249 init_cdb(krb5_context ctx, krb5_pwqual_moddata data, const char *path)
251 krb5_error_code code;
253 data->cdb_fd = open(path, O_RDONLY);
254 if (data->cdb_fd < 0)
255 return strength_error_system(ctx, "cannot open dictionary %s", path);
256 if (cdb_init(&data->cdb, data->cdb_fd) < 0) {
257 code = strength_error_system(ctx, "cannot init dictionary %s", path);
262 data->have_cdb = true;
269 * Stub for init_cdb if not built with CDB support.
271 static krb5_error_code
272 init_cdb(krb5_context ctx, krb5_pwqual_moddata data UNUSED,
273 const char *database UNUSED)
275 krb5_set_error_message(ctx, KADM5_BAD_SERVER_PARAMS, "CDB dictionary"
276 " requested but not built with CDB support");
277 return KADM5_BAD_SERVER_PARAMS;
284 * Initialize the module. Ensure that the dictionary file exists and is
285 * readable and store the path in the module context. Returns 0 on success,
286 * non-zero on failure. This function returns failure only if it could not
287 * allocate memory or internal Kerberos calls that shouldn't fail do.
289 * The dictionary file should not include the trailing .pwd extension.
290 * Currently, we don't cope with a NULL dictionary path.
293 strength_init(krb5_context ctx, const char *dictionary,
294 krb5_pwqual_moddata *moddata)
296 krb5_pwqual_moddata data = NULL;
297 char *cdb_path = NULL;
298 krb5_error_code code;
300 /* Allocate our internal data. */
301 data = calloc(1, sizeof(*data));
303 return strength_error_system(ctx, "cannot allocate memory");
306 /* Get minimum length information from krb5.conf. */
307 default_number(ctx, "krb5-strength", "minimum_length", &data->min_length);
309 /* Get character class restrictions from krb5.conf. */
310 default_boolean(ctx, "krb5-strength", "require_ascii_printable",
312 default_boolean(ctx, "krb5-strength", "require_non_letter",
315 /* Use dictionary if given, otherwise get from krb5.conf. */
316 if (dictionary == NULL)
317 default_string(ctx, "krb5-strength", "password_dictionary",
320 data->dictionary = strdup(dictionary);
321 if (data->dictionary == NULL) {
322 code = strength_error_system(ctx, "cannot allocate memory");
327 /* Get CDB dictionary path from krb5.conf. */
328 default_string(ctx, "krb5-strength", "password_dictionary_cdb", &cdb_path);
330 /* If there is no dictionary, abort our setup with an error. */
331 if (data->dictionary == NULL && cdb_path == NULL) {
332 code = KADM5_MISSING_CONF_PARAMS;
333 krb5_set_error_message(ctx, code, "password_dictionary not configured"
338 /* If there is a CrackLib dictionary, initialize CrackLib. */
339 if (data->dictionary != NULL) {
340 code = init_cracklib(ctx, data);
345 /* If there is a CDB dictionary, initialize TinyCDB. */
346 if (cdb_path != NULL) {
347 code = init_cdb(ctx, data, cdb_path);
352 /* Initialized. Set moddata and return. */
358 strength_close(ctx, data);
366 * Check if a password contains only printable ASCII characters.
369 only_printable_ascii(const char *password)
373 for (p = password; *p != '\0'; p++)
374 if (!isascii((unsigned char) *p) || !isprint((unsigned char) *p))
381 * Check if a password contains only letters and spaces.
384 only_alpha_space(const char *password)
388 for (p = password; *p != '\0'; p++)
389 if (!isalpha((unsigned char) *p) && *p != ' ')
396 * Check a given password. Takes a Kerberos context, our module data, the
397 * password, the principal the password is for, and a buffer and buffer length
398 * into which to put any failure message.
401 strength_check(krb5_context ctx UNUSED, krb5_pwqual_moddata data,
402 const char *password, const char *principal)
409 krb5_error_code code;
411 /* Check minimum length first, since that's easy. */
412 if ((long) strlen(password) < data->min_length)
413 return strength_error_tooshort(ctx, ERROR_SHORT);
416 * If desired, check whether the password contains non-ASCII or
417 * non-printable ASCII characters.
419 if (data->ascii && !only_printable_ascii(password))
420 return strength_error_generic(ctx, ERROR_ASCII);
423 * If desired, ensure the password has a non-letter (and non-space)
424 * character. This requires that people using phrases at least include a
425 * digit or punctuation to make phrase dictionary attacks or dictionary
426 * attacks via combinations of words harder.
428 if (data->nonletter && only_alpha_space(password))
429 return strength_error_class(ctx, ERROR_LETTER);
432 * We get the principal (in krb5_unparse_name format) and we want to be
433 * sure that the password doesn't match the username, the username
434 * reversed, or the username with trailing digits. We therefore have to
435 * copy the string so that we can manipulate it a bit.
437 if (strcasecmp(password, principal) == 0)
438 return strength_error_generic(ctx, ERROR_USERNAME);
439 user = strdup(principal);
441 return strength_error_system(ctx, "cannot allocate memory");
443 /* Strip the realm off of the principal. */
444 for (p = user; p[0] != '\0'; p++) {
445 if (p[0] == '\\' && p[1] != '\0') {
456 * If the length of the password matches the length of the local portion
457 * of the principal, check for exact matches or reversed matches.
459 if (strlen(password) == strlen(user)) {
460 if (strcasecmp(password, user) == 0) {
462 return strength_error_generic(ctx, ERROR_USERNAME);
465 /* Check against the reversed username. */
466 for (i = 0, j = strlen(user) - 1; i < j; i++, j--) {
471 if (strcasecmp(password, user) == 0) {
473 return strength_error_generic(ctx, ERROR_USERNAME);
478 * If the length is greater, check whether the user just added trailing
479 * digits to the local portion of the principal to form the password.
481 if (strlen(password) > strlen(user))
482 if (strncasecmp(password, user, strlen(user)) == 0) {
483 q = password + strlen(user);
484 while (isdigit((unsigned char) *q))
488 return strength_error_generic(ctx, ERROR_USERNAME);
493 /* Check the password against CrackLib if it is configured. */
494 if (data->dictionary != NULL) {
495 result = FascistCheck(password, data->dictionary);
497 return strength_error_generic(ctx, "%s", result);
500 /* Check the password against CDB if it is configured. */
501 if (data->have_cdb) {
502 code = strength_check_cdb(ctx, data, password);
511 * Cleanly shut down the password strength plugin. The only thing we have to
512 * do is free the memory allocated for our internal data.
515 strength_close(krb5_context ctx UNUSED, krb5_pwqual_moddata data)
518 strength_close_cdb(ctx, data);
519 free(data->dictionary);