]> eyrie.org Git - kerberos/krb5-strength.git/blob - plugin/api.c
Refactor error handling inside the plugin
[kerberos/krb5-strength.git] / plugin / api.c
1 /*
2  * The public APIs of the password strength checking kadmind plugin.
3  *
4  * Provides the public strength_init, strength_check, and strength_close APIs
5  * for the password strength plugin.
6  *
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
12  *
13  * See LICENSE for licensing terms.
14  */
15
16 #include <config.h>
17 #include <portable/kadmin.h>
18 #include <portable/krb5.h>
19 #include <portable/system.h>
20
21 #ifdef HAVE_CDB_H
22 # include <cdb.h>
23 #endif
24 #include <ctype.h>
25 #include <errno.h>
26 #include <fcntl.h>
27 #include <sys/stat.h>
28
29 #include <plugin/internal.h>
30 #include <util/macros.h>
31
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
35 #endif
36
37 /* If not built with CDB support, provide some stubs. */
38 #ifndef HAVE_CDB
39 # define strength_check_cdb(c, d, p) 0
40 # define strength_close_cdb(c, d)    /* empty */
41 #endif
42
43 /* The public function exported by the cracklib library. */
44 extern char *FascistCheck(const char *password, const char *dict);
45
46
47 /*
48  * Load a boolean option from Kerberos appdefaults.  Takes the Kerberos
49  * context, the section name, the option, and the result location.
50  *
51  * The stupidity of rewriting the realm argument into a krb5_data is required
52  * by MIT Kerberos.
53  */
54 static void
55 default_boolean(krb5_context ctx, const char *section, const char *opt,
56                 bool *result)
57 {
58     int tmp;
59     char *realm = NULL;
60     krb5_error_code code;
61 #ifdef HAVE_KRB5_REALM
62     krb5_const_realm rdata = realm;
63 #else
64     krb5_data realm_struct;
65     const krb5_data *rdata;
66 #endif
67
68     /* Get the default realm.  This is annoying for MIT Kerberos. */
69     code = krb5_get_default_realm(ctx, &realm);
70     if (code != 0)
71         realm = NULL;
72 #ifdef HAVE_KRB5_REALM
73     rdata = realm;
74 #else
75     if (realm == NULL)
76         rdata = NULL;
77     else {
78         rdata = &realm_struct;
79         realm_struct.magic = KV5M_DATA;
80         realm_struct.data = (void *) realm;
81         realm_struct.length = strlen(realm);
82     }
83 #endif
84
85     /*
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.
89      */
90     krb5_appdefault_boolean(ctx, section, rdata, opt, *result, &tmp);
91     *result = tmp;
92 }
93
94
95 /*
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
99  * convert.
100  */
101 static void
102 default_number(krb5_context ctx, const char *section, const char *opt,
103                long *result)
104 {
105     char *tmp = NULL;
106     char *realm = NULL;
107     char *end;
108     long value;
109     krb5_error_code code;
110 #ifdef HAVE_KRB5_REALM
111     krb5_const_realm rdata = realm;
112 #else
113     krb5_data realm_struct;
114     const krb5_data *rdata;
115 #endif
116
117     /* Get the default realm.  This is annoying for MIT Kerberos. */
118     code = krb5_get_default_realm(ctx, &realm);
119     if (code != 0)
120         realm = NULL;
121 #ifdef HAVE_KRB5_REALM
122     rdata = realm;
123 #else
124     if (realm == NULL)
125         rdata = NULL;
126     else {
127         rdata = &realm_struct;
128         realm_struct.magic = KV5M_DATA;
129         realm_struct.data = (void *) realm;
130         realm_struct.length = strlen(realm);
131     }
132 #endif
133
134     /* Obtain the string from [appdefaults]. */
135     krb5_appdefault_string(ctx, section, rdata, opt, "", &tmp);
136
137     /*
138      * If we found anything, convert it to a number.  Currently, we ignore
139      * errors here.
140      */
141     if (tmp != NULL && tmp[0] != '\0') {
142         errno = 0;
143         value = strtol(tmp, &end, 10);
144         if (errno == 0 && *end == '\0')
145             *result = value;
146     }
147     if (tmp != NULL)
148         krb5_free_string(ctx, tmp);
149 }
150
151
152 /*
153  * Load a string option from Kerberos appdefaults.  Takes the Kerberos
154  * context, the section name, the realm, the option, and the result location.
155  *
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.
161  */
162 static void
163 default_string(krb5_context ctx, const char *section, const char *opt,
164                char **result)
165 {
166     char *value = NULL;
167     char *realm = NULL;
168     krb5_error_code code;
169 #ifdef HAVE_KRB5_REALM
170     krb5_const_realm rdata;
171 #else
172     krb5_data realm_struct;
173     const krb5_data *rdata;
174 #endif
175
176     /* Get the default realm.  This is annoying for MIT Kerberos. */
177     code = krb5_get_default_realm(ctx, &realm);
178     if (code != 0)
179         realm = NULL;
180 #ifdef HAVE_KRB5_REALM
181     rdata = realm;
182 #else
183     if (realm == NULL)
184         rdata = NULL;
185     else {
186         rdata = &realm_struct;
187         realm_struct.magic = KV5M_DATA;
188         realm_struct.data = (void *) realm;
189         realm_struct.length = strlen(realm);
190     }
191 #endif
192
193     /* Obtain the string from [appdefaults]. */
194     krb5_appdefault_string(ctx, section, rdata, opt, "", &value);
195
196     /* If we got something back, store it in result. */
197     if (value != NULL) {
198         if (value[0] == '\0')
199             free(value);
200         else {
201             if (*result != NULL)
202                 free(*result);
203             *result = strdup(value);
204             krb5_free_string(ctx, value);
205         }
206     }
207
208     /* Free the realm if we got one. */
209     if (realm != NULL)
210         krb5_free_default_realm(ctx, realm);
211 }
212
213
214 /*
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.
218  *
219  * The dictionary file should not include the trailing .pwd extension.
220  * Currently, we don't cope with a NULL dictionary path.
221  */
222 static krb5_error_code
223 init_cracklib(krb5_context ctx, krb5_pwqual_moddata data)
224 {
225     char *file;
226     krb5_error_code code;
227
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);
233         free(file);
234         return code;
235     }
236     free(file);
237     return 0;
238 }
239
240
241 #ifdef HAVE_CDB
242 /*
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
246  * returns an error.
247  */
248 static krb5_error_code
249 init_cdb(krb5_context ctx, krb5_pwqual_moddata data, const char *path)
250 {
251     krb5_error_code code;
252
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);
258         close(data->cdb_fd);
259         data->cdb_fd = -1;
260         return code;
261     }
262     data->have_cdb = true;
263     return 0;
264 }
265
266 #else
267
268 /*
269  * Stub for init_cdb if not built with CDB support.
270  */
271 static krb5_error_code
272 init_cdb(krb5_context ctx, krb5_pwqual_moddata data UNUSED,
273          const char *database UNUSED)
274 {
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;
278 }
279
280 #endif
281
282
283 /*
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.
288  *
289  * The dictionary file should not include the trailing .pwd extension.
290  * Currently, we don't cope with a NULL dictionary path.
291  */
292 krb5_error_code
293 strength_init(krb5_context ctx, const char *dictionary,
294               krb5_pwqual_moddata *moddata)
295 {
296     krb5_pwqual_moddata data = NULL;
297     char *cdb_path = NULL;
298     krb5_error_code code;
299
300     /* Allocate our internal data. */
301     data = calloc(1, sizeof(*data));
302     if (data == NULL)
303         return strength_error_system(ctx, "cannot allocate memory");
304     data->cdb_fd = -1;
305
306     /* Get minimum length information from krb5.conf. */
307     default_number(ctx, "krb5-strength", "minimum_length", &data->min_length);
308
309     /* Get character class restrictions from krb5.conf. */
310     default_boolean(ctx, "krb5-strength", "require_ascii_printable",
311                     &data->ascii);
312     default_boolean(ctx, "krb5-strength", "require_non_letter",
313                     &data->nonletter);
314
315     /* Use dictionary if given, otherwise get from krb5.conf. */
316     if (dictionary == NULL)
317         default_string(ctx, "krb5-strength", "password_dictionary",
318                        &data->dictionary);
319     else {
320         data->dictionary = strdup(dictionary);
321         if (data->dictionary == NULL) {
322             code = strength_error_system(ctx, "cannot allocate memory");
323             goto fail;
324         }
325     }
326
327     /* Get CDB dictionary path from krb5.conf. */
328     default_string(ctx, "krb5-strength", "password_dictionary_cdb", &cdb_path);
329
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"
334                                " in krb5.conf");
335         goto fail;
336     }
337
338     /* If there is a CrackLib dictionary, initialize CrackLib. */
339     if (data->dictionary != NULL) {
340         code = init_cracklib(ctx, data);
341         if (code != 0)
342             goto fail;
343     }
344
345     /* If there is a CDB dictionary, initialize TinyCDB. */
346     if (cdb_path != NULL) {
347         code = init_cdb(ctx, data, cdb_path);
348         if (code != 0)
349             goto fail;
350     }
351
352     /* Initialized.  Set moddata and return. */
353     *moddata = data;
354     return 0;
355
356 fail:
357     if (data != NULL)
358         strength_close(ctx, data);
359     free(cdb_path);
360     *moddata = NULL;
361     return code;
362 }
363
364
365 /*
366  * Check if a password contains only printable ASCII characters.
367  */
368 static bool
369 only_printable_ascii(const char *password)
370 {
371     const char *p;
372
373     for (p = password; *p != '\0'; p++)
374         if (!isascii((unsigned char) *p) || !isprint((unsigned char) *p))
375             return false;
376     return true;
377 }
378
379
380 /*
381  * Check if a password contains only letters and spaces.
382  */
383 static bool
384 only_alpha_space(const char *password)
385 {
386     const char *p;
387
388     for (p = password; *p != '\0'; p++)
389         if (!isalpha((unsigned char) *p) && *p != ' ')
390             return false;
391     return true;
392 }
393
394
395 /*
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.
399  */
400 krb5_error_code
401 strength_check(krb5_context ctx UNUSED, krb5_pwqual_moddata data,
402                const char *password, const char *principal)
403 {
404     char *user, *p;
405     const char *q;
406     size_t i, j;
407     char c;
408     const char *result;
409     krb5_error_code code;
410
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);
414
415     /*
416      * If desired, check whether the password contains non-ASCII or
417      * non-printable ASCII characters.
418      */
419     if (data->ascii && !only_printable_ascii(password))
420         return strength_error_generic(ctx, ERROR_ASCII);
421
422     /*
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.
427      */
428     if (data->nonletter && only_alpha_space(password))
429         return strength_error_class(ctx, ERROR_LETTER);
430
431     /*
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.
436      */
437     if (strcasecmp(password, principal) == 0)
438         return strength_error_generic(ctx, ERROR_USERNAME);
439     user = strdup(principal);
440     if (user == NULL)
441         return strength_error_system(ctx, "cannot allocate memory");
442
443     /* Strip the realm off of the principal. */
444     for (p = user; p[0] != '\0'; p++) {
445         if (p[0] == '\\' && p[1] != '\0') {
446             p++;
447             continue;
448         }
449         if (p[0] == '@') {
450             p[0] = '\0';
451             break;
452         }
453     }
454
455     /*
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.
458      */
459     if (strlen(password) == strlen(user)) {
460         if (strcasecmp(password, user) == 0) {
461             free(user);
462             return strength_error_generic(ctx, ERROR_USERNAME);
463         }
464
465         /* Check against the reversed username. */
466         for (i = 0, j = strlen(user) - 1; i < j; i++, j--) {
467             c = user[i];
468             user[i] = user[j];
469             user[j] = c;
470         }
471         if (strcasecmp(password, user) == 0) {
472             free(user);
473             return strength_error_generic(ctx, ERROR_USERNAME);
474         }
475     }
476
477     /*
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.
480      */
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))
485                 q++;
486             if (*q == '\0') {
487                 free(user);
488                 return strength_error_generic(ctx, ERROR_USERNAME);
489             }
490         }
491     free(user);
492
493     /* Check the password against CrackLib if it is configured. */
494     if (data->dictionary != NULL) {
495         result = FascistCheck(password, data->dictionary);
496         if (result != NULL)
497             return strength_error_generic(ctx, "%s", result);
498     }
499
500     /* Check the password against CDB if it is configured. */
501     if (data->have_cdb) {
502         code = strength_check_cdb(ctx, data, password);
503         if (code != 0)
504             return code;
505     }
506     return 0;
507 }
508
509
510 /*
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.
513  */
514 void
515 strength_close(krb5_context ctx UNUSED, krb5_pwqual_moddata data)
516 {
517     if (data != NULL) {
518         strength_close_cdb(ctx, data);
519         free(data->dictionary);
520         free(data);
521     }
522 }